about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Makefile26
-rw-r--r--README62
-rw-r--r--listening.188
-rw-r--r--listening.c208
4 files changed, 384 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..d37460b
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,26 @@
+ALL=listening
+
+CFLAGS=-g -O2 -Wall -Wno-switch -Wextra -Wwrite-strings
+
+DESTDIR=
+PREFIX=/usr/local
+BINDIR=$(PREFIX)/bin
+MANDIR=$(PREFIX)/share/man
+
+all: $(ALL)
+
+clean: FRC
+	rm -f $(ALL)
+
+check: FRC all
+	prove -v
+
+install: FRC all
+	mkdir -p $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 $(DESTDIR)$(ZSHCOMPDIR)
+	install -m0755 $(ALL) $(DESTDIR)$(BINDIR)
+	install -m0644 $(ALL:=.1) $(DESTDIR)$(MANDIR)/man1
+
+README: listening.1
+	mandoc -Tutf8 $< | col -bx >$@
+
+FRC:
diff --git a/README b/README
new file mode 100644
index 0000000..c13b2a9
--- /dev/null
+++ b/README
@@ -0,0 +1,62 @@
+LISTENING(1)                General Commands Manual               LISTENING(1)
+
+NAME
+     listening – check if a TCP server is listening
+
+SYNOPSIS
+     listening [-t connect-timeout] [-w wait-timeout] [host:]port
+
+DESCRIPTION
+     The listening utility performs a TCP scan against the given host
+     (defaulting to localhost) and port.
+
+     This can be used to detect if a slowly starting service is ready to
+     accept connections.
+
+     The options are as follows:
+
+     -t connect-timeout
+             Wait at most connect-timeout seconds per connection attempt
+             (default: 0.2s, decimal fractions are allowed).
+
+     -w wait-timeout
+             Wait at most wait-timeout seconds total (decimal fractions are
+             allowed), and keep trying to connecting when connection has been
+             refused.
+
+DETAILS
+     listening implements a TCP SYN scan (half-open scan), which has several
+     benefits:
+
+     •   As the target program does not accept(2) the connection, there's no
+         trace of testing.
+
+     •   It's possible to do in unprivileged Linux userspace, thanks to
+         TCP_QUICKACK and SO_LINGER (but also Linux specific).
+
+     Note that firewalls may block this kind of scan, so for reliable results
+     listening should be used on localhost only or within a DMZ.
+
+EXIT STATUS
+     listening returns one of the following status codes:
+
+     0       when the port is up
+     1       when the port refuses connection
+     2       when timeout was reached
+     99      if some other error occurred
+
+SEE ALSO
+     nc(1), nmap(1)
+
+AUTHORS
+     Leah Neukirchen <leah@vuxu.org>
+
+LICENSE
+     listening is in the public domain.
+
+     To the extent possible under law, the creator of this work has waived all
+     copyright and related or neighboring rights to this work.
+
+     http://creativecommons.org/publicdomain/zero/1.0/
+
+Void Linux                      August 8, 2023                      Void Linux
diff --git a/listening.1 b/listening.1
new file mode 100644
index 0000000..54c9f5f
--- /dev/null
+++ b/listening.1
@@ -0,0 +1,88 @@
+.Dd August 8, 2023
+.Dt LISTENING 1
+.Os
+.Sh NAME
+.Nm listening
+.Nd check if a TCP server is listening
+.Sh SYNOPSIS
+.Nm
+.Oo Fl t Ar connect-timeout Oc
+.Oo Fl w Ar wait-timeout Oc
+.Oo Ar host Ns \&: Oc Ns Ar port
+.Sh DESCRIPTION
+The
+.Nm
+utility performs a TCP scan against the given
+.Ar host
+.Po defaulting to localhost Pc
+and
+.Ar port .
+.Pp
+This can be used to detect if a slowly starting service is ready to
+accept connections.
+.Pp
+The options are as follows:
+.Bl -tag -width Ds
+.It Fl t Ar connect-timeout
+Wait at most
+.Ar connect-timeout
+seconds per connection attempt
+.Po default: 0.2s, decimal fractions are allowed Pc .
+.It Fl w Ar wait-timeout
+Wait at most
+.Ar wait-timeout
+seconds total
+.Po decimal fractions are allowed Pc ,
+and keep trying to connecting when connection has been refused.
+.El
+.Sh DETAILS
+.Nm
+implements a TCP SYN scan (half-open scan),
+which has several benefits:
+.Bl -bullet
+.It
+As the target program does not
+.Xr accept 2
+the connection, there's no trace of testing.
+.It
+It's possible to do in unprivileged Linux userspace,
+thanks to
+.Dv TCP_QUICKACK
+and
+.Dv SO_LINGER
+.Po but also Linux specific Pc .
+.El
+.Pp
+Note that firewalls may block this kind of scan,
+so for reliable results
+.Nm
+should be used on localhost only or within a DMZ.
+.Sh EXIT STATUS
+.Nm
+returns one of the following status codes:
+.Pp
+.Bl -tag -compact -width Ds
+.It 0
+when the port is up
+.It 1
+when the port refuses connection
+.It 2
+when timeout was reached
+.It 99
+if some other error occurred
+.El
+.Sh SEE ALSO
+.Xr nc 1 ,
+.Xr nmap 1
+.Sh AUTHORS
+.An Leah Neukirchen Aq Mt leah@vuxu.org
+.Sh LICENSE
+.Nm
+is in the public domain.
+.Pp
+To the extent possible under law,
+the creator of this work
+has waived all copyright and related or
+neighboring rights to this work.
+.Pp
+.Lk http://creativecommons.org/publicdomain/zero/1.0/
diff --git a/listening.c b/listening.c
new file mode 100644
index 0000000..bb4d241
--- /dev/null
+++ b/listening.c
@@ -0,0 +1,208 @@
+/*
+ * listening - check if a TCP server is listening
+ *
+ * To the extent possible under law, Leah Neukirchen <leah@vuxu.org>
+ * has waived all copyright and related or neighboring rights to this work.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+#include <sys/socket.h>
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <poll.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+uint64_t wait_timeout = 0;       // nanoseconds
+uint64_t connect_timeout = 200;  // milliseconds
+
+struct timespec now;
+struct timespec deadline;
+struct timespec delay;
+
+int
+scanfix(char *s, uint64_t *result, int scale)
+{
+	uint64_t r = 0;
+	char *d = 0;
+
+	if (!*s)
+		return -EINVAL;
+
+	while (*s) {
+		if ((unsigned)(*s)-'0' < 10) {
+			if (r <= UINT64_MAX/10 && 10*r <= UINT64_MAX-((*s)-'0'))
+				r = r*10 + ((*s)-'0');
+			else
+				return -ERANGE;
+		} else if (*s == '.') {
+			if (d)
+				return -EINVAL;
+			d = s + 1;
+		} else {
+			return -EINVAL;
+		}
+
+		s++;
+	}
+
+	if (!d)
+		d = s;
+
+	int o = scale - (int)(s-d);
+	for (; o > 0; o--) {
+		if (r > UINT64_MAX/10)
+			return -ERANGE;
+		r *= 10;
+	}
+	for (; o < 0; o++) {
+		if (r % 10 != 0)
+			return -ERANGE;
+		r /= 10;
+	}
+
+	*result = r;
+
+	return 0;
+}
+
+int
+syn_scan(const char *host, int port)
+{
+	printf("test %s:%d\n", host, port);
+
+	struct sockaddr_in addr;
+	memset(&addr, 0, sizeof (struct sockaddr_in));
+	addr.sin_family = AF_INET;
+	addr.sin_port = htons(port);
+	// XXX use GAI
+	inet_aton(host, &addr.sin_addr);
+
+	int sock = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
+	struct linger l = {1, 0};
+	setsockopt(sock, SOL_SOCKET, SO_LINGER, (void *)&l, sizeof (struct linger));
+	int zero = 0;
+	setsockopt(sock, IPPROTO_TCP, TCP_QUICKACK, (void *)&zero, sizeof zero);
+	int r;
+	errno = 0;
+	r = connect(sock, (struct sockaddr *)&addr, sizeof (struct sockaddr_in));
+	if (r > 0 || errno != EINPROGRESS) {
+		fprintf(stderr, "connect failed: %m\n");
+		close(sock);
+		return 99;
+	}
+
+	struct pollfd fds[1];
+	fds[0] = (struct pollfd){ sock, POLLIN | POLLOUT | POLLERR, 0 };
+	r = poll(fds, 1, connect_timeout);
+	if (r == 0) {
+		printf("timeout");
+		return 2;
+	} else if (r != 1) {
+		printf("??? poll = %d %m\n", r);
+		close(sock);
+		return 99;
+	}
+
+	if (fds[0].revents & POLLERR) {
+		int error = 0;
+		socklen_t errlen = sizeof error;
+		getsockopt(sock, SOL_SOCKET, SO_ERROR, (void *)&error, &errlen);
+		printf("SO_ERROR = %d %s\n", error, strerror(error));
+		close(sock);
+		return 1;
+	} else if (fds[0].revents & POLLOUT) {
+		printf("socket is up\n");
+		close(sock);
+		return 0;
+	}
+
+	return 99;		// unreachable
+}
+
+int
+main(int argc, char *argv[])
+{
+	int c, err;
+
+	while ((c = getopt(argc, argv, "+t:w:")) != -1) {
+		switch (c) {
+                case 't':
+			if ((err = scanfix(optarg, &connect_timeout, 3)) < 0) {
+				fprintf(stderr, "failed to parse number '%s': %s\n",
+				    optarg, strerror(-err));
+				goto usage;
+			}
+			break;
+                case 'w':
+			if ((err = scanfix(optarg, &wait_timeout, 9)) < 0) {
+				fprintf(stderr, "failed to parse number '%s': %s\n",
+				    optarg, strerror(-err));
+				goto usage;
+			}
+			break;
+                default:
+		usage:
+                        fprintf(stderr,
+                            "Usage: %s [-w WAIT_TIMEOUT] [-t CONNECT_TIMEOUT] [HOST:]PORT\n",
+                            argv[0]);
+                        exit(99);
+                }
+	}
+
+	if (optind != argc - 1)
+		goto usage;
+
+	int port;
+	const char *host = argv[argc-1];
+	char *colon = strchr(host, ':');
+	if (colon) {
+		*colon = 0;
+		port = atoi(colon+1);
+	} else {
+		port = atoi(host);
+		host = "127.0.0.1";
+	}
+
+	if (wait_timeout == 0)
+		return syn_scan(host, port);
+
+	/* else we are waiting for the port to come up: */
+
+	clock_gettime(CLOCK_MONOTONIC, &now);
+	deadline.tv_sec = now.tv_sec;
+	deadline.tv_nsec = now.tv_nsec + wait_timeout;
+	if (deadline.tv_nsec >= 1000000000L) {
+		deadline.tv_sec += deadline.tv_nsec / 1000000000L;
+		deadline.tv_nsec = deadline.tv_nsec % 1000000000L;
+	}
+	delay.tv_sec = 0;
+	delay.tv_nsec = connect_timeout * 100000L;
+	if (delay.tv_nsec >= 1000000000L) {
+		delay.tv_sec += delay.tv_nsec / 1000000000L;
+		delay.tv_nsec = delay.tv_nsec % 1000000000L;
+	}
+
+	while (now.tv_sec < deadline.tv_sec ||
+	    (now.tv_sec == deadline.tv_sec && now.tv_nsec <= deadline.tv_nsec)) {
+		switch (syn_scan(host, port)) {
+		case 0:
+			return 0;
+		case 99:
+			return 99;
+		case 1:
+			nanosleep(&delay, 0);   // avoid busy wait
+		}
+
+		clock_gettime(CLOCK_MONOTONIC, &now);
+	}
+
+	return 2;
+}