Open-source project
yrutschle/sslh avatar
yrutschle/sslh

sslh: the first packet picks the backend, and the container ships only the select build

Applicative Protocol Multiplexer (e.g. share SSH and HTTPS on the same port)

5,124 stars395 forksCGPL-2.0

At a glance

What is it?
A protocol multiplexer written in C that shares one port between SSH, HTTPS, WireGuard, OpenVPN, XMPP, and anything a regular expression can match. It picks the backend from the client's first packet, which makes routing cheap and makes the multiplexer a single point where every service on that port depends.
Who is it for?
sslh earns its place when one address has to answer for several protocols, which is the common case behind a corporate firewall that almost never blocks port 443. Before you commit, check which process model you need, because the published image contains only the select-based binary and leaves the fork and libev builds to you.
Can I use it commercially?
Yes, with conditions. GPL-2.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 18 days ago.
What is it written in?
Mainly C, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • yrutschle/sslh5,124 ★ · +7
Star history
Repository2026-09-232026-11-14
yrutschle/sslh5.1k5.1k

7 Stars in the last 52 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

The client's first packet is the whole routing decision

`sslh` accepts connections on the ports you specify and forwards them onward based on tests performed on the first data packet the remote client sends. Probes exist for HTTP, TLS and SSL including SNI and ALPN, SSH, WireGuard, OpenVPN, tinc, XMPP, and SOCKS5, and any other protocol that can be tested with a regular expression can be recognised as well. Because the choice is made from what has already arrived, no handshake is completed before the multiplexer commits to a backend. That is the design's strength and its sharp edge: a client that stays silent, or that opens a connection before speaking, is judged on a first packet that does not yet describe what it wants, and the connection is routed on evidence rather than on a declaration. The daemon features around that core are the usual mature set, with privilege and capabilities dropping, inetd support, systemd support, transparent proxying, HAProxy proxyprotocol support, chroot, logging, IPv4 and IPv6, and both TCP and UDP.

Sharing 443 with sshd is the case the tool is built around

The reason this exists is stated plainly: a corporate firewall almost never blocks port 443, so serving SSH and HTTPS on that same port lets you reach a shell from inside such a network without opening port 22. That makes `sslh` a demultiplexer, or a switchboard, in front of services that each keep their own port on the far side. The SNI and ALPN probe extends the idea to a virtual host farm hosted behind a single IP address, where the host name in the TLS handshake is what separates tenants. Two consequences follow from putting several services behind one listener. First, each service you want to share the port has to be describable as a probe, so a protocol you cannot pattern match cannot be multiplexed this way. Second, the multiplexer sits in front of everything, so its own failure takes down SSH, HTTPS, and the VPN from that address at the same time, and its probe set becomes part of your security surface.

Three process models live in the tree, and the image builds one of them

The sources carry three separate process models for the same daemon: `sslh-fork.c`, `sslh-select.c`, and `sslh-ev.c`. The README describes a fork-based model, a select-based model, and a libev based model for larger installations. The Dockerfile picks exactly one of them:

dockerfile
RUN ./configure && make sslh-select && strip sslh-select

The resulting binary is what ends up in the published image, copied to `/usr/local/bin/sslh`. The other two are not built there, so the model recommended for a large installation is the one you have to compile yourself from the repository. The build is a two stage Alpine image with `ALPINE_VERSION` and `TARGET_ARCH` as build arguments, and the runtime stage installs libconfig, pcre2, iptables, ip6tables, and libcap. Its libraries are libproxyprotocol, which is not part of the sslh tree at all, so what the image can do depends on a third party repository being reachable at build time.

Two capabilities on the binary, and root in the container

Permissions are handled in two places that do not agree with each other. The runtime stage creates a dedicated account and hands the finished binary exactly the capabilities it needs:

dockerfile
setcap cap_net_bind_service,cap_net_raw+ep /usr/local/bin/sslh

`cap_net_bind_service` is what lets a non-root process listen on 443, and `cap_net_raw` covers the raw socket work. The container, however, ends on a different note. The entrypoint is the repository's `container-entrypoint.sh` installed as `/init`, and the final Dockerfile line is `USER root:root` carrying the comment that it is required for updating iptables. So the sslh account created during the build is not the identity the container runs as, and the iptables requirement is why. The compose examples follow the same split: the plain example adds `CAP_NET_RAW` and `CAP_NET_BIND_SERVICE` with `docker run`, while both transparent examples add `NET_ADMIN` on top of those two, since changing routes is beyond what the binary's own capabilities allow.

The proxy protocol library is cloned at build time with no version pinned

The image does not package libproxyprotocol from a release. It clones it during the build:

