├── .github └── workflows │ └── container-build.yaml ├── .gitignore ├── COPYING ├── ChangeLog ├── Dockerfile ├── Makefile.in ├── README.md ├── TODO ├── argtable3.c ├── argtable3.h ├── basic.cfg ├── collection.c ├── collection.h ├── common.c ├── common.h ├── config.h.in ├── configure ├── configure.ac ├── container-entrypoint.sh ├── doc ├── Daisy-Chaining-Transparency-Explained.md ├── FAQ.md ├── INSTALL.md ├── README.MacOSX ├── README.Windows.md ├── config.md ├── detailed-ip-transparency.png ├── detailed-ip-transparency.svg ├── podman.md ├── scenarios-for-simple-transparent-proxy.md ├── simple_transparent_proxy.md ├── sslh-examples-v3.png ├── sslh-examples-v3.svg ├── tproxy.md └── tproxy.svg ├── echo_test.cfg ├── echosrv-conf.c ├── echosrv-conf.h ├── echosrv.c ├── echosrv.cfg ├── echoѕrv-conf.h ├── example.cfg ├── gap.c ├── gap.h ├── genver.sh ├── hash.c ├── hash.h ├── hashtest ├── Makefile ├── delete.tst ├── delete.tst.ref ├── delete_at_end.tst ├── delete_at_end.tst.ref ├── delete_below_floor.tst ├── delete_below_floor.tst.ref ├── delete_discont.tst ├── delete_discont.tst.ref ├── delete_empty.tst ├── delete_empty.tst.ref ├── delete_full.tst ├── delete_full.tst.ref ├── delete_middle.tst ├── delete_middle.tst.ref ├── delete_wrap.tst ├── delete_wrap.tst.ref ├── delete_wrap_at_end.tst ├── delete_wrap_at_end.tst.ref ├── delete_wrap_below_floor.tst ├── delete_wrap_below_floor.tst.ref ├── delete_wrap_discont.tst ├── delete_wrap_discont.tst.ref ├── htest ├── htest.c ├── insert.tst ├── insert.tst.ref ├── insert_discont.tst ├── insert_discont.tst.ref ├── insert_full.tst ├── insert_full.tst.ref ├── insert_full_floor.tst ├── insert_full_floor.tst.ref ├── insert_wrap.tst ├── insert_wrap.tst.ref ├── mkrand.pl └── run ├── landlock.c ├── log.c ├── log.h ├── probe.c ├── probe.h ├── processes.c ├── processes.h ├── proxyprotocol.c ├── proxyprotocol.h ├── scripts ├── etc-init.d-sslh-debian-modified.sslh ├── etc.init.d.sslh ├── etc.rc.d.init.d.sslh.centos ├── etc.sysconfig.sslh ├── fail2ban │ ├── jail.conf │ └── sslh-ssh.conf ├── systemd.sslh-select@.service └── systemd.sslh@.service ├── sslh-conf.c ├── sslh-conf.h ├── sslh-ev.c ├── sslh-fork.c ├── sslh-main.c ├── sslh-select.c ├── sslh.pod ├── sslhconf.cfg ├── systemd-sslh-generator.c ├── t ├── t_load ├── tcp-listener.c ├── tcp-listener.h ├── tcp-probe.c ├── tcp-probe.h ├── test.cfg ├── tls.c ├── tls.h ├── udp-listener.c ├── udp-listener.h └── udp.cfg /.github/workflows/container-build.yaml: -------------------------------------------------------------------------------- 1 | name: Create and publish Container image 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - 'v*' 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | env: 14 | REGISTRY: ghcr.io 15 | IMAGE_NAME: ${{ github.repository }} 16 | 17 | jobs: 18 | build-and-push-image: 19 | runs-on: ubuntu-latest 20 | permissions: 21 | contents: read 22 | packages: write 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v3 27 | 28 | - name: Set up QEMU 29 | uses: docker/setup-qemu-action@v2 30 | 31 | - name: Set up Docker Buildx 32 | uses: docker/setup-buildx-action@v2 33 | 34 | - name: Login to Container registry 35 | uses: docker/login-action@v2 36 | with: 37 | registry: ${{ env.REGISTRY }} 38 | username: ${{ github.actor }} 39 | password: ${{ secrets.GITHUB_TOKEN }} 40 | 41 | - name: Docker meta 42 | id: meta 43 | uses: docker/metadata-action@v4 44 | with: 45 | images: | 46 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 47 | tags: | 48 | type=ref,event=branch 49 | type=ref,event=pr 50 | type=edge 51 | type=semver,pattern={{version}} 52 | type=semver,pattern={{major}}.{{minor}} 53 | type=semver,pattern={{major}} 54 | 55 | - name: Build and push 56 | uses: docker/build-push-action@v4 57 | with: 58 | platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7 59 | context: . 60 | file: Dockerfile 61 | push: ${{ github.event_name != 'pull_request' }} 62 | tags: ${{ steps.meta.outputs.tags }} 63 | labels: ${{ steps.meta.outputs.labels }} 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.o 4 | cscope.* 5 | echosrv 6 | libsslh.a 7 | sslh-fork 8 | sslh-select 9 | sslh-ev 10 | systemd-sslh-generator 11 | sslh.8.gz 12 | tags 13 | version.h 14 | /config.status 15 | /config.log 16 | /config.h 17 | /Makefile 18 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yrutschle/sslh/b7556c07bddf0e666ea9bf7bdfb0a140044190b0/ChangeLog -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ALPINE_VERSION="latest" 2 | ARG TARGET_ARCH="library" 3 | 4 | FROM docker.io/${TARGET_ARCH}/alpine:${ALPINE_VERSION} AS build 5 | 6 | WORKDIR /sslh 7 | 8 | RUN apk add --no-cache \ 9 | 'gcc' \ 10 | 'libconfig-dev' \ 11 | 'make' \ 12 | 'musl-dev' \ 13 | 'pcre2-dev' \ 14 | 'perl' \ 15 | ; 16 | 17 | COPY . /sslh 18 | 19 | RUN ./configure && make sslh-select && strip sslh-select 20 | 21 | FROM docker.io/${TARGET_ARCH}/alpine:${ALPINE_VERSION} 22 | 23 | COPY --from=build "/sslh/sslh-select" "/usr/local/bin/sslh" 24 | RUN apk add --no-cache \ 25 | 'libconfig' \ 26 | 'pcre2' \ 27 | 'iptables' \ 28 | 'ip6tables' \ 29 | 'libcap' \ 30 | && \ 31 | adduser -s '/bin/sh' -S -D sslh && \ 32 | setcap cap_net_bind_service,cap_net_raw+ep /usr/local/bin/sslh 33 | 34 | COPY "./container-entrypoint.sh" "/init" 35 | ENTRYPOINT [ "/init" ] 36 | 37 | # required for updating iptables 38 | USER root:root 39 | -------------------------------------------------------------------------------- /Makefile.in: -------------------------------------------------------------------------------- 1 | 2 | VERSION=$(shell ./genver.sh -r) 3 | 4 | # Configuration -- you probably need to `make clean` if you 5 | # change any of these 6 | 7 | # uncomment the following line to disable landlock 8 | # override undefine HAVE_LANDLOCK 9 | ENABLE_SANITIZER= # Enable ASAN/LSAN/UBSAN 10 | ENABLE_REGEX=1 # Enable regex probes 11 | USELIBCONFIG=1 # Use libconfig? (necessary to use configuration files) 12 | USELIBEV=1 # Use libev? 13 | USESYSTEMD= # Make use of systemd socket activation 14 | COV_TEST= # Perform test coverage? 15 | PREFIX?=/usr 16 | BINDIR?=$(PREFIX)/sbin 17 | MANDIR?=$(PREFIX)/share/man/man8 18 | 19 | MAN=sslh.8.gz # man page name 20 | 21 | # End of configuration -- the rest should take care of 22 | # itself 23 | 24 | ifneq ($(strip $(ENABLE_SANITIZER)),) 25 | CFLAGS_SAN=-fsanitize=address -fsanitize=leak -fsanitize=undefined -fsanitize=alignment 26 | endif 27 | 28 | ifneq ($(strip $(COV_TEST)),) 29 | CFLAGS_COV=-fprofile-arcs -ftest-coverage 30 | endif 31 | 32 | CC ?= gcc 33 | AR ?= ar 34 | CFLAGS +=-Wall -O2 -DLIBPCRE -g $(CFLAGS_COV) $(CFLAGS_SAN) 35 | 36 | 37 | LIBS=-lm -lpcre2-8 @LIBS@ 38 | OBJS=sslh-conf.o common.o log.o sslh-main.o probe.o tls.o argtable3.o collection.o gap.o tcp-probe.o landlock.o proxyprotocol.o 39 | OBJS_A=libsslh.a 40 | FORK_OBJS=sslh-fork.o $(OBJS_A) 41 | SELECT_OBJS=processes.o udp-listener.o sslh-select.o hash.o tcp-listener.o $(OBJS_A) 42 | EV_OBJS=processes.o udp-listener.o sslh-ev.o hash.o tcp-listener.o $(OBJS_A) 43 | 44 | CONDITIONAL_TARGETS= 45 | 46 | ifneq ($(strip $(ENABLE_REGEX)),) 47 | CPPFLAGS+=-DENABLE_REGEX 48 | endif 49 | 50 | ifneq ($(strip $(USELIBCONFIG)),) 51 | LIBS:=$(LIBS) -lconfig 52 | CPPFLAGS+=-DLIBCONFIG 53 | endif 54 | 55 | ifneq ($(strip $(USESYSTEMD)),) 56 | LIBS:=$(LIBS) -lsystemd 57 | CPPFLAGS+=-DSYSTEMD 58 | CONDITIONAL_TARGETS+=systemd-sslh-generator 59 | endif 60 | 61 | ifneq ($(strip $(USELIBEV)),) 62 | CONDITIONAL_TARGETS+=sslh-ev 63 | endif 64 | 65 | all: sslh-fork sslh-select $(MAN) echosrv $(CONDITIONAL_TARGETS) 66 | 67 | %.o: %.c %.h version.h 68 | $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@ 69 | 70 | $(OBJS_A): $(OBJS) 71 | $(AR) rcs $(OBJS_A) $(OBJS) 72 | 73 | version.h: 74 | ./genver.sh >version.h 75 | 76 | $(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 77 | 78 | 79 | c2s: 80 | conf2struct sslhconf.cfg 81 | conf2struct echosrv.cfg 82 | 83 | sslh-conf.c sslh-conf.h: sslhconf.cfg 84 | $(warning "sslhconf.cfg is more recent than sslh-conf.[ch]. Use `make c2s` to rebuild using `conf2struct`") 85 | 86 | sslh-fork: version.h Makefile $(FORK_OBJS) 87 | $(CC) $(CFLAGS) $(LDFLAGS) -o sslh-fork $(FORK_OBJS) $(LIBS) 88 | 89 | sslh-select: version.h $(SELECT_OBJS) Makefile 90 | $(CC) $(CFLAGS) $(LDFLAGS) -o sslh-select $(SELECT_OBJS) $(LIBS) 91 | 92 | sslh-ev: version.h $(EV_OBJS) Makefile 93 | $(CC) $(CFLAGS) $(LDFLAGS) -o sslh-ev $(EV_OBJS) $(LIBS) -lev 94 | 95 | systemd-sslh-generator: systemd-sslh-generator.o 96 | $(CC) $(CFLAGS) $(LDFLAGS) -o systemd-sslh-generator systemd-sslh-generator.o -lconfig 97 | 98 | echosrv-conf.c echosrv-conf.h: echosrv.cfg 99 | $(warning "echosrv.cfg is more recent than echosrv-conf.[ch]. Use `make c2s` to rebuild using `conf2struct`") 100 | 101 | echosrv: version.h echosrv-conf.c echosrv.o echosrv-conf.o argtable3.o 102 | $(CC) $(CFLAGS) $(LDFLAGS) -o echosrv echosrv.o echosrv-conf.o argtable3.o $(LIBS) 103 | 104 | 105 | landlock.o: config.h 106 | 107 | $(MAN): sslh.pod Makefile 108 | pod2man --section=8 --release=$(VERSION) --center=" " sslh.pod | gzip -9 - > $(MAN) 109 | 110 | # Create release: export clean tree and tag current 111 | # configuration 112 | release: 113 | git archive $(VERSION) --prefix="sslh-$(VERSION)/" | gzip > /tmp/sslh-$(VERSION).tar.gz 114 | gpg --detach-sign --armor /tmp/sslh-$(VERSION).tar.gz 115 | 116 | # Build docker image 117 | docker: 118 | docker image build -t "sslh:${VERSION}" . 119 | docker image tag "sslh:${VERSION}" sslh:latest 120 | 121 | docker-clean: 122 | yes | docker image rm "sslh:${VERSION}" sslh:latest 123 | yes | docker image prune 124 | 125 | # generic install: install binary and man page 126 | install: sslh-fork $(MAN) 127 | mkdir -p $(DESTDIR)/$(BINDIR) 128 | mkdir -p $(DESTDIR)/$(MANDIR) 129 | install -p sslh-fork $(DESTDIR)/$(BINDIR)/sslh 130 | install -p -m 0644 $(MAN) $(DESTDIR)/$(MANDIR)/$(MAN) 131 | 132 | # "extended" install for Debian: install startup script 133 | install-debian: install sslh $(MAN) 134 | sed -e "s+^PREFIX=+PREFIX=$(PREFIX)+" scripts/etc.init.d.sslh > /etc/init.d/sslh 135 | chmod 755 /etc/init.d/sslh 136 | update-rc.d sslh defaults 137 | 138 | uninstall: 139 | rm -f $(DESTDIR)$(BINDIR)/sslh $(DESTDIR)$(MANDIR)/$(MAN) $(DESTDIR)/etc/init.d/sslh $(DESTDIR)/etc/default/sslh 140 | update-rc.d sslh remove 141 | 142 | distclean: clean 143 | rm -f tags sslh-conf.[ch] echosrv-conf.[ch] cscope.* 144 | 145 | clean: 146 | rm -f sslh-fork sslh-select $(CONDITIONAL_TARGETS) echosrv version.h $(MAN) systemd-sslh-generator *.o *.gcov *.gcno *.gcda *.png *.html *.css *.info 147 | 148 | tags: 149 | ctags --globals -T *.[ch] 150 | 151 | cscope: 152 | -find . -name "*.[chS]" >cscope.files 153 | -cscope -b -R 154 | 155 | test: 156 | ./t 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sslh -- A ssl/ssh multiplexer 2 | ============================= 3 | 4 | `sslh` accepts connections on specified ports, and forwards 5 | them further based on tests performed on the first data 6 | packet sent by the remote client. 7 | 8 | Probes for HTTP, TLS/SSL (including SNI and ALPN), SSH, 9 | OpenVPN, tinc, XMPP, SOCKS5, are implemented, and any other 10 | protocol that can be tested using a regular expression, can 11 | be recognised. A typical use case is to allow serving 12 | several services on port 443 (e.g. to connect to SSH from 13 | inside a corporate firewall, which almost never block port 14 | 443) while still serving HTTPS on that port. 15 | 16 | Hence `sslh` acts as a protocol demultiplexer, or a 17 | switchboard. With the SNI and ALPN probe, it makes a good 18 | front-end to a virtual host farm hosted behind a single IP 19 | address. 20 | 21 | `sslh` has the bells and whistles expected from a mature 22 | daemon: privilege and capabilities dropping, inetd support, 23 | systemd support, transparent proxying, support for HAProxy's 24 | proxyprotocol, chroot, logging, IPv4 and IPv6, TCP and UDP, 25 | a fork-based, a select-based model, and yet another based on 26 | libev for larger installations. 27 | 28 | Install 29 | ======= 30 | 31 | Please refer to the [install guide](doc/INSTALL.md). 32 | 33 | 34 | Configuration 35 | ============= 36 | 37 | Please refer to the [configuration guide](doc/config.md). 38 | 39 | Transparent proxying 40 | -------------------- 41 | 42 | Transparent proxying allows the target server to see the 43 | original client IP address, i.e. `sslh` becomes invisible. 44 | 45 | The same result can be achieved more easily by using 46 | `proxyprotocol` if the backend server supports it. This is a 47 | simple setting to add to the `sslh` protocol configuration, 48 | usually with an equivalently simple setting to add in 49 | the backend server configuration, so try that first. 50 | 51 | This means services behind `sslh` (Apache, `sshd` and so on) 52 | will see the external IP and ports as if the external world 53 | connected directly to them. This simplifies IP-based access 54 | control (or makes it possible at all), and makes it possible 55 | to use IP-based banning tools such as `fail2ban`. 56 | 57 | There are two methods. One uses additional virtual network 58 | interfaces. The principle and basic setup is described 59 | [here](doc/simple_transparent_proxy.md), with further 60 | scenarios described [there](doc/scenarios-for-simple-transparent-proxy.md). 61 | 62 | There is also a guide to use [podman](doc/podman.md). 63 | 64 | Another method uses iptable packet marking features, and is 65 | highly dependent on your network environment and 66 | infrastructure setup. There is no known generic approach, 67 | and if you do not find directions for your exact setup, you 68 | will probably need an extensive knowledge of network 69 | management and iptables setup". 70 | 71 | It is described in its own [document](doc/tproxy.md). 72 | In most cases, you will be better off following the first 73 | method. 74 | 75 | 76 | Docker image 77 | ------------ 78 | 79 | How to use 80 | 81 | --- 82 | 83 | 84 | ```bash 85 | docker run \ 86 | --cap-add CAP_NET_RAW \ 87 | --cap-add CAP_NET_BIND_SERVICE \ 88 | --rm \ 89 | -it \ 90 | ghcr.io/yrutschle/sslh:latest \ 91 | --foreground \ 92 | --listen=0.0.0.0:443 \ 93 | --ssh=hostname:22 \ 94 | --tls=hostname:443 95 | ``` 96 | 97 | docker-compose example 98 | 99 | ```yaml 100 | version: "3" 101 | 102 | services: 103 | sslh: 104 | image: ghcr.io/yrutschle/sslh:latest 105 | hostname: sslh 106 | ports: 107 | - 443:443 108 | command: --foreground --listen=0.0.0.0:443 --tls=nginx:443 --openvpn=openvpn:1194 109 | depends_on: 110 | - nginx 111 | - openvpn 112 | 113 | nginx: 114 | image: nginx 115 | 116 | openvpn: 117 | image: openvpn 118 | ``` 119 | 120 | Transparent mode 1: using sslh container for networking 121 | 122 | _Note: For transparent mode to work, the sslh container must be able to reach your services via **localhost**_ 123 | ```yaml 124 | version: "3" 125 | 126 | services: 127 | sslh: 128 | build: https://github.com/yrutschle/sslh.git 129 | container_name: sslh 130 | environment: 131 | - TZ=${TZ} 132 | cap_add: 133 | - NET_ADMIN 134 | - NET_RAW 135 | - NET_BIND_SERVICE 136 | sysctls: 137 | - net.ipv4.conf.default.route_localnet=1 138 | - net.ipv4.conf.all.route_localnet=1 139 | command: --transparent --foreground --listen=0.0.0.0:443 --tls=localhost:8443 --openvpn=localhost:1194 140 | ports: 141 | - 443:443 #sslh 142 | 143 | - 80:80 #nginx 144 | - 8443:8443 #nginx 145 | 146 | - 1194:1194 #openvpn 147 | extra_hosts: 148 | - localbox:host-gateway 149 | restart: unless-stopped 150 | 151 | nginx: 152 | image: nginx:latest 153 | ..... 154 | network_mode: service:sslh #set nginx container to use sslh networking. 155 | # ^^^ This is required. This makes nginx reachable by sslh via localhost 156 | 157 | openvpn: 158 | image: openvpn:latest 159 | ..... 160 | network_mode: service:sslh #set openvpn container to use sslh networking 161 | ``` 162 | 163 | Transparent mode 2: using host networking 164 | 165 | ```yaml 166 | version: "3" 167 | 168 | services: 169 | sslh: 170 | build: https://github.com/yrutschle/sslh.git 171 | container_name: sslh 172 | environment: 173 | - TZ=${TZ} 174 | cap_add: 175 | - NET_ADMIN 176 | - NET_RAW 177 | - NET_BIND_SERVICE 178 | # must be set manually 179 | #sysctls: 180 | # - net.ipv4.conf.default.route_localnet=1 181 | # - net.ipv4.conf.all.route_localnet=1 182 | command: --transparent --foreground --listen=0.0.0.0:443 --tls=localhost:8443 --openvpn=localhost:1194 183 | network_mode: host 184 | restart: unless-stopped 185 | 186 | nginx: 187 | image: nginx:latest 188 | ..... 189 | ports: 190 | - 8443:8443 # bind to docker host on port 8443 191 | 192 | openvpn: 193 | image: openvpn:latest 194 | ..... 195 | ports: 196 | - 1194:1194 # bind to docker host on port 1194 197 | ``` 198 | 199 | Comments? Questions? 200 | ==================== 201 | 202 | You can subscribe to the `sslh` mailing list here: 203 | 204 | 205 | This mailing list should be used for discussion, feature 206 | requests, and will be the preferred channel for announcements. 207 | 208 | Of course, check the [FAQ](doc/FAQ.md) first! 209 | 210 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Here's a list of features that have been suggested or 2 | sometimes requested. This list is not a roadmap and 3 | shouldn't be construed to mean that any of this will happen. 4 | 5 | - configurable behaviour depending on services (e.g. 6 | select() for ssl but fork() for ssh). 7 | 8 | - have certain services available only from specified subnets 9 | 10 | - some sort of "service knocking" allowing to activate a 11 | service upon some external even, similar to port knocking; 12 | for example, go to a specific URL to enable sslh forwarding 13 | to sshd for a set period of time: 14 | * sslh listens on 443 and only directs to httpd 15 | * user goes somewhere to https://example.org/open_ssh.cgi 16 | * open_ssh.cgi tells sslh 17 | * sslh starts checking if incoming connections are ssh, and 18 | if they are, forward to sshd 19 | * 10 minutes later, sslh stops forwarding to ssh 20 | 21 | That would make it almost impossible for an observer 22 | (someone who'd telnet regularly on 443) to ever notice both 23 | services are available on 443. 24 | 25 | 26 | -------------------------------------------------------------------------------- /basic.cfg: -------------------------------------------------------------------------------- 1 | # This is a basic configuration file that should provide 2 | # sensible values for "standard" setup. 3 | 4 | # You will find extensive examples with explanations in 5 | # example.cfg 6 | 7 | timeout: 2; 8 | user: "nobody"; 9 | pidfile: "/var/run/sslh.pid"; 10 | 11 | 12 | # Change hostname with your external address name, or the IP 13 | # of the interface that receives connections 14 | # Default is to bind all interfaces. httpd can be started 15 | # first to bind on localhost, in which case sslh will bind 16 | # only other interfaces. 17 | listen: 18 | ( 19 | { host: "0.0.0.0"; port: "443"; }, 20 | { host: "[::]"; port: "443"; } 21 | ); 22 | 23 | 24 | # Change to the protocols you want to forward to. The 25 | # defaults here are sensible for services running on 26 | # localhost 27 | protocols: 28 | ( 29 | { name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; fork: true; }, 30 | { name: "openvpn"; host: "localhost"; port: "1194"; }, 31 | { name: "tls"; host: "localhost"; port: "443"; log_level: 0; }, 32 | { name: "anyprot"; host: "localhost"; port: "443"; } 33 | ); 34 | 35 | -------------------------------------------------------------------------------- /collection.c: -------------------------------------------------------------------------------- 1 | /* 2 | collection.c: management of a collection of connections, for sslh-select 3 | 4 | # Copyright (C) 2021 Yves Rutschle 5 | # 6 | # This program is free software; you can redistribute it 7 | # and/or modify it under the terms of the GNU General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2 of the License, or (at your option) any later 10 | # version. 11 | # 12 | # This program is distributed in the hope that it will be 13 | # useful, but WITHOUT ANY WARRANTY; without even the implied 14 | # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 15 | # PURPOSE. See the GNU General Public License for more 16 | # details. 17 | # 18 | # The full text for the General Public License is here: 19 | # http://www.gnu.org/licenses/gpl.html 20 | 21 | */ 22 | 23 | #include "common.h" 24 | #include "collection.h" 25 | #include "sslh-conf.h" 26 | #include "gap.h" 27 | 28 | /* Info to keep track of all connections */ 29 | struct cnx_collection { 30 | gap_array* fd2cnx; /* Array indexed by file descriptor to things in cnx[] */ 31 | }; 32 | 33 | /* Allocates and initialises a new collection of connections with at least 34 | * `len` elements. */ 35 | cnx_collection* collection_init(int len) 36 | { 37 | cnx_collection* collection; 38 | 39 | collection = malloc(sizeof(*collection)); 40 | CHECK_ALLOC(collection, "collection_init(collection)"); 41 | 42 | memset(collection, 0, sizeof(*collection)); 43 | 44 | collection->fd2cnx = gap_init(len); 45 | 46 | return collection; 47 | } 48 | 49 | /* Caveat: might not work, as has never been used */ 50 | void collection_destroy(cnx_collection* collection) 51 | { 52 | /* Caveat 2: no code to free connections yet */ 53 | gap_destroy(collection->fd2cnx); 54 | free(collection); 55 | } 56 | 57 | /* Points the file descriptor to the specified connection index */ 58 | int collection_add_fd(cnx_collection* collection, struct connection* cnx, int fd) 59 | { 60 | gap_set(collection->fd2cnx, fd, cnx); 61 | return 0; 62 | } 63 | 64 | /* Allocates a connection and inits it with specified file descriptor */ 65 | struct connection* collection_alloc_cnx_from_fd(struct cnx_collection* collection, int fd) 66 | { 67 | struct connection* cnx = malloc(sizeof(*cnx)); 68 | 69 | if (!cnx) return NULL; 70 | 71 | init_cnx(cnx); 72 | cnx->type = SOCK_STREAM; 73 | cnx->q[0].fd = fd; 74 | cnx->state = ST_PROBING; 75 | cnx->probe_timeout = time(NULL) + cfg.timeout; 76 | 77 | gap_set(collection->fd2cnx, fd, cnx); 78 | 79 | return cnx; 80 | } 81 | 82 | /* Remove a connection from the collection */ 83 | int collection_remove_cnx(cnx_collection* collection, struct connection *cnx) 84 | { 85 | if (cnx->q[0].fd != -1) 86 | gap_set(collection->fd2cnx, cnx->q[0].fd, NULL); 87 | if (cnx->q[1].fd != -1) 88 | gap_set(collection->fd2cnx, cnx->q[1].fd, NULL); 89 | free(cnx); 90 | return 0; 91 | } 92 | 93 | /* Returns the connection that contains the file descriptor */ 94 | struct connection* collection_get_cnx_from_fd(struct cnx_collection* collection, int fd) 95 | { 96 | return gap_get(collection->fd2cnx, fd); 97 | } 98 | 99 | -------------------------------------------------------------------------------- /collection.h: -------------------------------------------------------------------------------- 1 | #ifndef COLLECTION_H 2 | #define COLLECTION_H 3 | 4 | typedef struct cnx_collection cnx_collection; 5 | 6 | 7 | cnx_collection* collection_init(int len); 8 | void collection_destroy(cnx_collection* collection); 9 | 10 | struct connection* collection_alloc_cnx_from_fd(cnx_collection* collection, int fd); 11 | int collection_add_fd(cnx_collection* collection, struct connection* cnx, int fd); 12 | 13 | /* Remove a connection from the collection */ 14 | int collection_remove_cnx(cnx_collection* collection, struct connection *cnx); 15 | 16 | struct connection* collection_get_cnx_from_fd(struct cnx_collection* collection, int fd); 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /common.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMON_H 2 | #define COMMON_H 3 | 4 | /* FD_SETSIZE is 64 on Cygwin, which is really low. Just redefining it is 5 | * enough for the macros to adapt (http://support.microsoft.com/kb/111855) 6 | */ 7 | #ifdef __CYGWIN__ 8 | #undef FD_SETSIZE 9 | #define FD_SETSIZE 4096 10 | #endif 11 | 12 | #define _GNU_SOURCE 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #ifdef LIBCAP 33 | #include 34 | #include 35 | #endif 36 | 37 | #ifdef __APPLE__ 38 | #include 39 | #endif 40 | 41 | #include "config.h" 42 | #include "version.h" 43 | 44 | #define MAX(a, b) (((a) > (b)) ? (a) : (b)) 45 | 46 | 47 | #define CHECK_RES_DIE(res, str) \ 48 | if (res == -1) { \ 49 | print_message(msg_system_error, "%s:%d:", __FILE__, __LINE__); \ 50 | perror(str); \ 51 | exit(1); \ 52 | } 53 | 54 | #define CHECK_RES_RETURN(res, str, ret) \ 55 | if (res == -1) { \ 56 | print_message(msg_system_error, "%s:%d:%s:%d:%s\n", __FILE__, __LINE__, str, errno, strerror(errno)); \ 57 | return ret; \ 58 | } 59 | 60 | #define CHECK_ALLOC(a, str) \ 61 | if (!a) { \ 62 | print_message(msg_system_error, "%s:%d:", __FILE__, __LINE__); \ 63 | perror(str); \ 64 | exit(1); \ 65 | } 66 | 67 | #define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0])) 68 | 69 | #if 1 70 | #define TRACE fprintf(stderr, "%s:%d\n", __FILE__, __LINE__); 71 | #else 72 | #define TRACE 73 | #endif 74 | 75 | #ifndef IP_FREEBIND 76 | #define IP_FREEBIND 0 77 | #endif 78 | 79 | #ifndef TCP_FASTOPEN 80 | #define TCP_FASTOPEN 0 81 | #endif 82 | 83 | #ifndef TCP_FASTOPEN_CONNECT 84 | #define TCP_FASTOPEN_CONNECT 30 /* Attempt FastOpen with connect. */ 85 | #endif 86 | 87 | enum connection_state { 88 | ST_PROBING=1, /* Waiting for timeout to find where to forward */ 89 | ST_SHOVELING /* Connexion is established */ 90 | }; 91 | 92 | /* A 'queue' is composed of a file descriptor (which can be read from or 93 | * written to), and a queue for deferred write data */ 94 | struct queue { 95 | int fd; 96 | void *begin_deferred_data; 97 | void *deferred_data; 98 | int deferred_data_size; 99 | }; 100 | 101 | /* Double linked list for timeout management */ 102 | typedef struct { 103 | struct connection* head; 104 | struct connection* tail; 105 | } dl_list; 106 | 107 | struct connection { 108 | int type; /* SOCK_DGRAM | SOCK_STREAM */ 109 | struct sslhcfg_protocols_item* proto; /* Where to connect to */ 110 | 111 | /* SOCK_STREAM */ 112 | enum connection_state state; 113 | time_t probe_timeout; 114 | 115 | /* q[0]: queue for external connection (client); 116 | * q[1]: queue for internal connection (httpd or sshd); 117 | * */ 118 | struct queue q[2]; 119 | 120 | /* SOCK_DGRAM */ 121 | struct sockaddr_storage client_addr; /* Contains the remote client address */ 122 | socklen_t addrlen; 123 | 124 | int local_endpoint; /* Contains the local address */ 125 | 126 | time_t last_active; 127 | 128 | /* double linked list of timeouts */ 129 | struct connection *timeout_prev, *timeout_next; 130 | 131 | /* We need one local socket for each target server, so we know where to 132 | * forward server responses */ 133 | int target_sock; 134 | }; 135 | 136 | 137 | struct listen_endpoint { 138 | int socketfd; /* file descriptor of listening socket */ 139 | int type; /* SOCK_DGRAM | SOCK_STREAM */ 140 | int family; /* AF_INET | AF_UNIX */ 141 | }; 142 | 143 | #define FD_CNXCLOSED 0 144 | #define FD_NODATA -1 145 | #define FD_STALLED -2 146 | 147 | /* String description of a connection */ 148 | #define MAX_NAMELENGTH (NI_MAXHOST + NI_MAXSERV + 1) 149 | struct connection_desc { 150 | char peer[MAX_NAMELENGTH], service[MAX_NAMELENGTH], 151 | local[MAX_NAMELENGTH], target[MAX_NAMELENGTH]; 152 | }; 153 | 154 | typedef enum { 155 | NON_BLOCKING = 0, 156 | BLOCKING = 1 157 | } connect_blocking; 158 | 159 | 160 | /* common.c */ 161 | void init_cnx(struct connection *cnx); 162 | int set_nonblock(int fd); 163 | void connect_addr(struct connection *cnx, int fd_from, connect_blocking blocking); 164 | int fd2fd(struct queue *target, struct queue *from); 165 | char* sprintaddr(char* buf, size_t size, struct addrinfo *a); 166 | void resolve_name(struct addrinfo **out, char* fullname); 167 | int get_connection_desc(struct connection_desc* desc, const struct connection *cnx); 168 | void log_connection(struct connection_desc* desc, const struct connection *cnx); 169 | void set_proctitle_shovel(struct connection_desc* desc, const struct connection *cnx); 170 | int check_access_rights(int in_socket, const char* service); 171 | void setup_signals(void); 172 | void setup_syslog(const char* bin_name); 173 | void drop_privileges(const char* user_name, const char* chroot_path); 174 | void set_capabilities(int cap_net_admin); 175 | void write_pid_file(const char* pidfile); 176 | void dump_connection(struct connection *cnx); 177 | int resolve_split_name(struct addrinfo **out, char* hostname, char* port); 178 | 179 | int start_listen_sockets(struct listen_endpoint *sockfd[]); 180 | 181 | int defer_write(struct queue *q, void* data, ssize_t data_size); 182 | int defer_write_before(struct queue *q, void* data, ssize_t data_size); 183 | int flush_deferred(struct queue *q); 184 | 185 | extern struct sslhcfg_item cfg; 186 | extern struct addrinfo *addr_listen; 187 | extern const char* server_type; 188 | 189 | #if defined(__APPLE__) && (MAC_OS_X_VERSION_MIN_REQUIRED < 1080) 190 | extern int hosts_ctl(); 191 | #endif 192 | 193 | /* sslh-fork.c */ 194 | void start_shoveler(int); 195 | 196 | void main_loop(struct listen_endpoint *listen_sockets, int num_addr_listen); 197 | 198 | /* landlock.c */ 199 | void setup_landlock(void); 200 | 201 | 202 | #endif 203 | -------------------------------------------------------------------------------- /config.h.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ifndef CONFIG_H 4 | /* Template for config.h, filled by `configure`. */ 5 | 6 | /* Libwrap, to support host_ctl, /etc/allow and /etc/deny */ 7 | #undef HAVE_LIBWRAP 8 | 9 | /* Landlock sandboxing Linux LSM */ 10 | #undef HAVE_LANDLOCK 11 | 12 | /* Support for Proxy-protocol using libproxyprotocol */ 13 | #undef HAVE_PROXYPROTOCOL 14 | 15 | /* libcap support, to use Linux capabilities */ 16 | #undef HAVE_LIBCAP 17 | 18 | /* libbsd, to change process name */ 19 | #undef HAVE_LIBBSD 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | 2 | dnl Use autoconf to generate the `configure` script from this and Makefile.in 3 | dnl This is awkardly adapted from https://github.com/edrosten/autoconf_tutorial 4 | dnl (all mistakes mine) 5 | 6 | AC_INIT 7 | AC_CONFIG_HEADERS(config.h) 8 | AC_CONFIG_FILES([Makefile]) 9 | 10 | AC_CHECK_LIB([wrap], [hosts_ctl], [AC_DEFINE(HAVE_LIBWRAP) LIBS="$LIBS -lwrap" ], []) 11 | AC_CHECK_LIB([cap], [cap_get_proc], [AC_DEFINE(HAVE_LIBCAP) LIBS="$LIBS -lcap" ], []) 12 | AC_CHECK_LIB([bsd], [setproctitle], [AC_DEFINE(HAVE_LIBBSD) LIBS="$LIBS -lbsd" ], []) 13 | 14 | AC_CHECK_HEADERS(linux/landlock.h, AC_DEFINE(HAVE_LANDLOCK), []) 15 | AC_CHECK_HEADERS(proxy_protocol.h, [AC_DEFINE(HAVE_PROXYPROTOCOL) LIBS="$LIBS -lproxyprotocol" ], []) 16 | 17 | LIBS="$LIBS" 18 | AC_SUBST([LIBS]) 19 | 20 | AC_OUTPUT 21 | -------------------------------------------------------------------------------- /container-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: GPL2-or-later 3 | # 4 | # Copyright (C) 2023 Olliver Schinagl 5 | # 6 | # A beginning user should be able to docker run image bash (or sh) without 7 | # needing to learn about --entrypoint 8 | # https://github.com/docker-library/official-images#consistency 9 | 10 | set -eu 11 | 12 | bin='sslh' 13 | 14 | # run command if it is not starting with a "-" and is an executable in PATH 15 | if [ "${#}" -le 0 ] || \ 16 | [ "${1#-}" != "${1}" ] || \ 17 | [ -d "${1}" ] || \ 18 | ! command -v "${1}" > '/dev/null' 2>&1; then 19 | entrypoint='true' 20 | fi 21 | 22 | unconfigure_iptables() { 23 | echo "Received SIG TERM/INT/KILL. Removing iptables / routing changes" 24 | 25 | set +e # Don't exit if got error 26 | set -x 27 | 28 | iptables -t raw -D PREROUTING ! -i lo -d 127.0.0.0/8 -j DROP 29 | iptables -t mangle -D POSTROUTING ! -o lo -s 127.0.0.0/8 -j DROP 30 | 31 | 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 32 | iptables -t mangle -D OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f 33 | 34 | ip rule del fwmark 0x1 lookup 100 35 | ip route del local 0.0.0.0/0 dev lo table 100 36 | 37 | 38 | if [ $(cat /proc/sys/net/ipv6/conf/all/disable_ipv6) -eq 0 ]; then 39 | ip6tables -t raw -D PREROUTING ! -i lo -d ::1/128 -j DROP 40 | ip6tables -t mangle -D POSTROUTING ! -o lo -s ::1/128 -j DROP 41 | 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 42 | ip6tables -t mangle -D OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f 43 | 44 | ip -6 rule del fwmark 0x1 lookup 100 45 | ip -6 route del local ::/0 dev lo table 100 46 | fi 47 | 48 | set -e 49 | set +x 50 | } 51 | 52 | configure_iptables() { 53 | echo "Configuring iptables and routing..." 54 | 55 | set +e # Don't exit if got error 56 | set -x 57 | 58 | iptables -t raw -A PREROUTING ! -i lo -d 127.0.0.0/8 -j DROP 59 | iptables -t mangle -A POSTROUTING ! -o lo -s 127.0.0.0/8 -j DROP 60 | 61 | 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 62 | iptables -t mangle -A OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f 63 | 64 | ip rule add fwmark 0x1 lookup 100 65 | ip route add local 0.0.0.0/0 dev lo table 100 66 | 67 | if [ $(cat /proc/sys/net/ipv6/conf/all/disable_ipv6) -eq 0 ]; then 68 | ip6tables -t raw -A PREROUTING ! -i lo -d ::1/128 -j DROP 69 | ip6tables -t mangle -A POSTROUTING ! -o lo -s ::1/128 -j DROP 70 | 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 71 | ip6tables -t mangle -A OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f 72 | 73 | ip -6 rule add fwmark 0x1 lookup 100 74 | ip -6 route add local ::/0 dev lo table 100 75 | fi 76 | 77 | set -e 78 | set +x 79 | } 80 | 81 | for _args in "${@}" ; do 82 | if [ "${_args:-}" = '--transparent' ] ; then 83 | echo '--transparent flag is set' 84 | configure_iptables 85 | trap unconfigure_iptables TERM INT KILL 86 | break 87 | fi 88 | done 89 | 90 | # Drop privileges and run as sslh user 91 | sslh_cmd="${entrypoint:+${bin}} ${@}" 92 | echo "Executing with user 'sslh': ${sslh_cmd}" 93 | 94 | exec su - sslh -c "${sslh_cmd}" & 95 | wait "${!}" 96 | 97 | exit 0 98 | -------------------------------------------------------------------------------- /doc/FAQ.md: -------------------------------------------------------------------------------- 1 | Frequently Asked Questions 2 | ========================== 3 | 4 | When something doesn't work, look up here... and if it still 5 | doesn't work, report how what was suggested here went. 6 | 7 | It's also worth reading [how to ask 8 | questions](http://www.catb.org/~esr/faqs/smart-questions.html) 9 | before posting on the mailing list or opening an issue in 10 | GitHub. 11 | 12 | Getting more info 13 | ================= 14 | 15 | There are several `verbose` options that each enable a set 16 | of messages, each related to some event type. See 17 | `example.cfg` for a list of them. 18 | 19 | If something doesn't work, you'll want to run `sslh` with 20 | lots of logging, and the logging directly in the terminal 21 | (Otherwise, logs are sent to `syslog`, and usually end up in 22 | `/var/log/auth.log`). There is a general `--verbose` option 23 | that will allow you to enable all messages: 24 | 25 | ``` 26 | sslh -v 3 -f -F myconfig.cfg 27 | ``` 28 | 29 | 30 | forward to [PROBE] failed:connect: Connection refused 31 | ===================================================== 32 | 33 | Usually this means `sslh` is configured to forward a 34 | protocol somewhere, but no service is listening on the 35 | target address. Check your `sslh` configuration, check the 36 | corresponding server really is listening and running. 37 | Finally, check the server is listening where you expect it 38 | to: 39 | 40 | ``` 41 | netstat -lpt 42 | ``` 43 | 44 | I get a segmentation fault! 45 | =========================== 46 | 47 | Well, it's not yours (fault): a segfault is always a bug in 48 | the programme. Usually standard use cases are well tested, 49 | so it may be related to something unusual in your 50 | configuration, or even something wrong, but it should still 51 | never result in a segfault. 52 | 53 | Thankfully, when they are deterministic, segfaults are 54 | usually fairly easy to fix if you're willing to run a few 55 | diagnostics to help the developer. 56 | 57 | First, make sure you have debug symbols: 58 | ``` 59 | $ file sslh-select 60 | sslh-select: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=a758ac75ff11f1ace577705b4d6627e301940b59, with debug_info, not stripped 61 | ``` 62 | 63 | Note `with debug_info, not stripped` at the end. If you 64 | don't have that, your distribution stripped the binary: you 65 | will need to get the source code and compile it yourself 66 | (that way, you will also get the latest version). 67 | 68 | Install `valgrind` and run `sslh` under it: 69 | 70 | ``` 71 | valgrind --leak-check=full ./sslh-fork -v 2 -f -F yourconfig.cfg 72 | ``` 73 | 74 | Report the full output to the mailing list or github. 75 | Valgrind is very powerful and gives precise hints of what is 76 | wrong and why. For example on `sslh` issue 77 | (#273)[https://github.com/yrutschle/sslh/issues/273]: 78 | 79 | ``` 80 | sudo valgrind --leak-check=full ./sslh-fork -v 2 -f -F /etc/sslh.cfg 81 | ==20037== Memcheck, a memory error detector 82 | ==20037== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. 83 | ==20037== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info 84 | ==20037== Command: ./sslh-fork -v 2 -f -F /etc/sslh.cfg 85 | ==20037== 86 | sslh-fork v1.21b-1-g2c93a01-dirty started 87 | --20037-- WARNING: unhandled arm-linux syscall: 403 88 | --20037-- You may be able to write your own handler. 89 | --20037-- Read the file README_MISSING_SYSCALL_OR_IOCTL. 90 | --20037-- Nevertheless we consider this a bug. Please report 91 | --20037-- it at http://valgrind.org/support/bug_reports.html. 92 | ==20040== Conditional jump or move depends on uninitialised value(s) 93 | ==20040== at 0x112A3C: parse_tls_header (tls.c:162) 94 | ==20040== by 0x111CEF: is_tls_protocol (probe.c:214) 95 | ==20040== by 0x11239F: probe_client_protocol (probe.c:366) 96 | ==20040== by 0x10A8F7: start_shoveler (sslh-fork.c:98) 97 | ==20040== by 0x10AE9B: main_loop (sslh-fork.c:200) 98 | ==20040== by 0x1114FB: main (sslh-main.c:322) 99 | ==20040== 100 | ``` 101 | 102 | Here we see that something wrong is happening at `tls.c` 103 | line 162, and it's linked to an uninitialised value. 104 | 105 | Using sslh for virtual hosting 106 | ============================== 107 | 108 | Virtual hosting refers to having several domain names behind 109 | a single IP address. All Web servers handle this, but 110 | sometimes it can be useful to do it with `sslh`. 111 | 112 | TLS virtual hosting with SNI 113 | ---------------------------- 114 | 115 | For TLS, this is done very simply using Server Name 116 | Indication, SNI for short, which is a TLS extension whereby 117 | the client indicates the name of the server it wishes to 118 | connect to. This can be a very powerful way to separate 119 | several TLS-based services hosted behind the same port: 120 | simply name each service with its own hostname. For example, 121 | we could define `mail.rutschle.net`, `im.rutschle.net`, 122 | `www.rutschle.net`, all of which point to the same IP 123 | address. `sslh` uses the `sni_hostnames` setting of the 124 | TLS probe to do this, e.g.: 125 | 126 | ``` 127 | protocols: ( 128 | { name: "tls"; 129 | host: "localhost"; 130 | port: "993"; 131 | sni_hostnames: [ "mail.rutschle.net" ]; 132 | }, 133 | { name: "tls"; 134 | host: "localhost"; 135 | port: "xmpp-client"; 136 | sni_hostnames: [ "im.rutschle.net" ]; 137 | }, 138 | { name: "tls"; 139 | host: "localhost"; 140 | port: "4443"; 141 | sni_hostnames: [ "www.rutschle.net" ]; 142 | } 143 | ); 144 | ``` 145 | 146 | HTTP virtual hosting with regex 147 | ------------------------------- 148 | 149 | If you wish to serve several Web domains over HTTP through 150 | `sslh`, you can do this simply by using regular expressions 151 | on the Host specification part of the HTTP query. 152 | 153 | The following example forwards connections to `host_A.acme` 154 | to 192.168.0.2, and connections to `host_B.acme` to 155 | 192.168.0.3. 156 | 157 | ``` 158 | protocols: ( 159 | { name: "regex"; 160 | host: "192.168.0.2"; 161 | port: "80"; 162 | regex_patterns: 163 | ["^(GET|POST|PUT|OPTIONS|DELETE|HEADER) [^ ]* HTTP/[0-9.]*[\r\n]*Host: host_A.acme"] }, 164 | { name: "regex"; 165 | host: "192.168.0.3"; 166 | port: "80"; 167 | regex_patterns: 168 | ["^(GET|POST|PUT|OPTIONS|DELETE|HEADER) [^ ]* HTTP/[0-9.]*[\r\n]*Host: host_B.acme"] } 169 | ); 170 | ``` 171 | -------------------------------------------------------------------------------- /doc/INSTALL.md: -------------------------------------------------------------------------------- 1 | Pre-built binaries 2 | ================== 3 | 4 | Docker images of `master` and of the tagged versions are 5 | available directly from [Github](https://github.com/yrutschle/sslh/pkgs/container/sslh). 6 | 7 | Windows binaries for Cygwin are graciously produced by 8 | nono303 on his [repository](https://github.com/nono303/sslh). 9 | 10 | 11 | Compile and install 12 | =================== 13 | 14 | Dependencies 15 | ------------ 16 | 17 | `sslh` uses: 18 | 19 | * [libconfig](http://www.hyperrealm.com/libconfig/). 20 | For Debian this is contained in package `libconfig-dev`. 21 | You can compile with or without it using USELIBCONFIG in the Makefile. 22 | 23 | * [libwrap](http://packages.debian.org/source/unstable/tcp-wrappers). 24 | For Debian, this is contained in packages `libwrap0-dev`. 25 | Presence of libwrap is checked by the configure script. 26 | 27 | * [libsystemd](http://packages.debian.org/source/unstable/libsystemd-dev), in package `libsystemd-dev`. 28 | You can compile with or without it using USESYSTEMD in the Makefile. 29 | 30 | * [libcap](http://packages.debian.org/source/unstable/libcap-dev), in package `libcap-dev`. 31 | Presence of libcap is checked by the configure script. 32 | 33 | * [libconfig++-dev](https://packages.debian.org/bookworm/libconfig++-dev), in package `lìbconfig++-dev` 34 | 35 | * libbsd, to enable to change the process name (as shown in `ps`, 36 | so each forked process shows what protocol and what connection it is serving), 37 | which requires `libbsd` at runtime, and `libbsd-dev` at compile-time. 38 | Presence of libbsd is checked by the configure script. 39 | 40 | * libpcre2, in package `libpcre2-dev`. 41 | You can compile with or without it using ENABLE_REGEX in the Makefile. 42 | 43 | * libev-dev, in package `libev-dev`. 44 | If you build a binary specifically and do not build `sslh-ev`, you don't need this. 45 | 46 | * [libproxyprotocol](https://github.com/kosmas-valianos/libproxyprotocol.git) 47 | to support HAProxy's [ProxyProtocol](https://www.haproxy.org/download/2.3/doc/proxy-protocol.txt). 48 | As this is not part of the distribution packages, set 49 | C_INCLUDE_PATH, LD_LIBRARY_PATH, and LIBRARY_PATH to the appropriate 50 | values: 51 | ``` 52 | export C_INCLUDE_PATH=/home/user/src/libproxyprotocol/src 53 | export LD_LIBRARY_PATH=/home/user/src/libproxyprotocol/libs 54 | export LIBRARY_PATH=/home/user/src/libproxyprotocol/libs 55 | ``` 56 | 57 | For OpenSUSE, these are contained in packages libconfig9 and 58 | libconfig-dev in repository 59 | 60 | 61 | For Fedora, you'll need packages `libconfig` and `libconfig-devel`: 62 | 63 | yum install libconfig libconfig-devel 64 | 65 | If you want to rebuild `sslh-conf.c` (after a `make distclean` for example), 66 | you will also need to add [conf2struct](https://www.rutschle.net/tech/conf2struct/README.html) 67 | (v1.5) to your path. 68 | 69 | The test scripts are written in Perl, and will require 70 | `IO::Socket::INET6` (`libio-socket-inet6-perl` in Debian). 71 | 72 | 73 | Compilation 74 | ----------- 75 | First you have to run `./configure` in the _**./sslh**_ directory. After this, 76 | the Makefile is created, and you can do your configuration changes in the Makefile. 77 | After each run of ./configure, those changes are gone and the Makefile is recreated. 78 | 79 | There are a couple of configuration options at the beginning of the Makefile: 80 | 81 | * `# override undefine HAVE_LANDLOCK` if you uncomment this line, sslh will be compiled 82 | without landlock. This works with gcc versions < 12. Otherwise, if your system has 83 | linux/landlock.h in the include path, the configure script creates a _**config.h**_ file, 84 | which defines HAVE_LANDLOCK. It is not enough, to set this to 0, you must delete it, 85 | when you don't wish to have landlock in your binary. 86 | 87 | * `USELIBWRAP` compiles support for host access control (see `hosts_access(3)`), 88 | you will need `libwrap` headers and library to compile (`libwrap0-dev` in Debian). 89 | 90 | * `USELIBCONFIG` compiles support for the configuration file. 91 | You will need `libconfig` headers to compile (`libconfig8-dev` in Debian). 92 | 93 | * `USESYSTEMD` compiles support for using systemd socket activation. 94 | You will need `systemd` headers to compile (`systemd-devel` in Fedora). 95 | 96 | * `USELIBBSD` compiles support for updating the process name (as shown by `ps`). 97 | 98 | * `USELIBCAP` compiles support for libcap, which allows to inherit capabilities to 99 | daughter-processes, which run as restricted users. You need this, when you wish to 100 | make sure, that the --user= parameter can be used, without setting capabilities etc. 101 | to your binaries, to make this work. 102 | 103 | Now you can do either a plain `make` to create the binaries, or you can do an 104 | `make install` to create the binaries and install them. 105 | 106 | 107 | Generating the configuration parser 108 | ----------------------------------- 109 | 110 | The configuration file and command line parser is generated by `conf2struct`, 111 | from `sslhconf.cfg`, which generates `sslh-conf.c` and `sslh-conf.h`. 112 | The resulting files are included in the source 113 | so `sslh` can be built without `conf2struct` installed. 114 | 115 | Further, to prevent build issues, 116 | `sslh-conf.[ch]` has no dependency to `sslhconf.cfg` in the Makefile. 117 | In the event of adding configuration settings, 118 | they need to be regenerated using `make c2s`. 119 | 120 | 121 | Binaries 122 | -------- 123 | 124 | The Makefile produces three different executables: 125 | `sslh-fork`, `sslh-select` and `sslh-ev`: 126 | 127 | * `sslh-fork` forks a new process for each incoming connection. 128 | It is well-tested and very reliable, but incurs the overhead of many processes. 129 | If you are going to use `sslh` for a "small" setup 130 | (less than a dozen ssh connections and a low-traffic https server) 131 | then `sslh-fork` is probably more suited for you. 132 | 133 | * `sslh-select` uses only one thread, which monitors all connections at once. 134 | It only incurs a 16 byte overhead per connection. 135 | Also, if it stops, you'll lose all connections, 136 | which means you can't upgrade it remotely. 137 | If you are going to use `sslh` on a "medium" setup (a few hundreds of connections), 138 | or if you are on a system where forking is expensive (e.g. Windows), 139 | `sslh-select` will be better. 140 | 141 | * `sslh-ev` is similar to `sslh-select`, but uses `libev` as a backend. 142 | This allows using specific kernel APIs that 143 | allow to manage thousands of connections concurrently. 144 | 145 | 146 | Installation 147 | ------------ 148 | 149 | * In general: 150 | ```sh 151 | ./configure 152 | make 153 | cp sslh-fork /usr/local/sbin/sslh 154 | cp basic.cfg /etc/sslh.cfg 155 | vi /etc/sslh.cfg 156 | ``` 157 | * For Debian: 158 | ```sh 159 | cp scripts/etc.init.d.sslh /etc/init.d/sslh 160 | ``` 161 | * For CentOS: 162 | ```sh 163 | cp scripts/etc.rc.d.init.d.sslh.centos /etc/rc.d/init.d/sslh 164 | ``` 165 | 166 | You might need to create links in /etc/rc.d so that the server 167 | start automatically at boot-up, e.g. under Debian: 168 | ```sh 169 | update-rc.d sslh defaults 170 | ``` 171 | 172 | -------------------------------------------------------------------------------- /doc/README.MacOSX: -------------------------------------------------------------------------------- 1 | 2 | sslh is available for Mac OS X via MacPorts. If you have 3 | MacPorts installed on your system you can install sslh by 4 | executing the following in the Terminal: 5 | 6 | port install sslh 7 | 8 | Also, the following is a helpful launchd configuration that 9 | covers the most common use case of sslh. Save the following 10 | into a text file, e.g. 11 | /Library/LaunchDaemons/net.rutschle.sslh.plist, then load it 12 | with launchctl or simply reboot. 13 | 14 | ----BEGIN FILE---- 15 | 16 | 17 | 18 | 19 | Disabled 20 | 21 | KeepAlive 22 | 23 | Label 24 | net.rutschle.sslh 25 | ProgramArguments 26 | 27 | /opt/local/sbin/sslh 28 | -f 29 | -v 30 | -u 31 | nobody 32 | -p 33 | 0.0.0.0:443 34 | --ssh 35 | localhost:22 36 | --tls 37 | localhost:443 38 | 39 | QueueDirectories 40 | 41 | RunAtLoad 42 | 43 | StandardErrorPath 44 | /Library/Logs/sslh.log 45 | StandardOutPath 46 | /Library/Logs/sslh.log 47 | WatchPaths 48 | 49 | 50 | 51 | 52 | ----END FILE---- 53 | 54 | 55 | -------------------------------------------------------------------------------- /doc/README.Windows.md: -------------------------------------------------------------------------------- 1 | It is possible to run `sslh` on Windows. The `fork` model 2 | should be avoided as it is very inefficient on Windows, but 3 | `sslh-select` and `sslh-ev` both work with good performance 4 | (prefer the latter, however). 5 | 6 | 7 | The following script downloads the latest cygwin, the latest version of sslh, and then compiles and copies the binaries with dependancies to an output folder. 8 | 9 | It may be needed to correct it from time to time, but it works. I use it in a virtual machine. 10 | Just retrieve WGET.EXE from https://eternallybored.org/misc/wget/ or git binaries. 11 | 12 | Copy the 3 files 13 | 14 | GO.cmd 15 | wget.exe 16 | compile.sh 17 | 18 | to C root folder, then execute **GO.cmd** with administrative rights. 19 | 20 | with **GO.cmd** 21 | 22 | @ECHO OFF 23 | CD /D "%~dp0" 24 | 25 | NET SESSION >NUL 2>&1 26 | IF %ERRORLEVEL% NEQ 0 ( 27 | ECHO Permission denied. This script must be run as an Administrator. 28 | ECHO: 29 | GOTO FIN 30 | ) ELSE ( 31 | ECHO Running as Administrator. 32 | TIMEOUT /T 2 >NUL 33 | wget --no-check-certificate https://www.cygwin.com/setup-x86_64.exe 34 | IF NOT EXIST setup-x86_64.exe GOTO FIN 35 | MKDIR C:\Z 36 | setup-x86_64.exe -l C:\Z -s ftp://ftp.funet.fi/pub/mirrors/sourceware.org/pub/cygwin/ -q -P make -P git -P gcc-g++ -P autoconf -P automake -P libtool -P libpcre-devel -P libpcre2-devel -P bison -P libev-devel 37 | MKDIR C:\cygwin64\home\user 38 | COPY COMPILE.SH C:\cygwin64\home\user 39 | START C:\cygwin64\bin\mintty.exe /bin/bash --login -i ~/compile.sh 40 | START EXPLORER C:\zzSORTIE 41 | ) 42 | :FIN 43 | PAUSE 44 | EXIT 45 | 46 | 47 | and **compile.sh** 48 | 49 | # SAVE FILE TO UNIX FORMAT 50 | # COPY IT IN C cygwin64 home user 51 | git clone https://github.com/hyperrealm/libconfig.git 52 | cd libconfig 53 | autoreconf -fi 54 | ./configure 55 | make 56 | make install 57 | cd .. 58 | cp /usr/local/lib/libconfig.* /usr/lib 59 | git clone https://github.com/yrutschle/sslh.git 60 | cd sslh 61 | make 62 | cd .. 63 | mkdir /cygdrive/c/zzSORTIE 64 | cp ./sslh/sslh*.exe /cygdrive/c/zzSORTIE 65 | cp /usr/local/bin/cygconfig-11.dll /cygdrive/c/zzSORTIE 66 | cp /cygdrive/c/cygwin64/bin/cygwin1.dll /cygdrive/c/zzSORTIE 67 | cp /cygdrive/c/cygwin64/bin/cygpcreposix-0.dll /cygdrive/c/zzSORTIE 68 | cp /cygdrive/c/cygwin64/bin/cygpcre-1.dll /cygdrive/c/zzSORTIE 69 | cp /cygdrive/c/cygwin64/bin/cygev-4.dll /cygdrive/c/zzSORTIE 70 | cp /cygdrive/c/cygwin64/bin/cygpcre2-8-0.dll /cygdrive/c/zzSORTIE 71 | 72 | This method was contributed by lerenardo on [github](https://github.com/yrutschle/sslh/issues/196#issuecomment-1692805639). 73 | -------------------------------------------------------------------------------- /doc/config.md: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | If you use the scripts provided, sslh will get its 5 | configuration from /etc/sslh.cfg. Please refer to 6 | example.cfg for an overview of all the settings. 7 | 8 | A good scheme is to use the external name of the machine in 9 | `listen`, and bind `httpd` to `localhost:443` (instead of all 10 | binding to all interfaces): that way, HTTPS connections 11 | coming from inside your network don't need to go through 12 | `sslh`, and `sslh` is only there as a frontal for connections 13 | coming from the internet. 14 | 15 | Note that 'external name' in this context refers to the 16 | actual IP address of the machine as seen from your network, 17 | i.e. that that is not `127.0.0.1` in the output of 18 | `ifconfig(8)`. 19 | 20 | Libwrap support 21 | --------------- 22 | 23 | Sslh can optionally perform `libwrap` checks for the sshd 24 | service: because the connection to `sshd` will be coming 25 | locally from `sslh`, `sshd` cannot determine the IP of the 26 | client. 27 | 28 | OpenVPN support 29 | --------------- 30 | 31 | OpenVPN clients connecting to OpenVPN running with 32 | `-port-share` reportedly take more than one second between 33 | the time the TCP connection is established and the time they 34 | send the first data packet. This results in `sslh` with 35 | default settings timing out and assuming an SSH connection. 36 | To support OpenVPN connections reliably, it is necessary to 37 | increase `sslh`'s timeout to 5 seconds. 38 | 39 | Instead of using OpenVPN's port sharing, it is more reliable 40 | to use `sslh`'s `--openvpn` option to get `sslh` to do the 41 | port sharing. 42 | 43 | Using proxytunnel with sslh 44 | --------------------------- 45 | 46 | If you are connecting through a proxy that checks that the 47 | outgoing connection really is SSL and rejects SSH, you can 48 | encapsulate all your traffic in SSL using `proxytunnel` (this 49 | should work with `corkscrew` as well). On the server side you 50 | receive the traffic with `stunnel` to decapsulate SSL, then 51 | pipe through `sslh` to switch HTTP on one side and SSL on the 52 | other. 53 | 54 | In that case, you end up with something like this: 55 | 56 | ssh -> proxytunnel -e ----[ssh/ssl]---> stunnel ---[ssh]---> sslh --> sshd 57 | Web browser -------------[http/ssl]---> stunnel ---[http]--> sslh --> httpd 58 | 59 | Configuration goes like this on the server side, using `stunnel3`: 60 | 61 | stunnel -f -p mycert.pem -d thelonious:443 -l /usr/local/sbin/sslh -- \ 62 | sslh -i --http localhost:80 --ssh localhost:22 63 | 64 | * stunnel options: 65 | * `-f` for foreground/debugging 66 | * `-p` for specifying the key and certificate 67 | * `-d` for specifying which interface and port 68 | we're listening to for incoming connections 69 | * `-l` summons `sslh` in inetd mode. 70 | 71 | * sslh options: 72 | * `-i` for inetd mode 73 | * `--http` to forward HTTP connections to port 80, 74 | and SSH connections to port 22. 75 | 76 | Capabilities support 77 | -------------------- 78 | 79 | On Linux (only?), you can compile sslh with `USELIBCAP=1` set 80 | in the Makefile to make use of POSIX capabilities; this will 81 | save the required capabilities needed for transparent proxying 82 | for unprivileged processes. 83 | 84 | Alternatively, you may use filesystem capabilities instead 85 | of starting sslh as root and asking it to drop privileges. 86 | You will need `CAP_NET_BIND_SERVICE` for listening on port 443 87 | and `CAP_NET_RAW` for transparent proxying (see 88 | `capabilities(7)`). 89 | 90 | You can use the `setcap(8)` utility to give these capabilities 91 | to the executable: 92 | 93 | sudo setcap cap_net_bind_service,cap_net_raw+pe sslh-select 94 | 95 | Then you can run sslh-select as an unprivileged user, e.g.: 96 | 97 | sslh-select -p myname:443 --ssh localhost:22 --tls localhost:443 98 | 99 | Transparent proxy support 100 | ------------------------- 101 | 102 | Transparent proxying is described in its own 103 | [document](tproxy.md). 104 | 105 | It might be easier to configure `sslh` to use Proxyprotocol 106 | if the backend server supports it. 107 | 108 | Systemd Socket Activation 109 | ------------------------- 110 | If compiled with `USESYSTEMD` then it is possible to activate 111 | the service on demand and avoid running any code as root. 112 | 113 | In this mode any listen configuration options are ignored and 114 | the sockets are passed by systemd to the service. 115 | 116 | Example socket unit: 117 | 118 | [Unit] 119 | Before=sslh.service 120 | 121 | [Socket] 122 | ListenStream=1.2.3.4:443 123 | ListenStream=5.6.7.8:444 124 | ListenStream=9.10.11.12:445 125 | FreeBind=true 126 | 127 | [Install] 128 | WantedBy=sockets.target 129 | 130 | Example service unit: 131 | 132 | [Unit] 133 | PartOf=sslh.socket 134 | 135 | [Service] 136 | ExecStart=/usr/sbin/sslh -v -f --ssh 127.0.0.1:22 --tls 127.0.0.1:443 137 | KillMode=process 138 | CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_RAW 139 | PrivateTmp=true 140 | PrivateDevices=true 141 | ProtectSystem=full 142 | ProtectHome=true 143 | User=sslh 144 | 145 | 146 | With this setup only the socket needs to be enabled. The sslh service 147 | will be started on demand and does not need to run as root to bind the 148 | sockets as systemd has already bound and passed them over. If the sslh 149 | service is started on its own without the sockets being passed by systemd 150 | then it will look to use those defined on the command line or config 151 | file as usual. Any number of ListenStreams can be defined in the socket 152 | file and systemd will pass them all over to sslh to use as usual. 153 | 154 | To avoid inconsistency between starting via socket and starting directly 155 | via the service Requires=sslh.socket can be added to the service unit to 156 | mandate the use of the socket configuration. 157 | 158 | Rather than overwriting the entire socket file drop in values can be placed 159 | in /etc/systemd/system/sslh.socket.d/.conf with additional ListenStream 160 | values that will be merged. 161 | 162 | In addition to the above with manual .socket file configuration there is an 163 | optional systemd generator which can be compiled - systemd-sslh-generator 164 | 165 | This parses the /etc/sslh.cfg (or /etc/sslh/sslh.cfg file if that exists 166 | instead) configuration file and dynamically generates a socket file to use. 167 | 168 | This will also merge with any sslh.socket.d drop in configuration but will be 169 | overridden by a /etc/systemd/system/sslh.socket file. 170 | 171 | To use the generator place it in /usr/lib/systemd/system-generators and then 172 | call systemctl daemon-reload after any changes to /etc/sslh.cfg to generate 173 | the new dynamic socket unit. 174 | 175 | Fail2ban 176 | -------- 177 | 178 | If using transparent proxying, just use the standard ssh 179 | rules. If you can't or don't want to use transparent 180 | proxying, you can set `fail2ban` rules to block repeated ssh 181 | connections from an IP address (obviously this depends 182 | on the site, there might be legitimate reasons you would get 183 | many connections to ssh from the same IP address...) 184 | 185 | See example files in scripts/fail2ban. 186 | 187 | UDP 188 | --- 189 | 190 | `sslh` can perform demultiplexing on UDP packets as well. 191 | This does not work with `sslh-fork` (it is not possible to 192 | support UDP with a forking model). Specify a listening 193 | address and target protocols with `is_udp: true`. `sslh` 194 | will wait for incoming UDP packets, run the probes in the 195 | usual fashion, and forward packets to the appropriate 196 | target. `sslh` will then remember the association between 197 | remote host to target server for 60 seconds by default, 198 | which can be overridden with `udp_timeout`. This allows to 199 | process both single-datagram protocols such as DNS, and 200 | connection-based protocols such as QUIC. 201 | 202 | An example for supporting QUIC is shown in `example.cfg`. 203 | -------------------------------------------------------------------------------- /doc/detailed-ip-transparency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yrutschle/sslh/b7556c07bddf0e666ea9bf7bdfb0a140044190b0/doc/detailed-ip-transparency.png -------------------------------------------------------------------------------- /doc/podman.md: -------------------------------------------------------------------------------- 1 | Here is an example of deployment of sslh in transparent mode 2 | using ansible and podman, [submitted by Github user 3 | olegstepura](https://github.com/yrutschle/sslh/issues/448) 4 | 5 | ```yaml 6 | # ansible podman task 7 | - name: "Create sslh-co pod" 8 | containers.podman.podman_pod: 9 | name: sslh-co # read as "sslh and company" 10 | state: started 11 | ports: 12 | - "80:80" 13 | - "443:444" 14 | # ... (more ports if needed) 15 | network: 16 | - '{{ containers.config.network }}' # other services from this network can access containers in this network for example prometheus can read caddy metrics at sslh-co:2020, also caddy itself can connect to other services to act as a reverse proxy 17 | 18 | - name: "Create the sslh container" 19 | containers.podman.podman_container: 20 | name: sslh 21 | image: "yrutschle/sslh:latest" 22 | pod: sslh-co 23 | capabilities: 24 | - NET_RAW 25 | - NET_BIND_SERVICE 26 | - NET_ADMIN 27 | sysctl: 28 | net.ipv4.conf.default.route_localnet: 1 29 | net.ipv4.conf.all.route_localnet: 1 30 | expose: 31 | - 444 32 | volume: 33 | # ... (make sure to mount config as you like) 34 | command: --transparent -F/etc/sslh/sslh.cfg # parameter --transparent here is needed to trigger configure_iptables in init script 35 | state: started 36 | 37 | - name: "Create the caddy container" 38 | containers.podman.podman_container: 39 | name: caddy 40 | image: "lucaslorentz/caddy-docker-proxy:alpine" # regular caddy or nginx image will also work 41 | pod: sslh-co 42 | expose: 43 | - 80 44 | - 443 45 | - 2020 # metrics, since caddy-docker-proxy uses :2019 internally 46 | volume: 47 | # ... (mount your configs and other stuff here) 48 | - "/var/run/podman/podman.sock:/var/run/docker.sock" 49 | state: started 50 | notify: podman restart sslh 51 | 52 | - name: "Create the SSH proxy to host container" 53 | containers.podman.podman_container: 54 | name: ssh-proxy 55 | image: "alpine/socat:latest" 56 | pod: sslh-co 57 | expose: 58 | - 222 59 | command: TCP-LISTEN:222,fork TCP:host.containers.internal:22 60 | state: started 61 | ``` 62 | 63 | ```ini 64 | # sslh config 65 | foreground: true; 66 | inetd: false; 67 | numeric: true; 68 | transparent: true; 69 | timeout: 5; 70 | 71 | listen: 72 | ( 73 | { host: "0.0.0.0"; port: "444"; keepalive: true; }, 74 | ); 75 | 76 | protocols: 77 | ( 78 | { 79 | name: "ssh"; 80 | service: "ssh"; 81 | host: "localhost"; 82 | port: "222"; 83 | fork: true; 84 | }, 85 | { 86 | name: "http"; 87 | host: "localhost"; 88 | port: "80"; 89 | }, 90 | { 91 | name: "tls"; 92 | host: "localhost"; 93 | port: "443"; 94 | }, 95 | ); 96 | ``` 97 | 98 | I omitted caddy configs here as it's not important. Some unrelated container configs were also dropped. 99 | In the example above `sslh`, `caddy` and `ssh-proxy` are 3 containers in the same pod, all listening on `localhost`. SSLH has to listen on `444` because caddy already listens on `443` and it's more complex to reconfigure caddy port because of let's encrypt (caddy itself "thinks" it is bound to your host interface). 100 | 101 | Scheme of port mapping is (all containers share same `localhost`): 102 | ``` 103 | host 443 → pod 444 (sslh) → pod 443 (caddy) 104 | ``` 105 | - podman connects host `443` port to pod's `444` port 106 | - `sslh` listens `444` on `localhost`, reroutes tls to `caddy` on `localhost:443` 107 | - `caddy` listens `443` on `localhost` (reverse-proxies to other apps on private network) 108 | Reverse-proxied services get correct IP in `X-Forwarded-For` header. 109 | 110 | Takeaways: 111 | - `net.ipv4.conf.default.route_localnet` is setup only in container, not on host, which is nice. 112 | - same with iptables and route rules applied by `init` script in sslh container 113 | - `sslh` proxy to other services in the private network will not work (because transparent mode is enabled) even if that other service does not need real IP 114 | - with transparent mode all services that `sslh` will connect to should be attached to this pod making it listen on `localhost` of the pod 115 | - ports of containers should not clash 116 | - ports should be published on pod level, not on containers 117 | - containers should not be configured to connect to custom networks 118 | - because of the above proxying ssh to host will also not work out of the box, `sslh` should connect to `localhost` and not to a random IP. While adding another proxy as a container to the same pod sounds like an overkill I don't see any other solution, so `socat` is used as an additional proxy. 119 | - `reverse_proxy` by caddy to other containers in the same custom network (as pod is attached to) works (e.g. caddy can connect to other IPs from custom network). `socat` can also connect to host and/or other containers in custom network. 120 | 121 | -------------------------------------------------------------------------------- /doc/scenarios-for-simple-transparent-proxy.md: -------------------------------------------------------------------------------- 1 | # Three Scenarios for the simple transparent proxy setup # 2 | 3 | ![Simple Transparent Proxy Examples](./sslh-examples-v3.png) 4 | 5 | ## Introduction ## 6 | The first example is the configuration, which was described in the previous document. I omitted the loopback interface "lo" in those diagrams, trying not no overload the picture. 7 | The connections have two different endings, showing the direction of the opening connection (SYN flag) and the answer connection (SYN-ACK flags). This is important, as the traffic in the transparent proxy setup flows somewhat unexpected. 8 | 9 | ## Example 1 ## 10 | The first example shows the setup, which is described in the [previous document](./simple_transparent_proxy.md). You see the Client connecting to sslh (red connection). When sslh accepts this connection, the SYN-ACK packet is send back to the client, which sends the first data packet(s) together with the ACK for the SYN-ACK. So the bidirectional tcp connection is fully open. 11 | Sslh opens now the blue connection to sshd and needs for that elevation rights, as it uses the clients IP address as its own address for opening this connection. Now things are becoming complicated: Sshd send back the first packet with SYN-ACK flags (green line), addressed to the clients IP (dotted line). As already described, that would go wrong, so our routing trick makes this packet beeing deflected back to sslh, so this tcp connection is also opened. But we have here now an asymetric behaviour, that the read and write pathes of the tcp connection are going different routes. 12 | The sslh process shuffles now all the bytes coming from sshd from the green line to the red line, vice versa for the packets from the client. 13 | 14 | ## Example 2 ## 15 | In this example sshd is running on another server. No matter, if this is docker, kvm, virtualbox or another physical host, connected with an ethernet cable. Here we need no dummy interface, so we need another way, to configure our routing deflection. The principle is the same: We need to force packets coming back from sshd going to sslh and not directly back to the client. 16 | In this case its your decision, where those rules will be tied in, options are: 17 | 18 | * the startscript of sslh 19 | * the docker or kvm configuration 20 | * the configuration of the outgoing interface 21 | 22 | Its two lines you need: 23 | ``` 24 | ip rule add from SSHD_ADDRESS/32 table sslh 25 | ip route add local 0.0.0.0/0 dev lo table sslh 26 | ``` 27 | On the sshd host, we need no additional rules, as all traffic is coming back to our sslh host, because this is in this setting the default gateway. The only thing, we need to do: Assign a unique IP address only for sshd and all other services, you wish to hide behind sslh and host on this device. 28 | 29 | There are two ways, how you can add multiple ip addresses to one device. The new _**ip addr add**_ supports multiple add statements to one and the same interface name. So you can just duplicate the interface stancas in the _**/etc/network/interfaces**_ configuration. The problem with this method is, that some older managment tools, like ifconfig are unable to show the additional addresses. So when you are used to some older tools, you may configure sub-interfaces like eth0:1. 30 | However my recommendation is, migrate to new tools, get used to it, as old tools don't show you the whole configuration! 31 | 32 | ## Example 3 ## 33 | This is now the extended version of the previous example. The target host has another path back to the client, as there is a default route to another interface. Now we need **TWO** routing deflections, one on the sslh host, like in scenario 2, and one on the sshd target host. 34 | The routing setup on the target host looks like: 35 | * Add an routing table name for the deflection table in _**/etc/iproute2/rt_tables**_ 36 | * Find a location, where you will hook the two routing rules in 37 | ``` 38 | ip rule add from SSHD_ADDRESS/32 table sslh_routeback 39 | ip route add default via SERVER1_ETH1_IP dev eth0 table sslh_routeback 40 | 41 | ``` 42 | This is setting up a default route for all traffic, originating from the ip address sshd (or any other service) is using, back to the host, hosting sslh. On that host, those packets will be deflected again with the same rule from scenario 2. 43 | 44 | Be aware, that scenario 3 can look very different and the picture above shows only one of those setups. Each configuration, where packets from the target system can find their way back, without beeing forcibly routed through the sslh hosting system, belongs into this category. This are e.g. virtual machines or containers, having interfaces in the same network, like the sslh hosting system. Even, when they look in some drawings embedded in their host, as soon, as they have network interfaces, allowing a direct connection to the client, it is scenario 3! 45 | 46 | ## Modifications ## 47 | Now you can think about many modifications, but the tools will be the same, for all other thinkable scenarios. You must always make sure, that packets from foreign hosts, will find their way back to the sslh host. So if the chain consists of three or four servers, all need the deflection rules. 48 | 49 | ## Important Finding On Routing ## 50 | When I went ahead and wrote in my first drawings the warning, that the kernel in scenario 2 and 3 needs to have forwarding in place, I finally tested, that this is not true. **Both scenarios are working without kernel forwarding beeing activated!** 51 | The background: The deflecting routing table cames into the game, before the kernel has to made the decision, that packets with non local ip addresses in source and destination must be forwarded. After the routing rule deliveres the packet to sslh and sslh rewrites the source ip, the packet is treated as local, and can pass the system. 52 | -------------------------------------------------------------------------------- /doc/sslh-examples-v3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yrutschle/sslh/b7556c07bddf0e666ea9bf7bdfb0a140044190b0/doc/sslh-examples-v3.png -------------------------------------------------------------------------------- /echo_test.cfg: -------------------------------------------------------------------------------- 1 | 2 | # TODO: c2s does not warn if udp: 1 (instead of 'true') 3 | 4 | udp: true; 5 | 6 | prefix: "hello"; 7 | 8 | listen: "localhost:9000"; 9 | 10 | listen-host: "localhost"; 11 | listen-port: "9000"; 12 | 13 | -------------------------------------------------------------------------------- /echosrv-conf.h: -------------------------------------------------------------------------------- 1 | /* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README) 2 | * on Sun Apr 6 11:44:59 2025. 3 | 4 | # conf2struct: generate libconf parsers that read to structs 5 | # Copyright (C) 2018-2024 Yves Rutschle 6 | # All rights reserved. 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions are met: 10 | # 11 | # 1. Redistributions of source code must retain the above copyright notice, 12 | # this list of conditions and the following disclaimer. 13 | # 2. Redistributions in binary form must reproduce the above copyright 14 | # notice, this list of conditions and the following disclaimer in the 15 | # documentation and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | # POSSIBILITY OF SUCH DAMAGE. 28 | 29 | */ 30 | 31 | #ifndef C2S_ECHOCFG_H 32 | #define C2S_ECHOCFG_H 33 | #ifdef LIBCONFIG 34 | # include 35 | #endif 36 | 37 | 38 | 39 | struct echocfg_listen_item { 40 | char* host; 41 | char* port; 42 | }; 43 | 44 | struct echocfg_item { 45 | int udp; 46 | char* prefix; 47 | size_t listen_len; 48 | struct echocfg_listen_item* listen; 49 | }; 50 | 51 | int echocfg_parse_file( 52 | const char* filename, 53 | struct echocfg_item* echocfg, 54 | const char** errmsg); 55 | 56 | void echocfg_fprint( 57 | FILE* out, 58 | struct echocfg_item *echocfg, 59 | int depth); 60 | 61 | int echocfg_cl_parse( 62 | int argc, 63 | char* argv[], 64 | struct echocfg_item *echocfg); 65 | 66 | #endif 67 | -------------------------------------------------------------------------------- /echosrv.c: -------------------------------------------------------------------------------- 1 | /* echosrv: a simple line echo server with optional prefix adding. 2 | * 3 | * echosrv --listen localhost6:1234 --prefix "ssl: " 4 | * 5 | * This will bind to 1234, and echo every line pre-pending "ssl: ". This is 6 | * used for testing: we create several such servers with different prefixes, 7 | * then we connect test clients that can then check they get the proper data 8 | * back (thus testing that shoveling works both ways) with the correct prefix 9 | * (thus testing it connected to the expected service). 10 | * **/ 11 | 12 | #define _GNU_SOURCE 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #define cfg sslhcfg 32 | #include "common.h" 33 | #undef cfg 34 | 35 | #include "echosrv-conf.h" 36 | 37 | /* Added to make the code compilable under CYGWIN 38 | * */ 39 | #ifndef SA_NOCLDWAIT 40 | #define SA_NOCLDWAIT 0 41 | #endif 42 | 43 | struct echocfg_item cfg; 44 | 45 | void check_res_dump(int res, struct addrinfo *addr, char* syscall) 46 | { 47 | char buf[NI_MAXHOST]; 48 | 49 | if (res == -1) { 50 | if (addr) 51 | fprintf(stderr, "error %s:%s: %s\n", 52 | sprintaddr(buf, sizeof(buf), addr), 53 | syscall, 54 | strerror(errno)); 55 | else 56 | fprintf(stderr, "Dying just because\n"); 57 | 58 | exit(1); 59 | } 60 | } 61 | 62 | void start_echo(int fd) 63 | { 64 | ssize_t res; 65 | char buffer[1 << 20]; 66 | ssize_t ret; 67 | size_t prefix_len; 68 | int first = 1; 69 | 70 | prefix_len = strlen(cfg.prefix); 71 | 72 | memset(buffer, 0, sizeof(buffer)); 73 | strcpy(buffer, cfg.prefix); 74 | 75 | while (1) { 76 | ret = read(fd, buffer + prefix_len, sizeof(buffer) - prefix_len); 77 | if (ret <= 0) { 78 | fprintf(stderr, "%s", strerror(errno)); 79 | return; 80 | } 81 | if (first) { 82 | res = write(fd, buffer, ret + prefix_len); 83 | first = 0; 84 | if (write(1, buffer, ret + prefix_len) < 0) { 85 | fprintf(stderr, "%s", strerror(errno)); 86 | } 87 | } else { 88 | res = write(fd, buffer + prefix_len, ret); 89 | } 90 | if (res < 0) { 91 | fprintf(stderr, "%s", strerror(errno)); 92 | return; 93 | } 94 | } 95 | } 96 | 97 | /* TCP echo server: accepts connections to an endpoint, forks an echo for each 98 | * connection, forever. Prefix is added at start of response stream */ 99 | void tcp_echo(struct listen_endpoint* listen_socket) 100 | { 101 | while (1) { 102 | int in_socket = accept(listen_socket->socketfd, 0, 0); 103 | if (in_socket == -1) { 104 | perror("tcp_echo:accept"); 105 | exit(1); 106 | } 107 | 108 | if (!fork()) 109 | { 110 | close(listen_socket->socketfd); 111 | start_echo(in_socket); 112 | exit(0); 113 | } 114 | close(in_socket); 115 | waitpid(-1, NULL, WNOHANG); 116 | } 117 | } 118 | 119 | void print_udp_xchange(int sockfd, struct sockaddr* addr, socklen_t addrlen) 120 | { 121 | struct addrinfo src_addrinfo, to_addrinfo; 122 | char str_addr[NI_MAXHOST+1+NI_MAXSERV+1]; 123 | char str_addr2[NI_MAXHOST+1+NI_MAXSERV+1]; 124 | struct sockaddr_storage ss; 125 | 126 | src_addrinfo.ai_addr = (struct sockaddr*)&ss; 127 | src_addrinfo.ai_addrlen = sizeof(ss); 128 | getsockname(sockfd, src_addrinfo.ai_addr, &src_addrinfo.ai_addrlen); 129 | 130 | to_addrinfo.ai_addr = addr; 131 | to_addrinfo.ai_addrlen = sizeof(*addr); 132 | 133 | fprintf(stderr, "UDP local %s remote %s\n", 134 | sprintaddr(str_addr, sizeof(str_addr), &src_addrinfo), 135 | sprintaddr(str_addr2, sizeof(str_addr2), &to_addrinfo) 136 | ); 137 | } 138 | 139 | /* UDP echo server: receive packets, return them, forever. 140 | * Prefix is added at each packet */ 141 | void udp_echo(struct listen_endpoint* listen_socket) 142 | { 143 | char data[65536]; 144 | struct sockaddr src_addr; 145 | socklen_t addrlen; 146 | 147 | memset(data, 0, sizeof(data)); 148 | 149 | size_t prefix_len = strlen(cfg.prefix); 150 | memcpy(data, cfg.prefix, prefix_len); 151 | 152 | while (1) { 153 | addrlen = sizeof(src_addr); 154 | ssize_t len = recvfrom(listen_socket->socketfd, 155 | data + prefix_len, 156 | sizeof(data) - prefix_len, 157 | 0, 158 | &src_addr, 159 | &addrlen); 160 | 161 | if (len < 0) { 162 | perror("recvfrom"); 163 | } 164 | *(data + prefix_len + len) = 0; 165 | fprintf(stderr, "%zd %s\n", len, data + prefix_len); 166 | 167 | print_udp_xchange(listen_socket->socketfd, &src_addr, addrlen); 168 | 169 | ssize_t res = sendto(listen_socket->socketfd, 170 | data, 171 | len + prefix_len, 172 | 0, 173 | &src_addr, 174 | addrlen); 175 | if (res < 0) { 176 | perror("sendto"); 177 | } 178 | } 179 | } 180 | 181 | void main_loop(struct listen_endpoint listen_sockets[], int num_addr_listen) 182 | { 183 | int i; 184 | 185 | for (i = 0; i < num_addr_listen; i++) { 186 | if (!fork()) { 187 | if (cfg.udp) { 188 | udp_echo(&listen_sockets[i]); 189 | } else { 190 | tcp_echo(&listen_sockets[i]); 191 | } 192 | } 193 | } 194 | wait(NULL); 195 | } 196 | 197 | /* Following is a number of utility functions copied from common.c: linking 198 | * against common.o directly means echosrv has to work with sslh config struct, 199 | * which makes it all too awkward */ 200 | 201 | /* simplified from common.c */ 202 | char* sprintaddr(char* buf, size_t size, struct addrinfo *a) 203 | { 204 | char host[NI_MAXHOST], serv[NI_MAXSERV]; 205 | int res; 206 | 207 | res = getnameinfo(a->ai_addr, a->ai_addrlen, 208 | host, sizeof(host), 209 | serv, sizeof(serv), 210 | 0 ); 211 | 212 | if (res) { 213 | /* Name resolution failed: do it numerically instead */ 214 | res = getnameinfo(a->ai_addr, a->ai_addrlen, 215 | host, sizeof(host), 216 | serv, sizeof(serv), 217 | NI_NUMERICHOST | NI_NUMERICSERV); 218 | /* should not fail but... */ 219 | if (res) { 220 | strcpy(host, "?"); 221 | strcpy(serv, "?"); 222 | } 223 | } 224 | 225 | snprintf(buf, size, "%s:%s", host, serv); 226 | 227 | return buf; 228 | } 229 | 230 | 231 | /* simplified from common.c */ 232 | int listen_single_addr(struct addrinfo* addr, int keepalive, int udp) 233 | { 234 | struct sockaddr_storage *saddr; 235 | int sockfd, one, res; 236 | 237 | saddr = (struct sockaddr_storage*)addr->ai_addr; 238 | 239 | sockfd = socket(saddr->ss_family, udp ? SOCK_DGRAM : SOCK_STREAM, 0); 240 | check_res_dump(sockfd, addr, "socket"); 241 | 242 | one = 1; 243 | res = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (char*)&one, sizeof(one)); 244 | check_res_dump(res, addr, "setsockopt(SO_REUSEADDR)"); 245 | 246 | if (addr->ai_addr->sa_family == AF_INET6) { 247 | res = setsockopt(sockfd, IPPROTO_IPV6, IPV6_V6ONLY, (char*)&one, sizeof(one)); 248 | check_res_dump(res, addr, "setsockopt(IPV6_V6ONLY)"); 249 | } 250 | 251 | res = bind(sockfd, addr->ai_addr, addr->ai_addrlen); 252 | check_res_dump(res, addr, "bind"); 253 | 254 | if (!udp) { 255 | res = listen (sockfd, 50); 256 | check_res_dump(res, addr, "listen"); 257 | } 258 | 259 | return sockfd; 260 | } 261 | 262 | /* simplified from common.c */ 263 | int resolve_split_name(struct addrinfo **out, char* host, char* serv) 264 | { 265 | struct addrinfo hint; 266 | char *end; 267 | int res; 268 | 269 | memset(&hint, 0, sizeof(hint)); 270 | hint.ai_family = PF_UNSPEC; 271 | hint.ai_socktype = SOCK_STREAM; 272 | 273 | /* If it is a RFC-Compliant IPv6 address ("[1234::12]:443"), remove brackets 274 | * around IP address */ 275 | if (host[0] == '[') { 276 | end = strrchr(host, ']'); 277 | if (!end) { 278 | fprintf(stderr, "%s: no closing bracket in IPv6 address?\n", host); 279 | return -1; 280 | } 281 | host++; /* skip first bracket */ 282 | *end = 0; /* remove last bracket */ 283 | } 284 | 285 | res = getaddrinfo(host, serv, &hint, out); 286 | 287 | if (res) 288 | fprintf(stderr, "%s `%s:%s'\n", gai_strerror(res), host, serv); 289 | return res; 290 | } 291 | 292 | int start_listen_sockets(struct listen_endpoint *sockfd[]) 293 | { 294 | struct addrinfo *addr, *start_addr; 295 | char buf[NI_MAXHOST]; 296 | int i, res; 297 | int num_addr = 0, keepalive = 0, udp = 0; 298 | 299 | *sockfd = NULL; 300 | 301 | fprintf(stderr, "Listening to:\n"); 302 | 303 | for (i = 0; i < cfg.listen_len; i++) { 304 | udp = cfg.udp; 305 | 306 | 307 | res = resolve_split_name(&start_addr, cfg.listen[i].host, cfg.listen[i].port); 308 | if (res) exit(4); 309 | 310 | for (addr = start_addr; addr; addr = addr->ai_next) { 311 | num_addr++; 312 | *sockfd = realloc(*sockfd, num_addr * sizeof(*sockfd)); 313 | (*sockfd)[num_addr-1].socketfd = listen_single_addr(addr, keepalive, udp); 314 | (*sockfd)[num_addr-1].type = udp ? SOCK_DGRAM : SOCK_STREAM; 315 | fprintf(stderr, "%d:\t%s\n", (*sockfd)[num_addr-1].socketfd, sprintaddr(buf, sizeof(buf), addr)); 316 | } 317 | freeaddrinfo(start_addr); 318 | } 319 | 320 | return num_addr; 321 | } 322 | 323 | int main(int argc, char *argv[]) 324 | { 325 | int num_addr_listen; 326 | 327 | struct listen_endpoint *listen_sockets; 328 | 329 | memset(&cfg, 0, sizeof(cfg)); 330 | if (echocfg_cl_parse(argc, argv, &cfg)) 331 | exit(1); 332 | 333 | echocfg_fprint(stdout, &cfg, 0); 334 | 335 | num_addr_listen = start_listen_sockets(&listen_sockets); 336 | 337 | main_loop(listen_sockets, num_addr_listen); 338 | 339 | return 0; 340 | } 341 | -------------------------------------------------------------------------------- /echosrv.cfg: -------------------------------------------------------------------------------- 1 | # conf2struct for echosrv 2 | 3 | header: "echosrv-conf.h"; 4 | parser: "echosrv-conf.c"; 5 | 6 | printer: true; 7 | 8 | conffile_option: ("F", "config"); 9 | 10 | config: { 11 | name: "echocfg", 12 | type: "list", 13 | items: ( 14 | {name: "udp", type: "bool"; default: false; }, 15 | {name: "prefix", type: "string"; }, 16 | { name: "listen", 17 | type: "list", 18 | items: ( 19 | { name: "host"; type: "string"; var: true; }, 20 | { name: "port"; type: "string"; var: true; } 21 | ) 22 | } 23 | ) 24 | } 25 | 26 | 27 | cl_groups: ( 28 | { name: "listen"; pattern: "(.+):(\w+)"; description: "Listen on host:port"; 29 | short: "p"; argdesc: ""; 30 | list: "listen"; 31 | # no override, this just adds to the list (and thus can be specified several times) 32 | targets: ( 33 | { path: "host"; value: "$1" }, 34 | { path: "port"; value: "$2" } 35 | ); 36 | } 37 | ) 38 | -------------------------------------------------------------------------------- /echoѕrv-conf.h: -------------------------------------------------------------------------------- 1 | /* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README) 2 | * on Sat Nov 7 09:19:26 2020. 3 | 4 | # conf2struct: generate libconf parsers that read to structs 5 | # Copyright (C) 2018-2019 Yves Rutschle 6 | # All rights reserved. 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions are met: 10 | # 11 | # 1. Redistributions of source code must retain the above copyright notice, 12 | # this list of conditions and the following disclaimer. 13 | # 2. Redistributions in binary form must reproduce the above copyright 14 | # notice, this list of conditions and the following disclaimer in the 15 | # documentation and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | # POSSIBILITY OF SUCH DAMAGE. 28 | 29 | */ 30 | 31 | #ifndef C2S_SSLHCFG_H 32 | #define C2S_SSLHCFG_H 33 | #ifdef LIBCONFIG 34 | # include 35 | #endif 36 | 37 | 38 | #include "probe.h" 39 | #include 40 | #include 41 | #include 42 | 43 | struct sslhcfg_listen_item { 44 | char* host; 45 | char* port; 46 | int keepalive; 47 | }; 48 | 49 | struct sslhcfg_protocols_item { 50 | char* name; 51 | char* host; 52 | char* port; 53 | int service_is_present; 54 | char* service; 55 | int fork; 56 | int tfo_ok; 57 | int log_level; 58 | int keepalive; 59 | size_t sni_hostnames_len; 60 | char** sni_hostnames; 61 | size_t alpn_protocols_len; 62 | char** alpn_protocols; 63 | size_t regex_patterns_len; 64 | char** regex_patterns; 65 | int minlength_is_present; 66 | int minlength; 67 | T_PROBE* probe; 68 | struct addrinfo* saddr; 69 | void* data; 70 | }; 71 | 72 | struct sslhcfg_item { 73 | char* prefix; 74 | int verbose; 75 | int foreground; 76 | int inetd; 77 | int numeric; 78 | int transparent; 79 | int timeout; 80 | int user_is_present; 81 | char* user; 82 | int pidfile_is_present; 83 | char* pidfile; 84 | int chroot_is_present; 85 | char* chroot; 86 | char* syslog_facility; 87 | char* on_timeout; 88 | size_t listen_len; 89 | struct sslhcfg_listen_item* listen; 90 | size_t protocols_len; 91 | struct sslhcfg_protocols_item* protocols; 92 | }; 93 | 94 | int sslhcfg_parse_file( 95 | const char* filename, 96 | struct sslhcfg_item* sslhcfg, 97 | const char** errmsg); 98 | 99 | void sslhcfg_fprint( 100 | FILE* out, 101 | struct sslhcfg_item *sslhcfg, 102 | int depth); 103 | 104 | int sslhcfg_cl_parse( 105 | int argc, 106 | char* argv[], 107 | struct sslhcfg_item *sslhcfg); 108 | 109 | #endif 110 | -------------------------------------------------------------------------------- /example.cfg: -------------------------------------------------------------------------------- 1 | # This file is provided as documentation to show what is 2 | # possible. It should not be used as-is, and probably should 3 | # not be used as a starting point for a working 4 | # configuration. Instead use basic.cfg. 5 | 6 | foreground: true; 7 | inetd: false; 8 | numeric: false; 9 | transparent: false; 10 | timeout: 2; 11 | user: "nobody"; 12 | pidfile: "/var/run/sslh.pid"; 13 | chroot: "/var/empty"; 14 | 15 | # Logging configuration 16 | # Value: 1: stdout; 2: syslog; 3: stdout+syslog; 4: logfile; ...; 7: all 17 | # Defaults are indicated here, and should be sensible. Generally, you want *-error 18 | # to be always enabled, to know if something is going wrong. 19 | # Each option relates to a different set of messages. 20 | verbose-config: 0; # print configuration at startup 21 | verbose-config-error: 3; # print configuration errors 22 | verbose-connections: 3; # trace established incoming address to forward address 23 | verbose-connections-error: 3; # connection errors 24 | verbose-connections-try: 0; # connection attempts towards targets 25 | verbose-fd: 0; # file descriptor activity, open/close/whatnot 26 | verbose-packets: 0; # hexdump packets on which probing is done 27 | verbose-probe-info: 0; # what's happening during the probe process 28 | verbose-probe-error: 3; # failures and problems during probing 29 | verbose-system-error: 3; # system call problem, i.e. malloc, fork, failing 30 | verbose-int-error: 3; # internal errors, the kind that should never happen 31 | 32 | # This one is special and overrides all previous options if 33 | # set, as a quick way to get "as much as possible" 34 | #verbose: 3; 35 | 36 | # Specify a path to the logfile. 37 | #logfile: "/var/log/sslh.log" 38 | 39 | # Specify the number of concurrent UDP connection that can 40 | # be managed (default 1024) 41 | udp_max_connections: 16; 42 | 43 | # Specify which syslog facility to use (names for your 44 | # system are usually defined in /usr/include/*/sys/syslog.h 45 | # or equivalent) 46 | # Default is "auth" 47 | # "none" disables use of syslog 48 | syslog_facility: "auth"; 49 | 50 | # List of interfaces on which we should listen 51 | # Options: 52 | listen: 53 | ( 54 | { host: "thelonious"; port: "443"; }, 55 | { host: "thelonious"; port: "8080"; keepalive: true; }, 56 | { host: "thelonious"; is_udp: true; port: "443"; }, 57 | { host: "/tmp/unix_socket"; is_unix: true; port: ""; } 58 | ); 59 | 60 | # List of protocols 61 | # 62 | # Each protocol entry consists of: 63 | # name: name of the probe. These are listed on the command 64 | # line (ssh -?), plus 'regex' and 'timeout'. 65 | 66 | # service: (optional) libwrap service name (see hosts_access(5)) 67 | # host, port: where to connect when this probe succeeds 68 | # log_level: 0 to turn off logging 69 | # 1 to log each incoming connection 70 | # keepalive: Should TCP keepalive be on or off for that 71 | # connection (default is off) 72 | # fork: Should a new process be forked for this protocol? 73 | # (only useful for sslh-select) 74 | # tfo_ok: Set to true if the server supports TCP FAST OPEN 75 | # resolve_on_forward: Set to true if server address should be resolved on 76 | # (every) newly incoming connection (again) 77 | # transparent: Set to true to proxy this protocol 78 | # transparently (server sees the remote client IP 79 | # address). Same as the global option, but per-protocol 80 | # is_unix: [true|false] connect to a UNIX socket. The host 81 | # field becomes the pathname to the socket, and the port 82 | # field is unused (but necessary). 83 | # proxyprotocol: <1|2>; When connecting to the backend 84 | # server, a proxyprotocol header of the specified 85 | # version will be added, containing the client's 86 | # connection information. 87 | # 88 | # Probe-specific options: 89 | # (sslh will try each probe in order they are declared, and 90 | # connect to the first that matches.) 91 | # 92 | # tls: 93 | # sni_hostnames: list of FQDN for that target. Each name can 94 | # include wildcard following glob(7) rules. 95 | 96 | # alpn_protocols: list of ALPN protocols for that target, see: 97 | # https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids 98 | # 99 | # if both sni_hostnames AND alpn_protocols are specified, both must match 100 | # 101 | # if neither are set, it is just checked whether this is the TLS protocol or not 102 | # 103 | # Obviously set the most specific probes 104 | # first, and if you use TLS with no ALPN/SNI 105 | # set it as the last TLS probe 106 | # regex: 107 | # regex_patterns: list of patterns to match for 108 | # that target. 109 | # 110 | # You can specify several of 'regex' and 'tls'. 111 | # 112 | # If you want to filter on incoming IP addresses, you can 113 | # use libwrap which will use /etc/hosts.allow and 114 | # /etc/hosts.deny. 115 | 116 | protocols: 117 | ( 118 | { name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; 119 | keepalive: true; fork: true; tfo_ok: true }, 120 | 121 | # UNIX socket to a local NGINX. The socket name is in 'host'; 'port' is necessary but not used. 122 | { name: "http"; is_unix: true; host: "/tmp/nginx.sock"; port: ""; }, 123 | 124 | # match BOTH ALPN/SNI 125 | { name: "tls"; host: "localhost"; port: "5223"; alpn_protocols: [ "xmpp-client" ]; sni_hostnames: [ "im.somethingelse.net" ]; log_level: 0; tfo_ok: true }, 126 | 127 | # just match ALPN 128 | { name: "tls"; host: "localhost"; port: "443"; alpn_protocols: [ "h2", "http/1.1", "spdy/1", "spdy/2", "spdy/3" ]; log_level: 0; tfo_ok: true }, 129 | { name: "tls"; host: "localhost"; port: "xmpp-client"; alpn_protocols: [ "xmpp-client" ]; log_level: 0; tfo_ok: true }, 130 | 131 | # just match SNI 132 | { name: "tls"; host: "localhost"; port: "993"; sni_hostnames: [ "mail.rutschle.net", "mail.englishintoulouse.com" ]; log_level: 0; tfo_ok: true }, 133 | { name: "tls"; host: "localhost"; port: "xmpp-client"; sni_hostnames: [ "im.rutschle.net", "im.englishintoulouse.com" ]; log_level: 0; tfo_ok: true }, 134 | 135 | # Let's Encrypt (tls-alpn-* challenges) 136 | { name: "tls"; host: "localhost"; port: "letsencrypt-client"; alpn_protocols: [ "acme-tls/1" ]; log_level: 0;}, 137 | 138 | # catch anything else TLS 139 | { name: "tls"; host: "localhost"; port: "443"; tfo_ok: true }, 140 | 141 | # Forward UDP 142 | { name: "regex"; host: "localhost"; is_udp: true; port: "123"; 143 | udp_timeout: 20; # Time after which the "connection" is forgotten 144 | regex_patterns: [ "hello" ]; }, 145 | # Forward Teamspeak3 (Voice only) 146 | { name: "teamspeak"; host: "localhost"; is_udp: true; port: "9987"; }, 147 | # Forward IETF QUIC-50 ("Q050" -> "\x51\x30\x35\x30") 148 | # Remember that the regex needs to be adjusted for every supported QUIC version. 149 | { name: "regex"; host: "localhost"; is_udp: true; port: "4433"; regex_patterns: [ "\x51\x30\x35\x30" ]; }, 150 | 151 | # Regex examples -- better use the built-in probes for real-world use! 152 | # OpenVPN 153 | { name: "regex"; host: "localhost"; port: "1194"; regex_patterns: [ "^\x00[\x0D-\xFF]$", "^\x00[\x0D-\xFF]\x38" ]; }, 154 | # Jabber 155 | { name: "regex"; host: "localhost"; port: "5222"; regex_patterns: [ "jabber" ]; 156 | minlength: 60; # Won't even try to match the regex if we don't have that many bytes 157 | }, 158 | 159 | # Catch-all (but better use 'anyprot') 160 | { name: "regex"; host: "localhost"; port: "443"; regex_patterns: [ "" ]; }, 161 | 162 | # Where to connect in case of timeout (defaults to ssh) 163 | { name: "timeout"; service: "daytime"; host: "localhost"; port: "daytime"; } 164 | ); 165 | 166 | # Optionally, specify to which protocol to connect in case 167 | # of timeout (defaults to "ssh"). 168 | # You can timeout to any arbitrary address by setting an 169 | # entry in 'protocols' named "timeout". 170 | # This enables you to set a tcpd service name for this 171 | # protocol too. 172 | on-timeout: "timeout"; 173 | 174 | -------------------------------------------------------------------------------- /gap.c: -------------------------------------------------------------------------------- 1 | /* 2 | gap.c: gap, a simple, dynamically-growing array 3 | of pointers that never shrinks 4 | 5 | # Copyright (C) 2021 Yves Rutschle 6 | # 7 | # This program is free software; you can redistribute it 8 | # and/or modify it under the terms of the GNU General Public 9 | # License as published by the Free Software Foundation; either 10 | # version 2 of the License, or (at your option) any later 11 | # version. 12 | # 13 | # This program is distributed in the hope that it will be 14 | # useful, but WITHOUT ANY WARRANTY; without even the implied 15 | # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 16 | # PURPOSE. See the GNU General Public License for more 17 | # details. 18 | # 19 | # The full text for the General Public License is here: 20 | # http://www.gnu.org/licenses/gpl.html 21 | 22 | */ 23 | 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | #include "sslh-conf.h" 31 | #include "gap.h" 32 | 33 | 34 | /* Allocate one page-worth of elements */ 35 | static int gap_len_alloc(int elem_size) 36 | { 37 | return getpagesize() / elem_size; 38 | } 39 | 40 | /* Creates a new gap at least `len` big, all pointers are initialised at NULL */ 41 | gap_array* gap_init(int len) 42 | { 43 | gap_array* gap = malloc(sizeof(*gap)); 44 | if (!gap) return NULL; 45 | memset(gap, 0, sizeof(*gap)); 46 | 47 | int elem_size = sizeof(gap->array[0]); 48 | gap->len = gap_len_alloc(elem_size); 49 | if (gap->len < len) gap->len = len; 50 | gap->array = malloc(gap->len * elem_size); 51 | if (!gap->array) { 52 | free(gap); 53 | return NULL; 54 | } 55 | 56 | for (int i = 0; i < gap->len; i++) 57 | gap->array[i] = NULL; 58 | 59 | return gap; 60 | } 61 | 62 | int gap_extend(gap_array* gap) 63 | { 64 | int elem_size = sizeof(gap->array[0]); 65 | int new_length = gap->len + gap_len_alloc(elem_size); 66 | void** new = realloc(gap->array, new_length * elem_size); 67 | if (!new) return -1; 68 | 69 | gap->array = new; 70 | 71 | for (int i = gap->len; i < new_length; i++) { 72 | gap->array[i] = NULL; 73 | } 74 | 75 | gap->len = new_length; 76 | 77 | return 0; 78 | } 79 | 80 | void gap_destroy(gap_array* gap) 81 | { 82 | free(gap->array); 83 | free(gap); 84 | } 85 | 86 | 87 | /* In gap, find element pointing to ptr, then shift the rest of the array that 88 | * is considered len elements long. 89 | * A poor man's list, if you will. Currently only used to remove probing 90 | * connections, so it only copies a few pointers at most. 91 | * Returns -1 if ptr was not found */ 92 | int gap_remove_ptr(gap_array* gap, void* ptr, int len) 93 | { 94 | int start, i; 95 | 96 | for (i = 0; i < len; i++) 97 | if (gap->array[i] == ptr) 98 | break; 99 | 100 | if (i < len) 101 | start = i; 102 | else 103 | return -1; 104 | 105 | for (i = start; i < len - 1; i++) { 106 | gap->array[i] = gap->array[i+1]; 107 | } 108 | 109 | return 0; 110 | } 111 | 112 | -------------------------------------------------------------------------------- /gap.h: -------------------------------------------------------------------------------- 1 | #ifndef GAP_H 2 | #define GAP_H 3 | 4 | typedef struct gap_array gap_array; 5 | 6 | gap_array* gap_init(int len); 7 | static void* gap_get(gap_array* gap, int index); 8 | static int gap_set(gap_array* gap, int index, void* ptr); 9 | void gap_destroy(gap_array* gap); 10 | 11 | int gap_remove_ptr(gap_array* gap, void* ptr, int len); 12 | 13 | /* Private declarations to allow inlining. 14 | * Don't assume my implementation. */ 15 | typedef struct gap_array { 16 | int len; /* Number of elements in array */ 17 | void** array; 18 | } gap_array; 19 | 20 | int gap_extend(gap_array* gap); 21 | 22 | static inline int __attribute__((unused)) gap_set(gap_array* gap, int index, void* ptr) 23 | { 24 | while (index >= gap->len) { 25 | int res = gap_extend(gap); 26 | if (res == -1) return -1; 27 | } 28 | 29 | gap->array[index] = ptr; 30 | return 0; 31 | } 32 | 33 | static inline void* __attribute__((unused)) gap_get(gap_array* gap, int index) 34 | { 35 | /* sslh-ev routinely reads before it writes. It's not clear if it should be 36 | * its job to check the length (and add a gap_getlen()), or if it should be 37 | * gap_get()'s job. This will do for now */ 38 | if (index >= gap->len) return NULL; 39 | 40 | return gap->array[index]; 41 | } 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /genver.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | if [ ${#} -eq 1 ] && [ "x$1" = "x-r" ]; then 4 | # release text only 5 | QUIET=1 6 | else 7 | QUIET=0 8 | fi 9 | 10 | if [ ! -d .git ] || ! `(git status | grep -q "On branch") 2> /dev/null`; then 11 | # If we don't have git, we can't work out what 12 | # version this is. It must have been downloaded as a 13 | # zip file. 14 | 15 | # If downloaded from the release page, the directory 16 | # has the version number. 17 | release=`pwd | sed s/.*sslh-// | grep "[[:digit:]]"` 18 | 19 | if [ "x$release" = "x" ]; then 20 | # If downloaded from the head, GitHub creates the 21 | # zip file with all files dated from the last 22 | # change: use the Makefile's modification time as a 23 | # release number 24 | release=head-`perl -MPOSIX -e 'print strftime "%Y-%m-%d",localtime((stat "Makefile")[9])'` 25 | fi 26 | fi 27 | 28 | if [ -d .git ] && head=`git rev-parse --verify HEAD 2>/dev/null`; then 29 | # generate the version info based on the tag 30 | release=`(git describe --tags || git --describe || git describe --all --long) \ 31 | 2>/dev/null | tr -s '/' '-' | tr -d '\n'` 32 | 33 | # Are there uncommitted changes? 34 | git update-index --refresh --unmerged > /dev/null 35 | if git diff-index --name-only HEAD | grep -v "^scripts/package" \ 36 | | read dummy; then 37 | release="$release-dirty" 38 | fi 39 | fi 40 | 41 | 42 | if [ $QUIET -ne 1 ]; then 43 | printf "#ifndef VERSION_H \n" 44 | printf "#define VERSION_H \n\n" 45 | printf "#define VERSION \"$release\"\n" 46 | printf "#endif\n" 47 | else 48 | printf "$release\n" 49 | fi 50 | -------------------------------------------------------------------------------- /hash.c: -------------------------------------------------------------------------------- 1 | /* 2 | * a fixed-sized hash 3 | * 4 | # Copyright (C) 2022 Yves Rutschle 5 | # 6 | # This program is free software; you can redistribute it 7 | # and/or modify it under the terms of the GNU General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2 of the License, or (at your option) any later 10 | # version. 11 | # 12 | # This program is distributed in the hope that it will be 13 | # useful, but WITHOUT ANY WARRANTY; without even the implied 14 | # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 15 | # PURPOSE. See the GNU General Public License for more 16 | # details. 17 | # 18 | # The full text for the General Public License is here: 19 | # http://www.gnu.org/licenses/gpl.html 20 | # 21 | # */ 22 | 23 | 24 | /* * The hash is open-addressing, linear search, robin-hood insertion, with 25 | * backward shift deletion. References: 26 | * https://codecapsule.com/2013/11/11/robin-hood-hashing/ 27 | * https://codecapsule.com/2013/11/17/robin-hood-hashing-backward-shift-deletion/ 28 | * This means items are reordered upon insertion and deletion, and the hash 29 | * is well-ordered at all times with no tombstones. 30 | * 31 | * Each pointer is either: 32 | * - to a connection struct 33 | * - FREE (NULL) if not allocated 34 | * 35 | * */ 36 | 37 | #include 38 | #include 39 | 40 | #include "gap.h" 41 | 42 | typedef void* hash_item; 43 | #include "hash.h" 44 | 45 | static void* const FREE = NULL; 46 | 47 | struct hash { 48 | int hash_size; /* Max number of items in the hash */ 49 | int item_cnt; /* Number of items in the hash */ 50 | gap_array* data; 51 | 52 | hash_make_key_fn hash_make_key; 53 | hash_cmp_item_fn cmp_item; 54 | }; 55 | 56 | typedef struct hash hash; 57 | 58 | 59 | static int hash_make_key(hash* h, hash_item item) 60 | { 61 | return h->hash_make_key(item) % h->hash_size; 62 | } 63 | 64 | 65 | hash* hash_init(int hash_size, hash_make_key_fn make_key, hash_cmp_item_fn cmp_item) 66 | { 67 | hash* h = malloc(sizeof(*h)); 68 | if (!h) return NULL; 69 | 70 | h->hash_size = hash_size; 71 | h->item_cnt = 0; 72 | h->data = gap_init(hash_size); 73 | h->hash_make_key = make_key; 74 | h->cmp_item = cmp_item; 75 | 76 | return h; 77 | } 78 | 79 | /* Return the index following i in h */ 80 | static int hash_next_index(hash* h, int i) 81 | { 82 | return (i + 1) % h->hash_size; 83 | } 84 | 85 | /* Returns the index in h of specified address, -1 if not found 86 | * item is an item object that must return the target wanted index and for 87 | * which comparison with the searched object will succeed. 88 | * */ 89 | static int hash_find_index(hash* h, hash_item item) 90 | { 91 | hash_item cnx; 92 | int index = hash_make_key(h, item); 93 | int cnt = 0; 94 | 95 | cnx = gap_get(h->data, index); 96 | #ifdef DEBUG 97 | fprintf(stderr, "searching %d\n", index); 98 | #endif 99 | while (cnx != FREE) { 100 | if (cnt++ > h->hash_size) return -1; 101 | 102 | if (!h->cmp_item(cnx, item)) 103 | break; 104 | 105 | index = hash_next_index(h, index); 106 | cnx = gap_get(h->data, index); 107 | #ifdef DEBUG 108 | fprintf(stderr, "searching %d\n", index); 109 | #endif 110 | } 111 | if (cnx == FREE) return -1; 112 | return index; 113 | } 114 | 115 | hash_item hash_find(hash* h, hash_item item) 116 | { 117 | int index = hash_find_index(h, item); 118 | if (index == -1) return NULL; 119 | hash_item out = gap_get(h->data, index); 120 | return out; 121 | } 122 | 123 | 124 | /* Returns DIB: distance to initial bucket */ 125 | static int distance(int current_index, hash* h, hash_item item) 126 | { 127 | int wanted_index = hash_make_key(h, item); 128 | if (wanted_index <= current_index) 129 | return current_index - wanted_index; 130 | else 131 | return current_index - wanted_index + h->hash_size; 132 | } 133 | 134 | 135 | int hash_insert(hash* h, hash_item new) 136 | { 137 | int bubble_wanted_index = hash_make_key(h, new); 138 | int index = bubble_wanted_index; 139 | gap_array* hash = h->data; 140 | 141 | if (h->item_cnt == h->hash_size) 142 | return -1; 143 | 144 | hash_item curr_item = gap_get(hash, index); 145 | while (curr_item) { 146 | if (distance(index, h, curr_item) < distance(index, h, new)) { 147 | gap_set(h->data, index, new); 148 | #if DEBUG 149 | fprintf(stderr, "intermediate insert [%s] at %d\n", &new->client_addr, index); 150 | #endif 151 | new = curr_item; 152 | } 153 | 154 | index = hash_next_index(h, index); 155 | curr_item = gap_get(hash, index); 156 | } 157 | 158 | #if DEBUG 159 | fprintf(stderr, "final insert at %d\n", index); 160 | #endif 161 | gap_set(hash, index, new); 162 | h->item_cnt++; 163 | 164 | return 0; 165 | } 166 | 167 | /* Remove cnx from the hash */ 168 | int hash_remove(hash* h, hash_item item) 169 | { 170 | gap_array* hash = h->data; 171 | 172 | int index = hash_find_index(h, item); 173 | if (index == -1) return -1; /* Tried to remove something that isn't there */ 174 | 175 | while (1) { 176 | int next_index = hash_next_index(h, index); 177 | hash_item next = gap_get(h->data, next_index); 178 | if ((next == FREE) || (distance(next_index, h, next) == 0)) { 179 | h->item_cnt--; 180 | gap_set(hash, index, FREE); 181 | return 0; 182 | } 183 | 184 | gap_set(hash, index, next); 185 | 186 | index = hash_next_index(h, index);; 187 | } 188 | return 0; 189 | } 190 | 191 | #if HASH_TESTING 192 | #include 193 | #include 194 | #define STR_LENGTH 16 195 | struct hash_item { 196 | int wanted_index; 197 | char str[STR_LENGTH]; 198 | }; 199 | void hash_dump(hash* h, char* filename) 200 | { 201 | char str[STR_LENGTH]; 202 | FILE* out = fopen(filename, "w"); 203 | 204 | if (!out) { 205 | perror(filename); 206 | exit(1); 207 | } 208 | 209 | fprintf(out, "\n", h->item_cnt); 210 | for (int i = 0; i < h->hash_size; i++) { 211 | hash_item item = gap_get(h->data, i); 212 | int idx = 0; 213 | 214 | memset(str, 0, STR_LENGTH); 215 | if (item) { 216 | idx = hash_make_key(h, item); 217 | memcpy(str, ((struct hash_item*)item)->str, STR_LENGTH); 218 | } 219 | fprintf(out, "\t%d:%d:%s\n", i, idx, str); 220 | } 221 | fprintf(out, "\n"); 222 | fclose(out); 223 | } 224 | #endif 225 | -------------------------------------------------------------------------------- /hash.h: -------------------------------------------------------------------------------- 1 | #ifndef HASH_H 2 | #define HASH_H 3 | 4 | /* You will need to typedef a pointer type to hash_item before including this 5 | * .h */ 6 | 7 | typedef struct hash hash; 8 | 9 | /* Function that returns a key (index) for a given item. The key must be always 10 | * the same for an item. It doesn't need to be bounded (hash.c masks it for you) */ 11 | typedef int (*hash_make_key_fn)(hash_item item); 12 | 13 | /* Function that compares two items: returns 0 if they are the same */ 14 | typedef int (*hash_cmp_item_fn)(hash_item item1, hash_item item2); 15 | 16 | hash* hash_init(int hash_size, hash_make_key_fn make_key, hash_cmp_item_fn cmp_item); 17 | 18 | int hash_insert(hash* h, hash_item new); 19 | int hash_remove(hash* h, hash_item item); 20 | 21 | /* Returns the hash item that matches specification (meaning the 22 | * comparison function returns true for cmp(x, item), or NULL if not found */ 23 | hash_item hash_find(hash* h, hash_item item); 24 | 25 | 26 | void hash_dump(hash* h, char* filename); /* For development only */ 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /hashtest/Makefile: -------------------------------------------------------------------------------- 1 | 2 | CFLAGS=-DHASH_TESTING -O2 -Wall 3 | OBJ=../hash.o ../gap.o htest.o 4 | 5 | htest: $(OBJ) 6 | $(CC) -o htest $(OBJ) 7 | 8 | -------------------------------------------------------------------------------- /hashtest/delete.tst: -------------------------------------------------------------------------------- 1 | # Basic delete 2 | a 10 aa 3 | a 10 ab 4 | a 10 ac 5 | a 20 ba 6 | a 21 bb 7 | 8 | d 21 bb 9 | -------------------------------------------------------------------------------- /hashtest/delete.tst.ref: -------------------------------------------------------------------------------- 1 | 2 | 0:0: 3 | 1:0: 4 | 2:0: 5 | 3:0: 6 | 4:0: 7 | 5:0: 8 | 6:0: 9 | 7:0: 10 | 8:0: 11 | 9:0: 12 | 10:10:aa 13 | 11:10:ab 14 | 12:10:ac 15 | 13:0: 16 | 14:0: 17 | 15:0: 18 | 16:0: 19 | 17:0: 20 | 18:0: 21 | 19:0: 22 | 20:20:ba 23 | 21:0: 24 | 22:0: 25 | 23:0: 26 | 24:0: 27 | 25:0: 28 | 26:0: 29 | 27:0: 30 | 28:0: 31 | 29:0: 32 | 30:0: 33 | 31:0: 34 | 35 | -------------------------------------------------------------------------------- /hashtest/delete_at_end.tst: -------------------------------------------------------------------------------- 1 | # Delete inside a block with nothing after 2 | 3 | a 10 aa 4 | a 10 ab 5 | a 12 ac 6 | a 13 ad 7 | a 14 ae 8 | 9 | d 14 ae 10 | -------------------------------------------------------------------------------- /hashtest/delete_at_end.tst.ref: -------------------------------------------------------------------------------- 1 | 2 | 0:0: 3 | 1:0: 4 | 2:0: 5 | 3:0: 6 | 4:0: 7 | 5:0: 8 | 6:0: 9 | 7:0: 10 | 8:0: 11 | 9:0: 12 | 10:10:aa 13 | 11:10:ab 14 | 12:12:ac 15 | 13:13:ad 16 | 14:0: 17 | 15:0: 18 | 16:0: 19 | 17:0: 20 | 18:0: 21 | 19:0: 22 | 20:0: 23 | 21:0: 24 | 22:0: 25 | 23:0: 26 | 24:0: 27 | 25:0: 28 | 26:0: 29 | 27:0: 30 | 28:0: 31 | 29:0: 32 | 30:0: 33 | 31:0: 34 | 35 | -------------------------------------------------------------------------------- /hashtest/delete_below_floor.tst: -------------------------------------------------------------------------------- 1 | # wrap-around and delete below floor 2 | a 2 ba 3 | a 30 aa 4 | a 30 ab 5 | a 30 ac 6 | a 30 ad 7 | a 2 bb 8 | 9 | d 30 ab 10 | -------------------------------------------------------------------------------- /hashtest/delete_below_floor.tst.ref: -------------------------------------------------------------------------------- 1 | 2 | 0:30:ad 3 | 1:0: 4 | 2:2:ba 5 | 3:2:bb 6 | 4:0: 7 | 5:0: 8 | 6:0: 9 | 7:0: 10 | 8:0: 11 | 9:0: 12 | 10:0: 13 | 11:0: 14 | 12:0: 15 | 13:0: 16 | 14:0: 17 | 15:0: 18 | 16:0: 19 | 17:0: 20 | 18:0: 21 | 19:0: 22 | 20:0: 23 | 21:0: 24 | 22:0: 25 | 23:0: 26 | 24:0: 27 | 25:0: 28 | 26:0: 29 | 27:0: 30 | 28:0: 31 | 29:0: 32 | 30:30:aa 33 | 31:30:ac 34 | 35 | -------------------------------------------------------------------------------- /hashtest/delete_discont.tst: -------------------------------------------------------------------------------- 1 | # delete in a discontinuous block 2 | 3 | a 10 aa 4 | a 11 ab 5 | a 12 ac 6 | a 14 ad 7 | a 10 bc 8 | 9 | d 11 ab 10 | 11 | -------------------------------------------------------------------------------- /hashtest/delete_discont.tst.ref: -------------------------------------------------------------------------------- 1 | 2 | 0:0: 3 | 1:0: 4 | 2:0: 5 | 3:0: 6 | 4:0: 7 | 5:0: 8 | 6:0: 9 | 7:0: 10 | 8:0: 11 | 9:0: 12 | 10:10:aa 13 | 11:10:bc 14 | 12:12:ac 15 | 13:0: 16 | 14:14:ad 17 | 15:0: 18 | 16:0: 19 | 17:0: 20 | 18:0: 21 | 19:0: 22 | 20:0: 23 | 21:0: 24 | 22:0: 25 | 23:0: 26 | 24:0: 27 | 25:0: 28 | 26:0: 29 | 27:0: 30 | 28:0: 31 | 29:0: 32 | 30:0: 33 | 31:0: 34 | 35 | -------------------------------------------------------------------------------- /hashtest/delete_empty.tst: -------------------------------------------------------------------------------- 1 | # Delete an unexisting element. And on an empty hash 2 | 3 | a 10 aa 4 | 5 | d 10 ab 6 | d 12 bc 7 | 8 | # Empty for real 9 | d 10 aa 10 | 11 | d 10 aa 12 | -------------------------------------------------------------------------------- /hashtest/delete_empty.tst.ref: -------------------------------------------------------------------------------- 1 | 2 | 0:0: 3 | 1:0: 4 | 2:0: 5 | 3:0: 6 | 4:0: 7 | 5:0: 8 | 6:0: 9 | 7:0: 10 | 8:0: 11 | 9:0: 12 | 10:0: 13 | 11:0: 14 | 12:0: 15 | 13:0: 16 | 14:0: 17 | 15:0: 18 | 16:0: 19 | 17:0: 20 | 18:0: 21 | 19:0: 22 | 20:0: 23 | 21:0: 24 | 22:0: 25 | 23:0: 26 | 24:0: 27 | 25:0: 28 | 26:0: 29 | 27:0: 30 | 28:0: 31 | 29:0: 32 | 30:0: 33 | 31:0: 34 | 35 | -------------------------------------------------------------------------------- /hashtest/delete_full.tst: -------------------------------------------------------------------------------- 1 | # delete on a full hash 2 | 3 | # First, fill the hash :-) 4 | 5 | a 0 aa 6 | a 1 ab 7 | a 2 ac 8 | a 3 ad 9 | a 4 ae 10 | a 5 af 11 | a 6 ag 12 | a 7 ah 13 | a 8 ai 14 | a 9 af 15 | a 10 ba 16 | a 11 bb 17 | a 12 bc 18 | a 13 bd 19 | a 14 be 20 | a 15 bf 21 | a 16 bg 22 | a 17 bh 23 | a 18 bi 24 | a 19 bj 25 | a 20 ca 26 | a 21 cb 27 | a 22 cd 28 | a 23 ce 29 | a 24 cf 30 | a 25 cg 31 | a 26 ch 32 | a 27 ci 33 | a 28 cj 34 | a 29 ck 35 | a 30 da 36 | a 31 db 37 | 38 | 39 | d 21 cb 40 | -------------------------------------------------------------------------------- /hashtest/delete_full.tst.ref: -------------------------------------------------------------------------------- 1 | 2 | 0:0:aa 3 | 1:1:ab 4 | 2:2:ac 5 | 3:3:ad 6 | 4:4:ae 7 | 5:5:af 8 | 6:6:ag 9 | 7:7:ah 10 | 8:8:ai 11 | 9:9:af 12 | 10:10:ba 13 | 11:11:bb 14 | 12:12:bc 15 | 13:13:bd 16 | 14:14:be 17 | 15:15:bf 18 | 16:16:bg 19 | 17:17:bh 20 | 18:18:bi 21 | 19:19:bj 22 | 20:20:ca 23 | 21:0: 24 | 22:22:cd 25 | 23:23:ce 26 | 24:24:cf 27 | 25:25:cg 28 | 26:26:ch 29 | 27:27:ci 30 | 28:28:cj 31 | 29:29:ck 32 | 30:30:da 33 | 31:31:db 34 | 35 | -------------------------------------------------------------------------------- /hashtest/delete_middle.tst: -------------------------------------------------------------------------------- 1 | # Delete inside a block with something discontinuous 2 | 3 | a 10 aa 4 | a 10 ab 5 | a 12 ac 6 | a 13 ad 7 | a 14 ae 8 | 9 | # ab shifts, ac and next doesn't 10 | d 10 aa 11 | -------------------------------------------------------------------------------- /hashtest/delete_middle.tst.ref: -------------------------------------------------------------------------------- 1 | 2 | 0:0: 3 | 1:0: 4 | 2:0: 5 | 3:0: 6 | 4:0: 7 | 5:0: 8 | 6:0: 9 | 7:0: 10 | 8:0: 11 | 9:0: 12 | 10:10:ab 13 | 11:0: 14 | 12:12:ac 15 | 13:13:ad 16 | 14:14:ae 17 | 15:0: 18 | 16:0: 19 | 17:0: 20 | 18:0: 21 | 19:0: 22 | 20:0: 23 | 21:0: 24 | 22:0: 25 | 23:0: 26 | 24:0: 27 | 25:0: 28 | 26:0: 29 | 27:0: 30 | 28:0: 31 | 29:0: 32 | 30:0: 33 | 31:0: 34 | 35 | -------------------------------------------------------------------------------- /hashtest/delete_wrap.tst: -------------------------------------------------------------------------------- 1 | # Basic delete when wrapping, between wrap and floor 2 | a 30 aa 3 | a 30 ab 4 | a 30 ac 5 | a 30 ba 6 | a 30 bb 7 | 8 | d 30 ac 9 | -------------------------------------------------------------------------------- /hashtest/delete_wrap.tst.ref: -------------------------------------------------------------------------------- 1 | 2 | 0:30:ba 3 | 1:30:bb 4 | 2:0: 5 | 3:0: 6 | 4:0: 7 | 5:0: 8 | 6:0: 9 | 7:0: 10 | 8:0: 11 | 9:0: 12 | 10:0: 13 | 11:0: 14 | 12:0: 15 | 13:0: 16 | 14:0: 17 | 15:0: 18 | 16:0: 19 | 17:0: 20 | 18:0: 21 | 19:0: 22 | 20:0: 23 | 21:0: 24 | 22:0: 25 | 23:0: 26 | 24:0: 27 | 25:0: 28 | 26:0: 29 | 27:0: 30 | 28:0: 31 | 29:0: 32 | 30:30:aa 33 | 31:30:ab 34 | 35 | -------------------------------------------------------------------------------- /hashtest/delete_wrap_at_end.tst: -------------------------------------------------------------------------------- 1 | # Delete inside a block with wrapping, with something after 2 | 3 | a 30 aa 4 | a 30 ab 5 | a 30 ac 6 | a 1 ad 7 | a 3 ae 8 | 9 | # shift ad but not ae 10 | d 14 ae 11 | -------------------------------------------------------------------------------- /hashtest/delete_wrap_at_end.tst.ref: -------------------------------------------------------------------------------- 1 | 2 | 0:30:ac 3 | 1:1:ad 4 | 2:0: 5 | 3:3:ae 6 | 4:0: 7 | 5:0: 8 | 6:0: 9 | 7:0: 10 | 8:0: 11 | 9:0: 12 | 10:0: 13 | 11:0: 14 | 12:0: 15 | 13:0: 16 | 14:0: 17 | 15:0: 18 | 16:0: 19 | 17:0: 20 | 18:0: 21 | 19:0: 22 | 20:0: 23 | 21:0: 24 | 22:0: 25 | 23:0: 26 | 24:0: 27 | 25:0: 28 | 26:0: 29 | 27:0: 30 | 28:0: 31 | 29:0: 32 | 30:30:aa 33 | 31:30:ab 34 | 35 | -------------------------------------------------------------------------------- /hashtest/delete_wrap_below_floor.tst: -------------------------------------------------------------------------------- 1 | # delete before wrap 2 | a 30 aa 3 | a 30 ab 4 | a 30 ac 5 | a 30 ad 6 | 7 | # shift ac and ad 8 | d 30 ab 9 | -------------------------------------------------------------------------------- /hashtest/delete_wrap_below_floor.tst.ref: -------------------------------------------------------------------------------- 1 | 2 | 0:30:ad 3 | 1:0: 4 | 2:0: 5 | 3:0: 6 | 4:0: 7 | 5:0: 8 | 6:0: 9 | 7:0: 10 | 8:0: 11 | 9:0: 12 | 10:0: 13 | 11:0: 14 | 12:0: 15 | 13:0: 16 | 14:0: 17 | 15:0: 18 | 16:0: 19 | 17:0: 20 | 18:0: 21 | 19:0: 22 | 20:0: 23 | 21:0: 24 | 22:0: 25 | 23:0: 26 | 24:0: 27 | 25:0: 28 | 26:0: 29 | 27:0: 30 | 28:0: 31 | 29:0: 32 | 30:30:aa 33 | 31:30:ac 34 | 35 | -------------------------------------------------------------------------------- /hashtest/delete_wrap_discont.tst: -------------------------------------------------------------------------------- 1 | # Delete with wrapping in discontinuous group 2 | 3 | a 30 aa 4 | a 30 ab 5 | a 30 ac 6 | a 31 ad 7 | a 2 ba 8 | a 3 bb 9 | 10 | # shift ac and ad but not ba and bb 11 | d 30 ab 12 | -------------------------------------------------------------------------------- /hashtest/delete_wrap_discont.tst.ref: -------------------------------------------------------------------------------- 1 | 2 | 0:31:ad 3 | 1:0: 4 | 2:2:ba 5 | 3:3:bb 6 | 4:0: 7 | 5:0: 8 | 6:0: 9 | 7:0: 10 | 8:0: 11 | 9:0: 12 | 10:0: 13 | 11:0: 14 | 12:0: 15 | 13:0: 16 | 14:0: 17 | 15:0: 18 | 16:0: 19 | 17:0: 20 | 18:0: 21 | 19:0: 22 | 20:0: 23 | 21:0: 24 | 22:0: 25 | 23:0: 26 | 24:0: 27 | 25:0: 28 | 26:0: 29 | 27:0: 30 | 28:0: 31 | 29:0: 32 | 30:30:aa 33 | 31:30:ac 34 | 35 | -------------------------------------------------------------------------------- /hashtest/htest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yrutschle/sslh/b7556c07bddf0e666ea9bf7bdfb0a140044190b0/hashtest/htest -------------------------------------------------------------------------------- /hashtest/htest.c: -------------------------------------------------------------------------------- 1 | /* Wee testing program from the hash code: 2 | * htest