mirror of
https://github.com/yrutschle/sslh.git
synced 2026-10-22 18:12:10 +03:00
new test framework, with test classes for robustness and probe tests. Not all tests are ported yet.
This commit is contained in:
commit
071e7500a2
19 files changed
+696
-56
No files matched your search
@@ -10,6 +10,7 @@ sslh-ev
|
||||
systemd-sslh-generator
|
||||
sslh.8.gz
|
||||
tags
|
||||
version.h
|
||||
/config.status
|
||||
/config.log
|
||||
/config.h
|
||||
|
||||
@@ -1,11 +1,42 @@
|
||||
vNEXT:
|
||||
Fix proxyprotocol target field (Thanks to Github user 404-Not-Found)
|
||||
|
||||
v2.2.4:
|
||||
Fix CVE-2025-46806 for "Misaligned Memory Accesses in `is_openvpn_protocol()`"
|
||||
|
||||
Fix CVE-2025-46807 for "File Descriptor Exhaustion in sslh-select and sslh-ev"
|
||||
|
||||
Fix potential parsing of undefined data in syslog
|
||||
probe (no CVE assigned)
|
||||
|
||||
Thanks to Matthias Gerstner of the SUSE security
|
||||
team for the security review that found these
|
||||
defects!
|
||||
|
||||
v2.2.3:
|
||||
Reverse older commit: version.h cannot be included
|
||||
without breaking the build (everything recompiles
|
||||
every time) and the release archive creation (which
|
||||
relies on git tags).
|
||||
|
||||
v2.2.2:
|
||||
Fix potential vulnerability similar to CVE-2020-28935
|
||||
|
||||
v2.2.1:
|
||||
Added a boolean setting "is_unix" for listen and
|
||||
protocol entries. This will use the 'host' setting
|
||||
as a path name to a socket file, and connections
|
||||
(listening or connecting) will be performed on Unix
|
||||
socket instead of Internet sockets.
|
||||
|
||||
Support proxyprotocol on the backend server side.
|
||||
Support HAProxy's proxyprotocol on the backend
|
||||
server side.
|
||||
|
||||
Lots of documentation about a new, simpler way to
|
||||
perform transparent proxying.
|
||||
|
||||
New "verbose" option that overrides all other
|
||||
verbose settings.
|
||||
|
||||
v2.1.3:
|
||||
Fix Landlock access to /etc/hosts.deny and
|
||||
|
||||
+2
-4
@@ -22,7 +22,7 @@ MAN=sslh.8.gz # man page name
|
||||
# itself
|
||||
|
||||
ifneq ($(strip $(ENABLE_SANITIZER)),)
|
||||
CFLAGS_SAN=-fsanitize=address -fsanitize=leak -fsanitize=undefined
|
||||
CFLAGS_SAN=-fsanitize=address -fsanitize=leak -fsanitize=undefined -fsanitize=alignment
|
||||
endif
|
||||
|
||||
ifneq ($(strip $(COV_TEST)),)
|
||||
@@ -70,9 +70,8 @@ all: sslh-fork sslh-select $(MAN) echosrv $(CONDITIONAL_TARGETS)
|
||||
$(OBJS_A): $(OBJS)
|
||||
$(AR) rcs $(OBJS_A) $(OBJS)
|
||||
|
||||
version.h: .FORCE
|
||||
version.h:
|
||||
./genver.sh >version.h
|
||||
.FORCE:
|
||||
|
||||
$(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
|
||||
|
||||
@@ -145,7 +144,6 @@ distclean: clean
|
||||
|
||||
clean:
|
||||
rm -f sslh-fork sslh-select $(CONDITIONAL_TARGETS) echosrv version.h $(MAN) systemd-sslh-generator *.o *.gcov *.gcno *.gcda *.png *.html *.css *.info
|
||||
echo "// this is a placeholder for version.h, to make code-checking editors happy" > version.h
|
||||
|
||||
tags:
|
||||
ctags --globals -T *.[ch]
|
||||
|
||||
@@ -59,6 +59,7 @@ interfaces. The principle and basic setup is described
|
||||
[here](doc/simple_transparent_proxy.md), with further
|
||||
scenarios described [there](doc/scenarios-for-simple-transparent-proxy.md).
|
||||
|
||||
There is also a guide to use [podman](doc/podman.md).
|
||||
|
||||
Another method uses iptable packet marking features, and is
|
||||
highly dependent on your network environment and
|
||||
|
||||
@@ -986,26 +986,48 @@ void drop_privileges(const char* user_name, const char* chroot_path)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#ifndef O_NOFOLLOW
|
||||
#define O_NOFOLLOW 0
|
||||
#endif
|
||||
|
||||
/* Writes my PID */
|
||||
void write_pid_file(const char* pidfile)
|
||||
{
|
||||
FILE *f;
|
||||
int res;
|
||||
int fd;
|
||||
char pidbuf[32];
|
||||
size_t len, written = 0;
|
||||
ssize_t res;
|
||||
|
||||
f = fopen(pidfile, "w");
|
||||
if (!f) {
|
||||
/* Format PID as string */
|
||||
len = snprintf(pidbuf, sizeof(pidbuf), "%d\n", getpid());
|
||||
if (len >= sizeof(pidbuf)) {
|
||||
print_message(msg_system_error, "write_pid_file: PID string too long\n");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Open file with O_NOFOLLOW to prevent symlink attacks (Similar to CVE-2020-28935) */
|
||||
fd = open(pidfile, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW ,0644);
|
||||
|
||||
if (fd == -1) {
|
||||
print_message(msg_system_error, "write_pid_file: %s: %s\n", pidfile, strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
res = fprintf(f, "%d\n", getpid());
|
||||
if (res < 0) {
|
||||
print_message(msg_system_error, "write_pid_file: fprintf: %s\n", strerror(errno));
|
||||
/* Write PID to file with proper error handling */
|
||||
while (written < len) {
|
||||
res = write(fd, pidbuf + written, len - written);
|
||||
if (res == -1) {
|
||||
if (errno == EINTR || errno == EAGAIN)
|
||||
continue;
|
||||
print_message(msg_system_error, "write_pid_file: write: %s\n", strerror(errno));
|
||||
break;
|
||||
}
|
||||
written += res;
|
||||
}
|
||||
|
||||
res = fclose(f);
|
||||
if (res == EOF) {
|
||||
print_message(msg_system_error, "write_pid_file: fclose: %s\n", strerror(errno));
|
||||
return;
|
||||
/* Close file */
|
||||
if (close(fd) == -1) {
|
||||
print_message(msg_system_error, "write_pid_file: close: %s\n", strerror(errno));
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,10 @@
|
||||
#include <sys/capability.h>
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <AvailabilityMacros.h>
|
||||
#endif
|
||||
|
||||
#include "config.h"
|
||||
#include "version.h"
|
||||
|
||||
@@ -182,6 +186,10 @@ extern struct sslhcfg_item cfg;
|
||||
extern struct addrinfo *addr_listen;
|
||||
extern const char* server_type;
|
||||
|
||||
#if defined(__APPLE__) && (MAC_OS_X_VERSION_MIN_REQUIRED < 1080)
|
||||
extern int hosts_ctl();
|
||||
#endif
|
||||
|
||||
/* sslh-fork.c */
|
||||
void start_shoveler(int);
|
||||
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
Here is an example of deployment of sslh in transparent mode
|
||||
using ansible and podman, [submitted by Github user
|
||||
olegstepura](https://github.com/yrutschle/sslh/issues/448)
|
||||
|
||||
```yaml
|
||||
# ansible podman task
|
||||
- name: "Create sslh-co pod"
|
||||
containers.podman.podman_pod:
|
||||
name: sslh-co # read as "sslh and company"
|
||||
state: started
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:444"
|
||||
# ... (more ports if needed)
|
||||
network:
|
||||
- '{{ 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
|
||||
|
||||
- name: "Create the sslh container"
|
||||
containers.podman.podman_container:
|
||||
name: sslh
|
||||
image: "yrutschle/sslh:latest"
|
||||
pod: sslh-co
|
||||
capabilities:
|
||||
- NET_RAW
|
||||
- NET_BIND_SERVICE
|
||||
- NET_ADMIN
|
||||
sysctl:
|
||||
net.ipv4.conf.default.route_localnet: 1
|
||||
net.ipv4.conf.all.route_localnet: 1
|
||||
expose:
|
||||
- 444
|
||||
volume:
|
||||
# ... (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
|
||||
|
||||
- name: "Create the caddy container"
|
||||
containers.podman.podman_container:
|
||||
name: caddy
|
||||
image: "lucaslorentz/caddy-docker-proxy:alpine" # regular caddy or nginx image will also work
|
||||
pod: sslh-co
|
||||
expose:
|
||||
- 80
|
||||
- 443
|
||||
- 2020 # metrics, since caddy-docker-proxy uses :2019 internally
|
||||
volume:
|
||||
# ... (mount your configs and other stuff here)
|
||||
- "/var/run/podman/podman.sock:/var/run/docker.sock"
|
||||
state: started
|
||||
notify: podman restart sslh
|
||||
|
||||
- name: "Create the SSH proxy to host container"
|
||||
containers.podman.podman_container:
|
||||
name: ssh-proxy
|
||||
image: "alpine/socat:latest"
|
||||
pod: sslh-co
|
||||
expose:
|
||||
- 222
|
||||
command: TCP-LISTEN:222,fork TCP:host.containers.internal:22
|
||||
state: started
|
||||
```
|
||||
|
||||
```ini
|
||||
# sslh config
|
||||
foreground: true;
|
||||
inetd: false;
|
||||
numeric: true;
|
||||
transparent: true;
|
||||
timeout: 5;
|
||||
|
||||
listen:
|
||||
(
|
||||
{ host: "0.0.0.0"; port: "444"; keepalive: true; },
|
||||
);
|
||||
|
||||
protocols:
|
||||
(
|
||||
{
|
||||
name: "ssh";
|
||||
service: "ssh";
|
||||
host: "localhost";
|
||||
port: "222";
|
||||
fork: true;
|
||||
},
|
||||
{
|
||||
name: "http";
|
||||
host: "localhost";
|
||||
port: "80";
|
||||
},
|
||||
{
|
||||
name: "tls";
|
||||
host: "localhost";
|
||||
port: "443";
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
Scheme of port mapping is (all containers share same `localhost`):
|
||||
```
|
||||
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.
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
|
||||
* on Tue Apr 8 22:35:50 2025.
|
||||
* on Sun Jun 1 08:49:54 2025.
|
||||
|
||||
# conf2struct: generate libconf parsers that read to structs
|
||||
# Copyright (C) 2018-2024 Yves Rutschle
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
|
||||
* on Tue Apr 8 22:35:50 2025.
|
||||
* on Sun Jun 1 08:49:54 2025.
|
||||
|
||||
# conf2struct: generate libconf parsers that read to structs
|
||||
# Copyright (C) 2018-2024 Yves Rutschle
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#include <pwd.h>
|
||||
#include <syslog.h>
|
||||
#include <libgen.h>
|
||||
#include <getopt.h>
|
||||
#include <errno.h>
|
||||
|
||||
#define cfg sslhcfg
|
||||
@@ -323,9 +322,6 @@ int start_listen_sockets(struct listen_endpoint *sockfd[])
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
|
||||
extern char *optarg;
|
||||
extern int optind;
|
||||
int num_addr_listen;
|
||||
|
||||
struct listen_endpoint *listen_sockets;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#define PCRE2_CODE_UNIT_WIDTH 8
|
||||
#include <pcre2.h>
|
||||
#endif
|
||||
#include <regex.h>
|
||||
#include <ctype.h>
|
||||
#include "probe.h"
|
||||
#include "log.h"
|
||||
@@ -146,6 +147,7 @@ static int is_ssh_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_
|
||||
#define OVPN_OPCODE_MASK 0xF8
|
||||
#define OVPN_CONTROL_HARD_RESET_CLIENT_V1 (0x01 << 3)
|
||||
#define OVPN_CONTROL_HARD_RESET_CLIENT_V2 (0x07 << 3)
|
||||
#define OVPN_CONTROL_HARD_RESET_CLIENT_V3 (0x0A << 3)
|
||||
#define OVPN_HMAC_128 16
|
||||
#define OVPN_HMAC_160 20
|
||||
#define OVPN_HARD_RESET_PACKET_ID_OFFSET(hmac_size) (9 + hmac_size)
|
||||
@@ -164,8 +166,12 @@ 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_V2 &&
|
||||
(p[0] & OVPN_OPCODE_MASK) != OVPN_CONTROL_HARD_RESET_CLIENT_V3
|
||||
)
|
||||
return PROBE_NEXT;
|
||||
|
||||
/* The detection pattern above may not be reliable enough.
|
||||
@@ -176,12 +182,18 @@ static int is_openvpn_protocol (const char*p,ssize_t len, struct sslhcfg_protoco
|
||||
if (len <= OVPN_HARD_RESET_PACKET_ID_OFFSET(OVPN_HMAC_128) + sizeof(uint32_t))
|
||||
return PROBE_NEXT;
|
||||
|
||||
if (ntohl(*(uint32_t*)(p + OVPN_HARD_RESET_PACKET_ID_OFFSET(OVPN_HMAC_128))) <= 5u)
|
||||
uint32_t i;
|
||||
/* OVPN_HMAC_128 is unaligned, which requires special care e.g. on ARM */
|
||||
memcpy(&i, (p + OVPN_HARD_RESET_PACKET_ID_OFFSET(OVPN_HMAC_128)), sizeof(i));
|
||||
i = ntohl(i);
|
||||
if (i <= 5u)
|
||||
return PROBE_MATCH;
|
||||
|
||||
if (len <= OVPN_HARD_RESET_PACKET_ID_OFFSET(OVPN_HMAC_160) + sizeof(uint32_t))
|
||||
return PROBE_NEXT;
|
||||
|
||||
memcpy(&i, (p + OVPN_HARD_RESET_PACKET_ID_OFFSET(OVPN_HMAC_160)), sizeof(i));
|
||||
i = ntohl(i);
|
||||
if (ntohl(*(uint32_t*)(p + OVPN_HARD_RESET_PACKET_ID_OFFSET(OVPN_HMAC_160))) <= 5u)
|
||||
return PROBE_MATCH;
|
||||
|
||||
@@ -362,17 +374,38 @@ static int is_socks5_protocol(const char *p_in, ssize_t len, struct sslhcfg_prot
|
||||
return PROBE_MATCH;
|
||||
}
|
||||
|
||||
/* ******************
|
||||
* is_syslog_protocol
|
||||
* */
|
||||
static regex_t syslog_preg;
|
||||
static int configured_syslog_regex = 0;
|
||||
|
||||
static void config_syslog_regex(void)
|
||||
{
|
||||
/* two patterns for syslog messages:
|
||||
* <12> My message
|
||||
* 15 <12> My message
|
||||
* 12 is 'priority', 1 to 3 digits (RFC4234)
|
||||
* 15 is 'message length', a TCP-only option (RFC6587)
|
||||
*/
|
||||
int res = regcomp(&syslog_preg, "^([0-9]{1,3} )?<[0-9]{1,3}>", REG_EXTENDED);
|
||||
if (res) {
|
||||
print_message(msg_system_error, "regcomp");
|
||||
exit(1);
|
||||
}
|
||||
configured_syslog_regex = 1;
|
||||
}
|
||||
|
||||
static int is_syslog_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item* proto)
|
||||
{
|
||||
int res, i, j;
|
||||
char buf[len+1];
|
||||
|
||||
res = sscanf(p, "<%d>", &i);
|
||||
if (res == 1) return 1;
|
||||
if (!configured_syslog_regex) config_syslog_regex();
|
||||
|
||||
res = sscanf(p, "%d <%d>", &i, &j);
|
||||
if (res == 2) return 1;
|
||||
strncpy(buf, p, len);
|
||||
buf[len] = 0;
|
||||
|
||||
return 0;
|
||||
return (regexec(&syslog_preg, buf, (size_t)0, NULL, 0) == 0);
|
||||
}
|
||||
|
||||
static int is_teamspeak_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item* proto)
|
||||
|
||||
+24
-8
@@ -22,7 +22,7 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#if HAVE_PROXYPROTOCOL
|
||||
#ifdef HAVE_PROXYPROTOCOL
|
||||
|
||||
#include <proxy_protocol.h>
|
||||
#include "common.h"
|
||||
@@ -50,22 +50,38 @@ static int family_to_pp(int af_family)
|
||||
|
||||
typedef char libpp_addr[108]; /* This is hardcoded in libproxyprotocol/proxy_protocol.h */
|
||||
|
||||
|
||||
typedef enum {
|
||||
PEER,
|
||||
SOCK
|
||||
} sockpeer_t;
|
||||
|
||||
/* Fills *addr, *host and *serv with the connection information corresponding
|
||||
* to fd. *host is the IP address as string and *serv is the service (port)
|
||||
* to fd. sockpeer indicates if if is filled with the remote, or local, part of
|
||||
* the socket.
|
||||
* *host is the IP address as string and *serv is the service (port)
|
||||
* */
|
||||
static int get_info(int fd, struct addrinfo* addr, libpp_addr* host, uint16_t* serv)
|
||||
static int get_info(int fd, sockpeer_t sockpeer, struct addrinfo* addr, libpp_addr* host, uint16_t* serv)
|
||||
{
|
||||
char serv_str[NI_MAXSERV];
|
||||
int res;
|
||||
|
||||
res = getpeername(fd, addr->ai_addr, &addr->ai_addrlen);
|
||||
CHECK_RES_RETURN(res, "getpeername", -1);
|
||||
if (sockpeer == PEER) {
|
||||
res = getpeername(fd, addr->ai_addr, &addr->ai_addrlen);
|
||||
CHECK_RES_RETURN(res, "getpeername", -1);
|
||||
} else {
|
||||
res = getsockname(fd, addr->ai_addr, &addr->ai_addrlen);
|
||||
CHECK_RES_RETURN(res, "getsockname", -1);
|
||||
}
|
||||
|
||||
res = getnameinfo(addr->ai_addr, addr->ai_addrlen,
|
||||
(char*)host, sizeof(*host),
|
||||
serv_str, sizeof(serv_str),
|
||||
NI_NUMERICHOST | NI_NUMERICSERV );
|
||||
CHECK_RES_RETURN(res, "getnameinfo", -1);
|
||||
if (res != 0) {
|
||||
print_message(msg_system_error, "getnameinfo: %s\n", gai_strerror(res));
|
||||
return -1;
|
||||
}
|
||||
|
||||
*serv = atoi(serv_str);
|
||||
|
||||
@@ -89,14 +105,14 @@ int pp_write_header(int pp_version, struct connection* cnx)
|
||||
addr.ai_addr = (struct sockaddr*)&ss;
|
||||
addr.ai_addrlen = sizeof(ss);
|
||||
|
||||
res = get_info(cnx->q[0].fd,
|
||||
res = get_info(cnx->q[0].fd, PEER,
|
||||
&addr,
|
||||
&pp_info_in_v1.src_addr,
|
||||
&pp_info_in_v1.src_port);
|
||||
if (res == -1) return -1;
|
||||
pp_info_in_v1.address_family = family_to_pp(addr.ai_addr->sa_family);
|
||||
|
||||
res = get_info(cnx->q[1].fd,
|
||||
res = get_info(cnx->q[1].fd, SOCK,
|
||||
&addr,
|
||||
&pp_info_in_v1.dst_addr,
|
||||
&pp_info_in_v1.dst_port
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
|
||||
* on Tue Apr 8 22:35:50 2025.
|
||||
* on Sun Jun 1 08:49:54 2025.
|
||||
|
||||
# conf2struct: generate libconf parsers that read to structs
|
||||
# Copyright (C) 2018-2024 Yves Rutschle
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
|
||||
* on Tue Apr 8 22:35:50 2025.
|
||||
* on Sun Jun 1 08:49:54 2025.
|
||||
|
||||
# conf2struct: generate libconf parsers that read to structs
|
||||
# Copyright (C) 2018-2024 Yves Rutschle
|
||||
|
||||
@@ -280,9 +280,6 @@ void close_std(void)
|
||||
|
||||
int main(int argc, char *argv[], char* envp[])
|
||||
{
|
||||
|
||||
extern char *optarg;
|
||||
extern int optind;
|
||||
int res, num_addr_listen;
|
||||
struct listen_endpoint *listen_sockets;
|
||||
|
||||
|
||||
+7
-5
@@ -156,13 +156,15 @@ void main_loop(struct listen_endpoint listen_sockets[], int num_addr_listen)
|
||||
/* Check main socket for new connections */
|
||||
for (i = 0; i < num_addr_listen; i++) {
|
||||
if (FD_ISSET(listen_sockets[i].socketfd, &readfds)) {
|
||||
struct connection* new_cnx = cnx_accept_process(&fd_info, &listen_sockets[i]);
|
||||
|
||||
if (fd_out_of_range(new_cnx->q[0].fd))
|
||||
tidy_connection(new_cnx, &fd_info);
|
||||
|
||||
/* don't also process it as a read socket */
|
||||
FD_CLR(listen_sockets[i].socketfd, &readfds);
|
||||
|
||||
struct connection* new_cnx;
|
||||
while ((new_cnx = cnx_accept_process(&fd_info, &listen_sockets[i]))) {
|
||||
if (fd_out_of_range(new_cnx->q[0].fd))
|
||||
tidy_connection(new_cnx, &fd_info);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
#! /usr/bin/perl -w
|
||||
|
||||
# Test suite for sslh
|
||||
|
||||
# Uses Conf::Libconfig to read sslh config file: install
|
||||
# with:
|
||||
# cpan Conf::Libconfig
|
||||
|
||||
# Usage:
|
||||
# ./run # run all tests
|
||||
# ./run -l # list all tests
|
||||
# ./run 1 3 5 # run specified tests
|
||||
|
||||
use strict;
|
||||
use IO::Socket::INET6;
|
||||
use Test::More qw/no_plan/;
|
||||
use Conf::Libconfig 1.0.3;
|
||||
use Getopt::Long;
|
||||
|
||||
my ($coverage, $list_tests);
|
||||
GetOptions(
|
||||
'cover' => \$coverage,
|
||||
'list' => \$list_tests,
|
||||
);
|
||||
|
||||
|
||||
use Data::Dumper;
|
||||
|
||||
################################################################################
|
||||
# Global setup
|
||||
################################################################################
|
||||
my $no_listen = 8083; # Port on which no-one listens
|
||||
my $user = (getpwuid $<)[0]; # Run under current username
|
||||
|
||||
################################################################################
|
||||
# Helper functions
|
||||
################################################################################
|
||||
sub get_conf {
|
||||
my ($filename) = @_;
|
||||
my $conf = new Conf::Libconfig;
|
||||
$conf->read_file($filename) or die "$filename: $!";
|
||||
return $conf;
|
||||
}
|
||||
|
||||
# We want to keep track of tests to print a report at the
|
||||
# end, so we centralise all calls to Test::More::is here
|
||||
my $cnt = 1; # test counter
|
||||
my @results;
|
||||
sub my_is {
|
||||
my ($a, $b, $desc) = @_;
|
||||
|
||||
my $res = is($a, $b, $desc);
|
||||
push @results, [$cnt++, $desc, $res];
|
||||
}
|
||||
|
||||
#my $pidfile = $conf->lookup_value("pidfile");
|
||||
|
||||
sub verbose_exec
|
||||
{
|
||||
my ($cmd) = @_;
|
||||
|
||||
warn "$cmd\n";
|
||||
if (!fork) {
|
||||
exec $cmd;
|
||||
}
|
||||
}
|
||||
|
||||
# Returns a 'host:port' address targetting ssh from the configuration file
|
||||
sub ssh_address {
|
||||
my ($conf) = @_;
|
||||
my $ssh_conf = (grep { $_->{name} eq "ssh" } @{$conf->value("protocols")})[0];
|
||||
return $ssh_conf->{host} . ":" . $ssh_conf->{port};
|
||||
}
|
||||
|
||||
|
||||
# Returns the last TLS target from the configuration file
|
||||
sub ssl_address {
|
||||
my ($conf) = @_;
|
||||
my $ssl_conf = (grep { $_->{name} eq "tls" } @{$conf->value ("protocols")})[-1];
|
||||
return $ssl_conf->{host} . ":" . $ssl_conf->{port};
|
||||
}
|
||||
|
||||
# For SNI/ALPN, build a protocol name as such:
|
||||
# tls:sni1,sni2,...;alpn1,alpn2,...
|
||||
# input: a protocol entry from Libconfig
|
||||
sub make_sni_alpn_name {
|
||||
my ($prot) = @_;
|
||||
|
||||
return "tls:" . (join ",", @{$prot->{sni_hostnames} // []})
|
||||
. ";" . (join ",", @{$prot->{alpn_protocols} // [] });
|
||||
}
|
||||
|
||||
# Start an echoserver for each service
|
||||
my ($started_echosrvs);
|
||||
sub start_echosrv
|
||||
{
|
||||
my ($conf) = @_;
|
||||
|
||||
return if defined $started_echosrvs;
|
||||
|
||||
foreach my $s (@{$conf->value("protocols")}) {
|
||||
my $prefix = $s->{name};
|
||||
|
||||
$prefix =~ s/^ssl/tls/;
|
||||
|
||||
if ($s->{sni_hostnames} or $s->{alpn_protocols}) {
|
||||
$prefix = make_sni_alpn_name($s);
|
||||
}
|
||||
|
||||
verbose_exec "../echosrv --listen $s->{host}:$s->{port} --prefix '$prefix: '";
|
||||
}
|
||||
$started_echosrvs = 1;
|
||||
}
|
||||
|
||||
sub stop_echosrv
|
||||
{
|
||||
`killall echosrv` if $started_echosrvs;
|
||||
undef $started_echosrvs;
|
||||
}
|
||||
|
||||
|
||||
# Runs sslh
|
||||
my ($sslh_pid);
|
||||
sub start_sslh
|
||||
{
|
||||
my ($exe, $conf) = @_;
|
||||
return if defined $sslh_pid;
|
||||
my ($valgrind);
|
||||
if (!($sslh_pid = fork)) {
|
||||
my $user = (getpwuid $<)[0]; # Run under current username
|
||||
my $cmd = "../$exe -u $user -F $conf";
|
||||
print "****CMDLINE [$cmd]\n";
|
||||
#$valgrind = 1;
|
||||
#$cmd = "valgrind --leak-check=full $cmd";
|
||||
verbose_exec $cmd;
|
||||
exit 0;
|
||||
}
|
||||
sleep 1;
|
||||
warn "spawned $sslh_pid\n";
|
||||
sleep 5 if $valgrind; # valgrind can be heavy -- wait 5 seconds
|
||||
}
|
||||
|
||||
#stop sslh
|
||||
sub stop_sslh
|
||||
{
|
||||
if (defined $sslh_pid) {
|
||||
kill TERM => $sslh_pid or warn "kill process: $!\n";
|
||||
undef $sslh_pid;
|
||||
}
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Test methods
|
||||
################################################################################
|
||||
|
||||
# runs a test that ends in sslh exiting early
|
||||
# $cmdline: which command line to run
|
||||
# $test: test object
|
||||
# Returns: sslh exit code
|
||||
sub run_ending_test
|
||||
{
|
||||
my ($exe, $params, $test) = @_;
|
||||
my $conf = get_conf($test->{cfg});
|
||||
my $ssh_address = ssh_address($conf);
|
||||
my $ssl_address = ssl_address($conf);
|
||||
my $pidfile = $conf->lookup_value("pidfile");
|
||||
my $sslh_pid;
|
||||
if (!($sslh_pid = fork)) {
|
||||
my $cmdline = "../$exe $params";
|
||||
warn "eval `$cmdline`\n";
|
||||
$cmdline = eval "\"$cmdline\"";
|
||||
warn "running `$cmdline`\n";
|
||||
exec "$cmdline";
|
||||
}
|
||||
warn "spawned $sslh_pid\n";
|
||||
waitpid $sslh_pid, 0;
|
||||
my $code = $? >> 8;
|
||||
warn "exited with $code\n";
|
||||
my_is($code, $test->{exit}, "$exe: $test->{desc}");
|
||||
return $code;
|
||||
}
|
||||
|
||||
|
||||
|
||||
# Runs one test for one probe. Start echosrv's if required.
|
||||
# Extract sslh's listening port from the config file, run
|
||||
# sslh, connect, write the test pattern, read the result,
|
||||
# check it connected to the right echosrv, check the data
|
||||
# was transfered ok.
|
||||
sub run_test_probe
|
||||
{
|
||||
my ($exe, $test) = @_;
|
||||
|
||||
my $conf = get_conf($test->{cfg});
|
||||
start_echosrv($conf);
|
||||
start_sslh($exe, $test->{cfg});
|
||||
|
||||
my $expected = $test->{expected};
|
||||
|
||||
my $sslh_port = $conf->value("listen")->[0]->{port};
|
||||
print "test_probe [$expected] $sslh_port\n";
|
||||
my $cnx = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
|
||||
warn "t: $!\n" unless $cnx;
|
||||
return unless $cnx;
|
||||
|
||||
syswrite $cnx, $test->{data};
|
||||
|
||||
my $data;
|
||||
my $n = sysread $cnx, $data, 1024;
|
||||
$data =~ /^(.*?): /;
|
||||
my $prefix = $1;
|
||||
$data =~ s/$prefix: //g;
|
||||
print "Received $n bytes: protocol $prefix data [$data]\n";
|
||||
close $cnx;
|
||||
|
||||
my_is($prefix, $expected, "$exe:$expected: probe");
|
||||
my_is($data, $test->{data}, "$exe:$expected: shoveled");
|
||||
}
|
||||
|
||||
|
||||
################################################################################
|
||||
# Test bodies
|
||||
# Functions here each perform one test, and will be called with:
|
||||
# - exe: path the executable to test
|
||||
# - test: test object
|
||||
################################################################################
|
||||
|
||||
sub test_no_host
|
||||
{
|
||||
my ($exe, $test) = @_;
|
||||
run_ending_test(
|
||||
$exe,
|
||||
"-v 3 -f -u $user --listen $no_listen --ssh \$ssh_address --tls \$ssl_address -P \$pidfile",
|
||||
$test,
|
||||
);
|
||||
}
|
||||
|
||||
sub test_wrong_user
|
||||
{
|
||||
my ($exe, $test) = @_;
|
||||
run_ending_test(
|
||||
$exe,
|
||||
"-F $test->{cfg} -u ${user}_doesnt_exist --listen localhost:$no_listen --ssh \$ssh_address --tls \$ssl_address -P \$pidfile",
|
||||
$test,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
################################################################################
|
||||
|
||||
my @tests = (
|
||||
{
|
||||
desc => "No hostname in address",
|
||||
run => \&test_no_host,
|
||||
cfg => "test1.cfg",
|
||||
exit => 6,
|
||||
},
|
||||
{
|
||||
desc => "Changing to non-existant username",
|
||||
run => \&test_wrong_user,
|
||||
cfg => "test1.cfg",
|
||||
exit => 2,
|
||||
},
|
||||
|
||||
{
|
||||
desc => "ssh probe",
|
||||
run => \&run_test_probe,
|
||||
cfg => "test1.cfg",
|
||||
data => "SSH-2.0 Tester banner\n", # Sent to echosrv
|
||||
expected => "ssh", # prefix expected from echosrv
|
||||
},
|
||||
|
||||
{
|
||||
desc => "socks5 probe",
|
||||
run => \&run_test_probe,
|
||||
cfg => "test1.cfg",
|
||||
data => "\x05\x04\x01\x02\x03\x04",
|
||||
expected => "socks5", # prefix expected from echosrv
|
||||
},
|
||||
|
||||
);
|
||||
|
||||
|
||||
# Number tests
|
||||
my $i = 1;
|
||||
map { $_->{'index'} = $i++ } @tests;
|
||||
|
||||
# Only keep selected tests
|
||||
@tests = @tests[ map { $_ - 1 } @ARGV ] if @ARGV;
|
||||
|
||||
################################################################################
|
||||
#####List all tests
|
||||
format test_list_top =
|
||||
ID | Description
|
||||
----+-------------------------------------------------------------------------
|
||||
.
|
||||
format test_list =
|
||||
@>> | @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
$_->{'index'}, $_->{'desc'};
|
||||
.
|
||||
if ($list_tests) {
|
||||
format_name STDOUT "test_list";
|
||||
format_top_name STDOUT "test_list_top";
|
||||
map { write; } @tests;
|
||||
exit 0;
|
||||
}
|
||||
#####/list
|
||||
|
||||
################################################################################
|
||||
# Run selected tests
|
||||
foreach my $test (@tests) {
|
||||
warn "Running test: $test->{desc}\n";
|
||||
my $binary = 'sslh-ev';
|
||||
my $code = ($test->{run})->($binary, $test);
|
||||
}
|
||||
|
||||
stop_echosrv();
|
||||
stop_sslh();
|
||||
|
||||
done_testing();
|
||||
|
||||
format test_results_top =
|
||||
ID | Description | Status
|
||||
----+-------------------------------------------------------------------+-------
|
||||
.
|
||||
|
||||
format test_results =
|
||||
@>> | @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< | @>>
|
||||
$_->[0], $_->[1], $_->[2] ? "OK" : "NOK"
|
||||
.
|
||||
|
||||
format_name STDOUT "test_results";
|
||||
format_top_name STDOUT "test_results_top";
|
||||
map { write; } @results;
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
# Configuration file for testing (use both by sslh under
|
||||
# test and the test script `t`)
|
||||
|
||||
foreground: true;
|
||||
inetd: false;
|
||||
numeric: true;
|
||||
transparent: false;
|
||||
timeout: 10; # Probe test writes slowly
|
||||
pidfile: "/tmp/sslh_test.pid";
|
||||
|
||||
syslog_facility: "auth";
|
||||
|
||||
# Logging configuration
|
||||
# Value: 1: stdout; 2: syslog; 3: both
|
||||
# Defaults should be sensible. Generally, you want *-error
|
||||
# to be always enabled, to know if something is going wrong.
|
||||
verbose-config: 1; # print configuration at startup
|
||||
verbose-config-error: 1; # print configuration errors
|
||||
verbose-connections: 1; # trace established incoming address to forward address
|
||||
verbose-connections-error: 1; # connection errors
|
||||
verbose-connections-try: 1; # connection attempts towards targets
|
||||
verbose-fd: 0; # file descriptor activity, open/close/whatnot
|
||||
verbose-packets: 1; # hexdump packets on which probing is done
|
||||
verbose-probe-info: 1; # what's happening during the probe process
|
||||
verbose-probe-error: 1; # failures and problems during probing
|
||||
verbose-system-error: 1; # system call problem, i.e. malloc, fork, failing
|
||||
verbose-int-error: 1; # internal errors, the kind that should never happen
|
||||
|
||||
# List of interfaces on which we should listen
|
||||
# Options:
|
||||
listen:
|
||||
(
|
||||
{ host: "localhost"; port: "8080"; keepalive: true; proxyprotocol: true; },
|
||||
{ host: "localhost"; port: "8081"; keepalive: true; },
|
||||
{ host: "ip4-localhost"; is_udp: true; port: "8086"; },
|
||||
{ host: "/tmp/sslh.sock"; is_unix: true; port: ""; }
|
||||
);
|
||||
|
||||
|
||||
# Tester beware: when using fork, the forked process loses
|
||||
# track of buffers of other, concurrent connections. Memory
|
||||
# leak tools thus complain each time a forked process stops.
|
||||
|
||||
protocols:
|
||||
(
|
||||
{ name: "ssh"; host: "localhost"; port: "9000"; fork: true; transparent: true; resolve_on_forward: true; },
|
||||
{ name: "socks5"; host: "localhost"; port: "9001"; },
|
||||
{ name: "http"; host: "localhost"; port: "80"; },
|
||||
{ name: "tinc"; host: "localhost"; port: "9003"; },
|
||||
{ name: "openvpn"; host: "localhost"; port: "9004"; },
|
||||
{ name: "xmpp"; host: "localhost"; port: "9009"; },
|
||||
{ name: "adb"; host: "localhost"; port: "9010"; },
|
||||
{ name: "syslog"; host: "localhost"; port: "9013"; },
|
||||
{ name: "regex"; host: "ip4-localhost"; is_udp: true; port: "9020";
|
||||
udp_timeout: 30;
|
||||
regex_patterns: [ "^foo" ];
|
||||
resolve_on_forward: true;
|
||||
},
|
||||
{ name: "regex"; host: "localhost"; port: "9011";
|
||||
regex_patterns: [ "^foo", "^bar" ];
|
||||
minlength: 4;
|
||||
test_patterns: ( # this is used by the test script, not by sslh
|
||||
{ pattern: "foo"; result: "ssh"; }, # After timeout
|
||||
{ pattern: "fooo"; result: "regex"; },
|
||||
{ pattern: "bar"; result: "ssh"; },
|
||||
{ pattern: "barr"; result: "regex"; },
|
||||
{ pattern: "barrrr"; result: "regex"; }
|
||||
);
|
||||
},
|
||||
{ name: "tls"; host: "localhost"; port: "9021"; alpn_protocols: [ "alpn1", "alpn2" ]; sni_hostnames: [ "sni1" ]; },
|
||||
{ name: "tls"; host: "localhost"; port: "9022"; alpn_protocols: [ "alpn1", "alpn2" ]; sni_hostnames: [ "sni2", "sni3" ]; },
|
||||
{ name: "tls"; host: "localhost"; port: "9023"; alpn_protocols: [ "alpn3" ]; },
|
||||
{ name: "tls"; host: "localhost"; port: "9024"; sni_hostnames: [ "sni3" ]; },
|
||||
{ name: "tls"; host: "localhost"; port: "9025"; },
|
||||
{ name: "anyprot"; host: "localhost"; port: "9099"; }
|
||||
);
|
||||
|
||||
on_timeout: "ssh";
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
use strict;
|
||||
use IO::Socket::INET6;
|
||||
use Test::More qw/no_plan/;
|
||||
use Conf::Libconfig;
|
||||
use Conf::Libconfig 1.0.3;
|
||||
|
||||
my $conf = new Conf::Libconfig;
|
||||
$conf->read_file("test.cfg");
|
||||
@@ -17,7 +17,7 @@ $conf->read_file("test.cfg");
|
||||
|
||||
my $no_listen = 8083; # Port on which no-one listens
|
||||
my $pidfile = $conf->lookup_value("pidfile");
|
||||
my $sslh_port = $conf->fetch_array("listen")->[0]->{port};
|
||||
my $sslh_port = $conf->value("listen")->[0]->{port};
|
||||
my $user = (getpwuid $<)[0]; # Run under current username
|
||||
|
||||
# Which tests do we run
|
||||
@@ -84,8 +84,9 @@ sub make_sni_alpn_name {
|
||||
sub test_probe {
|
||||
my (%opts) = @_;
|
||||
|
||||
print "test_probe [$opts{expected}] $sslh_port\n";
|
||||
my $cnx = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
|
||||
warn "$!\n" unless $cnx;
|
||||
warn "t: $!\n" unless $cnx;
|
||||
return unless $cnx;
|
||||
|
||||
my $pattern = $opts{data};
|
||||
@@ -119,7 +120,7 @@ sub test_probe {
|
||||
sub test_probes {
|
||||
my (%in_opts) = @_;
|
||||
|
||||
my @probes = @{$conf->fetch_array("protocols")};
|
||||
my @probes = @{$conf->value("protocols")};
|
||||
foreach my $p (@probes) {
|
||||
my %protocols = (
|
||||
'ssh' => { data => "SSH-2.0 tester" },
|
||||
@@ -194,7 +195,7 @@ sub test_probes {
|
||||
|
||||
|
||||
# Start an echoserver for each service
|
||||
foreach my $s (@{$conf->fetch_array("protocols")}) {
|
||||
foreach my $s (@{$conf->value("protocols")}) {
|
||||
my $prefix = $s->{name};
|
||||
|
||||
$prefix =~ s/^ssl/tls/; # To remove in 1.21
|
||||
@@ -217,7 +218,7 @@ for my $binary (@binaries) {
|
||||
my ($sslh_pid, $valgrind);
|
||||
if (!($sslh_pid = fork)) {
|
||||
my $user = (getpwuid $<)[0]; # Run under current username
|
||||
my $cmd = "./$binary -v 4 -f -u $user -F test.cfg";
|
||||
my $cmd = "./$binary -u $user -F test.cfg";
|
||||
#$valgrind = 1;
|
||||
#$cmd = "valgrind --leak-check=full $cmd";
|
||||
verbose_exec $cmd;
|
||||
@@ -339,11 +340,11 @@ if ($RB_CNX_NOSERVER) {
|
||||
}
|
||||
|
||||
|
||||
my $ssh_conf = (grep { $_->{name} eq "ssh" } @{$conf->fetch_array("protocols")})[0];
|
||||
my $ssh_conf = (grep { $_->{name} eq "ssh" } @{$conf->value("protocols")})[0];
|
||||
my $ssh_address = $ssh_conf->{host} . ":" . $ssh_conf->{port};
|
||||
|
||||
# Use the last TLS echoserv (no SNI/ALPN)
|
||||
my $ssl_conf = (grep { $_->{name} eq "tls" } @{$conf->fetch_array("protocols")})[-1];
|
||||
my $ssl_conf = (grep { $_->{name} eq "tls" } @{$conf->value ("protocols")})[-1];
|
||||
my $ssl_address = $ssl_conf->{host} . ":" . $ssl_conf->{port};
|
||||
|
||||
|
||||
Reference in new issue
Block a user