Compare commits

..
19 Commits
Author SHA1 Message Date
Yves Rutschle 17a0425f4f incoming TLS connections get routed to GetEchClientHelloInner 2026-08-20 07:56:34 +00:00
Yves Rutschle 34645a62b2 ECH configs are build with sslh public name, not the target SNI... 2026-07-18 18:01:11 +00:00
Yves Rutschle 6d50082993 ECH configuration generation for each SNI, with DNS configuration printed in logs 2026-07-18 16:13:06 +00:00
Yves Rutschle 6219527ce4 remove warning if MAX is defined somewhere else 2026-06-20 16:31:59 +00:00
Yves Rutschle 0dd580f45b move symbol definition for Cygwin (fix #506) 2025-09-24 14:48:16 +02:00
Yves Rutschle 63e15bb55c Prepare for 2.3.0 2025-09-10 15:50:52 +02:00
Yves Rutschle 6f3dcfb365 Merge branch 'proxyprotocol' 2025-09-10 15:31:43 +02:00
Yves Rutschle f8e031ecb9 Merge branch 'master' of github.com:yrutschle/sslh 2025-09-10 15:29:59 +02:00
Yves Rutschle 387ef13f52 fix memory leak when server-side is not yet attached, and potential memory leak when forking hash is full (fix #504) 2025-09-04 23:50:17 +02:00
yrutschle b6c8e116af verify there is space in pid followup table before forking (fix #502) 2025-08-27 22:57:04 +02:00
yrutschle 4427a9570f remove noisy logging from harmless interrupted system calls 2025-08-27 22:21:51 +02:00
yrutschle 3c237b3c9a have some minimum hash size if no protocols have a max_connections setting 2025-08-26 23:22:17 +02:00
yrutschle 2914fe7b5c fix embarassingly wrong error check 2025-08-26 22:23:30 +02:00
yrutschle 829a7299e1 remove obsolete debug statement 2025-08-26 22:18:21 +02:00
yrutschle f2e86a3df7 do not log if accept returns EAGAIN 2025-08-26 22:17:58 +02:00
yrutschle f64f0c5ba5 some markdown formating fixes 2025-08-26 22:15:49 +02:00
Oleg Stepura 6d1e95cc52 Update podman.md 2025-08-26 22:00:56 +02:00
Oleg Stepura b9de23534e Update podman.md 2025-08-26 22:00:56 +02:00
Oleg Stepura 55872427f2 Update podman.md 2025-08-26 22:00:56 +02:00
27 changed files with 603 additions and 64 deletions

No files matched your search

+40 -2
View File
@@ -1,4 +1,41 @@
vNEXT:
ech branch:
This is an experimental version and does not work.
Do not use it unless you are experimenting with ECH
and an experimental wolfSSL.
A lot of context is discussed here:
https://github.com/wolfSSL/wolfssl/issues/10485
What it does:
- It creates a public ECH configuration for the
first public name (configure in the `listen`
section of the configuration file). This gets
printed in a file configured in `ech_conf_sink`
(see ech_test.cfg example).
- Upon incoming connections, it will call an
imaginary wolfSSL_GetEchClientHelloInner().
The resulting packet is hexdumped.
- It will also use wolfSSL to decode the SNI and
print it (but does not yet use it to forward).
Most things happen in tls_ech.c, in particular there
is a stub of GetEchClientHelloInner() that needs to
be removed when the function becomes available in
wolfSSL.
TODO:
Change configure script to check for appropriate
wolfSSL version.
Create ECH configurations for each public name
Link decoded InnerHello to SNI extraction code
....
Error cases still to be worked out (see
conversations on github)
Profit!
v2.3.0:
Added `max_connections` setting to `listen` and
`protocol` configuration; see the
[guide](doc/max_connections.md) for more
@@ -6,7 +43,8 @@ vNEXT:
accept() is now disabled for some time when ulimit -n is reached.
Support proxyprotocol on incoming connections.
Support proxyprotocol on incoming connections. See
the [guide](doc/proxyprotocol.md) for information.
Fix proxyprotocol target field (Thanks to Github user 404-Not-Found)
+1 -1
View File
@@ -35,7 +35,7 @@ CFLAGS +=-Wall -O2 -DLIBPCRE -g $(CFLAGS_COV) $(CFLAGS_SAN)
LIBS=-lm -lpcre2-8 @LIBS@
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
OBJS=sslh-conf.o common.o log.o sslh-main.o probe.o tls.o tls_ech.o argtable3.o collection.o gap.o tcp-probe.o landlock.o proxyprotocol.o
OBJS_A=libsslh.a
FORK_OBJS=sslh-fork.o $(OBJS_A)
SELECT_OBJS=processes.o udp-listener.o sslh-select.o hash.o tcp-listener.o $(OBJS_A)
-6
View File
@@ -25,12 +25,6 @@
#include <sys/prctl.h>
#endif
/* Added to make the code compilable under CYGWIN
* */
#ifndef SA_NOCLDWAIT
#define SA_NOCLDWAIT 0
#endif
/* Make use of systemd socket activation
* */
#ifdef SYSTEMD
+8
View File
@@ -9,6 +9,13 @@
#define FD_SETSIZE 4096
#endif
/* Cygwin does not define some symbols
* */
#ifndef SA_NOCLDWAIT
#define SA_NOCLDWAIT 0
#endif
#define _GNU_SOURCE
#include <sys/types.h>
#include <fcntl.h>
@@ -41,6 +48,7 @@
#include "config.h"
#include "version.h"
#undef MAX
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
+3
View File
@@ -18,4 +18,7 @@
/* libbsd, to change process name */
#undef HAVE_LIBBSD
/* wolfssl, to support Encrypted Client Hello (ECH) */
#undef HAVE_WOLFSSL
#endif
Vendored
+41
View File
@@ -3329,6 +3329,47 @@ then :
LIBS="$LIBS -lbsd"
fi
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for wolfSSL_Init in -lwolfssl" >&5
printf %s "checking for wolfSSL_Init in -lwolfssl... " >&6; }
if test ${ac_cv_lib_wolfssl_wolfSSL_Init+y}
then :
printf %s "(cached) " >&6
else $as_nop
ac_check_lib_save_LIBS=$LIBS
LIBS="-lwolfssl $LIBS"
cat confdefs.h - <<_ACEOF >conftest.$ac_ext
/* end confdefs.h. */
/* Override any GCC internal prototype to avoid an error.
Use char because int might match the return type of a GCC
builtin and then its argument prototype would still apply. */
char wolfSSL_Init ();
int
main (void)
{
return wolfSSL_Init ();
;
return 0;
}
_ACEOF
if ac_fn_c_try_link "$LINENO"
then :
ac_cv_lib_wolfssl_wolfSSL_Init=yes
else $as_nop
ac_cv_lib_wolfssl_wolfSSL_Init=no
fi
rm -f core conftest.err conftest.$ac_objext conftest.beam \
conftest$ac_exeext conftest.$ac_ext
LIBS=$ac_check_lib_save_LIBS
fi
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_wolfssl_wolfSSL_Init" >&5
printf "%s\n" "$ac_cv_lib_wolfssl_wolfSSL_Init" >&6; }
if test "x$ac_cv_lib_wolfssl_wolfSSL_Init" = xyes
then :
printf "%s\n" "#define HAVE_WOLFSSL 1" >>confdefs.h
LIBS="$LIBS -lwolfssl"
fi
ac_header= ac_cache=
for ac_item in $ac_header_c_list
+3
View File
@@ -11,6 +11,9 @@ AC_CHECK_LIB([wrap], [hosts_ctl], [AC_DEFINE(HAVE_LIBWRAP) LIBS="$LIBS -lwrap" ]
AC_CHECK_LIB([cap], [cap_get_proc], [AC_DEFINE(HAVE_LIBCAP) LIBS="$LIBS -lcap" ], [])
AC_CHECK_LIB([bsd], [setproctitle], [AC_DEFINE(HAVE_LIBBSD) LIBS="$LIBS -lbsd" ], [])
dnl TODO: check that wolfssl contains wolfSSL_GetEchClientHelloInner instead of just existing
AC_CHECK_LIB([wolfssl], [wolfSSL_Init], [AC_DEFINE(HAVE_WOLFSSL) LIBS="$LIBS -lwolfssl" ], [])
AC_CHECK_HEADERS(linux/landlock.h, AC_DEFINE(HAVE_LANDLOCK), [])
AC_CHECK_HEADERS(proxy_protocol.h, [AC_DEFINE(HAVE_PROXYPROTOCOL) LIBS="$LIBS -lproxyprotocol" ], [])
+53 -17
View File
@@ -33,6 +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
# ⚠️ 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:
@@ -95,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.
+45
View File
@@ -0,0 +1,45 @@
# sslh configuration for saaz:
# ssh to localhost
# https to nextcloud
foreground: true;
inetd: false;
numeric: false;
transparent: false;
user: "yves";
pidfile: "/tmp/sslh.pid";
syslog_facility: "auth";
# Where to write the ECH configurations
ech_conf_sink: "/tmp/ech";
# Logging configuration
# Value: 1: stdout; 2: syslog; 3: stdout+syslog; 4: logfile; ...; 7: all
verbose: 1;
# Change hostname with your external address name.
listen:
(
{ host: "inet"; port: "4443"; },
{ host: "chaton"; port: "4445"; }
);
protocols:
(
{ name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; fork: true; },
{ name: "tls"; sni_hostnames: [
"inet.rutschle.net", "lists.rutschle.net"
];
host: "localhost"; port: "4453"; },
{ name: "tls"; sni_hostnames: [
"chaton.rutschle.net"
];
host: "localhost"; port: "4455"; }
);
on-timeout: "ssh";
+1 -1
View File
@@ -1,5 +1,5 @@
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
* on Fri Aug 29 20:43:29 2025.
* on Wed Sep 10 15:49:49 2025.
# conf2struct: generate libconf parsers that read to structs
# Copyright (C) 2018-2024 Yves Rutschle
+1 -1
View File
@@ -1,5 +1,5 @@
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
* on Fri Aug 29 20:43:29 2025.
* on Wed Sep 10 15:49:49 2025.
# conf2struct: generate libconf parsers that read to structs
# Copyright (C) 2018-2024 Yves Rutschle
+3
View File
@@ -55,6 +55,9 @@ syslog_facility: "auth";
# is_unix: listen in the UNIX socket domain (default is TCP)
# keepalive: set TCP_KEEPALIVE
# max_connections: drop incoming connections after this count
# proxyprotocol: [true|false]: this endpoint expects
# proxyprotocol headers on incoming connections. Refer to
# doc/proxyprotocol.md
listen:
(
{ host: "thelonious"; port: "443"; max_connections: 5; },
+4
View File
@@ -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)
{
+1
View File
@@ -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);
-2
View File
@@ -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
+18 -5
View File
@@ -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)
@@ -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)
{
@@ -160,7 +167,10 @@ 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);
/* This should not happen as called is supposed to check that space is
* available in the hash */
exit(1);
}
}
@@ -168,8 +178,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;
}
+1
View File
@@ -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);
+62 -16
View File
@@ -1,5 +1,5 @@
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
* on Fri Aug 29 20:43:29 2025.
* on Sat Jul 18 17:34:37 2026.
# conf2struct: generate libconf parsers that read to structs
# Copyright (C) 2018-2024 Yves Rutschle
@@ -482,6 +482,8 @@ struct arg_file* sslhcfg_conffile;
struct arg_str* sslhcfg_chroot;
struct arg_str* sslhcfg_syslog_facility;
struct arg_str* sslhcfg_logfile;
struct arg_str* sslhcfg_ech_conf_sink;
struct arg_str* sslhcfg_ech_public_name;
struct arg_str* sslhcfg_on_timeout;
struct arg_str* sslhcfg_prefix;
struct arg_str* sslhcfg_listen;
@@ -926,7 +928,7 @@ static struct config_desc table_sslhcfg_listen[] = {
},
{ 0 }
};
static struct config_desc table_sslhcfg[] = {
@@ -1314,6 +1316,38 @@ static struct config_desc table_sslhcfg[] = {
/* default_val*/ .default_val.def_string = NULL
},
{
/* name */ "ech_conf_sink",
/* type */ CFG_STRING,
/* sub_group*/ NULL,
/* arg_cl */ & sslhcfg_ech_conf_sink,
/* base_addr */ NULL,
/* offset */ offsetof(struct sslhcfg_item, ech_conf_sink),
/* offset_len */ 0,
/* offset_present */ offsetof(struct sslhcfg_item, ech_conf_sink_is_present),
/* size */ sizeof(char*),
/* array_type */ -1,
/* mandatory */ 0,
/* optional */ 1,
/* default_val*/ .default_val.def_string = NULL
},
{
/* name */ "ech_public_name",
/* type */ CFG_STRING,
/* sub_group*/ NULL,
/* arg_cl */ & sslhcfg_ech_public_name,
/* base_addr */ NULL,
/* offset */ offsetof(struct sslhcfg_item, ech_public_name),
/* offset_len */ 0,
/* offset_present */ offsetof(struct sslhcfg_item, ech_public_name_is_present),
/* size */ sizeof(char*),
/* array_type */ -1,
/* mandatory */ 0,
/* optional */ 1,
/* default_val*/ .default_val.def_string = NULL
},
{
/* name */ "on_timeout",
/* type */ CFG_STRING,
@@ -1500,7 +1534,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: listen */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_listen,
.base_entry = & table_sslhcfg [26],
.base_entry = & table_sslhcfg [28],
.targets = sslhcfg_listen_targets,
@@ -1512,7 +1546,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: ssh */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_ssh,
.base_entry = & table_sslhcfg [27],
.base_entry = & table_sslhcfg [29],
.targets = sslhcfg_ssh_targets,
@@ -1524,7 +1558,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: tls */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_tls,
.base_entry = & table_sslhcfg [27],
.base_entry = & table_sslhcfg [29],
.targets = sslhcfg_tls_targets,
@@ -1536,7 +1570,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: ssl */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_ssl,
.base_entry = & table_sslhcfg [27],
.base_entry = & table_sslhcfg [29],
.targets = sslhcfg_ssl_targets,
@@ -1548,7 +1582,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: openvpn */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_openvpn,
.base_entry = & table_sslhcfg [27],
.base_entry = & table_sslhcfg [29],
.targets = sslhcfg_openvpn_targets,
@@ -1560,7 +1594,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: tinc */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_tinc,
.base_entry = & table_sslhcfg [27],
.base_entry = & table_sslhcfg [29],
.targets = sslhcfg_tinc_targets,
@@ -1572,7 +1606,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: wireguard */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_wireguard,
.base_entry = & table_sslhcfg [27],
.base_entry = & table_sslhcfg [29],
.targets = sslhcfg_wireguard_targets,
@@ -1584,7 +1618,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: xmpp */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_xmpp,
.base_entry = & table_sslhcfg [27],
.base_entry = & table_sslhcfg [29],
.targets = sslhcfg_xmpp_targets,
@@ -1596,7 +1630,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: http */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_http,
.base_entry = & table_sslhcfg [27],
.base_entry = & table_sslhcfg [29],
.targets = sslhcfg_http_targets,
@@ -1608,7 +1642,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: adb */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_adb,
.base_entry = & table_sslhcfg [27],
.base_entry = & table_sslhcfg [29],
.targets = sslhcfg_adb_targets,
@@ -1620,7 +1654,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: socks5 */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_socks5,
.base_entry = & table_sslhcfg [27],
.base_entry = & table_sslhcfg [29],
.targets = sslhcfg_socks5_targets,
@@ -1632,7 +1666,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: syslog */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_syslog,
.base_entry = & table_sslhcfg [27],
.base_entry = & table_sslhcfg [29],
.targets = sslhcfg_syslog_targets,
@@ -1644,7 +1678,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: msrdp */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_msrdp,
.base_entry = & table_sslhcfg [27],
.base_entry = & table_sslhcfg [29],
.targets = sslhcfg_msrdp_targets,
@@ -1656,7 +1690,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: anyprot */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_anyprot,
.base_entry = & table_sslhcfg [27],
.base_entry = & table_sslhcfg [29],
.targets = sslhcfg_anyprot_targets,
@@ -2345,6 +2379,8 @@ int sslhcfg_cl_parse(int argc, char* argv[], struct sslhcfg_item* cfg)
sslhcfg_chroot = arg_strn("C", "chroot", "<path>", 0, 1, "Root to change to after set-up"),
sslhcfg_syslog_facility = arg_strn(NULL, "syslog-facility", "<str>", 0, 1, "Facility to syslog to"),
sslhcfg_logfile = arg_strn(NULL, "logfile", "<str>", 0, 1, "Log messages to a file"),
sslhcfg_ech_conf_sink = arg_strn(NULL, "ech-conf-sink", "<str>", 0, 1, "File where to write ECH configurations"),
sslhcfg_ech_public_name = arg_strn(NULL, "ech-public-name", "<str>", 0, 1, "Public name for ECH client-facing proxy (usually, sslh's address)"),
sslhcfg_on_timeout = arg_strn(NULL, "on-timeout", "<str>", 0, 1, "Target to connect to when timing out"),
sslhcfg_prefix = arg_strn(NULL, "prefix", "<str>", 0, 1, "Reserved for testing"),
sslhcfg_listen = arg_strn("p", "listen", "<host:port>", 0, 10, "Listen on host:port"),
@@ -2621,6 +2657,16 @@ void sslhcfg_fprint(
fprintf(out, " <unset>");
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "ech_conf_sink: %s", sslhcfg->ech_conf_sink);
if (! sslhcfg->ech_conf_sink_is_present)
fprintf(out, " <unset>");
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "ech_public_name: %s", sslhcfg->ech_public_name);
if (! sslhcfg->ech_public_name_is_present)
fprintf(out, " <unset>");
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "on_timeout: %s", sslhcfg->on_timeout);
fprintf(out, "\n");
indent(out, depth);
+6 -1
View File
@@ -1,5 +1,5 @@
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
* on Fri Aug 29 20:43:29 2025.
* on Sat Jul 18 17:34:37 2026.
# conf2struct: generate libconf parsers that read to structs
# Copyright (C) 2018-2024 Yves Rutschle
@@ -38,6 +38,7 @@
#include "probe.h"
#include <sys/types.h>
#include <sys/socket.h>
#include <wolfssl/ssl.h>
#include <netdb.h>
struct sslhcfg_listen_item {
@@ -114,6 +115,10 @@ struct sslhcfg_item {
char* syslog_facility;
int logfile_is_present;
char* logfile;
int ech_conf_sink_is_present;
char* ech_conf_sink;
int ech_public_name_is_present;
char* ech_public_name;
char* on_timeout;
char* prefix;
size_t listen_len;
+2
View File
@@ -347,6 +347,8 @@ int main(int argc, char *argv[], char* envp[])
/* Open log file for writing */
setup_logfile();
ech_init(listen_sockets, num_addr_listen);
if (cfg.user || cfg.chroot)
drop_privileges(cfg.user, cfg.chroot);
setup_landlock();
+4 -2
View File
@@ -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++) {
+3
View File
@@ -18,6 +18,7 @@ includes: (
"probe.h",
" <sys/types.h>",
" <sys/socket.h>",
" <wolfssl/ssl.h>",
" <netdb.h>"
);
@@ -85,6 +86,8 @@ config: {
description: "Facility to syslog to"; },
{ name: "logfile"; type: "string"; optional: true;
description: "Log messages to a file" },
{ name: "ech_conf_sink"; type: "string"; optional: true;
description: "File where to write ECH configurations" },
{ name: "on-timeout"; type: "string"; default: "ssh";
description: "Target to connect to when timing out"; },
+12 -5
View File
@@ -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 ((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);
@@ -298,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 */
@@ -311,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
@@ -349,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);
}
+6
View File
@@ -61,6 +61,7 @@ static int parse_server_name_extension(const struct TLSProtocol *, const char *,
static int parse_alpn_extension(const struct TLSProtocol *, const char *, size_t);
static int has_match(const char**, size_t, const char*, size_t);
/* Parse a TLS packet for the Server Name Indication and ALPN extension in the client
* hello handshake, returning a status code
*
@@ -81,6 +82,11 @@ parse_tls_header(const struct TLSProtocol *tls_data, const char *data, size_t da
if (data_len < TLS_HEADER_LEN)
return TLS_ELENGTH;
/* If if it is an ECH, decrypt and match */
ech_match_sni(data, data_len);
tls_content_type = data[0];
if (tls_content_type != TLS_HANDSHAKE_CONTENT_TYPE) {
print_message(msg_probe_info, "Request did not begin with TLS handshake.\n");
+1
View File
@@ -44,5 +44,6 @@ struct TLSProtocol *tls_data_set_list(struct TLSProtocol *, int, const char**, s
#define TLS_ENOEXT -4 /* No ALPN or SNI extension found */
#define TLS_EPROTOCOL -5 /* Protocol error */
void ech_match_sni(const char* buffer_in, size_t buffer_len);
#endif
+284
View File
@@ -0,0 +1,284 @@
/*
# tls_ech.c: support for RFC9849 TLS Encrypted Client Hello (ECH)
#
# Copyright (C) 2026 Yves Rutschle
#
# This program is free software; you can redistribute it
# and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU General Public License for more
# details.
#
# The full text for the General Public License is here:
# http://www.gnu.org/licenses/gpl.html
*/
#define WOLFSSL_USE_OPTION_H
#include <wolfssl/options.h>
#include <wolfssl/ssl.h>
#include <wolfssl/wolfcrypt/random.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "config.h"
#include "log.h"
#include "sslh-conf.h"
#include "tls.h"
WOLFSSL_CTX* wolfssl_ctx;
// Hard-coded server certificate (PEM format)
// from snakeoil -- these serves no purpose beyond initialising the context
static const char* SERVER_CERT_PEM =
"-----BEGIN CERTIFICATE-----\n"
"MIICyjCCAbKgAwIBAgIUZrCzD4tEIu1r9EhkOn96raiupe4wDQYJKoZIhvcNAQEL\n"
"BQAwDzENMAsGA1UEAwwEc2FhejAeFw0yMTA1MjgyMDM0MjRaFw0zMTA1MjYyMDM0\n"
"MjRaMA8xDTALBgNVBAMMBHNhYXowggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\n"
"AoIBAQC5s1UhjcmsrRnSdJMH82NxRQ3hlwMxGZz5NnI0MukXN5kptAYSge4BQc6J\n"
"MAL1MbPtX0pfR+yBILWUdn6JCnDqhv9ipO8OpeVeh+RKa2kGiEdeoQhBbKNZdsoe\n"
"gD9FE3lE895GIhEv7Jd9bt/r8CcCpmOLuhJF6md0cLWGPrAp4l+iCV/ixyE0z0bK\n"
"fJzuOxi7oIeINLEiR0Z4hJsM2XAEXlBEsI4oh2KdDJiU6ow3w25IY7+/nYVDDLD2\n"
"QFMByfkEmAGp9Br+2tquRUmR8qmSvZPMtWpOzMv8+iKTrxUcAUCtCpDMjHqNR8fd\n"
"6G6TLenPbQPEaac5UgGZ7rJvL9idAgMBAAGjHjAcMAkGA1UdEwQCMAAwDwYDVR0R\n"
"BAgwBoIEc2FhejANBgkqhkiG9w0BAQsFAAOCAQEAZPasi1Hu7q4S9wJsbIzcGj7m\n"
"ZCAxNdm3AINeV3mxfhToeqtXSoa2sujCEgwCuiYi0OE8wmseLO8e1xIsRc/NEpOl\n"
"h9u5OupV2Xbf7nuKzPl31YQ7kv+90mnixizf6CFpHXAdU3WEZ33+fMtS+lV/ZBYo\n"
"Nh9GGNJ7Grv5KIKu/b2cR403pRF1hGrjWABrIDZO3ZCtNgRp/My1Qr+/04rTs8VF\n"
"WyDshOe0u6Xo//M3sCMbx5+62BopCMmAu2TxiPew0OTM8eDa3IBbc2kLGxp8c4J+\n"
"EYpAKTZbkhs/6SleNMC4BOEmZQ1tk6D0N0u4Y0b1IKEjgx7HqwZ6mIdmbU1lZQ==\n"
"-----END CERTIFICATE-----\n";
static const char* SERVER_KEY_PEM =
"-----BEGIN PRIVATE KEY-----\n"
"MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC5s1UhjcmsrRnS\n"
"dJMH82NxRQ3hlwMxGZz5NnI0MukXN5kptAYSge4BQc6JMAL1MbPtX0pfR+yBILWU\n"
"dn6JCnDqhv9ipO8OpeVeh+RKa2kGiEdeoQhBbKNZdsoegD9FE3lE895GIhEv7Jd9\n"
"bt/r8CcCpmOLuhJF6md0cLWGPrAp4l+iCV/ixyE0z0bKfJzuOxi7oIeINLEiR0Z4\n"
"hJsM2XAEXlBEsI4oh2KdDJiU6ow3w25IY7+/nYVDDLD2QFMByfkEmAGp9Br+2tqu\n"
"RUmR8qmSvZPMtWpOzMv8+iKTrxUcAUCtCpDMjHqNR8fd6G6TLenPbQPEaac5UgGZ\n"
"7rJvL9idAgMBAAECggEAbXNQctViD596/7rryhze+KztsI/UNsVU6uH3T+LN1XxU\n"
"jurnRVNFV4JU3DDrHV9tIDQw3pXCxJzlyRpKHDWGNgTZU2fI6sZGuX+4w1Apd8ss\n"
"3Y5mEkGYyaslF/MY13rR2B0wXf/IR4nYA/ssiaBjLcGqHHnQOVseHNgtdXFPXW9n\n"
"5IXJ2PSeaFW0Z13vbTQm9Cx2kCBKL4J3TymhkuSW3DUHxmlDfWUK14lkyXCZIZ3i\n"
"TbXQAXhwTrz3wqbG++iEP4MZM0Ka7/+WXqrsxA/yCirb+HnMewxwBKzGvkym2I2H\n"
"3BkjDn9exDgTePD7i0w22UYCF5uezDB9YOiPVZ634QKBgQDyKIgxD7XMMJezeg0Y\n"
"PaG+Eig/hE27mm2pyTQSd6MMZ1HpbDUpLQ/GO3Npy+bKS0/5eefNT4PWVcmpRL4W\n"
"blNIfixkcZq1a6wax9Nhk89jXZdhBinO8gfXFOiANFDS7B4fEXeTrfhmuryqUw9m\n"
"L6GJ0jk/H6fs6ads15y+O7A6tQKBgQDEUKlZg4qXxNMNKq7l+v7R6IH8VrNLt0I/\n"
"f4fhpW3IhmPJCRuOFOS0UpDPYjTeu14i3EFcpe0D+DVoQiggBTAh3I/qMHvRko5g\n"
"DT83YHp2/ec0AY0ocwnLwye224t6GF7S8R8vhG1FkIBk6DTJgWFZLDtY+dekZugz\n"
"IHvgtxSPSQKBgHWD7fPBJ/xbaIMUq94jqqZOsXGBhyePncBTgA2mOV3/leStOm8t\n"
"CwasOyoQZYOuYLU0z6T2/Ye6Qg7+6TCBgbEgafKknuuDwRWN+6rSzEXwWVIgZ5Gi\n"
"KuPZparxuHdjVorFMz5borxXys7tV//DBaWYe2eCuT9jdHiBefNni/IxAoGAWHTn\n"
"+deVqVEcsAAdkq8IBtk3SNZgL6vBhA8Y0QnTb9luOPWLnve3HbFeYrOjkwDmJ9sK\n"
"8I4rP/ClT/cPUW5FA9z4U9PI1uOsl4cghvlH9Tnu5bYVPranIVMsH+7I6Bj3ESFo\n"
"peaLvh6gW9dtgaZ0kNOnYi4hhoh/9Bmc9+JGt1ECgYAbGSQLa21L4Hx3NvMparVQ\n"
"bGjN26igvqKmhBaKrH8YolOlhEw/mCwTlg9zI7jrhdO0VClPrB8IVBoL+6HvrT21\n"
"LJM31J3ddqME9/5fQgKMQsD60kRDD6iV/iK2S6TOUjxJn1f6MuLmGSOHZYcbdBPZ\n"
"DyBKgbz/xGV6katjXvg74A==\n"
"-----END PRIVATE KEY-----\n";
#if 1
/* Will be provided by wolfSSL */
int wolfSSL_GetEchClientHelloInner(
WOLFSSL* ssl, /* Connected to an existing context with the ECH configs. Stores hpke state */
const byte* buffer_in, /* The full ClientHelloOuter */
word32 buffer_len, /* How many bytes in the buffer */
byte* buffer_out,
word32* buffer_out_len
)
{
char* inner = "some clienthelloinner";
*buffer_out_len = strlen(inner);
memcpy(buffer_out, inner, *buffer_out_len);
print_message(msg_fd, "GetEchClientHelloInner set\n");
return 0;
}
#endif
void ech_get_client_hello_inner(WOLFSSL_CTX* ctx, const byte* data, size_t datalen)
{
WOLFSSL* ssl = NULL;
printf("wolfSSL_new(%p)\n", ctx);
ssl = wolfSSL_new(ctx);
if (ssl == NULL) {
fprintf(stderr, "Failed to create WOLFSSL object\n");
return;
}
// Disable certificate verification on this connection
wolfSSL_set_verify(ssl, WOLFSSL_VERIFY_NONE, NULL);
byte client_hello_inner[1024];
word32 ch_len = sizeof(client_hello_inner);;
int res = wolfSSL_GetEchClientHelloInner(
ssl,
data, /* The full ClientHelloOuter */
datalen, /* How many bytes in the buffer */
(byte*)&client_hello_inner,
&ch_len
);
client_hello_inner[ch_len] = 0;
if (!res) {
print_message(msg_fd, "GetEchClientHelloInner: Innerlen: %d\n", ch_len);
hexdump(msg_fd, (char*)client_hello_inner, ch_len);
print_message(msg_fd, "/GetEchClientHelloInner\n");
}
byte sni_data[1024];
unsigned int sni_len = sizeof(sni_data);
memset(sni_data, 'A', sni_len);
printf("SNI_GetFromBuffer\n");
wolfSSL_SNI_GetFromBuffer(data, datalen, 0, (byte*)&sni_data, &sni_len);
sni_data[sni_len] = 0;
printf("sni_len = %d\n", sni_len);
printf("sni: %s\n", sni_data);
}
/* See if the context matches */
void ech_match_sni(const char* buffer_in, size_t buffer_len)
{
ech_get_client_hello_inner(wolfssl_ctx, (byte*)buffer_in, buffer_len);
}
/* Appends a configuration to the DNS configuration file */
static void append_conf(const char* sni, const char* ech)
{
FILE* f = fopen(cfg.ech_conf_sink, "a");
if (!f) {
print_message(msg_config_error, "Unable to open %s:%d:%s\n", cfg.ech_conf_sink, errno, strerror(errno));
return;
}
int res = fprintf(f, "%s IN HTTPS 1 . alpn=\"h2\" ech=\"%s\"\n", sni, ech);
printf("append: %s IN HTTPS 1 . alpn=\"h2\" ech=\"%s\" : %d\n", sni, ech, res);
fclose(f);
}
/* Erases the configuration file */
static void reset_conf(void)
{
FILE* f = fopen(cfg.ech_conf_sink, "w");
if (!f) {
print_message(msg_config_error, "Unable to open %s:%d:%s\n", cfg.ech_conf_sink, errno, strerror(errno));
return;
}
printf("erased and opend %s\n", cfg.ech_conf_sink);
}
/* Generate a keypair for a config; later this will be moved to a separate
* process, and sslh will just read it */
static void ech_genkey(WOLFSSL_CTX* ctx, const char* sni)
{
int ret;
printf("ech_genkey for ctx %p\n", ctx);
print_message(msg_config, "Generating ECH config for outer SNI: %s\n", sni);
ret = wolfSSL_CTX_GenerateEchConfig(ctx, sni, 0, 0, 0);
if (ret != WOLFSSL_SUCCESS) {
print_message(msg_system_error, "Failed to generate ECH config: %d\n", ret);
exit(1);
}
byte echConfig[512];
word32 echConfigLen = sizeof(echConfig);
ret = wolfSSL_CTX_GetEchConfigs(ctx, echConfig, &echConfigLen);
if (ret != WOLFSSL_SUCCESS) {
print_message(msg_system_error, "Failed to get ECH config: %d\n", ret);
exit(1);
}
char out[512];
word32 outLen = sizeof(out);
if (Base64_Encode_NoNl(echConfig, echConfigLen, (byte*)out, &outLen) != 0) {
print_message(msg_system_error, "Failed to encode to base64: %d\n", ret);
exit(1);
}
append_conf(sni, out);
}
/* For each public names, create a keypair and configuration.
* (Later, this will load the configurations from some place else */
static void ech_create_configs(WOLFSSL_CTX** ctx)
{
int ret;
reset_conf();
*ctx = wolfSSL_CTX_new(wolfTLSv1_3_server_method());
if (!*ctx) {
print_message(msg_system_error, "Failed to create WOLFSSL_CTX\n");
exit(1);
}
// Load certificate from memory
ret = wolfSSL_CTX_use_certificate_buffer(*ctx,
(const unsigned char*)SERVER_CERT_PEM,
strlen(SERVER_CERT_PEM),
WOLFSSL_FILETYPE_PEM);
if (ret != WOLFSSL_SUCCESS) {
fprintf(stderr, "Failed to load certificate: %d\n", ret);
fprintf(stderr, "Please replace SERVER_CERT_PEM with a valid certificate\n");
wolfSSL_CTX_free(*ctx);
exit(1);
}
// Load private key from memory
ret = wolfSSL_CTX_use_PrivateKey_buffer(*ctx,
(const unsigned char*)SERVER_KEY_PEM,
strlen(SERVER_KEY_PEM),
WOLFSSL_FILETYPE_PEM);
if (ret != WOLFSSL_SUCCESS) {
fprintf(stderr, "Failed to load private key: %d\n", ret);
fprintf(stderr, "Please replace SERVER_KEY_PEM with a valid private key\n");
wolfSSL_CTX_free(*ctx);
exit(1);
}
printf("Certificate and key loaded successfully\n");
ech_genkey(*ctx, cfg.listen[0].host); /* For now this only works on the first host */
}
/* Initialises the ECH subsystem: create wolfSSL context, create ECH configs,
* etc */
void ech_init()
{
wolfSSL_Init();
ech_create_configs(&wolfssl_ctx);
}
-5
View File
@@ -1,5 +0,0 @@
#ifndef VERSION_H
#define VERSION_H
#define VERSION "v2.2.4-52-geea50a9-dirty"
#endif