From 4c3beb8bcd62d72710efa5997d7b3406792a22a9 Mon Sep 17 00:00:00 2001 From: WT Michael Date: Tue, 2 Jan 2024 01:54:54 +0000 Subject: [PATCH] Initial commit 2.0 - including earlier mod to add live stat tracking --- .gitignore | 2 + Dockerfile | 15 + Makefile | 21 + README.md | 141 +++++++ UNLICENSE | 24 ++ endlessh.1 | 84 ++++ endlessh.c | 847 +++++++++++++++++++++++++++++++++++++++++ util/endlessh.service | 44 +++ util/openbsd/README.md | 34 ++ util/openbsd/endlessh | 9 + util/pivot.py | 35 ++ util/schema.sql | 8 + util/smf/README | 25 ++ util/smf/endlessh.conf | 27 ++ util/smf/endlessh.xml | 96 +++++ util/smf/init.endlessh | 137 +++++++ 16 files changed, 1549 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 UNLICENSE create mode 100644 endlessh.1 create mode 100644 endlessh.c create mode 100644 util/endlessh.service create mode 100644 util/openbsd/README.md create mode 100755 util/openbsd/endlessh create mode 100755 util/pivot.py create mode 100644 util/schema.sql create mode 100644 util/smf/README create mode 100644 util/smf/endlessh.conf create mode 100644 util/smf/endlessh.xml create mode 100644 util/smf/init.endlessh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6753b35 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +endlessh + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a819488 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +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"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..119347a --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +.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 diff --git a/README.md b/README.md index 1e3725b..ff4a2f8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,143 @@ # aperturessh +A fork of [EndleSSH: an SSH tarpit](https://github.com/skeeto/endlessh) by skeeto on GitHub. + +Modified (hopefully one day) to spit out the lyrics to Still Alive from the Portal credits. + +Origial README from the GitHub repo is below: + +---- + +# 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/ \ No newline at end of file diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 0000000..68a49da --- /dev/null +++ b/UNLICENSE @@ -0,0 +1,24 @@ +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 diff --git a/endlessh.1 b/endlessh.1 new file mode 100644 index 0000000..43834df --- /dev/null +++ b/endlessh.1 @@ -0,0 +1,84 @@ +.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 diff --git a/endlessh.c b/endlessh.c new file mode 100644 index 0000000..b14de73 --- /dev/null +++ b/endlessh.c @@ -0,0 +1,847 @@ +/* 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 active; + 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; + statistics.active-=1; + 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 active=%lld seconds=%lld.%03lld bytes=%lld", + statistics.connects, + statistics.active, + 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 { + statistics.active+=1; + 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(); +} diff --git a/util/endlessh.service b/util/endlessh.service new file mode 100644 index 0000000..fd12a00 --- /dev/null +++ b/util/endlessh.service @@ -0,0 +1,44 @@ +[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 + diff --git a/util/openbsd/README.md b/util/openbsd/README.md new file mode 100644 index 0000000..d3e24bc --- /dev/null +++ b/util/openbsd/README.md @@ -0,0 +1,34 @@ +# 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: +``` diff --git a/util/openbsd/endlessh b/util/openbsd/endlessh new file mode 100755 index 0000000..f7b2313 --- /dev/null +++ b/util/openbsd/endlessh @@ -0,0 +1,9 @@ +#!/bin/ksh +# + +daemon="/usr/local/bin/endlessh" +rc_bg=YES + +. /etc/rc.d/rc.subr + +rc_cmd $1 diff --git a/util/pivot.py b/util/pivot.py new file mode 100755 index 0000000..4e3ab72 --- /dev/null +++ b/util/pivot.py @@ -0,0 +1,35 @@ +#!/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) diff --git a/util/schema.sql b/util/schema.sql new file mode 100644 index 0000000..78bf50a --- /dev/null +++ b/util/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS log ( + host TEXT, + port INTEGER, + time REAL, + bytes INTEGER +); +.mode csv +.import /dev/stdin log diff --git a/util/smf/README b/util/smf/README new file mode 100644 index 0000000..e9feaf8 --- /dev/null +++ b/util/smf/README @@ -0,0 +1,25 @@ +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! :) \ No newline at end of file diff --git a/util/smf/endlessh.conf b/util/smf/endlessh.conf new file mode 100644 index 0000000..5137178 --- /dev/null +++ b/util/smf/endlessh.conf @@ -0,0 +1,27 @@ +# 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 diff --git a/util/smf/endlessh.xml b/util/smf/endlessh.xml new file mode 100644 index 0000000..fe4e803 --- /dev/null +++ b/util/smf/endlessh.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/util/smf/init.endlessh b/util/smf/init.endlessh new file mode 100644 index 0000000..c254bb9 --- /dev/null +++ b/util/smf/init.endlessh @@ -0,0 +1,137 @@ +#!/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