dockerfile
RUN git clone https://github.com/kosmas-valianos/libproxyprotocol.git && \
    cd libproxyprotocol && \
    make && \
    mkdir -p /usr/local/include/ && \
    mv libs/libproxyprotocol.so /usr/local/lib/ && \
    mv src/* /usr/local/include/

No tag, branch, or commit is named in that clone, so whatever the default branch holds at build time is what gets compiled in. The Dockerfile then points the compiler and linker at the copied headers and library through `C_INCLUDE_PATH`, `LIBRARY_PATH`, and `LD_LIBRARY_PATH`, with a comment that the configure script should detect libproxyprotocol on its own, and the runtime stage copies the shared object next to the binary. The practical effect is that two builds of the image, made at different times, are not guaranteed to contain the same library code even though both are tagged `latest`. If you build this yourself, pin the commit in the clone step rather than relying on the default branch.

Transparency has a cheaper sibling, and two hard methods behind it

Transparent proxying makes the target server see the original client address, so `sslh` becomes invisible. Before reaching for it, the recommendation is to try HAProxy's proxyprotocol instead when the backend supports it, because that is one simple setting in the sslh protocol configuration plus an equivalent setting in the backend. Either way, services behind `sslh`, Apache and `sshd` among them, see the external IP and ports as if the outside had connected to them directly, which simplifies IP based access control or makes it possible at all, and is what lets banning tools such as fail2ban work. When real transparency is needed there are two methods. The first adds virtual network interfaces, with the principle and basic setup in one document, further scenarios in another, and a podman guide. The second uses iptables packet marking and is described as highly dependent on your network environment, with no known generic approach, so it assumes extensive knowledge of network management and iptables setup. The advice in most cases is to follow the first method. The compose files show the same choice: the container variant sets `net.ipv4.conf.default.route_localnet` and `net.ipv4.conf.all.route_localnet` to 1 and reaches the services via localhost, while the `network_mode: host` variant marks those sysctls as having to be set manually.

A security review found two CVEs, and there is no release tag to pin

The project points at a security review rather than presenting itself as finished. Matthias Gerstner from OpenSUSE examined the code from a security point of view, which revealed a number of problems including two CVEs, and the more critical findings have partly been taken into account, with the rest left open. His full write-up covers denial-of-service vulnerabilities and is described as well worth reading if you are running `sslh` in production. Securing an installation also means configuring connection limits, which have their own guide. Configuration in general lives in a separate document from this overview, and installation instructions are pointed at as well, so the README itself is a map rather than a manual. One more thing to plan around: the repository publishes no releases, so there is no version to pin or diff against, and the tree carries its own ChangeLog instead. Discussion, feature requests, and announcements are routed to the project mailing list, and a FAQ document in the tree is suggested as the first place to look.

Editorial conclusion

sslh earns its place when one address has to answer for several protocols, which is the common case behind a corporate firewall that almost never blocks port 443. Before you commit, check which process model you need, because the published image contains only the select-based binary and leaves the fork and libev builds to you. If you rely on the container for transparency, set the route_localnet sysctls yourself when using host networking, and read the security review and the connection limits guide before putting it in front of real traffic.

Frequently asked questions

How do I share port 443 between SSH and HTTPS with sslh?

Give the container `CAP_NET_RAW` and `CAP_NET_BIND_SERVICE`, then pass `--foreground --listen=0.0.0.0:443 --ssh=hostname:22 --tls=hostname:443` to `ghcr.io/yrutschle/sslh:latest`. The stated reason for sharing 443 is that corporate firewalls almost never block it.

Which sslh process model does the published container image use?

The select-based one. The Dockerfile runs `./configure && make sslh-select && strip sslh-select` and copies that binary to `/usr/local/bin/sslh`, so the fork-based and libev variants named in the source tree are not present in the image.

Does sslh have a published security history I should read before deploying it?

Yes. Matthias Gerstner from OpenSUSE performed a code review from a security point of view, revealing problems including two CVEs, with the more critical ones partly taken into account. The full review covers denial-of-service vulnerabilities.

Why does my transparent sslh container not reach my services?

The compose note states that the sslh container must be able to reach your services via localhost. The container variant sets `net.ipv4.conf.default.route_localnet` and `net.ipv4.conf.all.route_localnet` to 1, while the `network_mode: host` variant marks those sysctls as having to be set manually.

How do I protect sslh against too many connections?

Configuring connection limits is part of securing an installation, and the project keeps a dedicated guide for it alongside separate documents for installation and for general configuration.

Official sources

  1. Issues
  2. License: GPL-2.0
  3. Project website
  4. README
  5. yrutschle/sslh on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/yrutschle-sslh.svg)](https://hysenlabs.com/projects/yrutschle-sslh)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/yrutschle-sslh.svg)](https://hysenlabs.com/projects/yrutschle-sslh)

Was this useful?