From 55872427f2c1aee51690b06eb81400a4c9f378ad Mon Sep 17 00:00:00 2001 From: Oleg Stepura Date: Sun, 24 Aug 2025 13:03:19 +0200 Subject: [PATCH 01/11] Update podman.md --- doc/podman.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/podman.md b/doc/podman.md index 7e268ff..bc33565 100644 --- a/doc/podman.md +++ b/doc/podman.md @@ -33,6 +33,7 @@ olegstepura](https://github.com/yrutschle/sslh/issues/448) # ... (make sure to mount config as you like) command: --transparent -F/etc/sslh/sslh.cfg # parameter --transparent here is needed to trigger configure_iptables in init script state: started + # never add a network here, otherwise setup will not work. Adding a network disconnects sslh container from pod's shared localhost - name: "Create the caddy container" containers.podman.podman_container: From b9de23534e9b5632fa38a11758955fa15b840dc8 Mon Sep 17 00:00:00 2001 From: Oleg Stepura Date: Sun, 24 Aug 2025 13:12:34 +0200 Subject: [PATCH 02/11] Update podman.md --- doc/podman.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/podman.md b/doc/podman.md index bc33565..1983e73 100644 --- a/doc/podman.md +++ b/doc/podman.md @@ -33,7 +33,8 @@ olegstepura](https://github.com/yrutschle/sslh/issues/448) # ... (make sure to mount config as you like) command: --transparent -F/etc/sslh/sslh.cfg # parameter --transparent here is needed to trigger configure_iptables in init script state: started - # never add a network here, otherwise setup will not work. Adding a network disconnects sslh container from pod's shared localhost + # ⚠️ Important: Never add a `network:` here, otherwise this setup will NOT work. + # Adding a network disconnects the sslh container from the pod's shared localhost. - name: "Create the caddy container" containers.podman.podman_container: From 6d1e95cc523de71ecea64595b14338688338d2ca Mon Sep 17 00:00:00 2001 From: Oleg Stepura Date: Sun, 24 Aug 2025 13:38:24 +0200 Subject: [PATCH 03/11] Update podman.md --- doc/podman.md | 68 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/doc/podman.md b/doc/podman.md index 1983e73..757e192 100644 --- a/doc/podman.md +++ b/doc/podman.md @@ -97,26 +97,60 @@ protocols: ); ``` -I omitted caddy configs here as it's not important. Some unrelated container configs were also dropped. -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). +I omitted Caddy configs here since they're irrelevant, and some unrelated container configs were also dropped for clarity. -Scheme of port mapping is (all containers share same `localhost`): +In the example above, `sslh`, `caddy`, and `ssh-proxy` are **three containers inside the same Podman pod**, all sharing the same **pod-local `localhost`**. + +`sslh` listens on port **444** instead of **443**, because `caddy` already binds to port `443`, and reconfiguring Caddy is more complex due to its Let's Encrypt integration (Caddy itself "thinks" it’s bound to your host interface). + +#### Port mapping scheme +All containers **share the same `localhost`** inside the pod: ``` host 443 → pod 444 (sslh) → pod 443 (caddy) ``` -- podman connects host `443` port to pod's `444` port -- `sslh` listens `444` on `localhost`, reroutes tls to `caddy` on `localhost:443` -- `caddy` listens `443` on `localhost` (reverse-proxies to other apps on private network) -Reverse-proxied services get correct IP in `X-Forwarded-For` header. -Takeaways: -- `net.ipv4.conf.default.route_localnet` is setup only in container, not on host, which is nice. -- same with iptables and route rules applied by `init` script in sslh container -- `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 - - with transparent mode all services that `sslh` will connect to should be attached to this pod making it listen on `localhost` of the pod - - ports of containers should not clash - - ports should be published on pod level, not on containers - - containers should not be configured to connect to custom networks -- 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. -- `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. +- **Podman** maps the host’s `443` → pod’s `444`. +- **SSHL** listens on `localhost:444` inside the pod and forwards TLS traffic to `caddy` on `localhost:443`. +- **Caddy** listens on `localhost:443` and reverse-proxies to other services in the private network. + +This ensures that **reverse-proxied services** receive the correct client IP in the `X-Forwarded-For` header. + +--- + +### Important note + +⚠️ Do **not** set a `network:` parameter for the `sslh` container when it belongs to a Podman pod. +Setting a custom network **forces Podman to detach the container from the pod’s shared network namespace** and creates a **separate `localhost`** for that container, breaking transparent routing. + +For example: + +```yaml +command: --transparent -F/etc/sslh/sslh.cfg # parameter --transparent here is needed to trigger configure_iptables in init script +state: started +# ⚠️ Never add a `network:` here! +# Adding a network disconnects the sslh container from the pod's shared localhost, +# causing transparent routing to fail. +``` + +### Key takeaways + +- `net.ipv4.conf.default.route_localnet` and iptables rules are applied **inside the `sslh` container only**, not on the host. +- The `sslh` **init** script manages all required `iptables` and routing rules inside of the container. +- **Transparent mode restrictions**: + - Any service `sslh` proxies traffic to **must** be inside the same pod and bound to its **pod-local `localhost`**. + - Container ports must not conflict within the pod. + - Ports should be published **at the pod level**, **not per-container**. + - Containers inside this pod **must not** be attached to any custom networks. +- Because of transparent routing, `sslh` cannot directly proxy SSH to the host IP. + A workaround is to run a lightweight **socat** container in the same pod: + `TCP-LISTEN:222 → TCP:host.containers.internal:22` +- Caddy’s `reverse_proxy` **to other containers** in the **custom network attached to the pod** still works properly. +- `socat` can also connect to both the **host** and **other containers** inside the custom network without issues. + +### Tested setup + +- Confirmed working on **Debian Trixie** +- Using **Podman 5.4.2** +- Pinned `podman` and related dependencies (`conmon`, `slirp4netns`, `aardvark-dns`, etc.) from **Debian testing**, + since **stable** often ships outdated versions that can break this setup. From f64f0c5ba560a89fbcb7e7fd0aa8f954d70d52b4 Mon Sep 17 00:00:00 2001 From: yrutschle Date: Wed, 20 Aug 2025 09:55:43 +0200 Subject: [PATCH 04/11] some markdown formating fixes --- doc/Daisy-Chaining-Transparency-Explained.md | 60 ++++++++++++++++++-- doc/INSTALL.md | 2 +- doc/simple_transparent_proxy.md | 1 + doc/tproxy.md | 2 +- 4 files changed, 57 insertions(+), 8 deletions(-) diff --git a/doc/Daisy-Chaining-Transparency-Explained.md b/doc/Daisy-Chaining-Transparency-Explained.md index 4e64632..1296158 100644 --- a/doc/Daisy-Chaining-Transparency-Explained.md +++ b/doc/Daisy-Chaining-Transparency-Explained.md @@ -1,7 +1,11 @@ # Daisy-Chaining-Transparency # + This documentation goes a level deeper, what happens in the operating system with IP-addresses, and why some combinations of programs are failing, when they use the same transparency method. -There are situations, where you need to combine two applications, both working as ip-transparent proxies, to reach your goal. One example is, having nginx or stunnel as an proxytunnel-endpoint for tls tunneled ssh connections through https-proxies. An example for such a combination will be desribed at the end of this article.
-Unfortunately you will see a lot of errors popping out: **Address already in use**
+ +There are situations, where you need to combine two applications, both working as ip-transparent proxies, to reach your goal. One example is, having nginx or stunnel as an proxytunnel-endpoint for tls tunneled ssh connections through https-proxies. An example for such a combination will be desribed at the end of this article. + +Unfortunately you will see a lot of errors popping out: **Address already in use** + [This article from Cloudflare blog](https://blog.cloudflare.com/how-to-stop-running-out-of-ephemeral-ports-and-start-to-love-long-lived-connections) explains why this is happening, while it is describing the solution to another problem. However this is a close relative to our problem. Let us look to the following example: We have sslh (S) accepting connections from a client (C) and forwarding one of those connections to a man-in-the-middle (M), which finally forwards this connection to sshd. If everything works perfectly, we would like to see those connections. @@ -9,10 +13,15 @@ Let us look to the following example: We have sslh (S) accepting connections fro ![Dataflow-Of-Daisy-Chain](./detailed-ip-transparency.png) But unfortunately we are receiving in many constellations errors, when M tries to open its connection to our final target sshd, here called T. -Let us look more close, why that is happening. We need for this two terminal windows on the same server.
+ +Let us look more close, why that is happening. We need for this two terminal windows on the same server. + ### First example, uncooperative applications ### + As the problem has nothing to do with transparency itself, but only of reuse of same IP-addresses and ports, we avoid the overhead of the additional capablities, to keep the example easy and clear. + In the first terminal we are starting python3 and entering the following three lines: + ``` user@host:~$ python3 Python 3.11.2 (main, May 2 2024, 11:59:08) [GCC 12.2.0] on linux @@ -24,6 +33,7 @@ Type "help", "copyright", "credits" or "license" for more information. ``` Now we are going to the second terminal window, and trying just the same: + ``` Python 3.11.2 (main, May 2 2024, 11:59:08) [GCC 12.2.0] on linux Type "help", "copyright", "credits" or "license" for more information. @@ -35,18 +45,26 @@ Traceback (most recent call last): OSError: [Errno 98] Address already in use sock.bind(("192.168.255.254", 12346)) ##this however works!! ``` + Here we are getting the error, which caused many of us hours of research. The problem is caused by the fact, that the kernel does not know at the moment of the bind()-call, how we want to use the socket. If we use this as a server socket, and will do a listen()-call as next, this will not work, as for server sockets, the two-value tuple ADDRESS:PORT needs to be unique only to the process, tied to this socket. + That is the reason, that there are port ranges, reserved for servers, where the administrator is responsible not to assign the same port to two applications. + But as server ports are coming from a range, which will not be used for client connections, a server can be sure, that if it is started at any time in the future, no outgoing client has used its port for an outbound connection. + Clients are usually using ports from a so called [ephemeral port range](https://en.wikipedia.org/wiki/Ephemeral_port). + However, for clients each connection is valid, as long as one value in the four-tuples describing the connection is different. In our example above, the two values from the destination are different, so this connection could be established (in theory) without conflicts. + To make that happen, you need to deploy a special socket option to this socket, to explain, that we "know the risks" and we will reuse the ip-port combination. + And, as we see from the second bind() in the second example, the error message: _**Address already in use**_ really means: _**Address:Port already in use**_ ### Taking Care, part I ### Ok, now we are entering our two terminals again, pressing Ctrl-D to finish python, and start a new session like this in both terminals: + ``` user@host:~$ python3 Python 3.11.2 (main, May 2 2024, 11:59:08) [GCC 12.2.0] on linux @@ -57,12 +75,15 @@ Type "help", "copyright", "credits" or "license" for more information. >>> sock.bind(("192.168.255.254", 12345)) >>> ``` + And we will see, that the problem is solved, when both applications are taking care. ### Taking Care, part II ### Ok, now we are going back to our terminals, pressing again Ctrl-D to finish python, and start new sessions like this: + In the first terminal we repeat the input from our first example, without the setsockopt() call: + ``` user@host:~$ python3 Python 3.11.2 (main, May 2 2024, 11:59:08) [GCC 12.2.0] on linux @@ -72,6 +93,7 @@ Type "help", "copyright", "credits" or "license" for more information. >>> sock.bind(("192.168.255.254", 12345)) >>> ``` + in the second terminal, we enter our modified cooperative example: ``` @@ -87,8 +109,11 @@ Traceback (most recent call last): OSError: [Errno 98] Address already in use >>> ``` -Oops, here is our error again. And this is the reason, why I called this method "cooperative". As the first application has bound to that IP:PORT combination, without telling, that "it knows the risk", the kernel denies us, to use this combinations, as we may break the already active application. The first application is not "cooperative" :-(
+ +Oops, here is our error again. And this is the reason, why I called this method "cooperative". As the first application has bound to that IP:PORT combination, without telling, that "it knows the risk", the kernel denies us, to use this combinations, as we may break the already active application. The first application is not "cooperative" :-( + Ok, but the kernel gives as some more possibilities: As we now get connections from a uncoperative application, we can no longer use the Client-IP-Port combination. We need to use a conflict free port for the client IP, to succeed. So lets get back to terminal 2 and continue after the error with the following commands: + ``` user@host:~$ python3 Python 3.11.2 (main, May 2 2024, 11:59:08) [GCC 12.2.0] on linux @@ -105,9 +130,13 @@ OSError: [Errno 98] Address already in use >>> sock.bind(("192.168.255.254", 0)) >>> ``` + This last example makes it neccessary, that you use a recent version of the python3 socket library, as older versions, have the option _IP_BIND_ADDRESS_NO_PORT_ not yet defined. + With his behaviour, we are telling the kernel, that the kernel should assign us a conflict free port address at latest possible moment, while calling connect(). + From `man ip`: + ``` Inform the kernel to not reserve an ephemeral port when using bind(2) with a port number of 0. The port will later be automatically chosen @@ -116,7 +145,9 @@ long as the 4-tuple is unique. ``` Ok, now with those two actions an application is really ready for cooperative working in ip-transparent chains. + If you are running in problems, with any kind of application, where you are redirecting transparent traffic to from sslh, check the following: + - Are you using a recent version of sslh, having the described feature enabled - are you using the most recent version of the other application If you can confirm both checks, tell the maintainers of the other application, about possible fixes, and send them a link to this article. @@ -124,6 +155,7 @@ If you can confirm both checks, tell the maintainers of the other application, a ## Practical Use Of Daisy-Chaining: Proxytunnel Endpoint ## One reasons, why we want to combine two programs is related to the core functionality of sslh. You wish to hide your ssh connection behind a https port. + But now you would like, to reach this port via the [proxy-tunnel application](https://github.com/proxytunnel/proxytunnel), though an restrictive http(s) proxy. This is in many cases, one of the few methods, to escape from restricted private networks, like in companies, schools and universities. Unfortunately, many of those proxy-servers will check, that the protocol leaving the proxy is really tls. Therefore you need and endpoint in your system, which will terminate the tls-connection and forward the encapsulated ssh stream to the sshd. Sslh can't do tls termination, as this is not a core job of tls. One of the solutions tried here is stunnel. Stunnel can do transparency like sslh, but unfortunately belongs to the uncooperative ip-transparent programs. At the time of writing this article, you can use stunnel as the first-in-chain, and a very recent sslh as second in chain. But nginx (or openresty) is capable of this, however it prefers (at least in the tested versions), mostly the second way just selecting a new random port and not (always) preserving the original source port, what makes debugging of events much easier. @@ -217,8 +249,11 @@ So it is not recommended for doing scp or sftp through such connections. ### Nginx Configration Explained ### However, there are situations, where this is the only way, to reach your server. + So we have now this nginx instance helping us out. + The server stanza listening on _192.168.255.254:1443_ is the main process, accepting incoming connections forwarded from sslh. It prereads tls SNI names, to get destination information. When in the nginx-configuration a variable is used, which is set by an map action, the mapping takes place just in that moment, when the variable needs to be expanded. + So we have either default values, in case there is no SNI, or values for individual SNI. The connection stream is than forwarded ip-transparent to a given destination, which is itself another nginx instance, defined in the configuration file. @@ -226,17 +261,21 @@ The connection stream is than forwarded ip-transparent to a given destination, w #### The Standard TLS-Termination #### The server stanza listening on _192.168.255.254:444_ is a very basic TLS terminating endpoint. In most cases, a proxy is happy with a destination like this. Here nginx just terminates the TLS connection and shovels the incoming packets over to sshd in ip-transparent way. The proxytunnel configuration for this target will look like: + ``` host t2.yourdomain.top ProxyCommand /path/to/proxytunnel -e -p PROXY-IP:3128 -d t2.yourdomain.top:443 ServerAliveInterval 30 ``` + If you have no proxytunnel on your system, you can also use openssl. In this case the configuration looks like: + ``` host t2-alias ProxyCommand openssl s_client -proxy PROXY-IP:3128 -connect t2.yourdomain.top:443 ServerAliveInterval 30 ``` + `ssh ts-alias` will use this configuration. One point from the above configuration needs further explanation, as we just refer to our common letsencrypt store, for certificates. @@ -244,7 +283,9 @@ One point from the above configuration needs further explanation, as we just ref ##### Excurse To File ACLs ##### We are using here our common letsencrypt certificate store, as copying certificates after renewal is a pain in the as and prone to errors. + A small script makes sure, that all applications can read the certificates: + ``` for i in Debian-exim dovecot mail www-data nginx ; do setfacl -Rdm u:$i:rX ./archive/ @@ -257,6 +298,7 @@ for i in Debian-exim dovecot mail www-data nginx ; do done done ``` + This script needs only to be run, when a new application user needs access. The first two lines are making sure (watch the **d**), that the given options on those directories will be the default for newly created files below. @@ -265,19 +307,25 @@ The uppercase **X** means, that **x**-access will only be granted to directories #### The Tricky TLS Termination #### The server stanza listening on _192.168.255.254:445_ is a more tricky TLS terminating endpoint. Some proxies are trying to inspect the destination, before letting you go. Some of them you can fool, others not. + One trick can be to use an phantasy domain name with a self signed certificate, no dns server will ever resolve. As you are using this name as SNI name in your proxy-tunnel connection, it will work. This can be also a way, to hide your sshd, if someone, who knows you are using sslh tries to find your sshd. As long, as this person does not get access to proxies you are using, this may help in some situations. + Another trick is, requiring a client certificate for authentication. This is in each case the much better approach, as you can combine this also with official connected domain names. Without the client certificate no sshd! + The client certificate does not increase the above mentioned ratio between payload and headers, as this is only used while establishing the TLS connection. + It prevents however a proxy, doing a parallel sneak to your destination, to figure out what is behind. I have seen proxies letting you connect with this method, others are denying access. To access this endpoint, the proxytunnel configuration inside `~/.ssh/config` will look like: + ``` host cryptic.foo.notexists ProxyCommand /path/to/proxytunnel -e -c ~/public.cert -k ~/private.pem -C ~/myOwnCA.cert -p PROXY-IP:3128 -o cryptic.foo.notexists -d t1.yourdomain.top:443 ServerAliveInterval 30 ``` --C gives the certfile to your certificate used for selfsigning the server certificate. You can also set -z for not verifying the certificate, but that is not recommended! `t1.yourdomain.top` represents a valid domain name, where the listening sslh can be reached. This name is not taken for SNI, because **-o** sets our hidden SNI name. Whenever you enter `ssh cryptic.foo.notexists` you will get connected to your server! -The best reommendation however is: Avoid self signed certs, if you are not really sure, what you are doing. +`-C` gives the certfile to your certificate used for selfsigning the server certificate. You can also set `-z` for not verifying the certificate, but that is not recommended! `t1.yourdomain.top` represents a valid domain name, where the listening sslh can be reached. This name is not taken for SNI, because **-o** sets our hidden SNI name. Whenever you enter `ssh cryptic.foo.notexists` you will get connected to your server! + +The best recommendation however is: Avoid self-signed certs, if you are not really sure what you are doing. diff --git a/doc/INSTALL.md b/doc/INSTALL.md index f761371..1d552f8 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -163,7 +163,7 @@ Installation cp scripts/etc.rc.d.init.d.sslh.centos /etc/rc.d/init.d/sslh ``` -You might need to create links in /etc/rc.d so that the server +You might need to create links in `/etc/rc.d` so that the server start automatically at boot-up, e.g. under Debian: ```sh update-rc.d sslh defaults diff --git a/doc/simple_transparent_proxy.md b/doc/simple_transparent_proxy.md index 27e8e19..bfc4df4 100644 --- a/doc/simple_transparent_proxy.md +++ b/doc/simple_transparent_proxy.md @@ -9,6 +9,7 @@ This chapter is a little excurse to the dataflow. First point of all is somethin Packets from local application talking to other local applications are routed through the loopback-interface. They leave postrouting to lo and reentering from there prerouting, without passing ingress/egress. This has nothing to do with the "**route_localnet = 1**" trick, which only makes, that the the local ip range 10.0.0.0/8 gets routed! As you can read in many articles, this is nothing you should do, as you may bring your system at risk, because it allows to leak packets from outside to applications, which feel themselves secure, by using those unroutable addresses. + #### A Simple Simulation #### You can prove this behaviour with a simple test: ``` diff --git a/doc/tproxy.md b/doc/tproxy.md index 3f28d14..eca2e87 100644 --- a/doc/tproxy.md +++ b/doc/tproxy.md @@ -1,7 +1,7 @@ # Transparent proxy using packet marking Before reading further, make sure you try to set up -transparent proxying using [IP routing](doc/simple_transparent_proxy.md). +transparent proxying using [IP routing](simple_transparent_proxy.md). It is conceptually easier to understand, cleaner, and more portable. From f2e86a3df74d6083d7d51cae9944cebf61a6378e Mon Sep 17 00:00:00 2001 From: yrutschle Date: Tue, 26 Aug 2025 22:17:58 +0200 Subject: [PATCH 05/11] do not log if accept returns EAGAIN --- tcp-listener.c | 1 + 1 file changed, 1 insertion(+) diff --git a/tcp-listener.c b/tcp-listener.c index 7572e62..9f2a16d 100644 --- a/tcp-listener.c +++ b/tcp-listener.c @@ -165,6 +165,7 @@ struct connection* accept_new_connection(struct listen_endpoint* endpoint, struc print_message(msg_fd, "accepting from %d\n", listen_socket); in_socket = accept(listen_socket, 0, 0); + if ((res == -1) && (errno == EAGAIN)) return NULL; /* Do not log if we're just retring next iteration */ CHECK_RES_RETURN(in_socket, "accept", NULL); res = set_nonblock(in_socket); From 829a7299e132e02e7dec1b72f615825ecc2a9be3 Mon Sep 17 00:00:00 2001 From: yrutschle Date: Tue, 26 Aug 2025 22:18:21 +0200 Subject: [PATCH 06/11] remove obsolete debug statement --- probe.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/probe.c b/probe.c index ed07a1c..eeb16b2 100644 --- a/probe.c +++ b/probe.c @@ -162,8 +162,6 @@ static int is_openvpn_protocol (const char*p,ssize_t len, struct sslhcfg_protoco if (len < 1) return PROBE_NEXT; - printf("opcode: %d\n", (p[0] & OVPN_OPCODE_MASK) >> 3); - if ((p[0] & OVPN_OPCODE_MASK) != OVPN_CONTROL_HARD_RESET_CLIENT_V1 && (p[0] & OVPN_OPCODE_MASK) != OVPN_CONTROL_HARD_RESET_CLIENT_V2 && (p[0] & OVPN_OPCODE_MASK) != OVPN_CONTROL_HARD_RESET_CLIENT_V3 From 2914fe7b5c169744fb3756d504ee22abbff46716 Mon Sep 17 00:00:00 2001 From: yrutschle Date: Tue, 26 Aug 2025 22:23:30 +0200 Subject: [PATCH 07/11] fix embarassingly wrong error check --- tcp-listener.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tcp-listener.c b/tcp-listener.c index 9f2a16d..1bd33ec 100644 --- a/tcp-listener.c +++ b/tcp-listener.c @@ -165,7 +165,7 @@ struct connection* accept_new_connection(struct listen_endpoint* endpoint, struc print_message(msg_fd, "accepting from %d\n", listen_socket); in_socket = accept(listen_socket, 0, 0); - if ((res == -1) && (errno == EAGAIN)) return NULL; /* Do not log if we're just retring next iteration */ + if ((in_socket == -1) && (errno == EAGAIN)) return NULL; /* Do not log if we're just retring next iteration */ CHECK_RES_RETURN(in_socket, "accept", NULL); res = set_nonblock(in_socket); From 3c237b3c9af928804afb1b85e162ceb191742e3e Mon Sep 17 00:00:00 2001 From: yrutschle Date: Tue, 26 Aug 2025 23:22:17 +0200 Subject: [PATCH 08/11] have some minimum hash size if no protocols have a max_connections setting --- processes.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/processes.c b/processes.c index 65be5bc..aa8acc0 100644 --- a/processes.c +++ b/processes.c @@ -160,7 +160,8 @@ void remember_child_data(struct loop_info* fd_info, pid2proto->proto = cnx->proto; pid2proto->endpoint = cnx->endpoint; if (hash_insert(fd_info->pid2proto, pid2proto)) { - /* TODO something if it fails */ + print_message(msg_int_error, "hash_insert for pid %d failed\n", pid); + /* TODO: error handling when this fails */ } } @@ -168,8 +169,11 @@ static int max_forking_connections(void) { int max_cnx = 0; for (int i = 0; i < cfg.protocols_len; i++) { - if (cfg.protocols[i].fork) - max_cnx += cfg.protocols[i].max_connections; + if (cfg.protocols[i].fork) { + if (cfg.protocols[i].max_connections_is_present) + max_cnx += cfg.protocols[i].max_connections; + else max_cnx += 512; /* A magic number that's hopefully high enough */ + } } return max_cnx; } From 4427a9570ff2a7ed2f78f33ca6b4ad69d745081c Mon Sep 17 00:00:00 2001 From: yrutschle Date: Wed, 27 Aug 2025 22:21:51 +0200 Subject: [PATCH 09/11] remove noisy logging from harmless interrupted system calls --- sslh-select.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sslh-select.c b/sslh-select.c index f43d28d..5893123 100644 --- a/sslh-select.c +++ b/sslh-select.c @@ -131,6 +131,7 @@ static void sigchld_process(struct loop_info* loop) received_sigchld = 0; do { chld = waitpid(-1, NULL, WNOHANG); + if ((chld == -1) && (errno == ECHILD)) return; CHECK_RES_RETURN(chld, "waitpid", ); if (chld) { decrease_forked_connection(loop, chld); @@ -221,8 +222,9 @@ void main_loop(struct listen_endpoint listen_sockets[], int num_addr_listen) fd_info.watchers->max_fd, fd_info.num_probing); res = select(fd_info.watchers->max_fd, &readfds, &writefds, NULL, fd_info.num_probing ? &tv : NULL); - if (res < 0) - perror("select"); + if ((res == -1) && ((errno != EAGAIN) && (errno != EINTR))) + print_message(msg_system_error, "%s:%d:%d:%s\n", + __FILE__, __LINE__, errno, strerror(errno)); \ /* Check main socket for new connections */ for (i = 0; i < num_addr_listen; i++) { From b6c8e116af1179c01c1a8efae0e3f6f397d56aa7 Mon Sep 17 00:00:00 2001 From: yrutschle Date: Wed, 27 Aug 2025 22:57:04 +0200 Subject: [PATCH 10/11] verify there is space in pid followup table before forking (fix #502) --- hash.c | 4 ++++ hash.h | 1 + processes.c | 11 ++++++++++- processes.h | 1 + tcp-listener.c | 6 ++++++ 5 files changed, 22 insertions(+), 1 deletion(-) diff --git a/hash.c b/hash.c index 5105254..e39f569 100644 --- a/hash.c +++ b/hash.c @@ -155,6 +155,10 @@ static int distance(int current_index, hash* h, hash_item item) return current_index - wanted_index + h->hash_size; } +int hash_insertable(hash* h) +{ + return (h->item_cnt < h->hash_size); +} int hash_insert(hash* h, hash_item new) { diff --git a/hash.h b/hash.h index 621790b..0a35d26 100644 --- a/hash.h +++ b/hash.h @@ -13,6 +13,7 @@ typedef int (*hash_cmp_item_fn)(hash_item item1, hash_item item2); hash* hash_init(int hash_size, hash_make_key_fn make_key, hash_cmp_item_fn cmp_item); +int hash_insertable(hash* h); /* Is it possible to insert: any space left? */ int hash_insert(hash* h, hash_item new); int hash_remove(hash* h, hash_item item); diff --git a/processes.c b/processes.c index aa8acc0..a1bd04e 100644 --- a/processes.c +++ b/processes.c @@ -152,6 +152,13 @@ void decrease_forked_connection(struct loop_info* loop, pid_t pid) { typedef struct pid2proto* hash_item; #include "hash.h" + +int has_space_to_fork(struct loop_info* fd_info) +{ + return hash_insertable(fd_info->pid2proto); +} + + void remember_child_data(struct loop_info* fd_info, struct connection* cnx, pid_t pid) { @@ -161,7 +168,9 @@ void remember_child_data(struct loop_info* fd_info, pid2proto->endpoint = cnx->endpoint; if (hash_insert(fd_info->pid2proto, pid2proto)) { print_message(msg_int_error, "hash_insert for pid %d failed\n", pid); - /* TODO: error handling when this fails */ + /* This should not happen as called is supposed to check that space is + * available in the hash */ + exit(1); } } diff --git a/processes.h b/processes.h index 6eb50ea..20566cc 100644 --- a/processes.h +++ b/processes.h @@ -37,6 +37,7 @@ void loop_init(struct loop_info* loop, int num_addr_listen); void remember_child_data(struct loop_info* fd_info, struct connection* cnx, pid_t pid); void decrease_forked_connection(struct loop_info* loop, pid_t pid); +int has_space_to_fork(struct loop_info* loop); /* These must be declared in the loop handler, sslh-ev or sslh-select */ void watchers_add_read(watchers* w, int fd); diff --git a/tcp-listener.c b/tcp-listener.c index 1bd33ec..5d2f8d0 100644 --- a/tcp-listener.c +++ b/tcp-listener.c @@ -299,6 +299,12 @@ static void connect_proxy(struct connection *cnx) /* Forks a shoveler process for one protocol */ void fork_shoveling_process(struct loop_info* fd_info, struct connection* cnx) { pid_t pid; + + if (!has_space_to_fork(fd_info)) { + print_message(msg_connections_error, "forking for %s: out of hash space\n", cnx->proto->name); + return; + } + switch (pid = fork()) { case 0: /* child */ /* TODO: close all file descriptors except 2 */ From 387ef13f525549dbaaa47da5a535a21e71defabf Mon Sep 17 00:00:00 2001 From: Yves Rutschle Date: Thu, 4 Sep 2025 23:50:17 +0200 Subject: [PATCH 11/11] fix memory leak when server-side is not yet attached, and potential memory leak when forking hash is full (fix #504) --- processes.c | 4 ++-- tcp-listener.c | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/processes.c b/processes.c index a1bd04e..27a10d6 100644 --- a/processes.c +++ b/processes.c @@ -46,9 +46,9 @@ int tidy_connection(struct connection *cnx, struct loop_info* fd_info) watchers_del_read(fd_info->watchers, cnx->q[i].fd); watchers_del_write(fd_info->watchers, cnx->q[i].fd); close(cnx->q[i].fd); - if (cnx->q[i].deferred_data) - free(cnx->q[i].deferred_data); } + if (cnx->q[i].begin_deferred_data) + free(cnx->q[i].begin_deferred_data); } if (cnx->type == SOCK_DGRAM) diff --git a/tcp-listener.c b/tcp-listener.c index 5d2f8d0..4840a58 100644 --- a/tcp-listener.c +++ b/tcp-listener.c @@ -318,11 +318,6 @@ void fork_shoveling_process(struct loop_info* fd_info, struct connection* cnx) { watcher_sigchld(fd_info, cnx, pid); break; } - /* Free file descriptor (used only in child), but do not reduce connection - * counts */ - cnx->proto = NULL; - cnx->endpoint = NULL; - tidy_connection(cnx, fd_info); } /* Process read activity on a socket in probe state @@ -356,6 +351,11 @@ void probing_read_process(struct connection* cnx, tidy_connection(cnx, fd_info); } else if (cnx->proto->fork) { fork_shoveling_process(fd_info, cnx); + /* Free file descriptor (used only in child), but do not reduce connection + * counts */ + cnx->proto = NULL; + cnx->endpoint = NULL; + tidy_connection(cnx, fd_info); } else { connect_queue(cnx, fd_info); }