From 454f553834e39d35d311714681d3cebe6922a5e3 Mon Sep 17 00:00:00 2001 From: Leah Neukirchen Date: Tue, 8 Aug 2023 21:48:53 +0200 Subject: initial commit of listening --- Makefile | 26 ++++++++ README | 62 ++++++++++++++++++ listening.1 | 88 +++++++++++++++++++++++++ listening.c | 208 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 384 insertions(+) create mode 100644 Makefile create mode 100644 README create mode 100644 listening.1 create mode 100644 listening.c 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 + +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 + * has waived all copyright and related or neighboring rights to this work. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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; +} -- cgit 1.4.1