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 |
No files matched your search
@@ -1,3 +1,33 @@
|
||||
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.
|
||||
|
||||
@@ -205,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
|
||||
@@ -235,46 +235,46 @@ The firewalling tables also need to be adjusted as follows.
|
||||
I don't think it is possible to have `httpd` and `sslh` both listen to 443 in
|
||||
this scheme -- let me know if you manage that:
|
||||
|
||||
$ # Set route_localnet = 1 on all interfaces so that ssl can use "localhost" as destination
|
||||
$ sysctl -w net.ipv4.conf.default.route_localnet=1
|
||||
$ sysctl -w net.ipv4.conf.all.route_localnet=1
|
||||
# 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
|
||||
# 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
|
||||
# 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
|
||||
# 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
|
||||
# Configure routing for those marked packets
|
||||
ip rule add fwmark 0x1 lookup 100
|
||||
ip route add local 0.0.0.0/0 dev lo table 100
|
||||
|
||||
Tranparent proxying with IPv6 is similarly set up as follows:
|
||||
|
||||
$ # Set route_localnet = 1 on all interfaces so that ssl can use "localhost" as destination
|
||||
$ # Not sure if this is needed for ipv6 though
|
||||
$ sysctl -w net.ipv4.conf.default.route_localnet=1
|
||||
$ sysctl -w net.ipv4.conf.all.route_localnet=1
|
||||
# Set route_localnet = 1 on all interfaces so that ssl can use "localhost" as destination
|
||||
# Not sure if this is needed for ipv6 though
|
||||
sysctl -w net.ipv4.conf.default.route_localnet=1
|
||||
sysctl -w net.ipv4.conf.all.route_localnet=1
|
||||
|
||||
$ # DROP martian packets as they would have been if route_localnet was zero
|
||||
$ # Note: packets not leaving the server aren't affected by this, thus sslh will still work
|
||||
$ ip6tables -t raw -A PREROUTING ! -i lo -d ::1/128 -j DROP
|
||||
$ ip6tables -t mangle -A POSTROUTING ! -o lo -s ::1/128 -j DROP
|
||||
# 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
|
||||
# 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
|
||||
# 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
|
||||
# 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
|
||||
|
||||
@@ -84,6 +84,7 @@ int get_fd_sockets(int *sockfd[])
|
||||
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;
|
||||
}
|
||||
@@ -116,10 +117,16 @@ int start_listen_sockets(int *sockfd[], struct addrinfo *addr_list)
|
||||
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) {
|
||||
@@ -161,6 +168,48 @@ int start_listen_sockets(int *sockfd[], struct addrinfo *addr_list)
|
||||
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
|
||||
@@ -178,39 +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 */
|
||||
struct ifaddrs *ifaddrs_p = NULL, *ifa;
|
||||
|
||||
getifaddrs(&ifaddrs_p);
|
||||
|
||||
for (ifa = ifaddrs_p; ifa != NULL; ifa = ifa->ifa_next)
|
||||
{
|
||||
if (!ifa->ifa_addr)
|
||||
continue;
|
||||
int match = 0;
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (match) /* the destination is the same as the source, should not create a transparent bind */
|
||||
return 0;
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -295,10 +316,7 @@ int defer_write(struct queue *q, void* data, int data_size)
|
||||
fprintf(stderr, "**** writing deferred on fd %d\n", q->fd);
|
||||
|
||||
p = realloc(q->begin_deferred_data, data_offset + q->deferred_data_size + data_size);
|
||||
if (!p) {
|
||||
perror("realloc");
|
||||
exit(1);
|
||||
}
|
||||
CHECK_ALLOC(p, "realloc");
|
||||
|
||||
q->begin_deferred_data = p;
|
||||
q->deferred_data = p + data_offset;
|
||||
@@ -477,6 +495,7 @@ int resolve_split_name(struct addrinfo **out, const char* ct_host, const char* s
|
||||
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 */
|
||||
@@ -664,7 +683,7 @@ void setup_syslog(const char* bin_name) {
|
||||
for (fn = 0; facilitynames[fn].c_val != -1; fn++)
|
||||
if (strcmp(facilitynames[fn].c_name, facility) == 0)
|
||||
break;
|
||||
if (fn == -1) {
|
||||
if (facilitynames[fn].c_val == -1) {
|
||||
fprintf(stderr, "Unknown facility %s\n", facility);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,13 @@
|
||||
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
|
||||
|
||||
+8
-3
@@ -43,20 +43,25 @@ listen:
|
||||
# (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.
|
||||
#
|
||||
# sslh will try each probe in order they are declared, and
|
||||
# connect to the first that matches.
|
||||
#
|
||||
# You can specify several of 'regex' and 'tls'.
|
||||
|
||||
protocols:
|
||||
|
||||
@@ -40,6 +40,7 @@ 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
|
||||
@@ -51,9 +52,9 @@ static struct proto builtins[] = {
|
||||
{ "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 },
|
||||
{ "ssl", NULL, NULL, 1, 0, 0, is_tls_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 }
|
||||
};
|
||||
|
||||
@@ -178,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)
|
||||
@@ -192,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? */
|
||||
@@ -221,45 +225,15 @@ static int is_http_protocol(const char *p, int len, struct proto *proto)
|
||||
return PROBE_NEXT;
|
||||
}
|
||||
|
||||
static int is_sni_alpn_protocol(const char *p, int len, struct proto *proto)
|
||||
{
|
||||
int valid_tls;
|
||||
|
||||
valid_tls = parse_tls_header(proto->data, p, len);
|
||||
|
||||
if(valid_tls < 0)
|
||||
return -1 == valid_tls ? PROBE_AGAIN : PROBE_NEXT;
|
||||
|
||||
/* There *was* a valid match */
|
||||
return PROBE_MATCH;
|
||||
}
|
||||
|
||||
/* 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 < 6)
|
||||
return PROBE_AGAIN;
|
||||
|
||||
/* TLS packet starts with a record "Hello" (0x16), followed by the number of
|
||||
* the highest version of SSL/TLS supported.
|
||||
*
|
||||
* A SSLv2 record header contains a two or three byte length code. If the
|
||||
* most significant bit is set in the first byte of the record length code
|
||||
* then the record has no padding and the total header length will be 2
|
||||
* bytes, otherwise the record has padding and the total header length will
|
||||
* be 3 bytes. Next, a 1 char sized client-hello (0x01) is expected,
|
||||
* followed by a 2 char sized version that indicates the highest version of
|
||||
* TLS/SSL supported by the sender. [SSL2] Hickman, Kipp, "The SSL Protocol"
|
||||
*
|
||||
* We're checking the highest version of TLS/SSL supported against
|
||||
* (0x03 0x00-0x03) (RFC6101 A.1). This means we reject the usage of SSLv2
|
||||
* and lower, which is actually a good thing (RFC6176).
|
||||
*/
|
||||
if (p[0] == 0x16) // TLS client-hello
|
||||
return p[1] == 0x03 && ( p[2] >= 0 && p[2] <= 0x03);
|
||||
if ((p[0] & 0x80) != 0) // SSLv2 client-hello, no padding
|
||||
return p[2] == 0x01 && p[3] == 0x03 && ( p[4] >= 0 && p[4] <= 0x03);
|
||||
else // SSLv2 client-hello, padded
|
||||
return p[3] == 0x01 && p[4] == 0x03 && ( p[5] >= 0 && p[5] <= 0x03);
|
||||
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)
|
||||
@@ -311,6 +285,39 @@ static int is_adb_protocol(const char *p, int len, struct proto *proto)
|
||||
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
|
||||
@@ -337,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
|
||||
@@ -347,36 +354,40 @@ int probe_client_protocol(struct connection *cnx)
|
||||
* function does not have to deal with a specific failure condition (the
|
||||
* connection will just fail later normally). */
|
||||
|
||||
/* Dump hex values of the packet */
|
||||
if (verbose>1) {
|
||||
fprintf(stderr, "hexdump of incoming packet:\n");
|
||||
hexdump(buffer, n);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -408,10 +419,6 @@ T_PROBE* get_probe(const char* description) {
|
||||
if (!strcmp(description, "regex"))
|
||||
return regex_probe;
|
||||
|
||||
/* Special case of "sni/alpn" probe for same reason as above*/
|
||||
if (!strcmp(description, "sni_alpn"))
|
||||
return is_sni_alpn_protocol;
|
||||
|
||||
/* 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"))
|
||||
|
||||
@@ -4,7 +4,7 @@ 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
|
||||
|
||||
@@ -145,6 +145,7 @@ 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++) {
|
||||
|
||||
+61
-8
@@ -40,7 +40,7 @@ const char* USAGE_STRING =
|
||||
"sslh " VERSION "\n" \
|
||||
"usage:\n" \
|
||||
"\tsslh [-v] [-i] [-V] [-f] [-n] [--transparent] [-F<file>]\n"
|
||||
"\t[-t <timeout>] [-P <pidfile>] [-u <username>] [-C <chroot>] -p <add> [-p <addr> ...] \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" \
|
||||
@@ -147,6 +147,32 @@ static void printsettings(void)
|
||||
}
|
||||
|
||||
|
||||
/* 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
|
||||
*/
|
||||
@@ -210,14 +236,21 @@ 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);
|
||||
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);
|
||||
@@ -235,7 +268,7 @@ static void setup_regex_probe(struct proto *p, config_setting_t* probes)
|
||||
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;
|
||||
const char * config_item, *server_name;
|
||||
char** sni_hostname_list;
|
||||
|
||||
num_probes = config_setting_length(config_items);
|
||||
@@ -246,16 +279,27 @@ static void setup_sni_alpn_list(struct proto *p, config_setting_t* config_items,
|
||||
|
||||
max_server_name_len = 0;
|
||||
for (i = 0; i < num_probes; i++) {
|
||||
server_name_len = strlen(config_setting_get_string_elem(config_items, 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]);
|
||||
}
|
||||
@@ -272,11 +316,9 @@ static void setup_sni_alpn(struct proto *p, config_setting_t* prot)
|
||||
alpn_protocols = config_setting_get_member(prot, "alpn_protocols");
|
||||
|
||||
if(sni_hostnames && config_setting_is_array(sni_hostnames)) {
|
||||
p->probe = get_probe("sni_alpn");
|
||||
setup_sni_alpn_list(p, sni_hostnames, "sni_hostnames", 0);
|
||||
}
|
||||
if(alpn_protocols && config_setting_is_array(alpn_protocols)) {
|
||||
p->probe = get_probe("sni_alpn");
|
||||
setup_sni_alpn_list(p, alpn_protocols, "alpn_protocols", 1);
|
||||
}
|
||||
}
|
||||
@@ -289,7 +331,8 @@ static void setup_sni_alpn(struct proto *p, config_setting_t* prot)
|
||||
static int config_protocols(config_t *config, struct proto **prots)
|
||||
{
|
||||
config_setting_t *setting, *prot, *patterns;
|
||||
const char *hostname, *port, *name;
|
||||
const char *hostname, *port, *cfg_name;
|
||||
char* name;
|
||||
int i, num_prots;
|
||||
struct proto *p, *prev = NULL;
|
||||
|
||||
@@ -298,15 +341,20 @@ 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);
|
||||
@@ -322,7 +370,7 @@ static int config_protocols(config_t *config, struct proto **prots)
|
||||
}
|
||||
|
||||
p->probe = get_probe(name);
|
||||
if (!p->probe || !strcmp(name, "sni_alpn")) {
|
||||
if (!p->probe) {
|
||||
fprintf(stderr, "line %d: %s: probe unknown\n", config_setting_source_line(prot), name);
|
||||
exit(1);
|
||||
}
|
||||
@@ -438,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());
|
||||
}
|
||||
@@ -455,6 +504,8 @@ 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
|
||||
@@ -516,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));
|
||||
|
||||
+3
-1
@@ -301,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
|
||||
@@ -321,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]);
|
||||
|
||||
|
||||
@@ -95,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>
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
#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 = (char*)malloc(strlen(hostname)+strlen(port)+2);
|
||||
char *conn = malloc(strlen(hostname)+strlen(port)+2);
|
||||
CHECK_ALLOC(conn, "malloc");
|
||||
strcpy(conn, hostname);
|
||||
strcat(conn, ":");
|
||||
strcat(conn, port);
|
||||
@@ -41,6 +43,7 @@ static int get_listen_from_conf(const char *filename, char **listen[]) {
|
||||
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) &&
|
||||
@@ -51,6 +54,7 @@ static int get_listen_from_conf(const char *filename, char **listen[]) {
|
||||
return -1;
|
||||
} else {
|
||||
(*listen)[i] = malloc(strlen(resolve_listen(hostname, port)));
|
||||
CHECK_ALLOC((*listen)[i], "malloc");
|
||||
strcpy((*listen)[i], resolve_listen(hostname, port));
|
||||
}
|
||||
}
|
||||
@@ -115,6 +119,7 @@ static int gen_sslh_config(char *runtime_unit_dir) {
|
||||
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");
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -41,9 +41,13 @@
|
||||
#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 {
|
||||
int use_alpn;
|
||||
TLS_MATCHMODE match_mode;
|
||||
char** sni_hostname_list;
|
||||
char** alpn_protocol_list;
|
||||
};
|
||||
@@ -57,12 +61,9 @@ static int has_match(char**, const char*, size_t);
|
||||
* hello handshake, returning a status code
|
||||
*
|
||||
* Returns:
|
||||
* >=0 - length of the hostname and updates *hostname
|
||||
* caller is responsible for freeing *hostname
|
||||
* -1 - Incomplete request
|
||||
* -2 - No Host header included in this request
|
||||
* -3 - Invalid hostname pointer
|
||||
* < -4 - Invalid TLS client hello
|
||||
* 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) {
|
||||
@@ -74,12 +75,12 @@ parse_tls_header(const struct TLSProtocol *tls_data, const char *data, size_t da
|
||||
|
||||
/* Check that our TCP payload is at least large enough for a TLS header */
|
||||
if (data_len < TLS_HEADER_LEN)
|
||||
return -1;
|
||||
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 -5;
|
||||
return TLS_EPROTOCOL;
|
||||
}
|
||||
|
||||
tls_version_major = data[1];
|
||||
@@ -88,7 +89,7 @@ parse_tls_header(const struct TLSProtocol *tls_data, const char *data, size_t da
|
||||
if (verbose) fprintf(stderr, "Received SSL %d.%d handshake which cannot be parsed.\n",
|
||||
tls_version_major, tls_version_minor);
|
||||
|
||||
return -2;
|
||||
return TLS_EVERSION;
|
||||
}
|
||||
|
||||
/* TLS record length */
|
||||
@@ -98,18 +99,18 @@ parse_tls_header(const struct TLSProtocol *tls_data, const char *data, size_t da
|
||||
|
||||
/* Check we received entire TLS record length */
|
||||
if (data_len < len)
|
||||
return -1;
|
||||
return TLS_ELENGTH;
|
||||
|
||||
/*
|
||||
* Handshake
|
||||
*/
|
||||
if (pos + 1 > data_len) {
|
||||
return -5;
|
||||
return TLS_EPROTOCOL;
|
||||
}
|
||||
if (data[pos] != TLS_HANDSHAKE_TYPE_CLIENT_HELLO) {
|
||||
if (verbose) fprintf(stderr, "Not a client hello\n");
|
||||
|
||||
return -5;
|
||||
return TLS_EPROTOCOL;
|
||||
}
|
||||
|
||||
/* Skip past fixed length records:
|
||||
@@ -123,46 +124,54 @@ parse_tls_header(const struct TLSProtocol *tls_data, const char *data, size_t da
|
||||
|
||||
/* Session ID */
|
||||
if (pos + 1 > data_len)
|
||||
return -5;
|
||||
return TLS_EPROTOCOL;
|
||||
len = (unsigned char)data[pos];
|
||||
pos += 1 + len;
|
||||
|
||||
/* Cipher Suites */
|
||||
if (pos + 2 > data_len)
|
||||
return -5;
|
||||
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 -5;
|
||||
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 -2;
|
||||
return TLS_EVERSION;
|
||||
}
|
||||
|
||||
/* Extensions */
|
||||
if (pos + 2 > data_len)
|
||||
return -5;
|
||||
return TLS_EPROTOCOL;
|
||||
len = ((unsigned char)data[pos] << 8) + (unsigned char)data[pos + 1];
|
||||
pos += 2;
|
||||
|
||||
if (pos + len > data_len)
|
||||
return -5;
|
||||
return parse_extensions(tls_data, data + pos, 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 last_matched = 0;
|
||||
int sni_match = 0, alpn_match = 0;
|
||||
|
||||
if (tls_data == NULL)
|
||||
return -3;
|
||||
return TLS_EINVAL;
|
||||
|
||||
/* Parse each 4 bytes for the extension header */
|
||||
while (pos + 4 <= data_len) {
|
||||
@@ -171,54 +180,17 @@ parse_extensions(const struct TLSProtocol *tls_data, const char *data, size_t da
|
||||
(unsigned char) data[pos + 3];
|
||||
|
||||
if (pos + 4 + len > data_len)
|
||||
return -5;
|
||||
return TLS_EPROTOCOL;
|
||||
|
||||
size_t extension_type = ((unsigned char) data[pos] << 8) +
|
||||
(unsigned char) data[pos + 1];
|
||||
|
||||
|
||||
/* Check if it's a server name extension */
|
||||
/* There can be only one extension of each type, so we break
|
||||
our state and move pos to beginning of the extension here */
|
||||
if (tls_data->use_alpn == 2) {
|
||||
/* we want BOTH alpn and sni to match */
|
||||
if (extension_type == 0x00) { /* Server Name */
|
||||
if (parse_server_name_extension(tls_data, data + pos + 4, len)) {
|
||||
/* SNI matched */
|
||||
if(last_matched) {
|
||||
/* this is only true if ALPN matched, so return true */
|
||||
return last_matched;
|
||||
} else {
|
||||
/* otherwise store that SNI matched */
|
||||
last_matched = 1;
|
||||
}
|
||||
} else {
|
||||
/* both can't match */
|
||||
return -2;
|
||||
}
|
||||
} else if (extension_type == 0x10) { /* ALPN */
|
||||
if (parse_alpn_extension(tls_data, data + pos + 4, len)) {
|
||||
/* ALPN matched */
|
||||
if(last_matched) {
|
||||
/* this is only true if SNI matched, so return true */
|
||||
return last_matched;
|
||||
} else {
|
||||
/* otherwise store that ALPN matched */
|
||||
last_matched = 1;
|
||||
}
|
||||
} else {
|
||||
/* both can't match */
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (extension_type == 0x00 && tls_data->use_alpn == 0) { /* Server Name */
|
||||
return parse_server_name_extension(tls_data, data + pos + 4, len);
|
||||
} else if (extension_type == 0x10 && tls_data->use_alpn == 1) { /* ALPN */
|
||||
if (parse_alpn_extension(tls_data, data + pos + 4, len) > 0) {
|
||||
return 1;
|
||||
}
|
||||
return parse_alpn_extension(tls_data, data + pos + 4, len);
|
||||
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 */
|
||||
@@ -226,9 +198,11 @@ parse_extensions(const struct TLSProtocol *tls_data, const char *data, size_t da
|
||||
|
||||
/* Check we ended where we expected to */
|
||||
if (pos != data_len)
|
||||
return -5;
|
||||
return TLS_EPROTOCOL;
|
||||
|
||||
return -2;
|
||||
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
|
||||
@@ -241,14 +215,14 @@ parse_server_name_extension(const struct TLSProtocol *tls_data, const char *data
|
||||
(unsigned char)data[pos + 2];
|
||||
|
||||
if (pos + 3 + len > data_len)
|
||||
return -5;
|
||||
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 -2;
|
||||
return TLS_ENOEXT;
|
||||
}
|
||||
default:
|
||||
if (verbose) fprintf(stderr, "Unknown server name extension name type: %d\n",
|
||||
@@ -258,9 +232,9 @@ parse_server_name_extension(const struct TLSProtocol *tls_data, const char *data
|
||||
}
|
||||
/* Check we ended where we expected to */
|
||||
if (pos != data_len)
|
||||
return -5;
|
||||
return TLS_EPROTOCOL;
|
||||
|
||||
return -2;
|
||||
return TLS_ENOEXT;
|
||||
}
|
||||
|
||||
static int
|
||||
@@ -272,7 +246,7 @@ parse_alpn_extension(const struct TLSProtocol *tls_data, const char *data, size_
|
||||
len = (unsigned char)data[pos];
|
||||
|
||||
if (pos + 1 + len > data_len)
|
||||
return -5;
|
||||
return TLS_EPROTOCOL;
|
||||
|
||||
if (len > 0 && has_match(tls_data->alpn_protocol_list, data + pos + 1, len)) {
|
||||
return len;
|
||||
@@ -283,15 +257,16 @@ parse_alpn_extension(const struct TLSProtocol *tls_data, const char *data, size_
|
||||
}
|
||||
/* Check we ended where we expected to */
|
||||
if (pos != data_len)
|
||||
return -5;
|
||||
return TLS_EPROTOCOL;
|
||||
|
||||
return -2;
|
||||
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';
|
||||
|
||||
@@ -309,9 +284,9 @@ has_match(char** list, const char* name, size_t name_len) {
|
||||
struct TLSProtocol *
|
||||
new_tls_data() {
|
||||
struct TLSProtocol *tls_data = malloc(sizeof(struct TLSProtocol));
|
||||
if (tls_data != NULL) {
|
||||
tls_data->use_alpn = -1;
|
||||
}
|
||||
CHECK_ALLOC(tls_data, "malloc");
|
||||
|
||||
memset(tls_data, 0, sizeof(*tls_data));
|
||||
|
||||
return tls_data;
|
||||
}
|
||||
@@ -320,16 +295,10 @@ struct TLSProtocol *
|
||||
tls_data_set_list(struct TLSProtocol *tls_data, int alpn, char** list) {
|
||||
if (alpn) {
|
||||
tls_data->alpn_protocol_list = list;
|
||||
if(tls_data->use_alpn == 0)
|
||||
tls_data->use_alpn = 2;
|
||||
else
|
||||
tls_data->use_alpn = 1;
|
||||
tls_data->match_mode.tls_match_alpn = 1;
|
||||
} else {
|
||||
tls_data->sni_hostname_list = list;
|
||||
if(tls_data->use_alpn == 1)
|
||||
tls_data->use_alpn = 2;
|
||||
else
|
||||
tls_data->use_alpn = 0;
|
||||
tls_data->match_mode.tls_match_sni = 1;
|
||||
}
|
||||
|
||||
return tls_data;
|
||||
|
||||
@@ -35,4 +35,14 @@ int parse_tls_header(const struct TLSProtocol *tls_data, const char *data, size_
|
||||
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