Merge branch 'master' into patch-1

This commit is contained in:
Yves Rutschle 2023-08-31 15:26:22 +02:00 committed by GitHub
commit 04f258e705
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 4140 additions and 1160 deletions

63
.github/workflows/container-build.yaml vendored Normal file
View File

@ -0,0 +1,63 @@
name: Create and publish Container image
on:
push:
branches:
- master
tags:
- 'v*'
pull_request:
branches:
- master
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Container registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=edge
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build and push
uses: docker/build-push-action@v4
with:
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7
context: .
file: Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -1,3 +1,32 @@
v2.0:
New sslh-ev: this is functionally equivalent to
sslh-select (mono-process, only forks for specified
protocols), but based on libev, which should make it
scalable to large numbers of connections.
New log system: instead of --verbose with arbitrary
levels, there are now several message classes. Each
message class can be set to go to stderr, syslog, or
both. Classes are documented in example.cfg.
UDP connections are now managed in a hash to avoid
linear searches. The downside is that the number of
UDP connections is a hard limit, configurable with
the 'udp_max_connections', which defaults to 1024.
Timeouts are managed with lists.
inetd merges stderr output to what is sent to the
client, which is a security issue as it might give
information to an attacker. When inetd is activated,
stderr is forcibly closed.
New protocol-level option `resolve_on_forward`,
requests that target names are resolved at each
connection instead of at startup. Useful for dynamic
DNS situations. (Paul Schroeder/milkpirate)
New probe for MSRDP (akappner).
v1.22: 17AUG2021
sslh-select now supports UDP protocols.
Probes specified in the `protocols`
@ -12,7 +41,7 @@ v1.22: 17AUG2021
combined with incoming TLS with SNI. UDP clients
and servers need to agree on the IPv4/IPv6 they use:
use the same protocol on all sides! Often, this
means explicitely using 'ip4-localhost'.
means explicitly using 'ip4-localhost'.
UDP sender-receiver pairs (connections, so to speak)
are kept for 60s, which can be changed with
`udp_timeout` in the configuration.
@ -54,7 +83,7 @@ v1.21: 11JUL2020
Added TCP_FASTOPEN support for client sockets (if
tfo_ok is specified in their configuration) and for
listenint socket, if all client protocols support it.
listening socket, if all client protocols support it.
(Craig Andrews)
Added 'minlength' option to skip a probe if less
@ -80,8 +109,8 @@ v1.20: 20NOV2018
Before, probes were tried in order, repeating on the
same probe as long it returned PROBE_AGAIN before
moving to the next one. This means a probe which
requires a lot of data (i.e. returne PROBE_AGAIN for
a long time) could prevent sucessful matches from
requires a lot of data (i.e. return PROBE_AGAIN for
a long time) could prevent successful matches from
subsequent probes. The configuration file needed to
take that into account.
@ -142,7 +171,7 @@ v1.18: 29MAR2016
v1.17: 09MAR2015
Support RFC5952-style IPv6 addresses, e.g. [::]:443.
Transparant proxy support for FreeBSD.
Transparent proxy support for FreeBSD.
(Ruben van Staveren)
Using -F with no argument will try
@ -171,7 +200,7 @@ v1.16: 11FEB2014
Libcap support: Keep only CAP_NET_ADMIN if started
as root with transparent proxying and dropping
priviledges (enable USELIBCAP in Makefile). This
privileges (enable USELIBCAP in Makefile). This
avoids having to mess with filesystem capabilities.
(Sebastian Schmidt/yath)
@ -180,7 +209,7 @@ v1.16: 11FEB2014
actual errors if connections are dropped before
getting to getpeername).
Set IP_FREEDBIND if available to bind to addresses
Set IP_FREEBIND if available to bind to addresses
that don't yet exist.
v1.15: 27JUL2013
@ -265,7 +294,7 @@ v1.11: 21APR2012
--user isn't specified, just run as current user.
No longer create PID file by default, it should be
explicitely set with --pidfile.
explicitly set with --pidfile.
No longer log to syslog if in foreground. Logs are
instead output to stderr.
@ -356,7 +385,7 @@ v1.8: 15JUL2011
v1.7: 01FEB2010
Added CentOS init.d script (Andre Krajnik).
Fixed default ssl address inconsistancy, now
Fixed default ssl address inconsistency, now
defaults to "localhost:443" and fixed documentation
accordingly (pointed by Markus Schalke).

View File

@ -1,23 +1,38 @@
FROM alpine:latest as build
ARG ALPINE_VERSION="latest"
ARG TARGET_ARCH="library"
ADD . /sslh
FROM docker.io/${TARGET_ARCH}/alpine:${ALPINE_VERSION} AS build
RUN \
apk add \
gcc \
libconfig-dev \
make \
musl-dev \
pcre-dev \
perl && \
cd /sslh && \
make sslh-select && \
strip sslh-select
WORKDIR /sslh
FROM alpine:latest
RUN apk add --no-cache \
'gcc' \
'libconfig-dev' \
'make' \
'musl-dev' \
'pcre2-dev' \
'perl' \
;
COPY --from=build /sslh/sslh-select /sslh
COPY . /sslh
RUN apk --no-cache add libconfig pcre
RUN make sslh-select && strip sslh-select
ENTRYPOINT [ "/sslh", "--foreground"]
FROM docker.io/${TARGET_ARCH}/alpine:${ALPINE_VERSION}
COPY --from=build "/sslh/sslh-select" "/usr/local/bin/sslh"
RUN apk add --no-cache \
'libconfig' \
'pcre2' \
'iptables' \
'ip6tables' \
'libcap' \
&& \
adduser -s '/bin/sh' -S -D sslh && \
setcap cap_net_bind_service,cap_net_raw+ep /usr/local/bin/sslh
COPY "./container-entrypoint.sh" "/init"
ENTRYPOINT [ "/init" ]
# required for updating iptables
USER root:root

View File

