Repository: skeeto/endlessh Branch: master Commit: dfe44eb2c5b6 Files: 15 Total size: 40.7 KB Directory structure: gitextract_autjkeyh/ ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── UNLICENSE ├── endlessh.1 ├── endlessh.c └── util/ ├── endlessh.service ├── openbsd/ │ └── README.md ├── pivot.py ├── schema.sql └── smf/ ├── README ├── endlessh.conf ├── endlessh.xml └── init.endlessh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ endlessh ================================================ FILE: Dockerfile ================================================ FROM alpine:3.9 as builder RUN apk add --no-cache build-base ADD endlessh.c Makefile / RUN make FROM alpine:3.9 COPY --from=builder /endlessh / EXPOSE 2222/tcp ENTRYPOINT ["/endlessh"] CMD ["-v"] ================================================ FILE: Makefile ================================================ .POSIX: CC = cc CFLAGS = -std=c99 -Wall -Wextra -Wno-missing-field-initializers -Os CPPFLAGS = LDFLAGS = -ggdb3 LDLIBS = PREFIX = /usr/local all: endlessh endlessh: endlessh.c $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) -o $@ endlessh.c $(LDLIBS) install: endlessh install -d $(DESTDIR)$(PREFIX)/bin install -m 755 endlessh $(DESTDIR)$(PREFIX)/bin/ install -d $(DESTDIR)$(PREFIX)/share/man/man1 install -m 644 endlessh.1 $(DESTDIR)$(PREFIX)/share/man/man1/ clean: rm -rf endlessh ================================================ FILE: README.md ================================================ # Endlessh: an SSH tarpit Endlessh is an SSH tarpit [that *very* slowly sends an endless, random SSH banner][np]. It keeps SSH clients locked up for hours or even days at a time. The purpose is to put your real SSH server on another port and then let the script kiddies get stuck in this tarpit instead of bothering a real server. Since the tarpit is in the banner before any cryptographic exchange occurs, this program doesn't depend on any cryptographic libraries. It's a simple, single-threaded, standalone C program. It uses `poll()` to trap multiple clients at a time. ## Usage Usage information is printed with `-h`. ``` Usage: endlessh [-vhs] [-d MS] [-f CONFIG] [-l LEN] [-m LIMIT] [-p PORT] -4 Bind to IPv4 only -6 Bind to IPv6 only -d INT Message millisecond delay [10000] -f Set and load config file [/etc/endlessh/config] -h Print this help message and exit -l INT Maximum banner line length (3-255) [32] -m INT Maximum number of clients [4096] -p INT Listening port [2222] -s Print diagnostics to syslog instead of standard output -v Print diagnostics (repeatable) ``` Argument order matters. The configuration file is loaded when the `-f` argument is processed, so only the options that follow will override the configuration file. By default no log messages are produced. The first `-v` enables basic logging and a second `-v` enables debugging logging (noisy). All log messages are sent to standard output by default. `-s` causes them to be sent to syslog. endlessh -v >endlessh.log 2>endlessh.err A SIGTERM signal will gracefully shut down the daemon, allowing it to write a complete, consistent log. A SIGHUP signal requests a reload of the configuration file (`-f`). A SIGUSR1 signal will print connections stats to the log. ## Sample Configuration File The configuration file has similar syntax to OpenSSH. ``` # The port on which to listen for new SSH connections. Port 2222 # The endless banner is sent one line at a time. This is the delay # in milliseconds between individual lines. Delay 10000 # The length of each line is randomized. This controls the maximum # length of each line. Shorter lines may keep clients on for longer if # they give up after a certain number of bytes. MaxLineLength 32 # Maximum number of connections to accept at a time. Connections beyond # this are not immediately rejected, but will wait in the queue. MaxClients 4096 # Set the detail level for the log. # 0 = Quiet # 1 = Standard, useful log messages # 2 = Very noisy debugging information LogLevel 0 # Set the family of the listening socket # 0 = Use IPv4 Mapped IPv6 (Both v4 and v6, default) # 4 = Use IPv4 only # 6 = Use IPv6 only BindFamily 0 ``` ## Build issues Some more esoteric systems require extra configuration when building. ### RHEL 6 / CentOS 6 This system uses a version of glibc older than 2.17 (December 2012), and `clock_gettime(2)` is still in librt. For these systems you will need to link against librt: make LDLIBS=-lrt ### Solaris / illumos These systems don't include all the necessary functionality in libc and the linker requires some extra libraries: make CC=gcc LDLIBS='-lnsl -lrt -lsocket' If you're not using GCC or Clang, also override `CFLAGS` and `LDFLAGS` to remove GCC-specific options. For example, on Solaris: make CFLAGS=-fast LDFLAGS= LDLIBS='-lnsl -lrt -lsocket' The feature test macros on these systems isn't reliable, so you may also need to use `-D__EXTENSIONS__` in `CFLAGS`. ### OpenBSD The man page needs to go into a different path for OpenBSD's `man` command: ``` diff --git a/Makefile b/Makefile index 119347a..dedf69d 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,8 @@ endlessh: endlessh.c install: endlessh install -d $(DESTDIR)$(PREFIX)/bin install -m 755 endlessh $(DESTDIR)$(PREFIX)/bin/ - install -d $(DESTDIR)$(PREFIX)/share/man/man1 - install -m 644 endlessh.1 $(DESTDIR)$(PREFIX)/share/man/man1/ + install -d $(DESTDIR)$(PREFIX)/man/man1 + install -m 644 endlessh.1 $(DESTDIR)$(PREFIX)/man/man1/ clean: rm -rf endlessh ``` [np]: https://nullprogram.com/blog/2019/03/22/ ================================================ FILE: UNLICENSE ================================================ This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to ================================================ FILE: endlessh.1 ================================================ .Dd $Mdocdate: January 29 2020 $ .Dt ENDLESSH 1 .Os .Sh NAME .Nm endless .Nd An SSH tarpit .Sh SYNOPSIS .Nm endless .Op Fl 46chsvV .Op Fl d Ar delay .Op Fl f Ar config .Op Fl l Ar max banner length .Op Fl m Ar max clients .Op Fl p Ar port .Sh DESCRIPTION .Nm is an SSH tarpit that very slowly sends an endless, random SSH banner. .Pp .Nm keeps SSH clients locked up for hours or even days at a time. The purpose is to put your real SSH server on another port and then let the script kiddies get stuck in this tarpit instead of bothering a real server. .Pp Since the tarpit is in the banner before any cryptographic exchange occurs, this program doesn't depend on any cryptographic libraries. It's a simple, single-threaded, standalone C program. It uses poll() to trap multiple clients at a time. .Pp The options are as follows: .Bl -tag -width Ds .It Fl 4 Forces .Nm to use IPv4 addresses only. .It Fl 6 Forces .Nm to use IPv6 addresses only. .It Fl d Ar delay Message milliseconds delay. Default: 10000 .It Fl f Ar config Set and load config file. By default .Nm looks for /etc/endlessh/config. .It Fl h Print the help message and exit. .It Fl l Ar max banner length Maximum banner line length (3-255). Default: 32 .It Fl m Ar max clients Maximum number of clients. Default: 4096 .It Fl p Ar port Set the listening port. By default .Nm listens on port 2222. .It Fl s Print diagnostics to syslog. By default .Nm prints them to standard output. .It Fl v Print diagnostics. Can be specified up to twice to increase verbosity. .It Fl V Causes .Nm to print version information and exit. .El .Pp If .Nm receives the SIGTERM signal it will gracefully shut down the daemon, allowing it to write a complete, consistent log. .Pp A SIGHUP signal requests a reload of its configuration file. .Pp A SIGUSR1 signal will print connections stats to the log. .Sh FILES .Bl -tag -width /etc/endlessh/config -compact .It Pa /etc/endlessh/config The default .Nm configuration file. .El ================================================ FILE: endlessh.c ================================================ /* Endlessh: an SSH tarpit * * This is free and unencumbered software released into the public domain. */ #if defined(__OpenBSD__) # define _BSD_SOURCE /* for pledge(2) and unveil(2) */ #else # define _XOPEN_SOURCE 600 #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define ENDLESSH_VERSION 1.1 #define DEFAULT_PORT 2222 #define DEFAULT_DELAY 10000 /* milliseconds */ #define DEFAULT_MAX_LINE_LENGTH 32 #define DEFAULT_MAX_CLIENTS 4096 #if defined(__FreeBSD__) # define DEFAULT_CONFIG_FILE "/usr/local/etc/endlessh.config" #else # define DEFAULT_CONFIG_FILE "/etc/endlessh/config" #endif #define DEFAULT_BIND_FAMILY AF_UNSPEC #define XSTR(s) STR(s) #define STR(s) #s static long long epochms(void) { struct timespec tv; clock_gettime(CLOCK_REALTIME, &tv); return tv.tv_sec * 1000ULL + tv.tv_nsec / 1000000ULL; } static enum loglevel { log_none, log_info, log_debug } loglevel = log_none; static void (*logmsg)(enum loglevel level, const char *, ...); static void logstdio(enum loglevel level, const char *format, ...) { if (loglevel >= level) { int save = errno; /* Print a timestamp */ long long now = epochms(); time_t t = now / 1000; char date[64]; struct tm tm[1]; strftime(date, sizeof(date), "%Y-%m-%dT%H:%M:%S", gmtime_r(&t, tm)); printf("%s.%03lldZ ", date, now % 1000); /* Print the rest of the log message */ va_list ap; va_start(ap, format); vprintf(format, ap); va_end(ap); fputc('\n', stdout); errno = save; } } static void logsyslog(enum loglevel level, const char *format, ...) { static const int prio_map[] = { LOG_NOTICE, LOG_INFO, LOG_DEBUG }; if (loglevel >= level) { int save = errno; /* Output the log message */ va_list ap; va_start(ap, format); char buf[256]; vsnprintf(buf, sizeof buf, format, ap); va_end(ap); syslog(prio_map[level], "%s", buf); errno = save; } } static struct { long long connects; long long milliseconds; long long bytes_sent; } statistics; struct client { char ipaddr[INET6_ADDRSTRLEN]; long long connect_time; long long send_next; long long bytes_sent; struct client *next; int port; int fd; }; static struct client * client_new(int fd, long long send_next) { struct client *c = malloc(sizeof(*c)); if (c) { c->ipaddr[0] = 0; c->connect_time = epochms(); c->send_next = send_next; c->bytes_sent = 0; c->next = 0; c->fd = fd; c->port = 0; /* Set the smallest possible recieve buffer. This reduces local * resource usage and slows down the remote end. */ int value = 1; int r = setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &value, sizeof(value)); logmsg(log_debug, "setsockopt(%d, SO_RCVBUF, %d) = %d", fd, value, r); if (r == -1) logmsg(log_debug, "errno = %d, %s", errno, strerror(errno)); /* Get IP address */ struct sockaddr_storage addr; socklen_t len = sizeof(addr); if (getpeername(fd, (struct sockaddr *)&addr, &len) != -1) { if (addr.ss_family == AF_INET) { struct sockaddr_in *s = (struct sockaddr_in *)&addr; c->port = ntohs(s->sin_port); inet_ntop(AF_INET, &s->sin_addr, c->ipaddr, sizeof(c->ipaddr)); } else { struct sockaddr_in6 *s = (struct sockaddr_in6 *)&addr; c->port = ntohs(s->sin6_port); inet_ntop(AF_INET6, &s->sin6_addr, c->ipaddr, sizeof(c->ipaddr)); } } } return c; } static void client_destroy(struct client *client) { logmsg(log_debug, "close(%d)", client->fd); long long dt = epochms() - client->connect_time; logmsg(log_info, "CLOSE host=%s port=%d fd=%d " "time=%lld.%03lld bytes=%lld", client->ipaddr, client->port, client->fd, dt / 1000, dt % 1000, client->bytes_sent); statistics.milliseconds += dt; close(client->fd); free(client); } static void statistics_log_totals(struct client *clients) { long long milliseconds = statistics.milliseconds; for (long long now = epochms(); clients; clients = clients->next) milliseconds += now - clients->connect_time; logmsg(log_info, "TOTALS connects=%lld seconds=%lld.%03lld bytes=%lld", statistics.connects, milliseconds / 1000, milliseconds % 1000, statistics.bytes_sent); } struct fifo { struct client *head; struct client *tail; int length; }; static void fifo_init(struct fifo *q) { q->head = q->tail = 0; q->length = 0; } static struct client * fifo_pop(struct fifo *q) { struct client *removed = q->head; q->head = q->head->next; removed->next = 0; if (!--q->length) q->tail = 0; return removed; } static void fifo_append(struct fifo *q, struct client *c) { if (!q->tail) { q->head = q->tail = c; } else { q->tail->next = c; q->tail = c; } q->length++; } static void fifo_destroy(struct fifo *q) { struct client *c = q->head; while (c) { struct client *dead = c; c = c->next; client_destroy(dead); } q->head = q->tail = 0; q->length = 0; } static void die(void) { fprintf(stderr, "endlessh: fatal: %s\n", strerror(errno)); exit(EXIT_FAILURE); } static unsigned rand16(unsigned long s[1]) { s[0] = s[0] * 1103515245UL + 12345UL; return (s[0] >> 16) & 0xffff; } static int randline(char *line, int maxlen, unsigned long s[1]) { int len = 3 + rand16(s) % (maxlen - 2); for (int i = 0; i < len - 2; i++) line[i] = 32 + rand16(s) % 95; line[len - 2] = 13; line[len - 1] = 10; if (memcmp(line, "SSH-", 4) == 0) line[0] = 'X'; return len; } static volatile sig_atomic_t running = 1; static void sigterm_handler(int signal) { (void)signal; running = 0; } static volatile sig_atomic_t reload = 0; static void sighup_handler(int signal) { (void)signal; reload = 1; } static volatile sig_atomic_t dumpstats = 0; static void sigusr1_handler(int signal) { (void)signal; dumpstats = 1; } struct config { int port; int delay; int max_line_length; int max_clients; int bind_family; }; #define CONFIG_DEFAULT { \ .port = DEFAULT_PORT, \ .delay = DEFAULT_DELAY, \ .max_line_length = DEFAULT_MAX_LINE_LENGTH, \ .max_clients = DEFAULT_MAX_CLIENTS, \ .bind_family = DEFAULT_BIND_FAMILY, \ } static void config_set_port(struct config *c, const char *s, int hardfail) { errno = 0; char *end; long tmp = strtol(s, &end, 10); if (errno || *end || tmp < 1 || tmp > 65535) { fprintf(stderr, "endlessh: Invalid port: %s\n", s); if (hardfail) exit(EXIT_FAILURE); } else { c->port = tmp; } } static void config_set_delay(struct config *c, const char *s, int hardfail) { errno = 0; char *end; long tmp = strtol(s, &end, 10); if (errno || *end || tmp < 1 || tmp > INT_MAX) { fprintf(stderr, "endlessh: Invalid delay: %s\n", s); if (hardfail) exit(EXIT_FAILURE); } else { c->delay = tmp; } } static void config_set_max_clients(struct config *c, const char *s, int hardfail) { errno = 0; char *end; long tmp = strtol(s, &end, 10); if (errno || *end || tmp < 1 || tmp > INT_MAX) { fprintf(stderr, "endlessh: Invalid max clients: %s\n", s); if (hardfail) exit(EXIT_FAILURE); } else { c->max_clients = tmp; } } static void config_set_max_line_length(struct config *c, const char *s, int hardfail) { errno = 0; char *end; long tmp = strtol(s, &end, 10); if (errno || *end || tmp < 3 || tmp > 255) { fprintf(stderr, "endlessh: Invalid line length: %s\n", s); if (hardfail) exit(EXIT_FAILURE); } else { c->max_line_length = tmp; } } static void config_set_bind_family(struct config *c, const char *s, int hardfail) { switch (*s) { case '4': c->bind_family = AF_INET; break; case '6': c->bind_family = AF_INET6; break; case '0': c->bind_family = AF_UNSPEC; break; default: fprintf(stderr, "endlessh: Invalid address family: %s\n", s); if (hardfail) exit(EXIT_FAILURE); break; } } enum config_key { KEY_INVALID, KEY_PORT, KEY_DELAY, KEY_MAX_LINE_LENGTH, KEY_MAX_CLIENTS, KEY_LOG_LEVEL, KEY_BIND_FAMILY, }; static enum config_key config_key_parse(const char *tok) { static const char *const table[] = { [KEY_PORT] = "Port", [KEY_DELAY] = "Delay", [KEY_MAX_LINE_LENGTH] = "MaxLineLength", [KEY_MAX_CLIENTS] = "MaxClients", [KEY_LOG_LEVEL] = "LogLevel", [KEY_BIND_FAMILY] = "BindFamily" }; for (size_t i = 1; i < sizeof(table) / sizeof(*table); i++) if (!strcmp(tok, table[i])) return i; return KEY_INVALID; } static void config_load(struct config *c, const char *file, int hardfail) { long lineno = 0; FILE *f = fopen(file, "r"); if (f) { char line[256]; while (fgets(line, sizeof(line), f)) { lineno++; /* Remove comments */ char *comment = strchr(line, '#'); if (comment) *comment = 0; /* Parse tokes on line */ char *save = 0; char *tokens[3]; int ntokens = 0; for (; ntokens < 3; ntokens++) { char *tok = strtok_r(ntokens ? 0 : line, " \r\n", &save); if (!tok) break; tokens[ntokens] = tok; } switch (ntokens) { case 0: /* Empty line */ continue; case 1: fprintf(stderr, "%s:%ld: Missing value\n", file, lineno); if (hardfail) exit(EXIT_FAILURE); continue; case 2: /* Expected */ break; case 3: fprintf(stderr, "%s:%ld: Too many values\n", file, lineno); if (hardfail) exit(EXIT_FAILURE); continue; } enum config_key key = config_key_parse(tokens[0]); switch (key) { case KEY_INVALID: fprintf(stderr, "%s:%ld: Unknown option '%s'\n", file, lineno, tokens[0]); break; case KEY_PORT: config_set_port(c, tokens[1], hardfail); break; case KEY_DELAY: config_set_delay(c, tokens[1], hardfail); break; case KEY_MAX_LINE_LENGTH: config_set_max_line_length(c, tokens[1], hardfail); break; case KEY_MAX_CLIENTS: config_set_max_clients(c, tokens[1], hardfail); break; case KEY_BIND_FAMILY: config_set_bind_family(c, tokens[1], hardfail); break; case KEY_LOG_LEVEL: { errno = 0; char *end; long v = strtol(tokens[1], &end, 10); if (errno || *end || v < log_none || v > log_debug) { fprintf(stderr, "%s:%ld: Invalid log level '%s'\n", file, lineno, tokens[1]); if (hardfail) exit(EXIT_FAILURE); } else { loglevel = v; } } break; } } fclose(f); } } static void config_log(const struct config *c) { logmsg(log_info, "Port %d", c->port); logmsg(log_info, "Delay %d", c->delay); logmsg(log_info, "MaxLineLength %d", c->max_line_length); logmsg(log_info, "MaxClients %d", c->max_clients); logmsg(log_info, "BindFamily %s", c->bind_family == AF_INET6 ? "IPv6 Only" : c->bind_family == AF_INET ? "IPv4 Only" : "IPv4 Mapped IPv6"); } static void usage(FILE *f) { fprintf(f, "Usage: endlessh [-vh] [-46] [-d MS] [-f CONFIG] [-l LEN] " "[-m LIMIT] [-p PORT]\n"); fprintf(f, " -4 Bind to IPv4 only\n"); fprintf(f, " -6 Bind to IPv6 only\n"); fprintf(f, " -d INT Message millisecond delay [" XSTR(DEFAULT_DELAY) "]\n"); fprintf(f, " -f Set and load config file [" DEFAULT_CONFIG_FILE "]\n"); fprintf(f, " -h Print this help message and exit\n"); fprintf(f, " -l INT Maximum banner line length (3-255) [" XSTR(DEFAULT_MAX_LINE_LENGTH) "]\n"); fprintf(f, " -m INT Maximum number of clients [" XSTR(DEFAULT_MAX_CLIENTS) "]\n"); fprintf(f, " -p INT Listening port [" XSTR(DEFAULT_PORT) "]\n"); fprintf(f, " -v Print diagnostics to standard output " "(repeatable)\n"); fprintf(f, " -V Print version information and exit\n"); } static void print_version(void) { puts("Endlessh " XSTR(ENDLESSH_VERSION)); } static int server_create(int port, int family) { int r, s, value; s = socket(family == AF_UNSPEC ? AF_INET6 : family, SOCK_STREAM, 0); logmsg(log_debug, "socket() = %d", s); if (s == -1) die(); /* Socket options are best effort, allowed to fail */ value = 1; r = setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value)); logmsg(log_debug, "setsockopt(%d, SO_REUSEADDR, true) = %d", s, r); if (r == -1) logmsg(log_debug, "errno = %d, %s", errno, strerror(errno)); /* * With OpenBSD IPv6 sockets are always IPv6-only, so the socket option * is read-only (not modifiable). * http://man.openbsd.org/ip6#IPV6_V6ONLY */ #ifndef __OpenBSD__ if (family == AF_INET6 || family == AF_UNSPEC) { errno = 0; value = (family == AF_INET6); r = setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &value, sizeof(value)); logmsg(log_debug, "setsockopt(%d, IPV6_V6ONLY, true) = %d", s, r); if (r == -1) logmsg(log_debug, "errno = %d, %s", errno, strerror(errno)); } #endif if (family == AF_INET) { struct sockaddr_in addr4 = { .sin_family = AF_INET, .sin_port = htons(port), .sin_addr = {INADDR_ANY} }; r = bind(s, (void *)&addr4, sizeof(addr4)); } else { struct sockaddr_in6 addr6 = { .sin6_family = AF_INET6, .sin6_port = htons(port), .sin6_addr = in6addr_any }; r = bind(s, (void *)&addr6, sizeof(addr6)); } logmsg(log_debug, "bind(%d, port=%d) = %d", s, port, r); if (r == -1) die(); r = listen(s, INT_MAX); logmsg(log_debug, "listen(%d) = %d", s, r); if (r == -1) die(); return s; } /* Write a line to a client, returning client if it's still up. */ static struct client * sendline(struct client *client, int max_line_length, unsigned long *rng) { char line[256]; int len = randline(line, max_line_length, rng); for (;;) { ssize_t out = write(client->fd, line, len); logmsg(log_debug, "write(%d) = %d", client->fd, (int)out); if (out == -1) { if (errno == EINTR) { continue; /* try again */ } else if (errno == EAGAIN || errno == EWOULDBLOCK) { return client; /* don't care */ } else { client_destroy(client); return 0; } } else { client->bytes_sent += out; statistics.bytes_sent += out; return client; } } } int main(int argc, char **argv) { logmsg = logstdio; struct config config = CONFIG_DEFAULT; const char *config_file = DEFAULT_CONFIG_FILE; #if defined(__OpenBSD__) unveil(config_file, "r"); /* return ignored as the file may not exist */ if (pledge("inet stdio rpath unveil", 0) == -1) die(); #endif config_load(&config, config_file, 1); int option; while ((option = getopt(argc, argv, "46d:f:hl:m:p:svV")) != -1) { switch (option) { case '4': config_set_bind_family(&config, "4", 1); break; case '6': config_set_bind_family(&config, "6", 1); break; case 'd': config_set_delay(&config, optarg, 1); break; case 'f': config_file = optarg; #if defined(__OpenBSD__) unveil(config_file, "r"); if (unveil(0, 0) == -1) die(); #endif config_load(&config, optarg, 1); break; case 'h': usage(stdout); exit(EXIT_SUCCESS); break; case 'l': config_set_max_line_length(&config, optarg, 1); break; case 'm': config_set_max_clients(&config, optarg, 1); break; case 'p': config_set_port(&config, optarg, 1); break; case 's': logmsg = logsyslog; break; case 'v': if (loglevel < log_debug) loglevel++; break; case 'V': print_version(); exit(EXIT_SUCCESS); break; default: usage(stderr); exit(EXIT_FAILURE); } } if (argv[optind]) { fprintf(stderr, "endlessh: too many arguments\n"); exit(EXIT_FAILURE); } if (logmsg == logsyslog) { /* Prepare the syslog */ const char *prog = strrchr(argv[0], '/'); prog = prog ? prog + 1 : argv[0]; openlog(prog, LOG_PID, LOG_DAEMON); } else { /* Set output (log) to line buffered */ setvbuf(stdout, 0, _IOLBF, 0); } /* Log configuration */ config_log(&config); /* Install the signal handlers */ signal(SIGPIPE, SIG_IGN); { struct sigaction sa = {.sa_handler = sigterm_handler}; int r = sigaction(SIGTERM, &sa, 0); if (r == -1) die(); } { struct sigaction sa = {.sa_handler = sighup_handler}; int r = sigaction(SIGHUP, &sa, 0); if (r == -1) die(); } { struct sigaction sa = {.sa_handler = sigusr1_handler}; int r = sigaction(SIGUSR1, &sa, 0); if (r == -1) die(); } struct fifo fifo[1]; fifo_init(fifo); unsigned long rng = epochms(); int server = server_create(config.port, config.bind_family); while (running) { if (reload) { /* Configuration reload requested (SIGHUP) */ int oldport = config.port; int oldfamily = config.bind_family; config_load(&config, config_file, 0); config_log(&config); if (oldport != config.port || oldfamily != config.bind_family) { close(server); server = server_create(config.port, config.bind_family); } reload = 0; } if (dumpstats) { /* print stats requested (SIGUSR1) */ statistics_log_totals(fifo->head); dumpstats = 0; } /* Enqueue clients that are due for another message */ int timeout = -1; long long now = epochms(); while (fifo->head) { if (fifo->head->send_next <= now) { struct client *c = fifo_pop(fifo); if (sendline(c, config.max_line_length, &rng)) { c->send_next = now + config.delay; fifo_append(fifo, c); } } else { timeout = fifo->head->send_next - now; break; } } /* Wait for next event */ struct pollfd fds = {server, POLLIN, 0}; int nfds = fifo->length < config.max_clients; logmsg(log_debug, "poll(%d, %d)", nfds, timeout); int r = poll(&fds, nfds, timeout); logmsg(log_debug, "= %d", r); if (r == -1) { switch (errno) { case EINTR: logmsg(log_debug, "EINTR"); continue; default: fprintf(stderr, "endlessh: fatal: %s\n", strerror(errno)); exit(EXIT_FAILURE); } } /* Check for new incoming connections */ if (fds.revents & POLLIN) { int fd = accept(server, 0, 0); logmsg(log_debug, "accept() = %d", fd); statistics.connects++; if (fd == -1) { const char *msg = strerror(errno); switch (errno) { case EMFILE: case ENFILE: config.max_clients = fifo->length; logmsg(log_info, "MaxClients %d", fifo->length); break; case ECONNABORTED: case EINTR: case ENOBUFS: case ENOMEM: case EPROTO: fprintf(stderr, "endlessh: warning: %s\n", msg); break; default: fprintf(stderr, "endlessh: fatal: %s\n", msg); exit(EXIT_FAILURE); } } else { long long send_next = epochms() + config.delay; struct client *client = client_new(fd, send_next); int flags = fcntl(fd, F_GETFL, 0); /* cannot fail */ fcntl(fd, F_SETFL, flags | O_NONBLOCK); /* cannot fail */ if (!client) { fprintf(stderr, "endlessh: warning: out of memory\n"); close(fd); } else { fifo_append(fifo, client); logmsg(log_info, "ACCEPT host=%s port=%d fd=%d n=%d/%d", client->ipaddr, client->port, client->fd, fifo->length, config.max_clients); } } } } fifo_destroy(fifo); statistics_log_totals(0); if (logmsg == logsyslog) closelog(); } ================================================ FILE: util/endlessh.service ================================================ [Unit] Description=Endlessh SSH Tarpit Documentation=man:endlessh(1) Requires=network-online.target [Service] Type=simple Restart=always RestartSec=30sec ExecStart=/usr/local/bin/endlessh KillSignal=SIGTERM # Stop trying to restart the service if it restarts too many times in a row StartLimitInterval=5min StartLimitBurst=4 StandardOutput=journal StandardError=journal StandardInput=null PrivateTmp=true PrivateDevices=true ProtectSystem=full ProtectHome=true InaccessiblePaths=/run /var ## If you want Endlessh to bind on ports < 1024 ## 1) run: ## setcap 'cap_net_bind_service=+ep' /usr/local/bin/endlessh ## 2) uncomment following line #AmbientCapabilities=CAP_NET_BIND_SERVICE ## 3) comment following line PrivateUsers=true NoNewPrivileges=true ConfigurationDirectory=endlessh ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true MemoryDenyWriteExecute=true [Install] WantedBy=multi-user.target ================================================ FILE: util/openbsd/README.md ================================================ # Running `endlessh` on OpenBSD ## Covering IPv4 and IPv6 If you want to cover both IPv4 and IPv6 you'll need to run *two* instances of `endlessh` due to OpenBSD limitations. Here's how I did it: - copy the `endlessh` script to `rc.d` twice, as `endlessh` and `endlessh6` - copy the `config` file to `/etc/endlessh` twice, as `config` and `config6` - use `BindFamily 4` in `config` - use `BindFamily 6` in `config6` - in `rc.conf.local` force `endlessh6` to load `config6` like so: ``` endlessh6_flags=-s -f /etc/endlessh/config6 endlessh_flags=-s ``` ## Covering more than 128 connections The defaults in OpenBSD only allow for 128 open file descriptors per process, so regardless of the `MaxClients` setting in `/etc/config` you'll end up with something like 124 clients at the most. You can increase these limits in `/etc/login.conf` for `endlessh` (and `endlessh6`) like so: ``` endlessh:\ :openfiles=1024:\ :tc=daemon: endlessh6:\ :openfiles=1024:\ :tc=daemon: ``` ================================================ FILE: util/pivot.py ================================================ #!/usr/bin/env python3 # This script accepts a log on standard input and produces a CSV table # with one connection per row. # # $ util/pivot.py 0: print('warning: %d hanging entries' % len(table), file=sys.stderr) ================================================ FILE: util/schema.sql ================================================ CREATE TABLE IF NOT EXISTS log ( host TEXT, port INTEGER, time REAL, bytes INTEGER ); .mode csv .import /dev/stdin log ================================================ FILE: util/smf/README ================================================ Solaris SMF installation ======================== Before installing SMF: 1. Put endlessh binary to /usr/local/bin 2. Edit endlessh.conf and put it to /usr/local/etc To install SMF: 1. Put endlessh.xml to /var/svc/manifest/network 2. Run svccfg import endlessh.xml 3. Put init.endlessh to /lib/svc/method 4. Run svcadm enable endlessh Note: Log will write to /var/log/endlessh.log by default. To uninstall SMF: 1. Run svcadm disable endlessh 2. rm -f /lib/svc/method/init.endlessh 3. svccfg delete svc:/network/endlessh:default 4. rm -f /var/svc/manifest/network/endlessh.xml Enjoy! :) ================================================ FILE: util/smf/endlessh.conf ================================================ # The port on which to listen for new SSH connections. Port 22 # The endless banner is sent one line at a time. This is the delay # in milliseconds between individual lines. Delay 10000 # The length of each line is randomized. This controls the maximum # length of each line. Shorter lines may keep clients on for longer if # they give up after a certain number of bytes. MaxLineLength 32 # Maximum number of connections to accept at a time. Connections beyond # this are not immediately rejected, but will wait in the queue. MaxClients 4096 # Set the detail level for the log. # 0 = Quiet # 1 = Standard, useful log messages # 2 = Very noisy debugging information LogLevel 1 # Set the family of the listening socket # 0 = Use IPv4 Mapped IPv6 (Both v4 and v6, default) # 4 = Use IPv4 only # 6 = Use IPv6 only BindFamily 0 ================================================ FILE: util/smf/endlessh.xml ================================================ ================================================ FILE: util/smf/init.endlessh ================================================ #!/sbin/sh # # Control Method for endlessh (/lib/svc/method/init.endlessh) # Written by Yuri Voinov (C) 2007,2019 # # ident "@(#)endlessh.sh 1.8 19/27/03 YV" # ############# # Variables # ############# # Base installation directory BASE_DIR="/usr/local" BASE_CONFIG_DIR=$BASE_DIR"/etc" # endlessh files paths ENDLESSH_PATH="$BASE_DIR""/bin" ENDLESSH_CONF_PATH="$BASE_CONFIG_DIR" # endlessh files ENDLESSH_BIN_FILE="endlessh" ENDLESSH_CONF_FILE=$ENDLESSH_BIN_FILE".conf" # Daemon settings ENDLESSH_CONF="$ENDLESSH_CONF_PATH/$ENDLESSH_CONF_FILE" # Log LOG_DIR="/var/log" LOGFILE=$LOG_DIR/$ENDLESSH_BIN_FILE".log" # # OS Commands location variables # CUT=`which cut` ECHO=`which echo` KILL=`which kill` PGREP=`which pgrep` UNAME=`which uname` # OS release OS_VER=`$UNAME -r|$CUT -f2 -d"."` OS_NAME=`$UNAME -s|$CUT -f1 -d" "` ############### # Subroutines # ############### check_endlessh () { # Check endlessh installed program=$1 if [ ! -f "$ENDLESSH_PATH/$program" -a ! -x "$ENDLESSH_PATH/$program" ]; then $ECHO "ERROR: endlessh not found!" $ECHO "Exiting..." exit 1 fi } check_os () { # Check OS version if [ ! "$OS_NAME" = "SunOS" -a ! "$OS_VER" -lt "10" ]; then $ECHO "ERROR: Unsupported OS $OS_NAME $OS_VER" $ECHO "Exiting..." exit 1 fi } checkconf () { # Check endlessh config file config=$1 if [ -f "$ENDLESSH_CONF_PATH"/"$config" ]; then $ECHO "1" else $ECHO "0" fi } startproc() { # Start endlessh daemon program=$1 if [ "`checkconf $ENDLESSH_CONF_FILE`" != "1" ]; then $ECHO "ERROR: Config file $ENDLESSH_CONF_PATH/$ENDLESSH_CONF_FILE not found." $ECHO "Exiting..." exit 2 else $ENDLESSH_PATH/$program -f $ENDLESSH_CONF_PATH/$ENDLESSH_CONF_FILE -v >$LOGFILE & fi } stopproc() { # Stop endlessh daemon program=$1 if [ "`checkconf $ENDLESSH_CONF_FILE`" != "1" ]; then $ECHO "ERROR: Config file $ENDLESSH_CONF_PATH/$ENDLESSH_CONF_FILE not found." $ECHO "Exiting..." exit 2 else $KILL -s TERM `$PGREP $program`>/dev/null 2>&1 fi } ############## # Main block # ############## # Check endlessh installed check_endlessh $ENDLESSH_BIN_FILE # Check OS version check_os case "$1" in "start") startproc $ENDLESSH_BIN_FILE ;; "stop") stopproc $ENDLESSH_BIN_FILE ;; "refresh") $KILL -s HUP `$PGREP $ENDLESSH_BIN_FILE`>/dev/null 2>&1 ;; "restart") stopproc $ENDLESSH_BIN_FILE startproc $ENDLESSH_BIN_FILE ;; *) $ECHO "Usage: $0 { start | stop | restart | refresh }" exit 1 esac exit 0