mirror of
https://github.com/yrutschle/sslh.git
synced 2026-10-22 18:12:10 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ae2e62d25 | ||
|
|
8ec9799ca0 | ||
|
|
80ad31aec0 | ||
|
|
d6c714166a | ||
|
|
aa77922ffd | ||
|
|
2ee0088c5f | ||
|
|
f480eb6c7d | ||
|
|
6431bb7e35 | ||
|
|
6d6ea50066 | ||
|
|
71265a8477 | ||
|
|
0003680137 | ||
|
|
e8f0d3ea53 | ||
|
|
a5d00568b5 | ||
|
|
ffe9971624 | ||
|
|
1693436cc3 | ||
|
|
e42f670112 | ||
|
|
60df92c2b2 | ||
|
|
8ad32816a6 | ||
|
|
677e385fec | ||
|
|
b6db83a701 | ||
|
|
3a17bd6832 | ||
|
|
94911c1c2a | ||
|
|
d7889588da | ||
|
|
ad2b595280 | ||
|
|
9df1ab8404 | ||
|
|
10fb0bce6f | ||
|
|
eb53c45351 | ||
|
|
a1cc399ae5 | ||
|
|
108a9780d8 | ||
|
|
9228171eb0 | ||
|
|
8ce2b2ea05 | ||
|
|
4c132e3c8d | ||
|
|
cfd0163a5b | ||
|
|
5cf591a254 | ||
|
|
c179d9a57b | ||
|
|
336e8bb9d1 | ||
|
|
0ada00474b | ||
|
|
95e8f5731c | ||
|
|
dfd9e14866 | ||
|
|
552723cc5f | ||
|
|
09aaf39e25 | ||
|
|
7acf9627ee | ||
|
|
b8e63a4d9d | ||
|
|
f451cc8bed | ||
|
|
a43dd11fc9 | ||
|
|
3a61c8b0b1 | ||
|
|
0c928fedbb | ||
|
|
1a6ba5edc0 | ||
|
|
83be63d282 | ||
|
|
3ab2acdafa | ||
|
|
c8c6688d16 | ||
|
|
9a85efded5 | ||
|
|
7bf3e12c30 | ||
|
|
bc72c4ac42 | ||
|
|
fc9cb27746 | ||
|
|
2c3518beb4 | ||
|
|
141e4b4f83 | ||
|
|
0fb4c6b2ad | ||
|
|
d9541392f8 | ||
|
|
24cd3d0974 | ||
|
|
d26eab728c | ||
|
|
53e00c27ff | ||
|
|
063169c167 | ||
|
|
79ca4df0d7 | ||
|
|
3db5e127fd | ||
|
|
981091e39d | ||
|
|
0fc6bc8d12 | ||
|
|
b36fc73b7a | ||
|
|
a7f0c456ab | ||
|
|
7808a3a766 | ||
|
|
f5b1b881a4 | ||
|
|
f8a16c7a29 | ||
|
|
b33c65ed53 | ||
|
|
2f8e635b67 | ||
|
|
b8851d6714 | ||
|
|
e33124718e | ||
|
|
85b94c3259 | ||
|
|
4e790e074f | ||
|
|
6ca1ee7bfd | ||
|
|
e66e443d5e | ||
|
|
faa928b75a | ||
|
|
2ad99fd36f | ||
|
|
64485d7a58 | ||
|
|
5b756ebd0a | ||
|
|
021eb836e4 | ||
|
|
cb90cc97ae | ||
|
|
338daafe87 | ||
|
|
9fcbe8c7ea | ||
|
|
2a7bafdd7f | ||
|
|
3f5d9a4168 | ||
|
|
1f98b97756 | ||
|
|
09d11e3bc8 | ||
|
|
2a70470f13 | ||
|
|
2544f20bdf | ||
|
|
60b11e4964 | ||
|
|
b7fafb5039 | ||
|
|
b56f302b85 | ||
|
|
684c9afcc6 | ||
|
|
a3df50f31f | ||
|
|
d243d36add | ||
|
|
74767cb781 | ||
|
|
2a76b520d5 | ||
|
|
0929d39a34 | ||
|
|
f4d2a8d2ad | ||
|
|
aa06261d70 | ||
|
|
dd900ebf3e | ||
|
|
21f524f711 | ||
|
|
1e65088b7e | ||
|
|
6cc33820d1 | ||
|
|
00d5872aa1 | ||
|
|
7d561af423 | ||
|
|
cce42c6882 | ||
|
|
b0f4e24ce0 | ||
|
|
078827ad3f | ||
|
|
4413284420 | ||
|
|
70a2ea926e | ||
|
|
e4a4e04bf8 | ||
|
|
067f5d7646 | ||
|
|
718fe0e2e9 | ||
|
|
864b7badd2 | ||
|
|
234c088324 | ||
|
|
f02ce3821c | ||
|
|
e492c2808c | ||
|
|
a4d2a53a47 | ||
|
|
efba558967 | ||
|
|
b4cb910438 | ||
|
|
975560aa4a | ||
|
|
63a83cf041 | ||
|
|
245fd26459 | ||
|
|
9cc47a157d | ||
|
|
09b5c84d34 | ||
|
|
e6cb3596d1 | ||
|
|
38447c8158 | ||
|
|
8f39c106e1 | ||
|
|
414ed7de11 | ||
|
|
1b9937b293 | ||
|
|
1814bcb43c | ||
|
|
b3f48d9876 | ||
|
|
555005f455 | ||
|
|
e511534b57 | ||
|
|
e5cb33fcb7 | ||
|
|
e7d3133ba5 | ||
|
|
0b6cc0d909 | ||
|
|
8af039d3eb | ||
|
|
8758a298ba | ||
|
|
2cb424c646 | ||
|
|
24612835c3 | ||
|
|
b09c3aab31 | ||
|
|
b108809a78 | ||
|
|
389ab9fbff | ||
|
|
717c285b31 | ||
|
|
ab3324be47 | ||
|
|
4cbaf447b5 | ||
|
|
ca461ea077 | ||
|
|
8fdaf6eb08 | ||
|
|
5886bd2d43 | ||
|
|
77ef29358d | ||
|
|
9475d9689b | ||
|
|
fbebdaf66c | ||
|
|
fecfb170c8 | ||
|
|
b988540105 | ||
|
|
3aefaf3004 | ||
|
|
ba945f1a8f | ||
|
|
eb3d3be3ab | ||
|
|
66f85dc608 | ||
|
|
3469f56012 | ||
|
|
7c35ef8528 | ||
|
|
3bad96865d | ||
|
|
728181109c | ||
|
|
2192b28303 | ||
|
|
77a74f0c52 | ||
|
|
48164c4d77 |
No files matched your search
@@ -1,3 +1,69 @@
|
||||
v1.20: 20NOV2018
|
||||
Added support for socks5 protocol (Eugene Protozanov)
|
||||
|
||||
New probing method:
|
||||
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
|
||||
subsequent probes. The configuration file needed to
|
||||
take that into account.
|
||||
|
||||
Now, all probes are tried each time new data is
|
||||
found. If any probe matches, use it. If at least one
|
||||
probe requires more data, wait for more. If all
|
||||
probes failed, connect to the last one. So the only
|
||||
thing to know when writing the configuration file is
|
||||
that 'anyprot' needs to be last.
|
||||
|
||||
Test suite heavily refactored; `t` uses `test.cfg`
|
||||
to decide which probes to test and all setup is
|
||||
automatic; probes get tested with 'fast' (entire
|
||||
first message in one packet) and 'slow' (one byte at
|
||||
a time); when SNI/ALPN are defined, all combinations
|
||||
are tested.
|
||||
|
||||
Old 'tls' probe removed, 'sni_alpn' probe renamed as 'tls'.
|
||||
You'll need to change 'sni_alpn' to 'tls' in
|
||||
your configuration file, if ever you used it.
|
||||
|
||||
v1.19: 20JAN2018
|
||||
Added 'syslog_facility' configuration option to
|
||||
specify where to log.
|
||||
|
||||
TLS now supports SNI and ALPN (Travis Burtrum),
|
||||
including support for Let's Encrypt challenges
|
||||
(Jonathan McCrohan)
|
||||
|
||||
ADB probe. (Mike Frysinger)
|
||||
|
||||
Added per-protocol 'fork' option. (Oleg Oshmyan)
|
||||
|
||||
Added chroot option. (Mike Frysinger)
|
||||
|
||||
A truckload of bug fixes and documentation
|
||||
improvements (Various contributors)
|
||||
|
||||
v1.18: 29MAR2016
|
||||
Added USELIBPCRE to make use of regex engine
|
||||
optional.
|
||||
|
||||
Added support for RFC4366 SNI and RFC7301 ALPN
|
||||
(Travis Burtrum)
|
||||
|
||||
Changed connection log to include the name of the probe that
|
||||
triggered.
|
||||
|
||||
Changed configuration file format: 'probe' field is
|
||||
no longer required, 'name' field can now contain
|
||||
'tls' or 'regex', with corresponding options (see
|
||||
example.cfg)
|
||||
Added 'log_level' option to each protocol, which
|
||||
allows to turn off generation of log at each
|
||||
connection.
|
||||
Added 'keepalive' option.
|
||||
|
||||
v1.17: 09MAR2015
|
||||
Support RFC5952-style IPv6 addresses, e.g. [::]:443.
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
# Configuration
|
||||
|
||||
VERSION=$(shell ./genver.sh -r)
|
||||
ENABLE_REGEX=1 # Enable regex probes
|
||||
USELIBCONFIG=1 # Use libconfig? (necessary to use configuration files)
|
||||
USELIBPCRE=1 # Use libpcre? (needed for regex on musl)
|
||||
USELIBWRAP?= # Use libwrap?
|
||||
USELIBCAP= # Use libcap?
|
||||
USESYSTEMD= # Make use of systemd socket activation
|
||||
COV_TEST= # Perform test coverage?
|
||||
PREFIX=/usr/local
|
||||
PREFIX?=/usr
|
||||
BINDIR?=$(PREFIX)/sbin
|
||||
MANDIR?=$(PREFIX)/share/man/man8
|
||||
|
||||
MAN=sslh.8.gz # man page name
|
||||
|
||||
@@ -20,13 +25,24 @@ CC ?= gcc
|
||||
CFLAGS ?=-Wall -g $(CFLAGS_COV)
|
||||
|
||||
LIBS=
|
||||
OBJS=common.o sslh-main.o probe.o
|
||||
OBJS=common.o sslh-main.o probe.o tls.o
|
||||
|
||||
CONDITIONAL_TARGETS=
|
||||
|
||||
ifneq ($(strip $(USELIBWRAP)),)
|
||||
LIBS:=$(LIBS) -lwrap
|
||||
CPPFLAGS+=-DLIBWRAP
|
||||
endif
|
||||
|
||||
ifneq ($(strip $(ENABLE_REGEX)),)
|
||||
CPPFLAGS+=-DENABLE_REGEX
|
||||
endif
|
||||
|
||||
ifneq ($(strip $(USELIBPCRE)),)
|
||||
CPPFLAGS+=-DLIBPCRE
|
||||
LIBS:=$(LIBS) -lpcreposix
|
||||
endif
|
||||
|
||||
ifneq ($(strip $(USELIBCONFIG)),)
|
||||
LIBS:=$(LIBS) -lconfig
|
||||
CPPFLAGS+=-DLIBCONFIG
|
||||
@@ -37,9 +53,16 @@ ifneq ($(strip $(USELIBCAP)),)
|
||||
CPPFLAGS+=-DLIBCAP
|
||||
endif
|
||||
|
||||
all: sslh $(MAN) echosrv
|
||||
ifneq ($(strip $(USESYSTEMD)),)
|
||||
LIBS:=$(LIBS) -lsystemd
|
||||
CPPFLAGS+=-DSYSTEMD
|
||||
CONDITIONAL_TARGETS+=systemd-sslh-generator
|
||||
endif
|
||||
|
||||
.c.o: *.h
|
||||
|
||||
all: sslh $(MAN) echosrv $(CONDITIONAL_TARGETS)
|
||||
|
||||
.c.o: *.h version.h
|
||||
$(CC) $(CFLAGS) $(CPPFLAGS) -c $<
|
||||
|
||||
version.h:
|
||||
@@ -47,6 +70,8 @@ version.h:
|
||||
|
||||
sslh: sslh-fork sslh-select
|
||||
|
||||
$(OBJS): version.h
|
||||
|
||||
sslh-fork: version.h $(OBJS) sslh-fork.o Makefile common.h
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o sslh-fork sslh-fork.o $(OBJS) $(LIBS)
|
||||
#strip sslh-fork
|
||||
@@ -55,8 +80,11 @@ sslh-select: version.h $(OBJS) sslh-select.o Makefile common.h
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o sslh-select sslh-select.o $(OBJS) $(LIBS)
|
||||
#strip sslh-select
|
||||
|
||||
echosrv: $(OBJS) echosrv.o
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o echosrv echosrv.o probe.o common.o $(LIBS)
|
||||
systemd-sslh-generator: systemd-sslh-generator.o
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o systemd-sslh-generator systemd-sslh-generator.o -lconfig
|
||||
|
||||
echosrv: version.h $(OBJS) echosrv.o
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o echosrv echosrv.o probe.o common.o tls.o $(LIBS)
|
||||
|
||||
$(MAN): sslh.pod Makefile
|
||||
pod2man --section=8 --release=$(VERSION) --center=" " sslh.pod | gzip -9 - > $(MAN)
|
||||
@@ -68,8 +96,10 @@ release:
|
||||
|
||||
# generic install: install binary and man page
|
||||
install: sslh $(MAN)
|
||||
install -pD sslh-fork $(DESTDIR)$(PREFIX)/sbin/sslh
|
||||
install -pD -m 0644 $(MAN) $(DESTDIR)$(PREFIX)/share/man/man8/$(MAN)
|
||||
mkdir -p $(DESTDIR)/$(BINDIR)
|
||||
mkdir -p $(DESTDIR)/$(MANDIR)
|
||||
install -p sslh-fork $(DESTDIR)/$(BINDIR)/sslh
|
||||
install -p -m 0644 $(MAN) $(DESTDIR)/$(MANDIR)/$(MAN)
|
||||
|
||||
# "extended" install for Debian: install startup script
|
||||
install-debian: install sslh $(MAN)
|
||||
@@ -78,14 +108,14 @@ install-debian: install sslh $(MAN)
|
||||
update-rc.d sslh defaults
|
||||
|
||||
uninstall:
|
||||
rm -f $(DESTDIR)$(PREFIX)/sbin/sslh $(DESTDIR)$(PREFIX)/share/man/man8/$(MAN) $(DESTDIR)/etc/init.d/sslh $(DESTDIR)/etc/default/sslh
|
||||
rm -f $(DESTDIR)$(BINDIR)/sslh $(DESTDIR)$(MANDIR)/$(MAN) $(DESTDIR)/etc/init.d/sslh $(DESTDIR)/etc/default/sslh
|
||||
update-rc.d sslh remove
|
||||
|
||||
distclean: clean
|
||||
rm -f tags cscope.*
|
||||
|
||||
clean:
|
||||
rm -f sslh-fork sslh-select echosrv version.h $(MAN) *.o *.gcov *.gcno *.gcda *.png *.html *.css *.info
|
||||
rm -f sslh-fork sslh-select echosrv version.h $(MAN) systemd-sslh-generator *.o *.gcov *.gcno *.gcda *.png *.html *.css *.info
|
||||
|
||||
tags:
|
||||
ctags --globals -T *.[ch]
|
||||
@@ -96,4 +126,3 @@ cscope:
|
||||
|
||||
test:
|
||||
./t
|
||||
|
||||
@@ -59,6 +59,8 @@ of the Makefile:
|
||||
file. You will need `libconfig` headers to compile
|
||||
(`libconfig8-dev` in Debian).
|
||||
|
||||
* `USESYSTEMD` compiles support for using systemd socket activation.
|
||||
You will need `systemd` headers to compile (`systemd-devel` in Fedora).
|
||||
|
||||
Binaries
|
||||
--------
|
||||
@@ -133,7 +135,7 @@ i.e. that that is not `127.0.0.1` in the output of
|
||||
Libwrap support
|
||||
---------------
|
||||
|
||||
Sslh can optionnaly perform `libwrap` checks for the sshd
|
||||
Sslh can optionally perform `libwrap` checks for the sshd
|
||||
service: because the connection to `sshd` will be coming
|
||||
locally from `sslh`, `sshd` cannot determine the IP of the
|
||||
client.
|
||||
@@ -143,10 +145,10 @@ OpenVPN support
|
||||
|
||||
OpenVPN clients connecting to OpenVPN running with
|
||||
`-port-share` reportedly take more than one second between
|
||||
the time the TCP connexion is established and the time they
|
||||
the time the TCP connection is established and the time they
|
||||
send the first data packet. This results in `sslh` with
|
||||
default settings timing out and assuming an SSH connexion.
|
||||
To support OpenVPN connexions reliably, it is necessary to
|
||||
default settings timing out and assuming an SSH connection.
|
||||
To support OpenVPN connections reliably, it is necessary to
|
||||
increase `sslh`'s timeout to 5 seconds.
|
||||
|
||||
Instead of using OpenVPN's port sharing, it is more reliable
|
||||
@@ -203,11 +205,11 @@ and `CAP_NET_ADMIN` for transparent proxying (see
|
||||
You can use the `setcap(8)` utility to give these capabilities
|
||||
to the executable:
|
||||
|
||||
# setcap cap_net_bind_service,cap_net_admin+pe sslh-select
|
||||
sudo setcap cap_net_bind_service,cap_net_admin+pe sslh-select
|
||||
|
||||
Then you can run sslh-select as an unpriviledged user, e.g.:
|
||||
|
||||
$ sslh-select -p myname:443 --ssh localhost:22 --ssl localhost:443
|
||||
sslh-select -p myname:443 --ssh localhost:22 --ssl localhost:443
|
||||
|
||||
Caveat: `CAP_NET_ADMIN` does give sslh too many rights, e.g.
|
||||
configuring the interface. If you're not going to use
|
||||
@@ -217,7 +219,7 @@ Transparent proxy support
|
||||
-------------------------
|
||||
|
||||
On Linux and FreeBSD you can use the `--transparent` option to
|
||||
request transparent proying. This means services behind `sslh`
|
||||
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
|
||||
@@ -229,35 +231,70 @@ Linux:
|
||||
give it `CAP_NET_ADMIN` capabilities (see appropriate chapter)
|
||||
or run it as root (but don't do that).
|
||||
|
||||
The firewalling tables also need to be adjusted as follow.
|
||||
The example connects to HTTPS on 4443 -- adapt to your needs ;
|
||||
I don't think it is possible to have `httpd` listen to 443 in
|
||||
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:
|
||||
|
||||
# iptables -t mangle -N SSLH
|
||||
# iptables -t mangle -A OUTPUT --protocol tcp --out-interface eth0 --sport 22 --jump SSLH
|
||||
# iptables -t mangle -A OUTPUT --protocol tcp --out-interface eth0 --sport 4443 --jump SSLH
|
||||
# iptables -t mangle -A SSLH --jump MARK --set-mark 0x1
|
||||
# iptables -t mangle -A SSLH --jump ACCEPT
|
||||
# ip rule add fwmark 0x1 lookup 100
|
||||
# ip route add local 0.0.0.0/0 dev lo table 100
|
||||
# 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:
|
||||
|
||||
# ip6tables -t mangle -N SSLH
|
||||
# ip6tables -t mangle -A OUTPUT --protocol tcp --out-interface eth0 --sport 22 --jump SSLH
|
||||
# ip6tables -t mangle -A OUTPUT --protocol tcp --out-interface eth0 --sport 4443 --jump SSLH
|
||||
# ip6tables -t mangle -A SSLH --jump MARK --set-mark 0x1
|
||||
# ip6tables -t mangle -A SSLH --jump ACCEPT
|
||||
# ip -6 rule add fwmark 0x1 lookup 100
|
||||
# ip -6 route add local ::/0 dev lo table 100
|
||||
# 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
|
||||
|
||||
Note that these rules will prevent from connecting directly
|
||||
to ssh on the port 22, as packets coming out of sshd will be
|
||||
tagged. If you need to retain direct access to ssh on port
|
||||
22 as well as through sslh, you can make sshd listen to
|
||||
22 AND another port (e.g. 2222), and change the above rules
|
||||
accordingly.
|
||||
# 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:
|
||||
|
||||
@@ -303,6 +340,90 @@ explicit IP addresses (or names):
|
||||
This will not work:
|
||||
|
||||
sslh --listen 192.168.0.1:443 --ssh 127.0.0.1:22 --ssl 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.
|
||||
|
||||
Systemd Socket Activation
|
||||
-------------------------
|
||||
If compiled with `USESYSTEMD` then it is possible to activate
|
||||
the service on demand and avoid running any code as root.
|
||||
|
||||
In this mode any listen configuration options are ignored and
|
||||
the sockets are passed by systemd to the service.
|
||||
|
||||
Example socket unit:
|
||||
|
||||
[Unit]
|
||||
Before=sslh.service
|
||||
|
||||
[Socket]
|
||||
ListenStream=1.2.3.4:443
|
||||
ListenStream=5.6.7.8:444
|
||||
ListenStream=9.10.11.12:445
|
||||
FreeBind=true
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
|
||||
Example service unit:
|
||||
|
||||
[Unit]
|
||||
PartOf=sslh.socket
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/sbin/sslh -v -f --ssh 127.0.0.1:22 --ssl 127.0.0.1:443
|
||||
KillMode=process
|
||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN CAP_SETGID CAP_SETUID
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=full
|
||||
ProtectHome=true
|
||||
User=sslh
|
||||
|
||||
|
||||
With this setup only the socket needs to be enabled. The sslh service
|
||||
will be started on demand and does not need to run as root to bind the
|
||||
sockets as systemd has already bound and passed them over. If the sslh
|
||||
service is started on its own without the sockets being passed by systemd
|
||||
then it will look to use those defined on the command line or config
|
||||
file as usual. Any number of ListenStreams can be defined in the socket
|
||||
file and systemd will pass them all over to sslh to use as usual.
|
||||
|
||||
To avoid inconsistency between starting via socket and starting directly
|
||||
via the service Requires=sslh.socket can be added to the service unit to
|
||||
mandate the use of the socket configuration.
|
||||
|
||||
Rather than overwriting the entire socket file drop in values can be placed
|
||||
in /etc/systemd/system/sslh.socket.d/<name>.conf with additional ListenStream
|
||||
values that will be merged.
|
||||
|
||||
In addition to the above with manual .socket file configuration there is an
|
||||
optional systemd generator which can be compiled - systemd-sslh-generator
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
the new dynamic socket unit.
|
||||
|
||||
Fail2ban
|
||||
--------
|
||||
@@ -310,8 +431,8 @@ Fail2ban
|
||||
If using transparent proxying, just use the standard ssh
|
||||
rules. If you can't or don't want to use transparent
|
||||
proxying, you can set `fail2ban` rules to block repeated ssh
|
||||
connections from a same IP address (obviously this depends
|
||||
on the site, there might be legimite reasons you would get
|
||||
connections from an IP address (obviously this depends
|
||||
on the site, there might be legitimate reasons you would get
|
||||
many connections to ssh from the same IP address...)
|
||||
|
||||
See example files in scripts/fail2ban.
|
||||
|
||||
@@ -6,9 +6,10 @@ foreground: false;
|
||||
inetd: false;
|
||||
numeric: false;
|
||||
transparent: false;
|
||||
timeout: "2";
|
||||
timeout: 2;
|
||||
user: "nobody";
|
||||
pidfile: "/var/run/sslh.pid";
|
||||
chroot: "/var/empty";
|
||||
|
||||
|
||||
# Change hostname with your external address name.
|
||||
@@ -19,11 +20,11 @@ listen:
|
||||
|
||||
protocols:
|
||||
(
|
||||
{ name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; probe: "builtin"; },
|
||||
{ name: "openvpn"; host: "localhost"; port: "1194"; probe: "builtin"; },
|
||||
{ name: "xmpp"; host: "localhost"; port: "5222"; probe: "builtin"; },
|
||||
{ name: "http"; host: "localhost"; port: "80"; probe: "builtin"; },
|
||||
{ name: "ssl"; host: "localhost"; port: "443"; probe: "builtin"; },
|
||||
{ name: "anyprot"; host: "localhost"; port: "443"; probe: "builtin"; }
|
||||
{ name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; fork: true; },
|
||||
{ name: "openvpn"; host: "localhost"; port: "1194"; },
|
||||
{ name: "xmpp"; host: "localhost"; port: "5222"; },
|
||||
{ name: "http"; host: "localhost"; port: "80"; },
|
||||
{ name: "ssl"; host: "localhost"; port: "443"; log_level: 0; },
|
||||
{ name: "anyprot"; host: "localhost"; port: "443"; }
|
||||
);
|
||||
|
||||
@@ -4,20 +4,32 @@
|
||||
* No code here should assume whether sockets are blocking or not.
|
||||
**/
|
||||
|
||||
#define SYSLOG_NAMES
|
||||
#define _GNU_SOURCE
|
||||
#include <stddef.h>
|
||||
#include <stdarg.h>
|
||||
#include <grp.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <netinet/in.h>
|
||||
|
||||
#include "common.h"
|
||||
#include "probe.h"
|
||||
|
||||
/* Added to make the code compilable under CYGWIN
|
||||
/* Added to make the code compilable under CYGWIN
|
||||
* */
|
||||
#ifndef SA_NOCLDWAIT
|
||||
#define SA_NOCLDWAIT 0
|
||||
#endif
|
||||
|
||||
/*
|
||||
/* Make use of systemd socket activation
|
||||
* */
|
||||
#ifdef SYSTEMD
|
||||
#include <systemd/sd-daemon.h>
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Settings that depend on the command line. They're set in main(), but also
|
||||
* used in other places in common.c, and it'd be heavy-handed to pass it all as
|
||||
* parameters
|
||||
@@ -29,7 +41,7 @@ int foreground = 0;
|
||||
int background = 0;
|
||||
int transparent = 0;
|
||||
int numeric = 0;
|
||||
const char *user_name, *pid_file;
|
||||
const char *user_name, *pid_file, *chroot_path, *facility = "auth";
|
||||
|
||||
struct addrinfo *addr_listen = NULL; /* what addresses do we listen to? */
|
||||
|
||||
@@ -38,20 +50,50 @@ struct addrinfo *addr_listen = NULL; /* what addresses do we listen to? */
|
||||
int allow_severity =0, deny_severity = 0;
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
CR_DIE,
|
||||
CR_WARN
|
||||
} CR_ACTION;
|
||||
|
||||
/* check result and die, printing the offending address and error */
|
||||
void check_res_dumpdie(int res, struct addrinfo *addr, char* syscall)
|
||||
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",
|
||||
sprintaddr(buf, sizeof(buf), addr),
|
||||
syscall,
|
||||
fprintf(stderr, "%s:%s: %s\n",
|
||||
sprintaddr(buf, sizeof(buf), addr),
|
||||
syscall,
|
||||
strerror(errno));
|
||||
exit(1);
|
||||
|
||||
if (act == CR_DIE)
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
int get_fd_sockets(int *sockfd[])
|
||||
{
|
||||
int sd = 0;
|
||||
|
||||
#ifdef SYSTEMD
|
||||
sd = sd_listen_fds(0);
|
||||
if (sd < 0) {
|
||||
fprintf(stderr, "sd_listen_fds(): %s\n", strerror(-sd));
|
||||
exit(1);
|
||||
}
|
||||
if (sd > 0) {
|
||||
int i;
|
||||
*sockfd = malloc(sd * sizeof(*sockfd[0]));
|
||||
CHECK_ALLOC(*sockfd, "malloc");
|
||||
for (i = 0; i < sd; i++) {
|
||||
(*sockfd)[i] = SD_LISTEN_FDS_START + i;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return sd;
|
||||
}
|
||||
|
||||
/* Starts listening sockets on specified addresses.
|
||||
* IN: addr[], num_addr
|
||||
* OUT: *sockfd[] pointer to newly-allocated array of file descriptors
|
||||
@@ -64,14 +106,27 @@ int start_listen_sockets(int *sockfd[], struct addrinfo *addr_list)
|
||||
struct addrinfo *addr;
|
||||
int i, res, one;
|
||||
int num_addr = 0;
|
||||
int sd_socks = 0;
|
||||
|
||||
sd_socks = get_fd_sockets(sockfd);
|
||||
|
||||
if (sd_socks > 0) {
|
||||
return sd_socks;
|
||||
}
|
||||
|
||||
for (addr = addr_list; addr; addr = addr->ai_next)
|
||||
num_addr++;
|
||||
|
||||
if (num_addr == 0) {
|
||||
fprintf(stderr, "FATAL: No available addresses.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (verbose)
|
||||
fprintf(stderr, "listening to %d addresses\n", num_addr);
|
||||
|
||||
*sockfd = malloc(num_addr * sizeof(*sockfd[0]));
|
||||
CHECK_ALLOC(*sockfd, "malloc");
|
||||
|
||||
for (i = 0, addr = addr_list; i < num_addr && addr; i++, addr = addr->ai_next) {
|
||||
if (!addr) {
|
||||
@@ -81,28 +136,80 @@ int start_listen_sockets(int *sockfd[], struct addrinfo *addr_list)
|
||||
saddr = (struct sockaddr_storage*)addr->ai_addr;
|
||||
|
||||
(*sockfd)[i] = socket(saddr->ss_family, SOCK_STREAM, 0);
|
||||
check_res_dumpdie((*sockfd)[i], addr, "socket");
|
||||
check_res_dump(CR_DIE, (*sockfd)[i], addr, "socket");
|
||||
|
||||
one = 1;
|
||||
res = setsockopt((*sockfd)[i], SOL_SOCKET, SO_REUSEADDR, (char*)&one, sizeof(one));
|
||||
check_res_dumpdie(res, addr, "setsockopt(SO_REUSEADDR)");
|
||||
check_res_dump(CR_DIE, res, addr, "setsockopt(SO_REUSEADDR)");
|
||||
|
||||
if (addr->ai_flags & SO_KEEPALIVE) {
|
||||
res = setsockopt((*sockfd)[i], SOL_SOCKET, SO_KEEPALIVE, (char*)&one, sizeof(one));
|
||||
check_res_dump(CR_DIE, res, addr, "setsockopt(SO_KEEPALIVE)");
|
||||
}
|
||||
|
||||
if (IP_FREEBIND) {
|
||||
res = setsockopt((*sockfd)[i], IPPROTO_IP, IP_FREEBIND, (char*)&one, sizeof(one));
|
||||
check_res_dumpdie(res, addr, "setsockopt(IP_FREEBIND)");
|
||||
check_res_dump(CR_WARN, res, addr, "setsockopt(IP_FREEBIND)");
|
||||
}
|
||||
|
||||
if (addr->ai_addr->sa_family == AF_INET6) {
|
||||
res = setsockopt((*sockfd)[i], IPPROTO_IPV6, IPV6_V6ONLY, (char*)&one, sizeof(one));
|
||||
check_res_dump(CR_WARN, res, addr, "setsockopt(IPV6_V6ONLY)");
|
||||
}
|
||||
|
||||
res = bind((*sockfd)[i], addr->ai_addr, addr->ai_addrlen);
|
||||
check_res_dumpdie(res, addr, "bind");
|
||||
check_res_dump(CR_DIE, res, addr, "bind");
|
||||
|
||||
res = listen ((*sockfd)[i], 50);
|
||||
check_res_dumpdie(res, addr, "listen");
|
||||
check_res_dump(CR_DIE, res, addr, "listen");
|
||||
|
||||
}
|
||||
|
||||
return num_addr;
|
||||
}
|
||||
|
||||
|
||||
/* returns 1 if given address is on the local machine: iterate through all
|
||||
* network interfaces and check their addresses */
|
||||
int is_same_machine(struct addrinfo* from)
|
||||
{
|
||||
struct ifaddrs *ifaddrs_p = NULL, *ifa;
|
||||
int match = 0;
|
||||
|
||||
getifaddrs(&ifaddrs_p);
|
||||
|
||||
for (ifa = ifaddrs_p; ifa != NULL; ifa = ifa->ifa_next)
|
||||
{
|
||||
if (!ifa->ifa_addr)
|
||||
continue;
|
||||
if (from->ai_addr->sa_family == ifa->ifa_addr->sa_family)
|
||||
{
|
||||
int family = ifa->ifa_addr->sa_family;
|
||||
if (family == AF_INET)
|
||||
{
|
||||
struct sockaddr_in *from_addr = (struct sockaddr_in*)from->ai_addr;
|
||||
struct sockaddr_in *ifa_addr = (struct sockaddr_in*)ifa->ifa_addr;
|
||||
if (from_addr->sin_addr.s_addr == ifa_addr->sin_addr.s_addr) {
|
||||
match = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (family == AF_INET6)
|
||||
{
|
||||
struct sockaddr_in6 *from_addr = (struct sockaddr_in6*)from->ai_addr;
|
||||
struct sockaddr_in6 *ifa_addr = (struct sockaddr_in6*)ifa->ifa_addr;
|
||||
if (!memcmp(from_addr->sin6_addr.s6_addr, ifa_addr->sin6_addr.s6_addr, 16)) {
|
||||
match = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
freeifaddrs(ifaddrs_p);
|
||||
return match;
|
||||
}
|
||||
|
||||
|
||||
/* Transparent proxying: bind the peer address of fd to the peer address of
|
||||
* fd_from */
|
||||
#define IP_TRANSPARENT 19
|
||||
@@ -120,6 +227,11 @@ int bind_peer(int fd, int fd_from)
|
||||
* got here */
|
||||
res = getpeername(fd_from, from.ai_addr, &from.ai_addrlen);
|
||||
CHECK_RES_RETURN(res, "getpeername");
|
||||
|
||||
/* if the destination is the same machine, there's no need to do bind */
|
||||
if (is_same_machine(&from))
|
||||
return 0;
|
||||
|
||||
#ifndef IP_BINDANY /* use IP_TRANSPARENT */
|
||||
res = setsockopt(fd, IPPROTO_IP, IP_TRANSPARENT, &trans, sizeof(trans));
|
||||
CHECK_RES_DIE(res, "setsockopt");
|
||||
@@ -141,7 +253,7 @@ int bind_peer(int fd, int fd_from)
|
||||
}
|
||||
|
||||
/* Connect to first address that works and returns a file descriptor, or -1 if
|
||||
* none work.
|
||||
* none work.
|
||||
* If transparent proxying is on, use fd_from peer address on external address
|
||||
* of new file descriptor. */
|
||||
int connect_addr(struct connection *cnx, int fd_from)
|
||||
@@ -149,7 +261,7 @@ int connect_addr(struct connection *cnx, int fd_from)
|
||||
struct addrinfo *a, from;
|
||||
struct sockaddr_storage ss;
|
||||
char buf[NI_MAXHOST];
|
||||
int fd, res;
|
||||
int fd, res, one;
|
||||
|
||||
memset(&from, 0, sizeof(from));
|
||||
from.ai_addr = (struct sockaddr*)&ss;
|
||||
@@ -162,8 +274,8 @@ int connect_addr(struct connection *cnx, int fd_from)
|
||||
/* When transparent, make sure both connections use the same address family */
|
||||
if (transparent && a->ai_family != from.ai_addr->sa_family)
|
||||
continue;
|
||||
if (verbose)
|
||||
fprintf(stderr, "connecting to %s family %d len %d\n",
|
||||
if (verbose)
|
||||
fprintf(stderr, "connecting to %s family %d len %d\n",
|
||||
sprintaddr(buf, sizeof(buf), a),
|
||||
a->ai_addr->sa_family, a->ai_addrlen);
|
||||
|
||||
@@ -179,10 +291,15 @@ int connect_addr(struct connection *cnx, int fd_from)
|
||||
}
|
||||
res = connect(fd, a->ai_addr, a->ai_addrlen);
|
||||
if (res == -1) {
|
||||
log_message(LOG_ERR, "forward to %s failed:connect: %s\n",
|
||||
log_message(LOG_ERR, "forward to %s failed:connect: %s\n",
|
||||
cnx->proto->description, strerror(errno));
|
||||
close(fd);
|
||||
} else {
|
||||
if (cnx->proto->keepalive) {
|
||||
one = 1;
|
||||
res = setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, (char*)&one, sizeof(one));
|
||||
CHECK_RES_RETURN(res, "setsockopt(SO_KEEPALIVE)");
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
}
|
||||
@@ -191,20 +308,19 @@ int connect_addr(struct connection *cnx, int fd_from)
|
||||
}
|
||||
|
||||
/* Store some data to write to the queue later */
|
||||
int defer_write(struct queue *q, void* data, int data_size)
|
||||
int defer_write(struct queue *q, void* data, int data_size)
|
||||
{
|
||||
char *p;
|
||||
if (verbose)
|
||||
ptrdiff_t data_offset = q->deferred_data - q->begin_deferred_data;
|
||||
if (verbose)
|
||||
fprintf(stderr, "**** writing deferred on fd %d\n", q->fd);
|
||||
|
||||
p = realloc(q->begin_deferred_data, q->deferred_data_size + data_size);
|
||||
if (!p) {
|
||||
perror("realloc");
|
||||
exit(1);
|
||||
}
|
||||
p = realloc(q->begin_deferred_data, data_offset + q->deferred_data_size + data_size);
|
||||
CHECK_ALLOC(p, "realloc");
|
||||
|
||||
q->deferred_data = q->begin_deferred_data = p;
|
||||
p += q->deferred_data_size;
|
||||
q->begin_deferred_data = p;
|
||||
q->deferred_data = p + data_offset;
|
||||
p += data_offset + q->deferred_data_size;
|
||||
q->deferred_data_size += data_size;
|
||||
memcpy(p, data, data_size);
|
||||
|
||||
@@ -258,7 +374,7 @@ void dump_connection(struct connection *cnx)
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
/*
|
||||
* moves data from one fd to other
|
||||
*
|
||||
* returns number of bytes copied if success
|
||||
@@ -326,16 +442,16 @@ char* sprintaddr(char* buf, size_t size, struct addrinfo *a)
|
||||
int res;
|
||||
|
||||
res = getnameinfo(a->ai_addr, a->ai_addrlen,
|
||||
host, sizeof(host),
|
||||
serv, sizeof(serv),
|
||||
host, sizeof(host),
|
||||
serv, sizeof(serv),
|
||||
numeric ? NI_NUMERICHOST | NI_NUMERICSERV : 0 );
|
||||
|
||||
if (res) {
|
||||
log_message(LOG_ERR, "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),
|
||||
serv, sizeof(serv),
|
||||
host, sizeof(host),
|
||||
serv, sizeof(serv),
|
||||
NI_NUMERICHOST | NI_NUMERICSERV);
|
||||
/* should not fail but... */
|
||||
if (res) {
|
||||
@@ -350,21 +466,45 @@ char* sprintaddr(char* buf, size_t size, struct addrinfo *a)
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Turns a hostname and port (or service) into a list of struct addrinfo
|
||||
/* Turns a hostname and port (or service) into a list of struct addrinfo
|
||||
* returns 0 on success, -1 otherwise and logs error
|
||||
**/
|
||||
int resolve_split_name(struct addrinfo **out, const char* host, const char* serv)
|
||||
*/
|
||||
int resolve_split_name(struct addrinfo **out, const char* ct_host, const char* serv)
|
||||
{
|
||||
struct addrinfo hint;
|
||||
char *end;
|
||||
int res;
|
||||
char* host, *host_base;
|
||||
|
||||
memset(&hint, 0, sizeof(hint));
|
||||
hint.ai_family = PF_UNSPEC;
|
||||
hint.ai_socktype = SOCK_STREAM;
|
||||
|
||||
/* Copy parameter so not to clobber data in libconfig */
|
||||
res = asprintf(&host_base, "%s", ct_host);
|
||||
if (res == -1) {
|
||||
log_message(LOG_ERR, "asprintf: cannot allocate memory");
|
||||
return -1;
|
||||
}
|
||||
|
||||
host = host_base;
|
||||
|
||||
/* If it is a RFC-Compliant IPv6 address ("[1234::12]:443"), remove brackets
|
||||
* around IP address */
|
||||
if (host[0] == '[') {
|
||||
end = strrchr(host, ']');
|
||||
if (!end) {
|
||||
fprintf(stderr, "%s: no closing bracket in IPv6 address?\n", host);
|
||||
return -1;
|
||||
}
|
||||
host++; /* skip first bracket */
|
||||
*end = 0; /* remove last bracket */
|
||||
}
|
||||
|
||||
res = getaddrinfo(host, serv, &hint, out);
|
||||
if (res)
|
||||
log_message(LOG_ERR, "%s `%s:%s'\n", gai_strerror(res), host, serv);
|
||||
free(host_base);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -374,7 +514,7 @@ fullname: input string -- it gets clobbered
|
||||
*/
|
||||
void resolve_name(struct addrinfo **out, char* fullname)
|
||||
{
|
||||
char *serv, *host, *end;
|
||||
char *serv, *host;
|
||||
int res;
|
||||
|
||||
/* Find port */
|
||||
@@ -388,17 +528,6 @@ void resolve_name(struct addrinfo **out, char* fullname)
|
||||
|
||||
host = fullname;
|
||||
|
||||
/* If it is a RFC-Compliant IPv6 address ("[1234::12]:443"), remove brackets
|
||||
* around IP address */
|
||||
if (host[0] == '[') {
|
||||
end = strrchr(host, ']');
|
||||
if (!end) {
|
||||
fprintf(stderr, "%s: no closing bracket in IPv6 address?\n", host);
|
||||
}
|
||||
host++; /* skip first bracket */
|
||||
*end = 0; /* remove last bracket */
|
||||
}
|
||||
|
||||
res = resolve_split_name(out, host, serv);
|
||||
if (res) {
|
||||
fprintf(stderr, "%s `%s'\n", gai_strerror(res), fullname);
|
||||
@@ -431,11 +560,14 @@ void log_connection(struct connection *cnx)
|
||||
local[MAX_NAMELENGTH], target[MAX_NAMELENGTH];
|
||||
int res;
|
||||
|
||||
if (cnx->proto->log_level < 1)
|
||||
return;
|
||||
|
||||
addr.ai_addr = (struct sockaddr*)&ss;
|
||||
addr.ai_addrlen = sizeof(ss);
|
||||
|
||||
res = getpeername(cnx->q[0].fd, addr.ai_addr, &addr.ai_addrlen);
|
||||
if (res == -1) return; /* Can happen if connection drops before we get here.
|
||||
if (res == -1) return; /* Can happen if connection drops before we get here.
|
||||
In that case, don't log anything (there is no connection) */
|
||||
sprintaddr(peer, sizeof(peer), &addr);
|
||||
|
||||
@@ -454,7 +586,8 @@ void log_connection(struct connection *cnx)
|
||||
if (res == -1) return;
|
||||
sprintaddr(local, sizeof(local), &addr);
|
||||
|
||||
log_message(LOG_INFO, "connection from %s to %s forwarded from %s to %s\n",
|
||||
log_message(LOG_INFO, "%s:connection from %s to %s forwarded from %s to %s\n",
|
||||
cnx->proto->description,
|
||||
peer,
|
||||
service,
|
||||
local,
|
||||
@@ -471,16 +604,19 @@ void log_connection(struct connection *cnx)
|
||||
int check_access_rights(int in_socket, const char* service)
|
||||
{
|
||||
#ifdef LIBWRAP
|
||||
struct sockaddr peeraddr;
|
||||
socklen_t size = sizeof(peeraddr);
|
||||
union {
|
||||
struct sockaddr saddr;
|
||||
struct sockaddr_storage ss;
|
||||
} peer;
|
||||
socklen_t size = sizeof(peer);
|
||||
char addr_str[NI_MAXHOST], host[NI_MAXHOST];
|
||||
int res;
|
||||
|
||||
res = getpeername(in_socket, &peeraddr, &size);
|
||||
res = getpeername(in_socket, &peer.saddr, &size);
|
||||
CHECK_RES_RETURN(res, "getpeername");
|
||||
|
||||
/* extract peer address */
|
||||
res = getnameinfo(&peeraddr, size, addr_str, sizeof(addr_str), NULL, 0, NI_NUMERICHOST);
|
||||
res = getnameinfo(&peer.saddr, size, addr_str, sizeof(addr_str), NULL, 0, NI_NUMERICHOST);
|
||||
if (res) {
|
||||
if (verbose)
|
||||
fprintf(stderr, "getnameinfo(NI_NUMERICHOST):%s\n", gai_strerror(res));
|
||||
@@ -489,7 +625,7 @@ int check_access_rights(int in_socket, const char* service)
|
||||
/* extract peer name */
|
||||
strcpy(host, STRING_UNKNOWN);
|
||||
if (!numeric) {
|
||||
res = getnameinfo(&peeraddr, size, host, sizeof(host), NULL, 0, NI_NAMEREQD);
|
||||
res = getnameinfo(&peer.saddr, size, host, sizeof(host), NULL, 0, NI_NAMEREQD);
|
||||
if (res) {
|
||||
if (verbose)
|
||||
fprintf(stderr, "getnameinfo(NI_NAMEREQD):%s\n", gai_strerror(res));
|
||||
@@ -534,17 +670,26 @@ void setup_signals(void)
|
||||
|
||||
}
|
||||
|
||||
/* Open syslog connection with appropriate banner;
|
||||
/* 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;
|
||||
int res, fn;
|
||||
|
||||
name1 = strdup(bin_name);
|
||||
res = asprintf(&name2, "%s[%d]", basename(name1), getpid());
|
||||
CHECK_RES_DIE(res, "asprintf");
|
||||
openlog(name2, LOG_CONS, LOG_AUTH);
|
||||
free(name1);
|
||||
|
||||
for (fn = 0; facilitynames[fn].c_val != -1; fn++)
|
||||
if (strcmp(facilitynames[fn].c_name, facility) == 0)
|
||||
break;
|
||||
if (facilitynames[fn].c_val == -1) {
|
||||
fprintf(stderr, "Unknown facility %s\n", 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);
|
||||
@@ -601,33 +746,47 @@ void set_capabilities(void) {
|
||||
}
|
||||
|
||||
/* We don't want to run as root -- drop privileges if required */
|
||||
void drop_privileges(const char* user_name)
|
||||
void drop_privileges(const char* user_name, const char* chroot_path)
|
||||
{
|
||||
int res;
|
||||
struct passwd *pw = getpwnam(user_name);
|
||||
if (!pw) {
|
||||
fprintf(stderr, "%s: not found\n", user_name);
|
||||
exit(2);
|
||||
struct passwd *pw = NULL;
|
||||
|
||||
if (user_name) {
|
||||
pw = getpwnam(user_name);
|
||||
if (!pw) {
|
||||
fprintf(stderr, "%s: not found\n", user_name);
|
||||
exit(2);
|
||||
}
|
||||
if (verbose)
|
||||
fprintf(stderr, "turning into %s\n", user_name);
|
||||
}
|
||||
if (verbose)
|
||||
fprintf(stderr, "turning into %s\n", user_name);
|
||||
|
||||
set_keepcaps(1);
|
||||
if (chroot_path) {
|
||||
if (verbose)
|
||||
fprintf(stderr, "chrooting into %s\n", chroot_path);
|
||||
|
||||
/* remove extraneous groups in case we belong to several extra groups that
|
||||
* may have unwanted rights. If non-root when calling setgroups(), it
|
||||
* fails, which is fine because... we have no unwanted rights
|
||||
* (see POS36-C for security context)
|
||||
* */
|
||||
setgroups(0, NULL);
|
||||
res = chroot(chroot_path);
|
||||
CHECK_RES_DIE(res, "chroot");
|
||||
}
|
||||
|
||||
res = setgid(pw->pw_gid);
|
||||
CHECK_RES_DIE(res, "setgid");
|
||||
res = setuid(pw->pw_uid);
|
||||
CHECK_RES_DIE(res, "setuid");
|
||||
if (user_name) {
|
||||
set_keepcaps(1);
|
||||
|
||||
set_capabilities();
|
||||
set_keepcaps(0);
|
||||
/* remove extraneous groups in case we belong to several extra groups
|
||||
* that may have unwanted rights. If non-root when calling setgroups(),
|
||||
* it fails, which is fine because... we have no unwanted rights
|
||||
* (see POS36-C for security context)
|
||||
* */
|
||||
setgroups(0, NULL);
|
||||
|
||||
res = setgid(pw->pw_gid);
|
||||
CHECK_RES_DIE(res, "setgid");
|
||||
res = setuid(pw->pw_uid);
|
||||
CHECK_RES_DIE(res, "setuid");
|
||||
|
||||
set_capabilities();
|
||||
set_keepcaps(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Writes my PID */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#ifndef __COMMON_H_
|
||||
#define __COMMON_H_
|
||||
#ifndef COMMON_H
|
||||
#define COMMON_H
|
||||
|
||||
/* FD_SETSIZE is 64 on Cygwin, which is really low. Just redefining it is
|
||||
* enough for the macros to adapt (http://support.microsoft.com/kb/111855)
|
||||
@@ -37,16 +37,24 @@
|
||||
|
||||
#define CHECK_RES_DIE(res, str) \
|
||||
if (res == -1) { \
|
||||
fprintf(stderr, "%s:%d:", __FILE__, __LINE__); \
|
||||
perror(str); \
|
||||
exit(1); \
|
||||
}
|
||||
|
||||
#define CHECK_RES_RETURN(res, str) \
|
||||
if (res == -1) { \
|
||||
log_message(LOG_CRIT, "%s:%d:%s\n", str, errno, strerror(errno)); \
|
||||
log_message(LOG_CRIT, "%s:%d:%s:%d:%s\n", __FILE__, __LINE__, str, errno, strerror(errno)); \
|
||||
return res; \
|
||||
}
|
||||
|
||||
#define CHECK_ALLOC(a, str) \
|
||||
if (!a) { \
|
||||
fprintf(stderr, "%s:%d:", __FILE__, __LINE__); \
|
||||
perror(str); \
|
||||
exit(1); \
|
||||
}
|
||||
|
||||
#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0]))
|
||||
|
||||
#if 1
|
||||
@@ -102,7 +110,7 @@ void log_connection(struct connection *cnx);
|
||||
int check_access_rights(int in_socket, const char* service);
|
||||
void setup_signals(void);
|
||||
void setup_syslog(const char* bin_name);
|
||||
void drop_privileges(const char* user_name);
|
||||
void drop_privileges(const char* user_name, const char* chroot_path);
|
||||
void write_pid_file(const char* pidfile);
|
||||
void log_message(int type, char* msg, ...);
|
||||
void dump_connection(struct connection *cnx);
|
||||
@@ -118,7 +126,7 @@ extern int probing_timeout, verbose, inetd, foreground,
|
||||
extern struct sockaddr_storage addr_ssl, addr_ssh, addr_openvpn;
|
||||
extern struct addrinfo *addr_listen;
|
||||
extern const char* USAGE_STRING;
|
||||
extern const char* user_name, *pid_file;
|
||||
extern const char* user_name, *pid_file, *chroot_path, *facility;
|
||||
extern const char* server_type;
|
||||
|
||||
/* sslh-fork.c */
|
||||
|
||||
+74
-18
@@ -8,45 +8,101 @@ foreground: true;
|
||||
inetd: false;
|
||||
numeric: false;
|
||||
transparent: false;
|
||||
timeout: "2";
|
||||
timeout: 2;
|
||||
user: "nobody";
|
||||
pidfile: "/var/run/sslh.pid";
|
||||
chroot: "/var/empty";
|
||||
|
||||
# Specify which syslog facility to use (names for your
|
||||
# system are usually defined in /usr/include/*/sys/syslog.h
|
||||
# or equivalent)
|
||||
# Default is "auth"
|
||||
syslog_facility: "auth";
|
||||
|
||||
# List of interfaces on which we should listen
|
||||
# Options:
|
||||
listen:
|
||||
(
|
||||
{ host: "thelonious"; port: "443"; },
|
||||
{ host: "thelonious"; port: "8080"; }
|
||||
{ host: "thelonious"; port: "8080"; keepalive: true; }
|
||||
);
|
||||
|
||||
# List of protocols
|
||||
#
|
||||
# Each protocol entry consists of:
|
||||
# name: name of the protocol
|
||||
# service: (optional) libwrap service name (see hosts_access(5))
|
||||
# host: host name to connect that protocol
|
||||
# port: port number to connect that protocol
|
||||
# probe: "builtin" or a list of regular expressions
|
||||
# (can be left out, e.g. to use with on-timeout)
|
||||
#
|
||||
# sslh will try each probe in order they are declared, and
|
||||
# connect to the first that matches.
|
||||
# name: name of the probe. These are listed on the command
|
||||
# line (ssh -?), plus 'regex' and 'timeout'.
|
||||
|
||||
# service: (optional) libwrap service name (see hosts_access(5))
|
||||
# host, port: where to connect when this probe succeeds
|
||||
# log_level: 0 to turn off logging
|
||||
# 1 to log each incoming connection
|
||||
# keepalive: Should TCP keepalive be on or off for that
|
||||
# connection (default is off)
|
||||
# fork: Should a new process be forked for this protocol?
|
||||
# (only useful for sslh-select)
|
||||
#
|
||||
# Probe-specific options:
|
||||
# (sslh will try each probe in order they are declared, and
|
||||
# connect to the first that matches.)
|
||||
#
|
||||
# tls:
|
||||
# sni_hostnames: list of FQDN for that target
|
||||
# alpn_protocols: list of ALPN protocols for that target, see:
|
||||
# https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
|
||||
#
|
||||
# if both sni_hostnames AND alpn_protocols are specified, both must match
|
||||
#
|
||||
# if neither are set, it is just checked whether this is the TLS protocol or not
|
||||
#
|
||||
# Obviously set the most specific probes
|
||||
# first, and if you use TLS with no ALPN/SNI
|
||||
# set it as the last TLS probe
|
||||
# regex:
|
||||
# regex_patterns: list of patterns to match for
|
||||
# that target.
|
||||
#
|
||||
# You can specify several of 'regex' and 'tls'.
|
||||
|
||||
protocols:
|
||||
(
|
||||
{ name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; probe: "builtin"; },
|
||||
{ name: "openvpn"; host: "localhost"; port: "1194"; probe: [ "^\x00[\x0D-\xFF]$", "^\x00[\x0D-\xFF]\x38" ]; },
|
||||
{ name: "xmpp"; host: "localhost"; port: "5222"; probe: [ "jabber" ]; },
|
||||
{ name: "http"; host: "localhost"; port: "80"; probe: "builtin"; },
|
||||
{ name: "ssl"; host: "localhost"; port: "443"; probe: [ "" ]; },
|
||||
{ name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; keepalive: true; fork: true; },
|
||||
{ name: "http"; host: "localhost"; port: "80"; },
|
||||
|
||||
# match BOTH ALPN/SNI
|
||||
{ name: "tls"; host: "localhost"; port: "5223"; alpn_protocols: [ "xmpp-client" ]; sni_hostnames: [ "im.somethingelse.net" ]; log_level: 0;},
|
||||
|
||||
# just match ALPN
|
||||
{ name: "tls"; host: "localhost"; port: "443"; alpn_protocols: [ "h2", "http/1.1", "spdy/1", "spdy/2", "spdy/3" ]; log_level: 0; },
|
||||
{ name: "tls"; host: "localhost"; port: "xmpp-client"; alpn_protocols: [ "xmpp-client" ]; log_level: 0;},
|
||||
|
||||
# just match SNI
|
||||
{ name: "tls"; host: "localhost"; port: "993"; sni_hostnames: [ "mail.rutschle.net", "mail.englishintoulouse.com" ]; log_level: 0; },
|
||||
{ name: "tls"; host: "localhost"; port: "xmpp-client"; sni_hostnames: [ "im.rutschle.net", "im.englishintoulouse.com" ]; log_level: 0;},
|
||||
|
||||
# Let's Encrypt (tls-sni-* challenges)
|
||||
{ name: "tls"; host: "localhost"; port: "letsencrypt-client"; sni_hostnames: [ "*.*.acme.invalid" ]; log_level: 0;},
|
||||
|
||||
# catch anything else TLS
|
||||
{ name: "tls"; host: "localhost"; port: "443"; },
|
||||
|
||||
# Regex examples -- better use the built-in probes for real-world use!
|
||||
# OpenVPN
|
||||
{ name: "regex"; host: "localhost"; port: "1194"; regex_patterns: [ "^\x00[\x0D-\xFF]$", "^\x00[\x0D-\xFF]\x38" ]; },
|
||||
# Jabber
|
||||
{ name: "regex"; host: "localhost"; port: "5222"; regex_patterns: [ "jabber" ]; },
|
||||
|
||||
# Catch-all
|
||||
{ name: "regex"; host: "localhost"; port: "443"; regex_patterns: [ "" ]; },
|
||||
|
||||
# Where to connect in case of timeout (defaults to ssh)
|
||||
{ name: "timeout"; service: "daytime"; host: "localhost"; port: "daytime"; }
|
||||
);
|
||||
|
||||
# Optionally, specify to which protocol to connect in case
|
||||
# of timeout (defaults to "ssh").
|
||||
# You can timeout to any arbitrary address by setting a
|
||||
# protocol with no probe, as is the case with this example.
|
||||
# You can timeout to any arbitrary address by setting an
|
||||
# entry in 'protocols' named "timeout".
|
||||
# This enables you to set a tcpd service name for this
|
||||
# protocol too.
|
||||
on-timeout: "timeout";
|
||||
|
||||
@@ -40,8 +40,8 @@ fi
|
||||
|
||||
|
||||
if [ $QUIET -ne 1 ]; then
|
||||
printf "#ifndef _VERSION_H_ \n"
|
||||
printf "#define _VERSION_H_ \n\n"
|
||||
printf "#ifndef VERSION_H \n"
|
||||
printf "#define VERSION_H \n\n"
|
||||
printf "#define VERSION \"$release\"\n"
|
||||
printf "#endif\n"
|
||||
else
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
# probe.c: Code for probing protocols
|
||||
#
|
||||
# Copyright (C) 2007-2012 Yves Rutschle
|
||||
# Copyright (C) 2007-2015 Yves Rutschle
|
||||
#
|
||||
# This program is free software; you can redistribute it
|
||||
# and/or modify it under the terms of the GNU General Public
|
||||
@@ -21,7 +21,13 @@
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#ifdef ENABLE_REGEX
|
||||
#ifdef LIBPCRE
|
||||
#include <pcreposix.h>
|
||||
#else
|
||||
#include <regex.h>
|
||||
#endif
|
||||
#endif
|
||||
#include <ctype.h>
|
||||
#include "probe.h"
|
||||
|
||||
@@ -33,20 +39,23 @@ static int is_tinc_protocol(const char *p, int len, struct proto*);
|
||||
static int is_xmpp_protocol(const char *p, int len, struct proto*);
|
||||
static int is_http_protocol(const char *p, int len, struct proto*);
|
||||
static int is_tls_protocol(const char *p, int len, struct proto*);
|
||||
static int is_adb_protocol(const char *p, int len, struct proto*);
|
||||
static int is_socks5_protocol(const char *p, int len, struct proto*);
|
||||
static int is_true(const char *p, int len, struct proto* proto) { return 1; }
|
||||
|
||||
/* Table of protocols that have a built-in probe
|
||||
*/
|
||||
static struct proto builtins[] = {
|
||||
/* description service saddr probe */
|
||||
{ "ssh", "sshd", NULL, is_ssh_protocol},
|
||||
{ "openvpn", NULL, NULL, is_openvpn_protocol },
|
||||
{ "tinc", NULL, NULL, is_tinc_protocol },
|
||||
{ "xmpp", NULL, NULL, is_xmpp_protocol },
|
||||
{ "http", NULL, NULL, is_http_protocol },
|
||||
{ "ssl", NULL, NULL, is_tls_protocol },
|
||||
{ "tls", NULL, NULL, is_tls_protocol },
|
||||
{ "anyprot", NULL, NULL, is_true }
|
||||
/* description service saddr log_level keepalive fork probe */
|
||||
{ "ssh", "sshd", NULL, 1, 0, 1, is_ssh_protocol},
|
||||
{ "openvpn", NULL, NULL, 1, 0, 1, is_openvpn_protocol },
|
||||
{ "tinc", NULL, NULL, 1, 0, 1, is_tinc_protocol },
|
||||
{ "xmpp", NULL, NULL, 1, 0, 0, is_xmpp_protocol },
|
||||
{ "http", NULL, NULL, 1, 0, 0, is_http_protocol },
|
||||
{ "tls", NULL, NULL, 1, 0, 0, is_tls_protocol },
|
||||
{ "adb", NULL, NULL, 1, 0, 0, is_adb_protocol },
|
||||
{ "socks5", NULL, NULL, 1, 0, 0, is_socks5_protocol },
|
||||
{ "anyprot", NULL, NULL, 1, 0, 0, is_true }
|
||||
};
|
||||
|
||||
static struct proto *protocols;
|
||||
@@ -99,25 +108,25 @@ void hexdump(const char *mem, unsigned int len)
|
||||
{
|
||||
/* print offset */
|
||||
if(i % HEXDUMP_COLS == 0)
|
||||
printf("0x%06x: ", i);
|
||||
fprintf(stderr, "0x%06x: ", i);
|
||||
|
||||
/* print hex data */
|
||||
if(i < len)
|
||||
printf("%02x ", 0xFF & mem[i]);
|
||||
fprintf(stderr, "%02x ", 0xFF & mem[i]);
|
||||
else /* end of block, just aligning for ASCII dump */
|
||||
printf(" ");
|
||||
fprintf(stderr, " ");
|
||||
|
||||
/* 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 */
|
||||
putchar(' ');
|
||||
fputc(' ', stderr);
|
||||
else if(isprint(mem[j])) /* printable char */
|
||||
putchar(0xFF & mem[j]);
|
||||
fputc(0xFF & mem[j], stderr);
|
||||
else /* other char */
|
||||
putchar('.');
|
||||
fputc('.', stderr);
|
||||
}
|
||||
putchar('\n');
|
||||
fputc('\n', stderr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,7 +162,8 @@ static int is_openvpn_protocol (const char*p,int len, struct proto *proto)
|
||||
}
|
||||
|
||||
/* Is the buffer the beginning of a tinc connections?
|
||||
* (protocol is undocumented, but starts with "0 " in 1.0.15)
|
||||
* Protocol is documented here: http://www.tinc-vpn.org/documentation/tinc.pdf
|
||||
* First connection starts with "0 " in 1.0.15)
|
||||
* */
|
||||
static int is_tinc_protocol( const char *p, int len, struct proto *proto)
|
||||
{
|
||||
@@ -169,13 +179,16 @@ static int is_tinc_protocol( const char *p, int len, struct proto *proto)
|
||||
* */
|
||||
static int is_xmpp_protocol( const char *p, int len, struct proto *proto)
|
||||
{
|
||||
if (memmem(p, len, "jabber", 6))
|
||||
return PROBE_MATCH;
|
||||
|
||||
/* sometimes the word 'jabber' shows up late in the initial string,
|
||||
sometimes after a newline. this makes sure we snarf the entire preamble
|
||||
and detect it. (fixed for adium/pidgin) */
|
||||
if (len < 50)
|
||||
return PROBE_AGAIN;
|
||||
|
||||
return memmem(p, len, "jabber", 6) ? 1 : 0;
|
||||
return PROBE_NEXT;
|
||||
}
|
||||
|
||||
static int probe_http_method(const char *p, int len, const char *opt)
|
||||
@@ -183,7 +196,7 @@ static int probe_http_method(const char *p, int len, const char *opt)
|
||||
if (len < strlen(opt))
|
||||
return PROBE_AGAIN;
|
||||
|
||||
return !strncmp(p, opt, len);
|
||||
return !strncmp(p, opt, strlen(opt));
|
||||
}
|
||||
|
||||
/* Is the buffer the beginning of an HTTP connection? */
|
||||
@@ -212,20 +225,102 @@ static int is_http_protocol(const char *p, int len, struct proto *proto)
|
||||
return PROBE_NEXT;
|
||||
}
|
||||
|
||||
/* Says if it's TLS, optionally with SNI and ALPN lists in proto->data */
|
||||
static int is_tls_protocol(const char *p, int len, struct proto *proto)
|
||||
{
|
||||
if (len < 3)
|
||||
switch (parse_tls_header(proto->data, p, len)) {
|
||||
case TLS_MATCH: return PROBE_MATCH;
|
||||
case TLS_NOMATCH: return PROBE_NEXT;
|
||||
case TLS_ELENGTH: return PROBE_AGAIN;
|
||||
default: return PROBE_NEXT;
|
||||
}
|
||||
}
|
||||
|
||||
static int probe_adb_cnxn_message(const char *p)
|
||||
{
|
||||
/* The initial ADB host->device packet has a command type of CNXN, and a
|
||||
* data payload starting with "host:". Note that current versions of the
|
||||
* client hardcode "host::" (with empty serialno and banner fields) but
|
||||
* other clients may populate those fields.
|
||||
*/
|
||||
return !memcmp(&p[0], "CNXN", 4) && !memcmp(&p[24], "host:", 5);
|
||||
}
|
||||
|
||||
static int is_adb_protocol(const char *p, int len, struct proto *proto)
|
||||
{
|
||||
/* amessage.data_length is not being checked, under the assumption that
|
||||
* a packet >= 30 bytes will have "something" in the payload field.
|
||||
*
|
||||
* 24 bytes for the message header and 5 bytes for the "host:" tag.
|
||||
*
|
||||
* ADB protocol:
|
||||
* https://android.googlesource.com/platform/system/adb/+/master/protocol.txt
|
||||
*/
|
||||
static const unsigned int min_data_packet_size = 30;
|
||||
|
||||
if (len < min_data_packet_size)
|
||||
return PROBE_AGAIN;
|
||||
|
||||
/* TLS packet starts with a record "Hello" (0x16), followed by version
|
||||
* (0x03 0x00-0x03) (RFC6101 A.1)
|
||||
* This means we reject SSLv2 and lower, which is actually a good thing (RFC6176)
|
||||
if (probe_adb_cnxn_message(&p[0]) == PROBE_MATCH)
|
||||
return PROBE_MATCH;
|
||||
|
||||
/* In ADB v26.0.0 rc1-4321094, the initial host->device packet sends an
|
||||
* empty message before sending the CNXN command type. This was an
|
||||
* unintended side effect introduced in
|
||||
* https://android-review.googlesource.com/c/342653, and will be reverted for
|
||||
* a future release.
|
||||
*/
|
||||
return p[0] == 0x16 && p[1] == 0x03 && ( p[2] >= 0 && p[2] <= 0x03);
|
||||
static const unsigned char empty_message[] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff
|
||||
};
|
||||
|
||||
if (len < min_data_packet_size + sizeof(empty_message))
|
||||
return PROBE_AGAIN;
|
||||
|
||||
if (memcmp(&p[0], empty_message, sizeof(empty_message)))
|
||||
return PROBE_NEXT;
|
||||
|
||||
return probe_adb_cnxn_message(&p[sizeof(empty_message)]);
|
||||
}
|
||||
|
||||
static int is_socks5_protocol(const char *p_in, int len, struct proto *proto)
|
||||
{
|
||||
unsigned char* p = (unsigned char*)p_in;
|
||||
int i;
|
||||
|
||||
if (len < 2)
|
||||
return PROBE_AGAIN;
|
||||
|
||||
/* First byte should be socks protocol version */
|
||||
if (p[0] != 5)
|
||||
return PROBE_NEXT;
|
||||
|
||||
/* Second byte should be number of supported
|
||||
* authentication methods, assuming maximum of 10,
|
||||
* as defined in https://www.iana.org/assignments/socks-methods/socks-methods.xhtml
|
||||
*/
|
||||
char m_count = p[1];
|
||||
if (m_count < 1 || m_count > 10)
|
||||
return PROBE_NEXT;
|
||||
|
||||
if (len < 2 + m_count)
|
||||
return PROBE_AGAIN;
|
||||
|
||||
/* Each authentication method number should be in range 0..9
|
||||
* (https://www.iana.org/assignments/socks-methods/socks-methods.xhtml)
|
||||
*/
|
||||
for (i = 0; i < m_count; i++) {
|
||||
if (p[2 + i] > 9)
|
||||
return PROBE_NEXT;
|
||||
}
|
||||
return PROBE_MATCH;
|
||||
}
|
||||
|
||||
static int regex_probe(const char *p, int len, struct proto *proto)
|
||||
{
|
||||
#ifdef ENABLE_REGEX
|
||||
regex_t **probe = proto->data;
|
||||
regmatch_t pos = { 0, len };
|
||||
|
||||
@@ -233,6 +328,11 @@ static int regex_probe(const char *p, int len, struct proto *proto)
|
||||
/* try them all */;
|
||||
|
||||
return (*probe != NULL);
|
||||
#else
|
||||
/* Should never happen as we check when loading config file */
|
||||
fprintf(stderr, "FATAL: regex probe called but not built in\n");
|
||||
exit(5);
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -244,8 +344,8 @@ static int regex_probe(const char *p, int len, struct proto *proto)
|
||||
int probe_client_protocol(struct connection *cnx)
|
||||
{
|
||||
char buffer[BUFSIZ];
|
||||
struct proto *p;
|
||||
int n;
|
||||
struct proto *p, *last_p = cnx->proto;
|
||||
int n, res, again = 0;
|
||||
|
||||
n = read(cnx->q[0].fd, buffer, sizeof(buffer));
|
||||
/* It's possible that read() returns an error, e.g. if the client
|
||||
@@ -253,30 +353,41 @@ int probe_client_protocol(struct connection *cnx)
|
||||
* 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) {
|
||||
int res = PROBE_NEXT;
|
||||
|
||||
defer_write(&cnx->q[1], buffer, n);
|
||||
|
||||
for (p = cnx->proto; p && res == PROBE_NEXT; p = p->next) {
|
||||
if (! p->probe) continue;
|
||||
if (verbose) fprintf(stderr, "probing for %s\n", p->description);
|
||||
|
||||
cnx->proto = p;
|
||||
res = p->probe(cnx->q[1].begin_deferred_data, cnx->q[1].deferred_data_size, p);
|
||||
if (verbose > 1) {
|
||||
fprintf(stderr, "hexdump of incoming packet:\n");
|
||||
hexdump(buffer, n);
|
||||
}
|
||||
if (res != PROBE_NEXT)
|
||||
return res;
|
||||
defer_write(&cnx->q[1], buffer, n);
|
||||
}
|
||||
|
||||
if (verbose)
|
||||
fprintf(stderr,
|
||||
"all probes failed, connecting to first protocol: %s\n",
|
||||
protocols->description);
|
||||
for (p = cnx->proto; p; p = p->next) {
|
||||
char* probe_str[3] = {"PROBE_NEXT", "PROBE_MATCH", "PROBE_AGAIN"};
|
||||
if (! p->probe) continue;
|
||||
|
||||
/* If none worked, return the first one affected (that's completely
|
||||
* arbitrary) */
|
||||
cnx->proto = protocols;
|
||||
/* Don't probe last protocol if it is anyprot (and store last protocol) */
|
||||
if (! p->next) {
|
||||
last_p = p;
|
||||
if (!strcmp(p->description, "anyprot"))
|
||||
break;
|
||||
}
|
||||
|
||||
res = p->probe(cnx->q[1].begin_deferred_data, cnx->q[1].deferred_data_size, p);
|
||||
if (verbose) fprintf(stderr, "probing for %s: %s\n", p->description, probe_str[res]);
|
||||
|
||||
if (res == PROBE_MATCH) {
|
||||
cnx->proto = p;
|
||||
return PROBE_MATCH;
|
||||
}
|
||||
if (res == PROBE_AGAIN)
|
||||
again++;
|
||||
}
|
||||
if ((again && (n > 0)) || ((n == -1) && (errno == EAGAIN)))
|
||||
return PROBE_AGAIN;
|
||||
|
||||
/* Everything failed: match the last one */
|
||||
cnx->proto = last_p;
|
||||
return PROBE_MATCH;
|
||||
}
|
||||
|
||||
@@ -308,6 +419,11 @@ T_PROBE* get_probe(const char* description) {
|
||||
if (!strcmp(description, "regex"))
|
||||
return regex_probe;
|
||||
|
||||
/* Special case of "timeout" is allowed as a probe name in the
|
||||
* configuration file even though it's not really a probe */
|
||||
if (!strcmp(description, "timeout"))
|
||||
return is_true;
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/* API for probe.c */
|
||||
|
||||
#ifndef __PROBE_H_
|
||||
#define __PROBE_H_
|
||||
#ifndef PROBE_H
|
||||
#define PROBE_H
|
||||
|
||||
#include "common.h"
|
||||
#include "tls.h"
|
||||
|
||||
typedef enum {
|
||||
PROBE_NEXT, /* Enough data, probe failed -- it's some other protocol */
|
||||
@@ -19,11 +20,17 @@ struct proto {
|
||||
const char* description; /* a string that says what it is (for logging and command-line parsing) */
|
||||
const char* service; /* service name to do libwrap checks */
|
||||
struct addrinfo *saddr; /* list of addresses to try and switch that protocol */
|
||||
int log_level; /* 0: No logging of connection
|
||||
* 1: Log incoming connection
|
||||
*/
|
||||
int keepalive; /* 0: No keepalive ; 1: Set Keepalive for this connection */
|
||||
int fork; /* 0: Connection can run within shared process ; 1: Separate process required for this connection */
|
||||
|
||||
/* function to probe that protocol; parameters are buffer and length
|
||||
* containing the data to probe, and a pointer to the protocol structure */
|
||||
T_PROBE* probe;
|
||||
void* data; /* opaque pointer ; used to pass list of regex to regex probe */
|
||||
/* opaque pointer ; used to pass list of regex to regex probe, or TLSProtocol struct to sni/alpn probe */
|
||||
void* data;
|
||||
struct proto *next; /* pointer to next protocol in list, NULL if last */
|
||||
};
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ DAEMON=$PREFIX/sbin/sslh
|
||||
start()
|
||||
{
|
||||
echo "Start services: sslh"
|
||||
$DAEMON -F /etc/sslh.cfg
|
||||
$DAEMON -F/etc/sslh.cfg
|
||||
logger -t ${tag} -p ${facility} -i 'Started sslh'
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,24 @@ After=network.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/etc/conf.d/sslh
|
||||
ExecStart=/usr/bin/sslh --foreground $DAEMON_OPTS
|
||||
ExecStart=/usr/sbin/sslh --foreground $DAEMON_OPTS
|
||||
KillMode=process
|
||||
#Hardening
|
||||
PrivateTmp=true
|
||||
CapabilityBoundingSet=CAP_SETGID CAP_SETUID CAP_NET_BIND_SERVICE
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
SecureBits=noroot-locked
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectControlGroups=true
|
||||
MountFlags=private
|
||||
NoNewPrivileges=true
|
||||
PrivateDevices=true
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||
MemoryDenyWriteExecute=true
|
||||
DynamicUser=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+23
-4
@@ -94,6 +94,8 @@ void start_shoveler(int in_socket)
|
||||
} else {
|
||||
/* Timed out: it's necessarily SSH */
|
||||
cnx.proto = timeout_protocol();
|
||||
if (verbose)
|
||||
log_message(LOG_INFO, "timed out, connect to %s\n", cnx.proto->description);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -143,11 +145,17 @@ void main_loop(int listen_sockets[], int num_addr_listen)
|
||||
|
||||
listener_pid_number = num_addr_listen;
|
||||
listener_pid = malloc(listener_pid_number * sizeof(listener_pid[0]));
|
||||
CHECK_ALLOC(listener_pid, "malloc");
|
||||
|
||||
/* Start one process for each listening address */
|
||||
for (i = 0; i < num_addr_listen; i++) {
|
||||
if (!(listener_pid[i] = fork())) {
|
||||
|
||||
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));
|
||||
break;
|
||||
// We're in the child, we have work to do
|
||||
case 0:
|
||||
/* Listening process just accepts a connection, forks, and goes
|
||||
* back to listening */
|
||||
while (1)
|
||||
@@ -155,15 +163,26 @@ void main_loop(int listen_sockets[], int num_addr_listen)
|
||||
in_socket = accept(listen_sockets[i], 0, 0);
|
||||
if (verbose) fprintf(stderr, "accepted fd %d\n", in_socket);
|
||||
|
||||
if (!fork())
|
||||
{
|
||||
switch(fork()) {
|
||||
case -1: log_message(LOG_ERR, "fork failed: err %d: %s\n", errno, strerror(errno));
|
||||
break;
|
||||
|
||||
/* In child process */
|
||||
case 0:
|
||||
for (i = 0; i < num_addr_listen; ++i)
|
||||
close(listen_sockets[i]);
|
||||
start_shoveler(in_socket);
|
||||
exit(0);
|
||||
|
||||
/* In parent process */
|
||||
default: break;
|
||||
}
|
||||
close(in_socket);
|
||||
}
|
||||
break;
|
||||
// We're in the parent, we don't need to do anything
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+211
-44
@@ -2,7 +2,7 @@
|
||||
# main: processing of config file, command line options and start the main
|
||||
# loop.
|
||||
#
|
||||
# Copyright (C) 2007-2014 Yves Rutschle
|
||||
# Copyright (C) 2007-2016 Yves Rutschle
|
||||
#
|
||||
# This program is free software; you can redistribute it
|
||||
# and/or modify it under the terms of the GNU General Public
|
||||
@@ -25,7 +25,13 @@
|
||||
#ifdef LIBCONFIG
|
||||
#include <libconfig.h>
|
||||
#endif
|
||||
#ifdef ENABLE_REGEX
|
||||
#ifdef LIBPCRE
|
||||
#include <pcreposix.h>
|
||||
#else
|
||||
#include <regex.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include "common.h"
|
||||
#include "probe.h"
|
||||
@@ -33,8 +39,8 @@
|
||||
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> -p <add> [-p <addr> ...] \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" \
|
||||
@@ -42,8 +48,9 @@ const char* USAGE_STRING =
|
||||
"-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\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" \
|
||||
@@ -65,6 +72,7 @@ static struct option const_options[] = {
|
||||
{ "user", required_argument, 0, 'u' },
|
||||
{ "config", optional_argument, 0, 'F' },
|
||||
{ "pidfile", required_argument, 0, 'P' },
|
||||
{ "chroot", required_argument, 0, 'C' },
|
||||
{ "timeout", required_argument, 0, 't' },
|
||||
{ "on-timeout", required_argument, 0, OPT_ONTIMEOUT },
|
||||
{ "listen", required_argument, 0, 'p' },
|
||||
@@ -72,7 +80,7 @@ static struct option const_options[] = {
|
||||
};
|
||||
static struct option* all_options;
|
||||
static struct proto* builtins;
|
||||
static const char *optstr = "vt:T:p:VP:F::";
|
||||
static const char *optstr = "vt:T:p:VP:C:F::";
|
||||
|
||||
|
||||
|
||||
@@ -117,22 +125,54 @@ static void printsettings(void)
|
||||
|
||||
for (p = get_first_protocol(); p; p = p->next) {
|
||||
fprintf(stderr,
|
||||
"%s addr: %s. libwrap service: %s family %d %d\n",
|
||||
"%s addr: %s. libwrap service: %s log_level: %d family %d %d [%s] [%s]\n",
|
||||
p->description,
|
||||
sprintaddr(buf, sizeof(buf), p->saddr),
|
||||
p->service,
|
||||
p->log_level,
|
||||
p->saddr->ai_family,
|
||||
p->saddr->ai_addr->sa_family);
|
||||
p->saddr->ai_addr->sa_family,
|
||||
p->keepalive ? "keepalive" : "",
|
||||
p->fork ? "fork" : "");
|
||||
}
|
||||
fprintf(stderr, "listening on:\n");
|
||||
for (a = addr_listen; a; a = a->ai_next) {
|
||||
fprintf(stderr, "\t%s\n", sprintaddr(buf, sizeof(buf), a));
|
||||
fprintf(stderr,
|
||||
"\t%s\t[%s]\n",
|
||||
sprintaddr(buf, sizeof(buf), a),
|
||||
a->ai_flags & SO_KEEPALIVE ? "keepalive" : "");
|
||||
}
|
||||
fprintf(stderr, "timeout: %d\non-timeout: %s\n", probing_timeout,
|
||||
timeout_protocol()->description);
|
||||
}
|
||||
|
||||
|
||||
/* To removed in v1.21 */
|
||||
const char* ssl_err_msg = "Usage of 'ssl' setting is deprecated and will be removed in v1.21. Please use 'tls' instead\n";
|
||||
void ssl_to_tls(char* setting)
|
||||
{
|
||||
if (!strcmp(setting, "ssl")) {
|
||||
strcpy(setting, "tls"); /* legacy configuration */
|
||||
log_message(LOG_INFO, ssl_err_msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Turn 'ssl' command line option to 'tls'. To removed in v1.21 */
|
||||
void cmd_ssl_to_tls(int argc, char* argv[])
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < argc; i++) {
|
||||
if (!strcmp(argv[i], "--ssl")) {
|
||||
strcpy(argv[i], "--tls");
|
||||
/* foreground option not parsed yet, syslog not open, just print on
|
||||
* stderr and hope for the best */
|
||||
fprintf(stderr, ssl_err_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Extract configuration on addresses and ports on which to listen.
|
||||
* out: newly allocated list of addrinfo to listen to
|
||||
*/
|
||||
@@ -140,7 +180,7 @@ static void printsettings(void)
|
||||
static int config_listen(config_t *config, struct addrinfo **listen)
|
||||
{
|
||||
config_setting_t *setting, *addr;
|
||||
int len, i;
|
||||
int len, i, keepalive;
|
||||
const char *hostname, *port;
|
||||
|
||||
setting = config_lookup(config, "listen");
|
||||
@@ -156,12 +196,20 @@ static int config_listen(config_t *config, struct addrinfo **listen)
|
||||
return -1;
|
||||
}
|
||||
|
||||
keepalive = 0;
|
||||
config_setting_lookup_bool(addr, "keepalive", &keepalive);
|
||||
|
||||
resolve_split_name(listen, hostname, port);
|
||||
|
||||
/* getaddrinfo returned a list of addresses corresponding to the
|
||||
* specification; move the pointer to the end of that list before
|
||||
* processing the next specification */
|
||||
for (; *listen; listen = &((*listen)->ai_next));
|
||||
* processing the next specification, while setting flags for
|
||||
* start_listen_sockets() through ai_flags (which is not meant for
|
||||
* that, but is only used as hint in getaddrinfo, so it's OK) */
|
||||
for (; *listen; listen = &((*listen)->ai_next)) {
|
||||
if (keepalive)
|
||||
(*listen)->ai_flags = SO_KEEPALIVE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +222,7 @@ static int config_listen(config_t *config, struct addrinfo **listen)
|
||||
#ifdef LIBCONFIG
|
||||
static void setup_regex_probe(struct proto *p, config_setting_t* probes)
|
||||
{
|
||||
#ifdef ENABLE_REGEX
|
||||
int num_probes, errsize, i, res;
|
||||
char *err;
|
||||
const char * expr;
|
||||
@@ -187,20 +236,91 @@ static void setup_regex_probe(struct proto *p, config_setting_t* probes)
|
||||
|
||||
p->probe = get_probe("regex");
|
||||
probe_list = calloc(num_probes + 1, sizeof(*probe_list));
|
||||
CHECK_ALLOC(probe_list, "calloc");
|
||||
p->data = (void*)probe_list;
|
||||
|
||||
for (i = 0; i < num_probes; i++) {
|
||||
probe_list[i] = malloc(sizeof(*(probe_list[i])));
|
||||
CHECK_ALLOC(probe_list[i], "malloc");
|
||||
expr = config_setting_get_string_elem(probes, i);
|
||||
res = regcomp(probe_list[i], expr, 0);
|
||||
if (expr == NULL) {
|
||||
fprintf(stderr, "%s: invalid probe specified\n", p->description);
|
||||
exit(1);
|
||||
}
|
||||
res = regcomp(probe_list[i], expr, REG_EXTENDED);
|
||||
if (res) {
|
||||
err = malloc(errsize = regerror(res, probe_list[i], NULL, 0));
|
||||
CHECK_ALLOC(err, "malloc");
|
||||
regerror(res, probe_list[i], err, errsize);
|
||||
fprintf(stderr, "%s:%s\n", expr, err);
|
||||
free(err);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
#else
|
||||
fprintf(stderr, "line %d: regex probe specified but not compiled in\n", config_setting_source_line(probes));
|
||||
exit(5);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef LIBCONFIG
|
||||
static void setup_sni_alpn_list(struct proto *p, config_setting_t* config_items, const char* name, int alpn)
|
||||
{
|
||||
int num_probes, i, max_server_name_len, server_name_len;
|
||||
const char * config_item, *server_name;
|
||||
char** sni_hostname_list;
|
||||
|
||||
num_probes = config_setting_length(config_items);
|
||||
if (!num_probes) {
|
||||
fprintf(stderr, "%s: no %s specified\n", p->description, name);
|
||||
return;
|
||||
}
|
||||
|
||||
max_server_name_len = 0;
|
||||
for (i = 0; i < num_probes; i++) {
|
||||
server_name = config_setting_get_string_elem(config_items, i);
|
||||
if (server_name == NULL) {
|
||||
fprintf(stderr, "%s: invalid %s specified\n", p->description, name);
|
||||
exit(1);
|
||||
}
|
||||
server_name_len = strlen(server_name);
|
||||
if(server_name_len > max_server_name_len)
|
||||
max_server_name_len = server_name_len;
|
||||
}
|
||||
|
||||
sni_hostname_list = calloc(num_probes + 1, ++max_server_name_len);
|
||||
CHECK_ALLOC(sni_hostname_list, "calloc");
|
||||
|
||||
for (i = 0; i < num_probes; i++) {
|
||||
config_item = config_setting_get_string_elem(config_items, i);
|
||||
if (config_item == NULL) {
|
||||
fprintf(stderr, "%s: invalid %s specified\n", p->description, name);
|
||||
exit(1);
|
||||
}
|
||||
sni_hostname_list[i] = malloc(max_server_name_len);
|
||||
CHECK_ALLOC(sni_hostname_list[i], "malloc");
|
||||
strcpy (sni_hostname_list[i], config_item);
|
||||
if(verbose) fprintf(stderr, "%s: %s[%d]: %s\n", p->description, name, i, sni_hostname_list[i]);
|
||||
}
|
||||
|
||||
p->data = (void*)tls_data_set_list(p->data, alpn, sni_hostname_list);
|
||||
}
|
||||
|
||||
static void setup_sni_alpn(struct proto *p, config_setting_t* prot)
|
||||
{
|
||||
config_setting_t *sni_hostnames, *alpn_protocols;
|
||||
|
||||
p->data = (void*)new_tls_data();
|
||||
sni_hostnames = config_setting_get_member(prot, "sni_hostnames");
|
||||
alpn_protocols = config_setting_get_member(prot, "alpn_protocols");
|
||||
|
||||
if(sni_hostnames && config_setting_is_array(sni_hostnames)) {
|
||||
setup_sni_alpn_list(p, sni_hostnames, "sni_hostnames", 0);
|
||||
}
|
||||
if(alpn_protocols && config_setting_is_array(alpn_protocols)) {
|
||||
setup_sni_alpn_list(p, alpn_protocols, "alpn_protocols", 1);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -210,8 +330,9 @@ static void setup_regex_probe(struct proto *p, config_setting_t* probes)
|
||||
#ifdef LIBCONFIG
|
||||
static int config_protocols(config_t *config, struct proto **prots)
|
||||
{
|
||||
config_setting_t *setting, *prot, *probes;
|
||||
const char *hostname, *port, *name;
|
||||
config_setting_t *setting, *prot, *patterns;
|
||||
const char *hostname, *port, *cfg_name;
|
||||
char* name;
|
||||
int i, num_prots;
|
||||
struct proto *p, *prev = NULL;
|
||||
|
||||
@@ -220,44 +341,56 @@ static int config_protocols(config_t *config, struct proto **prots)
|
||||
num_prots = config_setting_length(setting);
|
||||
for (i = 0; i < num_prots; i++) {
|
||||
p = calloc(1, sizeof(*p));
|
||||
CHECK_ALLOC(p, "calloc");
|
||||
if (i == 0) *prots = p;
|
||||
if (prev) prev->next = p;
|
||||
prev = p;
|
||||
|
||||
prot = config_setting_get_elem(setting, i);
|
||||
if ((config_setting_lookup_string(prot, "name", &name) &&
|
||||
if ((config_setting_lookup_string(prot, "name", &cfg_name) &&
|
||||
config_setting_lookup_string(prot, "host", &hostname) &&
|
||||
config_setting_lookup_string(prot, "port", &port)
|
||||
)) {
|
||||
/* To removed in v1.21 */
|
||||
name = strdup(cfg_name);
|
||||
ssl_to_tls(name);
|
||||
/* /remove */
|
||||
p->description = name;
|
||||
config_setting_lookup_string(prot, "service", &(p->service));
|
||||
config_setting_lookup_bool(prot, "keepalive", &p->keepalive);
|
||||
config_setting_lookup_bool(prot, "fork", &p->fork);
|
||||
|
||||
resolve_split_name(&(p->saddr), hostname, port);
|
||||
if (config_setting_lookup_int(prot, "log_level", &p->log_level) == CONFIG_FALSE) {
|
||||
p->log_level = 1;
|
||||
}
|
||||
|
||||
if (resolve_split_name(&(p->saddr), hostname, port)) {
|
||||
fprintf(stderr, "line %d: cannot resolve %s:%s\n", config_setting_source_line(prot), hostname, port);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
probes = config_setting_get_member(prot, "probe");
|
||||
if (probes) {
|
||||
if (config_setting_is_array(probes)) {
|
||||
/* If 'probe' is an array, setup a regex probe using the
|
||||
* array of strings as pattern */
|
||||
p->probe = get_probe(name);
|
||||
if (!p->probe) {
|
||||
fprintf(stderr, "line %d: %s: probe unknown\n", config_setting_source_line(prot), name);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
setup_regex_probe(p, probes);
|
||||
|
||||
} else {
|
||||
/* if 'probe' is 'builtin', set the probe to the
|
||||
* appropriate builtin protocol */
|
||||
if (!strcmp(config_setting_get_string(probes), "builtin")) {
|
||||
p->probe = get_probe(name);
|
||||
if (!p->probe) {
|
||||
fprintf(stderr, "%s: no builtin probe for this protocol\n", name);
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "%s: illegal probe name\n", name);
|
||||
exit(1);
|
||||
}
|
||||
/* Probe-specific options: regex patterns */
|
||||
if (!strcmp(name, "regex")) {
|
||||
patterns = config_setting_get_member(prot, "regex_patterns");
|
||||
if (patterns && config_setting_is_array(patterns)) {
|
||||
setup_regex_probe(p, patterns);
|
||||
}
|
||||
}
|
||||
|
||||
/* Probe-specific options: SNI/ALPN */
|
||||
if (!strcmp(name, "tls")) {
|
||||
setup_sni_alpn(p, prot);
|
||||
}
|
||||
|
||||
} else {
|
||||
fprintf(stderr, "line %d: Illegal protocol description (missing name, host or port)\n", config_setting_source_line(prot));
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,6 +403,7 @@ static int config_protocols(config_t *config, struct proto **prots)
|
||||
* in: *filename
|
||||
* out: *listen, a newly-allocated linked list of listen addrinfo
|
||||
* *prots, a newly-allocated linked list of protocols
|
||||
* 1 on error, 0 on success
|
||||
*/
|
||||
#ifdef LIBCONFIG
|
||||
static int config_parse(char *filename, struct addrinfo **listen, struct proto **prots)
|
||||
@@ -280,17 +414,26 @@ static int config_parse(char *filename, struct addrinfo **listen, struct proto *
|
||||
|
||||
config_init(&config);
|
||||
if (config_read_file(&config, filename) == CONFIG_FALSE) {
|
||||
if (config_error_type(&config) == CONFIG_ERR_PARSE) {
|
||||
/* If it's a parse error then there will be a line number for the failure
|
||||
* an I/O error (such as non-existent file) will have the error line as 0
|
||||
*/
|
||||
if (config_error_line(&config) != 0) {
|
||||
fprintf(stderr, "%s:%d:%s\n",
|
||||
filename,
|
||||
config_error_line(&config),
|
||||
config_error_text(&config));
|
||||
exit(1);
|
||||
}
|
||||
fprintf(stderr, "%s:%s\n",
|
||||
filename,
|
||||
config_error_text(&config));
|
||||
return 1;
|
||||
}
|
||||
|
||||
config_lookup_bool(&config, "verbose", &verbose);
|
||||
if(config_lookup_bool(&config, "verbose", &verbose) == CONFIG_FALSE) {
|
||||
config_lookup_int(&config, "verbose", &verbose);
|
||||
}
|
||||
|
||||
config_lookup_bool(&config, "inetd", &inetd);
|
||||
config_lookup_bool(&config, "foreground", &foreground);
|
||||
config_lookup_bool(&config, "numeric", &numeric);
|
||||
@@ -306,6 +449,9 @@ static int config_parse(char *filename, struct addrinfo **listen, struct proto *
|
||||
|
||||
config_lookup_string(&config, "user", &user_name);
|
||||
config_lookup_string(&config, "pidfile", &pid_file);
|
||||
config_lookup_string(&config, "chroot", &chroot_path);
|
||||
|
||||
config_lookup_string(&config, "syslog_facility", &facility);
|
||||
|
||||
config_listen(&config, listen);
|
||||
config_protocols(&config, prots);
|
||||
@@ -340,6 +486,7 @@ static void make_alloptions(void)
|
||||
/* Create all_options, composed of const_options followed by one option per
|
||||
* known protocol */
|
||||
all_options = calloc(ARRAY_SIZE(const_options) + get_num_builtins(), sizeof(struct option));
|
||||
CHECK_ALLOC(all_options, "calloc");
|
||||
memcpy(all_options, const_options, sizeof(const_options));
|
||||
append_protocols(all_options, ARRAY_SIZE(const_options) - 1, builtins, get_num_builtins());
|
||||
}
|
||||
@@ -357,16 +504,20 @@ static void cmdline_config(int argc, char* argv[], struct proto** prots)
|
||||
char *config_filename;
|
||||
#endif
|
||||
|
||||
cmd_ssl_to_tls(argc, argv); /* To remove in v1.21 */
|
||||
|
||||
make_alloptions();
|
||||
|
||||
#ifdef LIBCONFIG
|
||||
optind = 1;
|
||||
opterr = 0; /* we're missing protocol options at this stage so don't output errors */
|
||||
while ((c = getopt_long_only(argc, argv, optstr, all_options, NULL)) != -1) {
|
||||
if (c == 'v') {
|
||||
verbose++;
|
||||
}
|
||||
if (c == 'F') {
|
||||
config_filename = optarg;
|
||||
if (config_filename) {
|
||||
fprintf(stderr, "config: %s\n", config_filename);
|
||||
res = config_parse(config_filename, &addr_listen, prots);
|
||||
} else {
|
||||
/* No configuration file specified -- try default file locations */
|
||||
@@ -416,8 +567,10 @@ next_arg:
|
||||
if (!prots) {
|
||||
/* No protocols yet -- create the list */
|
||||
p = prots = calloc(1, sizeof(*p));
|
||||
CHECK_ALLOC(p, "calloc");
|
||||
} else {
|
||||
p->next = calloc(1, sizeof(*p));
|
||||
CHECK_ALLOC(p->next, "calloc");
|
||||
p = p->next;
|
||||
}
|
||||
memcpy(p, &builtins[c-PROT_SHIFT], sizeof(*p));
|
||||
@@ -464,6 +617,10 @@ next_arg:
|
||||
pid_file = optarg;
|
||||
break;
|
||||
|
||||
case 'C':
|
||||
chroot_path = optarg;
|
||||
break;
|
||||
|
||||
case 'v':
|
||||
verbose++;
|
||||
break;
|
||||
@@ -481,10 +638,13 @@ next_arg:
|
||||
|
||||
set_protocol_list(prots);
|
||||
|
||||
/* If compiling with systemd socket support no need to require listen address */
|
||||
#ifndef SYSTEMD
|
||||
if (!addr_listen && !inetd) {
|
||||
fprintf(stderr, "No listening address specified; use at least one -p option\n");
|
||||
exit(1);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Did command-line override foreground setting? */
|
||||
if (background)
|
||||
@@ -505,6 +665,7 @@ int main(int argc, char *argv[])
|
||||
/* Init defaults */
|
||||
pid_file = NULL;
|
||||
user_name = NULL;
|
||||
chroot_path = NULL;
|
||||
|
||||
cmdline_config(argc, argv, &protocols);
|
||||
parse_cmdline(argc, argv, protocols);
|
||||
@@ -521,6 +682,13 @@ int main(int argc, char *argv[])
|
||||
|
||||
num_addr_listen = start_listen_sockets(&listen_sockets, addr_listen);
|
||||
|
||||
#ifdef SYSTEMD
|
||||
if (num_addr_listen < 1) {
|
||||
fprintf(stderr, "No listening sockets found, restart sockets or specify addresses in config\n");
|
||||
exit(1);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!foreground) {
|
||||
if (fork() > 0) exit(0); /* Detach */
|
||||
|
||||
@@ -536,13 +704,12 @@ int main(int argc, char *argv[])
|
||||
if (pid_file)
|
||||
write_pid_file(pid_file);
|
||||
|
||||
if (user_name)
|
||||
drop_privileges(user_name);
|
||||
|
||||
|
||||
/* Open syslog connection */
|
||||
/* Open syslog connection before we drop privs/chroot */
|
||||
setup_syslog(argv[0]);
|
||||
|
||||
if (user_name || chroot_path)
|
||||
drop_privileges(user_name, chroot_path);
|
||||
|
||||
if (verbose)
|
||||
printcaps();
|
||||
|
||||
|
||||
+138
-18
@@ -27,6 +27,8 @@
|
||||
|
||||
const char* server_type = "sslh-select";
|
||||
|
||||
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
|
||||
|
||||
/* cnx_num_alloc is the number of connection to allocate at once (at start-up,
|
||||
* and then every time we get too many simultaneous connections: e.g. start
|
||||
* with 100 slots, then if we get more than 100 connections allocate another
|
||||
@@ -61,9 +63,9 @@ int tidy_connection(struct connection *cnx, fd_set *fds, fd_set *fds2)
|
||||
if (verbose)
|
||||
fprintf(stderr, "closing fd %d\n", cnx->q[i].fd);
|
||||
|
||||
close(cnx->q[i].fd);
|
||||
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);
|
||||
}
|
||||
@@ -93,11 +95,16 @@ int accept_new_connection(int listen_socket, struct connection *cnx[], int* cnx_
|
||||
in_socket = accept(listen_socket, 0, 0);
|
||||
CHECK_RES_RETURN(in_socket, "accept");
|
||||
|
||||
if (!fd_is_in_range(in_socket))
|
||||
if (!fd_is_in_range(in_socket)) {
|
||||
close(in_socket);
|
||||
return -1;
|
||||
}
|
||||
|
||||
res = set_nonblock(in_socket);
|
||||
if (res == -1) return -1;
|
||||
if (res == -1) {
|
||||
close(in_socket);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Find an empty slot */
|
||||
for (free = 0; (free < *cnx_size) && ((*cnx)[free].q[0].fd != -1); free++) {
|
||||
@@ -109,6 +116,7 @@ int accept_new_connection(int listen_socket, struct connection *cnx[], int* cnx_
|
||||
new = realloc(*cnx, (*cnx_size + cnx_num_alloc) * sizeof((*cnx)[0]));
|
||||
if (!new) {
|
||||
log_message(LOG_ERR, "unable to realloc -- dropping connection\n");
|
||||
close(in_socket);
|
||||
return -1;
|
||||
}
|
||||
*cnx = new;
|
||||
@@ -140,9 +148,9 @@ int connect_queue(struct connection *cnx, fd_set *fds_r, fd_set *fds_w)
|
||||
flush_deferred(q);
|
||||
if (q->deferred_data) {
|
||||
FD_SET(q->fd, fds_w);
|
||||
} else {
|
||||
FD_SET(q->fd, fds_r);
|
||||
FD_CLR(cnx->q[0].fd, fds_r);
|
||||
}
|
||||
FD_SET(q->fd, fds_r);
|
||||
return q->fd;
|
||||
} else {
|
||||
tidy_connection(cnx, fds_r, fds_w);
|
||||
@@ -180,6 +188,93 @@ void shovel(struct connection *cnx, int active_fd,
|
||||
}
|
||||
}
|
||||
|
||||
/* shovels data from one fd to the other and vice-versa
|
||||
returns after one socket closed
|
||||
*/
|
||||
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 (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 (verbose)
|
||||
fprintf(stderr, "socket closed\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Child process that makes internal connection and proxies
|
||||
*/
|
||||
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);
|
||||
CHECK_RES_DIE(out_socket, "connect");
|
||||
|
||||
cnx->q[1].fd = out_socket;
|
||||
|
||||
log_connection(cnx);
|
||||
|
||||
shovel_single(cnx);
|
||||
|
||||
close(in_socket);
|
||||
close(out_socket);
|
||||
|
||||
if (verbose)
|
||||
fprintf(stderr, "connection closed down\n");
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/* returns true if specified fd is initialised and present in fd_set */
|
||||
int is_fd_active(int fd, fd_set* set)
|
||||
{
|
||||
@@ -206,7 +301,8 @@ void main_loop(int listen_sockets[], int num_addr_listen)
|
||||
fd_set fds_r, fds_w; /* reference fd sets (used to init the next 2) */
|
||||
fd_set readfds, writefds; /* working read and write fd sets */
|
||||
struct timeval tv;
|
||||
int max_fd, in_socket, i, j, res;
|
||||
int max_fd, i, j, res;
|
||||
int in_socket = 0;
|
||||
struct connection *cnx;
|
||||
int num_cnx; /* Number of connections in *cnx */
|
||||
int num_probing = 0; /* Number of connections currently probing
|
||||
@@ -226,6 +322,7 @@ void main_loop(int listen_sockets[], int num_addr_listen)
|
||||
|
||||
num_cnx = cnx_num_alloc; /* Start with a set pool of slots */
|
||||
cnx = malloc(num_cnx * sizeof(struct connection));
|
||||
CHECK_ALLOC(cnx, "malloc");
|
||||
for (i = 0; i < num_cnx; i++)
|
||||
init_cnx(&cnx[i]);
|
||||
|
||||
@@ -248,15 +345,12 @@ void main_loop(int listen_sockets[], int num_addr_listen)
|
||||
for (i = 0; i < num_addr_listen; i++) {
|
||||
if (FD_ISSET(listen_sockets[i], &readfds)) {
|
||||
in_socket = accept_new_connection(listen_sockets[i], &cnx, &num_cnx);
|
||||
if (in_socket != -1)
|
||||
num_probing++;
|
||||
|
||||
if (in_socket > 0) {
|
||||
num_probing++;
|
||||
FD_SET(in_socket, &fds_r);
|
||||
if (in_socket >= max_fd)
|
||||
max_fd = in_socket + 1;;
|
||||
}
|
||||
FD_CLR(listen_sockets[i], &readfds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +399,10 @@ void main_loop(int listen_sockets[], int num_addr_listen)
|
||||
* data so probe the protocol */
|
||||
if ((cnx[i].probe_timeout < time(NULL))) {
|
||||
cnx[i].proto = timeout_protocol();
|
||||
if (verbose)
|
||||
log_message(LOG_INFO,
|
||||
"timed out, connect to %s\n",
|
||||
cnx[i].proto->description);
|
||||
} else {
|
||||
res = probe_client_protocol(&cnx[i]);
|
||||
if (res == PROBE_AGAIN)
|
||||
@@ -314,14 +412,36 @@ void main_loop(int listen_sockets[], int num_addr_listen)
|
||||
num_probing--;
|
||||
cnx[i].state = ST_SHOVELING;
|
||||
|
||||
/* libwrap check if required for this protocol */
|
||||
if (cnx[i].proto->service &&
|
||||
check_access_rights(in_socket, cnx[i].proto->service)) {
|
||||
tidy_connection(&cnx[i], &fds_r, &fds_w);
|
||||
res = -1;
|
||||
} else {
|
||||
res = connect_queue(&cnx[i], &fds_r, &fds_w);
|
||||
}
|
||||
/* libwrap check if required for this protocol */
|
||||
if (cnx[i].proto->service &&
|
||||
check_access_rights(in_socket, cnx[i].proto->service)) {
|
||||
tidy_connection(&cnx[i], &fds_r, &fds_w);
|
||||
res = -1;
|
||||
} else if (cnx[i].proto->fork) {
|
||||
struct connection *pcnx_i = &cnx[i];
|
||||
struct connection cnx_i = *pcnx_i;
|
||||
switch (fork()) {
|
||||
case 0: /* child */
|
||||
for (i = 0; i < num_addr_listen; i++)
|
||||
close(listen_sockets[i]);
|
||||
for (i = 0; i < num_cnx; i++)
|
||||
if (&cnx[i] != pcnx_i)
|
||||
for (j = 0; j < 2; j++)
|
||||
if (cnx[i].q[j].fd != -1)
|
||||
close(cnx[i].q[j].fd);
|
||||
free(cnx);
|
||||
connect_proxy(&cnx_i);
|
||||
exit(0);
|
||||
case -1: log_message(LOG_ERR, "fork failed: err %d: %s\n", errno, strerror(errno));
|
||||
break;
|
||||
default: /* parent */
|
||||
break;
|
||||
}
|
||||
tidy_connection(&cnx[i], &fds_r, &fds_w);
|
||||
res = -1;
|
||||
} else {
|
||||
res = connect_queue(&cnx[i], &fds_r, &fds_w);
|
||||
}
|
||||
|
||||
if (res >= max_fd)
|
||||
max_fd = res + 1;;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
sslh [B<-F> I<config file>] [ B<-t> I<num> ] [B<--transparent>] [B<-p> I<listening address> [B<-p> I<listening address> ...] [B<--ssl> I<target address for SSL>] [B<--ssh> I<target address for SSH>] [B<--openvpn> I<target address for OpenVPN>] [B<--http> I<target address for HTTP>] [B<--anyprot> I<default target address>] [B<--on-timeout> I<protocol name>] [B<-u> I<username>] [B<-P> I<pidfile>] [-v] [-i] [-V] [-f] [-n]
|
||||
sslh [B<-F>I<config file>] [B<-t> I<num>] [B<--transparent>] [B<-p> I<listening address> [B<-p> I<listening address> ...] [B<--ssl> I<target address for SSL>] [B<--tls> I<target address for TLS>] [B<--ssh> I<target address for SSH>] [B<--openvpn> I<target address for OpenVPN>] [B<--http> I<target address for HTTP>] [B<--xmpp> I<target address for XMPP>] [B<--tinc> I<target address for TINC>] [B<--anyprot> I<default target address>] [B<--on-timeout> I<protocol name>] [B<-u> I<username>] [B<-C> I<chroot>] [B<-P> I<pidfile>] [-v] [-i] [-V] [-f] [-n]
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
@@ -51,18 +51,14 @@ and the list of protocols).
|
||||
|
||||
The configuration file makes it possible to specify
|
||||
protocols using regular expressions: a list of regular
|
||||
expressions is given as the I<probe> parameter, and if the
|
||||
expressions is given as the I<regex_patterns> parameter, and if the
|
||||
first packet received from the client matches any of these
|
||||
expressions, B<sslh> connects to that protocol.
|
||||
|
||||
Alternatively, the I<probe> parameter can be set to
|
||||
"builtin", to use the compiled probes which are much faster
|
||||
than regular expressions.
|
||||
|
||||
=head2 Probing protocols
|
||||
|
||||
When receiving an incoming connection, B<sslh> will read the
|
||||
first bytes sent be the connecting client. It will then
|
||||
first bytes sent by the connecting client. It will then
|
||||
probe for the protocol in the order specified on the command
|
||||
line (or the configuration file). Therefore B<--anyprot>
|
||||
should alway be used last, as it always succeeds and further
|
||||
@@ -82,12 +78,15 @@ connections and LOG_ERR for failures.
|
||||
|
||||
=over 4
|
||||
|
||||
=item B<-F> I<filename>, B<--config> I<filename>
|
||||
=item B<-F>I<filename>, B<--config> I<filename>
|
||||
|
||||
Uses I<filename> has configuration file. If other
|
||||
Uses I<filename> as configuration file. If other
|
||||
command-line options are specified, they will override the
|
||||
configuration file's settings.
|
||||
|
||||
When using the shorthand version, make sure there should be
|
||||
no space between B<-F> and the I<filename>.
|
||||
|
||||
=item B<-t> I<num>, B<--timeout> I<num>
|
||||
|
||||
Timeout before forwarding the connection to the timeout
|
||||
@@ -96,7 +95,11 @@ protocol (which should usually be SSH). Default is 2s.
|
||||
=item B<--on-timeout> I<protocol name>
|
||||
|
||||
Name of the protocol to connect to after the timeout period
|
||||
is over. Default is 'ssh'.
|
||||
is over. Default is to forward to the first specified
|
||||
protocol. It usually makes sense to specify 'ssh' as the
|
||||
timeout protocol, as the SSH specification does not tell
|
||||
who is supposed to speak first and a large number of SSH
|
||||
clients wait for the server to send its banner.
|
||||
|
||||
=item B<--transparent>
|
||||
|
||||
@@ -127,7 +130,7 @@ inside your network to just connect directly to B<httpd>.
|
||||
|
||||
Also, B<sslh> probes for SSLv3 (or TLSv1) handshake and will
|
||||
reject connections from clients requesting SSLv2. This is
|
||||
compliant to RFC6176 which prohibits the usage of SSLv2. If
|
||||
compliant with RFC6176 which prohibits the usage of SSLv2. If
|
||||
you wish to accept SSLv2, use B<--default> instead.
|
||||
|
||||
=item B<--ssh> I<target address>
|
||||
@@ -185,6 +188,10 @@ Prints B<sslh> version.
|
||||
|
||||
Requires to run under the specified username.
|
||||
|
||||
=item B<-C> I<chroot>, B<--chroot> I<chroot>
|
||||
|
||||
Requires to run under the specified chroot.
|
||||
|
||||
=item B<-P> I<pidfile>, B<--pidfile> I<pidfile>
|
||||
|
||||
Specifies a file in which to write the PID of the main
|
||||
@@ -230,10 +237,10 @@ detailed explanation of the variables used by B<sslh>.
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
Last version available from
|
||||
The latest version is available from
|
||||
L<http://www.rutschle.net/tech/sslh>, and can be tracked
|
||||
from L<http://freecode.com/projects/sslh>.
|
||||
|
||||
=head1 AUTHOR
|
||||
|
||||
Written by Yves Rutschle
|
||||
Written by Yves Rutschle.
|
||||
@@ -0,0 +1,153 @@
|
||||
#include <libconfig.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "common.h"
|
||||
|
||||
|
||||
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 */
|
||||
char *conn = malloc(strlen(hostname)+strlen(port)+2);
|
||||
CHECK_ALLOC(conn, "malloc");
|
||||
strcpy(conn, hostname);
|
||||
strcat(conn, ":");
|
||||
strcat(conn, port);
|
||||
|
||||
return conn;
|
||||
|
||||
}
|
||||
|
||||
|
||||
static int get_listen_from_conf(const char *filename, char **listen[]) {
|
||||
config_t config;
|
||||
config_setting_t *setting, *addr;
|
||||
const char *hostname, *port;
|
||||
int len = 0;
|
||||
|
||||
/* look up the listen stanzas in the config file so these
|
||||
* can be used in the socket file generated */
|
||||
config_init(&config);
|
||||
if (config_read_file(&config, filename) == CONFIG_FALSE) {
|
||||
/* we don't care if file is missing, skip it */
|
||||
if (config_error_line(&config) != 0) {
|
||||
fprintf(stderr, "%s:%d:%s\n",
|
||||
filename,
|
||||
config_error_line(&config),
|
||||
config_error_text(&config));
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
setting = config_lookup(&config, "listen");
|
||||
if (setting) {
|
||||
int i;
|
||||
len = config_setting_length(setting);
|
||||
*listen = malloc(len * sizeof(**listen));
|
||||
CHECK_ALLOC(*listen, "malloc");
|
||||
for (i = 0; i < len; i++) {
|
||||
addr = config_setting_get_elem(setting, i);
|
||||
if (! (config_setting_lookup_string(addr, "host", &hostname) &&
|
||||
config_setting_lookup_string(addr, "port", &port))) {
|
||||
fprintf(stderr,
|
||||
"line %d:Incomplete specification (hostname and port required)\n",
|
||||
config_setting_source_line(addr));
|
||||
return -1;
|
||||
} else {
|
||||
(*listen)[i] = malloc(strlen(resolve_listen(hostname, port)));
|
||||
CHECK_ALLOC((*listen)[i], "malloc");
|
||||
strcpy((*listen)[i], resolve_listen(hostname, port));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return len;
|
||||
|
||||
}
|
||||
|
||||
static int write_socket_unit(FILE *socket, char *listen[], int num_addr, const char *source) {
|
||||
int i;
|
||||
|
||||
fprintf(socket,
|
||||
"# Automatically generated by systemd-sslh-generator\n\n"
|
||||
"[Unit]\n"
|
||||
"Before=sslh.service\n"
|
||||
"SourcePath=%s\n"
|
||||
"Documentation=man:sslh(8) man:systemd-sslh-generator(8)\n\n"
|
||||
"[Socket]\n"
|
||||
"FreeBind=true\n",
|
||||
source);
|
||||
|
||||
for (i = 0; i < num_addr; i++) {
|
||||
fprintf(socket, "ListenStream=%s\n", listen[i]);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int gen_sslh_config(char *runtime_unit_dir) {
|
||||
char *sslh_conf;
|
||||
int num_addr;
|
||||
FILE *config;
|
||||
char **listen;
|
||||
FILE *runtime_conf_fd = stdout;
|
||||
const char *unit_file;
|
||||
|
||||
/* There are two default locations so check both with first given preference */
|
||||
sslh_conf = "/etc/sslh.cfg";
|
||||
|
||||
config = fopen(sslh_conf, "r");
|
||||
if (config == NULL) {
|
||||
sslh_conf="/etc/sslh/sslh.cfg";
|
||||
config = fopen(sslh_conf, "r");
|
||||
if (config == NULL) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(config);
|
||||
|
||||
num_addr = get_listen_from_conf(sslh_conf, &listen);
|
||||
if (num_addr < 0)
|
||||
return -1;
|
||||
|
||||
/* If this is run by systemd directly write to the location told to
|
||||
* otherwise write to standard out so that it's trivial to check what
|
||||
* will be written */
|
||||
if (runtime_unit_dir && *runtime_unit_dir) {
|
||||
unit_file = "/sslh.socket";
|
||||
size_t uf_len = strlen(unit_file);
|
||||
size_t runtime_len = strlen(runtime_unit_dir) + uf_len + 1;
|
||||
char *runtime_conf = malloc(runtime_len);
|
||||
CHECK_ALLOC(runtime_conf, "malloc");
|
||||
strcpy(runtime_conf, runtime_unit_dir);
|
||||
strcat(runtime_conf, unit_file);
|
||||
runtime_conf_fd = fopen(runtime_conf, "w");
|
||||
}
|
||||
|
||||
|
||||
return write_socket_unit(runtime_conf_fd, listen, num_addr, sslh_conf);
|
||||
}
|
||||
|
||||
|
||||
int main(int argc, char *argv[]){
|
||||
int r = 0;
|
||||
int k;
|
||||
char *runtime_unit_dest = "";
|
||||
|
||||
if (argc > 1 && (argc != 4) ) {
|
||||
printf("This program takes three or no arguments.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (argc > 1)
|
||||
runtime_unit_dest = argv[1];
|
||||
|
||||
k = gen_sslh_config(runtime_unit_dest);
|
||||
if (k < 0)
|
||||
r = k;
|
||||
|
||||
return r < 0 ? -1 : 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,27 +2,30 @@
|
||||
|
||||
# Test script for sslh
|
||||
|
||||
# Uses Conf::Libconfig to read sslh config file: install
|
||||
# with:
|
||||
# cpan Conf::Libconfig
|
||||
|
||||
use strict;
|
||||
use IO::Socket::INET6;
|
||||
use Test::More qw/no_plan/;
|
||||
use Conf::Libconfig;
|
||||
|
||||
# We use ports 9000, 9001 and 9002 -- hope that won't clash
|
||||
# with anything...
|
||||
my $ssh_address = "ip6-localhost:9000";
|
||||
my $ssl_address = "ip6-localhost:9001";
|
||||
my $sslh_port = 9002;
|
||||
my $no_listen = 9003; # Port on which no-one listens
|
||||
my $pidfile = "/tmp/sslh_test.pid";
|
||||
my $conf = new Conf::Libconfig;
|
||||
$conf->read_file("test.cfg");
|
||||
|
||||
|
||||
my $no_listen = 8083; # Port on which no-one listens
|
||||
my $pidfile = $conf->lookup_value("pidfile");
|
||||
my $sslh_port = $conf->fetch_array("listen")->[0]->{port};
|
||||
my $user = (getpwuid $<)[0]; # Run under current username
|
||||
|
||||
# Which tests do we run
|
||||
my $SSL_CNX = 1;
|
||||
my $SSH_SHY_CNX = 1;
|
||||
my $SSH_BOLD_CNX = 1;
|
||||
my $SSH_PROBE_AGAIN = 1;
|
||||
my $PROBES_NOFRAG = 1;
|
||||
my $PROBES_AGAIN = 1;
|
||||
my $SSL_MIX_SSH = 1;
|
||||
my $SSH_MIX_SSL = 1;
|
||||
my $BIG_MSG = 0; # This test is unreliable
|
||||
my $STALL_CNX = 0; # This test needs fixing
|
||||
|
||||
# Robustness tests. These are mostly to achieve full test
|
||||
# coverage, but do not necessarily result in an actual test
|
||||
@@ -32,22 +35,158 @@ my $RB_CNX_NOSERVER = 1;
|
||||
my $RB_PARAM_NOHOST = 1;
|
||||
my $RB_WRONG_USERNAME = 1;
|
||||
my $RB_OPEN_PID_FILE = 1;
|
||||
my $RB_BIND_ADDRESS = 1;
|
||||
my $RB_RESOLVE_ADDRESS = 1;
|
||||
|
||||
`lcov --directory . --zerocounters`;
|
||||
|
||||
sub verbose_exec
|
||||
{
|
||||
my ($cmd) = @_;
|
||||
|
||||
my ($ssh_pid, $ssl_pid);
|
||||
|
||||
if (!($ssh_pid = fork)) {
|
||||
exec "./echosrv --listen $ssh_address --prefix 'ssh: '";
|
||||
warn "$cmd\n";
|
||||
if (!fork) {
|
||||
exec $cmd;
|
||||
}
|
||||
}
|
||||
|
||||
if (!($ssl_pid = fork)) {
|
||||
exec "./echosrv --listen $ssl_address --prefix 'ssl: '";
|
||||
|
||||
|
||||
# For SNI/ALPN, build a protocol name as such:
|
||||
# tls:sni1,sni2,...;alpn1,alpn2,...
|
||||
# input: a protocol entry from Libconfig
|
||||
sub make_sni_alpn_name {
|
||||
my ($prot) = @_;
|
||||
|
||||
return "tls:" . (join ",", @{$prot->{sni_hostnames} // []})
|
||||
. ";" . (join ",", @{$prot->{alpn_protocols} // [] });
|
||||
}
|
||||
|
||||
|
||||
# Tests one probe: given input data, connect, verify we get
|
||||
# the expected server, verify shoveling works
|
||||
# Named options:
|
||||
# data: what to write
|
||||
# expected: expected protocol prefix
|
||||
# no_frag: don't print byte-per-byte
|
||||
sub test_probe {
|
||||
my (%opts) = @_;
|
||||
|
||||
my $cnx = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
|
||||
warn "$!\n" unless $cnx;
|
||||
return unless $cnx;
|
||||
|
||||
my $pattern = $opts{data};
|
||||
if ($opts{no_frag}) {
|
||||
syswrite $cnx, $pattern;
|
||||
} else {
|
||||
while (length $pattern) {
|
||||
syswrite $cnx, (substr $pattern, 0, 1, '');
|
||||
select undef, undef, undef, .01;
|
||||
}
|
||||
}
|
||||
|
||||
my $data;
|
||||
my $n = sysread $cnx, $data, 1024;
|
||||
$data =~ /^(.*?): /;
|
||||
my $prefix = $1;
|
||||
$data =~ s/$prefix: //g;
|
||||
print "Received: protocol $prefix data [$data]\n";
|
||||
close $cnx;
|
||||
|
||||
$opts{expected} =~ s/^ssl/tls/; # to remove in 1.21
|
||||
is($prefix, $opts{expected}, "probe $opts{expected} connected correctly");
|
||||
is($data, $opts{data}, "data shoveled correctly");
|
||||
}
|
||||
|
||||
# Test all probes, with or without fragmentation
|
||||
# options:
|
||||
# no_frag: write test patterns all at once (also
|
||||
# available per-protocol as some probes don't support
|
||||
# fragmentation)
|
||||
sub test_probes {
|
||||
my (%opts) = @_;
|
||||
|
||||
my @probes = @{$conf->fetch_array("protocols")};
|
||||
foreach my $p (@probes) {
|
||||
my %protocols = (
|
||||
'ssh' => { data => "SSH-2.0 tester" },
|
||||
'socks5' => { data => "\x05\x04\x01\x02\x03\x04" },
|
||||
'http' => {
|
||||
data => "GET index.html HTTP/1.1",
|
||||
no_frag => 1 },
|
||||
'ssl' => {
|
||||
data => "\x16\x03\x01\x00\xab\x01\x00\x00\xa7\x03\x03\x89\x22\x33\x95\x43\x7a\xc3\x89\x45\x51\x12\x3c\x28\x24\x1b\x6a\x78\xbf\xbe\x95\xd8\x90\x58\xd7\x65\xf7\xbb\x2d\xb2\x8d\xa0\x75\x00\x00\x38\xc0\x2c\xc0\x30\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0\x2b\xc0\x2f\x00\x9e\xc0\x24\xc0\x28\x00\x6b\xc0\x23\xc0\x27\x00\x67\xc0\x0a\xc0\x14\x00\x39\xc0\x09\xc0\x13\x00\x33\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x00\xff\x01\x00\x00\x46\x00\x0b\x00\x04\x03\x00\x01\x02\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x19\x00\x18\x00\x23\x00\x00\x00\x0d\x00\x20\x00\x1e\x06\x01\x06\x02\x06\x03\x05\x01\x05\x02\x05\x03\x04\x01\x04\x02\x04\x03\x03\x01\x03\x02\x03\x03\x02\x01\x02\x02\x02\x03\x00\x16\x00\x00\x00\x17\x00\x00hello ssl alone"
|
||||
},
|
||||
'tls' => {
|
||||
# Packet with SNI and ALPN (`openssl s_client -connect localhost:443 -alpn alpn1 -servername sni1`)
|
||||
data_sni_alpn => "\x16\x03\x01\x00\xc4\x01\x00\x00\xc0\x03\x03\x03\x19\x01\x00\x40\x14\x13\xcc\x1b\x94\xad\x20\x5d\x13\x1a\x8d\xd2\x65\x23\x70\xde\xd1\x3c\x5d\x05\x19\xcb\x27\x0d\x7c\x2c\x89\x00\x00\x38\xc0\x2c\xc0\x30\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0\x2b\xc0\x2f\x00\x9e\xc0\x24\xc0\x28\x00\x6b\xc0\x23\xc0\x27\x00\x67\xc0\x0a\xc0\x14\x00\x39\xc0\x09\xc0\x13\x00\x33\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x00\xff\x01\x00\x00\x5f\x00\x00\x00\x09\x00\x07\x00\x00\x04\$sni\x00\x0b\x00\x04\x03\x00\x01\x02\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x19\x00\x18\x00\x23\x00\x00\x00\x0d\x00\x20\x00\x1e\x06\x01\x06\x02\x06\x03\x05\x01\x05\x02\x05\x03\x04\x01\x04\x02\x04\x03\x03\x01\x03\x02\x03\x03\x02\x01\x02\x02\x02\x03\x00\x10\x00\x08\x00\x06\x05\$alpn\x00\x16\x00\x00\x00\x17\x00\x00hello sni/alpn",
|
||||
# Packet with SNI alone
|
||||
data_sni => "\x16\x03\x01\x00\xb8\x01\x00\x00\xb4\x03\x03\x97\xe4\xe9\xad\x86\xe1\x21\xfd\xc4\x5b\x27\x0e\xad\x4b\x55\xc2\x50\xe4\x1c\x86\x2f\x37\x25\xde\xe8\x9c\x59\xfc\x1b\xa9\x37\x32\x00\x00\x38\xc0\x2c\xc0\x30\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0\x2b\xc0\x2f\x00\x9e\xc0\x24\xc0\x28\x00\x6b\xc0\x23\xc0\x27\x00\x67\xc0\x0a\xc0\x14\x00\x39\xc0\x09\xc0\x13\x00\x33\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x00\xff\x01\x00\x00\x53\x00\x00\x00\x09\x00\x07\x00\x00\x04\$sni\x00\x0b\x00\x04\x03\x00\x01\x02\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x19\x00\x18\x00\x23\x00\x00\x00\x0d\x00\x20\x00\x1e\x06\x01\x06\x02\x06\x03\x05\x01\x05\x02\x05\x03\x04\x01\x04\x02\x04\x03\x03\x01\x03\x02\x03\x03\x02\x01\x02\x02\x02\x03\x00\x16\x00\x00\x00\x17\x00\x00hello sni",
|
||||
# packet with ALPN alone
|
||||
data_alpn => "\x16\x03\x01\x00\xb7\x01\x00\x00\xb3\x03\x03\xe2\x90\xa2\x29\x03\x31\xad\x98\x44\x51\x54\x90\x5b\xd9\x51\x0e\x66\xb5\x3f\xe8\x8b\x09\xc9\xe4\x2b\x97\x24\xef\xad\x56\x06\xc9\x00\x00\x38\xc0\x2c\xc0\x30\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0\x2b\xc0\x2f\x00\x9e\xc0\x24\xc0\x28\x00\x6b\xc0\x23\xc0\x27\x00\x67\xc0\x0a\xc0\x14\x00\x39\xc0\x09\xc0\x13\x00\x33\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x00\xff\x01\x00\x00\x52\x00\x0b\x00\x04\x03\x00\x01\x02\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x19\x00\x18\x00\x23\x00\x00\x00\x0d\x00\x20\x00\x1e\x06\x01\x06\x02\x06\x03\x05\x01\x05\x02\x05\x03\x04\x01\x04\x02\x04\x03\x03\x01\x03\x02\x03\x03\x02\x01\x02\x02\x02\x03\x00\x10\x00\x08\x00\x06\x05\$alpn\x00\x16\x00\x00\x00\x17\x00\x00hello alpn",
|
||||
# packet with no SNI, no ALPN
|
||||
data => "\x16\x03\x01\x00\xab\x01\x00\x00\xa7\x03\x03\x89\x22\x33\x95\x43\x7a\xc3\x89\x45\x51\x12\x3c\x28\x24\x1b\x6a\x78\xbf\xbe\x95\xd8\x90\x58\xd7\x65\xf7\xbb\x2d\xb2\x8d\xa0\x75\x00\x00\x38\xc0\x2c\xc0\x30\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0\x2b\xc0\x2f\x00\x9e\xc0\x24\xc0\x28\x00\x6b\xc0\x23\xc0\x27\x00\x67\xc0\x0a\xc0\x14\x00\x39\xc0\x09\xc0\x13\x00\x33\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x00\xff\x01\x00\x00\x46\x00\x0b\x00\x04\x03\x00\x01\x02\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x19\x00\x18\x00\x23\x00\x00\x00\x0d\x00\x20\x00\x1e\x06\x01\x06\x02\x06\x03\x05\x01\x05\x02\x05\x03\x04\x01\x04\x02\x04\x03\x03\x01\x03\x02\x03\x03\x02\x01\x02\x02\x02\x03\x00\x16\x00\x00\x00\x17\x00\x00hello tls alone"
|
||||
},
|
||||
'openvpn' => { data => "\x00\x00" },
|
||||
'tinc' => { data => "0 hello" },
|
||||
'xmpp' => {data => "I should get a real jabber connection initialisation here" },
|
||||
'adb' => { data => "CNXN....................host:..." },
|
||||
'anyprot' => {data => "hello anyprot this needs to be longer than xmpp and adb which expect about 50 characters, which I all have to write before the timeout!" },
|
||||
);
|
||||
|
||||
my $pattern = $protocols{$p->{name}}->{data};
|
||||
|
||||
$opts{no_frag} = 1 if $protocols{$p->{name}}->{no_frag};
|
||||
|
||||
if ($p->{sni_hostnames} or $p->{alpn_protocols}) {
|
||||
my $pname = make_sni_alpn_name($p);
|
||||
|
||||
my @sni = @{$p->{sni_hostnames} // [""] };
|
||||
my @alpn = @{$p->{alpn_protocols} // [""] };
|
||||
|
||||
foreach my $sni ( @sni ) {
|
||||
foreach my $alpn ( @alpn ) {
|
||||
print "sni: $sni\nalpn: $alpn\n";
|
||||
$pattern = $protocols{tls}->{
|
||||
"data". ($sni ? "_sni" : "") .
|
||||
($alpn ? "_alpn": "")
|
||||
};
|
||||
$pattern =~ s/(\$\w+)/$1/eeg;
|
||||
|
||||
test_probe(
|
||||
data => $pattern,
|
||||
expected => $pname,
|
||||
%opts
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
test_probe(
|
||||
data => $pattern,
|
||||
expected => $p->{name},
|
||||
%opts
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
# Start an echoserver for each service
|
||||
foreach my $s (@{$conf->fetch_array("protocols")}) {
|
||||
my $prefix = $s->{name};
|
||||
|
||||
$prefix =~ s/^ssl/tls/; # To remove in 1.21
|
||||
|
||||
if ($s->{sni_hostnames} or $s->{alpn_protocols}) {
|
||||
$prefix = make_sni_alpn_name($s);
|
||||
}
|
||||
|
||||
verbose_exec "./echosrv --listen $s->{host}:$s->{port} --prefix '$prefix: '";
|
||||
}
|
||||
|
||||
|
||||
my @binaries = ('sslh-select', 'sslh-fork');
|
||||
for my $binary (@binaries) {
|
||||
warn "Testing $binary\n";
|
||||
@@ -56,10 +195,10 @@ for my $binary (@binaries) {
|
||||
my $sslh_pid;
|
||||
if (!($sslh_pid = fork)) {
|
||||
my $user = (getpwuid $<)[0]; # Run under current username
|
||||
my $cmd = "./$binary -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address --ssl $ssl_address -P $pidfile";
|
||||
warn "$cmd\n";
|
||||
#exec $cmd;
|
||||
exec "valgrind --leak-check=full ./$binary -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address -ssl $ssl_address -P $pidfile";
|
||||
#my $cmd = "./$binary -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address --ssl $ssl_address -P $pidfile";
|
||||
my $cmd = "./$binary -v -f -u $user -Ftest.cfg";
|
||||
verbose_exec $cmd;
|
||||
#exec "valgrind --leak-check=full ./$binary -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address -ssl $ssl_address -P $pidfile";
|
||||
exit 0;
|
||||
}
|
||||
warn "spawned $sslh_pid\n";
|
||||
@@ -67,21 +206,7 @@ for my $binary (@binaries) {
|
||||
|
||||
|
||||
my $test_data = "hello world\n";
|
||||
# my $ssl_test_data = (pack 'n', ((length $test_data) + 2)) . $test_data;
|
||||
my $ssl_test_data = "\x16\x03\x03$test_data\n";
|
||||
|
||||
# Test: SSL connection
|
||||
if ($SSL_CNX) {
|
||||
print "***Test: SSL connection\n";
|
||||
my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
|
||||
warn "$!\n" unless $cnx_l;
|
||||
if (defined $cnx_l) {
|
||||
print $cnx_l $ssl_test_data;
|
||||
my $data;
|
||||
my $n = sysread $cnx_l, $data, 1024;
|
||||
is($data, "ssl: $ssl_test_data", "SSL connection");
|
||||
}
|
||||
}
|
||||
my $ssl_test_data = "\x16\x03\x01\x00\xab\x01\x00\x00\xa7\x03\x03\x89\x22\x33\x95\x43\x7a\xc3\x89\x45\x51\x12\x3c\x28\x24\x1b\x6a\x78\xbf\xbe\x95\xd8\x90\x58\xd7\x65\xf7\xbb\x2d\xb2\x8d\xa0\x75\x00\x00\x38\xc0\x2c\xc0\x30\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0\x2b\xc0\x2f\x00\x9e\xc0\x24\xc0\x28\x00\x6b\xc0\x23\xc0\x27\x00\x67\xc0\x0a\xc0\x14\x00\x39\xc0\x09\xc0\x13\x00\x33\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x00\xff\x01\x00\x00\x46\x00\x0b\x00\x04\x03\x00\x01\x02\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x19\x00\x18\x00\x23\x00\x00\x00\x0d\x00\x20\x00\x1e\x06\x01\x06\x02\x06\x03\x05\x01\x05\x02\x05\x03\x04\x01\x04\x02\x04\x03\x03\x01\x03\x02\x03\x03\x02\x01\x02\x02\x02\x03\x00\x16\x00\x00\x00\x17\x00\x00hello tls alone";
|
||||
|
||||
# Test: Shy SSH connection
|
||||
if ($SSH_SHY_CNX) {
|
||||
@@ -96,35 +221,6 @@ for my $binary (@binaries) {
|
||||
}
|
||||
}
|
||||
|
||||
# Test: Bold SSH connection
|
||||
if ($SSH_BOLD_CNX) {
|
||||
print "***Test: Bold SSH connection\n";
|
||||
my $cnx_h = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
|
||||
warn "$!\n" unless $cnx_h;
|
||||
if (defined $cnx_h) {
|
||||
my $td = "SSH-2.0 testsuite\t$test_data";
|
||||
print $cnx_h $td;
|
||||
my $data = <$cnx_h>;
|
||||
is($data, "ssh: $td", "Bold SSH connection");
|
||||
}
|
||||
}
|
||||
|
||||
# Test: PROBE_AGAIN, incomplete first frame
|
||||
if ($SSH_PROBE_AGAIN) {
|
||||
print "***Test: incomplete SSH first frame\n";
|
||||
my $cnx_h = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
|
||||
warn "$!\n" unless $cnx_h;
|
||||
if (defined $cnx_h) {
|
||||
my $td = "SSH-2.0 testsuite\t$test_data";
|
||||
print $cnx_h substr $td, 0, 2;
|
||||
sleep 1;
|
||||
print $cnx_h substr $td, 2;
|
||||
my $data = <$cnx_h>;
|
||||
is($data, "ssh: $td", "Incomplete first SSH frame");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Test: One SSL half-started then one SSH
|
||||
if ($SSL_MIX_SSH) {
|
||||
print "***Test: One SSL half-started then one SSH\n";
|
||||
@@ -142,7 +238,7 @@ for my $binary (@binaries) {
|
||||
}
|
||||
my $data;
|
||||
my $n = sysread $cnx_l, $data, 1024;
|
||||
is($data, "ssl: $ssl_test_data", "SSL connection interrupted by SSH");
|
||||
is($data, "tls: $ssl_test_data", "SSL connection interrupted by SSH");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +255,7 @@ for my $binary (@binaries) {
|
||||
print $cnx_l $ssl_test_data;
|
||||
my $data;
|
||||
my $n = sysread $cnx_l, $data, 1024;
|
||||
is($data, "ssl: $ssl_test_data", "SSL during SSH being established");
|
||||
is($data, "tls: $ssl_test_data", "SSL during SSH being established");
|
||||
}
|
||||
print $cnx_h $test_data;
|
||||
my $data = <$cnx_h>;
|
||||
@@ -168,53 +264,12 @@ for my $binary (@binaries) {
|
||||
}
|
||||
|
||||
|
||||
# Test: Big messages (careful: don't go over echosrv's buffer limit (1M))
|
||||
if ($BIG_MSG) {
|
||||
print "***Test: big message\n";
|
||||
my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
|
||||
warn "$!\n" unless $cnx_l;
|
||||
my $rept = 1000;
|
||||
my $test_data2 = $ssl_test_data . ("helloworld"x$rept);
|
||||
if (defined $cnx_l) {
|
||||
my $n = syswrite $cnx_l, $test_data2;
|
||||
my ($data);
|
||||
$n = sysread $cnx_l, $data, 1 << 20;
|
||||
is($data, "ssl: ". $test_data2, "Big message");
|
||||
}
|
||||
if ($PROBES_NOFRAG) {
|
||||
test_probes(no_frag => 1);
|
||||
}
|
||||
|
||||
# Test: Stalled connection
|
||||
# Create two connections, stall one, check the other one
|
||||
# works, unstall first and check it works fine
|
||||
# This test needs fixing.
|
||||
# Now that echosrv no longer works on "lines" (finishing
|
||||
# with '\n'), it may cut blocks randomly with prefixes.
|
||||
# The whole thing needs to be re-thought as it'll only
|
||||
# work by chance.
|
||||
if ($STALL_CNX) {
|
||||
print "***Test: Stalled connection\n";
|
||||
my $cnx_1 = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
|
||||
warn "$!\n" unless defined $cnx_1;
|
||||
my $cnx_2 = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
|
||||
warn "$!\n" unless defined $cnx_2;
|
||||
my $test_data2 = "helloworld";
|
||||
sleep 4;
|
||||
my $rept = 1000;
|
||||
if (defined $cnx_1 and defined $cnx_2) {
|
||||
print $cnx_1 ($test_data2 x $rept);
|
||||
print $cnx_1 "\n";
|
||||
print $cnx_2 ($test_data2 x $rept);
|
||||
print $cnx_2 "\n";
|
||||
my $data = <$cnx_2>;
|
||||
is($data, "ssh: " . ($test_data2 x $rept) . "\n", "Stalled connection (1)");
|
||||
print $cnx_2 ($test_data2 x $rept);
|
||||
print $cnx_2 "\n";
|
||||
$data = <$cnx_2>;
|
||||
is($data, "ssh: " . ($test_data2 x $rept) . "\n", "Stalled connection (2)");
|
||||
$data = <$cnx_1>;
|
||||
is($data, "ssh: " . ($test_data2 x $rept) . "\n", "Stalled connection (3)");
|
||||
|
||||
}
|
||||
if ($PROBES_AGAIN) {
|
||||
test_probes;
|
||||
}
|
||||
|
||||
my $pid = `cat $pidfile`;
|
||||
@@ -228,7 +283,6 @@ if ($RB_CNX_NOSERVER) {
|
||||
print "***Test: Connecting to non-existant server\n";
|
||||
my $sslh_pid;
|
||||
if (!($sslh_pid = fork)) {
|
||||
my $user = (getpwuid $<)[0]; # Run under current username
|
||||
exec "./sslh-select -v -f -u $user --listen localhost:$sslh_port --ssh localhost:$no_listen --ssl localhost:$no_listen -P $pidfile";
|
||||
}
|
||||
warn "spawned $sslh_pid\n";
|
||||
@@ -249,12 +303,19 @@ if ($RB_CNX_NOSERVER) {
|
||||
}
|
||||
|
||||
|
||||
my $ssh_conf = (grep { $_->{name} eq "ssh" } @{$conf->fetch_array("protocols")})[0];
|
||||
my $ssh_address = $ssh_conf->{host} . ":" . $ssh_conf->{port};
|
||||
|
||||
# Use the last TLS echoserv (no SNI/ALPN)
|
||||
my $ssl_conf = (grep { $_->{name} eq "tls" } @{$conf->fetch_array("protocols")})[-1];
|
||||
my $ssl_address = $ssl_conf->{host} . ":" . $ssl_conf->{port};
|
||||
|
||||
|
||||
# Robustness: No hostname in address
|
||||
if ($RB_PARAM_NOHOST) {
|
||||
print "***Test: No hostname in address\n";
|
||||
my $sslh_pid;
|
||||
if (!($sslh_pid = fork)) {
|
||||
my $user = (getpwuid $<)[0]; # Run under current username
|
||||
exec "./sslh-select -v -f -u $user --listen $sslh_port --ssh $ssh_address --ssl $ssl_address -P $pidfile";
|
||||
}
|
||||
warn "spawned $sslh_pid\n";
|
||||
@@ -269,7 +330,6 @@ if ($RB_WRONG_USERNAME) {
|
||||
print "***Test: Changing to non-existant username\n";
|
||||
my $sslh_pid;
|
||||
if (!($sslh_pid = fork)) {
|
||||
my $user = (getpwuid $<)[0]; # Run under current username
|
||||
exec "./sslh-select -v -f -u ${user}_doesnt_exist --listen localhost:$sslh_port --ssh $ssh_address --ssl $ssl_address -P $pidfile";
|
||||
}
|
||||
warn "spawned $sslh_pid\n";
|
||||
@@ -284,7 +344,6 @@ if ($RB_OPEN_PID_FILE) {
|
||||
print "***Test: Can't open PID file\n";
|
||||
my $sslh_pid;
|
||||
if (!($sslh_pid = fork)) {
|
||||
my $user = (getpwuid $<)[0]; # Run under current username
|
||||
exec "./sslh-select -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address --ssl $ssl_address -P /dont_exist/$pidfile";
|
||||
# You don't have a /dont_exist/ directory, do you?!
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Configuration file for testing (use both by sslh under
|
||||
# test and the test script `t`)
|
||||
|
||||
verbose: 2;
|
||||
foreground: true;
|
||||
inetd: false;
|
||||
numeric: false;
|
||||
transparent: false;
|
||||
timeout: 10; # Probe test writes slowly
|
||||
pidfile: "/tmp/sslh_test.pid";
|
||||
|
||||
syslog_facility: "auth";
|
||||
|
||||
|
||||
# List of interfaces on which we should listen
|
||||
# Options:
|
||||
listen:
|
||||
(
|
||||
{ host: "localhost"; port: "8080"; keepalive: true; },
|
||||
{ host: "localhost"; port: "8081"; keepalive: true; }
|
||||
);
|
||||
|
||||
|
||||
protocols:
|
||||
(
|
||||
{ name: "ssh"; host: "localhost"; port: "9000"; fork: true; },
|
||||
{ name: "socks5"; host: "localhost"; port: "9001"; },
|
||||
{ name: "http"; host: "localhost"; port: "9002"; },
|
||||
{ name: "tinc"; host: "localhost"; port: "9003"; },
|
||||
{ name: "openvpn"; host: "localhost"; port: "9004"; },
|
||||
{ name: "xmpp"; host: "localhost"; port: "9009"; },
|
||||
{ name: "adb"; host: "localhost"; port: "9010"; },
|
||||
{ name: "tls"; host: "localhost"; port: "9021"; alpn_protocols: [ "alpn1", "alpn2" ]; sni_hostnames: [ "sni1" ]; },
|
||||
{ name: "ssl"; host: "localhost"; port: "9022"; alpn_protocols: [ "alpn1", "alpn2" ]; sni_hostnames: [ "sni2", "sni3" ]; },
|
||||
{ name: "tls"; host: "localhost"; port: "9023"; alpn_protocols: [ "alpn3" ]; },
|
||||
{ name: "tls"; host: "localhost"; port: "9024"; sni_hostnames: [ "sni3" ]; },
|
||||
{ name: "ssl"; host: "localhost"; port: "9025"; },
|
||||
{ name: "anyprot"; host: "localhost"; port: "9099"; }
|
||||
);
|
||||
|
||||
on-timeout: "ssh";
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* Copyright (c) 2011 and 2012, Dustin Lundquist <dustin@null-ptr.net>
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
* This is a minimal TLS implementation intended only to parse the server name
|
||||
* extension. This was created based primarily on Wireshark dissection of a
|
||||
* TLS handshake and RFC4366.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h> /* malloc() */
|
||||
#include <fnmatch.h> /* fnmatch() */
|
||||
#include "tls.h"
|
||||
|
||||
#define TLS_HEADER_LEN 5
|
||||
#define TLS_HANDSHAKE_CONTENT_TYPE 0x16
|
||||
#define TLS_HANDSHAKE_TYPE_CLIENT_HELLO 0x01
|
||||
|
||||
#ifndef MIN
|
||||
#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
int tls_match_sni : 1;
|
||||
int tls_match_alpn : 1;
|
||||
} TLS_MATCHMODE;
|
||||
|
||||
struct TLSProtocol {
|
||||
TLS_MATCHMODE match_mode;
|
||||
char** sni_hostname_list;
|
||||
char** alpn_protocol_list;
|
||||
};
|
||||
|
||||
static int parse_extensions(const struct TLSProtocol *, const char *, size_t);
|
||||
static int parse_server_name_extension(const struct TLSProtocol *, const char *, size_t);
|
||||
static int parse_alpn_extension(const struct TLSProtocol *, const char *, size_t);
|
||||
static int has_match(char**, const char*, size_t);
|
||||
|
||||
/* Parse a TLS packet for the Server Name Indication and ALPN extension in the client
|
||||
* hello handshake, returning a status code
|
||||
*
|
||||
* Returns:
|
||||
* 0: no match
|
||||
* 1: match
|
||||
* < 0: error code (see tls.h)
|
||||
*/
|
||||
int
|
||||
parse_tls_header(const struct TLSProtocol *tls_data, const char *data, size_t data_len) {
|
||||
char tls_content_type;
|
||||
char tls_version_major;
|
||||
char tls_version_minor;
|
||||
size_t pos = TLS_HEADER_LEN;
|
||||
size_t len;
|
||||
|
||||
/* Check that our TCP payload is at least large enough for a TLS header */
|
||||
if (data_len < TLS_HEADER_LEN)
|
||||
return TLS_ELENGTH;
|
||||
|
||||
tls_content_type = data[0];
|
||||
if (tls_content_type != TLS_HANDSHAKE_CONTENT_TYPE) {
|
||||
if (verbose) fprintf(stderr, "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 (verbose) fprintf(stderr, "Received SSL %d.%d handshake which cannot be parsed.\n",
|
||||
tls_version_major, tls_version_minor);
|
||||
|
||||
return TLS_EVERSION;
|
||||
}
|
||||
|
||||
/* TLS record length */
|
||||
len = ((unsigned char)data[3] << 8) +
|
||||
(unsigned char)data[4] + TLS_HEADER_LEN;
|
||||
data_len = MIN(data_len, len);
|
||||
|
||||
/* Check we received entire TLS record length */
|
||||
if (data_len < len)
|
||||
return TLS_ELENGTH;
|
||||
|
||||
/*
|
||||
* Handshake
|
||||
*/
|
||||
if (pos + 1 > data_len) {
|
||||
return TLS_EPROTOCOL;
|
||||
}
|
||||
if (data[pos] != TLS_HANDSHAKE_TYPE_CLIENT_HELLO) {
|
||||
if (verbose) fprintf(stderr, "Not a client hello\n");
|
||||
|
||||
return TLS_EPROTOCOL;
|
||||
}
|
||||
|
||||
/* Skip past fixed length records:
|
||||
1 Handshake Type
|
||||
3 Length
|
||||
2 Version (again)
|
||||
32 Random
|
||||
to Session ID Length
|
||||
*/
|
||||
pos += 38;
|
||||
|
||||
/* Session ID */
|
||||
if (pos + 1 > data_len)
|
||||
return TLS_EPROTOCOL;
|
||||
len = (unsigned char)data[pos];
|
||||
pos += 1 + len;
|
||||
|
||||
/* Cipher Suites */
|
||||
if (pos + 2 > data_len)
|
||||
return TLS_EPROTOCOL;
|
||||
len = ((unsigned char)data[pos] << 8) + (unsigned char)data[pos + 1];
|
||||
pos += 2 + len;
|
||||
|
||||
/* Compression Methods */
|
||||
if (pos + 1 > data_len)
|
||||
return TLS_EPROTOCOL;
|
||||
len = (unsigned char)data[pos];
|
||||
pos += 1 + len;
|
||||
|
||||
if (pos == data_len && tls_version_major == 3 && tls_version_minor == 0) {
|
||||
if (verbose) fprintf(stderr, "Received SSL 3.0 handshake without extensions\n");
|
||||
return TLS_EVERSION;
|
||||
}
|
||||
|
||||
/* Extensions */
|
||||
if (pos + 2 > data_len)
|
||||
return TLS_EPROTOCOL;
|
||||
len = ((unsigned char)data[pos] << 8) + (unsigned char)data[pos + 1];
|
||||
pos += 2;
|
||||
|
||||
if (pos + len > data_len)
|
||||
return TLS_EPROTOCOL;
|
||||
|
||||
/* By now we know it's TLS. if SNI or ALPN is set, parse extensions to see if
|
||||
* they match. Otherwise, it's a match already */
|
||||
if (tls_data &&
|
||||
(tls_data->match_mode.tls_match_alpn || tls_data->match_mode.tls_match_sni)) {
|
||||
return parse_extensions(tls_data, data + pos, len);
|
||||
} else {
|
||||
return TLS_MATCH;
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
parse_extensions(const struct TLSProtocol *tls_data, const char *data, size_t data_len) {
|
||||
size_t pos = 0;
|
||||
size_t len;
|
||||
int sni_match = 0, alpn_match = 0;
|
||||
|
||||
if (tls_data == NULL)
|
||||
return TLS_EINVAL;
|
||||
|
||||
/* Parse each 4 bytes for the extension header */
|
||||
while (pos + 4 <= data_len) {
|
||||
/* Extension Length */
|
||||
len = ((unsigned char) data[pos + 2] << 8) +
|
||||
(unsigned char) data[pos + 3];
|
||||
|
||||
if (pos + 4 + len > data_len)
|
||||
return TLS_EPROTOCOL;
|
||||
|
||||
size_t extension_type = ((unsigned char) data[pos] << 8) +
|
||||
(unsigned char) data[pos + 1];
|
||||
|
||||
if (extension_type == 0x00 && tls_data->match_mode.tls_match_sni) { /* Server Name */
|
||||
sni_match = parse_server_name_extension(tls_data, data + pos + 4, len);
|
||||
if (sni_match < 0) return sni_match;
|
||||
} else if (extension_type == 0x10 && tls_data->match_mode.tls_match_alpn) { /* ALPN */
|
||||
alpn_match = parse_alpn_extension(tls_data, data + pos + 4, len);
|
||||
if (alpn_match < 0) return alpn_match;
|
||||
}
|
||||
|
||||
pos += 4 + len; /* Advance to the next extension header */
|
||||
}
|
||||
|
||||
/* Check we ended where we expected to */
|
||||
if (pos != data_len)
|
||||
return TLS_EPROTOCOL;
|
||||
|
||||
return (sni_match && alpn_match)
|
||||
|| (!tls_data->match_mode.tls_match_sni && alpn_match)
|
||||
|| (!tls_data->match_mode.tls_match_alpn && sni_match);
|
||||
}
|
||||
|
||||
static int
|
||||
parse_server_name_extension(const struct TLSProtocol *tls_data, const char *data, size_t data_len) {
|
||||
size_t pos = 2; /* skip server name list length */
|
||||
size_t len;
|
||||
|
||||
while (pos + 3 < data_len) {
|
||||
len = ((unsigned char)data[pos + 1] << 8) +
|
||||
(unsigned char)data[pos + 2];
|
||||
|
||||
if (pos + 3 + len > data_len)
|
||||
return TLS_EPROTOCOL;
|
||||
|
||||
switch (data[pos]) { /* name type */
|
||||
case 0x00: /* host_name */
|
||||
if(has_match(tls_data->sni_hostname_list, data + pos + 3, len)) {
|
||||
return len;
|
||||
} else {
|
||||
return TLS_ENOEXT;
|
||||
}
|
||||
default:
|
||||
if (verbose) fprintf(stderr, "Unknown server name extension name type: %d\n",
|
||||
data[pos]);
|
||||
}
|
||||
pos += 3 + len;
|
||||
}
|
||||
/* Check we ended where we expected to */
|
||||
if (pos != data_len)
|
||||
return TLS_EPROTOCOL;
|
||||
|
||||
return TLS_ENOEXT;
|
||||
}
|
||||
|
||||
static int
|
||||
parse_alpn_extension(const struct TLSProtocol *tls_data, const char *data, size_t data_len) {
|
||||
size_t pos = 2;
|
||||
size_t len;
|
||||
|
||||
while (pos + 1 < data_len) {
|
||||
len = (unsigned char)data[pos];
|
||||
|
||||
if (pos + 1 + len > data_len)
|
||||
return TLS_EPROTOCOL;
|
||||
|
||||
if (len > 0 && has_match(tls_data->alpn_protocol_list, data + pos + 1, len)) {
|
||||
return len;
|
||||
} else if (len > 0) {
|
||||
if (verbose) fprintf(stderr, "Unknown ALPN name: %.*s\n", (int)len, data + pos + 1);
|
||||
}
|
||||
pos += 1 + len;
|
||||
}
|
||||
/* Check we ended where we expected to */
|
||||
if (pos != data_len)
|
||||
return TLS_EPROTOCOL;
|
||||
|
||||
return TLS_ENOEXT;
|
||||
}
|
||||
|
||||
static int
|
||||
has_match(char** list, const char* name, size_t name_len) {
|
||||
char **item;
|
||||
char *name_nullterminated = malloc(name_len+1);
|
||||
CHECK_ALLOC(name_nullterminated, "malloc");
|
||||
memcpy(name_nullterminated, name, name_len);
|
||||
name_nullterminated[name_len]='\0';
|
||||
|
||||
for (item = list; *item; item++) {
|
||||
if (verbose) fprintf(stderr, "matching [%.*s] with [%s]\n", (int)name_len, name, *item);
|
||||
if(!fnmatch(*item, name_nullterminated, 0)) {
|
||||
free(name_nullterminated);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
free(name_nullterminated);
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct TLSProtocol *
|
||||
new_tls_data() {
|
||||
struct TLSProtocol *tls_data = malloc(sizeof(struct TLSProtocol));
|
||||
CHECK_ALLOC(tls_data, "malloc");
|
||||
|
||||
memset(tls_data, 0, sizeof(*tls_data));
|
||||
|
||||
return tls_data;
|
||||
}
|
||||
|
||||
struct TLSProtocol *
|
||||
tls_data_set_list(struct TLSProtocol *tls_data, int alpn, char** list) {
|
||||
if (alpn) {
|
||||
tls_data->alpn_protocol_list = list;
|
||||
tls_data->match_mode.tls_match_alpn = 1;
|
||||
} else {
|
||||
tls_data->sni_hostname_list = list;
|
||||
tls_data->match_mode.tls_match_sni = 1;
|
||||
}
|
||||
|
||||
return tls_data;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2011 and 2012, Dustin Lundquist <dustin@null-ptr.net>
|
||||
* 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 TLS_H
|
||||
#define TLS_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
struct TLSProtocol;
|
||||
|
||||
int parse_tls_header(const struct TLSProtocol *tls_data, const char *data, size_t data_len);
|
||||
|
||||
struct TLSProtocol *new_tls_data();
|
||||
struct TLSProtocol *tls_data_set_list(struct TLSProtocol *, int, char**);
|
||||
|
||||
#define TLS_MATCH 1
|
||||
#define TLS_NOMATCH 0
|
||||
|
||||
#define TLS_EINVAL -1 /* Invalid parameter (NULL data pointer) */
|
||||
#define TLS_ELENGTH -2 /* Incomplete request */
|
||||
#define TLS_EVERSION -3 /* TLS version that cannot be parsed */
|
||||
#define TLS_ENOEXT -4 /* No ALPN or SNI extension found */
|
||||
#define TLS_EPROTOCOL -5 /* Protocol error */
|
||||
|
||||
|
||||
#endif
|
||||
Reference in new issue
Block a user