@ -3,6 +3,7 @@ VERSION=$(shell ./genver.sh -r)
# Configuration -- you probably need to `make clean` if you
# change any of these
ENABLE_SANITIZER= # Enable ASAN/LSAN/UBSAN
ENABLE_REGEX=1 # Enable regex probes
USELIBCONFIG=1 # Use libconfig? (necessary to use configuration files)
USELIBWRAP?= # Use libwrap?
@ -19,15 +20,25 @@ MAN=sslh.8.gz # man page name
# End of configuration -- the rest should take care of
# itself
ifneq ($(strip $(ENABLE_SANITIZER)),)
CFLAGS_SAN=-fsanitize=address -fsanitize=leak -fsanitize=undefined
endif
ifneq ($(strip $(COV_TEST)),)
CFLAGS_COV=-fprofile-arcs -ftest-coverage
endif
CC ?= gcc
CFLAGS +=-Wall -DLIBPCRE -g $(CFLAGS_COV)
AR ?= ar
CFLAGS +=-Wall -O2 -DLIBPCRE -g $(CFLAGS_COV) $(CFLAGS_SAN)
LIBS=-lm -lpcre2-8
OBJS=sslh-conf.o common.o sslh-main.o probe.o tls.o argtable3.o udp-listener.o collection.o gap.o
OBJS=sslh-conf.o common.o log.o sslh-main.o probe.o tls.o argtable3.o collection.o gap.o tcp-probe.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)
EV_OBJS=processes.o udp-listener.o sslh-ev.o hash.o tcp-listener.o $(OBJS_A)
CONDITIONAL_TARGETS=
@ -64,32 +75,41 @@ endif
all: sslh $(MAN) echosrv $(CONDITIONAL_TARGETS)
.c.o: *.h version.h
$(CC) $(CFLAGS) $(CPPFLAGS) -c $<
%.o: %.c %.h version.h
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
$(OBJS_A): $(OBJS)
$(AR) rcs $(OBJS_A) $(OBJS)
version.h:
./genver.sh >version.h
sslh: sslh-fork sslh-select
sslh: sslh-fork sslh-select sslh-ev
$(OBJS): version.h common.h collection.h sslh-conf.h gap.h
$(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
c2s:
conf2struct sslhconf.cfg
conf2struct echosrv.cfg
sslh-conf.c sslh-conf.h: sslhconf.cfg
conf2struct sslhconf.cfg
$(warning "sslhconf.cfg is more recent than sslh-conf.[ch]. Use `make c2s` to rebuild using `conf2struct`")
sslh-fork: version.h $(OBJS) sslh-fork.o Makefile
$(CC) $(CFLAGS) $(LDFLAGS) -o sslh-fork sslh-fork.o $(OBJS) $(LIBS)
#strip sslh-fork
sslh-fork: version.h Makefile $(FORK_OBJS)
$(CC) $(CFLAGS) $(LDFLAGS) -o sslh-fork $(FORK_OBJS) $(LIBS)
sslh-select: version.h $(OBJS) sslh-select.o Makefile
$(CC) $(CFLAGS) $(LDFLAGS) -o sslh-select sslh-select.o $(OBJS) $(LIBS)
#strip sslh-select
sslh-select: version.h $(SELECT_OBJS) Makefile
$(CC) $(CFLAGS) $(LDFLAGS) -o sslh-select $(SELECT_OBJS) $(LIBS)
sslh-ev: version.h $(EV_OBJS) Makefile
$(CC) $(CFLAGS) $(LDFLAGS) -o sslh-ev $(EV_OBJS) $(LIBS) -lev
systemd-sslh-generator: systemd-sslh-generator.o
$(CC) $(CFLAGS) $(LDFLAGS) -o systemd-sslh-generator systemd-sslh-generator.o -lconfig
echosrv-conf.c echosrv-conf.h: echosrv.cfg
conf2struct echosrv.cfg
$(warning "echosrv.cfg is more recent than echosrv-conf.[ch]. Use `make c2s` to rebuild using `conf2struct`")
echosrv: version.h echosrv-conf.c echosrv.o echosrv-conf.o argtable3.o
$(CC) $(CFLAGS) $(LDFLAGS) -o echosrv echosrv.o echosrv-conf.o argtable3.o $(LIBS)
@ -133,7 +153,7 @@ distclean: clean
rm -f tags sslh-conf.[ch] echosrv-conf.[ch] cscope.*
clean:
rm -f sslh-fork sslh-select echosrv version.h $(MAN) systemd-sslh-generator *.o *.gcov *.gcno *.gcda *.png *.html *.css *.info
rm -f sslh-fork sslh-select sslh-ev echosrv version.h $(MAN) systemd-sslh-generator *.o *.gcov *.gcno *.gcda *.png *.html *.css *.info
tags:
ctags --globals -T *.[ch]

107
README.md
View File

@ -11,7 +11,7 @@ protocol that can be tested using a regular expression, can
be recognised. A typical use case is to allow serving
several services on port 443 (e.g. to connect to SSH from
inside a corporate firewall, which almost never block port
443) while still serving HTTPS on that port.
443) while still serving HTTPS on that port.
Hence `sslh` acts as a protocol demultiplexer, or a
switchboard. With the SNI and ALPN probe, it makes a good
@ -20,8 +20,8 @@ address.
`sslh` has the bells and whistles expected from a mature
daemon: privilege and capabilities dropping, inetd support,
systemd support, transparent proxying, chroot, logging,
IPv4 and IPv6, TCP and UDP, a fork-based and a select-based
systemd support, transparent proxying, chroot, logging,
IPv4 and IPv6, TCP and UDP, a fork-based and a select-based
model, and more.
Install
@ -44,25 +44,23 @@ How to use
---
Build docker image
make docker
```bash
docker container run \
docker run \
--cap-add CAP_NET_RAW \
--cap-add CAP_NET_BIND_SERVICE \
--rm \
-it \
ghcr.io/yrutschle/sslh:latest \
--foreground \
--listen=0.0.0.0:443 \
--ssh=hostname:22 \
--tlshostname:443 \
sslh:latest
--tls=hostname:443
```
docker-compose example
```
---
```yaml
version: "3"
services:
@ -70,19 +68,96 @@ services:
image: sslh:latest
hostname: sslh
ports:
- 443:443/tcp
command: --listen=0.0.0.0:443 --tlshostname:443 --openvpn=openvpn:1194
- 443:443
command: --foreground --listen=0.0.0.0:443 --tls=nginx:443 --openvpn=openvpn:1194
depends_on:
- nginx
- openvpn
nginx:
image: nginx
hostname: nginx
openvpn:
image: openvpn
```
Transparent mode 1: using sslh container for networking
_Note: For transparent mode to work, the sslh container must be able to reach your services via **localhost**_
```yaml
version: "3"
services:
sslh:
build: https://github.com/yrutschle/sslh.git
container_name: sslh
environment:
- TZ=${TZ}
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
sysctls:
- net.ipv4.conf.default.route_localnet=1
- net.ipv4.conf.all.route_localnet=1
command: --transparent --foreground --listen=0.0.0.0:443 --tls=localhost:8443 --openvpn=localhost:1194
ports:
- 443:443 #sslh
- 80:80 #nginx
- 8443:8443 #nginx
- 1194:1194 #openvpn
extra_hosts:
- localbox:host-gateway
restart: unless-stopped
nginx:
image: nginx:latest
.....
network_mode: service:sslh #set nginx container to use sslh networking.
# ^^^ This is required. This makes nginx reachable by sslh via localhost
openvpn:
image: openvpn:latest
.....
network_mode: service:sslh #set openvpn container to use sslh networking
```
Transparent mode 2: using host networking
```yaml
version: "3"
services:
sslh:
build: https://github.com/yrutschle/sslh.git
container_name: sslh
environment:
- TZ=${TZ}
cap_add:
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
# must be set manually
#sysctls:
# - net.ipv4.conf.default.route_localnet=1
# - net.ipv4.conf.all.route_localnet=1
command: --transparent --foreground --listen=0.0.0.0:443 --tls=localhost:8443 --openvpn=localhost:1194
network_mode: host
restart: unless-stopped
nginx:
image: nginx:latest
.....
ports:
- 8443:8443 # bind to docker host on port 8443
openvpn:
image: openvpn:latest
hostname: openvpn
.....
ports:
- 1194:1194 # bind to docker host on port 1194
```
Comments? Questions?

View File

@ -2876,9 +2876,9 @@ static void arg_file_resetfn(struct arg_file* parent) {
static const char* arg_basename(const char* filename) {
const char *result = NULL, *result1, *result2;
/* Find the last occurrence of eother file separator character. */
/* Two alternative file separator chars are supported as legal */
/* file separators but not both together in the same filename. */
/* Find the last occurrence of other file separator character. */
/* Two alternative file separator chars are supported as legal */
/* file separators but not both together in the same filename. */
result1 = (filename ? strrchr(filename, FILESEPARATOR1) : NULL);
result2 = (filename ? strrchr(filename, FILESEPARATOR2) : NULL);
@ -2927,7 +2927,7 @@ static int arg_file_scanfn(struct arg_file* parent, const char* argval) {
} else if (!argval) {
/* a valid argument with no argument value was given. */
/* This happens when an optional argument value was invoked. */
/* leave parent arguiment value unaltered but still count the argument. */
/* leave parent argument value unaltered but still count the argument. */
parent->count++;
} else {
parent->filename[parent->count] = argval;
@ -3173,7 +3173,7 @@ static int arg_int_scanfn(struct arg_int* parent, const char* argval) {
} else if (!argval) {
/* a valid argument with no argument value was given. */
/* This happens when an optional argument value was invoked. */
/* leave parent arguiment value unaltered but still count the argument. */
/* leave parent argument value unaltered but still count the argument. */
parent->count++;
} else {
long int val;
@ -3813,8 +3813,8 @@ static const TRexChar* g_nnames[] = {_SC("NONE"), _SC("OP_GREEDY"), _SC("OP_O
#endif
#define OP_GREEDY (MAX_CHAR + 1) /* * + ? {n} */
#define OP_OR (MAX_CHAR + 2)
#define OP_EXPR (MAX_CHAR + 3) /* parentesis () */
#define OP_NOCAPEXPR (MAX_CHAR + 4) /* parentesis (?:) */
#define OP_EXPR (MAX_CHAR + 3) /* parenthesis () */
#define OP_NOCAPEXPR (MAX_CHAR + 4) /* parenthesis (?:) */
#define OP_DOT (MAX_CHAR + 5)
#define OP_CLASS (MAX_CHAR + 6)
#define OP_CCLASS (MAX_CHAR + 7)
@ -5313,7 +5313,7 @@ static void arg_parse_untagged(int argc, char** argv, struct arg_hdr** table, st
}
}
/* if a tenative error still remains at this point then register it as a proper error */
/* if a tentative error still remains at this point then register it as a proper error */
if (errorlast) {
arg_register_error(endtable, parentlast, errorlast, optarglast);
optind++;
@ -5384,7 +5384,7 @@ int arg_parse(int argc, char** argv, void** argtable) {
/*
Fill in the local copy of argv[]. We need a local copy
because getopt rearranges argv[] which adversely affects
susbsequent parsing attempts.
subsequent parsing attempts.
*/
for (i = 0; i < argc; i++)
argvcopy[i] = argv[i];
@ -5451,7 +5451,7 @@ static void arg_cat_option(char* dest, size_t ndest, const char* shortopts, cons
if (shortopts) {
char option[3];
/* note: option array[] is initialiazed dynamically here to satisfy */
/* note: option array[] is initialized dynamically here to satisfy */
/* a deficiency in the watcom compiler wrt static array initializers. */
option[0] = '-';
option[1] = shortopts[0];
@ -5509,7 +5509,7 @@ static void arg_cat_optionv(char* dest, size_t ndest, const char* shortopts, con
/* "-a|-b|-c" */
char shortopt[3];
/* note: shortopt array[] is initialiazed dynamically here to satisfy */
/* note: shortopt array[] is initialized dynamically here to satisfy */
/* a deficiency in the watcom compiler wrt static array initializers. */
shortopt[0] = '-';
shortopt[1] = *c;
@ -5881,7 +5881,7 @@ static void arg_print_formatted_ds(arg_dstr_t ds, const unsigned lmargin, const
* Prints the glossary in strict GNU format.
* Differences to arg_print_glossary() are:
* - wraps lines after 80 chars
* - indents lines without shortops
* - indents lines without shortopts
* - does not accept formatstrings
*
* Contributed by Uli Fouquet
@ -5956,7 +5956,7 @@ int arg_nullcheck(void** argtable) {
* that entry were still allocated ok. Those subsequent allocations will not be
* deallocated by arg_free().
* Despite the unlikeliness of the problem occurring, and the even unlikelier event
* that it has any deliterious effect, it is fixed regardless by replacing arg_free()
* that it has any deleterious effect, it is fixed regardless by replacing arg_free()
* with the newer arg_freetable() function.
* We still keep arg_free() for backwards compatibility.
*/

View File

@ -87,7 +87,7 @@ typedef int(arg_comparefn)(const void* k1, const void* k2);
* that particular arg_xxx arguments, performing post-parse checks, and
* reporting errors.
* These functions are private to the individual arg_xxx source code
* and are the pointer to them are initiliased by that arg_xxx struct's
* and are the pointer to them are initialised by that arg_xxx struct's
* constructor function. The user could alter them after construction
* if desired, but the original intention is for them to be set by the
* constructor and left unaltered.
@ -95,7 +95,7 @@ typedef int(arg_comparefn)(const void* k1, const void* k2);
typedef struct arg_hdr {
char flag; /* Modifier flags: ARG_TERMINATOR, ARG_HASVALUE. */
const char* shortopts; /* String defining the short options */
const char* longopts; /* String defiing the long options */
const char* longopts; /* String defining the long options */
const char* datatype; /* Description of the argument data type */
const char* glossary; /* Description of the option as shown by arg_print_glossary function */
int mincount; /* Minimum number of occurences of this option accepted */

View File

@ -1,23 +1,25 @@
# This is a basic configuration file that should provide
# sensible values for "standard" setup.
verbose: 0;
foreground: false;
inetd: false;
numeric: false;
transparent: false;
# You will find extensive examples with explanations in
# example.cfg
timeout: 2;
user: "nobody";
pidfile: "/var/run/sslh.pid";
chroot: "/var/empty";
# Change hostname with your external address name.
# Change hostname with your external address name, or the IP
# of the interface that receives connections
listen:
(
{ host: "thelonious"; port: "443"; }
);
# Change to the protocols you want to forward to. The
# defaults here are sensible for services running on
# localhost
protocols:
(
{ name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; fork: true; },

View File

@ -30,8 +30,9 @@ struct cnx_collection {
gap_array* fd2cnx; /* Array indexed by file descriptor to things in cnx[] */
};
/* Allocates and initialises a new collection of connections. */
cnx_collection* collection_init(void)
/* Allocates and initialises a new collection of connections with at least
* `len` elements. */
cnx_collection* collection_init(int len)
{
cnx_collection* collection;
@ -40,7 +41,7 @@ cnx_collection* collection_init(void)
memset(collection, 0, sizeof(*collection));
collection->fd2cnx = gap_init();
collection->fd2cnx = gap_init(len);
return collection;
}

View File

@ -4,7 +4,7 @@
typedef struct cnx_collection cnx_collection;
cnx_collection* collection_init(void);
cnx_collection* collection_init(int len);
void collection_destroy(cnx_collection* collection);
struct connection* collection_alloc_cnx_from_fd(cnx_collection* collection, int fd);

169
common.c
View File

@ -4,7 +4,6 @@
* No code here should assume whether sockets are blocking or not.
**/
#define SYSLOG_NAMES
#define _GNU_SOURCE
#include <stddef.h>
#include <stdarg.h>
@ -16,6 +15,7 @@
#include "common.h"
#include "probe.h"
#include "log.h"
#include "sslh-conf.h"
/* Added to make the code compilable under CYGWIN
@ -42,8 +42,6 @@ struct sslhcfg_item cfg;
struct addrinfo *addr_listen = NULL; /* what addresses do we listen to? */
static int do_syslog = 1; /* Should we syslog? controled by syslog_facility = "none" */
#ifdef LIBWRAP
#include <tcpd.h>
int allow_severity =0, deny_severity = 0;
@ -60,7 +58,7 @@ void check_res_dump(CR_ACTION act, int res, struct addrinfo *addr, char* syscall
char buf[NI_MAXHOST];
if (res == -1) {
fprintf(stderr, "%s:%s: %s\n",
print_message(msg_system_error, "%s:%s: %s\n",
sprintaddr(buf, sizeof(buf), addr),
syscall,
strerror(errno));
@ -77,7 +75,7 @@ int get_fd_sockets(struct listen_endpoint *sockfd[])
#ifdef SYSTEMD
sd = sd_listen_fds(0);
if (sd < 0) {
fprintf(stderr, "sd_listen_fds(): %s\n", strerror(-sd));
print_message(msg_system_error, "sd_listen_fds(): %s\n", strerror(-sd));
exit(1);
}
if (sd > 0) {
@ -177,7 +175,7 @@ int start_listen_sockets(struct listen_endpoint *sockfd[])
*sockfd = NULL;
if (cfg.verbose) fprintf(stderr, "Listening to:\n");
print_message(msg_config, "Listening to:\n");
for (i = 0; i < cfg.listen_len; i++) {
keepalive = cfg.listen[i].keepalive;
@ -188,13 +186,12 @@ int start_listen_sockets(struct listen_endpoint *sockfd[])
for (addr = start_addr; addr; addr = addr->ai_next) {
num_addr++;
*sockfd = realloc(*sockfd, num_addr * sizeof(*sockfd));
*sockfd = realloc(*sockfd, num_addr * sizeof(*sockfd[0]));
(*sockfd)[num_addr-1].socketfd = listen_single_addr(addr, keepalive, udp);
(*sockfd)[num_addr-1].type = udp ? SOCK_DGRAM : SOCK_STREAM;
if (cfg.verbose)
fprintf(stderr, "%d:\t%s\t[%s] [%s]\n", (*sockfd)[num_addr-1].socketfd, sprintaddr(buf, sizeof(buf), addr),
cfg.listen[i].keepalive ? "keepalive" : "",
cfg.listen[i].is_udp ? "udp" : "");
print_message(msg_config, "%d:\t%s\t[%s] [%s]\n", (*sockfd)[num_addr-1].socketfd, sprintaddr(buf, sizeof(buf), addr),
cfg.listen[i].keepalive ? "keepalive" : "",
cfg.listen[i].is_udp ? "udp" : "");
}
freeaddrinfo(start_addr);
}
@ -322,19 +319,23 @@ int connect_addr(struct connection *cnx, int fd_from, connect_blocking blocking)
res = getpeername(fd_from, from.ai_addr, &from.ai_addrlen);
CHECK_RES_RETURN(res, "getpeername", res);
if (cnx->proto->resolve_on_forward) {
resolve_split_name(&(cnx->proto->saddr), cnx->proto->host,
cnx->proto->port);
}
for (a = cnx->proto->saddr; a; a = a->ai_next) {
/* When transparent, make sure both connections use the same address family */
if (transparent && a->ai_family != from.ai_addr->sa_family)
continue;
if (cfg.verbose)
fprintf(stderr, "connecting to %s family %d len %d\n",
print_message(msg_connections_try, "trying to connect to %s family %d len %d\n",
sprintaddr(buf, sizeof(buf), a),
a->ai_addr->sa_family, a->ai_addrlen);
/* XXX Needs to match ai_family from fd_from when being transparent! */
fd = socket(a->ai_family, SOCK_STREAM, 0);
if (fd == -1) {
log_message(LOG_ERR, "forward to %s failed:socket: %s\n",
print_message(msg_connections_error, "forward to %s failed:socket: %s\n",
cnx->proto->name, strerror(errno));
} else {
one = 1;
@ -347,6 +348,7 @@ int connect_addr(struct connection *cnx, int fd_from, connect_blocking blocking)
if (transparent) {
res = bind_peer(fd, fd_from);
if (res == -1) close(fd);
CHECK_RES_RETURN(res, "bind_peer", res);
}
res = connect(fd, a->ai_addr, a->ai_addrlen);
@ -354,7 +356,7 @@ int connect_addr(struct connection *cnx, int fd_from, connect_blocking blocking)
/* EINPROGRESS indicates it might take time. If it eventually
* fails, it'll be caught as a failed read */
if ((res == -1) && (errno != EINPROGRESS)) {
log_message(LOG_ERR, "forward to %s failed:connect: %s\n",
print_message(msg_connections_error, "forward to %s failed:connect: %s\n",
cnx->proto->name, strerror(errno));
close(fd);
continue; /* Try the next address */
@ -374,9 +376,8 @@ int defer_write(struct queue *q, void* data, int data_size)
{
char *p;
ptrdiff_t data_offset = q->deferred_data - q->begin_deferred_data;
if (cfg.verbose)
fprintf(stderr, "**** writing deferred on fd %d\n", q->fd);
print_message(msg_fd, "writing deferred on fd %d\n", q->fd);
p = realloc(q->begin_deferred_data, data_offset + q->deferred_data_size + data_size);
CHECK_ALLOC(p, "realloc");
@ -397,8 +398,7 @@ int flush_deferred(struct queue *q)
{
int n;
if (cfg.verbose)
fprintf(stderr, "flushing deferred data to fd %d\n", q->fd);
print_message(msg_fd, "flushing deferred data to fd %d\n", q->fd);
n = write(q->fd, q->deferred_data, q->deferred_data_size);
if (n == -1)
@ -430,11 +430,12 @@ void init_cnx(struct connection *cnx)
void dump_connection(struct connection *cnx)
{
printf("state: %d\n", cnx->state);
printf("0: fd %d, %d deferred\n", cnx->q[0].fd, cnx->q[0].deferred_data_size);
hexdump(cnx->q[0].deferred_data, cnx->q[0].deferred_data_size);
printf("1: fd %d, %d deferred\n", cnx->q[1].fd, cnx->q[1].deferred_data_size);
hexdump(cnx->q[1].deferred_data, cnx->q[1].deferred_data_size);
print_message(msg_int_error, "type: %s\n", cnx->type == SOCK_DGRAM ? "UDP" : "TCP");
print_message(msg_int_error, "state: %d\n", cnx->state);
print_message(msg_int_error, "0: fd %d, %d deferred\n", cnx->q[0].fd, cnx->q[0].deferred_data_size);
hexdump(msg_int_error, cnx->q[0].deferred_data, cnx->q[0].deferred_data_size);
print_message(msg_int_error, "1: fd %d, %d deferred\n", cnx->q[1].fd, cnx->q[1].deferred_data_size);
hexdump(msg_int_error, cnx->q[1].deferred_data, cnx->q[1].deferred_data_size);
}
@ -459,8 +460,6 @@ int fd2fd(struct queue *target_q, struct queue *from_q)
if (size_r == -1) {
switch (errno) {
case EAGAIN:
if (cfg.verbose)
fprintf(stderr, "reading 0 from %d\n", from);
return FD_NODATA;
case ECONNRESET:
@ -511,7 +510,7 @@ char* sprintaddr(char* buf, size_t size, struct addrinfo *a)
cfg.numeric ? NI_NUMERICHOST | NI_NUMERICSERV : 0 );
if (res) {
log_message(LOG_ERR, "sprintaddr:getnameinfo: %s\n", gai_strerror(res));
print_message(msg_system_error, "sprintaddr:getnameinfo: %s\n", gai_strerror(res));
/* Name resolution failed: do it numerically instead */
res = getnameinfo(a->ai_addr, a->ai_addrlen,
host, sizeof(host),
@ -519,7 +518,7 @@ char* sprintaddr(char* buf, size_t size, struct addrinfo *a)
NI_NUMERICHOST | NI_NUMERICSERV);
/* should not fail but... */
if (res) {
log_message(LOG_ERR, "sprintaddr:getnameinfo(NUM): %s\n", gai_strerror(res));
print_message(msg_system_error, "sprintaddr:getnameinfo(NUM): %s\n", gai_strerror(res));
strcpy(host, "?");
strcpy(serv, "?");
}
@ -548,7 +547,7 @@ int resolve_split_name(struct addrinfo **out, char* host, char* serv)
if (host[0] == '[') {
end = strrchr(host, ']');
if (!end) {
fprintf(stderr, "%s: no closing bracket in IPv6 address?\n", host);
print_message(msg_config_error, "%s: no closing bracket in IPv6 address?\n", host);
return -1;
}
host++; /* skip first bracket */
@ -557,7 +556,7 @@ int resolve_split_name(struct addrinfo **out, char* host, char* serv)
res = getaddrinfo(host, serv, &hint, out);
if (res)
log_message(LOG_ERR, "%s `%s:%s'\n", gai_strerror(res), host, serv);
print_message(msg_system_error, "%s `%s:%s'\n", gai_strerror(res), host, serv);
return res;
}
@ -573,7 +572,7 @@ void resolve_name(struct addrinfo **out, char* fullname)
/* Find port */
char *sep = strrchr(fullname, ':');
if (!sep) { /* No separator: parameter is just a port */
fprintf(stderr, "%s: names must be fully specified as hostname:port\n", fullname);
print_message(msg_config_error, "%s: names must be fully specified as hostname:port\n", fullname);
exit(1);
}
serv = sep+1;
@ -583,30 +582,13 @@ void resolve_name(struct addrinfo **out, char* fullname)
res = resolve_split_name(out, host, serv);
if (res) {
fprintf(stderr, "%s `%s'\n", gai_strerror(res), fullname);
print_message(msg_config_error, "%s `%s'\n", gai_strerror(res), fullname);
if (res == EAI_SERVICE)
fprintf(stderr, "(Check you have specified all ports)\n");
print_message(msg_config_error, "(Check you have specified all ports)\n");
exit(4);
}
}
/* Log to syslog or stderr if foreground */
void log_message(int type, const char* msg, ...)
{
va_list ap;
va_start(ap, msg);
if (cfg.foreground)
vfprintf(stderr, msg, ap);
va_end(ap);
if (do_syslog) {
va_start(ap, msg);
vsyslog(type, msg, ap);
va_end(ap);
}
}
/* Fills a connection description; returns 0 on failure */
int get_connection_desc(struct connection_desc* desc, const struct connection *cnx)
@ -641,30 +623,6 @@ int get_connection_desc(struct connection_desc* desc, const struct connection *c
return 1;
}
/* syslogs who connected to where
* desc: string description of the connection. if NULL, log_connection will
* manage on its own
* cnx: connection descriptor
* */
void log_connection(struct connection_desc* desc, const struct connection *cnx)
{
struct connection_desc d;
if (cnx->proto->log_level < 1)
return;
if (!desc) {
desc = &d;
get_connection_desc(desc, cnx);
}
log_message(LOG_INFO, "%s:connection from %s to %s forwarded from %s to %s\n",
cnx->proto->name,
desc->peer,
desc->service,
desc->local,
desc->target);
}
void set_proctitle_shovel(struct connection_desc* desc, const struct connection *cnx)
{
@ -708,8 +666,7 @@ int check_access_rights(int in_socket, const char* service)
/* extract peer address */
res = getnameinfo(&peer.saddr, size, addr_str, sizeof(addr_str), NULL, 0, NI_NUMERICHOST);
if (res) {
if (cfg.verbose)
fprintf(stderr, "getnameinfo(NI_NUMERICHOST):%s\n", gai_strerror(res));
print_message(msg_system_error, "getnameinfo(NI_NUMERICHOST):%s\n", gai_strerror(res));
strcpy(addr_str, STRING_UNKNOWN);
}
/* extract peer name */
@ -717,15 +674,12 @@ int check_access_rights(int in_socket, const char* service)
if (!cfg.numeric) {
res = getnameinfo(&peer.saddr, size, host, sizeof(host), NULL, 0, NI_NAMEREQD);
if (res) {
if (cfg.verbose)
fprintf(stderr, "getnameinfo(NI_NAMEREQD):%s\n", gai_strerror(res));
print_message(msg_system_error, "getnameinfo(NI_NAMEREQD):%s\n", gai_strerror(res));
}
}
if (!hosts_ctl(service, host, addr_str, STRING_UNKNOWN)) {
if (cfg.verbose)
fprintf(stderr, "access denied\n");
log_message(LOG_INFO, "connection from %s(%s): access denied", host, addr_str);
print_message(msg_connections, "connection from %s(%s): access denied", host, addr_str);
close(in_socket);
return -1;
}
@ -760,35 +714,6 @@ void setup_signals(void)
}
/* Open syslog connection with appropriate banner;
* banner is made up of basename(bin_name)+"[pid]" */
void setup_syslog(const char* bin_name) {
char *name1, *name2;
int res, fn;
if (!strcmp(cfg.syslog_facility, "none")) {
do_syslog = 0;
return;
}
name1 = strdup(bin_name);
res = asprintf(&name2, "%s[%d]", basename(name1), getpid());
CHECK_RES_DIE(res, "asprintf");
for (fn = 0; facilitynames[fn].c_val != -1; fn++)
if (strcmp(facilitynames[fn].c_name, cfg.syslog_facility) == 0)
break;
if (facilitynames[fn].c_val == -1) {
fprintf(stderr, "Unknown facility %s\n", cfg.syslog_facility);
exit(1);
}
openlog(name2, LOG_CONS, facilitynames[fn].c_val);
free(name1);
/* Don't free name2, as openlog(3) uses it (at least in glibc) */
log_message(LOG_INFO, "%s %s started\n", server_type, VERSION);
}
/* Ask OS to keep capabilities over a setuid(nonzero) */
void set_keepcaps(int val) {
@ -866,16 +791,14 @@ void drop_privileges(const char* user_name, const char* chroot_path)
if (user_name) {
pw = getpwnam(user_name);
if (!pw) {
fprintf(stderr, "%s: not found\n", user_name);
print_message(msg_config_error, "%s: not found\n", user_name);
exit(2);
}
if (cfg.verbose)
fprintf(stderr, "turning into %s\n", user_name);
print_message(msg_config, "turning into %s\n", user_name);
}
if (chroot_path) {
if (cfg.verbose)
fprintf(stderr, "chrooting into %s\n", chroot_path);
print_message(msg_config, "chrooting into %s\n", chroot_path);
res = chroot(chroot_path);
CHECK_RES_DIE(res, "chroot");
@ -905,14 +828,24 @@ void drop_privileges(const char* user_name, const char* chroot_path)
void write_pid_file(const char* pidfile)
{
FILE *f;
int res;
f = fopen(pidfile, "w");
if (!f) {
perror(pidfile);
print_message(msg_system_error, "write_pid_file:%s:%s", pidfile, strerror(errno));
exit(3);
}
fprintf(f, "%d\n", getpid());
fclose(f);
res = fprintf(f, "%d\n", getpid());
if (res < 0) {
print_message(msg_system_error, "write_pid_file:fprintf:%s", strerror(errno));
exit(3);
}
res = fclose(f);
if (res == EOF) {
print_message(msg_system_error, "write_pid_file:fclose:%s", strerror(errno));
exit(3);
}
}

View File

@ -40,20 +40,20 @@
#define CHECK_RES_DIE(res, str) \
if (res == -1) { \
fprintf(stderr, "%s:%d:", __FILE__, __LINE__); \
print_message(msg_system_error, "%s:%d:", __FILE__, __LINE__); \
perror(str); \
exit(1); \
}
#define CHECK_RES_RETURN(res, str, ret) \
if (res == -1) { \
log_message(LOG_CRIT, "%s:%d:%s:%d:%s\n", __FILE__, __LINE__, str, errno, strerror(errno)); \
print_message(msg_system_error, "%s:%d:%s:%d:%s\n", __FILE__, __LINE__, str, errno, strerror(errno)); \
return ret; \
}
#define CHECK_ALLOC(a, str) \
if (!a) { \
fprintf(stderr, "%s:%d:", __FILE__, __LINE__); \
print_message(msg_system_error, "%s:%d:", __FILE__, __LINE__); \
perror(str); \
exit(1); \
}
@ -83,9 +83,6 @@ enum connection_state {
ST_SHOVELING /* Connexion is established */
};
/* this is used to pass protocols through the command-line parameter parsing */
#define PROT_SHIFT 1000 /* protocol options will be 1000, 1001, etc */
/* A 'queue' is composed of a file descriptor (which can be read from or
* written to), and a queue for deferred write data */
struct queue {
@ -95,6 +92,12 @@ struct queue {
int deferred_data_size;
};
/* Double linked list for timeout management */
typedef struct {
struct connection* head;
struct connection* tail;
} dl_list;
struct connection {
int type; /* SOCK_DGRAM | SOCK_STREAM */
struct sslhcfg_protocols_item* proto; /* Where to connect to */
@ -109,13 +112,16 @@ struct connection {
struct queue q[2];
/* SOCK_DGRAM */
struct sockaddr client_addr; /* Contains the remote client address */
struct sockaddr_storage client_addr; /* Contains the remote client address */
socklen_t addrlen;
int local_endpoint; /* Contains the local address */
time_t last_active;
/* double linked list of timeouts */
struct connection *timeout_prev, *timeout_next;
/* We need one local socket for each target server, so we know where to
* forward server responses */
int target_sock;
@ -160,7 +166,6 @@ void setup_syslog(const char* bin_name);
void drop_privileges(const char* user_name, const char* chroot_path);
void set_capabilities(int cap_net_admin);
void write_pid_file(const char* pidfile);
void log_message(int type, const char* msg, ...);
void dump_connection(struct connection *cnx);
int resolve_split_name(struct addrinfo **out, char* hostname, char* port);
@ -171,7 +176,6 @@ int flush_deferred(struct queue *q);
extern struct sslhcfg_item cfg;
extern struct addrinfo *addr_listen;
extern const char* USAGE_STRING;
extern const char* server_type;
/* sslh-fork.c */

97
container-entrypoint.sh Executable file
View File

@ -0,0 +1,97 @@
#!/bin/sh
# SPDX-License-Identifier: GPL2-or-later
#
# Copyright (C) 2023 Olliver Schinagl <oliver@schinagl.nl>
#
# A beginning user should be able to docker run image bash (or sh) without
# needing to learn about --entrypoint
# https://github.com/docker-library/official-images#consistency
set -eu
bin='sslh'
# run command if it is not starting with a "-" and is an executable in PATH
if [ "${#}" -le 0 ] || \
[ "${1#-}" != "${1}" ] || \
[ -d "${1}" ] || \
! command -v "${1}" > '/dev/null' 2>&1; then
entrypoint='true'
fi
unconfigure_iptables() {
echo "Received SIG TERM/INT/KILL. Removing iptables / routing changes"
set +e # Don't exit if got error
set -x
iptables -t raw -D PREROUTING ! -i lo -d 127.0.0.0/8 -j DROP
iptables -t mangle -D POSTROUTING ! -o lo -s 127.0.0.0/8 -j DROP
iptables -t nat -D OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f
iptables -t mangle -D OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f
ip rule del fwmark 0x1 lookup 100
ip route del local 0.0.0.0/0 dev lo table 100
if [ $(cat /proc/sys/net/ipv6/conf/all/disable_ipv6) -eq 0 ]; then
ip6tables -t raw -D PREROUTING ! -i lo -d ::1/128 -j DROP
ip6tables -t mangle -D POSTROUTING ! -o lo -s ::1/128 -j DROP
ip6tables -t nat -D OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f
ip6tables -t mangle -D OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f
ip -6 rule del fwmark 0x1 lookup 100
ip -6 route del local ::/0 dev lo table 100
fi
set -e
set +x
}
configure_iptables() {
echo "Configuring iptables and routing..."
set +e # Don't exit if got error
set -x
iptables -t raw -A PREROUTING ! -i lo -d 127.0.0.0/8 -j DROP
iptables -t mangle -A POSTROUTING ! -o lo -s 127.0.0.0/8 -j DROP
iptables -t nat -A OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f
iptables -t mangle -A OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f
ip rule add fwmark 0x1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100
if [ $(cat /proc/sys/net/ipv6/conf/all/disable_ipv6) -eq 0 ]; then
ip6tables -t raw -A PREROUTING ! -i lo -d ::1/128 -j DROP
ip6tables -t mangle -A POSTROUTING ! -o lo -s ::1/128 -j DROP
ip6tables -t nat -A OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f
ip6tables -t mangle -A OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f
ip -6 rule add fwmark 0x1 lookup 100
ip -6 route add local ::/0 dev lo table 100
fi
set -e
set +x
}
for _args in "${@}" ; do
if [ "${_args:-}" = '--transparent' ] ; then
echo '--transparent flag is set'
configure_iptables
trap unconfigure_iptables TERM INT KILL
break
fi
done
# Drop privileges and run as sslh user
sslh_cmd="${entrypoint:+${bin}} ${@}"
echo "Executing with user 'sslh': ${sslh_cmd}"
exec su - sslh -c "${sslh_cmd}" &
wait "${!}"
exit 0

View File

@ -7,7 +7,7 @@ doesn't work, report how what was suggested here went.
It's also worth reading [how to ask
questions](http://www.catb.org/~esr/faqs/smart-questions.html)
before posting on the mailing list or opening an issue in
Github.
GitHub.
Getting more info
=================

View File

@ -7,7 +7,7 @@ Dependencies
`sslh` uses:
* [libconfig](http://www.hyperrealm.com/libconfig/). For
Debian this is contained in package `libconfig8-dev`. You
Debian this is contained in package `libconfig-dev`. You
can compile with or without it using USELIBCONFIG in the
Makefile.
@ -29,6 +29,13 @@ Makefile.
which requires `libbsd` at runtime, and `libbsd-dev` at
compile-time.
* libpcre2, in package `libpcre-dev`. You can compile
with or without it using ENABLE_REGEX in the Makefile.
* libev-dev, in package `libev-dev`. If you build a binary
specifically and do not build `sslh-ev`, you don't need
this.
For OpenSUSE, these are contained in packages libconfig9 and
libconfig-dev in repository
@ -44,6 +51,10 @@ distclean` for example), you will also need to add
[conf2struct](https://www.rutschle.net/tech/conf2struct/README.html)
(v1.5) to your path.
The test scripts are written in Perl, and will require
IO::Socket::INET6 (libio-socket-inet6-perl in Debian).
Compilation
-----------
@ -68,11 +79,25 @@ of the Makefile:
* `USELIBBSD` compiles support for updating the process name (as shown
by `ps`).
Generating the configuration parser
-----------------------------------
The configuration file and command line parser is generated
by `conf2struct`, from `sslhconf.cfg`, which generates
`sslh-conf.c` and `sslh-conf.h`. The resulting files are
included in the source so `sslh` can be built without
`conf2struct` installed.
Further, to prevent build issues, `sslh-conf.[ch]` has no
dependency to `sslhconf.cfg` in the Makefile. In the event
of adding configuration settings, they need to be
regenerated using `make c2s`.
Binaries
--------
The Makefile produces two different executables: `sslh-fork`
and `sslh-select`:
The Makefile produces three different executables: `sslh-fork`,
`sslh-select` and `sslh-ev`:
* `sslh-fork` forks a new process for each incoming connection.
It is well-tested and very reliable, but incurs the overhead
@ -81,18 +106,17 @@ If you are going to use `sslh` for a "small" setup (less than
a dozen ssh connections and a low-traffic https server) then
`sslh-fork` is probably more suited for you.
* `sslh-select` uses only one thread, which monitors all connections
at once. It is more recent and less tested, but only incurs a 16
byte overhead per connection. Also, if it stops, you'll lose all
connections, which means you can't upgrade it remotely.
If you are going to use `sslh` on a "medium" setup (a few thousand ssh
connections, and another few thousand ssl connections),
`sslh-select` will be better.
If you have a very large site (tens of thousands of connections),
you'll need a vapourware version that would use libevent or
something like that.
* `sslh-select` uses only one thread, which monitors all
connections at once. It only incurs a 16 byte overhead per
connection. Also, if it stops, you'll lose all connections,
which means you can't upgrade it remotely. If you are going
to use `sslh` on a "medium" setup (a few hundreds of
connections), or if you are on a system where forking is
expensive (e.g. Windows), `sslh-select` will be better.
* `sslh-ev` is similar to `sslh-select`, but uses `libev` as
a backend. This allows using specific kernel APIs that
allow to manage thousands of connections concurrently.
Installation
------------

View File

@ -33,7 +33,7 @@ with launchctl or simply reboot.
<string>0.0.0.0:443</string>
<string>--ssh</string>
<string>localhost:22</string>
<string>--ssl</string>
<string>--tls</string>
<string>localhost:443</string>
</array>
<key>QueueDirectories</key>

View File

@ -65,13 +65,13 @@ Configuration goes like this on the server side, using `stunnel3`:
* `-f` for foreground/debugging
* `-p` for specifying the key and certificate
* `-d` for specifying which interface and port
we're listening to for incoming connexions
we're listening to for incoming connections
* `-l` summons `sslh` in inetd mode.
* sslh options:
* `-i` for inetd mode
* `--http` to forward HTTP connexions to port 80,
and SSH connexions to port 22.
* `--http` to forward HTTP connections to port 80,
and SSH connections to port 22.
Capabilities support
--------------------
@ -92,165 +92,19 @@ to the executable:
sudo setcap cap_net_bind_service,cap_net_raw+pe sslh-select
Then you can run sslh-select as an unpriviledged user, e.g.:
Then you can run sslh-select as an unprivileged user, e.g.:
sslh-select -p myname:443 --ssh localhost:22 --tls localhost:443
Transparent proxy support
-------------------------
On Linux and FreeBSD you can use the `--transparent` option to
request transparent proxying. This means services behind `sslh`
(Apache, `sshd` and so on) will see the external IP and ports
as if the external world connected directly to them. This
simplifies IP-based access control (or makes it possible at
all).
Transparent proxying allows the target server to see the
original client IP address, i.e. `sslh` becomes invisible.
This makes it easier to use the server's logs, and potential
IP-based banning ability.
You can refer to Sean Warn'ѕ [tutorial](tproxy.md) for a
different set-up which enables transparent proxying between
two different machines. The following may only work if
`sslh` and the final servers are on the same machine.
Note that getting this to work is very tricky and
detail-dependant: depending on whether the target server and
sslh are on the same machine, different machines, or
different dockers, and tool versions, all seem to change the
required network configuration somewhat. If it doesn't work,
it's almost certain that the problem is not linked to `sslh`
but to the network setup that surrounds it.
Linux:
`sslh` needs extended rights to perform this: you'll need to
give it `CAP_NET_RAW` capabilities (see appropriate chapter)
or run it as root (but don't do that).
The firewalling tables also need to be adjusted as follows.
I don't think it is possible to have `httpd` and `sslh` both listen to 443 in
this scheme -- let me know if you manage that:
# Set route_localnet = 1 on all interfaces so that ssl can use "localhost" as destination
sysctl -w net.ipv4.conf.default.route_localnet=1
sysctl -w net.ipv4.conf.all.route_localnet=1
# DROP martian packets as they would have been if route_localnet was zero
# Note: packets not leaving the server aren't affected by this, thus sslh will still work
iptables -t raw -A PREROUTING ! -i lo -d 127.0.0.0/8 -j DROP
iptables -t mangle -A POSTROUTING ! -o lo -s 127.0.0.0/8 -j DROP
# Mark all connections made by ssl for special treatment (here sslh is run as user "sslh")
iptables -t nat -A OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f
# Outgoing packets that should go to sslh instead have to be rerouted, so mark them accordingly (copying over the connection mark)
iptables -t mangle -A OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f
# Configure routing for those marked packets
ip rule add fwmark 0x1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100
Tranparent proxying with IPv6 is similarly set up as follows:
# Set route_localnet = 1 on all interfaces so that ssl can use "localhost" as destination
# Not sure if this is needed for ipv6 though
sysctl -w net.ipv4.conf.default.route_localnet=1
sysctl -w net.ipv4.conf.all.route_localnet=1
# DROP martian packets as they would have been if route_localnet was zero
# Note: packets not leaving the server aren't affected by this, thus sslh will still work
ip6tables -t raw -A PREROUTING ! -i lo -d ::1/128 -j DROP
ip6tables -t mangle -A POSTROUTING ! -o lo -s ::1/128 -j DROP
# Mark all connections made by ssl for special treatment (here sslh is run as user "sslh")
ip6tables -t nat -A OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f
# Outgoing packets that should go to sslh instead have to be rerouted, so mark them accordingly (copying over the connection mark)
ip6tables -t mangle -A OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f
# Configure routing for those marked packets
ip -6 rule add fwmark 0x1 lookup 100
ip -6 route add local ::/0 dev lo table 100
Explanation:
To be able to use `localhost` as destination in your sslh config along with transparent proxying
you have to allow routing of loopback addresses as done above.
This is something you usually should not do (see [this stackoverflow post](https://serverfault.com/questions/656279/how-to-force-linux-to-accept-packet-with-loopback-ip/656484#656484))
The two `DROP` iptables rules emulate the behaviour of `route_localnet` set to off (with one small difference:
allowing the reroute-check to happen after the fwmark is set on packets destined for sslh).
See [this diagram](https://upload.wikimedia.org/wikipedia/commons/3/37/Netfilter-packet-flow.svg) for a good visualisation
showing how packets will traverse the iptables chains.
Note:
You have to run `sslh` as dedicated user (in this example the user is also named `sslh`), to not mess up with your normal networking.
These rules will allow you to connect directly to ssh on port
22 (or to any other service behind sslh) as well as through sslh on port 443.
Also remember that iptables configuration and ip routes and
rules won't be necessarily persisted after you reboot. Make
sure to save them properly. For example in CentOS7, you would
do `iptables-save > /etc/sysconfig/iptables`, and add both
`ip` commands to your `/etc/rc.local`.
FreeBSD:
Given you have no firewall defined yet, you can use the following configuration
to have ipfw properly redirect traffic back to sslh
/etc/rc.conf
firewall_enable="YES"
firewall_type="open"
firewall_logif="YES"
firewall_coscripts="/etc/ipfw/sslh.rules"
/etc/ipfw/sslh.rules
#! /bin/sh
# ssl
ipfw add 20000 fwd 192.0.2.1,443 log tcp from 192.0.2.1 8443 to any out
ipfw add 20010 fwd 2001:db8::1,443 log tcp from 2001:db8::1 8443 to any out
# ssh
ipfw add 20100 fwd 192.0.2.1,443 log tcp from 192.0.2.1 8022 to any out
ipfw add 20110 fwd 2001:db8::1,443 log tcp from 2001:db8::1 8022 to any out
# xmpp
ipfw add 20200 fwd 192.0.2.1,443 log tcp from 192.0.2.1 5222 to any out
ipfw add 20210 fwd 2001:db8::1,443 log tcp from 2001:db8::1 5222 to any out
# openvpn (running on other internal system)
ipfw add 20300 fwd 192.0.2.1,443 log tcp from 198.51.100.7 1194 to any out
ipfw add 20310 fwd 2001:db8::1,443 log tcp from 2001:db8:1::7 1194 to any out
General notes:
This will only work if `sslh` does not use any loopback
addresses (no `127.0.0.1` or `localhost`), you'll need to use
explicit IP addresses (or names):
sslh --listen 192.168.0.1:443 --ssh 192.168.0.1:22 --tls 192.168.0.1:4443
This will not work:
sslh --listen 192.168.0.1:443 --ssh 127.0.0.1:22 --tls 127.0.0.1:4443
Transparent proxying means the target server sees the real
origin address, so it means if the client connects using
IPv6, the server must also support IPv6. It is easy to
support both IPv4 and IPv6 by configuring the server
accordingly, and setting `sslh` to connect to a name that
resolves to both IPv4 and IPv6, e.g.:
sslh --transparent --listen <extaddr>:443 --ssh insideaddr:22
/etc/hosts:
192.168.0.1 insideaddr
201::::2 insideaddr
Upon incoming IPv6 connection, `sslh` will first try to
connect to the IPv4 address (which will fail), then connect
to the IPv6 address.
Set up can get complicated, so it has its own [document](tproxy.md).
Systemd Socket Activation
-------------------------
@ -313,7 +167,7 @@ This parses the /etc/sslh.cfg (or /etc/sslh/sslh.cfg file if that exists
instead) configuration file and dynamically generates a socket file to use.
This will also merge with any sslh.socket.d drop in configuration but will be
overriden by a /etc/systemd/system/sslh.socket file.
overridden by a /etc/systemd/system/sslh.socket file.
To use the generator place it in /usr/lib/systemd/system-generators and then
call systemctl daemon-reload after any changes to /etc/sslh.cfg to generate
@ -335,13 +189,15 @@ UDP
---
`sslh` can perform demultiplexing on UDP packets as well.
This only works with `sslh-select` (it is not possible to
This does not work with `sslh-fork` (it is not possible to
support UDP with a forking model). Specify a listening
address and target protocols with `is_udp: true`. `sslh`
will wait for incoming UDP packets, run the probes in the
usual fashion, and forward packets to the appropriate
target. `sslh` will then remember the association between
remote host to target server for 60 seconds by default,
which can be overriden with `udp_timeout`. This allows to
which can be overridden with `udp_timeout`. This allows to
process both single-datagram protocols such as DNS, and
connection-based protocols such as QUIC.
An example for supporting QUIC is shown in `example.cfg`.

View File

@ -1,17 +1,181 @@
Transparent Proxy to Two Hosts
==============================
# Transparent proxy
On Linux and FreeBSD you can use the `--transparent` option to
request transparent proxying. This means services behind `sslh`
(Apache, `sshd` and so on) will see the external IP and ports
as if the external world connected directly to them. This
simplifies IP-based access control (or makes it possible at
all).
This document shows recipes that may help to do that.
Note that getting this to work is very tricky and
detail-dependant: depending on whether the target server and
sslh are on the same machine, different machines, or
different dockers, and tool versions, all seem to change the
required network configuration somewhat. If it doesn't work,
it's almost certain that the problem is not linked to `sslh`
but to the network setup that surrounds it. If in trouble,
it might be worth trying to set up the network rules
with a simpler server than `sslh`, such as
[`socat`](http://www.dest-unreach.org/socat/)
Users have tried to do at least the following:
* `sslh` and the target servers run on the same host (see [below](#transparent-proxy-to-one-host))
* `sslh` runs on a host, and the target servers run on LXC or dockers running on the same host. No known working setup.
* `sslh` runs on a host, and the target servers run on different hosts on the same local network(see [below](#transparent-proxy-to-two-hosts))
* `sslh` runs on a host, and the target servers run on a different host on a different network (there is a [usecase](https://github.com/yrutschle/sslh/issues/295) for this). No known working setup, and it's unclear it is possible.
## Transparent proxy to one host
### Linux
`sslh` needs extended rights to perform this: you'll need to
give it `CAP_NET_RAW` capabilities (see appropriate chapter)
or run it as root (but don't do that).
The firewalling tables also need to be adjusted as follows.
I don't think it is possible to have `httpd` and `sslh` both listen to 443 in
this scheme -- let me know if you manage that:
# Set route_localnet = 1 on all interfaces so that ssl can use "localhost" as destination
sysctl -w net.ipv4.conf.default.route_localnet=1
sysctl -w net.ipv4.conf.all.route_localnet=1
# DROP martian packets as they would have been if route_localnet was zero
# Note: packets not leaving the server aren't affected by this, thus sslh will still work
iptables -t raw -A PREROUTING ! -i lo -d 127.0.0.0/8 -j DROP
iptables -t mangle -A POSTROUTING ! -o lo -s 127.0.0.0/8 -j DROP
# Mark all connections made by ssl for special treatment (here sslh is run as user "sslh")
iptables -t nat -A OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f
# Outgoing packets that should go to sslh instead have to be rerouted, so mark them accordingly (copying over the connection mark)
iptables -t mangle -A OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f
# Configure routing for those marked packets
ip rule add fwmark 0x1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100
Transparent proxying with IPv6 is similarly set up as follows:
# Set route_localnet = 1 on all interfaces so that ssl can use "localhost" as destination
# Not sure if this is needed for ipv6 though
sysctl -w net.ipv4.conf.default.route_localnet=1
sysctl -w net.ipv4.conf.all.route_localnet=1
# DROP martian packets as they would have been if route_localnet was zero
# Note: packets not leaving the server aren't affected by this, thus sslh will still work
ip6tables -t raw -A PREROUTING ! -i lo -d ::1/128 -j DROP
ip6tables -t mangle -A POSTROUTING ! -o lo -s ::1/128 -j DROP
# Mark all connections made by ssl for special treatment (here sslh is run as user "sslh")
ip6tables -t nat -A OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f
# Outgoing packets that should go to sslh instead have to be rerouted, so mark them accordingly (copying over the connection mark)
ip6tables -t mangle -A OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f
# Configure routing for those marked packets
ip -6 rule add fwmark 0x1 lookup 100
ip -6 route add local ::/0 dev lo table 100
Explanation:
To be able to use `localhost` as destination in your sslh config along with transparent proxying
you have to allow routing of loopback addresses as done above.
This is something you usually should not do (see [this stackoverflow post](https://serverfault.com/questions/656279/how-to-force-linux-to-accept-packet-with-loopback-ip/656484#656484))
The two `DROP` iptables rules emulate the behaviour of `route_localnet` set to off (with one small difference:
allowing the reroute-check to happen after the fwmark is set on packets destined for sslh).
See [this diagram](https://upload.wikimedia.org/wikipedia/commons/3/37/Netfilter-packet-flow.svg) for a good visualisation
showing how packets will traverse the iptables chains.
Note:
You have to run `sslh` as dedicated user (in this example the user is also named `sslh`), to not mess up with your normal networking.
These rules will allow you to connect directly to ssh on port
22 (or to any other service behind sslh) as well as through sslh on port 443.
Also remember that iptables configuration and ip routes and
rules won't be necessarily persisted after you reboot. Make
sure to save them properly. For example in CentOS7, you would
do `iptables-save > /etc/sysconfig/iptables`, and add both
`ip` commands to your `/etc/rc.local`.
### FreeBSD
Given you have no firewall defined yet, you can use the following configuration
to have ipfw properly redirect traffic back to sslh
/etc/rc.conf
firewall_enable="YES"
firewall_type="open"
firewall_logif="YES"
firewall_coscripts="/etc/ipfw/sslh.rules"
/etc/ipfw/sslh.rules
#! /bin/sh
# ssl
ipfw add 20000 fwd 192.0.2.1,443 log tcp from 192.0.2.1 8443 to any out
ipfw add 20010 fwd 2001:db8::1,443 log tcp from 2001:db8::1 8443 to any out
# ssh
ipfw add 20100 fwd 192.0.2.1,443 log tcp from 192.0.2.1 8022 to any out
ipfw add 20110 fwd 2001:db8::1,443 log tcp from 2001:db8::1 8022 to any out
# xmpp
ipfw add 20200 fwd 192.0.2.1,443 log tcp from 192.0.2.1 5222 to any out
ipfw add 20210 fwd 2001:db8::1,443 log tcp from 2001:db8::1 5222 to any out
# openvpn (running on other internal system)
ipfw add 20300 fwd 192.0.2.1,443 log tcp from 198.51.100.7 1194 to any out
ipfw add 20310 fwd 2001:db8::1,443 log tcp from 2001:db8:1::7 1194 to any out
General notes:
This will only work if `sslh` does not use any loopback
addresses (no `127.0.0.1` or `localhost`), you'll need to use
explicit IP addresses (or names):
sslh --listen 192.168.0.1:443 --ssh 192.168.0.1:22 --tls 192.168.0.1:4443
This will not work:
sslh --listen 192.168.0.1:443 --ssh 127.0.0.1:22 --tls 127.0.0.1:4443
Transparent proxying means the target server sees the real
origin address, so it means if the client connects using
IPv6, the server must also support IPv6. It is easy to
support both IPv4 and IPv6 by configuring the server
accordingly, and setting `sslh` to connect to a name that
resolves to both IPv4 and IPv6, e.g.:
sslh --transparent --listen <extaddr>:443 --ssh insideaddr:22
/etc/hosts:
192.168.0.1 insideaddr
201::::2 insideaddr
Upon incoming IPv6 connection, `sslh` will first try to
connect to the IPv4 address (which will fail), then connect
to the IPv6 address.
## Transparent Proxy to Two Hosts
Tutorial by Sean Warner. 19 June 2019 20:35
Aim
---
### Aim
* Show that `sslh` can transparently proxy requests from the internet to services on two separate hosts that are both on the same LAN.
* The IP address of the client initiating the request is what the destination should see… and not the IP address of the host that `sslh` is running on, which is what happens when `sslh` is not running in transparent mode.
* The solution here only works for my very specific use-case but hopefully others can adapt it to suits their needs.
Overview of my Network
----------------------
### Overview of my Network
Two Raspberry Pis on my home LAN:
* Pi A: 192.168.1.124 `sslh` (Port 4433), Apache2 web server for https (port 443), `stunnel` (port 4480) to decrypt ssh traffic and forward to SSH server (also on Pi A at Port 1022)
@ -20,8 +184,7 @@ Two Raspberry Pis on my home LAN:
![Architecture](tproxy.svg)
`sslh` build
------------
### `sslh` build
 
`sslh` Version: sslh v1.19c-2-gf451cc8-dirty.
@ -47,8 +210,7 @@ MAN=sslh.8.gz         # man page name
# itself
```
 
systemd setup
-------------
### systemd setup
Create an sslh systemd service file...
```
@ -83,8 +245,7 @@ Start it again to test…
# systemctl start sslh
```
 
Configure `sslh`
----------------
### Configure `sslh`
First stop `sslh` then open the config file and replace with below, save and start `sslh` again
```
@ -123,8 +284,7 @@ protocols:
);
```
 
Configure `stunnel`
-------------------
### Configure `stunnel`
First stop `stunnel` then open the config file and replace with below, save and start `stunnel` again
```
@ -151,8 +311,7 @@ connect = 192.168.1.124:1022
TIMEOUTclose = 0
```
 
Configure iptables for Pi A
--------------------------
### Configure iptables for Pi A
The `_add.sh` script creates the rules, the `_rm.sh` script removes the rules.
They will be lost if you reboot but there are ways to make them load again on start-up..
@ -194,8 +353,7 @@ Now run the "add" script on Pi A!
# piA_tproxy_rm.sh
```
Configure iptables for Pi B
--------------------------
## Configure iptables for Pi B
```
# nano /usr/local/sbin/piB_tproxy_add.sh
@ -235,8 +393,8 @@ Now run the "add" script on Pi B!
# piB_tproxy_rm.sh
```
 
Testing
-------
### Testing
* Getting to sshd on PiA
I did this test using 4G from my phone (outside the LAN)

12
echo_test.cfg Normal file
View File

@ -0,0 +1,12 @@
# TODO: c2s does not warn if udp: 1 (instead of 'true')
udp: true;
prefix: "hello";
listen: "localhost:9000";
listen-host: "localhost";
listen-port: "9000";

View File

@ -1,5 +1,5 @@
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
* on Fri Aug 13 18:03:20 2021.
* on Sat Apr 30 09:55:03 2022.
# conf2struct: generate libconf parsers that read to structs
# Copyright (C) 2018-2021 Yves Rutschle
@ -365,7 +365,7 @@ static int clcpy(config_type type, void* target, const void* cl_arg)
return 0;
}
/* Copy the value of a string argument to arbitary memory
/* Copy the value of a string argument to arbitrary memory
* location that must be large enough, converting on the way
* (i.e. CFG_INT gets atoi() and so on) */
/* 0: success
@ -862,7 +862,7 @@ static int set_target_fields(void* target_addr, struct compound_cl_arg* arg, con
if (pmatch[pmatch_cnt].rm_so == -1) {
/* This should not happen as regexec() did
* match before, unless there is a
* discrepency between the regex and the
* discrepancy between the regex and the
* number of backreferences */
return 0;
}
@ -1155,7 +1155,7 @@ static void scalar_to_string(char** strp, config_setting_t* s)
/* Typesets all the settings in a configuration as a
* newly-allocated string. The string management is caller's
* responsability.
* responsibility.
* Returns the number of scalars in the configuration */
static int cfg_as_string(config_setting_t* parent, const char* path, char** strp)
{

View File

@ -1,5 +1,5 @@
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
* on Fri Aug 13 18:03:20 2021.
* on Sat Apr 30 09:55:03 2022.
# conf2struct: generate libconf parsers that read to structs
# Copyright (C) 2018-2021 Yves Rutschle

View File

@ -1,6 +1,6 @@
/* echosrv: a simple line echo server with optional prefix adding.
*
* echsrv --listen localhost6:1234 --prefix "ssl: "
* echosrv --listen localhost6:1234 --prefix "ssl: "
*
* This will bind to 1234, and echo every line pre-pending "ssl: ". This is
* used for testing: we create several such servers with different prefixes,
@ -100,7 +100,10 @@ void tcp_echo(struct listen_endpoint* listen_socket)
{
while (1) {
int in_socket = accept(listen_socket->socketfd, 0, 0);
CHECK_RES_DIE(in_socket, "accept");
if (in_socket == -1) {
perror("tcp_echo:accept");
exit(1);
}
if (!fork())
{
@ -109,6 +112,7 @@ void tcp_echo(struct listen_endpoint* listen_socket)
exit(0);
}
close(in_socket);
waitpid(-1, NULL, WNOHANG);
}
}

109
echoѕrv-conf.h Normal file
View File

@ -0,0 +1,109 @@
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
* on Sat Nov 7 09:19:26 2020.
# conf2struct: generate libconf parsers that read to structs
# Copyright (C) 2018-2019 Yves Rutschle
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef C2S_SSLHCFG_H
#define C2S_SSLHCFG_H
#ifdef LIBCONFIG
# include <libconfig.h>
#endif
#include "probe.h"
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
struct sslhcfg_listen_item {
char* host;
char* port;
int keepalive;
};
struct sslhcfg_protocols_item {
char* name;
char* host;
char* port;
int service_is_present;
char* service;
int fork;
int tfo_ok;
int log_level;
int keepalive;
size_t sni_hostnames_len;
char** sni_hostnames;
size_t alpn_protocols_len;
char** alpn_protocols;
size_t regex_patterns_len;
char** regex_patterns;
int minlength_is_present;
int minlength;
T_PROBE* probe;
struct addrinfo* saddr;
void* data;
};
struct sslhcfg_item {
char* prefix;
int verbose;
int foreground;
int inetd;
int numeric;
int transparent;
int timeout;
int user_is_present;
char* user;
int pidfile_is_present;
char* pidfile;
int chroot_is_present;
char* chroot;
char* syslog_facility;
char* on_timeout;
size_t listen_len;
struct sslhcfg_listen_item* listen;
size_t protocols_len;
struct sslhcfg_protocols_item* protocols;
};
int sslhcfg_parse_file(
const char* filename,
struct sslhcfg_item* sslhcfg,
const char** errmsg);
void sslhcfg_fprint(
FILE* out,
struct sslhcfg_item *sslhcfg,
int depth);
int sslhcfg_cl_parse(
int argc,
char* argv[],
struct sslhcfg_item *sslhcfg);
#endif

View File

@ -3,7 +3,6 @@
# not be used as a starting point for a working
# configuration. Instead use basic.cfg.
verbose: 0;
foreground: true;
inetd: false;
numeric: false;
@ -13,6 +12,30 @@ user: "nobody";
pidfile: "/var/run/sslh.pid";
chroot: "/var/empty";
# Logging configuration
# Value: 1: stdout; 2: syslog; 3: stdout+syslog; 4: logfile; ...; 7: all
# Defaults are indicated here, and should be sensible. Generally, you want *-error
# to be always enabled, to know if something is going wrong.
verbose-config: 0; # print configuration at startup
verbose-config-error: 3; # print configuration errors
verbose-connections: 3; # trace established incoming address to forward address
verbose-connections-error: 3; # connection errors
verbose-connections-try: 0; # connection attempts towards targets
verbose-fd: 0; # file descriptor activity, open/close/whatnot
verbose-packets: 0; # hexdump packets on which probing is done
verbose-probe-info: 0; # what's happening during the probe process
verbose-probe-error: 3; # failures and problems during probing
verbose-system-error: 3; # system call problem, i.e. malloc, fork, failing
verbose-int-error: 3; # internal errors, the kind that should never happen
# Specify a path to the logfile.
#logfile: "/var/log/sslh.log"
# Specify the number of concurrent UDP connection that can
# be managed (default 1024)
udp_max_connections: 16;
# Specify which syslog facility to use (names for your
# system are usually defined in /usr/include/*/sys/syslog.h
# or equivalent)
@ -44,6 +67,8 @@ listen:
# fork: Should a new process be forked for this protocol?
# (only useful for sslh-select)
# tfo_ok: Set to true if the server supports TCP FAST OPEN
# resolve_on_forward: Set to true if server address should be resolved on
# (every) newly incoming connection (again)
# transparent: Set to true to proxy this protocol
# transparently (server sees the remote client IP
# address). Same as the global option, but per-protocol
@ -98,7 +123,7 @@ protocols:
udp_timeout: 20; # Time after which the "connection" is forgotten
regex_patterns: [ "hello" ]; },
# Forward Teamspeak3 (Voice only)
{ name: "regex"; host: "localhost"; is_udp: true; port: "9987"; regex_patterns: [ "TS3INIT1" ]; },
{ name: "teamspeak"; host: "localhost"; is_udp: true; port: "9987"; },
# Forward IETF QUIC-50 ("Q050" -> "\x51\x30\x35\x30")
# Remember that the regex needs to be adjusted for every supported QUIC version.
{ name: "regex"; host: "localhost"; is_udp: true; port: "4433"; regex_patterns: [ "\x51\x30\x35\x30" ]; },

35
gap.c
View File

@ -31,19 +31,14 @@
#include "gap.h"
typedef struct gap_array {
int len; /* Number of elements in array */
void** array;
} gap_array;
/* Allocate one page-worth of elements */
static int gap_len_alloc(int elem_size)
{
return getpagesize() / elem_size;
}
/* Creates a new gap, all pointers are initialised at NULL */
gap_array* gap_init(void)
/* Creates a new gap at least `len` big, all pointers are initialised at NULL */
gap_array* gap_init(int len)
{
gap_array* gap = malloc(sizeof(*gap));
if (!gap) return NULL;
@ -51,8 +46,12 @@ gap_array* gap_init(void)
int elem_size = sizeof(gap->array[0]);
gap->len = gap_len_alloc(elem_size);
if (gap->len < len) gap->len = len;
gap->array = malloc(gap->len * elem_size);
if (!gap->array) return NULL;
if (!gap->array) {
free(gap);
return NULL;
}
for (int i = 0; i < gap->len; i++)
gap->array[i] = NULL;
@ -60,12 +59,7 @@ gap_array* gap_init(void)
return gap;
}
void* gap_get(gap_array* gap, int index)
{
return gap->array[index];
}
static int gap_extend(gap_array* gap)
int gap_extend(gap_array* gap)
{
int elem_size = sizeof(gap->array[0]);
int new_length = gap->len + gap_len_alloc(elem_size);
@ -83,17 +77,6 @@ static int gap_extend(gap_array* gap)
return 0;
}
int gap_set(gap_array* gap, int index, void* ptr)
{
if (index >= gap->len) {
int res = gap_extend(gap);
if (res == -1) return -1;
}
gap->array[index] = ptr;
return 0;
}
void gap_destroy(gap_array* gap)
{
free(gap->array);
@ -119,7 +102,7 @@ int gap_remove_ptr(gap_array* gap, void* ptr, int len)
else
return -1;
for (i = start; i < len; i++) {
for (i = start; i < len - 1; i++) {
gap->array[i] = gap->array[i+1];
}

36
gap.h
View File

@ -3,11 +3,41 @@
typedef struct gap_array gap_array;
gap_array* gap_init();
void* gap_get(gap_array* gap, int index);
int gap_set(gap_array* gap, int index, void* ptr);
gap_array* gap_init(int len);
static void* gap_get(gap_array* gap, int index);
static int gap_set(gap_array* gap, int index, void* ptr);
void gap_destroy(gap_array* gap);
int gap_remove_ptr(gap_array* gap, void* ptr, int len);
/* Private declarations to allow inlining.
* Don't assume my implementation. */
typedef struct gap_array {
int len; /* Number of elements in array */
void** array;
} gap_array;
int gap_extend(gap_array* gap);
static inline int __attribute__((unused)) gap_set(gap_array* gap, int index, void* ptr)
{
while (index >= gap->len) {
int res = gap_extend(gap);
if (res == -1) return -1;
}
gap->array[index] = ptr;
return 0;
}
static inline void* __attribute__((unused)) gap_get(gap_array* gap, int index)
{
/* sslh-ev routinely reads before it writes. It's not clear if it should be
* its job to check the length (and add a gap_getlen()), or if it should be
* gap_get()'s job. This will do for now */
if (index >= gap->len) return NULL;
return gap->array[index];
}
#endif

View File

@ -10,14 +10,14 @@ fi
if [ ! -d .git ] || ! `(git status | grep -q "On branch") 2> /dev/null`; then
# If we don't have git, we can't work out what
# version this is. It must have been downloaded as a
# zip file.
# zip file.
# If downloaded from the release page, the directory
# has the version number.
release=`pwd | sed s/.*sslh-// | grep "[[:digit:]]"`
if [ "x$release" = "x" ]; then
# If downloaded from the head, Github creates the
# If downloaded from the head, GitHub creates the
# zip file with all files dated from the last
# change: use the Makefile's modification time as a
# release number
@ -28,7 +28,7 @@ fi
if [ -d .git ] && head=`git rev-parse --verify HEAD 2>/dev/null`; then
# generate the version info based on the tag
release=`(git describe --tags || git --describe || git describe --all --long) \
2>/dev/null | tr -d '\n'`
2>/dev/null | tr -s '/' '-' | tr -d '\n'`
# Are there uncommitted changes?
git update-index --refresh --unmerged > /dev/null

224
hash.c Normal file
View File

@ -0,0 +1,224 @@
/*
* a fixed-sized hash
*
# Copyright (C) 2022 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
#
# */
/* * The hash is open-addressing, linear search, robin-hood insertion, with
* backward shift deletion. References:
* https://codecapsule.com/2013/11/11/robin-hood-hashing/
* https://codecapsule.com/2013/11/17/robin-hood-hashing-backward-shift-deletion/
* This means items are reordered upon insertion and deletion, and the hash
* is well-ordered at all times with no tombstones.
*
* Each pointer is either:
* - to a connection struct
* - FREE (NULL) if not allocated
*
* */
#include <stdlib.h>
#include <stddef.h>
#include "gap.h"
typedef void* hash_item;
#include "hash.h"
static void* const FREE = NULL;
struct hash {
int hash_size; /* Max number of items in the hash */
int item_cnt; /* Number of items in the hash */
gap_array* data;
hash_make_key_fn hash_make_key;
hash_cmp_item_fn cmp_item;
};
typedef struct hash hash;
static int hash_make_key(hash* h, hash_item item)
{
return h->hash_make_key(item) % h->hash_size;
}
hash* hash_init(int hash_size, hash_make_key_fn make_key, hash_cmp_item_fn cmp_item)
{
hash* h = malloc(sizeof(*h));
if (!h) return NULL;
h->hash_size = hash_size;
h->item_cnt = 0;
h->data = gap_init(hash_size);
h->hash_make_key = make_key;
h->cmp_item = cmp_item;
return h;
}
/* Return the index following i in h */
static int hash_next_index(hash* h, int i)
{
return (i + 1) % h->hash_size;
}
/* Returns the index in h of specified address, -1 if not found
* item is an item object that must return the target wanted index and for
* which comparison with the searched object will succeed.
* */
static int hash_find_index(hash* h, hash_item item)
{
hash_item cnx;
int index = hash_make_key(h, item);
int cnt = 0;
cnx = gap_get(h->data, index);
#ifdef DEBUG
fprintf(stderr, "searching %d\n", index);
#endif
while (cnx != FREE) {
if (cnt++ > h->hash_size) return -1;
if (!h->cmp_item(cnx, item))
break;
index = hash_next_index(h, index);
cnx = gap_get(h->data, index);
#ifdef DEBUG
fprintf(stderr, "searching %d\n", index);
#endif
}
if (cnx == FREE) return -1;
return index;
}
hash_item hash_find(hash* h, hash_item item)
{
int index = hash_find_index(h, item);
if (index == -1) return NULL;
hash_item out = gap_get(h->data, index);
return out;
}
/* Returns DIB: distance to initial bucket */
static int distance(int current_index, hash* h, hash_item item)
{
int wanted_index = hash_make_key(h, item);
if (wanted_index <= current_index)
return current_index - wanted_index;
else
return current_index - wanted_index + h->hash_size;
}
int hash_insert(hash* h, hash_item new)
{
int bubble_wanted_index = hash_make_key(h, new);
int index = bubble_wanted_index;
gap_array* hash = h->data;
if (h->item_cnt == h->hash_size)
return -1;
hash_item curr_item = gap_get(hash, index);
while (curr_item) {
if (distance(index, h, curr_item) < distance(index, h, new)) {
gap_set(h->data, index, new);
#if DEBUG
fprintf(stderr, "intermediate insert [%s] at %d\n", &new->client_addr, index);
#endif
new = curr_item;
}
index = hash_next_index(h, index);
curr_item = gap_get(hash, index);
}
#if DEBUG
fprintf(stderr, "final insert at %d\n", index);
#endif
gap_set(hash, index, new);
h->item_cnt++;
return 0;
}
/* Remove cnx from the hash */
int hash_remove(hash* h, hash_item item)
{
gap_array* hash = h->data;
int index = hash_find_index(h, item);
if (index == -1) return -1; /* Tried to remove something that isn't there */
while (1) {
int next_index = hash_next_index(h, index);
hash_item next = gap_get(h->data, next_index);
if ((next == FREE) || (distance(next_index, h, next) == 0)) {
h->item_cnt--;
gap_set(hash, index, FREE);
return 0;
}
gap_set(hash, index, next);
index = hash_next_index(h, index);;
}
return 0;
}
#if HASH_TESTING
#include <stdio.h>
#include <string.h>
#define STR_LENGTH 16
struct hash_item {
int wanted_index;
char str[STR_LENGTH];
};
void hash_dump(hash* h, char* filename)
{
char str[STR_LENGTH];
FILE* out = fopen(filename, "w");
if (!out) {
perror(filename);
exit(1);
}
fprintf(out, "<hash elem=%d>\n", h->item_cnt);
for (int i = 0; i < h->hash_size; i++) {
hash_item item = gap_get(h->data, i);
int idx = 0;
memset(str, 0, STR_LENGTH);
if (item) {
idx = hash_make_key(h, item);
memcpy(str, ((struct hash_item*)item)->str, STR_LENGTH);
}
fprintf(out, "\t%d:%d:%s\n", i, idx, str);
}
fprintf(out, "</hash>\n");
fclose(out);
}
#endif

28
hash.h Normal file
View File

@ -0,0 +1,28 @@
#ifndef HASH_H
#define HASH_H
/* You will need to typedef a pointer type to hash_item before including this
* .h */
typedef struct hash hash;
/* Function that returns a key (index) for a given item. The key must be always
* the same for an item. It doesn't need to be bounded (hash.c masks it for you) */
typedef int (*hash_make_key_fn)(hash_item item);
/* Function that compares two items: returns 0 if they are the same */
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_insert(hash* h, hash_item new);
int hash_remove(hash* h, hash_item item);
/* Returns the hash item that matches specification (meaning the
* comparison function returns true for cmp(x, item), or NULL if not found */
hash_item hash_find(hash* h, hash_item item);
void hash_dump(hash* h, char* filename); /* For development only */
#endif

7
hashtest/Makefile Normal file
View File

@ -0,0 +1,7 @@
CFLAGS=-DHASH_TESTING -O2 -Wall
OBJ=../hash.o ../gap.o htest.o
htest: $(OBJ)
$(CC) -o htest $(OBJ)

8
hashtest/delete.tst Normal file
View File

@ -0,0 +1,8 @@
# Basic delete
a 10 aa
a 10 ab
a 10 ac
a 20 ba
a 21 bb
d 21 bb

34
hashtest/delete.tst.ref Normal file
View File

@ -0,0 +1,34 @@
<hash elem=4>
0:0:
1:0:
2:0:
3:0:
4:0:
5:0:
6:0:
7:0:
8:0:
9:0:
10:10:aa
11:10:ab
12:10:ac
13:0:
14:0:
15:0:
16:0:
17:0:
18:0:
19:0:
20:20:ba
21:0:
22:0:
23:0:
24:0:
25:0:
26:0:
27:0:
28:0:
29:0:
30:0:
31:0:
</hash>

View File

@ -0,0 +1,9 @@
# Delete inside a block with nothing after
a 10 aa
a 10 ab
a 12 ac
a 13 ad
a 14 ae
d 14 ae

View File

@ -0,0 +1,34 @@
<hash elem=4>
0:0:
1:0:
2:0:
3:0:
4:0:
5:0:
6:0:
7:0:
8:0:
9:0:
10:10:aa
11:10:ab
12:12:ac
13:13:ad
14:0:
15:0:
16:0:
17:0:
18:0:
19:0:
20:0:
21:0:
22:0:
23:0:
24:0:
25:0:
26:0:
27:0:
28:0:
29:0:
30:0:
31:0:
</hash>

View File

@ -0,0 +1,9 @@
# wrap-around and delete below floor
a 2 ba
a 30 aa
a 30 ab
a 30 ac
a 30 ad
a 2 bb
d 30 ab

View File

@ -0,0 +1,34 @@
<hash elem=5>
0:30:ad
1:0:
2:2:ba
3:2:bb
4:0:
5:0:
6:0:
7:0:
8:0:
9:0:
10:0:
11:0:
12:0:
13:0:
14:0:
15:0:
16:0:
17:0:
18:0:
19:0:
20:0:
21:0:
22:0:
23:0:
24:0:
25:0:
26:0:
27:0:
28:0:
29:0:
30:30:aa
31:30:ac
</hash>

View File

@ -0,0 +1,10 @@
# delete in a discontinuous block
a 10 aa
a 11 ab
a 12 ac
a 14 ad
a 10 bc
d 11 ab

View File

@ -0,0 +1,34 @@
<hash elem=4>
0:0:
1:0:
2:0:
3:0:
4:0:
5:0:
6:0:
7:0:
8:0:
9:0:
10:10:aa
11:10:bc
12:12:ac
13:0:
14:14:ad
15:0:
16:0:
17:0:
18:0:
19:0:
20:0:
21:0:
22:0:
23:0:
24:0:
25:0:
26:0:
27:0:
28:0:
29:0:
30:0:
31:0:
</hash>

11
hashtest/delete_empty.tst Normal file
View File

@ -0,0 +1,11 @@
# Delete an unexisting element. And on an empty hash
a 10 aa
d 10 ab
d 12 bc
# Empty for real
d 10 aa
d 10 aa

View File

@ -0,0 +1,34 @@
<hash elem=0>
0:0:
1:0:
2:0:
3:0:
4:0:
5:0:
6:0:
7:0:
8:0:
9:0:
10:0:
11:0:
12:0:
13:0:
14:0:
15:0:
16:0:
17:0:
18:0:
19:0:
20:0:
21:0:
22:0:
23:0:
24:0:
25:0:
26:0:
27:0:
28:0:
29:0:
30:0:
31:0:
</hash>

39
hashtest/delete_full.tst Normal file
View File

@ -0,0 +1,39 @@
# delete on a full hash
# First, fill the hash :-)
a 0 aa
a 1 ab
a 2 ac
a 3 ad
a 4 ae
a 5 af
a 6 ag
a 7 ah
a 8 ai
a 9 af
a 10 ba
a 11 bb
a 12 bc
a 13 bd
a 14 be
a 15 bf
a 16 bg
a 17 bh
a 18 bi
a 19 bj
a 20 ca
a 21 cb
a 22 cd
a 23 ce
a 24 cf
a 25 cg
a 26 ch
a 27 ci
a 28 cj
a 29 ck
a 30 da
a 31 db
d 21 cb

View File

@ -0,0 +1,34 @@
<hash elem=31>
0:0:aa
1:1:ab
2:2:ac
3:3:ad
4:4:ae
5:5:af
6:6:ag
7:7:ah
8:8:ai
9:9:af
10:10:ba
11:11:bb
12:12:bc
13:13:bd
14:14:be
15:15:bf
16:16:bg
17:17:bh
18:18:bi
19:19:bj
20:20:ca
21:0:
22:22:cd
23:23:ce
24:24:cf
25:25:cg
26:26:ch
27:27:ci
28:28:cj
29:29:ck
30:30:da
31:31:db
</hash>

View File

@ -0,0 +1,10 @@
# Delete inside a block with something discontinuous
a 10 aa
a 10 ab
a 12 ac
a 13 ad
a 14 ae
# ab shifts, ac and next doesn't
d 10 aa

View File

@ -0,0 +1,34 @@
<hash elem=4>
0:0:
1:0:
2:0:
3:0:
4:0:
5:0:
6:0:
7:0:
8:0:
9:0:
10:10:ab
11:0:
12:12:ac
13:13:ad
14:14:ae
15:0:
16:0:
17:0:
18:0:
19:0:
20:0:
21:0:
22:0:
23:0:
24:0:
25:0:
26:0:
27:0:
28:0:
29:0:
30:0:
31:0:
</hash>

8
hashtest/delete_wrap.tst Normal file
View File

@ -0,0 +1,8 @@
# Basic delete when wrapping, between wrap and floor
a 30 aa
a 30 ab
a 30 ac
a 30 ba
a 30 bb
d 30 ac

View File

@ -0,0 +1,34 @@
<hash elem=4>
0:30:ba
1:30:bb
2:0:
3:0:
4:0:
5:0:
6:0:
7:0:
8:0:
9:0:
10:0:
11:0:
12:0:
13:0:
14:0:
15:0:
16:0:
17:0:
18:0:
19:0:
20:0:
21:0:
22:0:
23:0:
24:0:
25:0:
26:0:
27:0:
28:0:
29:0:
30:30:aa
31:30:ab
</hash>

View File

@ -0,0 +1,10 @@
# Delete inside a block with wrapping, with something after
a 30 aa
a 30 ab
a 30 ac
a 1 ad
a 3 ae
# shift ad but not ae
d 14 ae

View File

@ -0,0 +1,34 @@
<hash elem=5>
0:30:ac
1:1:ad
2:0:
3:3:ae
4:0:
5:0:
6:0:
7:0:
8:0:
9:0:
10:0:
11:0:
12:0:
13:0:
14:0:
15:0:
16:0:
17:0:
18:0:
19:0:
20:0:
21:0:
22:0:
23:0:
24:0:
25:0:
26:0:
27:0:
28:0:
29:0:
30:30:aa
31:30:ab
</hash>

View File

@ -0,0 +1,8 @@
# delete before wrap
a 30 aa
a 30 ab
a 30 ac
a 30 ad
# shift ac and ad
d 30 ab

View File

@ -0,0 +1,34 @@
<hash elem=3>
0:30:ad
1:0:
2:0:
3:0:
4:0:
5:0:
6:0:
7:0:
8:0:
9:0:
10:0:
11:0:
12:0:
13:0:
14:0:
15:0:
16:0:
17:0:
18:0:
19:0:
20:0:
21:0:
22:0:
23:0:
24:0:
25:0:
26:0:
27:0:
28:0:
29:0:
30:30:aa
31:30:ac
</hash>

View File

@ -0,0 +1,11 @@
# Delete with wrapping in discontinuous group
a 30 aa
a 30 ab
a 30 ac
a 31 ad
a 2 ba
a 3 bb
# shift ac and ad but not ba and bb
d 30 ab

View File

@ -0,0 +1,34 @@
<hash elem=5>
0:31:ad
1:0:
2:2:ba
3:3:bb
4:0:
5:0:
6:0:
7:0:
8:0:
9:0:
10:0:
11:0:
12:0:
13:0:
14:0:
15:0:
16:0:
17:0:
18:0:
19:0:
20:0:
21:0:
22:0:
23:0:
24:0:
25:0:
26:0:
27:0:
28:0:
29:0:
30:30:aa
31:30:ac
</hash>

BIN
hashtest/htest Executable file

Binary file not shown.

109
hashtest/htest.c Normal file
View File

@ -0,0 +1,109 @@
/* Wee testing program from the hash code:
* htest <script> <dump>
*
* scripts are a list of operations:
* a $index $string
* => add an element at specified index
* d $index $string
* => remove an element
* s $index $string
* => prints the actual element index, if it's there
*
* The hash is dumped to the dump file at each iteration.
*/
#include <stdlib.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
/* tests have been written for a hash that holds 32 items */
#define HASH_SIZE 32
#define STR_LENGTH 16
struct hash_item {
int wanted_index;
char str[STR_LENGTH];
};
typedef struct hash_item* hash_item;
#include "../hash.h"
static int cmp_item(hash_item item1, hash_item item2)
{
return strcmp(item1->str, item2->str);
}
static int hash_make_key(hash_item item)
{
return item->wanted_index;
}
static void htest_next_key(FILE* f, char* action, int* key, char str[STR_LENGTH])
{
int res = 0;
while ((res != 3) && (res != EOF))
res = fscanf(f, "%c %d %s\n", action, key, str);
if (res == EOF) exit(0);
}
int main(int argc, char* argv[])
{
hash* h = hash_init(HASH_SIZE, &hash_make_key, &cmp_item);
char action;
hash_item item;
int line = 0;
FILE* f;
if (argc != 3) {
fprintf(stderr, "Usage: htest <script file> <dump file>\n");
exit(1);
}
char* script_file = argv[1];
char* dump_file = argv[2];
f = fopen(argv[1], "r");
if (!f) {
perror(script_file);
exit(1);
}
while (1) {
item= malloc(sizeof(*item));
action = ' ';
line++;
htest_next_key(f, &action, &item->wanted_index, item->str);
fprintf(stderr, "action %d: %c %d %s\n", line, action, item->wanted_index, item->str);
switch (action) {
case 'a': /* add */
fprintf(stderr, "inserting [%s] at %d\n", item->str, item->wanted_index);
hash_insert(h, item);
break;
case 'd': /* del */
fprintf(stderr, "removing [%s] at %d\n", item->str, item->wanted_index);
hash_remove(h, item);
break;
case 's': /* search */
fprintf(stderr, "searching\n");
struct hash_item* found = hash_find(h, item);
fprintf(stderr, "searching %d[%s]: %p\n", item->wanted_index, item->str, found);
break;
case 'q': /* quit */
exit(1);
}
hash_dump(h, dump_file);
}
return 0;
}

6
hashtest/insert.tst Normal file
View File

@ -0,0 +1,6 @@
# Basic insertions
a 10 aa
a 10 ab
a 10 ac
a 20 ba
a 21 bb

34
hashtest/insert.tst.ref Normal file
View File

@ -0,0 +1,34 @@
<hash elem=5>
0:0:
1:0:
2:0:
3:0:
4:0:
5:0:
6:0:
7:0:
8:0:
9:0:
10:10:aa
11:10:ab
12:10:ac
13:0:
14:0:
15:0:
16:0:
17:0:
18:0:
19:0:
20:20:ba
21:21:bb
22:0:
23:0:
24:0:
25:0:
26:0:
27:0:
28:0:
29:0:
30:0:
31:0:
</hash>

View File

@ -0,0 +1,8 @@
# insert and bubble with single empty space
a 10 aa
a 11 ab
a 12 ac
a 14 ad
a 10 bc

View File

@ -0,0 +1,34 @@
<hash elem=5>
0:0:
1:0:
2:0:
3:0:
4:0:
5:0:
6:0:
7:0:
8:0:
9:0:
10:10:aa
11:10:bc
12:11:ab
13:12:ac
14:14:ad
15:0:
16:0:
17:0:
18:0:
19:0:
20:0:
21:0:
22:0:
23:0:
24:0:
25:0:
26:0:
27:0:
28:0:
29:0:
30:0:
31:0:
</hash>

40
hashtest/insert_full.tst Normal file
View File

@ -0,0 +1,40 @@
# Insert on a full hash
# First, fill the hash :-)
a 0 aa
a 1 ab
a 2 ac
a 3 ad
a 4 ae
a 5 af
a 6 ag
a 7 ah
a 8 ai
a 9 af
a 10 ba
a 11 bb
a 12 bc
a 13 bd
a 14 be
a 15 bf
a 16 bg
a 17 bh
a 18 bi
a 19 bj
a 20 ca
a 21 cb
a 22 cd
a 23 ce
a 24 cf
a 25 cg
a 26 ch
a 27 ci
a 28 cj
a 29 ck
a 30 da
a 31 db
# it's full!
a 20 zz
a 31 za

View File

@ -0,0 +1,34 @@
<hash elem=32>
0:0:aa
1:1:ab
2:2:ac
3:3:ad
4:4:ae
5:5:af
6:6:ag
7:7:ah
8:8:ai
9:9:af
10:10:ba
11:11:bb
12:12:bc
13:13:bd
14:14:be
15:15:bf
16:16:bg
17:17:bh
18:18:bi
19:19:bj
20:20:ca
21:21:cb
22:22:cd
23:23:ce
24:24:cf
25:25:cg
26:26:ch
27:27:ci
28:28:cj
29:29:ck
30:30:da
31:31:db
</hash>

View File

@ -0,0 +1,7 @@
# wrap-around and insert at full floor
a 2 ba
a 30 aa
a 30 ab
a 30 ac
a 30 ad
a 2 bb

View File

@ -0,0 +1,34 @@
<hash elem=6>
0:30:ac
1:30:ad
2:2:ba
3:2:bb
4:0:
5:0:
6:0:
7:0:
8:0:
9:0:
10:0:
11:0:
12:0:
13:0:
14:0:
15:0:
16:0:
17:0:
18:0:
19:0:
20:0:
21:0:
22:0:
23:0:
24:0:
25:0:
26:0:
27:0:
28:0:
29:0:
30:30:aa
31:30:ab
</hash>

7
hashtest/insert_wrap.tst Normal file
View File

@ -0,0 +1,7 @@
# wrap-around and insert above floor
a 30 aa
a 30 ab
a 30 ac
a 30 ad
a 0 ba
a 0 bb

View File

@ -0,0 +1,34 @@
<hash elem=6>
0:30:ac
1:30:ad
2:0:ba
3:0:bb
4:0:
5:0:
6:0:
7:0:
8:0:
9:0:
10:0:
11:0:
12:0:
13:0:
14:0:
15:0:
16:0:
17:0:
18:0:
19:0:
20:0:
21:0:
22:0:
23:0:
24:0:
25:0:
26:0:
27:0:
28:0:
29:0:
30:30:aa
31:30:ab
</hash>

41
hashtest/mkrand.pl Executable file
View File

@ -0,0 +1,41 @@
#! /usr/bin/perl
# Creates a script of random accesses and deletes
use strict;
my $i = 0;
sub mkstr {
$i++;
return chr(ord('a') + ($i / 26) % 26) . chr(ord('a') + $i % 26);
}
my @elems;
sub add_elem {
my $val = int(rand(32));
my $str = mkstr($val);
push @elems, "$val $str";
print "a $val $str\n";
}
sub del_elem {
my $remove = splice(@elems, rand @elems, 1);
print "d $remove\n";
}
while (1) {
if (@elems < 5) {
add_elem;
} elsif (@elems > 28) {
del_elem;
} else {
if (rand() < .5) {
add_elem;
} else {
del_elem;
}
}
}

30
hashtest/run Executable file
View File

@ -0,0 +1,30 @@
#! /usr/bin/perl -w
# This runs all the tests.
# Tests scripts are in *.tst files.
# Corresponding output is put in *.out.
# Reference output is put in *.ref.
# Any discrepancy will be reported!
use strict;
my @res;
foreach my $fn (`ls *.tst`) {
chomp $fn;
my $cmd = "./htest $fn $fn.out";
print "$cmd\n";
`$cmd`;
my $res = system("diff -u $fn.ref $fn.out");
push @res, [$fn, ($res == 0 ? "OK" : "*KO*")];
}
format =
@<<<<<<<<<<<<<<<<<<<<<<<<<< @>>>
$_->[0], $_->[1]
.
#format_name STDOUT test_result;
map { write; } @res;

211
log.c Normal file
View File

@ -0,0 +1,211 @@
/*
# log: processing of all outgoing messages
#
# Copyright (C) 2007-2021 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 SYSLOG_NAMES
#define _GNU_SOURCE
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include "sslh-conf.h"
#include "common.h"
#include "log.h"
msg_info msg_config = {
LOG_INFO,
&cfg.verbose_config
};
msg_info msg_config_error = {
LOG_ERR,
&cfg.verbose_config_error
};
msg_info msg_fd = {
LOG_DEBUG,
&cfg.verbose_fd
};
/* Internal errors: inconsistent states, impossible values, things that should never happen, and are therefore the sign of memory corruption: hence the LOG_CRIT */
msg_info msg_int_error = {
LOG_CRIT,
&cfg.verbose_system_error
};
/* System errors: when the system around us fails us: memory allocation, fork, ... */
msg_info msg_system_error = {
LOG_ERR,
&cfg.verbose_system_error
};
msg_info msg_packets = {
LOG_INFO,
&cfg.verbose_packets
};
/* additional info when attempting outgoing connections */
msg_info msg_connections_try = {
LOG_DEBUG,
&cfg.verbose_connections_try
};
/* Connection information and failures (e.g. forbidden by policy) */
msg_info msg_connections = {
LOG_INFO,
&cfg.verbose_connections
};
/* Connection failures, e.g. target server not present */
msg_info msg_connections_error = {
LOG_ERR,
&cfg.verbose_connections_error
};
/* comment the probing process */
msg_info msg_probe_info = {
LOG_INFO,
&cfg.verbose_probe_info
};
/* probing errors, e.g. inconsistent data in connections */
msg_info msg_probe_error = {
LOG_ERR,
&cfg.verbose_probe_error
};
/* Bitmasks in verbose-* values */
#define MSG_STDOUT 1
#define MSG_SYSLOG 2
#define MSG_FILE 4
static FILE* logfile_fp = NULL;
/* Prints a message to stderr and/or syslog if appropriate */
void print_message(msg_info info, const char* str, ...)
{
va_list ap;
if ((*info.verbose & MSG_STDOUT) && ! cfg.inetd) {
va_start(ap, str);
vfprintf(stderr, str, ap);
va_end(ap);
}
if (*info.verbose & MSG_SYSLOG) {
va_start(ap, str);
vsyslog(info.log_level, str, ap);
va_end(ap);
}
if (*info.verbose & MSG_FILE && logfile_fp != NULL) {
va_start(ap, str);
vfprintf(logfile_fp, str, ap);
fflush(logfile_fp);
va_end(ap);
}
}
static int do_syslog = 1; /* Should we syslog? controled by syslog_facility = "none" */
/* Open syslog connection with appropriate banner;
* banner is made up of basename(bin_name)+"[pid]" */
void setup_syslog(const char* bin_name) {
char *name1, *name2;
int res, fn;
if (!strcmp(cfg.syslog_facility, "none")) {
do_syslog = 0;
return;
}
name1 = strdup(bin_name);
res = asprintf(&name2, "%s[%d]", basename(name1), getpid());
CHECK_RES_DIE(res, "asprintf");
for (fn = 0; facilitynames[fn].c_val != -1; fn++)
if (strcmp(facilitynames[fn].c_name, cfg.syslog_facility) == 0)
break;
if (facilitynames[fn].c_val == -1) {
fprintf(stderr, "Unknown facility %s\n", cfg.syslog_facility);
exit(1);
}
openlog(name2, LOG_CONS, facilitynames[fn].c_val);
free(name1);
/* Don't free name2, as openlog(3) uses it (at least in glibc) */
}
void setup_logfile()
{
if (cfg.logfile == NULL)
{
return;
}
logfile_fp = fopen(cfg.logfile, "a");
if (logfile_fp == NULL)
{
fprintf(stderr, "Could not open logfile %s for writing: %s\n", cfg.logfile, strerror(errno));
exit(1);
}
}
void close_logfile()
{
if (logfile_fp != NULL)
{
fclose(logfile_fp);
logfile_fp = NULL;
}
}
/* syslogs who connected to where
* desc: string description of the connection. if NULL, log_connection will
* manage on its own
* cnx: connection descriptor
* */
void log_connection(struct connection_desc* desc, const struct connection *cnx)
{
struct connection_desc d;
if (cnx->proto->log_level < 1)
return;
if (!desc) {
desc = &d;
if (!get_connection_desc(desc, cnx)) {
print_message(msg_connections, "%s: lost incoming connection\n",
cnx->proto->name);
return;
}
}
print_message(msg_connections, "%s:connection from %s to %s forwarded from %s to %s\n",
cnx->proto->name,
desc->peer,
desc->service,
desc->local,
desc->target);
}

36
log.h Normal file
View File

@ -0,0 +1,36 @@
#ifndef LOG_H
#define LOG_H
#include "common.h"
void setup_syslog(const char* bin_name);
void setup_logfile();
void close_logfile();
void log_connection(struct connection_desc* desc, const struct connection *cnx);
typedef struct s_msg_info{
int log_level;
int *verbose;
} msg_info;
void print_message(msg_info info, const char* str, ...);
extern msg_info msg_config;
extern msg_info msg_config_error;
extern msg_info msg_fd;
extern msg_info msg_packets;
extern msg_info msg_int_error;
extern msg_info msg_system_error;
extern msg_info msg_connections_try;
extern msg_info msg_connections_error;
extern msg_info msg_connections;
extern msg_info msg_probe_info;
extern msg_info msg_probe_error;
#endif /* LOG_H */

180
probe.c
View File

@ -1,7 +1,7 @@
/*
# probe.c: Code for probing protocols
#
# Copyright (C) 2007-2019 Yves Rutschle
# Copyright (C) 2007-2021 Yves Rutschle
#
# This program is free software; you can redistribute it
# and/or modify it under the terms of the GNU General Public
@ -27,11 +27,13 @@
#endif
#include <ctype.h>
#include "probe.h"
#include "log.h"
static int is_ssh_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item*);
static int is_openvpn_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item*);
static int is_wireguard_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item*);
static int is_tinc_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item*);
static int is_xmpp_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item*);
static int is_http_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item*);
@ -39,6 +41,8 @@ static int is_tls_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_
static int is_adb_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item*);
static int is_socks5_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item*);
static int is_syslog_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item*);
static int is_teamspeak_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item*);
static int is_msrdp_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item*);
static int is_true(const char *p, ssize_t len, struct sslhcfg_protocols_item* proto) { return 1; }
/* Table of protocols that have a built-in probe
@ -47,6 +51,7 @@ static struct protocol_probe_desc builtins[] = {
/* description probe */
{ "ssh", is_ssh_protocol},
{ "openvpn", is_openvpn_protocol },
{ "wireguard", is_wireguard_protocol },
{ "tinc", is_tinc_protocol },
{ "xmpp", is_xmpp_protocol },
{ "http", is_http_protocol },
@ -54,6 +59,8 @@ static struct protocol_probe_desc builtins[] = {
{ "adb", is_adb_protocol },
{ "socks5", is_socks5_protocol },
{ "syslog", is_syslog_protocol },
{ "teamspeak", is_teamspeak_protocol },
{ "msrdp", is_msrdp_protocol },
{ "anyprot", is_true }
};
@ -81,33 +88,38 @@ struct sslhcfg_protocols_item* timeout_protocol(void)
/* From http://grapsus.net/blog/post/Hexadecimal-dump-in-C */
#define HEXDUMP_COLS 16
void hexdump(const char *mem, unsigned int len)
void hexdump(msg_info msg_info, const char *mem, unsigned int len)
{
unsigned int i, j;
char str[10 + HEXDUMP_COLS * 4 + 2];
int c = 0; /* index in str */
for(i = 0; i < len + ((len % HEXDUMP_COLS) ? (HEXDUMP_COLS - len % HEXDUMP_COLS) : 0); i++)
{
/* print offset */
if(i % HEXDUMP_COLS == 0)
fprintf(stderr, "0x%06x: ", i);
c += sprintf(&str[c], "0x%06x: ", i);
/* print hex data */
if(i < len)
fprintf(stderr, "%02x ", 0xFF & mem[i]);
c += sprintf(&str[c], "%02x ", 0xFF & mem[i]);
else /* end of block, just aligning for ASCII dump */
fprintf(stderr, " ");
c+= sprintf(&str[c], " ");
/* print ASCII dump */
if(i % HEXDUMP_COLS == (HEXDUMP_COLS - 1)) {
for(j = i - (HEXDUMP_COLS - 1); j <= i; j++) {
if(j >= len) /* end of block, not really printing */
fputc(' ', stderr);
str[c++] = ' ';
else if(isprint(mem[j])) /* printable char */
fputc(0xFF & mem[j], stderr);
str[c++] = 0xFF & mem[j];
else /* other char */
fputc('.', stderr);
str[c++] = '.';
}
fputc('\n', stderr);
str[c++] = '\n';
str[c++] = 0;
print_message(msg_info, "%s", str);
c = 0;
}
}
}
@ -131,15 +143,66 @@ static int is_ssh_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_
* http://www.fengnet.com/book/vpns%20illustrated%20tunnels%20%20vpnsand%20ipsec/ch08lev1sec5.html
* and OpenVPN ssl.c, ssl.h and options.c
*/
#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_HMAC_128 16
#define OVPN_HMAC_160 20
#define OVPN_HARD_RESET_PACKET_ID_OFFSET(hmac_size) (9 + hmac_size)
static int is_openvpn_protocol (const char*p,ssize_t len, struct sslhcfg_protocols_item* proto)
{
int packet_len;
if (len < 2)
return PROBE_AGAIN;
if (proto->is_udp == 0)
{
if (len < 2)
return PROBE_AGAIN;
packet_len = ntohs(*(uint16_t*)p);
return packet_len == len - 2;
packet_len = ntohs(*(uint16_t*)p);
return packet_len == len - 2;
} else {
if (len < 1)
return PROBE_NEXT;
if ((p[0] & OVPN_OPCODE_MASK) != OVPN_CONTROL_HARD_RESET_CLIENT_V1 &&
(p[0] & OVPN_OPCODE_MASK) != OVPN_CONTROL_HARD_RESET_CLIENT_V2)
return PROBE_NEXT;
/* The detection pattern above may not be reliable enough.
* Check the packet id: OpenVPN sents five initial packets
* whereas the packet id is increased with every transmitted datagram.
*/
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)
return PROBE_MATCH;
if (len <= OVPN_HARD_RESET_PACKET_ID_OFFSET(OVPN_HMAC_160) + sizeof(uint32_t))
return PROBE_NEXT;
if (ntohl(*(uint32_t*)(p + OVPN_HARD_RESET_PACKET_ID_OFFSET(OVPN_HMAC_160))) <= 5u)
return PROBE_MATCH;
return PROBE_NEXT;
}
}
static int is_wireguard_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item* proto)
{
if (proto->is_udp == 0)
return PROBE_NEXT;
// Handshake Init: 148 bytes
if (len != 148)
return PROBE_NEXT;
// Handshake Init: p[0] = 0x01, p[1..3] = 0x000000 (reserved)
if (ntohl(*(uint32_t*)p) != 0x01000000)
return PROBE_NEXT;
return PROBE_MATCH;
}
/* Is the buffer the beginning of a tinc connections?
@ -312,6 +375,27 @@ static int is_syslog_protocol(const char *p, ssize_t len, struct sslhcfg_protoco
return 0;
}
static int is_teamspeak_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item* proto)
{
if (len < 8)
return PROBE_NEXT;
return !strncmp(p, "TS3INIT1", len);
}
static int is_msrdp_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item* proto)
{
char version;
char packet_len;
if (len < 7)
return PROBE_NEXT;
version=*p;
if (version!=0x03)
return 0;
packet_len = ntohs(*(uint16_t*)(p+2));
return packet_len == len;
}
static int regex_probe(const char *p, ssize_t len, struct sslhcfg_protocols_item* proto)
{
#ifdef ENABLE_REGEX
@ -328,51 +412,58 @@ static int regex_probe(const char *p, ssize_t len, struct sslhcfg_protocols_item
return 0;
#else
/* Should never happen as we check when loading config file */
fprintf(stderr, "FATAL: regex probe called but not built in\n");
print_message(msg_int_error, "FATAL: regex probe called but not built in\n");
exit(5);
#endif
}
/* Run all the probes on a buffer
* buf, len: buffer to test on
* proto_in, proto_len: array of protocols to try
* proto_out: protocol that matched
*
* Returns
* PROBE_AGAIN if not enough data, and set *proto to NULL
* PROBE_MATCH if protocol is identified, in which case *proto is set to
* point to the appropriate protocol
* */
int probe_buffer(char* buf, int len, struct sslhcfg_protocols_item** proto)
int probe_buffer(char* buf, int len,
struct sslhcfg_protocols_item** proto_in,
int proto_len,
struct sslhcfg_protocols_item** proto_out
)
{
struct sslhcfg_protocols_item* p;
int i, res, again = 0;
if (cfg.verbose > 1) {
fprintf(stderr, "hexdump of incoming packet:\n");
hexdump(buf, len);
}
print_message(msg_packets, "hexdump of incoming packet:\n");
hexdump(msg_packets, buf, len);
*proto = NULL;
for (i = 0; i < cfg.protocols_len; i++) {
*proto_out = NULL;
for (i = 0; i < proto_len; i++) {
char* probe_str[3] = {"PROBE_NEXT", "PROBE_MATCH", "PROBE_AGAIN"};
p = &cfg.protocols[i];
p = proto_in[i];
if (! p->probe) continue;
if (cfg.verbose) fprintf(stderr, "probing for %s\n", p->name);
print_message(msg_probe_info, "probing for %s\n", p->name);
/* Don't probe last protocol if it is anyprot (and store last protocol) */
if ((i == cfg.protocols_len - 1) && (!strcmp(p->name, "anyprot")))
if ((i == proto_len - 1) && (!strcmp(p->name, "anyprot")))
break;
if (p->minlength_is_present && (len < p->minlength )) {
fprintf(stderr, "input too short, %d bytes but need %d\n", len , p->minlength);
print_message(msg_probe_info, "input too short, %d bytes but need %d\n",
len , p->minlength);
again++;
continue;
}
res = p->probe(buf, len, p);
if (cfg.verbose) fprintf(stderr, "probed for %s: %s\n", p->name, probe_str[res]);
print_message(msg_probe_info, "probed for %s: %s\n", p->name, probe_str[res]);
if (res == PROBE_MATCH) {
*proto = p;
*proto_out = p;
return PROBE_MATCH;
}
if (res == PROBE_AGAIN)
@ -382,37 +473,14 @@ int probe_buffer(char* buf, int len, struct sslhcfg_protocols_item** proto)
return PROBE_AGAIN;
/* Everything failed: match the last one */
*proto = &cfg.protocols[cfg.protocols_len-1];
return PROBE_MATCH;
}
/*
* Read the beginning of data coming from the client connection and check if
* it's a known protocol.
* Return PROBE_AGAIN if not enough data, or PROBE_MATCH if it succeeded in
* which case cnx->proto is set to the appropriate protocol.
*/
int probe_client_protocol(struct connection *cnx)
{
char buffer[BUFSIZ];
ssize_t n;
n = read(cnx->q[0].fd, buffer, sizeof(buffer));
/* It's possible that read() returns an error, e.g. if the client
* disconnected between the previous call to select() and now. If that
* happens, we just connect to the default protocol so the caller of this
* function does not have to deal with a specific failure condition (the
* connection will just fail later normally). */
if (n > 0) {
defer_write(&cnx->q[1], buffer, n);
return probe_buffer(cnx->q[1].begin_deferred_data,
cnx->q[1].deferred_data_size,
&cnx->proto);
if (proto_len == 0) {
/* This should be caught by configuration sanity checks, but just in
* case, die gracefully rather than segfaulting */
print_message(msg_int_error, "Received traffic on transport that has no target\n");
exit(0);
}
/* read() returned an error, so just connect to the last protocol to die */
cnx->proto = &cfg.protocols[cfg.protocols_len-1];
*proto_out = proto_in[proto_len-1];
return PROBE_MATCH;
}

11
probe.h
View File

@ -5,6 +5,7 @@
#include "common.h"
#include "tls.h"
#include "log.h"
typedef enum {
PROBE_NEXT, /* Enough data, probe failed -- it's some other protocol */
@ -47,8 +48,12 @@ void set_protocol_list(struct sslhcfg_protocols_item*);
*/
int probe_client_protocol(struct connection *cnx);
/* Probe, but on a buffer */
int probe_buffer(char* buf, int len, struct sslhcfg_protocols_item** proto);
/* Probe on a buffer */
int probe_buffer(char* buf, int len,
struct sslhcfg_protocols_item** proto_in,
int proto_len,
struct sslhcfg_protocols_item** proto_out
);
/* set the protocol to connect to in case of timeout */
void set_ontimeout(const char* name);
@ -59,6 +64,6 @@ void set_ontimeout(const char* name);
*/
struct sslhcfg_protocols_item* timeout_protocol(void);
void hexdump(const char*, unsigned int);
void hexdump(msg_info, const char*, unsigned int);
#endif

115
processes.c Normal file
View File

@ -0,0 +1,115 @@
/*
Processes that are common to sslh-ev and sslh-select
# Copyright (C) 2021 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
*/
#include "udp-listener.h"
#include "tcp-listener.h"
#include "processes.h"
#include "probe.h"
#include "log.h"
int tidy_connection(struct connection *cnx, struct loop_info* fd_info)
{
int i;
for (i = 0; i < 2; i++) {
if (cnx->q[i].fd != -1) {
print_message(msg_fd, "closing fd %d\n", cnx->q[i].fd);
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->type == SOCK_DGRAM)
udp_tidy(cnx, fd_info);
if (gap_remove_ptr(fd_info->probing_list, cnx, fd_info->num_probing) != -1)
fd_info->num_probing--;
collection_remove_cnx(fd_info->collection, cnx);
return 0;
}
/* Process a connection that is active in read */
void cnx_read_process(struct loop_info* fd_info, int fd)
{
cnx_collection* collection = fd_info->collection;
struct connection* cnx = collection_get_cnx_from_fd(collection, fd);
switch (cnx->type) {
case SOCK_STREAM:
tcp_read_process(fd_info, fd);
break;
case SOCK_DGRAM:
udp_s2c_forward(cnx);
break;
default:
print_message(msg_int_error, "cnx_read_process: Illegal connection type %d\n", cnx->type);
dump_connection(cnx);
exit(1);
}
}
/* Process a connection that accepts a socket
* (For UDP, this means all traffic coming from remote clients)
* Returns new connection object, or NULL
* */
struct connection* cnx_accept_process(struct loop_info* fd_info, struct listen_endpoint* listen_socket)
{
int fd = listen_socket->socketfd;
int type = listen_socket->type;
struct connection* cnx;
switch (type) {
case SOCK_STREAM:
cnx = accept_new_connection(fd, fd_info);
if (!cnx) return NULL;
break;
case SOCK_DGRAM:
cnx = udp_c2s_forward(fd, fd_info);
if (!cnx) return NULL;
break;
default:
print_message(msg_int_error, "Inconsistent cnx type: %d\n", type);
exit(1);
}
int new_fd = cnx->q[0].fd;
watchers_add_read(fd_info->watchers, new_fd);
return cnx;
}

41
processes.h Normal file
View File

@ -0,0 +1,41 @@
#ifndef PROCESSES_H
#define PROCESSES_H
#include "common.h"
#include "collection.h"
#include "gap.h"
typedef struct connection* hash_item;
#include "hash.h"
/* Provided by event loop, sslh-ev or sslh-select, for implementation-dependant
* data */
typedef struct watchers watchers;
/* Global state for a loop */
struct loop_info {
int num_probing; /* Number of connections currently probing
* We use this to know if we need to time out of
* select() */
gap_array* probing_list; /* Pointers to cnx that are in probing mode */
hash* hash_sources; /* UDP remote sources previously encountered */
watchers* watchers;
cnx_collection* collection; /* Collection of connections linked to this loop */
};
void cnx_read_process(struct loop_info* fd_info, int fd);
struct connection* cnx_accept_process(struct loop_info* fd_info, struct listen_endpoint* listen_socket);
int tidy_connection(struct connection *cnx, struct loop_info* fd_info);
/* These must be declared in the loop handler, sslh-ev or sslh-select */
void watchers_add_read(watchers* w, int fd);
void watchers_del_read(watchers* w, int fd);
void watchers_add_write(watchers* w, int fd);
void watchers_del_write(watchers* w, int fd);
#endif

View File

@ -18,7 +18,7 @@
#CONFIG=/etc/sslh.cfg
#
# Extra option to pass on comand line
# Extra option to pass on command line
# Those can supersede configuration file settings
#
#OPTIONS=

View File

@ -8,7 +8,7 @@
# but many connection attempts from the same
# origin is reason enough to block.
#
# Verion: 2014-03-28
# Version: 2014-03-28
[INCLUDES]
@ -16,7 +16,7 @@
[Definition]
failregex = ^.+ sslh\[.+\]: connection from <HOST>:.+ to .+ forwarded
failregex = ^.+ sslh\[.+\]: ssh:connection from <HOST>:.+ to .+ forwarded
from .+ to .+:ssh\s*$
ignoreregex =

View File

@ -1,5 +1,5 @@
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
* on Fri Aug 13 18:03:18 2021.
* on Sun Sep 11 21:43:25 2022.
# conf2struct: generate libconf parsers that read to structs
# Copyright (C) 2018-2021 Yves Rutschle
@ -365,7 +365,7 @@ static int clcpy(config_type type, void* target, const void* cl_arg)
return 0;
}
/* Copy the value of a string argument to arbitary memory
/* Copy the value of a string argument to arbitrary memory
* location that must be large enough, converting on the way
* (i.e. CFG_INT gets atoi() and so on) */
/* 0: success
@ -443,16 +443,29 @@ struct compound_cl_arg {
struct arg_file* sslhcfg_conffile;
struct arg_int* sslhcfg_verbose;
struct arg_int* sslhcfg_verbose_config;
struct arg_int* sslhcfg_verbose_config_error;
struct arg_int* sslhcfg_verbose_connections;
struct arg_int* sslhcfg_verbose_connections_try;
struct arg_int* sslhcfg_verbose_connections_error;
struct arg_int* sslhcfg_verbose_fd;
struct arg_int* sslhcfg_verbose_packets;
struct arg_int* sslhcfg_verbose_probe_info;
struct arg_int* sslhcfg_verbose_probe_error;
struct arg_int* sslhcfg_verbose_system_error;
struct arg_int* sslhcfg_verbose_int_error;
struct arg_lit* sslhcfg_version;
struct arg_lit* sslhcfg_foreground;
struct arg_lit* sslhcfg_inetd;
struct arg_lit* sslhcfg_numeric;
struct arg_lit* sslhcfg_transparent;
struct arg_int* sslhcfg_timeout;
struct arg_int* sslhcfg_udp_max_connections;
struct arg_str* sslhcfg_user;
struct arg_str* sslhcfg_pidfile;
struct arg_str* sslhcfg_chroot;
struct arg_str* sslhcfg_syslog_facility;
struct arg_str* sslhcfg_logfile;
struct arg_str* sslhcfg_on_timeout;
struct arg_str* sslhcfg_prefix;
struct arg_str* sslhcfg_listen;
@ -460,15 +473,17 @@ struct arg_file* sslhcfg_conffile;
struct arg_str* sslhcfg_tls;
struct arg_str* sslhcfg_openvpn;
struct arg_str* sslhcfg_tinc;
struct arg_str* sslhcfg_wireguard;
struct arg_str* sslhcfg_xmpp;
struct arg_str* sslhcfg_http;
struct arg_str* sslhcfg_adb;
struct arg_str* sslhcfg_socks5;
struct arg_str* sslhcfg_syslog;
struct arg_str* sslhcfg_msrdp;
struct arg_str* sslhcfg_anyprot;
struct arg_end* sslhcfg_end;
static struct config_desc table_sslhcfg_protocols[] = {
@ -616,6 +631,22 @@ static struct config_desc table_sslhcfg_protocols[] = {
/* default_val*/ .default_val.def_bool = 0
},
{
/* name */ "resolve_on_forward",
/* type */ CFG_BOOL,
/* sub_group*/ NULL,
/* arg_cl */ NULL,
/* base_addr */ NULL,
/* offset */ offsetof(struct sslhcfg_protocols_item, resolve_on_forward),
/* offset_len */ 0,
/* offset_present */ 0,
/* size */ sizeof(int),
/* array_type */ -1,
/* mandatory */ 0,
/* optional */ 0,
/* default_val*/ .default_val.def_bool = 0
},
{
/* name */ "log_level",
/* type */ CFG_INT,
@ -782,17 +813,17 @@ static struct config_desc table_sslhcfg_listen[] = {
},
{ 0 }
};
static struct config_desc table_sslhcfg[] = {
{
/* name */ "verbose",
/* name */ "verbose_config",
/* type */ CFG_INT,
/* sub_group*/ NULL,
/* arg_cl */ & sslhcfg_verbose,
/* arg_cl */ & sslhcfg_verbose_config,
/* base_addr */ NULL,
/* offset */ offsetof(struct sslhcfg_item, verbose),
/* offset */ offsetof(struct sslhcfg_item, verbose_config),
/* offset_len */ 0,
/* offset_present */ 0,
/* size */ sizeof(int),
@ -802,6 +833,182 @@ static struct config_desc table_sslhcfg[] = {
/* default_val*/ .default_val.def_int = 0
},
{
/* name */ "verbose_config_error",
/* type */ CFG_INT,
/* sub_group*/ NULL,
/* arg_cl */ & sslhcfg_verbose_config_error,
/* base_addr */ NULL,
/* offset */ offsetof(struct sslhcfg_item, verbose_config_error),
/* offset_len */ 0,
/* offset_present */ 0,
/* size */ sizeof(int),
/* array_type */ -1,
/* mandatory */ 0,
/* optional */ 0,
/* default_val*/ .default_val.def_int = 3
},
{
/* name */ "verbose_connections",
/* type */ CFG_INT,
/* sub_group*/ NULL,
/* arg_cl */ & sslhcfg_verbose_connections,
/* base_addr */ NULL,
/* offset */ offsetof(struct sslhcfg_item, verbose_connections),
/* offset_len */ 0,
/* offset_present */ 0,
/* size */ sizeof(int),
/* array_type */ -1,
/* mandatory */ 0,
/* optional */ 0,
/* default_val*/ .default_val.def_int = 3
},
{
/* name */ "verbose_connections_try",
/* type */ CFG_INT,
/* sub_group*/ NULL,
/* arg_cl */ & sslhcfg_verbose_connections_try,
/* base_addr */ NULL,
/* offset */ offsetof(struct sslhcfg_item, verbose_connections_try),
/* offset_len */ 0,
/* offset_present */ 0,
/* size */ sizeof(int),
/* array_type */ -1,
/* mandatory */ 0,
/* optional */ 0,
/* default_val*/ .default_val.def_int = 0
},
{
/* name */ "verbose_connections_error",
/* type */ CFG_INT,
/* sub_group*/ NULL,
/* arg_cl */ & sslhcfg_verbose_connections_error,
/* base_addr */ NULL,
/* offset */ offsetof(struct sslhcfg_item, verbose_connections_error),
/* offset_len */ 0,
/* offset_present */ 0,
/* size */ sizeof(int),
/* array_type */ -1,
/* mandatory */ 0,
/* optional */ 0,
/* default_val*/ .default_val.def_int = 3
},
{
/* name */ "verbose_fd",
/* type */ CFG_INT,
/* sub_group*/ NULL,
/* arg_cl */ & sslhcfg_verbose_fd,
/* base_addr */ NULL,
/* offset */ offsetof(struct sslhcfg_item, verbose_fd),
/* offset_len */ 0,
/* offset_present */ 0,
/* size */ sizeof(int),
/* array_type */ -1,
/* mandatory */ 0,
/* optional */ 0,
/* default_val*/ .default_val.def_int = 0
},
{
/* name */ "verbose_packets",
/* type */ CFG_INT,
/* sub_group*/ NULL,
/* arg_cl */ & sslhcfg_verbose_packets,
/* base_addr */ NULL,
/* offset */ offsetof(struct sslhcfg_item, verbose_packets),
/* offset_len */ 0,
/* offset_present */ 0,
/* size */ sizeof(int),
/* array_type */ -1,
/* mandatory */ 0,
/* optional */ 0,
/* default_val*/ .default_val.def_int = 0
},
{
/* name */ "verbose_probe_info",
/* type */ CFG_INT,
/* sub_group*/ NULL,
/* arg_cl */ & sslhcfg_verbose_probe_info,
/* base_addr */ NULL,
/* offset */ offsetof(struct sslhcfg_item, verbose_probe_info),
/* offset_len */ 0,
/* offset_present */ 0,
/* size */ sizeof(int),
/* array_type */ -1,
/* mandatory */ 0,
/* optional */ 0,
/* default_val*/ .default_val.def_int = 0
},
{
/* name */ "verbose_probe_error",
/* type */ CFG_INT,
/* sub_group*/ NULL,
/* arg_cl */ & sslhcfg_verbose_probe_error,
/* base_addr */ NULL,
/* offset */ offsetof(struct sslhcfg_item, verbose_probe_error),
/* offset_len */ 0,
/* offset_present */ 0,
/* size */ sizeof(int),
/* array_type */ -1,
/* mandatory */ 0,
/* optional */ 0,
/* default_val*/ .default_val.def_int = 3
},
{
/* name */ "verbose_system_error",
/* type */ CFG_INT,
/* sub_group*/ NULL,
/* arg_cl */ & sslhcfg_verbose_system_error,
/* base_addr */ NULL,
/* offset */ offsetof(struct sslhcfg_item, verbose_system_error),
/* offset_len */ 0,
/* offset_present */ 0,
/* size */ sizeof(int),
/* array_type */ -1,
/* mandatory */ 0,
/* optional */ 0,
/* default_val*/ .default_val.def_int = 3
},
{
/* name */ "verbose_int_error",
/* type */ CFG_INT,
/* sub_group*/ NULL,
/* arg_cl */ & sslhcfg_verbose_int_error,
/* base_addr */ NULL,
/* offset */ offsetof(struct sslhcfg_item, verbose_int_error),
/* offset_len */ 0,
/* offset_present */ 0,
/* size */ sizeof(int),
/* array_type */ -1,
/* mandatory */ 0,
/* optional */ 0,
/* default_val*/ .default_val.def_int = 3
},
{
/* name */ "version",
/* type */ CFG_BOOL,
/* sub_group*/ NULL,
/* arg_cl */ & sslhcfg_version,
/* base_addr */ NULL,
/* offset */ offsetof(struct sslhcfg_item, version),
/* offset_len */ 0,
/* offset_present */ 0,
/* size */ sizeof(int),
/* array_type */ -1,
/* mandatory */ 0,
/* optional */ 0,
/* default_val*/ .default_val.def_bool = 0
},
{
/* name */ "foreground",
/* type */ CFG_BOOL,
@ -882,6 +1089,22 @@ static struct config_desc table_sslhcfg[] = {
/* default_val*/ .default_val.def_int = 5
},
{
/* name */ "udp_max_connections",
/* type */ CFG_INT,
/* sub_group*/ NULL,
/* arg_cl */ & sslhcfg_udp_max_connections,
/* base_addr */ NULL,
/* offset */ offsetof(struct sslhcfg_item, udp_max_connections),
/* offset_len */ 0,
/* offset_present */ 0,
/* size */ sizeof(int),
/* array_type */ -1,
/* mandatory */ 0,
/* optional */ 0,
/* default_val*/ .default_val.def_int = 1024
},
{
/* name */ "user",
/* type */ CFG_STRING,
@ -946,6 +1169,22 @@ static struct config_desc table_sslhcfg[] = {
/* default_val*/ .default_val.def_string = "auth"
},
{
/* name */ "logfile",
/* type */ CFG_STRING,
/* sub_group*/ NULL,
/* arg_cl */ & sslhcfg_logfile,
/* base_addr */ NULL,
/* offset */ offsetof(struct sslhcfg_item, logfile),
/* offset_len */ 0,
/* offset_present */ offsetof(struct sslhcfg_item, logfile_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,
@ -1015,7 +1254,15 @@ static struct compound_cl_target sslhcfg_anyprot_targets [] = {
{ & table_sslhcfg_protocols[0], 0, .value.def_string = "anyprot" },
{ & table_sslhcfg_protocols[1], 1, .value.def_string = "0" },
{ & table_sslhcfg_protocols[2], 2, .value.def_string = "0" },
{ & table_sslhcfg_protocols[9], 0, .value.def_int = 1 },
{ & table_sslhcfg_protocols[10], 0, .value.def_int = 1 },
{ 0 }
};
static struct compound_cl_target sslhcfg_msrdp_targets [] = {
{ & table_sslhcfg_protocols[0], 0, .value.def_string = "msrdp" },
{ & table_sslhcfg_protocols[1], 1, .value.def_string = "0" },
{ & table_sslhcfg_protocols[2], 2, .value.def_string = "0" },
{ & table_sslhcfg_protocols[10], 0, .value.def_int = 1 },
{ 0 }
};
@ -1023,7 +1270,7 @@ static struct compound_cl_target sslhcfg_syslog_targets [] = {
{ & table_sslhcfg_protocols[0], 0, .value.def_string = "syslog" },
{ & table_sslhcfg_protocols[1], 1, .value.def_string = "0" },
{ & table_sslhcfg_protocols[2], 2, .value.def_string = "0" },
{ & table_sslhcfg_protocols[9], 0, .value.def_int = 1 },
{ & table_sslhcfg_protocols[10], 0, .value.def_int = 1 },
{ 0 }
};
@ -1031,7 +1278,7 @@ static struct compound_cl_target sslhcfg_socks5_targets [] = {
{ & table_sslhcfg_protocols[0], 0, .value.def_string = "socks5" },
{ & table_sslhcfg_protocols[1], 1, .value.def_string = "0" },
{ & table_sslhcfg_protocols[2], 2, .value.def_string = "0" },
{ & table_sslhcfg_protocols[9], 0, .value.def_int = 1 },
{ & table_sslhcfg_protocols[10], 0, .value.def_int = 1 },
{ 0 }
};
@ -1039,7 +1286,7 @@ static struct compound_cl_target sslhcfg_adb_targets [] = {
{ & table_sslhcfg_protocols[0], 0, .value.def_string = "adb" },
{ & table_sslhcfg_protocols[1], 1, .value.def_string = "0" },
{ & table_sslhcfg_protocols[2], 2, .value.def_string = "0" },
{ & table_sslhcfg_protocols[9], 0, .value.def_int = 1 },
{ & table_sslhcfg_protocols[10], 0, .value.def_int = 1 },
{ 0 }
};
@ -1047,7 +1294,7 @@ static struct compound_cl_target sslhcfg_http_targets [] = {
{ & table_sslhcfg_protocols[0], 0, .value.def_string = "http" },
{ & table_sslhcfg_protocols[1], 1, .value.def_string = "0" },
{ & table_sslhcfg_protocols[2], 2, .value.def_string = "0" },
{ & table_sslhcfg_protocols[9], 0, .value.def_int = 1 },
{ & table_sslhcfg_protocols[10], 0, .value.def_int = 1 },
{ 0 }
};
@ -1055,7 +1302,16 @@ static struct compound_cl_target sslhcfg_xmpp_targets [] = {
{ & table_sslhcfg_protocols[0], 0, .value.def_string = "xmpp" },
{ & table_sslhcfg_protocols[1], 1, .value.def_string = "0" },
{ & table_sslhcfg_protocols[2], 2, .value.def_string = "0" },
{ & table_sslhcfg_protocols[9], 0, .value.def_int = 1 },
{ & table_sslhcfg_protocols[10], 0, .value.def_int = 1 },
{ 0 }
};
static struct compound_cl_target sslhcfg_wireguard_targets [] = {
{ & table_sslhcfg_protocols[0], 0, .value.def_string = "wireguard" },
{ & table_sslhcfg_protocols[1], 1, .value.def_string = "0" },
{ & table_sslhcfg_protocols[2], 2, .value.def_string = "0" },
{ & table_sslhcfg_protocols[10], 0, .value.def_int = 1 },
{ & table_sslhcfg_protocols[7], 0, .value.def_bool = 1 },
{ 0 }
};
@ -1063,7 +1319,7 @@ static struct compound_cl_target sslhcfg_tinc_targets [] = {
{ & table_sslhcfg_protocols[0], 0, .value.def_string = "tinc" },
{ & table_sslhcfg_protocols[1], 1, .value.def_string = "0" },
{ & table_sslhcfg_protocols[2], 2, .value.def_string = "0" },
{ & table_sslhcfg_protocols[9], 0, .value.def_int = 1 },
{ & table_sslhcfg_protocols[10], 0, .value.def_int = 1 },
{ & table_sslhcfg_protocols[7], 0, .value.def_bool = 1 },
{ 0 }
};
@ -1072,7 +1328,7 @@ static struct compound_cl_target sslhcfg_openvpn_targets [] = {
{ & table_sslhcfg_protocols[0], 0, .value.def_string = "openvpn" },
{ & table_sslhcfg_protocols[1], 1, .value.def_string = "0" },
{ & table_sslhcfg_protocols[2], 2, .value.def_string = "0" },
{ & table_sslhcfg_protocols[9], 0, .value.def_int = 1 },
{ & table_sslhcfg_protocols[10], 0, .value.def_int = 1 },
{ & table_sslhcfg_protocols[7], 0, .value.def_bool = 1 },
{ 0 }
};
@ -1081,7 +1337,7 @@ static struct compound_cl_target sslhcfg_tls_targets [] = {
{ & table_sslhcfg_protocols[0], 0, .value.def_string = "tls" },
{ & table_sslhcfg_protocols[1], 1, .value.def_string = "0" },
{ & table_sslhcfg_protocols[2], 2, .value.def_string = "0" },
{ & table_sslhcfg_protocols[9], 0, .value.def_int = 1 },
{ & table_sslhcfg_protocols[10], 0, .value.def_int = 1 },
{ & table_sslhcfg_protocols[7], 0, .value.def_bool = 1 },
{ 0 }
};
@ -1091,7 +1347,7 @@ static struct compound_cl_target sslhcfg_ssh_targets [] = {
{ & table_sslhcfg_protocols[1], 1, .value.def_string = "0" },
{ & table_sslhcfg_protocols[2], 2, .value.def_string = "0" },
{ & table_sslhcfg_protocols[6], 0, .value.def_bool = 1 },
{ & table_sslhcfg_protocols[9], 0, .value.def_int = 1 },
{ & table_sslhcfg_protocols[10], 0, .value.def_int = 1 },
{ & table_sslhcfg_protocols[7], 0, .value.def_bool = 1 },
{ 0 }
};
@ -1106,7 +1362,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: listen */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_listen,
.base_entry = & table_sslhcfg [12],
.base_entry = & table_sslhcfg [25],
.targets = sslhcfg_listen_targets,
@ -1118,7 +1374,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: ssh */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_ssh,
.base_entry = & table_sslhcfg [13],
.base_entry = & table_sslhcfg [26],
.targets = sslhcfg_ssh_targets,
@ -1130,7 +1386,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: tls */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_tls,
.base_entry = & table_sslhcfg [13],
.base_entry = & table_sslhcfg [26],
.targets = sslhcfg_tls_targets,
@ -1142,7 +1398,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: openvpn */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_openvpn,
.base_entry = & table_sslhcfg [13],
.base_entry = & table_sslhcfg [26],
.targets = sslhcfg_openvpn_targets,
@ -1154,7 +1410,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: tinc */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_tinc,
.base_entry = & table_sslhcfg [13],
.base_entry = & table_sslhcfg [26],
.targets = sslhcfg_tinc_targets,
@ -1163,10 +1419,22 @@ static struct compound_cl_arg compound_cl_args[] = {
.override_const = "tinc",
},
{ /* arg: wireguard */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_wireguard,
.base_entry = & table_sslhcfg [26],
.targets = sslhcfg_wireguard_targets,
.override_desc = & table_sslhcfg_protocols [0],
.override_matchindex = 0,
.override_const = "wireguard",
},
{ /* arg: xmpp */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_xmpp,
.base_entry = & table_sslhcfg [13],
.base_entry = & table_sslhcfg [26],
.targets = sslhcfg_xmpp_targets,
@ -1178,7 +1446,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: http */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_http,
.base_entry = & table_sslhcfg [13],
.base_entry = & table_sslhcfg [26],
.targets = sslhcfg_http_targets,
@ -1190,7 +1458,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: adb */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_adb,
.base_entry = & table_sslhcfg [13],
.base_entry = & table_sslhcfg [26],
.targets = sslhcfg_adb_targets,
@ -1202,7 +1470,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: socks5 */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_socks5,
.base_entry = & table_sslhcfg [13],
.base_entry = & table_sslhcfg [26],
.targets = sslhcfg_socks5_targets,
@ -1214,7 +1482,7 @@ static struct compound_cl_arg compound_cl_args[] = {
{ /* arg: syslog */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_syslog,
.base_entry = & table_sslhcfg [13],
.base_entry = & table_sslhcfg [26],
.targets = sslhcfg_syslog_targets,
@ -1223,10 +1491,22 @@ static struct compound_cl_arg compound_cl_args[] = {
.override_const = "syslog",
},
{ /* arg: msrdp */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_msrdp,
.base_entry = & table_sslhcfg [26],
.targets = sslhcfg_msrdp_targets,
.override_desc = & table_sslhcfg_protocols [0],
.override_matchindex = 0,
.override_const = "msrdp",
},
{ /* arg: anyprot */
.regex = "(.+):(\\w+)",
.arg_cl = & sslhcfg_anyprot,
.base_entry = & table_sslhcfg [13],
.base_entry = & table_sslhcfg [26],
.targets = sslhcfg_anyprot_targets,
@ -1538,7 +1818,7 @@ static int set_target_fields(void* target_addr, struct compound_cl_arg* arg, con
if (pmatch[pmatch_cnt].rm_so == -1) {
/* This should not happen as regexec() did
* match before, unless there is a
* discrepency between the regex and the
* discrepancy between the regex and the
* number of backreferences */
return 0;
}
@ -1831,7 +2111,7 @@ static void scalar_to_string(char** strp, config_setting_t* s)
/* Typesets all the settings in a configuration as a
* newly-allocated string. The string management is caller's
* responsability.
* responsibility.
* Returns the number of scalars in the configuration */
static int cfg_as_string(config_setting_t* parent, const char* path, char** strp)
{
@ -1891,16 +2171,29 @@ int sslhcfg_cl_parse(int argc, char* argv[], struct sslhcfg_item* cfg)
#ifdef LIBCONFIG
sslhcfg_conffile = arg_filen("F", "config", "<file>", 0, 1, "Specify configuration file"),
#endif
sslhcfg_verbose = arg_intn("v", "verbose", "<n>", 0, 1, ""),
sslhcfg_verbose_config = arg_intn(NULL, "verbose-config", "<n>", 0, 1, "Print configuration at startup"),
sslhcfg_verbose_config_error = arg_intn(NULL, "verbose-config-error", "<n>", 0, 1, "Print configuration errors"),
sslhcfg_verbose_connections = arg_intn(NULL, "verbose-connections", "<n>", 0, 1, "Trace established incoming address to forward address"),
sslhcfg_verbose_connections_try = arg_intn(NULL, "verbose-connections-try", "<n>", 0, 1, "Connection errors"),
sslhcfg_verbose_connections_error = arg_intn(NULL, "verbose-connections-error", "<n>", 0, 1, "Connection attempts towards targets"),
sslhcfg_verbose_fd = arg_intn(NULL, "verbose-fd", "<n>", 0, 1, "File descriptor activity, open/close/whatnot"),
sslhcfg_verbose_packets = arg_intn(NULL, "verbose-packets", "<n>", 0, 1, "Hexdump packets on which probing is done"),
sslhcfg_verbose_probe_info = arg_intn(NULL, "verbose-probe-info", "<n>", 0, 1, "Trace the probe process"),
sslhcfg_verbose_probe_error = arg_intn(NULL, "verbose-probe-error", "<n>", 0, 1, "Failures and problems during probing"),
sslhcfg_verbose_system_error = arg_intn(NULL, "verbose-system-error", "<n>", 0, 1, "System call failures"),
sslhcfg_verbose_int_error = arg_intn(NULL, "verbose-int-error", "<n>", 0, 1, "Internal errors that should never happen"),
sslhcfg_version = arg_litn("V", "version", 0, 1, "Print version information and exit"),
sslhcfg_foreground = arg_litn("f", "foreground", 0, 1, "Run in foreground instead of as a daemon"),
sslhcfg_inetd = arg_litn("i", "inetd", 0, 1, "Run in inetd mode: use stdin/stdout instead of network listen"),
sslhcfg_numeric = arg_litn("n", "numeric", 0, 1, "Print IP addresses and ports as numbers"),
sslhcfg_transparent = arg_litn(NULL, "transparent", 0, 1, "Set up as a transparent proxy"),
sslhcfg_timeout = arg_intn("t", "timeout", "<n>", 0, 1, "Set up timeout before connecting to default target"),
sslhcfg_udp_max_connections = arg_intn(NULL, "udp-max-connections", "<n>", 0, 1, "Number of concurrent UDP connections"),
sslhcfg_user = arg_strn("u", "user", "<str>", 0, 1, "Username to change to after set-up"),
sslhcfg_pidfile = arg_strn("P", "pidfile", "<file>", 0, 1, "Path to file to store PID of current instance"),
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_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"),
@ -1908,11 +2201,13 @@ int sslhcfg_cl_parse(int argc, char* argv[], struct sslhcfg_item* cfg)
sslhcfg_tls = arg_strn(NULL, "tls", "<host:port>", 0, 10, "Set up TLS/SSL target"),
sslhcfg_openvpn = arg_strn(NULL, "openvpn", "<host:port>", 0, 10, "Set up OpenVPN target"),
sslhcfg_tinc = arg_strn(NULL, "tinc", "<host:port>", 0, 10, "Set up tinc target"),
sslhcfg_wireguard = arg_strn(NULL, "wireguard", "<host:port>", 0, 10, "Set up WireGuard target"),
sslhcfg_xmpp = arg_strn(NULL, "xmpp", "<host:port>", 0, 10, "Set up XMPP target"),
sslhcfg_http = arg_strn(NULL, "http", "<host:port>", 0, 10, "Set up HTTP (plain) target"),
sslhcfg_adb = arg_strn(NULL, "adb", "<host:port>", 0, 10, "Set up ADB (Android Debug) target"),
sslhcfg_socks5 = arg_strn(NULL, "socks5", "<host:port>", 0, 10, "Set up socks5 target"),
sslhcfg_syslog = arg_strn(NULL, "syslog", "<host:port>", 0, 10, "Set up syslog target"),
sslhcfg_msrdp = arg_strn(NULL, "msrdp", "<host:port>", 0, 10, "Set up msrdp target"),
sslhcfg_anyprot = arg_strn(NULL, "anyprot", "<host:port>", 0, 10, "Set up default target"),
sslhcfg_end = arg_end(10)
@ -2002,6 +2297,9 @@ static void sslhcfg_protocols_fprint(
fprintf(out, "transparent: %d", sslhcfg_protocols->transparent);
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "resolve_on_forward: %d", sslhcfg_protocols->resolve_on_forward);
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "log_level: %d", sslhcfg_protocols->log_level);
fprintf(out, "\n");
indent(out, depth);
@ -2059,7 +2357,40 @@ void sslhcfg_fprint(
{
int i;
indent(out, depth);
fprintf(out, "verbose: %d", sslhcfg->verbose);
fprintf(out, "verbose_config: %d", sslhcfg->verbose_config);
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "verbose_config_error: %d", sslhcfg->verbose_config_error);
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "verbose_connections: %d", sslhcfg->verbose_connections);
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "verbose_connections_try: %d", sslhcfg->verbose_connections_try);
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "verbose_connections_error: %d", sslhcfg->verbose_connections_error);
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "verbose_fd: %d", sslhcfg->verbose_fd);
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "verbose_packets: %d", sslhcfg->verbose_packets);
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "verbose_probe_info: %d", sslhcfg->verbose_probe_info);
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "verbose_probe_error: %d", sslhcfg->verbose_probe_error);
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "verbose_system_error: %d", sslhcfg->verbose_system_error);
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "verbose_int_error: %d", sslhcfg->verbose_int_error);
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "version: %d", sslhcfg->version);
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "foreground: %d", sslhcfg->foreground);
@ -2077,6 +2408,9 @@ void sslhcfg_fprint(
fprintf(out, "timeout: %d", sslhcfg->timeout);
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "udp_max_connections: %d", sslhcfg->udp_max_connections);
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "user: %s", sslhcfg->user);
if (! sslhcfg->user_is_present)
fprintf(out, " <unset>");
@ -2095,6 +2429,11 @@ void sslhcfg_fprint(
fprintf(out, "syslog_facility: %s", sslhcfg->syslog_facility);
fprintf(out, "\n");
indent(out, depth);
fprintf(out, "logfile: %s", sslhcfg->logfile);
if (! sslhcfg->logfile_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);

View File

@ -1,5 +1,5 @@
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
* on Fri Aug 13 18:03:18 2021.
* on Sun Sep 11 21:43:25 2022.
# conf2struct: generate libconf parsers that read to structs
# Copyright (C) 2018-2021 Yves Rutschle
@ -58,6 +58,7 @@ struct sslhcfg_protocols_item {
int fork;
int tfo_ok;
int transparent;
int resolve_on_forward;
int log_level;
int keepalive;
size_t sni_hostnames_len;
@ -71,15 +72,28 @@ struct sslhcfg_protocols_item {
T_PROBE* probe;
struct addrinfo* saddr;
void* data;
dl_list timeouts;
};
struct sslhcfg_item {
int verbose;
int verbose_config;
int verbose_config_error;
int verbose_connections;
int verbose_connections_try;
int verbose_connections_error;
int verbose_fd;
int verbose_packets;
int verbose_probe_info;
int verbose_probe_error;
int verbose_system_error;
int verbose_int_error;
int version;
int foreground;
int inetd;
int numeric;
int transparent;
int timeout;
int udp_max_connections;
int user_is_present;
char* user;
int pidfile_is_present;
@ -87,6 +101,8 @@ struct sslhcfg_item {
int chroot_is_present;
char* chroot;
char* syslog_facility;
int logfile_is_present;
char* logfile;
char* on_timeout;
char* prefix;
size_t listen_len;

153
sslh-ev.c Normal file
View File

@ -0,0 +1,153 @@
/*
sslh-ev: mono-processus server based on libev
# Copyright (C) 2021 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
*/
#include <stdlib.h>
#include <ev.h>
#include "gap.h"
#include "log.h"
#include "udp-listener.h"
#include "tcp-listener.h"
const char* server_type = "sslh-ev";
static struct ev_loop* loop;
/* Libev watchers */
struct watchers {
/* one set of ev_io for read, one for write, indexed by file descriptor */
gap_array *ev_ior, *ev_iow;
struct listen_endpoint* listen_sockets;
gap_array* fd2ls; /* Array indexed by file descriptor, pointing to listen_sockets */
};
static void cnx_read_cb(EV_P_ ev_io *w, int revents);
static void cnx_write_cb(EV_P_ ev_io *w, int wevents);
static void cnx_accept_cb(EV_P_ ev_io *w, int revents);
static void watchers_init(watchers** w, struct listen_endpoint* listen_sockets,
int num_addr_listen)
{
*w = malloc(sizeof(**w));
(*w)->ev_ior = gap_init(num_addr_listen);
(*w)->ev_iow = gap_init(num_addr_listen);
(*w)->listen_sockets = listen_sockets;
(*w)->fd2ls = gap_init(0);
/* Create watchers for listen sockets */
for (int i = 0; i < num_addr_listen; i++) {
ev_io* io = malloc(sizeof(*io));
ev_io_init(io, &cnx_accept_cb, listen_sockets[i].socketfd, EV_READ);
ev_io_start(EV_A_ io);
gap_set((*w)->ev_ior, i, io);
gap_set((*w)->fd2ls, listen_sockets[i].socketfd, &listen_sockets[i]);
set_nonblock(listen_sockets[i].socketfd);
}
}
void watchers_add_read(watchers* w, int fd)
{
ev_io* io = gap_get(w->ev_ior, fd);
if (!io) {
io = malloc(sizeof(*io));
ev_io_init(io, &cnx_read_cb, fd, EV_READ);
ev_io_set(io, fd, EV_READ);
gap_set(w->ev_ior, fd, io);
}
ev_io_start(loop, io);
}
void watchers_del_read(watchers* w, int fd)
{
ev_io* io = gap_get(w->ev_ior, fd);
if (io) ev_io_stop(EV_A_ io);
}
void watchers_add_write(watchers* w, int fd)
{
ev_io* io = gap_get(w->ev_iow, fd);
if (!io) {
io = malloc(sizeof(*io));
ev_io_init(io, &cnx_write_cb, fd, EV_WRITE);
ev_io_set(io, fd, EV_WRITE);
gap_set(w->ev_iow, fd, io);
}
ev_io_start(loop, io);
}
void watchers_del_write(watchers* w, int fd)
{
ev_io* io = gap_get(w->ev_iow, fd);
if (io) ev_io_stop(EV_A_ io);
}
/* /watchers */
#include "processes.h"
/* Libev callbacks */
static void cnx_read_cb(EV_P_ ev_io *w, int revents)
{
struct loop_info* info = ev_userdata(EV_A);
cnx_read_process(info, w->fd);
}
static void cnx_write_cb(EV_P_ ev_io *w, int wevents)
{
struct loop_info* info = ev_userdata(EV_A);
cnx_write_process(info, w->fd);
}
static void cnx_accept_cb(EV_P_ ev_io *w, int revents)
{
struct loop_info* info = ev_userdata(EV_A);
cnx_accept_process(info, gap_get(info->watchers->fd2ls, w->fd));
}
void main_loop(struct listen_endpoint listen_sockets[], int num_addr_listen)
{
struct loop_info ev_info = {0};
loop = EV_DEFAULT;
ev_info.collection = collection_init(0);
ev_info.probing_list = gap_init(0);
udp_init(&ev_info);
tcp_init();
watchers_init(&ev_info.watchers, listen_sockets, num_addr_listen);
ev_set_userdata(EV_A_ &ev_info);
ev_run(EV_A_ 0);
}
void start_shoveler(int listen_socket) {
print_message(msg_config_error, "inetd mode is not supported in libev mode\n");
exit(1);
}

View File

@ -23,7 +23,8 @@
#include "common.h"
#include "probe.h"
#include "sslh-conf.h"
#include "udp-listener.h"
#include "tcp-probe.h"
#include "log.h"
#ifdef LIBBSD
#include <bsd/unistd.h>
@ -58,8 +59,7 @@ int shovel(struct connection *cnx)
if (FD_ISSET(cnx->q[i].fd, &fds)) {
res = fd2fd(&cnx->q[1-i], &cnx->q[i]);
if (res == FD_CNXCLOSED) {
if (cfg.verbose)
fprintf(stderr, "%s %s", i ? "client" : "server", "socket closed\n");
print_message(msg_fd, "%s %s", i ? "client" : "server", "socket closed\n");
return res;
}
}
@ -99,8 +99,7 @@ void start_shoveler(int in_socket)
} else {
/* Timed out: it's necessarily SSH */
cnx.proto = timeout_protocol();
if (cfg.verbose)
log_message(LOG_INFO, "timed out, connect to %s\n", cnx.proto->name);
print_message(msg_fd, "timed out, connect to %s\n", cnx.proto->name);
break;
}
}
@ -129,8 +128,7 @@ void start_shoveler(int in_socket)
close(in_socket);
close(out_socket);
if (cfg.verbose)
fprintf(stderr, "connection closed down\n");
print_message(msg_fd, "connection closed down\n");
exit(0);
}
@ -179,10 +177,11 @@ void tcp_listener(struct listen_endpoint* endpoint, int num_endpoints, int activ
while (1) {
in_socket = accept(endpoint[active_endpoint].socketfd, 0, 0);
if (cfg.verbose) fprintf(stderr, "accepted fd %d\n", in_socket);
CHECK_RES_RETURN(in_socket, "accept", /*void*/ );
print_message(msg_fd, "accepted fd %d\n", in_socket);
switch(fork()) {
case -1: log_message(LOG_ERR, "fork failed: err %d: %s\n", errno, strerror(errno));
case -1: print_message(msg_system_error, "fork failed: err %d: %s\n", errno, strerror(errno));
break;
case 0: /* In child process */
@ -209,18 +208,20 @@ void main_loop(struct listen_endpoint listen_sockets[], int num_addr_listen)
listener_pid = malloc(listener_pid_number * sizeof(listener_pid[0]));
CHECK_ALLOC(listener_pid, "malloc");
tcp_init();
/* Start one process for each listening address */
for (i = 0; i < num_addr_listen; i++) {
listener_pid[i] = fork();
switch(listener_pid[i]) {
/* Log if fork() fails for some reason */
case -1: log_message(LOG_ERR, "fork failed: err %d: %s\n", errno, strerror(errno));
case -1: print_message(msg_system_error, "fork failed: err %d: %s\n", errno, strerror(errno));
break;
/* We're in the child, we have work to do */
case 0:
set_listen_procname(&listen_sockets[i]);
if (listen_sockets[i].type == SOCK_DGRAM)
log_message(LOG_ERR, "UDP not (yet?) supported in sslh-fork\n");
print_message(msg_config_error, "UDP not (yet?) supported in sslh-fork\n");
else
tcp_listener(listen_sockets, num_addr_listen, i);
break;

View File

@ -36,29 +36,7 @@
#include "common.h"
#include "probe.h"
const char* USAGE_STRING =
"sslh " VERSION "\n" \
"usage:\n" \
"\tsslh [-v] [-i] [-V] [-f] [-n] [--transparent] [-F<file>]\n"
"\t[-t <timeout>] [-P <pidfile>] [-u <username>] [-C <chroot>] -p <addr> [-p <addr> ...] \n" \
"%s\n\n" /* Dynamically built list of builtin protocols */ \
"\t[--on-timeout <addr>]\n" \
"-v: verbose\n" \
"-V: version\n" \
"-f: foreground\n" \
"-n: numeric output\n" \
"-u: specify under which user to run\n" \
"-C: specify under which chroot path to run\n" \
"--transparent: behave as a transparent proxy\n" \
"-F: use configuration file (warning: no space between -F and file name!)\n" \
"--on-timeout: connect to specified address upon timeout (default: ssh address)\n" \
"-t: seconds to wait before connecting to --on-timeout address.\n" \
"-p: address and port to listen on.\n Can be used several times to bind to several addresses.\n" \
"--[ssh,ssl,...]: where to connect connections from corresponding protocol.\n" \
"-P: PID file.\n" \
"-i: Run as a inetd service.\n" \
"";
#include "log.h"
/* Constants for options that have no one-character shorthand */
#define OPT_ONTIMEOUT 257
@ -73,7 +51,7 @@ static void printcaps(void) {
desc = cap_to_text(caps, &len);
fprintf(stderr, "capabilities: %s\n", desc);
print_message(msg_config, "capabilities: %s\n", desc);
cap_free(caps);
cap_free(desc);
@ -88,21 +66,26 @@ static void printsettings(void)
for (i = 0; i < cfg.protocols_len; i++ ) {
p = &cfg.protocols[i];
fprintf(stderr,
"%s addr: %s. libwrap service: %s log_level: %d family %d %d [%s] [%s] [%s]\n",
p->name,
sprintaddr(buf, sizeof(buf), p->saddr),
p->service,
p->log_level,
p->saddr->ai_family,
p->saddr->ai_addr->sa_family,
p->keepalive ? "keepalive" : "",
p->fork ? "fork" : "",
p->transparent ? "transparent" : ""
);
print_message(msg_config,
"%s addr: %s. libwrap service: %s log_level: %d family %d %d [%s] [%s] [%s]\n",
p->name,
sprintaddr(buf, sizeof(buf), p->saddr),
p->service,
p->log_level,
p->saddr->ai_family,
p->saddr->ai_addr->sa_family,
p->keepalive ? "keepalive" : "",
p->fork ? "fork" : "",
p->transparent ? "transparent" : ""
);
}
fprintf(stderr, "timeout: %d\non-timeout: %s\n", cfg.timeout,
timeout_protocol()->name);
print_message(msg_config,
"timeout: %d\n"
"on-timeout: %s\n"
"UDP hash size: %d\n",
cfg.timeout,
timeout_protocol()->name,
cfg.udp_max_connections);
}
@ -126,7 +109,7 @@ static void setup_regex_probe(struct sslhcfg_protocols_item *p)
&error, &error_offset, NULL);
if (!pattern_list[i]) {
pcre2_get_error_message(error, err_str, sizeof(err_str));
fprintf(stderr, "compiling pattern /%s/:%d:%s at offset %ld\n",
print_message(msg_config_error, "compiling pattern /%s/:%d:%s at offset %ld\n",
p->regex_patterns[i], error, err_str, error_offset);
exit(1);
}
@ -146,14 +129,19 @@ static void config_protocols()
int i;
for (i = 0; i < cfg.protocols_len; i++) {
struct sslhcfg_protocols_item* p = &(cfg.protocols[i]);
if (resolve_split_name(&(p->saddr), p->host, p->port)) {
fprintf(stderr, "cannot resolve %s:%s\n", p->host, p->port);
if (
!p->resolve_on_forward &&
resolve_split_name(&(p->saddr), p->host, p->port)
) {
print_message(msg_config_error, "cannot resolve %s:%s\n",
p->host, p->port);
exit(4);
}
p->probe = get_probe(p->name);
if (!p->probe) {
fprintf(stderr, "%s: probe unknown\n", p->name);
print_message(msg_config_error, "%s: probe unknown\n", p->name);
exit(1);
}
@ -172,23 +160,55 @@ static void config_protocols()
(const char**) cfg.protocols[i].alpn_protocols,
cfg.protocols[i].alpn_protocols_len);
}
p->timeouts.head = NULL;
p->timeouts.tail = NULL;
}
}
void config_sanity_check(struct sslhcfg_item* cfg) {
if (!cfg->protocols_len) {
fprintf(stderr, "At least one target protocol must be specified.\n");
exit(2);
}
void config_sanity_check(struct sslhcfg_item* cfg)
{
size_t i;
/* If compiling with systemd socket support no need to require listen address */
#ifndef SYSTEMD
if (!cfg->listen_len && !cfg->inetd) {
fprintf(stderr, "No listening address specified; use at least one -p option\n");
print_message(msg_config_error, "No listening address specified; use at least one -p option\n");
exit(1);
}
#endif
for (i = 0; i < cfg->protocols_len; ++i) {
if (strcmp(cfg->protocols[i].name, "tls")) {
if (cfg->protocols[i].sni_hostnames_len) {
print_message(msg_config_error, "name: \"%s\"; host: \"%s\"; port: \"%s\": "
"Config option sni_hostnames is only applicable for tls\n",
cfg->protocols[i].name, cfg->protocols[i].host, cfg->protocols[i].port);
exit(1);
}
if (cfg->protocols[i].alpn_protocols_len) {
print_message(msg_config_error, "name: \"%s\"; host: \"%s\"; port: \"%s\": "
"Config option alpn_protocols is only applicable for tls\n",
cfg->protocols[i].name, cfg->protocols[i].host, cfg->protocols[i].port);
exit(1);
}
}
if (cfg->protocols[i].is_udp) {
if (cfg->protocols[i].tfo_ok) {
print_message(msg_config_error, "name: \"%s\"; host: \"%s\"; port: \"%s\": "
"Config option tfo_ok is not applicable for udp connections\n",
cfg->protocols[i].name, cfg->protocols[i].host, cfg->protocols[i].port);
exit(1);
}
} else {
if (!strcmp(cfg->protocols[i].name, "wireguard")) {
print_message(msg_config_error, "Wireguard works only with UDP\n");
exit(1);
}
}
}
}
@ -207,26 +227,29 @@ int main(int argc, char *argv[], char* envp[])
memset(&cfg, 0, sizeof(cfg));
res = sslhcfg_cl_parse(argc, argv, &cfg);
if (res) exit(6);
if (cfg.verbose > 3)
sslhcfg_fprint(stderr, &cfg, 0);
if (cfg.version) {
printf("%s %s\n", server_type, VERSION);
exit(0);
}
config_protocols();
config_sanity_check(&cfg);
if (cfg.inetd)
{
cfg.verbose = 0;
close(fileno(stderr)); /* Make sure no error will go to client */
start_shoveler(0);
exit(0);
}
if (cfg.verbose)
printsettings();
printsettings();
num_addr_listen = start_listen_sockets(&listen_sockets);
#ifdef SYSTEMD
if (num_addr_listen < 1) {
fprintf(stderr, "No listening sockets found, restart sockets or specify addresses in config\n");
print_message(msg_config_error, "No listening sockets found, restart sockets or specify addresses in config\n");
exit(1);
}
#endif
@ -249,13 +272,21 @@ int main(int argc, char *argv[], char* envp[])
/* Open syslog connection before we drop privs/chroot */
setup_syslog(argv[0]);
/* Open log file for writing */
setup_logfile();
if (cfg.user || cfg.chroot)
drop_privileges(cfg.user, cfg.chroot);
if (cfg.verbose)
printcaps();
printcaps();
print_message(msg_config, "%s %s started\n", server_type, VERSION);
main_loop(listen_sockets, num_addr_listen);
close_logfile();
free(listen_sockets);
return 0;
}

View File

@ -25,482 +25,88 @@
* https://daniel.haxx.se/docs/poll-vs-select.html suggests that over a few
* hundred file descriptors, both become very slow, so there is little
* incentive to move to poll() to support more than FD_SETSIZE (which is 1024
* on many Linux. To support large numbers of descriptors, either use the fork
* version, or we'll have to write a new version based on libev. */
* on many Linux. To support large numbers of descriptors efficiently, either use sslh-fork
* or sslh-ev. */
#define __LINUX__
#include <limits.h>
#include "common.h"
#include "probe.h"
#include "tcp-listener.h"
#include "udp-listener.h"
#include "collection.h"
#include "processes.h"
#include "gap.h"
static int debug = 0;
#include "log.h"
const char* server_type = "sslh-select";
/* Global state for a select() loop */
struct select_info {
int max_fd; /* Highest fd number to pass to select() */
int num_probing; /* Number of connections currently probing
* We use this to know if we need to time out of
* select() */
gap_array* probing_list; /* Pointers to cnx that are in probing mode */
/* watcher type for a select() loop */
struct watchers {
fd_set fds_r, fds_w; /* reference fd sets (used to init working copies) */
cnx_collection* collection; /* Collection of connections linked to this loop */
time_t next_timeout; /* time at which next UDP connection times out */
int max_fd; /* Highest fd number to pass to select() */
};
static int tidy_connection(struct connection *cnx, struct select_info* fd_info)
static void watchers_init(watchers** w, struct listen_endpoint* listen_sockets,
int num_addr_listen)
{
int i;
fd_set* fds = &fd_info->fds_r;
fd_set* fds2 = &fd_info->fds_w;
*w = malloc(sizeof(**w));
CHECK_ALLOC(*w, "malloc");
for (i = 0; i < 2; i++) {
if (cnx->q[i].fd != -1) {
if (cfg.verbose)
fprintf(stderr, "closing fd %d\n", cnx->q[i].fd);
memset(*w, 0, sizeof(**w));
FD_ZERO(&(*w)->fds_r);
FD_ZERO(&(*w)->fds_w);
FD_CLR(cnx->q[i].fd, fds);
FD_CLR(cnx->q[i].fd, fds2);
close(cnx->q[i].fd);
if (cnx->q[i].deferred_data)
free(cnx->q[i].deferred_data);
}
for (int i = 0; i < num_addr_listen; i++) {
watchers_add_read(*w, listen_sockets[i].socketfd);
set_nonblock(listen_sockets[i].socketfd);
}
collection_remove_cnx(fd_info->collection, cnx);
return 0;
}
void watchers_add_read(watchers* w, int fd)
{
FD_SET(fd, &w->fds_r);
if (fd > w->max_fd)
w->max_fd = fd + 1;
}
void watchers_del_read(watchers* w, int fd)
{
FD_CLR(fd, &w->fds_r);
}
void watchers_add_write(watchers* w, int fd)
{
FD_SET(fd, &w->fds_w);
if (fd > w->max_fd)
w->max_fd = fd + 1;
}
void watchers_del_write(watchers* w, int fd)
{
FD_CLR(fd, &w->fds_w);
}
/* /end watchers */
/* if fd becomes higher than FD_SETSIZE, things won't work so well with FD_SET
* and FD_CLR. Need to drop connections if we go above that limit */
static int fd_is_in_range(int fd) {
static int fd_out_of_range(int fd) {
if (fd >= FD_SETSIZE) {
log_message(LOG_ERR, "too many open file descriptor to monitor them all -- dropping connection\n");
return 0;
print_message(msg_system_error, "too many open file descriptor to monitor them all -- dropping connection\n");
return 1;
}
return 1;
}
/* Accepts a connection from the main socket and assigns it to an empty slot.
* If no slots are available, allocate another few. If that fails, drop the
* connexion */
static struct connection* accept_new_connection(int listen_socket, struct cnx_collection *collection)
{
int in_socket, res;
if (cfg.verbose) fprintf(stderr, "accepting from %d\n", listen_socket);
in_socket = accept(listen_socket, 0, 0);
CHECK_RES_RETURN(in_socket, "accept", NULL);
if (!fd_is_in_range(in_socket)) {
close(in_socket);
return NULL;
}
res = set_nonblock(in_socket);
if (res == -1) {
close(in_socket);
return NULL;
}
struct connection* cnx = collection_alloc_cnx_from_fd(collection, in_socket);
if (!cnx) {
close(in_socket);
return NULL;
}
return cnx;
return 0;
}
/* Connect queue 1 of connection to SSL; returns new file descriptor */
static int connect_queue(struct connection* cnx,
struct select_info* fd_info)
{
struct queue *q = &cnx->q[1];
q->fd = connect_addr(cnx, cnx->q[0].fd, NON_BLOCKING);
if ((q->fd != -1) && fd_is_in_range(q->fd)) {
log_connection(NULL, cnx);
flush_deferred(q);
if (q->deferred_data) {
FD_SET(q->fd, &fd_info->fds_w);
FD_CLR(cnx->q[0].fd, &fd_info->fds_r);
}
FD_SET(q->fd, &fd_info->fds_r);
collection_add_fd(fd_info->collection, cnx, q->fd);
return q->fd;
} else {
tidy_connection(cnx, fd_info);
return -1;
}
}
/* shovels data from active fd to the other
returns after one socket closed or operation would block
*/
static void shovel(struct connection *cnx, int active_fd, struct select_info* fd_info)
{
struct queue *read_q, *write_q;
read_q = &cnx->q[active_fd];
write_q = &cnx->q[1-active_fd];
if (cfg.verbose)
fprintf(stderr, "activity on fd%d\n", read_q->fd);
switch(fd2fd(write_q, read_q)) {
case -1:
case FD_CNXCLOSED:
tidy_connection(cnx, fd_info);
break;
case FD_STALLED:
FD_SET(write_q->fd, &fd_info->fds_w);
FD_CLR(read_q->fd, &fd_info->fds_r);
break;
default: /* Nothing */
break;
}
}
/* shovels data from one fd to the other and vice-versa
returns after one socket closed
*/
static void shovel_single(struct connection *cnx)
{
fd_set fds_r, fds_w;
int res, i;
int max_fd = MAX(cnx->q[0].fd, cnx->q[1].fd) + 1;
FD_ZERO(&fds_r);
FD_ZERO(&fds_w);
while (1) {
for (i = 0; i < 2; i++) {
if (cnx->q[i].deferred_data_size) {
FD_SET(cnx->q[i].fd, &fds_w);
FD_CLR(cnx->q[1-i].fd, &fds_r);
} else {
FD_CLR(cnx->q[i].fd, &fds_w);
FD_SET(cnx->q[1-i].fd, &fds_r);
}
}
res = select(
max_fd,
&fds_r,
&fds_w,
NULL,
NULL
);
CHECK_RES_DIE(res, "select");
for (i = 0; i < 2; i++) {
if (FD_ISSET(cnx->q[i].fd, &fds_w)) {
res = flush_deferred(&cnx->q[i]);
if ((res == -1) && ((errno == EPIPE) || (errno == ECONNRESET))) {
if (cfg.verbose)
fprintf(stderr, "%s socket closed\n", i ? "server" : "client");
return;
}
}
if (FD_ISSET(cnx->q[i].fd, &fds_r)) {
res = fd2fd(&cnx->q[1-i], &cnx->q[i]);
if (!res) {
if (cfg.verbose)
fprintf(stderr, "socket closed\n");
return;
}
}
}
}
}
/* Child process that makes internal connection and proxies
*/
static void connect_proxy(struct connection *cnx)
{
int in_socket;
int out_socket;
/* Minimize the file descriptor value to help select() */
in_socket = dup(cnx->q[0].fd);
if (in_socket == -1) {
in_socket = cnx->q[0].fd;
} else {
close(cnx->q[0].fd);
cnx->q[0].fd = in_socket;
}
/* Connect the target socket */
out_socket = connect_addr(cnx, in_socket, BLOCKING);
CHECK_RES_DIE(out_socket, "connect");
cnx->q[1].fd = out_socket;
log_connection(NULL, cnx);
shovel_single(cnx);
close(in_socket);
close(out_socket);
if (cfg.verbose)
fprintf(stderr, "connection closed down\n");
exit(0);
}
/* Removes cnx from probing list */
static void remove_probing_cnx(struct select_info* fd_info, struct connection* cnx)
{
gap_remove_ptr(fd_info->probing_list, cnx, fd_info->num_probing);
fd_info->num_probing--;
}
static void add_probing_cnx(struct select_info* fd_info, struct connection* cnx)
{
gap_set(fd_info->probing_list, fd_info->num_probing, cnx);
fd_info->num_probing++;
}
/* Process read activity on a socket in probe state
* IN/OUT cnx: connection data, updated if connected
* IN/OUT info: updated if connected
* */
static void probing_read_process(struct connection* cnx,
struct select_info* fd_info)
{
int res;
/* If timed out it's SSH, otherwise the client sent
* data so probe the protocol */
if ((cnx->probe_timeout < time(NULL))) {
cnx->proto = timeout_protocol();
if (cfg.verbose)
log_message(LOG_INFO,
"timed out, connect to %s\n",
cnx->proto->name);
} else {
res = probe_client_protocol(cnx);
if (res == PROBE_AGAIN)
return;
}
remove_probing_cnx(fd_info, cnx);
cnx->state = ST_SHOVELING;
/* libwrap check if required for this protocol */
if (cnx->proto->service &&
check_access_rights(cnx->q[0].fd, cnx->proto->service)) {
tidy_connection(cnx, fd_info);
res = -1;
} else if (cnx->proto->fork) {
switch (fork()) {
case 0: /* child */
/* TODO: close all file descriptors except 2 */
/* free(cnx); */
connect_proxy(cnx);
exit(0);
case -1: log_message(LOG_ERR, "fork failed: err %d: %s\n", errno, strerror(errno));
break;
default: /* parent */
break;
}
tidy_connection(cnx, fd_info);
res = -1;
} else {
res = connect_queue(cnx, fd_info);
}
if (res >= fd_info->max_fd)
fd_info->max_fd = res + 1;;
}
/* Returns the queue index that contains the specified file descriptor */
int active_queue(struct connection* cnx, int fd)
{
if (cnx->q[0].fd == fd) return 0;
if (cnx->q[1].fd == fd) return 1;
log_message(LOG_ERR, "file descriptor %d not found in connection object\n", fd);
return -1;
}
/* Process a connection that is active in read */
static void tcp_read_process(struct select_info* fd_info,
int fd)
{
if (debug) fprintf(stderr, "cnx_read_process fd %d\n", fd);
cnx_collection* collection = fd_info->collection;
struct connection* cnx = collection_get_cnx_from_fd(collection, fd);
/* Determine active queue (0 or 1): if fd is that of q[1], active_q = 1,
* otherwise it's 0 */
int active_q = active_queue(cnx, fd);
switch (cnx->state) {
case ST_PROBING:
if (active_q == 1) {
fprintf(stderr, "Activity on fd2 while probing, impossible\n");
dump_connection(cnx);
exit(1);
}
probing_read_process(cnx, fd_info);
break;
case ST_SHOVELING:
shovel(cnx, active_q, fd_info);
break;
default: /* illegal */
log_message(LOG_ERR, "Illegal connection state %d\n", cnx->state);
dump_connection(cnx);
exit(1);
}
}
static void cnx_read_process(struct select_info* fd_info, int fd)
{
cnx_collection* collection = fd_info->collection;
struct connection* cnx = collection_get_cnx_from_fd(collection, fd);
switch (cnx->type) {
case SOCK_STREAM:
tcp_read_process(fd_info, fd);
break;
case SOCK_DGRAM:
udp_s2c_forward(cnx);
break;
default:
log_message(LOG_ERR, "cnx_read_process: Illegal connection type %d\n", cnx->type);
dump_connection(cnx);
exit(1);
}
}
/* Process a connection that is active in write */
static void cnx_write_process(struct select_info* fd_info, int fd)
{
if (debug) fprintf(stderr, "cnx_write_process fd %d\n", fd);
struct connection* cnx = collection_get_cnx_from_fd(fd_info->collection, fd);
int res;
int queue = active_queue(cnx, fd);
res = flush_deferred(&cnx->q[queue]);
if ((res == -1) && ((errno == EPIPE) || (errno == ECONNRESET))) {
if (cnx->state == ST_PROBING) remove_probing_cnx(fd_info, cnx);
tidy_connection(cnx, fd_info);
} else {
/* If no deferred data is left, stop monitoring the fd
* for write, and restart monitoring the other one for reads*/
if (!cnx->q[queue].deferred_data_size) {
FD_CLR(cnx->q[queue].fd, &fd_info->fds_w);
FD_SET(cnx->q[1-queue].fd, &fd_info->fds_r);
}
}
}
/* Process a connection that accepts a socket
* (For UDP, this means all traffic coming from remote clients)
* */
void cnx_accept_process(struct select_info* fd_info, struct listen_endpoint* listen_socket)
{
int fd = listen_socket->socketfd;
int type = listen_socket->type;
struct connection* cnx;
int new_fd;
if (debug) fprintf(stderr, "cnx_accept_process fd %d\n", fd);
switch (type) {
case SOCK_STREAM:
cnx = accept_new_connection(fd, fd_info->collection);
if (cnx) {
add_probing_cnx(fd_info, cnx);
new_fd = cnx->q[0].fd;
}
break;
case SOCK_DGRAM:
new_fd = udp_c2s_forward(fd, fd_info->collection, fd_info->max_fd);
fprintf(stderr, "new_fd %d\n", new_fd);
if (new_fd == -1)
return;
break;
default:
log_message(LOG_ERR, "Inconsistent cnx type: %d\n", type);
exit(1);
return;
}
FD_SET(new_fd, &fd_info->fds_r);
if (new_fd >= fd_info->max_fd)
fd_info->max_fd = new_fd + 1;
}
/* Check all connections to see if a UDP connections has timed out, then free
* it. At the same time, keep track of the closest, next timeout. Only do the
* search through connections if that timeout actually happened. If the
* connection that would have timed out has had activity, it doesn't matter: we
* go through connections to find the next timeout, which was needed anyway. */
static void udp_timeouts(struct select_info* fd_info)
{
time_t now = time(NULL);
if (now < fd_info->next_timeout) return;
for (int i = 0; i < fd_info->max_fd; i++) {
time_t next_timeout = INT_MAX;
/* if it's either in read or write set, there is a connection
* behind that file descriptor */
if (FD_ISSET(i, &fd_info->fds_r) || FD_ISSET(i, &fd_info->fds_w)) {
struct connection* cnx = collection_get_cnx_from_fd(fd_info->collection, i);
if (cnx) {
time_t timeout = udp_timeout(cnx);
if (cnx && (timeout <= now)) {
if (cfg.verbose > 3)
fprintf(stderr, "timed out UDP %d\n", cnx->target_sock);
close(cnx->target_sock);
FD_CLR(i, &fd_info->fds_r);
FD_CLR(i, &fd_info->fds_w);
collection_remove_cnx(fd_info->collection, cnx);
} else {
if (timeout < next_timeout) next_timeout = timeout;
}
}
}
if (next_timeout != INT_MAX)
fd_info->next_timeout = next_timeout;
}
}
/* Main loop: the idea is as follow:
* - fds_r and fds_w contain the file descriptors to monitor in read and write
@ -518,48 +124,42 @@ static void udp_timeouts(struct select_info* fd_info)
*/
void main_loop(struct listen_endpoint listen_sockets[], int num_addr_listen)
{
struct select_info fd_info = {0};
struct loop_info fd_info = {0};
fd_set readfds, writefds; /* working read and write fd sets */
struct timeval tv;
int i, res;
fd_info.num_probing = 0;
FD_ZERO(&fd_info.fds_r);
FD_ZERO(&fd_info.fds_w);
fd_info.probing_list = gap_init();
fd_info.probing_list = gap_init(0);
udp_init(&fd_info);
tcp_init();
for (i = 0; i < num_addr_listen; i++) {
FD_SET(listen_sockets[i].socketfd, &fd_info.fds_r);
set_nonblock(listen_sockets[i].socketfd);
}
fd_info.max_fd = listen_sockets[num_addr_listen-1].socketfd + 1;
watchers_init(&fd_info.watchers, listen_sockets, num_addr_listen);
fd_info.collection = collection_init();
fd_info.collection = collection_init(fd_info.watchers->max_fd);
while (1)
{
memset(&tv, 0, sizeof(tv));
tv.tv_sec = cfg.timeout;
memcpy(&readfds, &fd_info.fds_r, sizeof(readfds));
memcpy(&writefds, &fd_info.fds_w, sizeof(writefds));
memcpy(&readfds, &fd_info.watchers->fds_r, sizeof(readfds));
memcpy(&writefds, &fd_info.watchers->fds_w, sizeof(writefds));
if (cfg.verbose)
fprintf(stderr, "selecting... max_fd=%d num_probing=%d\n",
fd_info.max_fd, fd_info.num_probing);
res = select(fd_info.max_fd, &readfds, &writefds,
print_message(msg_fd, "selecting... max_fd=%d num_probing=%d\n",
fd_info.watchers->max_fd, fd_info.num_probing);
res = select(fd_info.watchers->max_fd + 1, &readfds, &writefds,
NULL, fd_info.num_probing ? &tv : NULL);
if (res < 0)
perror("select");
/* UDP timeouts: clear out connections after some idle time */
udp_timeouts(&fd_info);
/* Check main socket for new connections */
for (i = 0; i < num_addr_listen; i++) {
if (FD_ISSET(listen_sockets[i].socketfd, &readfds)) {
cnx_accept_process(&fd_info, &listen_sockets[i]);
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);
@ -567,8 +167,11 @@ void main_loop(struct listen_endpoint listen_sockets[], int num_addr_listen)
}
/* Check all sockets for write activity */
for (i = 0; i < fd_info.max_fd; i++) {
if (FD_ISSET(i, &writefds)) {
for (i = 0; i < fd_info.watchers->max_fd; i++) {
/* Check if it's active AND currently monitored (if a connection
* died, it gets tidied, which closes both sockets, but writefs does
* not know about that */
if (FD_ISSET(i, &writefds) && FD_ISSET(i, &fd_info.watchers->fds_w)) {
cnx_write_process(&fd_info, i);
}
}
@ -577,24 +180,23 @@ void main_loop(struct listen_endpoint listen_sockets[], int num_addr_listen)
for (i = 0; i < fd_info.num_probing; i++) {
struct connection* cnx = gap_get(fd_info.probing_list, i);
if (!cnx || cnx->state != ST_PROBING) {
log_message(LOG_ERR, "Inconsistent probing: cnx=%0xp\n", cnx);
print_message(msg_int_error, "Inconsistent probing: cnx=0x%p\n", cnx);
if (cnx)
log_message(LOG_ERR, "Inconsistent probing: state=%d\n", cnx);
print_message(msg_int_error, "Inconsistent probing: state=%d\n", cnx->state);
exit(1);
}
if (cnx->probe_timeout < time(NULL)) {
if (cfg.verbose)
fprintf(stderr, "timeout slot %d\n", i);
print_message(msg_fd, "timeout slot %d\n", i);
probing_read_process(cnx, &fd_info);
}
}
/* Check all sockets for read activity */
for (i = 0; i < fd_info.max_fd; i++) {
for (i = 0; i < fd_info.watchers->max_fd; i++) {
/* Check if it's active AND currently monitored (if a connection
* died, it gets tidied, which closes both sockets, but readfs does
* not know about that */
if (FD_ISSET(i, &readfds) && FD_ISSET(i, &fd_info.fds_r)) {
if (FD_ISSET(i, &readfds) && FD_ISSET(i, &fd_info.watchers->fds_r)) {
cnx_read_process(&fd_info, i);
}
}
@ -603,7 +205,7 @@ void main_loop(struct listen_endpoint listen_sockets[], int num_addr_listen)
void start_shoveler(int listen_socket) {
fprintf(stderr, "inetd mode is not supported in select mode\n");
print_message(msg_config_error, "inetd mode is not supported in select mode\n");
exit(1);
}

View File

@ -25,7 +25,34 @@ config: {
name : "sslhcfg",
type: "list",
items: (
{ name: "verbose"; type: "int"; default: 0; short: "v"; },
{ name: "verbose-config"; type: "int"; default: 0;
description: "Print configuration at startup" },
{ name: "verbose-config-error"; type: "int"; default: 3;
description: "Print configuration errors" },
{ name: "verbose-connections"; type: "int"; default: 3;
description: "Trace established incoming address to forward address" },
{ name: "verbose-connections-try"; type: "int"; default: 0;
description: "Connection errors" },
{ name: "verbose-connections-error"; type: "int"; default: 3;
description: "Connection attempts towards targets" },
{ name: "verbose-fd"; type: "int"; default: 0;
description: "File descriptor activity, open/close/whatnot" },
{ name: "verbose-packets"; type: "int"; default: 0;
description: "Hexdump packets on which probing is done" },
{ name: "verbose-probe-info"; type: "int"; default: 0;
description: "Trace the probe process" },
{ name: "verbose-probe-error"; type: "int"; default: 3;
description: "Failures and problems during probing" },
{ name: "verbose-system-error"; type: "int"; default: 3;
description: "System call failures" },
{ name: "verbose-int-error"; type: "int"; default: 3;
description: "Internal errors that should never happen" },
{ name: "version"; type: "bool"; default: false;
short: "V";
description: "Print version information and exit"; },
{ name: "foreground"; type: "bool"; default: false;
short: "f";
description: "Run in foreground instead of as a daemon"; },
@ -40,6 +67,8 @@ config: {
{ name: "timeout"; type: "int"; default: 5;
short: "t";
description: "Set up timeout before connecting to default target"; },
{ name: "udp_max_connections"; type: "int"; default: 1024;
description: "Number of concurrent UDP connections"; },
{ name: "user"; type: "string"; optional: true;
short: "u";
description: "Username to change to after set-up"; },
@ -51,6 +80,8 @@ config: {
description: "Root to change to after set-up"; },
{ name: "syslog_facility"; type: "string"; default: "auth";
description: "Facility to syslog to"; },
{ name: "logfile"; type: "string"; optional: true;
description: "Log messages to a file" },
{ name: "on-timeout"; type: "string"; default: "ssh";
description: "Target to connect to when timing out"; },
@ -81,8 +112,10 @@ config: {
{ name: "fork"; type: "bool"; default: false },
{ name: "tfo_ok"; type: "bool"; default: false;
description: "Set to true if this protocol supports TCP FAST OPEN" },
{ name: "transparent"; type: "bool"; default: false;
{ name: "transparent"; type: "bool"; default: false;
description: "Set to proxy this protocol transparently" },
{ name: "resolve_on_forward"; type: "bool"; default: false;
description: "Set to true if server address should be resolved on (every) newly incoming connection (again)" },
{ name: "log_level"; type: "int"; default: 1 },
{ name: "keepalive"; type: "bool"; default: false },
{ name: "sni_hostnames",
@ -102,7 +135,8 @@ config: {
# Runtime data
{ name: "probe"; type: "runtime"; c_type: "T_PROBE*" },
{ name: "saddr"; type: "runtime"; c_type: "struct addrinfo*" },
{ name: "data"; type: "runtime"; c_type: "void*" }
{ name: "data"; type: "runtime"; c_type: "void*" },
{ name: "timeouts"; type: "runtime"; c_type: "dl_list" }
)
}
)
@ -176,6 +210,18 @@ cl_groups: (
{ path: "tfo_ok"; value: 1 }
);
},
{ name: "wireguard"; pattern: "(.+):(\w+)"; description: "Set up WireGuard target";
list: "protocols";
override: "name";
argdesc: "<host:port>";
targets: (
{ path: "name"; value: "wireguard" },
{ path: "host"; value: "$1" },
{ path: "port"; value: "$2" },
{ path: "log_level"; value: 1 },
{ path: "tfo_ok"; value: 1 }
);
},
{ name: "xmpp"; pattern: "(.+):(\w+)"; description: "Set up XMPP target";
list: "protocols";
override: "name";
@ -231,6 +277,17 @@ cl_groups: (
{ path: "log_level"; value: 1 }
);
},
{ name: "msrdp"; pattern: "(.+):(\w+)"; description: "Set up msrdp target";
list: "protocols";
override: "name";
argdesc: "<host:port>";
targets: (
{ path: "name"; value: "msrdp" },
{ path: "host"; value: "$1" },
{ path: "port"; value: "$2" },
{ path: "log_level"; value: 1 }
);
},
{ name: "anyprot"; pattern: "(.+):(\w+)"; description: "Set up default target";
list: "protocols";
override: "name";

View File

@ -5,6 +5,8 @@
#include "common.h"
#define print_message(sink, format, file, line) fprintf(stderr, format, file, line)
static char* resolve_listen(const char *hostname, const char *port) {
/* Need room in the strcat for \0 and :
* the format in the socket unit file is hostname:port */
@ -53,9 +55,12 @@ static int get_listen_from_conf(const char *filename, char **listen[]) {
config_setting_source_line(addr));
return -1;
} else {
(*listen)[i] = malloc(strlen(resolve_listen(hostname, port)));
char *resolved_listen = resolve_listen(hostname, port);
(*listen)[i] = malloc(strlen(resolved_listen));
CHECK_ALLOC((*listen)[i], "malloc");
strcpy((*listen)[i], resolve_listen(hostname, port));
strcpy((*listen)[i], resolved_listen);
free(resolved_listen);
}
}
}
@ -123,6 +128,7 @@ static int gen_sslh_config(char *runtime_unit_dir) {
strcpy(runtime_conf, runtime_unit_dir);
strcat(runtime_conf, unit_file);
runtime_conf_fd = fopen(runtime_conf, "w");
free(runtime_conf);
}

16
t
View File

@ -26,6 +26,7 @@ my $PROBES_NOFRAG = 1;
my $PROBES_AGAIN = 1;
my $SSL_MIX_SSH = 1;
my $SSH_MIX_SSL = 1;
my $DROP_CNX = 1;
# Robustness tests. These are mostly to achieve full test
# coverage, but do not necessarily result in an actual test
@ -285,6 +286,19 @@ for my $binary (@binaries) {
}
}
# Test: Drop connection without writing anything
if ($DROP_CNX) {
print "***Test: Connect but don't write anything\n";
my $cnx_h = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
warn "$!\n" unless $cnx_h;
if ($cnx_h) {
close $cnx_h;
my_is(1, "$binary: Connect and write nothing");
# The goal of the test is to check sslh doesn't
# crash
}
}
if ($PROBES_NOFRAG) {
test_probes(no_frag => 1, binary => $binary);
@ -382,7 +396,7 @@ if ($RB_RESOLVE_ADDRESS) {
my $sslh_pid;
if (!($sslh_pid = fork)) {
my $user = (getpwuid $<)[0]; # Run under current username
exec "./sslh-select -v 3 -f -u $user --listen blahblah.dontexist:9000 --ssh $ssh_address --tls $ssl_address -P $pidfile";
exec "./sslh-select -v 3 -f -u $user --listen blahblah.nonexistent:9000 --ssh $ssh_address --tls $ssl_address -P $pidfile";
}
warn "spawned $sslh_pid\n";
waitpid $sslh_pid, 0;

8
t_load
View File

@ -20,7 +20,7 @@ use Conf::Libconfig;
# How many total clients to we start? Each client will pick
# a new protocol among what's in test.cfg.
my $NUM_CNX = 50;
my $NUM_CNX = 16;
# Delay between starting new processes when starting up. If
# you start 200 processes in under a second, things go wrong
@ -35,7 +35,7 @@ my $block_rpt = 5;
# Probability to stop a client after a message (e.g. with
# .01 a client will send an average of 100 messages before
# disconnecting).
my $stop_client_probability = .001;
my $stop_client_probability = .0001;
##END CONFIG
@ -65,7 +65,7 @@ my %connect_params = (
test_data => "foo bar",
resp_len => 12,
},
ssh => {
ssh => {
sleep => 20, # So it times out 50% of connections
test_data => "SSH-2.0 hello",
resp_len => 18, # length "ssh: SSH-2.0 hello" => 18
@ -73,7 +73,7 @@ my %connect_params = (
tinc => {
sleep => 0,
test_data => "0 ",
resp_len => 8, # length "tinc: 0 " => 10
resp_len => 8, # length "tinc: 0 " => 10
},
openvpn => {
sleep => 0,

333
tcp-listener.c Normal file
View File

@ -0,0 +1,333 @@
/*
tcp-listener.c: handles demultiplexing TCP protocols
# Copyright (C) 2022 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
*/
#include <errno.h>
#include "tcp-listener.h"
#include "probe.h"
#include "log.h"
/* Removes cnx from probing list */
static void remove_probing_cnx(struct loop_info* fd_info, struct connection* cnx)
{
gap_remove_ptr(fd_info->probing_list, cnx, fd_info->num_probing);
fd_info->num_probing--;
}
static void add_probing_cnx(struct loop_info* fd_info, struct connection* cnx)
{
gap_set(fd_info->probing_list, fd_info->num_probing, cnx);
fd_info->num_probing++;
}
/* shovels data from active fd to the other
returns after one socket closed or operation would block
*/
static void shovel(struct connection *cnx, int active_fd, struct loop_info* fd_info)
{
struct queue *read_q, *write_q;
read_q = &cnx->q[active_fd];
write_q = &cnx->q[1-active_fd];
print_message(msg_fd, "activity on fd%d\n", read_q->fd);
switch(fd2fd(write_q, read_q)) {
case -1:
case FD_CNXCLOSED:
tidy_connection(cnx, fd_info);
break;
case FD_STALLED:
watchers_add_write(fd_info->watchers, write_q->fd);
watchers_del_read(fd_info->watchers, read_q->fd);
break;
default: /* Nothing */
break;
}
}
/* Returns the queue index that contains the specified file descriptor.
* This is called by the *_process functions, which got cnx from the fd, so it
* is impossible for fd to not be cnx, hence we die if it happens */
static int active_queue(struct connection* cnx, int fd)
{
if (cnx->q[0].fd == fd) return 0;
if (cnx->q[1].fd == fd) return 1;
print_message(msg_int_error, "file descriptor %d not found in connection object\n", fd);
exit(1);
}
/* Process a TCP read event on the specified file descriptor */
void tcp_read_process(struct loop_info* fd_info,
int fd)
{
cnx_collection* collection = fd_info->collection;
struct connection* cnx = collection_get_cnx_from_fd(collection, fd);
/* connection can get tidied if there is an error on the other file
* descriptor -- then cnx is NULL */
if (!cnx) return;
/* Determine active queue (0 or 1): if fd is that of q[1], active_q = 1,
* otherwise it's 0 */
int active_q = active_queue(cnx, fd);
switch (cnx->state) {
case ST_PROBING:
if (active_q == 1) {
print_message(msg_int_error, "Activity on fd2 while probing, impossible\n");
dump_connection(cnx);
exit(1);
}
probing_read_process(cnx, fd_info);
break;
case ST_SHOVELING:
shovel(cnx, active_q, fd_info);
break;
default: /* illegal */
print_message(msg_int_error, "Illegal connection state %d\n", cnx->state);
dump_connection(cnx);
exit(1);
}
}
/* Accepts a connection from the main socket and assigns it to an empty slot.
* If no slots are available, allocate another few. If that fails, drop the
* connexion */
struct connection* accept_new_connection(int listen_socket, struct loop_info* fd_info)
{
int in_socket, res;
print_message(msg_fd, "accepting from %d\n", listen_socket);
in_socket = accept(listen_socket, 0, 0);
CHECK_RES_RETURN(in_socket, "accept", NULL);
res = set_nonblock(in_socket);
if (res == -1) {
close(in_socket);
return NULL;
}
struct connection* cnx = collection_alloc_cnx_from_fd(fd_info->collection, in_socket);
if (!cnx) {
close(in_socket);
return NULL;
}
add_probing_cnx(fd_info, cnx);
return cnx;
}
/* Connect queue 1 of connection to SSL; returns new file descriptor */
static int connect_queue(struct connection* cnx,
struct loop_info* fd_info)
{
struct queue *q = &cnx->q[1];
q->fd = connect_addr(cnx, cnx->q[0].fd, NON_BLOCKING);
if (q->fd != -1) {
log_connection(NULL, cnx);
flush_deferred(q);
if (q->deferred_data) {
watchers_add_write(fd_info->watchers, q->fd);
watchers_del_read(fd_info->watchers, cnx->q[0].fd);
}
watchers_add_read(fd_info->watchers, q->fd);
collection_add_fd(fd_info->collection, cnx, q->fd);
return q->fd;
} else {
tidy_connection(cnx, fd_info);
return -1;
}
}
/* shovels data from one fd to the other and vice-versa
returns after one socket closed
*/
static void shovel_single(struct connection *cnx)
{
fd_set fds_r, fds_w;
int res, i;
int max_fd = MAX(cnx->q[0].fd, cnx->q[1].fd) + 1;
FD_ZERO(&fds_r);
FD_ZERO(&fds_w);
while (1) {
for (i = 0; i < 2; i++) {
if (cnx->q[i].deferred_data_size) {
FD_SET(cnx->q[i].fd, &fds_w);
FD_CLR(cnx->q[1-i].fd, &fds_r);
} else {
FD_CLR(cnx->q[i].fd, &fds_w);
FD_SET(cnx->q[1-i].fd, &fds_r);
}
}
res = select(
max_fd,
&fds_r,
&fds_w,
NULL,
NULL
);
CHECK_RES_DIE(res, "select");
for (i = 0; i < 2; i++) {
if (FD_ISSET(cnx->q[i].fd, &fds_w)) {
res = flush_deferred(&cnx->q[i]);
if ((res == -1) && ((errno == EPIPE) || (errno == ECONNRESET))) {
print_message(msg_fd, "%s socket closed\n", i ? "server" : "client");
return;
}
}
if (FD_ISSET(cnx->q[i].fd, &fds_r)) {
res = fd2fd(&cnx->q[1-i], &cnx->q[i]);
if (!res) {
print_message(msg_fd, "socket closed\n");
return;
}
}
}
}
}
/* Child process that makes internal connection and proxies
*/
static void connect_proxy(struct connection *cnx)
{
int in_socket;
int out_socket;
/* Minimize the file descriptor value to help select() */
in_socket = dup(cnx->q[0].fd);
if (in_socket == -1) {
in_socket = cnx->q[0].fd;
} else {
close(cnx->q[0].fd);
cnx->q[0].fd = in_socket;
}
/* Connect the target socket */
out_socket = connect_addr(cnx, in_socket, BLOCKING);
CHECK_RES_DIE(out_socket, "connect");
cnx->q[1].fd = out_socket;
log_connection(NULL, cnx);
shovel_single(cnx);
close(in_socket);
close(out_socket);
print_message(msg_fd, "connection closed down\n");
exit(0);
}
/* Process read activity on a socket in probe state
* IN/OUT cnx: connection data, updated if connected
* IN/OUT info: updated if connected
* */
void probing_read_process(struct connection* cnx,
struct loop_info* fd_info)
{
int res;
/* If timed out it's SSH, otherwise the client sent
* data so probe the protocol */
if ((cnx->probe_timeout < time(NULL))) {
cnx->proto = timeout_protocol();
print_message(msg_fd, "timed out, connect to %s\n", cnx->proto->name);
} else {
res = probe_client_protocol(cnx);
if (res == PROBE_AGAIN)
return;
}
remove_probing_cnx(fd_info, cnx);
cnx->state = ST_SHOVELING;
/* libwrap check if required for this protocol */
if (cnx->proto->service &&
check_access_rights(cnx->q[0].fd, cnx->proto->service)) {
tidy_connection(cnx, fd_info);
res = -1;
} else if (cnx->proto->fork) {
switch (fork()) {
case 0: /* child */
/* TODO: close all file descriptors except 2 */
/* free(cnx); */
connect_proxy(cnx);
exit(0);
case -1: print_message(msg_system_error, "fork failed: err %d: %s\n", errno, strerror(errno));
break;
default: /* parent */
break;
}
tidy_connection(cnx, fd_info);
res = -1;
} else {
res = connect_queue(cnx, fd_info);
}
}
/* Process a connection that is active in write (this is TCP only, as
* UDP sockets are never "full" or deferred or whatever) */
void cnx_write_process(struct loop_info* fd_info, int fd)
{
struct connection* cnx = collection_get_cnx_from_fd(fd_info->collection, fd);
/* connection can get tidied if there is an error on the other file
* descriptor -- then cnx is NULL */
if (!cnx) return;
int res;
int queue = active_queue(cnx, fd);
res = flush_deferred(&cnx->q[queue]);
if ((res == -1) && ((errno == EPIPE) || (errno == ECONNRESET))) {
if (cnx->state == ST_PROBING) remove_probing_cnx(fd_info, cnx);
tidy_connection(cnx, fd_info);
} else {
/* If no deferred data is left, stop monitoring the fd
* for write, and restart monitoring the other one for reads*/
if (!cnx->q[queue].deferred_data_size) {
watchers_del_write(fd_info->watchers, cnx->q[queue].fd);
watchers_add_read(fd_info->watchers, cnx->q[1-queue].fd);
}
}
}

13
tcp-listener.h Normal file
View File

@ -0,0 +1,13 @@
#ifndef TCP_LISTENER_H
#define TCP_LISTENER_H
#include "processes.h"
#include "collection.h"
#include "tcp-probe.h"
void tcp_read_process(struct loop_info* fd_info, int fd);
struct connection* accept_new_connection(int listen_socket, struct loop_info* fd_info);
void probing_read_process(struct connection* cnx, struct loop_info* fd_info);
void cnx_write_process(struct loop_info* fd_info, int fd);
#endif

99
tcp-probe.c Normal file
View File

@ -0,0 +1,99 @@
/*
# tcp-probe.c: TCP code that is common to the sslh-fork and sslh-[ev|select]
#
# Copyright (C) 2022 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
*/
#include "probe.h"
static struct sslhcfg_protocols_item** tcp_protocols;
static int tcp_protocols_len = 0;
/*
* Read the beginning of data coming from the client connection and check if
* it's a known protocol.
* Return PROBE_AGAIN if not enough data, or PROBE_MATCH if it succeeded in
* which case cnx->proto is set to the appropriate protocol.
*/
int probe_client_protocol(struct connection *cnx)
{
char buffer[BUFSIZ];
ssize_t n;
n = read(cnx->q[0].fd, buffer, sizeof(buffer));
/* It's possible that read() returns an error, e.g. if the client
* disconnected between the previous call to select() and now. If that
* happens, we just connect to the default protocol so the caller of this
* function does not have to deal with a specific failure condition (the
* connection will just fail later normally). */
if (n > 0) {
defer_write(&cnx->q[1], buffer, n);
return probe_buffer(cnx->q[1].begin_deferred_data,
cnx->q[1].deferred_data_size,
tcp_protocols, tcp_protocols_len,
&cnx->proto
);
}
/* read() returned an error, so just connect to the last protocol to die */
cnx->proto = &cfg.protocols[cfg.protocols_len-1];
return PROBE_MATCH;
}
static void tcp_protocol_list_init(void)
{
for (int i = 0; i < cfg.protocols_len; i++) {
struct sslhcfg_protocols_item* p = &cfg.protocols[i];
if (!p->is_udp) {
tcp_protocols_len++;
tcp_protocols = realloc(tcp_protocols, tcp_protocols_len * sizeof(*tcp_protocols));
CHECK_ALLOC(tcp_protocols, "realloc");
tcp_protocols[tcp_protocols_len-1] = p;
}
}
}
/* Configuration sanity check for TCP:
* - If there is a listening socket, there must be at least one target
*/
static void tcp_sanity_check(void)
{
int tcp_present = 0;
for (int i = 0; i < cfg.listen_len; i++) {
struct sslhcfg_listen_item* p = &cfg.listen[i];
if (!p->is_udp) {
tcp_present = 1;
break;
}
}
if (tcp_present && !tcp_protocols_len) {
print_message(msg_config_error, "At least one TCP target protocol must be specified.\n");
exit(2);
}
}
void tcp_init(void)
{
tcp_protocol_list_init();
tcp_sanity_check();
}

6
tcp-probe.h Normal file
View File

@ -0,0 +1,6 @@
#ifndef TCP_PROBE_H
#define TCP_PROBE_H
void tcp_init(void);
#endif

View File

@ -1,7 +1,6 @@
# Configuration file for testing (use both by sslh under
# test and the test script `t`)
verbose: 4;
foreground: true;
inetd: false;
numeric: true;
@ -11,6 +10,21 @@ 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: 0; # 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:
@ -21,6 +35,10 @@ listen:
{ host: "ip4-localhost"; is_udp: true; port: "8086"; }
);
# 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:
(

15
tls.c
View File

@ -33,6 +33,7 @@
#include <fnmatch.h> /* fnmatch() */
#include "tls.h"
#include "sslh-conf.h"
#include "log.h"
#define TLS_HEADER_LEN 5
#define TLS_HANDSHAKE_CONTENT_TYPE 0x16
@ -82,14 +83,14 @@ parse_tls_header(const struct TLSProtocol *tls_data, const char *data, size_t da
tls_content_type = data[0];
if (tls_content_type != TLS_HANDSHAKE_CONTENT_TYPE) {
if (cfg.verbose) fprintf(stderr, "Request did not begin with TLS handshake.\n");
print_message(msg_probe_info, "Request did not begin with TLS handshake.\n");
return TLS_EPROTOCOL;
}
tls_version_major = data[1];
tls_version_minor = data[2];
if (tls_version_major < 3) {
if (cfg.verbose) fprintf(stderr, "Received SSL %d.%d handshake which cannot be parsed.\n",
print_message(msg_probe_error, "Received SSL %d.%d handshake which cannot be parsed.\n",
tls_version_major, tls_version_minor);
return TLS_EVERSION;
@ -111,7 +112,7 @@ parse_tls_header(const struct TLSProtocol *tls_data, const char *data, size_t da
return TLS_EPROTOCOL;
}
if (data[pos] != TLS_HANDSHAKE_TYPE_CLIENT_HELLO) {
if (cfg.verbose) fprintf(stderr, "Not a client hello\n");
print_message(msg_probe_error, "Not a client hello\n");
return TLS_EPROTOCOL;
}
@ -144,7 +145,7 @@ parse_tls_header(const struct TLSProtocol *tls_data, const char *data, size_t da
pos += 1 + len;
if (pos == data_len && tls_version_major == 3 && tls_version_minor == 0) {
if (cfg.verbose) fprintf(stderr, "Received SSL 3.0 handshake without extensions\n");
print_message(msg_probe_error, "Received SSL 3.0 handshake without extensions\n");
return TLS_EVERSION;
}
@ -228,7 +229,7 @@ parse_server_name_extension(const struct TLSProtocol *tls_data, const char *data
return TLS_ENOEXT;
}
default:
if (cfg.verbose) fprintf(stderr, "Unknown server name extension name type: %d\n",
print_message(msg_probe_error, "Unknown server name extension name type: %d\n",
data[pos]);
}
pos += 3 + len;
@ -254,7 +255,7 @@ parse_alpn_extension(const struct TLSProtocol *tls_data, const char *data, size_
if (len > 0 && has_match(tls_data->alpn_protocol_list, tls_data->alpn_list_len, data + pos + 1, len)) {
return len;
} else if (len > 0) {
if (cfg.verbose) fprintf(stderr, "Unknown ALPN name: %.*s\n", (int)len, data + pos + 1);
print_message(msg_probe_error, "Unknown ALPN name: %.*s\n", (int)len, data + pos + 1);
}
pos += 1 + len;
}
@ -276,7 +277,7 @@ has_match(const char** list, size_t list_len, const char* name, size_t name_len)
for (i = 0; i < list_len; i++) {
item = &list[i];
if (cfg.verbose) fprintf(stderr, "matching [%.*s] with [%s]\n", (int)name_len, name, *item);
print_message(msg_probe_error, "matching [%.*s] with [%s]\n", (int)name_len, name, *item);
if(!fnmatch(*item, name_nullterminated, 0)) {
free(name_nullterminated);
return 1;

View File

@ -1,7 +1,7 @@
/*
udp-listener.c: handles demultplexing UDP protocols
udp-listener.c: handles demultiplexing UDP protocols
# Copyright (C) 2020-2021 Yves Rutschle
# Copyright (C) 2020-2022 Yves Rutschle
#
# This program is free software; you can redistribute it
# and/or modify it under the terms of the GNU General Public
@ -20,78 +20,271 @@
*/
#include <limits.h>
#include "common.h"
#include "probe.h"
#include "sslh-conf.h"
#include "udp-listener.h"
/* Incoming connections are of course all received on a single socket. Create a
* hash that associates (incoming sockaddr) => struct connection*, so finding
* the connection related to an incoming packet is fast.
*/
/* Find if the specified source has been seen before. -1 if not found
*
* TODO This is linear search and needs to be changed to something better for
* production if we have more than a dozen sources
* Also, this assumes src_addr from recvfrom() are repeatable for a specific
* source...
* */
static int known_source(cnx_collection* collection, int max_fd, struct sockaddr* addr, socklen_t addrlen)
static int cnx_cmp(struct connection* cnx1, struct connection* cnx2)
{
int i;
struct sockaddr* addr1 = &cnx1->client_addr;
socklen_t addrlen1 = cnx1->addrlen;
for (i = 0; i < max_fd; i++) {
struct connection* cnx = collection_get_cnx_from_fd(collection, i);
if (cnx && (cnx->type == SOCK_DGRAM) && cnx->target_sock) {
if (!memcmp(&cnx->client_addr, addr, addrlen)) {
return i;
}
struct sockaddr* addr2 = &cnx2->client_addr;
socklen_t addrlen2 = cnx2->addrlen;
if (addrlen1 != addrlen2) return -1;
return memcmp(addr1, addr2, addrlen1);
}
/* From an IP address, create something that's useable as a hash key.
* Currently:
* lowest bytes of remote port */
static int hash_make_key(hash_item new)
{
struct sockaddr* addr = &new->client_addr;
//socklen_t addrlen = new->addrlen;
struct sockaddr_in* addr4;
struct sockaddr_in6* addr6;
int out;
switch (addr->sa_family) {
case AF_INET:
addr4 = (struct sockaddr_in*)addr;
out = addr4->sin_port;
break;
case AF_INET6:
addr6 = (struct sockaddr_in6*)addr;
out = addr6->sin6_port;
break;
default: /* Just use the first bytes, skipping the address family */
out = ((char*)addr)[2];
break;
}
return out;
}
static struct sslhcfg_protocols_item** udp_protocols;
static int udp_protocols_len = 0;
static void udp_protocol_list_init(void)
{
for (int i = 0; i < cfg.protocols_len; i++) {
struct sslhcfg_protocols_item* p = &cfg.protocols[i];
if (p->is_udp) {
udp_protocols_len++;
udp_protocols = realloc(udp_protocols, udp_protocols_len * sizeof(*udp_protocols));
CHECK_ALLOC(udp_protocols, "realloc");
udp_protocols[udp_protocols_len-1] = p;
}
}
return -1;
}
/* Configuration sanity check for UDP:
* - If there is a listening address, there must be at least one target
*/
static void udp_sanity_check(void)
{
int udp_present = 0;
for (int i = 0; i < cfg.listen_len; i++) {
struct sslhcfg_listen_item* p = &cfg.listen[i];
if (p->is_udp) {
udp_present = 1;
break;
}
}
if (udp_present && !udp_protocols_len) {
print_message(msg_config_error, "At least one UDP target protocol must be specified.\n");
exit(2);
}
}
/* Init the UDP subsystem.
* - Initialise the hash
* - that's all, folks
* */
void udp_init(struct loop_info* fd_info)
{
fd_info->hash_sources = hash_init(cfg.udp_max_connections, &hash_make_key, &cnx_cmp);
udp_protocol_list_init();
udp_sanity_check();
}
/* Find if the specified source has been seen before.
* If yes, returns file descriptor of connection
* If not, returns -1
* */
static int known_source(hash* h, struct sockaddr_storage* addr, socklen_t addrlen)
{
struct connection search;
search.client_addr = *addr;
search.addrlen = addrlen;
struct connection* cnx = hash_find(h, &search);
if (!cnx) return -1;
return cnx->q[0].fd;
}
static int new_source(hash* h, struct connection* new)
{
return hash_insert(h, new);
}
/* Double linked list utilities: push element at tail of list */
static void list_push(dl_list* list, struct connection* cnx)
{
cnx->timeout_next = NULL;
if (!list->head) {
cnx->timeout_prev = NULL;
list->head = cnx;
}
if (list->tail) {
list->tail->timeout_next = cnx;
cnx->timeout_prev = list->tail;
}
list->tail = cnx;
}
/* Double linked list utilities: remove element */
static void list_remove(dl_list* list, struct connection* cnx)
{
if (list->head == cnx) list->head = cnx->timeout_next;
if (list->tail == cnx) list->tail = cnx->timeout_prev;
if (cnx->timeout_prev)
cnx->timeout_prev->timeout_next = cnx->timeout_next;
if (cnx->timeout_next)
cnx->timeout_next->timeout_prev = cnx->timeout_prev;
}
/* Timeouts are managed with one list for each protocol. Whenever a connection
* is active, it gets moved to the end of the list. Each call will pop the
* first elements that have timed out and free their resources.
*
* This gets called every time a UDP packet is received from the outside, i.e.
* every time we might need to free up resources. If no packets come in, we
* don't time out anything, as we don't need the resources.
* */
void udp_timeouts(struct loop_info* fd_info)
{
time_t now = time(NULL);
for (int i = 0; i < cfg.protocols_len; i++) {
struct connection *cnx = cfg.protocols[i].timeouts.head;
while (cnx && (now - cnx->last_active > cfg.protocols[i].udp_timeout)) {
print_message(msg_fd, "timed out UDP %d\n", cnx->target_sock);
tidy_connection(cnx, fd_info);
cnx = cfg.protocols[i].timeouts.head;
}
}
}
void udp_tidy(struct connection* cnx, struct loop_info* fd_info)
{
close(cnx->target_sock);
hash_remove(fd_info->hash_sources, cnx);
list_remove(&cnx->proto->timeouts, cnx);
}
/* Mark the connection was active */
static void mark_active(struct connection* cnx)
{
cnx->last_active = time(NULL);
dl_list* list = &cnx->proto->timeouts;
list_remove(list, cnx);
list_push(list, cnx);
}
/* Creates a new non-blocking socket */
static int nonblocking_socket(struct sslhcfg_protocols_item* proto)
{
int out = socket(proto->saddr->ai_family, SOCK_DGRAM, 0);
int res = set_nonblock(out);
if (res == -1) {
print_message(msg_system_error, "%s:%d:%s:%d:%s\n", __FILE__, __LINE__, "udp:socket:nonblock", errno, strerror(errno));
close(out);
return -1;
}
return out;
}
/* Process UDP coming from outside (client towards server)
* If it's a new source, probe; otherwise, forward to previous target
* Returns: >= 0 sockfd of newly allocated socket, for new connections
* -1 otherwise
* Returns: newly allocate connections, for new connections
* NULL otherwise
* */
int udp_c2s_forward(int sockfd, cnx_collection* collection, int max_fd)
struct connection* udp_c2s_forward(int sockfd, struct loop_info* fd_info)
{
char addr_str[NI_MAXHOST+1+NI_MAXSERV+1];
struct sockaddr src_addr;
struct sockaddr_storage src_addr;
struct addrinfo addrinfo;
struct sslhcfg_protocols_item* proto;
cnx_collection* collection = fd_info->collection;
struct connection* cnx;
ssize_t len;
socklen_t addrlen;
int res, target, out = -1;
char data[65536]; /* Theoritical max is 65507 (https://en.wikipedia.org/wiki/User_Datagram_Protocol).
char data[65536]; /* Theoretical max is 65507 (https://en.wikipedia.org/wiki/User_Datagram_Protocol).
This will do. Dynamic allocation is possible with the MSG_PEEK flag in recvfrom(2), but that'd imply
malloc/free overhead for each packet, when really 64K is not that much */
udp_timeouts(fd_info);
addrlen = sizeof(src_addr);
len = recvfrom(sockfd, data, sizeof(data), 0, &src_addr, &addrlen);
len = recvfrom(sockfd, data, sizeof(data), 0, (struct sockaddr*) &src_addr, &addrlen);
if (len < 0) {
perror("recvfrom");
return -1;
return NULL;
}
target = known_source(collection, max_fd, &src_addr, addrlen);
addrinfo.ai_addr = &src_addr;
target = known_source(fd_info->hash_sources, &src_addr, addrlen);
addrinfo.ai_addr = (struct sockaddr*) &src_addr;
addrinfo.ai_addrlen = addrlen;
if (cfg.verbose)
fprintf(stderr, "received %ld UDP from %d:%s\n", len, target, sprintaddr(addr_str, sizeof(addr_str), &addrinfo));
print_message(msg_probe_info, "received %ld UDP from %d:%s\n",
len, target, sprintaddr(addr_str, sizeof(addr_str), &addrinfo));
if (target == -1) {
res = probe_buffer(data, len, &proto);
res = probe_buffer(data, len, udp_protocols, udp_protocols_len, &proto);
/* First version: if we can't work out the protocol from the first
* packet, drop it. Conceivably, we could store several packets to
* run probes on packet sets */
if (cfg.verbose) fprintf(stderr, "UDP probed: %d\n", res);
print_message(msg_probe_info, "UDP probed: %d\n", res);
if (res != PROBE_MATCH) {
return -1;
return NULL;
}
out = socket(proto->saddr->ai_family, SOCK_DGRAM, 0);
out = nonblocking_socket(proto);
if (out == -1) return NULL;
struct connection* cnx = collection_alloc_cnx_from_fd(collection, out);
if (!cnx) return -1;
if (!cnx) return NULL;
target = out;
cnx->target_sock = out;
cnx->proto = proto;
@ -99,18 +292,25 @@ int udp_c2s_forward(int sockfd, cnx_collection* collection, int max_fd)
cnx->client_addr = src_addr;
cnx->addrlen = addrlen;
cnx->local_endpoint = sockfd;
res = new_source(fd_info->hash_sources, cnx);
if (res == -1) {
print_message(msg_connections_error, "Out of hash space for new incoming UDP connection -- increase udp_max_connections");
collection_remove_cnx(collection, cnx);
return NULL;
}
}
cnx = collection_get_cnx_from_fd(collection, target);
/* at this point src is the UDP connection */
res = sendto(cnx->target_sock, data, len, 0,
cnx->proto->saddr->ai_addr, cnx->proto->saddr->ai_addrlen);
cnx->last_active = time(NULL);
fprintf(stderr, "sending %d to %s\n",
mark_active(cnx);
print_message(msg_fd, "sending %d to %s\n",
res, sprintaddr(data, sizeof(data), cnx->proto->saddr));
return out;
}
return cnx;
}
void udp_s2c_forward(struct connection* cnx)
{
@ -119,20 +319,10 @@ void udp_s2c_forward(struct connection* cnx)
int res;
res = recvfrom(sockfd, data, sizeof(data), 0, NULL, NULL);
fprintf(stderr, "recvfrom %d\n", res);
if ((res == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) return;
CHECK_RES_DIE(res, "udp_listener/recvfrom");
res = sendto(cnx->local_endpoint, data, res, 0,
&cnx->client_addr, cnx->addrlen);
cnx->last_active = time(NULL);
fprintf(stderr, "sendto %d to\n", res);
}
/* returns date at which this socket times out. */
int udp_timeout(struct connection* cnx)
{
if (cnx->type != SOCK_DGRAM) return 0; /* Not a UDP connection */
return cnx->proto->udp_timeout + cnx->last_active;
mark_active(cnx);
}

View File

@ -1,7 +1,10 @@
#ifndef UDPLISTENER_H
#define UDPLISTENER_H
#include "collection.h"
#include "processes.h"
#include "common.h"
/* UDP listener: upon incoming packet, find where it should go
* This is run in its own process and never returns.
@ -11,18 +14,16 @@ void udp_listener(struct listen_endpoint* endpoint, int num_endpoints, int activ
/* Process UDP coming from outside (client towards server)
* If it's a new source, probe; otherwise, forward to previous target
* Returns: >= 0 sockfd of newly allocated socket, for new connections
* Returns: newly allocate connections, for new connections
* -1 otherwise
* */
int udp_c2s_forward(int sockfd, cnx_collection* collection, int max_fd);
struct connection* udp_c2s_forward(int sockfd, struct loop_info* fd_info);
/* Process UDP coming from inside (server towards client) */
void udp_s2c_forward(struct connection* cnx);
/* returns how many seconds before socket times out. Negative if timed out
* already.
*/
int udp_timeout(struct connection* cnx);
void udp_init(struct loop_info* fd_info);
void udp_tidy(struct connection* cnx, struct loop_info* fd_info);
#endif /* UDPLISTENER_H */