Prevent oracle attacks in the legacy protocol (CVE-2018-16737, CVE-2018-16738)
authorGuus Sliepen <guus@tinc-vpn.org>
Sun, 9 Sep 2018 16:19:15 +0000 (18:19 +0200)
committerGuus Sliepen <guus@tinc-vpn.org>
Sun, 9 Sep 2018 20:12:08 +0000 (22:12 +0200)
The legacy authentication protocol allows an oracle attack that could
potentially be exploited. This commit contains several mitigations:

- Connections are no longer closed immediately on error, but put in
  a "tarpit".
- The authentication protocol now requires a valid CHAL_REPLY from the
  initiator of a connection before sending a CHAL_REPLY of its own.
- Reduce the amount of connections per second accepted.
- Null ciphers or digests are no longer allowed in METAKEYs.
- Connections that claim to have the same name as the local node are
  rejected.

Just to be on the safe side:

- The new protocol now requires a valid SIG from the initiator of a
  connection before sending a SIG of its own.

15 files changed:
src/connection.c
src/connection.h
src/net.c
src/net.h
src/net_packet.c
src/net_setup.c
src/net_socket.c
src/protocol.c
src/protocol_auth.c
src/protocol_edge.c
src/sptps.c
src/tincctl.c
test/Makefile.am
test/security.test [new file with mode: 0755]
test/splice.c [new file with mode: 0644]

index 92a9f48ef0517e53ae52c38f9935adb3392a58fd..1c638a4d0bac178f6443b59c7aed333a380097ec 100644 (file)
@@ -27,6 +27,7 @@
 #include "control_common.h"
 #include "list.h"
 #include "logger.h"
+#include "net.h"
 #include "rsa.h"
 #include "subnet.h"
 #include "utils.h"
