diff --git a/.github/workflows/container-build.yaml b/.github/workflows/container-build.yaml new file mode 100644 index 0000000..d0a197d --- /dev/null +++ b/.github/workflows/container-build.yaml @@ -0,0 +1,63 @@ +name: Create and publish Container image + +on: + push: + branches: + - master + tags: + - 'v*' + pull_request: + branches: + - master + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=edge + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7 + context: . + file: Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/ChangeLog b/ChangeLog index 5e4ad26..1d24e02 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,32 @@ +v2.0: + New sslh-ev: this is functionally equivalent to + sslh-select (mono-process, only forks for specified + protocols), but based on libev, which should make it + scalable to large numbers of connections. + + New log system: instead of --verbose with arbitrary + levels, there are now several message classes. Each + message class can be set to go to stderr, syslog, or + both. Classes are documented in example.cfg. + + UDP connections are now managed in a hash to avoid + linear searches. The downside is that the number of + UDP connections is a hard limit, configurable with + the 'udp_max_connections', which defaults to 1024. + Timeouts are managed with lists. + + inetd merges stderr output to what is sent to the + client, which is a security issue as it might give + information to an attacker. When inetd is activated, + stderr is forcibly closed. + + New protocol-level option `resolve_on_forward`, + requests that target names are resolved at each + connection instead of at startup. Useful for dynamic + DNS situations. (Paul Schroeder/milkpirate) + + New probe for MSRDP (akappner). + v1.22: 17AUG2021 sslh-select now supports UDP protocols. Probes specified in the `protocols` @@ -12,7 +41,7 @@ v1.22: 17AUG2021 combined with incoming TLS with SNI. UDP clients and servers need to agree on the IPv4/IPv6 they use: use the same protocol on all sides! Often, this - means explicitely using 'ip4-localhost'. + means explicitly using 'ip4-localhost'. UDP sender-receiver pairs (connections, so to speak) are kept for 60s, which can be changed with `udp_timeout` in the configuration. @@ -54,7 +83,7 @@ v1.21: 11JUL2020 Added TCP_FASTOPEN support for client sockets (if tfo_ok is specified in their configuration) and for - listenint socket, if all client protocols support it. + listening socket, if all client protocols support it. (Craig Andrews) Added 'minlength' option to skip a probe if less @@ -80,8 +109,8 @@ v1.20: 20NOV2018 Before, probes were tried in order, repeating on the same probe as long it returned PROBE_AGAIN before moving to the next one. This means a probe which - requires a lot of data (i.e. returne PROBE_AGAIN for - a long time) could prevent sucessful matches from + requires a lot of data (i.e. return PROBE_AGAIN for + a long time) could prevent successful matches from subsequent probes. The configuration file needed to take that into account. @@ -142,7 +171,7 @@ v1.18: 29MAR2016 v1.17: 09MAR2015 Support RFC5952-style IPv6 addresses, e.g. [::]:443. - Transparant proxy support for FreeBSD. + Transparent proxy support for FreeBSD. (Ruben van Staveren) Using -F with no argument will try @@ -171,7 +200,7 @@ v1.16: 11FEB2014 Libcap support: Keep only CAP_NET_ADMIN if started as root with transparent proxying and dropping - priviledges (enable USELIBCAP in Makefile). This + privileges (enable USELIBCAP in Makefile). This avoids having to mess with filesystem capabilities. (Sebastian Schmidt/yath) @@ -180,7 +209,7 @@ v1.16: 11FEB2014 actual errors if connections are dropped before getting to getpeername). - Set IP_FREEDBIND if available to bind to addresses + Set IP_FREEBIND if available to bind to addresses that don't yet exist. v1.15: 27JUL2013 @@ -265,7 +294,7 @@ v1.11: 21APR2012 --user isn't specified, just run as current user. No longer create PID file by default, it should be - explicitely set with --pidfile. + explicitly set with --pidfile. No longer log to syslog if in foreground. Logs are instead output to stderr. @@ -356,7 +385,7 @@ v1.8: 15JUL2011 v1.7: 01FEB2010 Added CentOS init.d script (Andre Krajnik). - Fixed default ssl address inconsistancy, now + Fixed default ssl address inconsistency, now defaults to "localhost:443" and fixed documentation accordingly (pointed by Markus Schalke). diff --git a/Dockerfile b/Dockerfile index cb8de3a..cd22891 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,38 @@ -FROM alpine:latest as build +ARG ALPINE_VERSION="latest" +ARG TARGET_ARCH="library" -ADD . /sslh +FROM docker.io/${TARGET_ARCH}/alpine:${ALPINE_VERSION} AS build -RUN \ - apk add \ - gcc \ - libconfig-dev \ - make \ - musl-dev \ - pcre-dev \ - perl && \ - cd /sslh && \ - make sslh-select && \ - strip sslh-select +WORKDIR /sslh -FROM alpine:latest +RUN apk add --no-cache \ + 'gcc' \ + 'libconfig-dev' \ + 'make' \ + 'musl-dev' \ + 'pcre2-dev' \ + 'perl' \ + ; -COPY --from=build /sslh/sslh-select /sslh +COPY . /sslh -RUN apk --no-cache add libconfig pcre +RUN make sslh-select && strip sslh-select -ENTRYPOINT [ "/sslh", "--foreground"] +FROM docker.io/${TARGET_ARCH}/alpine:${ALPINE_VERSION} + +COPY --from=build "/sslh/sslh-select" "/usr/local/bin/sslh" +RUN apk add --no-cache \ + 'libconfig' \ + 'pcre2' \ + 'iptables' \ + 'ip6tables' \ + 'libcap' \ + && \ + adduser -s '/bin/sh' -S -D sslh && \ + setcap cap_net_bind_service,cap_net_raw+ep /usr/local/bin/sslh + +COPY "./container-entrypoint.sh" "/init" +ENTRYPOINT [ "/init" ] + +# required for updating iptables +USER root:root diff --git a/Makefile b/Makefile index 365ddf4..a4425c3 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ VERSION=$(shell ./genver.sh -r) # Configuration -- you probably need to `make clean` if you # change any of these +ENABLE_SANITIZER= # Enable ASAN/LSAN/UBSAN ENABLE_REGEX=1 # Enable regex probes USELIBCONFIG=1 # Use libconfig? (necessary to use configuration files) USELIBWRAP?= # Use libwrap? @@ -19,15 +20,25 @@ MAN=sslh.8.gz # man page name # End of configuration -- the rest should take care of # itself +ifneq ($(strip $(ENABLE_SANITIZER)),) + CFLAGS_SAN=-fsanitize=address -fsanitize=leak -fsanitize=undefined +endif + ifneq ($(strip $(COV_TEST)),) CFLAGS_COV=-fprofile-arcs -ftest-coverage endif CC ?= gcc -CFLAGS +=-Wall -DLIBPCRE -g $(CFLAGS_COV) +AR ?= ar +CFLAGS +=-Wall -O2 -DLIBPCRE -g $(CFLAGS_COV) $(CFLAGS_SAN) + LIBS=-lm -lpcre2-8 -OBJS=sslh-conf.o common.o sslh-main.o probe.o tls.o argtable3.o udp-listener.o collection.o gap.o +OBJS=sslh-conf.o common.o log.o sslh-main.o probe.o tls.o argtable3.o collection.o gap.o tcp-probe.o +OBJS_A=libsslh.a +FORK_OBJS=sslh-fork.o $(OBJS_A) +SELECT_OBJS=processes.o udp-listener.o sslh-select.o hash.o tcp-listener.o $(OBJS_A) +EV_OBJS=processes.o udp-listener.o sslh-ev.o hash.o tcp-listener.o $(OBJS_A) CONDITIONAL_TARGETS= @@ -64,32 +75,41 @@ endif all: sslh $(MAN) echosrv $(CONDITIONAL_TARGETS) -.c.o: *.h version.h - $(CC) $(CFLAGS) $(CPPFLAGS) -c $< +%.o: %.c %.h version.h + $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@ + +$(OBJS_A): $(OBJS) + $(AR) rcs $(OBJS_A) $(OBJS) version.h: ./genver.sh >version.h -sslh: sslh-fork sslh-select +sslh: sslh-fork sslh-select sslh-ev -$(OBJS): version.h common.h collection.h sslh-conf.h gap.h +$(OBJS) $(FORK_OBJS) $(SELECT_OBJS) $(EV_OBJS): argtable3.h collection.h common.h gap.h hash.h log.h probe.h processes.h sslh-conf.h tcp-listener.h tcp-probe.h tls.h udp-listener.h version.h + + +c2s: + conf2struct sslhconf.cfg + conf2struct echosrv.cfg sslh-conf.c sslh-conf.h: sslhconf.cfg - conf2struct sslhconf.cfg + $(warning "sslhconf.cfg is more recent than sslh-conf.[ch]. Use `make c2s` to rebuild using `conf2struct`") -sslh-fork: version.h $(OBJS) sslh-fork.o Makefile - $(CC) $(CFLAGS) $(LDFLAGS) -o sslh-fork sslh-fork.o $(OBJS) $(LIBS) - #strip sslh-fork +sslh-fork: version.h Makefile $(FORK_OBJS) + $(CC) $(CFLAGS) $(LDFLAGS) -o sslh-fork $(FORK_OBJS) $(LIBS) -sslh-select: version.h $(OBJS) sslh-select.o Makefile - $(CC) $(CFLAGS) $(LDFLAGS) -o sslh-select sslh-select.o $(OBJS) $(LIBS) - #strip sslh-select +sslh-select: version.h $(SELECT_OBJS) Makefile + $(CC) $(CFLAGS) $(LDFLAGS) -o sslh-select $(SELECT_OBJS) $(LIBS) + +sslh-ev: version.h $(EV_OBJS) Makefile + $(CC) $(CFLAGS) $(LDFLAGS) -o sslh-ev $(EV_OBJS) $(LIBS) -lev systemd-sslh-generator: systemd-sslh-generator.o $(CC) $(CFLAGS) $(LDFLAGS) -o systemd-sslh-generator systemd-sslh-generator.o -lconfig echosrv-conf.c echosrv-conf.h: echosrv.cfg - conf2struct echosrv.cfg + $(warning "echosrv.cfg is more recent than echosrv-conf.[ch]. Use `make c2s` to rebuild using `conf2struct`") echosrv: version.h echosrv-conf.c echosrv.o echosrv-conf.o argtable3.o $(CC) $(CFLAGS) $(LDFLAGS) -o echosrv echosrv.o echosrv-conf.o argtable3.o $(LIBS) @@ -133,7 +153,7 @@ distclean: clean rm -f tags sslh-conf.[ch] echosrv-conf.[ch] cscope.* clean: - rm -f sslh-fork sslh-select echosrv version.h $(MAN) systemd-sslh-generator *.o *.gcov *.gcno *.gcda *.png *.html *.css *.info + rm -f sslh-fork sslh-select sslh-ev echosrv version.h $(MAN) systemd-sslh-generator *.o *.gcov *.gcno *.gcda *.png *.html *.css *.info tags: ctags --globals -T *.[ch] diff --git a/README.md b/README.md index 2c391fa..c91d522 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ protocol that can be tested using a regular expression, can be recognised. A typical use case is to allow serving several services on port 443 (e.g. to connect to SSH from inside a corporate firewall, which almost never block port -443) while still serving HTTPS on that port. +443) while still serving HTTPS on that port. Hence `sslh` acts as a protocol demultiplexer, or a switchboard. With the SNI and ALPN probe, it makes a good @@ -20,8 +20,8 @@ address. `sslh` has the bells and whistles expected from a mature daemon: privilege and capabilities dropping, inetd support, -systemd support, transparent proxying, chroot, logging, -IPv4 and IPv6, TCP and UDP, a fork-based and a select-based +systemd support, transparent proxying, chroot, logging, +IPv4 and IPv6, TCP and UDP, a fork-based and a select-based model, and more. Install @@ -44,25 +44,23 @@ How to use --- -Build docker image - - make docker - ```bash -docker container run \ +docker run \ + --cap-add CAP_NET_RAW \ + --cap-add CAP_NET_BIND_SERVICE \ --rm \ -it \ + ghcr.io/yrutschle/sslh:latest \ + --foreground \ --listen=0.0.0.0:443 \ --ssh=hostname:22 \ - --tlshostname:443 \ - sslh:latest + --tls=hostname:443 ``` docker-compose example -``` ---- +```yaml version: "3" services: @@ -70,19 +68,96 @@ services: image: sslh:latest hostname: sslh ports: - - 443:443/tcp - command: --listen=0.0.0.0:443 --tlshostname:443 --openvpn=openvpn:1194 + - 443:443 + command: --foreground --listen=0.0.0.0:443 --tls=nginx:443 --openvpn=openvpn:1194 depends_on: - nginx - openvpn nginx: image: nginx - hostname: nginx + + openvpn: + image: openvpn +``` + +Transparent mode 1: using sslh container for networking + +_Note: For transparent mode to work, the sslh container must be able to reach your services via **localhost**_ +```yaml +version: "3" + +services: + sslh: + build: https://github.com/yrutschle/sslh.git + container_name: sslh + environment: + - TZ=${TZ} + cap_add: + - NET_ADMIN + - NET_RAW + - NET_BIND_SERVICE + sysctls: + - net.ipv4.conf.default.route_localnet=1 + - net.ipv4.conf.all.route_localnet=1 + command: --transparent --foreground --listen=0.0.0.0:443 --tls=localhost:8443 --openvpn=localhost:1194 + ports: + - 443:443 #sslh + + - 80:80 #nginx + - 8443:8443 #nginx + + - 1194:1194 #openvpn + extra_hosts: + - localbox:host-gateway + restart: unless-stopped + + nginx: + image: nginx:latest + ..... + network_mode: service:sslh #set nginx container to use sslh networking. + # ^^^ This is required. This makes nginx reachable by sslh via localhost + + openvpn: + image: openvpn:latest + ..... + network_mode: service:sslh #set openvpn container to use sslh networking +``` + +Transparent mode 2: using host networking + +```yaml +version: "3" + +services: + sslh: + build: https://github.com/yrutschle/sslh.git + container_name: sslh + environment: + - TZ=${TZ} + cap_add: + - NET_ADMIN + - NET_RAW + - NET_BIND_SERVICE + # must be set manually + #sysctls: + # - net.ipv4.conf.default.route_localnet=1 + # - net.ipv4.conf.all.route_localnet=1 + command: --transparent --foreground --listen=0.0.0.0:443 --tls=localhost:8443 --openvpn=localhost:1194 + network_mode: host + restart: unless-stopped + + nginx: + image: nginx:latest + ..... + ports: + - 8443:8443 # bind to docker host on port 8443 openvpn: image: openvpn:latest - hostname: openvpn + ..... + ports: + - 1194:1194 # bind to docker host on port 1194 ``` Comments? Questions? diff --git a/argtable3.c b/argtable3.c index ba15bb5..5b85398 100644 --- a/argtable3.c +++ b/argtable3.c @@ -2876,9 +2876,9 @@ static void arg_file_resetfn(struct arg_file* parent) { static const char* arg_basename(const char* filename) { const char *result = NULL, *result1, *result2; - /* Find the last occurrence of eother file separator character. */ - /* Two alternative file separator chars are supported as legal */ - /* file separators but not both together in the same filename. */ + /* Find the last occurrence of other file separator character. */ + /* Two alternative file separator chars are supported as legal */ + /* file separators but not both together in the same filename. */ result1 = (filename ? strrchr(filename, FILESEPARATOR1) : NULL); result2 = (filename ? strrchr(filename, FILESEPARATOR2) : NULL); @@ -2927,7 +2927,7 @@ static int arg_file_scanfn(struct arg_file* parent, const char* argval) { } else if (!argval) { /* a valid argument with no argument value was given. */ /* This happens when an optional argument value was invoked. */ - /* leave parent arguiment value unaltered but still count the argument. */ + /* leave parent argument value unaltered but still count the argument. */ parent->count++; } else { parent->filename[parent->count] = argval; @@ -3173,7 +3173,7 @@ static int arg_int_scanfn(struct arg_int* parent, const char* argval) { } else if (!argval) { /* a valid argument with no argument value was given. */ /* This happens when an optional argument value was invoked. */ - /* leave parent arguiment value unaltered but still count the argument. */ + /* leave parent argument value unaltered but still count the argument. */ parent->count++; } else { long int val; @@ -3813,8 +3813,8 @@ static const TRexChar* g_nnames[] = {_SC("NONE"), _SC("OP_GREEDY"), _SC("OP_O #endif #define OP_GREEDY (MAX_CHAR + 1) /* * + ? {n} */ #define OP_OR (MAX_CHAR + 2) -#define OP_EXPR (MAX_CHAR + 3) /* parentesis () */ -#define OP_NOCAPEXPR (MAX_CHAR + 4) /* parentesis (?:) */ +#define OP_EXPR (MAX_CHAR + 3) /* parenthesis () */ +#define OP_NOCAPEXPR (MAX_CHAR + 4) /* parenthesis (?:) */ #define OP_DOT (MAX_CHAR + 5) #define OP_CLASS (MAX_CHAR + 6) #define OP_CCLASS (MAX_CHAR + 7) @@ -5313,7 +5313,7 @@ static void arg_parse_untagged(int argc, char** argv, struct arg_hdr** table, st } } - /* if a tenative error still remains at this point then register it as a proper error */ + /* if a tentative error still remains at this point then register it as a proper error */ if (errorlast) { arg_register_error(endtable, parentlast, errorlast, optarglast); optind++; @@ -5384,7 +5384,7 @@ int arg_parse(int argc, char** argv, void** argtable) { /* Fill in the local copy of argv[]. We need a local copy because getopt rearranges argv[] which adversely affects - susbsequent parsing attempts. + subsequent parsing attempts. */ for (i = 0; i < argc; i++) argvcopy[i] = argv[i]; @@ -5451,7 +5451,7 @@ static void arg_cat_option(char* dest, size_t ndest, const char* shortopts, cons if (shortopts) { char option[3]; - /* note: option array[] is initialiazed dynamically here to satisfy */ + /* note: option array[] is initialized dynamically here to satisfy */ /* a deficiency in the watcom compiler wrt static array initializers. */ option[0] = '-'; option[1] = shortopts[0]; @@ -5509,7 +5509,7 @@ static void arg_cat_optionv(char* dest, size_t ndest, const char* shortopts, con /* "-a|-b|-c" */ char shortopt[3]; - /* note: shortopt array[] is initialiazed dynamically here to satisfy */ + /* note: shortopt array[] is initialized dynamically here to satisfy */ /* a deficiency in the watcom compiler wrt static array initializers. */ shortopt[0] = '-'; shortopt[1] = *c; @@ -5881,7 +5881,7 @@ static void arg_print_formatted_ds(arg_dstr_t ds, const unsigned lmargin, const * Prints the glossary in strict GNU format. * Differences to arg_print_glossary() are: * - wraps lines after 80 chars - * - indents lines without shortops + * - indents lines without shortopts * - does not accept formatstrings * * Contributed by Uli Fouquet @@ -5956,7 +5956,7 @@ int arg_nullcheck(void** argtable) { * that entry were still allocated ok. Those subsequent allocations will not be * deallocated by arg_free(). * Despite the unlikeliness of the problem occurring, and the even unlikelier event - * that it has any deliterious effect, it is fixed regardless by replacing arg_free() + * that it has any deleterious effect, it is fixed regardless by replacing arg_free() * with the newer arg_freetable() function. * We still keep arg_free() for backwards compatibility. */ diff --git a/argtable3.h b/argtable3.h index 487c22d..4785fff 100644 --- a/argtable3.h +++ b/argtable3.h @@ -87,7 +87,7 @@ typedef int(arg_comparefn)(const void* k1, const void* k2); * that particular arg_xxx arguments, performing post-parse checks, and * reporting errors. * These functions are private to the individual arg_xxx source code - * and are the pointer to them are initiliased by that arg_xxx struct's + * and are the pointer to them are initialised by that arg_xxx struct's * constructor function. The user could alter them after construction * if desired, but the original intention is for them to be set by the * constructor and left unaltered. @@ -95,7 +95,7 @@ typedef int(arg_comparefn)(const void* k1, const void* k2); typedef struct arg_hdr { char flag; /* Modifier flags: ARG_TERMINATOR, ARG_HASVALUE. */ const char* shortopts; /* String defining the short options */ - const char* longopts; /* String defiing the long options */ + const char* longopts; /* String defining the long options */ const char* datatype; /* Description of the argument data type */ const char* glossary; /* Description of the option as shown by arg_print_glossary function */ int mincount; /* Minimum number of occurences of this option accepted */ diff --git a/basic.cfg b/basic.cfg index d8e4336..2cb4d41 100644 --- a/basic.cfg +++ b/basic.cfg @@ -1,23 +1,25 @@ # This is a basic configuration file that should provide # sensible values for "standard" setup. -verbose: 0; -foreground: false; -inetd: false; -numeric: false; -transparent: false; +# You will find extensive examples with explanations in +# example.cfg + timeout: 2; user: "nobody"; pidfile: "/var/run/sslh.pid"; -chroot: "/var/empty"; -# Change hostname with your external address name. +# Change hostname with your external address name, or the IP +# of the interface that receives connections listen: ( { host: "thelonious"; port: "443"; } ); + +# Change to the protocols you want to forward to. The +# defaults here are sensible for services running on +# localhost protocols: ( { name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; fork: true; }, diff --git a/collection.c b/collection.c index e0d556d..8680915 100644 --- a/collection.c +++ b/collection.c @@ -30,8 +30,9 @@ struct cnx_collection { gap_array* fd2cnx; /* Array indexed by file descriptor to things in cnx[] */ }; -/* Allocates and initialises a new collection of connections. */ -cnx_collection* collection_init(void) +/* Allocates and initialises a new collection of connections with at least + * `len` elements. */ +cnx_collection* collection_init(int len) { cnx_collection* collection; @@ -40,7 +41,7 @@ cnx_collection* collection_init(void) memset(collection, 0, sizeof(*collection)); - collection->fd2cnx = gap_init(); + collection->fd2cnx = gap_init(len); return collection; } diff --git a/collection.h b/collection.h index f44b270..8d6ebb2 100644 --- a/collection.h +++ b/collection.h @@ -4,7 +4,7 @@ typedef struct cnx_collection cnx_collection; -cnx_collection* collection_init(void); +cnx_collection* collection_init(int len); void collection_destroy(cnx_collection* collection); struct connection* collection_alloc_cnx_from_fd(cnx_collection* collection, int fd); diff --git a/common.c b/common.c index 909729c..59437f1 100644 --- a/common.c +++ b/common.c @@ -4,7 +4,6 @@ * No code here should assume whether sockets are blocking or not. **/ -#define SYSLOG_NAMES #define _GNU_SOURCE #include #include @@ -16,6 +15,7 @@ #include "common.h" #include "probe.h" +#include "log.h" #include "sslh-conf.h" /* Added to make the code compilable under CYGWIN @@ -42,8 +42,6 @@ struct sslhcfg_item cfg; struct addrinfo *addr_listen = NULL; /* what addresses do we listen to? */ -static int do_syslog = 1; /* Should we syslog? controled by syslog_facility = "none" */ - #ifdef LIBWRAP #include int allow_severity =0, deny_severity = 0; @@ -60,7 +58,7 @@ void check_res_dump(CR_ACTION act, int res, struct addrinfo *addr, char* syscall char buf[NI_MAXHOST]; if (res == -1) { - fprintf(stderr, "%s:%s: %s\n", + print_message(msg_system_error, "%s:%s: %s\n", sprintaddr(buf, sizeof(buf), addr), syscall, strerror(errno)); @@ -77,7 +75,7 @@ int get_fd_sockets(struct listen_endpoint *sockfd[]) #ifdef SYSTEMD sd = sd_listen_fds(0); if (sd < 0) { - fprintf(stderr, "sd_listen_fds(): %s\n", strerror(-sd)); + print_message(msg_system_error, "sd_listen_fds(): %s\n", strerror(-sd)); exit(1); } if (sd > 0) { @@ -177,7 +175,7 @@ int start_listen_sockets(struct listen_endpoint *sockfd[]) *sockfd = NULL; - if (cfg.verbose) fprintf(stderr, "Listening to:\n"); + print_message(msg_config, "Listening to:\n"); for (i = 0; i < cfg.listen_len; i++) { keepalive = cfg.listen[i].keepalive; @@ -188,13 +186,12 @@ int start_listen_sockets(struct listen_endpoint *sockfd[]) for (addr = start_addr; addr; addr = addr->ai_next) { num_addr++; - *sockfd = realloc(*sockfd, num_addr * sizeof(*sockfd)); + *sockfd = realloc(*sockfd, num_addr * sizeof(*sockfd[0])); (*sockfd)[num_addr-1].socketfd = listen_single_addr(addr, keepalive, udp); (*sockfd)[num_addr-1].type = udp ? SOCK_DGRAM : SOCK_STREAM; - if (cfg.verbose) - fprintf(stderr, "%d:\t%s\t[%s] [%s]\n", (*sockfd)[num_addr-1].socketfd, sprintaddr(buf, sizeof(buf), addr), - cfg.listen[i].keepalive ? "keepalive" : "", - cfg.listen[i].is_udp ? "udp" : ""); + print_message(msg_config, "%d:\t%s\t[%s] [%s]\n", (*sockfd)[num_addr-1].socketfd, sprintaddr(buf, sizeof(buf), addr), + cfg.listen[i].keepalive ? "keepalive" : "", + cfg.listen[i].is_udp ? "udp" : ""); } freeaddrinfo(start_addr); } @@ -322,19 +319,23 @@ int connect_addr(struct connection *cnx, int fd_from, connect_blocking blocking) res = getpeername(fd_from, from.ai_addr, &from.ai_addrlen); CHECK_RES_RETURN(res, "getpeername", res); + if (cnx->proto->resolve_on_forward) { + resolve_split_name(&(cnx->proto->saddr), cnx->proto->host, + cnx->proto->port); + } + for (a = cnx->proto->saddr; a; a = a->ai_next) { /* When transparent, make sure both connections use the same address family */ if (transparent && a->ai_family != from.ai_addr->sa_family) continue; - if (cfg.verbose) - fprintf(stderr, "connecting to %s family %d len %d\n", + print_message(msg_connections_try, "trying to connect to %s family %d len %d\n", sprintaddr(buf, sizeof(buf), a), a->ai_addr->sa_family, a->ai_addrlen); /* XXX Needs to match ai_family from fd_from when being transparent! */ fd = socket(a->ai_family, SOCK_STREAM, 0); if (fd == -1) { - log_message(LOG_ERR, "forward to %s failed:socket: %s\n", + print_message(msg_connections_error, "forward to %s failed:socket: %s\n", cnx->proto->name, strerror(errno)); } else { one = 1; @@ -347,6 +348,7 @@ int connect_addr(struct connection *cnx, int fd_from, connect_blocking blocking) if (transparent) { res = bind_peer(fd, fd_from); + if (res == -1) close(fd); CHECK_RES_RETURN(res, "bind_peer", res); } res = connect(fd, a->ai_addr, a->ai_addrlen); @@ -354,7 +356,7 @@ int connect_addr(struct connection *cnx, int fd_from, connect_blocking blocking) /* EINPROGRESS indicates it might take time. If it eventually * fails, it'll be caught as a failed read */ if ((res == -1) && (errno != EINPROGRESS)) { - log_message(LOG_ERR, "forward to %s failed:connect: %s\n", + print_message(msg_connections_error, "forward to %s failed:connect: %s\n", cnx->proto->name, strerror(errno)); close(fd); continue; /* Try the next address */ @@ -374,9 +376,8 @@ int defer_write(struct queue *q, void* data, int data_size) { char *p; ptrdiff_t data_offset = q->deferred_data - q->begin_deferred_data; - if (cfg.verbose) - fprintf(stderr, "**** writing deferred on fd %d\n", q->fd); + print_message(msg_fd, "writing deferred on fd %d\n", q->fd); p = realloc(q->begin_deferred_data, data_offset + q->deferred_data_size + data_size); CHECK_ALLOC(p, "realloc"); @@ -397,8 +398,7 @@ int flush_deferred(struct queue *q) { int n; - if (cfg.verbose) - fprintf(stderr, "flushing deferred data to fd %d\n", q->fd); + print_message(msg_fd, "flushing deferred data to fd %d\n", q->fd); n = write(q->fd, q->deferred_data, q->deferred_data_size); if (n == -1) @@ -430,11 +430,12 @@ void init_cnx(struct connection *cnx) void dump_connection(struct connection *cnx) { - printf("state: %d\n", cnx->state); - printf("0: fd %d, %d deferred\n", cnx->q[0].fd, cnx->q[0].deferred_data_size); - hexdump(cnx->q[0].deferred_data, cnx->q[0].deferred_data_size); - printf("1: fd %d, %d deferred\n", cnx->q[1].fd, cnx->q[1].deferred_data_size); - hexdump(cnx->q[1].deferred_data, cnx->q[1].deferred_data_size); + print_message(msg_int_error, "type: %s\n", cnx->type == SOCK_DGRAM ? "UDP" : "TCP"); + print_message(msg_int_error, "state: %d\n", cnx->state); + print_message(msg_int_error, "0: fd %d, %d deferred\n", cnx->q[0].fd, cnx->q[0].deferred_data_size); + hexdump(msg_int_error, cnx->q[0].deferred_data, cnx->q[0].deferred_data_size); + print_message(msg_int_error, "1: fd %d, %d deferred\n", cnx->q[1].fd, cnx->q[1].deferred_data_size); + hexdump(msg_int_error, cnx->q[1].deferred_data, cnx->q[1].deferred_data_size); } @@ -459,8 +460,6 @@ int fd2fd(struct queue *target_q, struct queue *from_q) if (size_r == -1) { switch (errno) { case EAGAIN: - if (cfg.verbose) - fprintf(stderr, "reading 0 from %d\n", from); return FD_NODATA; case ECONNRESET: @@ -511,7 +510,7 @@ char* sprintaddr(char* buf, size_t size, struct addrinfo *a) cfg.numeric ? NI_NUMERICHOST | NI_NUMERICSERV : 0 ); if (res) { - log_message(LOG_ERR, "sprintaddr:getnameinfo: %s\n", gai_strerror(res)); + print_message(msg_system_error, "sprintaddr:getnameinfo: %s\n", gai_strerror(res)); /* Name resolution failed: do it numerically instead */ res = getnameinfo(a->ai_addr, a->ai_addrlen, host, sizeof(host), @@ -519,7 +518,7 @@ char* sprintaddr(char* buf, size_t size, struct addrinfo *a) NI_NUMERICHOST | NI_NUMERICSERV); /* should not fail but... */ if (res) { - log_message(LOG_ERR, "sprintaddr:getnameinfo(NUM): %s\n", gai_strerror(res)); + print_message(msg_system_error, "sprintaddr:getnameinfo(NUM): %s\n", gai_strerror(res)); strcpy(host, "?"); strcpy(serv, "?"); } @@ -548,7 +547,7 @@ int resolve_split_name(struct addrinfo **out, char* host, char* serv) if (host[0] == '[') { end = strrchr(host, ']'); if (!end) { - fprintf(stderr, "%s: no closing bracket in IPv6 address?\n", host); + print_message(msg_config_error, "%s: no closing bracket in IPv6 address?\n", host); return -1; } host++; /* skip first bracket */ @@ -557,7 +556,7 @@ int resolve_split_name(struct addrinfo **out, char* host, char* serv) res = getaddrinfo(host, serv, &hint, out); if (res) - log_message(LOG_ERR, "%s `%s:%s'\n", gai_strerror(res), host, serv); + print_message(msg_system_error, "%s `%s:%s'\n", gai_strerror(res), host, serv); return res; } @@ -573,7 +572,7 @@ void resolve_name(struct addrinfo **out, char* fullname) /* Find port */ char *sep = strrchr(fullname, ':'); if (!sep) { /* No separator: parameter is just a port */ - fprintf(stderr, "%s: names must be fully specified as hostname:port\n", fullname); + print_message(msg_config_error, "%s: names must be fully specified as hostname:port\n", fullname); exit(1); } serv = sep+1; @@ -583,30 +582,13 @@ void resolve_name(struct addrinfo **out, char* fullname) res = resolve_split_name(out, host, serv); if (res) { - fprintf(stderr, "%s `%s'\n", gai_strerror(res), fullname); + print_message(msg_config_error, "%s `%s'\n", gai_strerror(res), fullname); if (res == EAI_SERVICE) - fprintf(stderr, "(Check you have specified all ports)\n"); + print_message(msg_config_error, "(Check you have specified all ports)\n"); exit(4); } } -/* Log to syslog or stderr if foreground */ -void log_message(int type, const char* msg, ...) -{ - va_list ap; - - va_start(ap, msg); - if (cfg.foreground) - vfprintf(stderr, msg, ap); - va_end(ap); - - if (do_syslog) { - va_start(ap, msg); - vsyslog(type, msg, ap); - va_end(ap); - } -} - /* Fills a connection description; returns 0 on failure */ int get_connection_desc(struct connection_desc* desc, const struct connection *cnx) @@ -641,30 +623,6 @@ int get_connection_desc(struct connection_desc* desc, const struct connection *c return 1; } -/* syslogs who connected to where - * desc: string description of the connection. if NULL, log_connection will - * manage on its own - * cnx: connection descriptor - * */ -void log_connection(struct connection_desc* desc, const struct connection *cnx) -{ - struct connection_desc d; - - if (cnx->proto->log_level < 1) - return; - - if (!desc) { - desc = &d; - get_connection_desc(desc, cnx); - } - - log_message(LOG_INFO, "%s:connection from %s to %s forwarded from %s to %s\n", - cnx->proto->name, - desc->peer, - desc->service, - desc->local, - desc->target); -} void set_proctitle_shovel(struct connection_desc* desc, const struct connection *cnx) { @@ -708,8 +666,7 @@ int check_access_rights(int in_socket, const char* service) /* extract peer address */ res = getnameinfo(&peer.saddr, size, addr_str, sizeof(addr_str), NULL, 0, NI_NUMERICHOST); if (res) { - if (cfg.verbose) - fprintf(stderr, "getnameinfo(NI_NUMERICHOST):%s\n", gai_strerror(res)); + print_message(msg_system_error, "getnameinfo(NI_NUMERICHOST):%s\n", gai_strerror(res)); strcpy(addr_str, STRING_UNKNOWN); } /* extract peer name */ @@ -717,15 +674,12 @@ int check_access_rights(int in_socket, const char* service) if (!cfg.numeric) { res = getnameinfo(&peer.saddr, size, host, sizeof(host), NULL, 0, NI_NAMEREQD); if (res) { - if (cfg.verbose) - fprintf(stderr, "getnameinfo(NI_NAMEREQD):%s\n", gai_strerror(res)); + print_message(msg_system_error, "getnameinfo(NI_NAMEREQD):%s\n", gai_strerror(res)); } } if (!hosts_ctl(service, host, addr_str, STRING_UNKNOWN)) { - if (cfg.verbose) - fprintf(stderr, "access denied\n"); - log_message(LOG_INFO, "connection from %s(%s): access denied", host, addr_str); + print_message(msg_connections, "connection from %s(%s): access denied", host, addr_str); close(in_socket); return -1; } @@ -760,35 +714,6 @@ void setup_signals(void) } -/* Open syslog connection with appropriate banner; - * banner is made up of basename(bin_name)+"[pid]" */ -void setup_syslog(const char* bin_name) { - char *name1, *name2; - int res, fn; - - if (!strcmp(cfg.syslog_facility, "none")) { - do_syslog = 0; - return; - } - - name1 = strdup(bin_name); - res = asprintf(&name2, "%s[%d]", basename(name1), getpid()); - CHECK_RES_DIE(res, "asprintf"); - - for (fn = 0; facilitynames[fn].c_val != -1; fn++) - if (strcmp(facilitynames[fn].c_name, cfg.syslog_facility) == 0) - break; - if (facilitynames[fn].c_val == -1) { - fprintf(stderr, "Unknown facility %s\n", cfg.syslog_facility); - exit(1); - } - - openlog(name2, LOG_CONS, facilitynames[fn].c_val); - free(name1); - /* Don't free name2, as openlog(3) uses it (at least in glibc) */ - - log_message(LOG_INFO, "%s %s started\n", server_type, VERSION); -} /* Ask OS to keep capabilities over a setuid(nonzero) */ void set_keepcaps(int val) { @@ -866,16 +791,14 @@ void drop_privileges(const char* user_name, const char* chroot_path) if (user_name) { pw = getpwnam(user_name); if (!pw) { - fprintf(stderr, "%s: not found\n", user_name); + print_message(msg_config_error, "%s: not found\n", user_name); exit(2); } - if (cfg.verbose) - fprintf(stderr, "turning into %s\n", user_name); + print_message(msg_config, "turning into %s\n", user_name); } if (chroot_path) { - if (cfg.verbose) - fprintf(stderr, "chrooting into %s\n", chroot_path); + print_message(msg_config, "chrooting into %s\n", chroot_path); res = chroot(chroot_path); CHECK_RES_DIE(res, "chroot"); @@ -905,14 +828,24 @@ void drop_privileges(const char* user_name, const char* chroot_path) void write_pid_file(const char* pidfile) { FILE *f; + int res; f = fopen(pidfile, "w"); if (!f) { - perror(pidfile); + print_message(msg_system_error, "write_pid_file:%s:%s", pidfile, strerror(errno)); exit(3); } - fprintf(f, "%d\n", getpid()); - fclose(f); + res = fprintf(f, "%d\n", getpid()); + if (res < 0) { + print_message(msg_system_error, "write_pid_file:fprintf:%s", strerror(errno)); + exit(3); + } + + res = fclose(f); + if (res == EOF) { + print_message(msg_system_error, "write_pid_file:fclose:%s", strerror(errno)); + exit(3); + } } diff --git a/common.h b/common.h index 4b8e389..6a011f4 100644 --- a/common.h +++ b/common.h @@ -40,20 +40,20 @@ #define CHECK_RES_DIE(res, str) \ if (res == -1) { \ - fprintf(stderr, "%s:%d:", __FILE__, __LINE__); \ + print_message(msg_system_error, "%s:%d:", __FILE__, __LINE__); \ perror(str); \ exit(1); \ } #define CHECK_RES_RETURN(res, str, ret) \ if (res == -1) { \ - log_message(LOG_CRIT, "%s:%d:%s:%d:%s\n", __FILE__, __LINE__, str, errno, strerror(errno)); \ + print_message(msg_system_error, "%s:%d:%s:%d:%s\n", __FILE__, __LINE__, str, errno, strerror(errno)); \ return ret; \ } #define CHECK_ALLOC(a, str) \ if (!a) { \ - fprintf(stderr, "%s:%d:", __FILE__, __LINE__); \ + print_message(msg_system_error, "%s:%d:", __FILE__, __LINE__); \ perror(str); \ exit(1); \ } @@ -83,9 +83,6 @@ enum connection_state { ST_SHOVELING /* Connexion is established */ }; -/* this is used to pass protocols through the command-line parameter parsing */ -#define PROT_SHIFT 1000 /* protocol options will be 1000, 1001, etc */ - /* A 'queue' is composed of a file descriptor (which can be read from or * written to), and a queue for deferred write data */ struct queue { @@ -95,6 +92,12 @@ struct queue { int deferred_data_size; }; +/* Double linked list for timeout management */ +typedef struct { + struct connection* head; + struct connection* tail; +} dl_list; + struct connection { int type; /* SOCK_DGRAM | SOCK_STREAM */ struct sslhcfg_protocols_item* proto; /* Where to connect to */ @@ -109,13 +112,16 @@ struct connection { struct queue q[2]; /* SOCK_DGRAM */ - struct sockaddr client_addr; /* Contains the remote client address */ + struct sockaddr_storage client_addr; /* Contains the remote client address */ socklen_t addrlen; int local_endpoint; /* Contains the local address */ time_t last_active; + /* double linked list of timeouts */ + struct connection *timeout_prev, *timeout_next; + /* We need one local socket for each target server, so we know where to * forward server responses */ int target_sock; @@ -160,7 +166,6 @@ void setup_syslog(const char* bin_name); void drop_privileges(const char* user_name, const char* chroot_path); void set_capabilities(int cap_net_admin); void write_pid_file(const char* pidfile); -void log_message(int type, const char* msg, ...); void dump_connection(struct connection *cnx); int resolve_split_name(struct addrinfo **out, char* hostname, char* port); @@ -171,7 +176,6 @@ int flush_deferred(struct queue *q); extern struct sslhcfg_item cfg; extern struct addrinfo *addr_listen; -extern const char* USAGE_STRING; extern const char* server_type; /* sslh-fork.c */ diff --git a/container-entrypoint.sh b/container-entrypoint.sh new file mode 100755 index 0000000..a8d6e04 --- /dev/null +++ b/container-entrypoint.sh @@ -0,0 +1,97 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL2-or-later +# +# Copyright (C) 2023 Olliver Schinagl +# +# A beginning user should be able to docker run image bash (or sh) without +# needing to learn about --entrypoint +# https://github.com/docker-library/official-images#consistency + +set -eu + +bin='sslh' + +# run command if it is not starting with a "-" and is an executable in PATH +if [ "${#}" -le 0 ] || \ + [ "${1#-}" != "${1}" ] || \ + [ -d "${1}" ] || \ + ! command -v "${1}" > '/dev/null' 2>&1; then + entrypoint='true' +fi + +unconfigure_iptables() { + echo "Received SIG TERM/INT/KILL. Removing iptables / routing changes" + + set +e # Don't exit if got error + set -x + + iptables -t raw -D PREROUTING ! -i lo -d 127.0.0.0/8 -j DROP + iptables -t mangle -D POSTROUTING ! -o lo -s 127.0.0.0/8 -j DROP + + iptables -t nat -D OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f + iptables -t mangle -D OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f + + ip rule del fwmark 0x1 lookup 100 + ip route del local 0.0.0.0/0 dev lo table 100 + + + if [ $(cat /proc/sys/net/ipv6/conf/all/disable_ipv6) -eq 0 ]; then + ip6tables -t raw -D PREROUTING ! -i lo -d ::1/128 -j DROP + ip6tables -t mangle -D POSTROUTING ! -o lo -s ::1/128 -j DROP + ip6tables -t nat -D OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f + ip6tables -t mangle -D OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f + + ip -6 rule del fwmark 0x1 lookup 100 + ip -6 route del local ::/0 dev lo table 100 + fi + + set -e + set +x +} + +configure_iptables() { + echo "Configuring iptables and routing..." + + set +e # Don't exit if got error + set -x + + iptables -t raw -A PREROUTING ! -i lo -d 127.0.0.0/8 -j DROP + iptables -t mangle -A POSTROUTING ! -o lo -s 127.0.0.0/8 -j DROP + + iptables -t nat -A OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f + iptables -t mangle -A OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f + + ip rule add fwmark 0x1 lookup 100 + ip route add local 0.0.0.0/0 dev lo table 100 + + if [ $(cat /proc/sys/net/ipv6/conf/all/disable_ipv6) -eq 0 ]; then + ip6tables -t raw -A PREROUTING ! -i lo -d ::1/128 -j DROP + ip6tables -t mangle -A POSTROUTING ! -o lo -s ::1/128 -j DROP + ip6tables -t nat -A OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f + ip6tables -t mangle -A OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f + + ip -6 rule add fwmark 0x1 lookup 100 + ip -6 route add local ::/0 dev lo table 100 + fi + + set -e + set +x +} + +for _args in "${@}" ; do + if [ "${_args:-}" = '--transparent' ] ; then + echo '--transparent flag is set' + configure_iptables + trap unconfigure_iptables TERM INT KILL + break + fi +done + +# Drop privileges and run as sslh user +sslh_cmd="${entrypoint:+${bin}} ${@}" +echo "Executing with user 'sslh': ${sslh_cmd}" + +exec su - sslh -c "${sslh_cmd}" & +wait "${!}" + +exit 0 diff --git a/doc/FAQ.md b/doc/FAQ.md index 8e91c48..528c9d9 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -7,7 +7,7 @@ doesn't work, report how what was suggested here went. It's also worth reading [how to ask questions](http://www.catb.org/~esr/faqs/smart-questions.html) before posting on the mailing list or opening an issue in -Github. +GitHub. Getting more info ================= diff --git a/doc/INSTALL.md b/doc/INSTALL.md index b06859e..fb1b087 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -7,7 +7,7 @@ Dependencies `sslh` uses: * [libconfig](http://www.hyperrealm.com/libconfig/). For - Debian this is contained in package `libconfig8-dev`. You + Debian this is contained in package `libconfig-dev`. You can compile with or without it using USELIBCONFIG in the Makefile. @@ -29,6 +29,13 @@ Makefile. which requires `libbsd` at runtime, and `libbsd-dev` at compile-time. +* libpcre2, in package `libpcre-dev`. You can compile + with or without it using ENABLE_REGEX in the Makefile. + +* libev-dev, in package `libev-dev`. If you build a binary + specifically and do not build `sslh-ev`, you don't need + this. + For OpenSUSE, these are contained in packages libconfig9 and libconfig-dev in repository @@ -44,6 +51,10 @@ distclean` for example), you will also need to add [conf2struct](https://www.rutschle.net/tech/conf2struct/README.html) (v1.5) to your path. + +The test scripts are written in Perl, and will require +IO::Socket::INET6 (libio-socket-inet6-perl in Debian). + Compilation ----------- @@ -68,11 +79,25 @@ of the Makefile: * `USELIBBSD` compiles support for updating the process name (as shown by `ps`). +Generating the configuration parser +----------------------------------- + +The configuration file and command line parser is generated +by `conf2struct`, from `sslhconf.cfg`, which generates +`sslh-conf.c` and `sslh-conf.h`. The resulting files are +included in the source so `sslh` can be built without +`conf2struct` installed. + +Further, to prevent build issues, `sslh-conf.[ch]` has no +dependency to `sslhconf.cfg` in the Makefile. In the event +of adding configuration settings, they need to be +regenerated using `make c2s`. + Binaries -------- -The Makefile produces two different executables: `sslh-fork` -and `sslh-select`: +The Makefile produces three different executables: `sslh-fork`, +`sslh-select` and `sslh-ev`: * `sslh-fork` forks a new process for each incoming connection. It is well-tested and very reliable, but incurs the overhead @@ -81,18 +106,17 @@ If you are going to use `sslh` for a "small" setup (less than a dozen ssh connections and a low-traffic https server) then `sslh-fork` is probably more suited for you. -* `sslh-select` uses only one thread, which monitors all connections -at once. It is more recent and less tested, but only incurs a 16 -byte overhead per connection. Also, if it stops, you'll lose all -connections, which means you can't upgrade it remotely. -If you are going to use `sslh` on a "medium" setup (a few thousand ssh -connections, and another few thousand ssl connections), -`sslh-select` will be better. - -If you have a very large site (tens of thousands of connections), -you'll need a vapourware version that would use libevent or -something like that. +* `sslh-select` uses only one thread, which monitors all + connections at once. It only incurs a 16 byte overhead per +connection. Also, if it stops, you'll lose all connections, +which means you can't upgrade it remotely. If you are going +to use `sslh` on a "medium" setup (a few hundreds of +connections), or if you are on a system where forking is +expensive (e.g. Windows), `sslh-select` will be better. +* `sslh-ev` is similar to `sslh-select`, but uses `libev` as + a backend. This allows using specific kernel APIs that +allow to manage thousands of connections concurrently. Installation ------------ diff --git a/doc/README.MacOSX b/doc/README.MacOSX index 2e70a7b..0f69ed2 100644 --- a/doc/README.MacOSX +++ b/doc/README.MacOSX @@ -33,7 +33,7 @@ with launchctl or simply reboot. 0.0.0.0:443 --ssh localhost:22 - --ssl + --tls localhost:443 QueueDirectories diff --git a/doc/config.md b/doc/config.md index ad1c903..d215fcf 100644 --- a/doc/config.md +++ b/doc/config.md @@ -65,13 +65,13 @@ Configuration goes like this on the server side, using `stunnel3`: * `-f` for foreground/debugging * `-p` for specifying the key and certificate * `-d` for specifying which interface and port - we're listening to for incoming connexions + we're listening to for incoming connections * `-l` summons `sslh` in inetd mode. * sslh options: * `-i` for inetd mode - * `--http` to forward HTTP connexions to port 80, - and SSH connexions to port 22. + * `--http` to forward HTTP connections to port 80, + and SSH connections to port 22. Capabilities support -------------------- @@ -92,165 +92,19 @@ to the executable: sudo setcap cap_net_bind_service,cap_net_raw+pe sslh-select -Then you can run sslh-select as an unpriviledged user, e.g.: +Then you can run sslh-select as an unprivileged user, e.g.: sslh-select -p myname:443 --ssh localhost:22 --tls localhost:443 Transparent proxy support ------------------------- -On Linux and FreeBSD you can use the `--transparent` option to -request transparent proxying. This means services behind `sslh` -(Apache, `sshd` and so on) will see the external IP and ports -as if the external world connected directly to them. This -simplifies IP-based access control (or makes it possible at -all). +Transparent proxying allows the target server to see the +original client IP address, i.e. `sslh` becomes invisible. +This makes it easier to use the server's logs, and potential +IP-based banning ability. -You can refer to Sean Warn'ѕ [tutorial](tproxy.md) for a -different set-up which enables transparent proxying between -two different machines. The following may only work if -`sslh` and the final servers are on the same machine. - -Note that getting this to work is very tricky and -detail-dependant: depending on whether the target server and -sslh are on the same machine, different machines, or -different dockers, and tool versions, all seem to change the -required network configuration somewhat. If it doesn't work, -it's almost certain that the problem is not linked to `sslh` -but to the network setup that surrounds it. - -Linux: - -`sslh` needs extended rights to perform this: you'll need to -give it `CAP_NET_RAW` capabilities (see appropriate chapter) -or run it as root (but don't do that). - -The firewalling tables also need to be adjusted as follows. -I don't think it is possible to have `httpd` and `sslh` both listen to 443 in -this scheme -- let me know if you manage that: - - # Set route_localnet = 1 on all interfaces so that ssl can use "localhost" as destination - sysctl -w net.ipv4.conf.default.route_localnet=1 - sysctl -w net.ipv4.conf.all.route_localnet=1 - - # DROP martian packets as they would have been if route_localnet was zero - # Note: packets not leaving the server aren't affected by this, thus sslh will still work - iptables -t raw -A PREROUTING ! -i lo -d 127.0.0.0/8 -j DROP - iptables -t mangle -A POSTROUTING ! -o lo -s 127.0.0.0/8 -j DROP - - # Mark all connections made by ssl for special treatment (here sslh is run as user "sslh") - iptables -t nat -A OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f - - # Outgoing packets that should go to sslh instead have to be rerouted, so mark them accordingly (copying over the connection mark) - iptables -t mangle -A OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f - - # Configure routing for those marked packets - ip rule add fwmark 0x1 lookup 100 - ip route add local 0.0.0.0/0 dev lo table 100 - -Tranparent proxying with IPv6 is similarly set up as follows: - - # Set route_localnet = 1 on all interfaces so that ssl can use "localhost" as destination - # Not sure if this is needed for ipv6 though - sysctl -w net.ipv4.conf.default.route_localnet=1 - sysctl -w net.ipv4.conf.all.route_localnet=1 - - # DROP martian packets as they would have been if route_localnet was zero - # Note: packets not leaving the server aren't affected by this, thus sslh will still work - ip6tables -t raw -A PREROUTING ! -i lo -d ::1/128 -j DROP - ip6tables -t mangle -A POSTROUTING ! -o lo -s ::1/128 -j DROP - - # Mark all connections made by ssl for special treatment (here sslh is run as user "sslh") - ip6tables -t nat -A OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f - - # Outgoing packets that should go to sslh instead have to be rerouted, so mark them accordingly (copying over the connection mark) - ip6tables -t mangle -A OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f - - # Configure routing for those marked packets - ip -6 rule add fwmark 0x1 lookup 100 - ip -6 route add local ::/0 dev lo table 100 - -Explanation: -To be able to use `localhost` as destination in your sslh config along with transparent proxying -you have to allow routing of loopback addresses as done above. -This is something you usually should not do (see [this stackoverflow post](https://serverfault.com/questions/656279/how-to-force-linux-to-accept-packet-with-loopback-ip/656484#656484)) -The two `DROP` iptables rules emulate the behaviour of `route_localnet` set to off (with one small difference: -allowing the reroute-check to happen after the fwmark is set on packets destined for sslh). -See [this diagram](https://upload.wikimedia.org/wikipedia/commons/3/37/Netfilter-packet-flow.svg) for a good visualisation -showing how packets will traverse the iptables chains. - -Note: -You have to run `sslh` as dedicated user (in this example the user is also named `sslh`), to not mess up with your normal networking. -These rules will allow you to connect directly to ssh on port -22 (or to any other service behind sslh) as well as through sslh on port 443. - -Also remember that iptables configuration and ip routes and -rules won't be necessarily persisted after you reboot. Make -sure to save them properly. For example in CentOS7, you would -do `iptables-save > /etc/sysconfig/iptables`, and add both -`ip` commands to your `/etc/rc.local`. - -FreeBSD: - -Given you have no firewall defined yet, you can use the following configuration -to have ipfw properly redirect traffic back to sslh - - /etc/rc.conf - firewall_enable="YES" - firewall_type="open" - firewall_logif="YES" - firewall_coscripts="/etc/ipfw/sslh.rules" - - -/etc/ipfw/sslh.rules - - #! /bin/sh - - # ssl - ipfw add 20000 fwd 192.0.2.1,443 log tcp from 192.0.2.1 8443 to any out - ipfw add 20010 fwd 2001:db8::1,443 log tcp from 2001:db8::1 8443 to any out - - # ssh - ipfw add 20100 fwd 192.0.2.1,443 log tcp from 192.0.2.1 8022 to any out - ipfw add 20110 fwd 2001:db8::1,443 log tcp from 2001:db8::1 8022 to any out - - # xmpp - ipfw add 20200 fwd 192.0.2.1,443 log tcp from 192.0.2.1 5222 to any out - ipfw add 20210 fwd 2001:db8::1,443 log tcp from 2001:db8::1 5222 to any out - - # openvpn (running on other internal system) - ipfw add 20300 fwd 192.0.2.1,443 log tcp from 198.51.100.7 1194 to any out - ipfw add 20310 fwd 2001:db8::1,443 log tcp from 2001:db8:1::7 1194 to any out - -General notes: - - -This will only work if `sslh` does not use any loopback -addresses (no `127.0.0.1` or `localhost`), you'll need to use -explicit IP addresses (or names): - - sslh --listen 192.168.0.1:443 --ssh 192.168.0.1:22 --tls 192.168.0.1:4443 - -This will not work: - - sslh --listen 192.168.0.1:443 --ssh 127.0.0.1:22 --tls 127.0.0.1:4443 - -Transparent proxying means the target server sees the real -origin address, so it means if the client connects using -IPv6, the server must also support IPv6. It is easy to -support both IPv4 and IPv6 by configuring the server -accordingly, and setting `sslh` to connect to a name that -resolves to both IPv4 and IPv6, e.g.: - - sslh --transparent --listen :443 --ssh insideaddr:22 - - /etc/hosts: - 192.168.0.1 insideaddr - 201::::2 insideaddr - -Upon incoming IPv6 connection, `sslh` will first try to -connect to the IPv4 address (which will fail), then connect -to the IPv6 address. +Set up can get complicated, so it has its own [document](tproxy.md). Systemd Socket Activation ------------------------- @@ -313,7 +167,7 @@ This parses the /etc/sslh.cfg (or /etc/sslh/sslh.cfg file if that exists instead) configuration file and dynamically generates a socket file to use. This will also merge with any sslh.socket.d drop in configuration but will be -overriden by a /etc/systemd/system/sslh.socket file. +overridden by a /etc/systemd/system/sslh.socket file. To use the generator place it in /usr/lib/systemd/system-generators and then call systemctl daemon-reload after any changes to /etc/sslh.cfg to generate @@ -335,13 +189,15 @@ UDP --- `sslh` can perform demultiplexing on UDP packets as well. -This only works with `sslh-select` (it is not possible to +This does not work with `sslh-fork` (it is not possible to support UDP with a forking model). Specify a listening address and target protocols with `is_udp: true`. `sslh` will wait for incoming UDP packets, run the probes in the usual fashion, and forward packets to the appropriate target. `sslh` will then remember the association between remote host to target server for 60 seconds by default, -which can be overriden with `udp_timeout`. This allows to +which can be overridden with `udp_timeout`. This allows to process both single-datagram protocols such as DNS, and connection-based protocols such as QUIC. + +An example for supporting QUIC is shown in `example.cfg`. diff --git a/doc/tproxy.md b/doc/tproxy.md index 3baac7f..057d720 100644 --- a/doc/tproxy.md +++ b/doc/tproxy.md @@ -1,17 +1,181 @@ -Transparent Proxy to Two Hosts -============================== +# Transparent proxy + +On Linux and FreeBSD you can use the `--transparent` option to +request transparent proxying. This means services behind `sslh` +(Apache, `sshd` and so on) will see the external IP and ports +as if the external world connected directly to them. This +simplifies IP-based access control (or makes it possible at +all). + +This document shows recipes that may help to do that. + +Note that getting this to work is very tricky and +detail-dependant: depending on whether the target server and +sslh are on the same machine, different machines, or +different dockers, and tool versions, all seem to change the +required network configuration somewhat. If it doesn't work, +it's almost certain that the problem is not linked to `sslh` +but to the network setup that surrounds it. If in trouble, +it might be worth trying to set up the network rules +with a simpler server than `sslh`, such as +[`socat`](http://www.dest-unreach.org/socat/) + + +Users have tried to do at least the following: + + * `sslh` and the target servers run on the same host (see [below](#transparent-proxy-to-one-host)) + * `sslh` runs on a host, and the target servers run on LXC or dockers running on the same host. No known working setup. + * `sslh` runs on a host, and the target servers run on different hosts on the same local network(see [below](#transparent-proxy-to-two-hosts)) + * `sslh` runs on a host, and the target servers run on a different host on a different network (there is a [usecase](https://github.com/yrutschle/sslh/issues/295) for this). No known working setup, and it's unclear it is possible. + + +## Transparent proxy to one host + +### Linux + +`sslh` needs extended rights to perform this: you'll need to +give it `CAP_NET_RAW` capabilities (see appropriate chapter) +or run it as root (but don't do that). + +The firewalling tables also need to be adjusted as follows. +I don't think it is possible to have `httpd` and `sslh` both listen to 443 in +this scheme -- let me know if you manage that: + + # Set route_localnet = 1 on all interfaces so that ssl can use "localhost" as destination + sysctl -w net.ipv4.conf.default.route_localnet=1 + sysctl -w net.ipv4.conf.all.route_localnet=1 + + # DROP martian packets as they would have been if route_localnet was zero + # Note: packets not leaving the server aren't affected by this, thus sslh will still work + iptables -t raw -A PREROUTING ! -i lo -d 127.0.0.0/8 -j DROP + iptables -t mangle -A POSTROUTING ! -o lo -s 127.0.0.0/8 -j DROP + + # Mark all connections made by ssl for special treatment (here sslh is run as user "sslh") + iptables -t nat -A OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f + + # Outgoing packets that should go to sslh instead have to be rerouted, so mark them accordingly (copying over the connection mark) + iptables -t mangle -A OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f + + # Configure routing for those marked packets + ip rule add fwmark 0x1 lookup 100 + ip route add local 0.0.0.0/0 dev lo table 100 + +Transparent proxying with IPv6 is similarly set up as follows: + + # Set route_localnet = 1 on all interfaces so that ssl can use "localhost" as destination + # Not sure if this is needed for ipv6 though + sysctl -w net.ipv4.conf.default.route_localnet=1 + sysctl -w net.ipv4.conf.all.route_localnet=1 + + # DROP martian packets as they would have been if route_localnet was zero + # Note: packets not leaving the server aren't affected by this, thus sslh will still work + ip6tables -t raw -A PREROUTING ! -i lo -d ::1/128 -j DROP + ip6tables -t mangle -A POSTROUTING ! -o lo -s ::1/128 -j DROP + + # Mark all connections made by ssl for special treatment (here sslh is run as user "sslh") + ip6tables -t nat -A OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f + + # Outgoing packets that should go to sslh instead have to be rerouted, so mark them accordingly (copying over the connection mark) + ip6tables -t mangle -A OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f + + # Configure routing for those marked packets + ip -6 rule add fwmark 0x1 lookup 100 + ip -6 route add local ::/0 dev lo table 100 + +Explanation: +To be able to use `localhost` as destination in your sslh config along with transparent proxying +you have to allow routing of loopback addresses as done above. +This is something you usually should not do (see [this stackoverflow post](https://serverfault.com/questions/656279/how-to-force-linux-to-accept-packet-with-loopback-ip/656484#656484)) +The two `DROP` iptables rules emulate the behaviour of `route_localnet` set to off (with one small difference: +allowing the reroute-check to happen after the fwmark is set on packets destined for sslh). +See [this diagram](https://upload.wikimedia.org/wikipedia/commons/3/37/Netfilter-packet-flow.svg) for a good visualisation +showing how packets will traverse the iptables chains. + +Note: +You have to run `sslh` as dedicated user (in this example the user is also named `sslh`), to not mess up with your normal networking. +These rules will allow you to connect directly to ssh on port +22 (or to any other service behind sslh) as well as through sslh on port 443. + +Also remember that iptables configuration and ip routes and +rules won't be necessarily persisted after you reboot. Make +sure to save them properly. For example in CentOS7, you would +do `iptables-save > /etc/sysconfig/iptables`, and add both +`ip` commands to your `/etc/rc.local`. + +### FreeBSD + +Given you have no firewall defined yet, you can use the following configuration +to have ipfw properly redirect traffic back to sslh + + /etc/rc.conf + firewall_enable="YES" + firewall_type="open" + firewall_logif="YES" + firewall_coscripts="/etc/ipfw/sslh.rules" + + +/etc/ipfw/sslh.rules + + #! /bin/sh + + # ssl + ipfw add 20000 fwd 192.0.2.1,443 log tcp from 192.0.2.1 8443 to any out + ipfw add 20010 fwd 2001:db8::1,443 log tcp from 2001:db8::1 8443 to any out + + # ssh + ipfw add 20100 fwd 192.0.2.1,443 log tcp from 192.0.2.1 8022 to any out + ipfw add 20110 fwd 2001:db8::1,443 log tcp from 2001:db8::1 8022 to any out + + # xmpp + ipfw add 20200 fwd 192.0.2.1,443 log tcp from 192.0.2.1 5222 to any out + ipfw add 20210 fwd 2001:db8::1,443 log tcp from 2001:db8::1 5222 to any out + + # openvpn (running on other internal system) + ipfw add 20300 fwd 192.0.2.1,443 log tcp from 198.51.100.7 1194 to any out + ipfw add 20310 fwd 2001:db8::1,443 log tcp from 2001:db8:1::7 1194 to any out + +General notes: + + +This will only work if `sslh` does not use any loopback +addresses (no `127.0.0.1` or `localhost`), you'll need to use +explicit IP addresses (or names): + + sslh --listen 192.168.0.1:443 --ssh 192.168.0.1:22 --tls 192.168.0.1:4443 + +This will not work: + + sslh --listen 192.168.0.1:443 --ssh 127.0.0.1:22 --tls 127.0.0.1:4443 + +Transparent proxying means the target server sees the real +origin address, so it means if the client connects using +IPv6, the server must also support IPv6. It is easy to +support both IPv4 and IPv6 by configuring the server +accordingly, and setting `sslh` to connect to a name that +resolves to both IPv4 and IPv6, e.g.: + + sslh --transparent --listen :443 --ssh insideaddr:22 + + /etc/hosts: + 192.168.0.1 insideaddr + 201::::2 insideaddr + +Upon incoming IPv6 connection, `sslh` will first try to +connect to the IPv4 address (which will fail), then connect +to the IPv6 address. + + +## Transparent Proxy to Two Hosts Tutorial by Sean Warner. 19 June 2019 20:35 -Aim ---- +### Aim * Show that `sslh` can transparently proxy requests from the internet to services on two separate hosts that are both on the same LAN. * The IP address of the client initiating the request is what the destination should see… and not the IP address of the host that `sslh` is running on, which is what happens when `sslh` is not running in transparent mode. * The solution here only works for my very specific use-case but hopefully others can adapt it to suits their needs. -Overview of my Network ----------------------- +### Overview of my Network Two Raspberry Pis on my home LAN: * Pi A: 192.168.1.124 – `sslh` (Port 4433), Apache2 web server for https (port 443), `stunnel` (port 4480) to decrypt ssh traffic and forward to SSH server (also on Pi A at Port 1022) @@ -20,8 +184,7 @@ Two Raspberry Pis on my home LAN: ![Architecture](tproxy.svg) -`sslh` build ------------- +### `sslh` build   `sslh` Version: sslh v1.19c-2-gf451cc8-dirty. @@ -47,8 +210,7 @@ MAN=sslh.8.gz         # man page name # itself ```   -systemd setup -------------- +### systemd setup Create an sslh systemd service file... ``` @@ -83,8 +245,7 @@ Start it again to test… # systemctl start sslh ```   -Configure `sslh` ----------------- +### Configure `sslh` First stop `sslh` then open the config file and replace with below, save and start `sslh` again ``` @@ -123,8 +284,7 @@ protocols: ); ```   -Configure `stunnel` -------------------- +### Configure `stunnel` First stop `stunnel` then open the config file and replace with below, save and start `stunnel` again ``` @@ -151,8 +311,7 @@ connect = 192.168.1.124:1022 TIMEOUTclose = 0 ```   -Configure iptables for Pi A --------------------------- +### Configure iptables for Pi A The `_add.sh` script creates the rules, the `_rm.sh` script removes the rules. They will be lost if you reboot but there are ways to make them load again on start-up.. @@ -194,8 +353,7 @@ Now run the "add" script on Pi A! # piA_tproxy_rm.sh ``` -Configure iptables for Pi B --------------------------- +## Configure iptables for Pi B ``` # nano /usr/local/sbin/piB_tproxy_add.sh @@ -235,8 +393,8 @@ Now run the "add" script on Pi B! # piB_tproxy_rm.sh ```   -Testing -------- +### Testing + * Getting to sshd on PiA I did this test using 4G from my phone (outside the LAN) diff --git a/echo_test.cfg b/echo_test.cfg new file mode 100644 index 0000000..5d88e70 --- /dev/null +++ b/echo_test.cfg @@ -0,0 +1,12 @@ + +# TODO: c2s does not warn if udp: 1 (instead of 'true') + +udp: true; + +prefix: "hello"; + +listen: "localhost:9000"; + +listen-host: "localhost"; +listen-port: "9000"; + diff --git a/echosrv-conf.c b/echosrv-conf.c index 38ddc3b..87a46e4 100644 --- a/echosrv-conf.c +++ b/echosrv-conf.c @@ -1,5 +1,5 @@ /* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README) - * on Fri Aug 13 18:03:20 2021. + * on Sat Apr 30 09:55:03 2022. # conf2struct: generate libconf parsers that read to structs # Copyright (C) 2018-2021 Yves Rutschle @@ -365,7 +365,7 @@ static int clcpy(config_type type, void* target, const void* cl_arg) return 0; } -/* Copy the value of a string argument to arbitary memory +/* Copy the value of a string argument to arbitrary memory * location that must be large enough, converting on the way * (i.e. CFG_INT gets atoi() and so on) */ /* 0: success @@ -862,7 +862,7 @@ static int set_target_fields(void* target_addr, struct compound_cl_arg* arg, con if (pmatch[pmatch_cnt].rm_so == -1) { /* This should not happen as regexec() did * match before, unless there is a - * discrepency between the regex and the + * discrepancy between the regex and the * number of backreferences */ return 0; } @@ -1155,7 +1155,7 @@ static void scalar_to_string(char** strp, config_setting_t* s) /* Typesets all the settings in a configuration as a * newly-allocated string. The string management is caller's -* responsability. +* responsibility. * Returns the number of scalars in the configuration */ static int cfg_as_string(config_setting_t* parent, const char* path, char** strp) { diff --git a/echosrv-conf.h b/echosrv-conf.h index 4f49e9e..ec99846 100644 --- a/echosrv-conf.h +++ b/echosrv-conf.h @@ -1,5 +1,5 @@ /* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README) - * on Fri Aug 13 18:03:20 2021. + * on Sat Apr 30 09:55:03 2022. # conf2struct: generate libconf parsers that read to structs # Copyright (C) 2018-2021 Yves Rutschle diff --git a/echosrv.c b/echosrv.c index 4d22eb4..6b513eb 100644 --- a/echosrv.c +++ b/echosrv.c @@ -1,6 +1,6 @@ /* echosrv: a simple line echo server with optional prefix adding. * - * echsrv --listen localhost6:1234 --prefix "ssl: " + * echosrv --listen localhost6:1234 --prefix "ssl: " * * This will bind to 1234, and echo every line pre-pending "ssl: ". This is * used for testing: we create several such servers with different prefixes, @@ -100,7 +100,10 @@ void tcp_echo(struct listen_endpoint* listen_socket) { while (1) { int in_socket = accept(listen_socket->socketfd, 0, 0); - CHECK_RES_DIE(in_socket, "accept"); + if (in_socket == -1) { + perror("tcp_echo:accept"); + exit(1); + } if (!fork()) { @@ -109,6 +112,7 @@ void tcp_echo(struct listen_endpoint* listen_socket) exit(0); } close(in_socket); + waitpid(-1, NULL, WNOHANG); } } diff --git a/echoѕrv-conf.h b/echoѕrv-conf.h new file mode 100644 index 0000000..58701a7 --- /dev/null +++ b/echoѕrv-conf.h @@ -0,0 +1,109 @@ +/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README) + * on Sat Nov 7 09:19:26 2020. + +# conf2struct: generate libconf parsers that read to structs +# Copyright (C) 2018-2019 Yves Rutschle +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +*/ + +#ifndef C2S_SSLHCFG_H +#define C2S_SSLHCFG_H +#ifdef LIBCONFIG +# include +#endif + + +#include "probe.h" +#include +#include +#include + +struct sslhcfg_listen_item { + char* host; + char* port; + int keepalive; +}; + +struct sslhcfg_protocols_item { + char* name; + char* host; + char* port; + int service_is_present; + char* service; + int fork; + int tfo_ok; + int log_level; + int keepalive; + size_t sni_hostnames_len; + char** sni_hostnames; + size_t alpn_protocols_len; + char** alpn_protocols; + size_t regex_patterns_len; + char** regex_patterns; + int minlength_is_present; + int minlength; + T_PROBE* probe; + struct addrinfo* saddr; + void* data; +}; + +struct sslhcfg_item { + char* prefix; + int verbose; + int foreground; + int inetd; + int numeric; + int transparent; + int timeout; + int user_is_present; + char* user; + int pidfile_is_present; + char* pidfile; + int chroot_is_present; + char* chroot; + char* syslog_facility; + char* on_timeout; + size_t listen_len; + struct sslhcfg_listen_item* listen; + size_t protocols_len; + struct sslhcfg_protocols_item* protocols; +}; + +int sslhcfg_parse_file( + const char* filename, + struct sslhcfg_item* sslhcfg, + const char** errmsg); + +void sslhcfg_fprint( + FILE* out, + struct sslhcfg_item *sslhcfg, + int depth); + +int sslhcfg_cl_parse( + int argc, + char* argv[], + struct sslhcfg_item *sslhcfg); + +#endif diff --git a/example.cfg b/example.cfg index 4b70d48..b818e89 100644 --- a/example.cfg +++ b/example.cfg @@ -3,7 +3,6 @@ # not be used as a starting point for a working # configuration. Instead use basic.cfg. -verbose: 0; foreground: true; inetd: false; numeric: false; @@ -13,6 +12,30 @@ user: "nobody"; pidfile: "/var/run/sslh.pid"; chroot: "/var/empty"; +# Logging configuration +# Value: 1: stdout; 2: syslog; 3: stdout+syslog; 4: logfile; ...; 7: all +# Defaults are indicated here, and should be sensible. Generally, you want *-error +# to be always enabled, to know if something is going wrong. +verbose-config: 0; # print configuration at startup +verbose-config-error: 3; # print configuration errors +verbose-connections: 3; # trace established incoming address to forward address +verbose-connections-error: 3; # connection errors +verbose-connections-try: 0; # connection attempts towards targets +verbose-fd: 0; # file descriptor activity, open/close/whatnot +verbose-packets: 0; # hexdump packets on which probing is done +verbose-probe-info: 0; # what's happening during the probe process +verbose-probe-error: 3; # failures and problems during probing +verbose-system-error: 3; # system call problem, i.e. malloc, fork, failing +verbose-int-error: 3; # internal errors, the kind that should never happen + + +# Specify a path to the logfile. +#logfile: "/var/log/sslh.log" + +# Specify the number of concurrent UDP connection that can +# be managed (default 1024) +udp_max_connections: 16; + # Specify which syslog facility to use (names for your # system are usually defined in /usr/include/*/sys/syslog.h # or equivalent) @@ -44,6 +67,8 @@ listen: # fork: Should a new process be forked for this protocol? # (only useful for sslh-select) # tfo_ok: Set to true if the server supports TCP FAST OPEN +# resolve_on_forward: Set to true if server address should be resolved on +# (every) newly incoming connection (again) # transparent: Set to true to proxy this protocol # transparently (server sees the remote client IP # address). Same as the global option, but per-protocol @@ -98,7 +123,7 @@ protocols: udp_timeout: 20; # Time after which the "connection" is forgotten regex_patterns: [ "hello" ]; }, # Forward Teamspeak3 (Voice only) - { name: "regex"; host: "localhost"; is_udp: true; port: "9987"; regex_patterns: [ "TS3INIT1" ]; }, + { name: "teamspeak"; host: "localhost"; is_udp: true; port: "9987"; }, # Forward IETF QUIC-50 ("Q050" -> "\x51\x30\x35\x30") # Remember that the regex needs to be adjusted for every supported QUIC version. { name: "regex"; host: "localhost"; is_udp: true; port: "4433"; regex_patterns: [ "\x51\x30\x35\x30" ]; }, diff --git a/gap.c b/gap.c index f3f71d2..3b3f98c 100644 --- a/gap.c +++ b/gap.c @@ -31,19 +31,14 @@ #include "gap.h" -typedef struct gap_array { - int len; /* Number of elements in array */ - void** array; -} gap_array; - /* Allocate one page-worth of elements */ static int gap_len_alloc(int elem_size) { return getpagesize() / elem_size; } -/* Creates a new gap, all pointers are initialised at NULL */ -gap_array* gap_init(void) +/* Creates a new gap at least `len` big, all pointers are initialised at NULL */ +gap_array* gap_init(int len) { gap_array* gap = malloc(sizeof(*gap)); if (!gap) return NULL; @@ -51,8 +46,12 @@ gap_array* gap_init(void) int elem_size = sizeof(gap->array[0]); gap->len = gap_len_alloc(elem_size); + if (gap->len < len) gap->len = len; gap->array = malloc(gap->len * elem_size); - if (!gap->array) return NULL; + if (!gap->array) { + free(gap); + return NULL; + } for (int i = 0; i < gap->len; i++) gap->array[i] = NULL; @@ -60,12 +59,7 @@ gap_array* gap_init(void) return gap; } -void* gap_get(gap_array* gap, int index) -{ - return gap->array[index]; -} - -static int gap_extend(gap_array* gap) +int gap_extend(gap_array* gap) { int elem_size = sizeof(gap->array[0]); int new_length = gap->len + gap_len_alloc(elem_size); @@ -83,17 +77,6 @@ static int gap_extend(gap_array* gap) return 0; } -int gap_set(gap_array* gap, int index, void* ptr) -{ - if (index >= gap->len) { - int res = gap_extend(gap); - if (res == -1) return -1; - } - - gap->array[index] = ptr; - return 0; -} - void gap_destroy(gap_array* gap) { free(gap->array); @@ -119,7 +102,7 @@ int gap_remove_ptr(gap_array* gap, void* ptr, int len) else return -1; - for (i = start; i < len; i++) { + for (i = start; i < len - 1; i++) { gap->array[i] = gap->array[i+1]; } diff --git a/gap.h b/gap.h index 8044673..26690a6 100644 --- a/gap.h +++ b/gap.h @@ -3,11 +3,41 @@ typedef struct gap_array gap_array; -gap_array* gap_init(); -void* gap_get(gap_array* gap, int index); -int gap_set(gap_array* gap, int index, void* ptr); +gap_array* gap_init(int len); +static void* gap_get(gap_array* gap, int index); +static int gap_set(gap_array* gap, int index, void* ptr); void gap_destroy(gap_array* gap); int gap_remove_ptr(gap_array* gap, void* ptr, int len); +/* Private declarations to allow inlining. + * Don't assume my implementation. */ +typedef struct gap_array { + int len; /* Number of elements in array */ + void** array; +} gap_array; + +int gap_extend(gap_array* gap); + +static inline int __attribute__((unused)) gap_set(gap_array* gap, int index, void* ptr) +{ + while (index >= gap->len) { + int res = gap_extend(gap); + if (res == -1) return -1; + } + + gap->array[index] = ptr; + return 0; +} + +static inline void* __attribute__((unused)) gap_get(gap_array* gap, int index) +{ + /* sslh-ev routinely reads before it writes. It's not clear if it should be + * its job to check the length (and add a gap_getlen()), or if it should be + * gap_get()'s job. This will do for now */ + if (index >= gap->len) return NULL; + + return gap->array[index]; +} + #endif diff --git a/genver.sh b/genver.sh index 5eec682..e164466 100755 --- a/genver.sh +++ b/genver.sh @@ -10,14 +10,14 @@ fi if [ ! -d .git ] || ! `(git status | grep -q "On branch") 2> /dev/null`; then # If we don't have git, we can't work out what # version this is. It must have been downloaded as a - # zip file. - + # zip file. + # If downloaded from the release page, the directory # has the version number. release=`pwd | sed s/.*sslh-// | grep "[[:digit:]]"` - + if [ "x$release" = "x" ]; then - # If downloaded from the head, Github creates the + # If downloaded from the head, GitHub creates the # zip file with all files dated from the last # change: use the Makefile's modification time as a # release number @@ -28,7 +28,7 @@ fi if [ -d .git ] && head=`git rev-parse --verify HEAD 2>/dev/null`; then # generate the version info based on the tag release=`(git describe --tags || git --describe || git describe --all --long) \ - 2>/dev/null | tr -d '\n'` + 2>/dev/null | tr -s '/' '-' | tr -d '\n'` # Are there uncommitted changes? git update-index --refresh --unmerged > /dev/null diff --git a/hash.c b/hash.c new file mode 100644 index 0000000..608a552 --- /dev/null +++ b/hash.c @@ -0,0 +1,224 @@ +/* + * a fixed-sized hash + * +# Copyright (C) 2022 Yves Rutschle +# +# This program is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be +# useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more +# details. +# +# The full text for the General Public License is here: +# http://www.gnu.org/licenses/gpl.html +# +# */ + + +/* * The hash is open-addressing, linear search, robin-hood insertion, with + * backward shift deletion. References: + * https://codecapsule.com/2013/11/11/robin-hood-hashing/ + * https://codecapsule.com/2013/11/17/robin-hood-hashing-backward-shift-deletion/ + * This means items are reordered upon insertion and deletion, and the hash + * is well-ordered at all times with no tombstones. + * + * Each pointer is either: + * - to a connection struct + * - FREE (NULL) if not allocated + * + * */ + +#include +#include + +#include "gap.h" + +typedef void* hash_item; +#include "hash.h" + +static void* const FREE = NULL; + +struct hash { + int hash_size; /* Max number of items in the hash */ + int item_cnt; /* Number of items in the hash */ + gap_array* data; + + hash_make_key_fn hash_make_key; + hash_cmp_item_fn cmp_item; +}; + +typedef struct hash hash; + + +static int hash_make_key(hash* h, hash_item item) +{ + return h->hash_make_key(item) % h->hash_size; +} + + +hash* hash_init(int hash_size, hash_make_key_fn make_key, hash_cmp_item_fn cmp_item) +{ + hash* h = malloc(sizeof(*h)); + if (!h) return NULL; + + h->hash_size = hash_size; + h->item_cnt = 0; + h->data = gap_init(hash_size); + h->hash_make_key = make_key; + h->cmp_item = cmp_item; + + return h; +} + +/* Return the index following i in h */ +static int hash_next_index(hash* h, int i) +{ + return (i + 1) % h->hash_size; +} + +/* Returns the index in h of specified address, -1 if not found + * item is an item object that must return the target wanted index and for + * which comparison with the searched object will succeed. + * */ +static int hash_find_index(hash* h, hash_item item) +{ + hash_item cnx; + int index = hash_make_key(h, item); + int cnt = 0; + + cnx = gap_get(h->data, index); +#ifdef DEBUG + fprintf(stderr, "searching %d\n", index); +#endif + while (cnx != FREE) { + if (cnt++ > h->hash_size) return -1; + + if (!h->cmp_item(cnx, item)) + break; + + index = hash_next_index(h, index); + cnx = gap_get(h->data, index); +#ifdef DEBUG + fprintf(stderr, "searching %d\n", index); +#endif + } + if (cnx == FREE) return -1; + return index; +} + +hash_item hash_find(hash* h, hash_item item) +{ + int index = hash_find_index(h, item); + if (index == -1) return NULL; + hash_item out = gap_get(h->data, index); + return out; +} + + +/* Returns DIB: distance to initial bucket */ +static int distance(int current_index, hash* h, hash_item item) +{ + int wanted_index = hash_make_key(h, item); + if (wanted_index <= current_index) + return current_index - wanted_index; + else + return current_index - wanted_index + h->hash_size; +} + + +int hash_insert(hash* h, hash_item new) +{ + int bubble_wanted_index = hash_make_key(h, new); + int index = bubble_wanted_index; + gap_array* hash = h->data; + + if (h->item_cnt == h->hash_size) + return -1; + + hash_item curr_item = gap_get(hash, index); + while (curr_item) { + if (distance(index, h, curr_item) < distance(index, h, new)) { + gap_set(h->data, index, new); +#if DEBUG + fprintf(stderr, "intermediate insert [%s] at %d\n", &new->client_addr, index); +#endif + new = curr_item; + } + + index = hash_next_index(h, index); + curr_item = gap_get(hash, index); + } + +#if DEBUG + fprintf(stderr, "final insert at %d\n", index); +#endif + gap_set(hash, index, new); + h->item_cnt++; + + return 0; +} + +/* Remove cnx from the hash */ +int hash_remove(hash* h, hash_item item) +{ + gap_array* hash = h->data; + + int index = hash_find_index(h, item); + if (index == -1) return -1; /* Tried to remove something that isn't there */ + + while (1) { + int next_index = hash_next_index(h, index); + hash_item next = gap_get(h->data, next_index); + if ((next == FREE) || (distance(next_index, h, next) == 0)) { + h->item_cnt--; + gap_set(hash, index, FREE); + return 0; + } + + gap_set(hash, index, next); + + index = hash_next_index(h, index);; + } + return 0; +} + +#if HASH_TESTING +#include +#include +#define STR_LENGTH 16 +struct hash_item { + int wanted_index; + char str[STR_LENGTH]; +}; +void hash_dump(hash* h, char* filename) +{ + char str[STR_LENGTH]; + FILE* out = fopen(filename, "w"); + + if (!out) { + perror(filename); + exit(1); + } + + fprintf(out, "\n", h->item_cnt); + for (int i = 0; i < h->hash_size; i++) { + hash_item item = gap_get(h->data, i); + int idx = 0; + + memset(str, 0, STR_LENGTH); + if (item) { + idx = hash_make_key(h, item); + memcpy(str, ((struct hash_item*)item)->str, STR_LENGTH); + } + fprintf(out, "\t%d:%d:%s\n", i, idx, str); + } + fprintf(out, "\n"); + fclose(out); +} +#endif diff --git a/hash.h b/hash.h new file mode 100644 index 0000000..66d79ab --- /dev/null +++ b/hash.h @@ -0,0 +1,28 @@ +#ifndef HASH_H +#define HASH_H + +/* You will need to typedef a pointer type to hash_item before including this + * .h */ + +typedef struct hash hash; + +/* Function that returns a key (index) for a given item. The key must be always + * the same for an item. It doesn't need to be bounded (hash.c masks it for you) */ +typedef int (*hash_make_key_fn)(hash_item item); + +/* Function that compares two items: returns 0 if they are the same */ +typedef int (*hash_cmp_item_fn)(hash_item item1, hash_item item2); + +hash* hash_init(int hash_size, hash_make_key_fn make_key, hash_cmp_item_fn cmp_item); + +int hash_insert(hash* h, hash_item new); +int hash_remove(hash* h, hash_item item); + +/* Returns the hash item that matches specification (meaning the + * comparison function returns true for cmp(x, item), or NULL if not found */ +hash_item hash_find(hash* h, hash_item item); + + +void hash_dump(hash* h, char* filename); /* For development only */ + +#endif diff --git a/hashtest/Makefile b/hashtest/Makefile new file mode 100644 index 0000000..9cb67c6 --- /dev/null +++ b/hashtest/Makefile @@ -0,0 +1,7 @@ + +CFLAGS=-DHASH_TESTING -O2 -Wall +OBJ=../hash.o ../gap.o htest.o + +htest: $(OBJ) + $(CC) -o htest $(OBJ) + diff --git a/hashtest/delete.tst b/hashtest/delete.tst new file mode 100644 index 0000000..5cee269 --- /dev/null +++ b/hashtest/delete.tst @@ -0,0 +1,8 @@ +# Basic delete +a 10 aa +a 10 ab +a 10 ac +a 20 ba +a 21 bb + +d 21 bb diff --git a/hashtest/delete.tst.ref b/hashtest/delete.tst.ref new file mode 100644 index 0000000..ca930bb --- /dev/null +++ b/hashtest/delete.tst.ref @@ -0,0 +1,34 @@ + + 0:0: + 1:0: + 2:0: + 3:0: + 4:0: + 5:0: + 6:0: + 7:0: + 8:0: + 9:0: + 10:10:aa + 11:10:ab + 12:10:ac + 13:0: + 14:0: + 15:0: + 16:0: + 17:0: + 18:0: + 19:0: + 20:20:ba + 21:0: + 22:0: + 23:0: + 24:0: + 25:0: + 26:0: + 27:0: + 28:0: + 29:0: + 30:0: + 31:0: + diff --git a/hashtest/delete_at_end.tst b/hashtest/delete_at_end.tst new file mode 100644 index 0000000..e89ec12 --- /dev/null +++ b/hashtest/delete_at_end.tst @@ -0,0 +1,9 @@ +# Delete inside a block with nothing after + +a 10 aa +a 10 ab +a 12 ac +a 13 ad +a 14 ae + +d 14 ae diff --git a/hashtest/delete_at_end.tst.ref b/hashtest/delete_at_end.tst.ref new file mode 100644 index 0000000..c9ba1e5 --- /dev/null +++ b/hashtest/delete_at_end.tst.ref @@ -0,0 +1,34 @@ + + 0:0: + 1:0: + 2:0: + 3:0: + 4:0: + 5:0: + 6:0: + 7:0: + 8:0: + 9:0: + 10:10:aa + 11:10:ab + 12:12:ac + 13:13:ad + 14:0: + 15:0: + 16:0: + 17:0: + 18:0: + 19:0: + 20:0: + 21:0: + 22:0: + 23:0: + 24:0: + 25:0: + 26:0: + 27:0: + 28:0: + 29:0: + 30:0: + 31:0: + diff --git a/hashtest/delete_below_floor.tst b/hashtest/delete_below_floor.tst new file mode 100644 index 0000000..432b5b9 --- /dev/null +++ b/hashtest/delete_below_floor.tst @@ -0,0 +1,9 @@ +# wrap-around and delete below floor +a 2 ba +a 30 aa +a 30 ab +a 30 ac +a 30 ad +a 2 bb + +d 30 ab diff --git a/hashtest/delete_below_floor.tst.ref b/hashtest/delete_below_floor.tst.ref new file mode 100644 index 0000000..d256c1f --- /dev/null +++ b/hashtest/delete_below_floor.tst.ref @@ -0,0 +1,34 @@ + + 0:30:ad + 1:0: + 2:2:ba + 3:2:bb + 4:0: + 5:0: + 6:0: + 7:0: + 8:0: + 9:0: + 10:0: + 11:0: + 12:0: + 13:0: + 14:0: + 15:0: + 16:0: + 17:0: + 18:0: + 19:0: + 20:0: + 21:0: + 22:0: + 23:0: + 24:0: + 25:0: + 26:0: + 27:0: + 28:0: + 29:0: + 30:30:aa + 31:30:ac + diff --git a/hashtest/delete_discont.tst b/hashtest/delete_discont.tst new file mode 100644 index 0000000..f27d8dc --- /dev/null +++ b/hashtest/delete_discont.tst @@ -0,0 +1,10 @@ +# delete in a discontinuous block + +a 10 aa +a 11 ab +a 12 ac +a 14 ad +a 10 bc + +d 11 ab + diff --git a/hashtest/delete_discont.tst.ref b/hashtest/delete_discont.tst.ref new file mode 100644 index 0000000..18b76f8 --- /dev/null +++ b/hashtest/delete_discont.tst.ref @@ -0,0 +1,34 @@ + + 0:0: + 1:0: + 2:0: + 3:0: + 4:0: + 5:0: + 6:0: + 7:0: + 8:0: + 9:0: + 10:10:aa + 11:10:bc + 12:12:ac + 13:0: + 14:14:ad + 15:0: + 16:0: + 17:0: + 18:0: + 19:0: + 20:0: + 21:0: + 22:0: + 23:0: + 24:0: + 25:0: + 26:0: + 27:0: + 28:0: + 29:0: + 30:0: + 31:0: + diff --git a/hashtest/delete_empty.tst b/hashtest/delete_empty.tst new file mode 100644 index 0000000..ce29d4d --- /dev/null +++ b/hashtest/delete_empty.tst @@ -0,0 +1,11 @@ +# Delete an unexisting element. And on an empty hash + +a 10 aa + +d 10 ab +d 12 bc + +# Empty for real +d 10 aa + +d 10 aa diff --git a/hashtest/delete_empty.tst.ref b/hashtest/delete_empty.tst.ref new file mode 100644 index 0000000..c7539cf --- /dev/null +++ b/hashtest/delete_empty.tst.ref @@ -0,0 +1,34 @@ + + 0:0: + 1:0: + 2:0: + 3:0: + 4:0: + 5:0: + 6:0: + 7:0: + 8:0: + 9:0: + 10:0: + 11:0: + 12:0: + 13:0: + 14:0: + 15:0: + 16:0: + 17:0: + 18:0: + 19:0: + 20:0: + 21:0: + 22:0: + 23:0: + 24:0: + 25:0: + 26:0: + 27:0: + 28:0: + 29:0: + 30:0: + 31:0: + diff --git a/hashtest/delete_full.tst b/hashtest/delete_full.tst new file mode 100644 index 0000000..f950d38 --- /dev/null +++ b/hashtest/delete_full.tst @@ -0,0 +1,39 @@ +# delete on a full hash + +# First, fill the hash :-) + +a 0 aa +a 1 ab +a 2 ac +a 3 ad +a 4 ae +a 5 af +a 6 ag +a 7 ah +a 8 ai +a 9 af +a 10 ba +a 11 bb +a 12 bc +a 13 bd +a 14 be +a 15 bf +a 16 bg +a 17 bh +a 18 bi +a 19 bj +a 20 ca +a 21 cb +a 22 cd +a 23 ce +a 24 cf +a 25 cg +a 26 ch +a 27 ci +a 28 cj +a 29 ck +a 30 da +a 31 db + + +d 21 cb diff --git a/hashtest/delete_full.tst.ref b/hashtest/delete_full.tst.ref new file mode 100644 index 0000000..3cc373a --- /dev/null +++ b/hashtest/delete_full.tst.ref @@ -0,0 +1,34 @@ + + 0:0:aa + 1:1:ab + 2:2:ac + 3:3:ad + 4:4:ae + 5:5:af + 6:6:ag + 7:7:ah + 8:8:ai + 9:9:af + 10:10:ba + 11:11:bb + 12:12:bc + 13:13:bd + 14:14:be + 15:15:bf + 16:16:bg + 17:17:bh + 18:18:bi + 19:19:bj + 20:20:ca + 21:0: + 22:22:cd + 23:23:ce + 24:24:cf + 25:25:cg + 26:26:ch + 27:27:ci + 28:28:cj + 29:29:ck + 30:30:da + 31:31:db + diff --git a/hashtest/delete_middle.tst b/hashtest/delete_middle.tst new file mode 100644 index 0000000..29b0771 --- /dev/null +++ b/hashtest/delete_middle.tst @@ -0,0 +1,10 @@ +# Delete inside a block with something discontinuous + +a 10 aa +a 10 ab +a 12 ac +a 13 ad +a 14 ae + +# ab shifts, ac and next doesn't +d 10 aa diff --git a/hashtest/delete_middle.tst.ref b/hashtest/delete_middle.tst.ref new file mode 100644 index 0000000..1d27ad8 --- /dev/null +++ b/hashtest/delete_middle.tst.ref @@ -0,0 +1,34 @@ + + 0:0: + 1:0: + 2:0: + 3:0: + 4:0: + 5:0: + 6:0: + 7:0: + 8:0: + 9:0: + 10:10:ab + 11:0: + 12:12:ac + 13:13:ad + 14:14:ae + 15:0: + 16:0: + 17:0: + 18:0: + 19:0: + 20:0: + 21:0: + 22:0: + 23:0: + 24:0: + 25:0: + 26:0: + 27:0: + 28:0: + 29:0: + 30:0: + 31:0: + diff --git a/hashtest/delete_wrap.tst b/hashtest/delete_wrap.tst new file mode 100644 index 0000000..cea8800 --- /dev/null +++ b/hashtest/delete_wrap.tst @@ -0,0 +1,8 @@ +# Basic delete when wrapping, between wrap and floor +a 30 aa +a 30 ab +a 30 ac +a 30 ba +a 30 bb + +d 30 ac diff --git a/hashtest/delete_wrap.tst.ref b/hashtest/delete_wrap.tst.ref new file mode 100644 index 0000000..aabc8bd --- /dev/null +++ b/hashtest/delete_wrap.tst.ref @@ -0,0 +1,34 @@ + + 0:30:ba + 1:30:bb + 2:0: + 3:0: + 4:0: + 5:0: + 6:0: + 7:0: + 8:0: + 9:0: + 10:0: + 11:0: + 12:0: + 13:0: + 14:0: + 15:0: + 16:0: + 17:0: + 18:0: + 19:0: + 20:0: + 21:0: + 22:0: + 23:0: + 24:0: + 25:0: + 26:0: + 27:0: + 28:0: + 29:0: + 30:30:aa + 31:30:ab + diff --git a/hashtest/delete_wrap_at_end.tst b/hashtest/delete_wrap_at_end.tst new file mode 100644 index 0000000..e2b1ce1 --- /dev/null +++ b/hashtest/delete_wrap_at_end.tst @@ -0,0 +1,10 @@ +# Delete inside a block with wrapping, with something after + +a 30 aa +a 30 ab +a 30 ac +a 1 ad +a 3 ae + +# shift ad but not ae +d 14 ae diff --git a/hashtest/delete_wrap_at_end.tst.ref b/hashtest/delete_wrap_at_end.tst.ref new file mode 100644 index 0000000..aa6683a --- /dev/null +++ b/hashtest/delete_wrap_at_end.tst.ref @@ -0,0 +1,34 @@ + + 0:30:ac + 1:1:ad + 2:0: + 3:3:ae + 4:0: + 5:0: + 6:0: + 7:0: + 8:0: + 9:0: + 10:0: + 11:0: + 12:0: + 13:0: + 14:0: + 15:0: + 16:0: + 17:0: + 18:0: + 19:0: + 20:0: + 21:0: + 22:0: + 23:0: + 24:0: + 25:0: + 26:0: + 27:0: + 28:0: + 29:0: + 30:30:aa + 31:30:ab + diff --git a/hashtest/delete_wrap_below_floor.tst b/hashtest/delete_wrap_below_floor.tst new file mode 100644 index 0000000..7fa5038 --- /dev/null +++ b/hashtest/delete_wrap_below_floor.tst @@ -0,0 +1,8 @@ +# delete before wrap +a 30 aa +a 30 ab +a 30 ac +a 30 ad + +# shift ac and ad +d 30 ab diff --git a/hashtest/delete_wrap_below_floor.tst.ref b/hashtest/delete_wrap_below_floor.tst.ref new file mode 100644 index 0000000..c47ff54 --- /dev/null +++ b/hashtest/delete_wrap_below_floor.tst.ref @@ -0,0 +1,34 @@ + + 0:30:ad + 1:0: + 2:0: + 3:0: + 4:0: + 5:0: + 6:0: + 7:0: + 8:0: + 9:0: + 10:0: + 11:0: + 12:0: + 13:0: + 14:0: + 15:0: + 16:0: + 17:0: + 18:0: + 19:0: + 20:0: + 21:0: + 22:0: + 23:0: + 24:0: + 25:0: + 26:0: + 27:0: + 28:0: + 29:0: + 30:30:aa + 31:30:ac + diff --git a/hashtest/delete_wrap_discont.tst b/hashtest/delete_wrap_discont.tst new file mode 100644 index 0000000..1c49746 --- /dev/null +++ b/hashtest/delete_wrap_discont.tst @@ -0,0 +1,11 @@ +# Delete with wrapping in discontinuous group + +a 30 aa +a 30 ab +a 30 ac +a 31 ad +a 2 ba +a 3 bb + +# shift ac and ad but not ba and bb +d 30 ab diff --git a/hashtest/delete_wrap_discont.tst.ref b/hashtest/delete_wrap_discont.tst.ref new file mode 100644 index 0000000..d734495 --- /dev/null +++ b/hashtest/delete_wrap_discont.tst.ref @@ -0,0 +1,34 @@ + + 0:31:ad + 1:0: + 2:2:ba + 3:3:bb + 4:0: + 5:0: + 6:0: + 7:0: + 8:0: + 9:0: + 10:0: + 11:0: + 12:0: + 13:0: + 14:0: + 15:0: + 16:0: + 17:0: + 18:0: + 19:0: + 20:0: + 21:0: + 22:0: + 23:0: + 24:0: + 25:0: + 26:0: + 27:0: + 28:0: + 29:0: + 30:30:aa + 31:30:ac + diff --git a/hashtest/htest b/hashtest/htest new file mode 100755 index 0000000..90a62e3 Binary files /dev/null and b/hashtest/htest differ diff --git a/hashtest/htest.c b/hashtest/htest.c new file mode 100644 index 0000000..dacb27e --- /dev/null +++ b/hashtest/htest.c @@ -0,0 +1,109 @@ +/* Wee testing program from the hash code: + * htest