@@ -68,6 +69,7 @@ void free_connection(connection_t *c) {
        ecdsa_free(c->ecdsa);
 
        free(c->hischallenge);
+       free(c->mychallenge);
 
        buffer_clear(&c->inbuf);
        buffer_clear(&c->outbuf);
@@ -75,7 +77,11 @@ void free_connection(connection_t *c) {
        io_del(&c->io);
 
        if(c->socket > 0) {
-               closesocket(c->socket);
+               if(c->status.tarpit) {
+                       tarpit(c->socket);
+               } else {
+                       closesocket(c->socket);
+               }
        }
 
        free(c->name);
index 48c839b538cca38d7d15234216c051ddd795f6c2..206417b73cbeb477d4e50137f6eccfecf241d04b 100644 (file)
@@ -49,7 +49,8 @@ typedef struct connection_status_t {
        unsigned int log: 1;                    /* 1 if this is a control connection requesting log dump */
        unsigned int invitation: 1;             /* 1 if this is an invitation */
        unsigned int invitation_used: 1;        /* 1 if the invitation has been consumed */
-       unsigned int unused: 18;
+       unsigned int tarpit: 1;                 /* 1 if the connection should be added to the tarpit */
+       unsigned int unused: 17;
 } connection_status_t;
 
 #include "ecdsa.h"
@@ -94,6 +95,7 @@ typedef struct connection_t {
        int outcompression;
 
        char *hischallenge;             /* The challenge we sent to him */
+       char *mychallenge;              /* The challenge we received */
 
        struct buffer_t inbuf;
        struct buffer_t outbuf;
index 5d84741d0472aa66a99ee063f7879a719212595d..e9aed34c48ba74b72fd08f765427c628430bceb8 100644 (file)
--- a/src/net.c
+++ b/src/net.c
@@ -92,6 +92,22 @@ void purge(void) {
        }
 }
 
+/* Put a misbehaving connection in the tarpit */
+void tarpit(int fd) {
+       static int pits[10] = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1};
+       static int next_pit = 0;
+
+       if(pits[next_pit] != -1) {
+               closesocket(pits[next_pit]);
+       }
+
+       pits[next_pit++] = fd;
+
+       if(next_pit >= sizeof pits / sizeof pits[0]) {
+               next_pit = 0;
+       }
+}
+
 /*
   Terminate a connection:
   - Mark it as inactive
@@ -218,6 +234,7 @@ static void timeout_handler(void *data) {
                                logger(DEBUG_CONNECTIONS, LOG_WARNING, "Timeout while connecting to %s (%s)", c->name, c->hostname);
                        } else {
                                logger(DEBUG_CONNECTIONS, LOG_WARNING, "Timeout from %s (%s) during authentication", c->name, c->hostname);
+                               c->status.tarpit = true;
                        }
 
                        terminate_connection(c, c->edge);
@@ -285,6 +302,10 @@ static void periodic_handler(void *data) {
 
 void handle_meta_connection_data(connection_t *c) {
        if(!receive_meta(c)) {
+               if(!c->status.control) {
+                       c->status.tarpit = true;
+               }
+
                terminate_connection(c, c->edge);
                return;
        }
index d69eabdacab702624afbedcf8c4e21944f9eb746..aaf29b6fb6fdab9787099a579f70f8ce6bc867ae 100644 (file)
--- a/src/net.h
+++ b/src/net.h
@@ -214,6 +214,7 @@ extern void retry(void);
 extern int reload_configuration(void);
 extern void load_all_nodes(void);
 extern void try_tx(struct node_t *n, bool mtu);
+extern void tarpit(int fd);
 
 #ifndef HAVE_MINGW
 #define closesocket(s) close(s)
index 5d704815939b09590f551309505d985d461cf6a5..a516b4a9fb893e38831c7beb40e153c97326d5ab 100644 (file)
@@ -461,10 +461,11 @@ static bool receive_udppacket(node_t *n, vpn_packet_t *inpkt) {
 
                inpkt = outpkt;
 
-               if (origlen > MTU / 64 + 20)
+               if(origlen > MTU / 64 + 20) {
                        origlen -= MTU / 64 + 20;
-               else
+               } else {
                        origlen = 0;
+               }
        }
 
        if(inpkt->len > n->maxrecentlen) {
index 73d46c998e2db1d9bf20c6d69289c3ac15405183..d8b3116b488a2b6cc8bf27326923e17edbcb1348 100644 (file)
@@ -687,7 +687,7 @@ bool setup_myself_reloadable(void) {
                keylifetime = 3600;
        }
 
-       if (!get_config_bool(lookup_config(config_tree, "AutoConnect"), &autoconnect)) {
+       if(!get_config_bool(lookup_config(config_tree, "AutoConnect"), &autoconnect)) {
                autoconnect = true;
        }
 
index 2da6253ee3a3dc6c8d62dd669629ce1fbcc90c8d..15d32db2fefaefe5bce6030441dd6af5c9f602b8 100644 (file)
@@ -41,7 +41,7 @@ int maxtimeout = 900;
 int seconds_till_retry = 5;
 int udp_rcvbuf = 1024 * 1024;
 int udp_sndbuf = 1024 * 1024;
-int max_connection_burst = 100;
+int max_connection_burst = 10;
 int fwmark;
 
 listen_socket_t listen_socket[MAXSOCKETS];
@@ -672,12 +672,6 @@ void handle_new_meta_connection(void *data, int flags) {
        // Check if we get many connections from the same host
 
        static sockaddr_t prev_sa;
-       static int tarpit = -1;
-
-       if(tarpit >= 0) {
-               closesocket(tarpit);
-               tarpit = -1;
-       }
 
        if(!sockaddrcmp_noport(&sa, &prev_sa)) {
                static int samehost_burst;
@@ -693,7 +687,7 @@ void handle_new_meta_connection(void *data, int flags) {
                samehost_burst++;
 
                if(samehost_burst > max_connection_burst) {
-                       tarpit = fd;
+                       tarpit(fd);
                        return;
                }
        }
@@ -716,7 +710,7 @@ void handle_new_meta_connection(void *data, int flags) {
 
        if(connection_burst >= max_connection_burst) {
                connection_burst = max_connection_burst;
-               tarpit = fd;
+               tarpit(fd);
                return;
        }
 
@@ -745,7 +739,6 @@ void handle_new_meta_connection(void *data, int flags) {
        connection_add(c);
 
        c->allow_request = ID;
-       send_id(c);
 }
 
 #ifndef HAVE_MINGW
@@ -782,8 +775,6 @@ void handle_new_unix_connection(void *data, int flags) {
        connection_add(c);
 
        c->allow_request = ID;
-
-       send_id(c);
 }
 #endif
 
index 7cb19448d9e356802684a7a2163f11c6c42c5c54..c7dd8fb4a974a5ce724dae763388994d0de629c9 100644 (file)
@@ -82,7 +82,8 @@ bool send_request(connection_t *c, const char *format, ...) {
                return false;
        }
 
-       logger(DEBUG_META, LOG_DEBUG, "Sending %s to %s (%s): %s", request_name[atoi(request)], c->name, c->hostname, request);
+       int id = atoi(request);
+       logger(DEBUG_META, LOG_DEBUG, "Sending %s to %s (%s): %s", request_name[id], c->name, c->hostname, request);
 
        request[len++] = '\n';
 
@@ -90,7 +91,12 @@ bool send_request(connection_t *c, const char *format, ...) {
                broadcast_meta(NULL, request, len);
                return true;
        } else {
-               return send_meta(c, request, len);
+               if(id) {
+                       return send_meta(c, request, len);
+               } else {
+                       send_meta_raw(c, request, len);
+                       return true;
+               }
        }
 }
 
index 1e00f09f4f70be0786aeb36c3b6f162d42601a27..9d61ab8fc7826b8d431f95cae3291f9bbf64436e 100644 (file)
@@ -300,7 +300,7 @@ static bool receive_invitation_sptps(void *handle, uint8_t type, const void *dat
 
        buf[len] = 0;
 
-       if(!*buf || !*name || strcasecmp(buf, "Name") || !check_id(name)) {
+       if(!*buf || !*name || strcasecmp(buf, "Name") || !check_id(name) || !strcmp(name, myself->name)) {
                logger(DEBUG_ALWAYS, LOG_ERR, "Invalid invitation file %s\n", cookie);
                fclose(f);
                return false;
@@ -346,6 +346,10 @@ bool id_h(connection_t *c, const char *request) {
                free(c->name);
                c->name = xstrdup("<control>");
 
+               if(!c->outgoing) {
+                       send_id(c);
+               }
+
                return send_request(c, "%d %d %d", ACK, TINC_CTL_VERSION_CURRENT, getpid());
        }
 
@@ -369,6 +373,10 @@ bool id_h(connection_t *c, const char *request) {
                        return false;
                }
 
+               if(!c->outgoing) {
+                       send_id(c);
+               }
+
                if(!send_request(c, "%d %s", ACK, mykey)) {
                        return false;
                }
@@ -382,7 +390,7 @@ bool id_h(connection_t *c, const char *request) {
 
        /* Check if identity is a valid name */
 
-       if(!check_id(name)) {
+       if(!check_id(name) || !strcmp(name, myself->name)) {
                logger(DEBUG_ALWAYS, LOG_ERR, "Got bad %s from %s (%s): %s", "ID", c->name,
                       c->hostname, "invalid name");
                return false;
@@ -418,6 +426,11 @@ bool id_h(connection_t *c, const char *request) {
                }
 
                c->allow_request = ACK;
+
+               if(!c->outgoing) {
+                       send_id(c);
+               }
+
                return send_ack(c);
        }
 
@@ -454,6 +467,10 @@ bool id_h(connection_t *c, const char *request) {
 
        c->allow_request = METAKEY;
 
+       if(!c->outgoing) {
+               send_id(c);
+       }
+
        if(c->protocol_minor >= 2) {
                c->allow_request = ACK;
                char label[25 + strlen(myself->name) + strlen(c->name)];
@@ -618,7 +635,8 @@ bool metakey_h(connection_t *c, const char *request) {
                        return false;
                }
        } else {
-               c->incipher = NULL;
+               logger(DEBUG_ALWAYS, LOG_ERR, "Possible intruder %s (%s): %s", c->name, c->hostname, "null cipher");
+               return false;
        }
 
        c->inbudget = cipher_budget(c->incipher);
@@ -629,7 +647,8 @@ bool metakey_h(connection_t *c, const char *request) {
                        return false;
                }
        } else {
-               c->indigest = NULL;
+               logger(DEBUG_ALWAYS, LOG_ERR, "Possible intruder %s (%s): %s", c->name, c->hostname, "null digest");
+               return false;
        }
 
        c->status.decryptin = true;
@@ -647,9 +666,7 @@ bool send_challenge(connection_t *c) {
        const size_t len = rsa_size(c->rsa);
        char buffer[len * 2 + 1];
 
-       if(!c->hischallenge) {
-               c->hischallenge = xrealloc(c->hischallenge, len);
-       }
+       c->hischallenge = xrealloc(c->hischallenge, len);
 
        /* Copy random data to the buffer */
 
@@ -676,41 +693,59 @@ bool challenge_h(connection_t *c, const char *request) {
 
        char buffer[MAX_STRING_SIZE];
        const size_t len = rsa_size(myself->connection->rsa);
-       size_t digestlen = digest_length(c->indigest);
-       char digest[digestlen];
 
        if(sscanf(request, "%*d " MAX_STRING, buffer) != 1) {
                logger(DEBUG_ALWAYS, LOG_ERR, "Got bad %s from %s (%s)", "CHALLENGE", c->name, c->hostname);
                return false;
        }
 
-       /* Convert the challenge from hexadecimal back to binary */
-
-       int inlen = hex2bin(buffer, buffer, sizeof(buffer));
-
        /* Check if the length of the challenge is all right */
 
-       if(inlen != len) {
+       if(strlen(buffer) != (size_t)len * 2) {
                logger(DEBUG_ALWAYS, LOG_ERR, "Possible intruder %s (%s): %s", c->name, c->hostname, "wrong challenge length");
                return false;
        }
 
+       c->mychallenge = xrealloc(c->mychallenge, len);
+
+       /* Convert the challenge from hexadecimal back to binary */
+
+       hex2bin(buffer, c->mychallenge, len);
+
+       /* The rest is done by send_chal_reply() */
+
+       c->allow_request = CHAL_REPLY;
+
+       if(c->outgoing) {
+               return send_chal_reply(c);
+       } else {
+               return true;
+       }
+
+#endif
+}
+
+bool send_chal_reply(connection_t *c) {
+       const size_t len = rsa_size(myself->connection->rsa);
+       size_t digestlen = digest_length(c->indigest);
+       char digest[digestlen * 2 + 1];
+
        /* Calculate the hash from the challenge we received */
 
-       if(!digest_create(c->indigest, buffer, len, digest)) {
+       if(!digest_create(c->indigest, c->mychallenge, len, digest)) {
                return false;
        }
 
+       free(c->mychallenge);
+       c->mychallenge = NULL;
+
        /* Convert the hash to a hexadecimal formatted string */
 
-       bin2hex(digest, buffer, digestlen);
+       bin2hex(digest, digest, digestlen);
 
        /* Send the reply */
 
-       c->allow_request = CHAL_REPLY;
-
-       return send_request(c, "%d %s", CHAL_REPLY, buffer);
-#endif
+       return send_request(c, "%d %s", CHAL_REPLY, digest);
 }
 
 bool chal_reply_h(connection_t *c, const char *request) {
@@ -752,6 +787,10 @@ bool chal_reply_h(connection_t *c, const char *request) {
        c->hischallenge = NULL;
        c->allow_request = ACK;
 
+       if(!c->outgoing) {
+               send_chal_reply(c);
+       }
+
        return send_ack(c);
 #endif
 }
index 18b6befeca0b77d4b8aaad1e76efe39870973633..9fd301f239c9e1d785eac185dcc947d0a33aedd9 100644 (file)
@@ -85,7 +85,7 @@ bool add_edge_h(connection_t *c, const char *request) {
 
        /* Check if names are valid */
 
-       if(!check_id(from_name) || !check_id(to_name)) {
+       if(!check_id(from_name) || !check_id(to_name) || !strcmp(from_name, to_name)) {
                logger(DEBUG_ALWAYS, LOG_ERR, "Got bad %s from %s (%s): %s", "ADD_EDGE", c->name,
                       c->hostname, "invalid name");
                return false;
@@ -237,7 +237,7 @@ bool del_edge_h(connection_t *c, const char *request) {
 
        /* Check if names are valid */
 
-       if(!check_id(from_name) || !check_id(to_name)) {
+       if(!check_id(from_name) || !check_id(to_name) || !strcmp(from_name, to_name)) {
                logger(DEBUG_ALWAYS, LOG_ERR, "Got bad %s from %s (%s): %s", "DEL_EDGE", c->name,
                       c->hostname, "invalid name");
                return false;
index 93a7ad3ff1e9037d9846ecbd56dfae66813589e5..82e913eb29a67967318549b41918f6c877c165f0 100644 (file)
@@ -287,7 +287,11 @@ static bool receive_kex(sptps_t *s, const char *data, uint16_t len) {
 
        memcpy(s->hiskex, data, len);
 
-       return send_sig(s);
+       if(s->initiator) {
+               return send_sig(s);
+       } else {
+               return true;
+       }
 }
 
 // Receive a SIGnature record, verify it, if it passed, compute the shared secret and calculate the session keys.
@@ -327,6 +331,10 @@ static bool receive_sig(sptps_t *s, const char *data, uint16_t len) {
                return false;
        }
 
+       if(!s->initiator && !send_sig(s)) {
+               return false;
+       }
+
        free(s->mykex);
        free(s->hiskex);
 
index 12e5ead920b5a3209030adcf855b4bb09b3989c8..7244779ee438daa3da96b7570fb9cd838f9af211 100644 (file)
@@ -902,6 +902,8 @@ bool connect_tincd(bool verbose) {
        setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, (void *)&one, sizeof(one));
 #endif
 
+       sendline(fd, "%d ^%s %d", ID, controlcookie, TINC_CTL_VERSION_CURRENT);
+
        char data[4096];
        int version;
 
@@ -915,8 +917,6 @@ bool connect_tincd(bool verbose) {
                return false;
        }
 
-       sendline(fd, "%d ^%s %d", ID, controlcookie, TINC_CTL_VERSION_CURRENT);
-
        if(!recvline(fd, line, sizeof(line)) || sscanf(line, "%d %d %d", &code, &version, &pid) != 3 || code != 4 || version != TINC_CTL_VERSION_CURRENT) {
                if(verbose) {
                        fprintf(stderr, "Could not fully establish control socket connection\n");
index 29b2c21d34a323d572097a195475ced98c94666d..9c2f0011cba932f7d7046f06a49a74cacf426eb9 100644 (file)
@@ -8,6 +8,7 @@ TESTS = \
        invite-tinc-up.test \
        ns-ping.test \
        scripts.test \
+       security.test \
        sptps-basic.test \
        variables.test
 
@@ -17,6 +18,11 @@ EXTRA_DIST = testlib.sh
 
 AM_CFLAGS = -iquote.
 
+check_PROGRAMS = \
+       splice
+
+splice_SOURCES = splice.c
+
 clean-local:
        -for pid in *.test.?/pid; do ../src/tinc --pidfile="$$pid" stop; done
        -killall ../src/sptps_test
diff --git a/test/security.test b/test/security.test
new file mode 100755 (executable)
index 0000000..91d29e2
--- /dev/null
@@ -0,0 +1,98 @@
+#!/bin/sh
+
+. "${0%/*}/testlib.sh"
+
+# Skip this test if tools are missing
+
+which socket >/dev/null || exit 77
+which timeout >/dev/null || exit 77
+
+# Initialize two nodes
+
+$tinc $c1 <<EOF
+init foo
+set DeviceType dummy
+set Port 32754
+set Address localhost
+set PingTimeout 1
+set AutoConnect no
+EOF
+
+$tinc $c2 <<EOF
+init bar
+set DeviceType dummy
+set Port 32755
+set PingTimeout 1
+set MaxTimeout 1
+set ExperimentalProtocol no
+set AutoConnect no
+EOF
+
+# Exchange host config files
+
+$tinc $c1 export | $tinc $c2 exchange | $tinc $c1 import
+
+$tinc $c1 start $r1
+$tinc $c2 start $r2
+
+# No ID sent by responding node if we don't send an ID first, before the timeout
+
+result=`(sleep 2; echo "0 bar 17.7") | timeout 3 socket localhost 32754` && exit 1
+test $? = 124
+test -z "$result"
+
+# ID sent if initiator sends first, but still tarpitted
+
+result=`echo "0 bar 17.7" | timeout 3 socket localhost 32754` && exit 1
+test $? = 124
+test "`echo "$result" | head -c 10`" = "0 foo 17.7"
+
+# No invalid IDs allowed
+
+result=`echo "0 foo 17.7" | timeout 1 socket localhost 32754` && exit 1
+test $? = 124
+test -z "$result"
+
+result=`echo "0 baz 17.7" | timeout 1 socket localhost 32754` && exit 1
+test $? = 124
+test -z "$result"
+
+# No NULL METAKEYs allowed
+
+result=`printf "0 foo 17.0\n1 0 672 0 0 834188619F4D943FD0F4B1336F428BD4AC06171FEABA66BD2356BC9593F0ECD643F0E4B748C670D7750DFDE75DC9F1D8F65AB1026F5ED2A176466FBA4167CC567A2085ABD070C1545B180BDA86020E275EA9335F509C57786F4ED2378EFFF331869B856DDE1C05C461E4EECAF0E2FB97AF77B7BC2AD1B34C12992E45F5D1254BBF0C3FB224ABB3E8859594A83B6CA393ED81ECAC9221CE6BC71A727BCAD87DD80FC0834B87BADB5CB8FD3F08BEF90115A8DF1923D7CD9529729F27E1B8ABD83C4CF8818AE10257162E0057A658E265610B71F9BA4B365A20C70578FAC65B51B91100392171BA12A440A5E93C4AA62E0C9B6FC9B68F953514AAA7831B4B2C31C4\n" | timeout 3 socket localhost 32755` && exit 1
+test $? = 124
+test -z "$result" # Not even the ID should be sent when the first packet contains illegal data
+
+# No splicing allowed
+
+$tinc $c2 stop
+$tinc $c2 del ExperimentalProtocol
+$tinc $c2 start $r2
+
+./splice foo localhost 32754 bar localhost 32755 17.7 &
+sleep 3
+test `$tinc $c1 dump reachable nodes | wc -l` = 1
+test `$tinc $c2 dump reachable nodes | wc -l` = 1
+kill $!
+
+$tinc $c2 stop
+$tinc $c1 stop
+
+# Test splicing again with legacy protocol
+
+$tinc $c1 set ExperimentalProtocol no
+$tinc $c2 set ExperimentalProtocol no
+
+$tinc $c1 start $r1
+$tinc $c2 start $r2
+
+./splice foo localhost 32754 bar localhost 32755 17.0 &
+sleep 3
+test `$tinc $c1 dump reachable nodes | wc -l` = 1
+test `$tinc $c2 dump reachable nodes | wc -l` = 1
+kill $!
+
+# Clean up
+
+$tinc $c2 stop
+$tinc $c1 stop
diff --git a/test/splice.c b/test/splice.c
new file mode 100644 (file)
index 0000000..5345148
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+    splice.c -- Splice two outgoing tinc connections together
+    Copyright (C) 2018 Guus Sliepen <guus@tinc-vpn.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include <stdio.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+
+#ifdef HAVE_MINGW
+extern const char *winerror(int);
+#define strerror(x) ((x)>0?strerror(x):winerror(GetLastError()))
+#define sockerrno WSAGetLastError()
+#define sockstrerror(x) winerror(x)
+#else
+#define sockerrno errno
+#define sockstrerror(x) strerror(x)
+#endif
+
+int main(int argc, char *argv[]) {
+       if(argc < 7) {
+               fprintf(stderr, "Usage: %s name1 host1 port1 name2 host2 port2 [protocol]\n", argv[0]);
+               return 1;
+       }
+
+       const char *protocol;
+
+       if(argc >= 8) {
+               protocol = argv[7];
+       } else {
+               protocol = "17.7";
+       }
+
+#ifdef HAVE_MINGW
+       static struct WSAData wsa_state;
+
+       if(WSAStartup(MAKEWORD(2, 2), &wsa_state)) {
+               return 1;
+       }
+
+#endif
+       int sock[2];
+       char buf[1024];
+
+       struct addrinfo *ai, hint;
+       memset(&hint, 0, sizeof(hint));
+
+       hint.ai_family = AF_UNSPEC;
+       hint.ai_socktype = SOCK_STREAM;
+       hint.ai_protocol = IPPROTO_TCP;
+       hint.ai_flags = 0;
+
+       for (int i = 0; i < 2; i++) {
+               if(getaddrinfo(argv[2 + 3 * i], argv[3 + 3 * i], &hint, &ai) || !ai) {
+                       fprintf(stderr, "getaddrinfo() failed: %s\n", sockstrerror(sockerrno));
+                       return 1;
+               }
+
+               sock[i] = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+
+               if(sock[i] == -1) {
+                       fprintf(stderr, "Could not create socket: %s\n", sockstrerror(sockerrno));
+                       return 1;
+               }
+
+               if(connect(sock[i], ai->ai_addr, ai->ai_addrlen)) {
+                       fprintf(stderr, "Could not connect to %s: %s\n", argv[i + 3 * i], sockstrerror(sockerrno));
+                       return 1;
+               }
+
+               fprintf(stderr, "Connected to %s\n", argv[1 + 3 * i]);
+
+               /* Pretend to be the other one */
+               int len = snprintf(buf, sizeof buf, "0 %s %s\n", argv[4 - 3 * i], protocol);
+               if (send(sock[i], buf, len, 0) != len) {
+                       fprintf(stderr, "Error sending data to %s: %s\n", argv[1 + 3 * i], sockstrerror(sockerrno));
+                       return 1;
+               }
+
+               /* Ignore the response */
+               do {
+                       if (recv(sock[i], buf, 1, 0) != 1) {
+                               fprintf(stderr, "Error reading data from %s: %s\n", argv[1 + 3 * i], sockstrerror(sockerrno));
+                               return 1;
+                       }
+               } while(*buf != '\n');
+       }
+
+       fprintf(stderr, "Splicing...\n");
+
+       int nfds = (sock[0] > sock[1] ? sock[0] : sock[1]) + 1;
+
+       while(true) {
+               fd_set fds;
+               FD_ZERO(&fds);
+               FD_SET(sock[0], &fds);
+               FD_SET(sock[1], &fds);
+
+               if(select(nfds, &fds, NULL, NULL, NULL) <= 0) {
+                       return 1;
+               }
+
+               for(int i = 0; i < 2; i++ ) {
+                       if(FD_ISSET(sock[i], &fds)) {
+                               ssize_t len = recv(sock[i], buf, sizeof buf, 0);
+
+                               if(len < 0) {
+                                       fprintf(stderr, "Error while reading from %s: %s\n", argv[1 + i * 3], sockstrerror(sockerrno));
+                                       return 1;
+                               }
+
+                               if(len == 0) {
+                                       fprintf(stderr, "Connection closed by %s\n", argv[1 + i * 3]);
+                                       return 0;
+                               }
+
+                               if(send(sock[i ^ 1], buf, len, 0) != len) {
+                                       fprintf(stderr, "Error while writing to %s: %s\n", argv[4 - i * 3], sockstrerror(sockerrno));
+                                       return 1;
+                               }
+                       }
+               }
+       }
+
+       return 0;
+}