From 62fe72133c8a2f22c65b5b854201e0caa80b3efe Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Tue, 23 Aug 2016 03:56:42 +0200 Subject: [PATCH] examples: add nat-hole-punching Signed-off-by: Jason A. Donenfeld --- contrib/nat-hole-punching/README | 41 ++++ contrib/nat-hole-punching/nat-punch-client.c | 203 +++++++++++++++++++ contrib/nat-hole-punching/nat-punch-server.c | 110 ++++++++++ 3 files changed, 354 insertions(+) create mode 100644 contrib/nat-hole-punching/README create mode 100644 contrib/nat-hole-punching/nat-punch-client.c create mode 100644 contrib/nat-hole-punching/nat-punch-server.c diff --git a/contrib/nat-hole-punching/README b/contrib/nat-hole-punching/README new file mode 100644 index 0000000..46e6201 --- /dev/null +++ b/contrib/nat-hole-punching/README @@ -0,0 +1,41 @@ +== NAT Hole Punching Example == + +This code should never be used, ever. But, it's a nice demonstration of how +to punch holes and have two NAT'd peers talk to each other. + +Compile with: + $ gcc nat-punch-client.c -o client -lresolv + $ gcc nat-punch-server.c -o server + + +Server is 1.2.3.4 and is on the public internet accepting UDP:49918. +Client A is NAT'd and doesnt't know its IP address. +Client B is NAT'd and doesnt't know its IP address. + + +Server runs: + $ ./server + +Client A runs: + # ip link add wg0 type wireguard + # ip addr add 10.200.200.1 peer 10.200.200.2 dev wg0 + # wg set wg0 private-key ... peer ... allowed-ips 10.200.200.2/32 + # ./client 1.2.3.4 wg0 + # ping 10.200.200.2 + +Client B runs: + # ip link add wg0 type wireguard + # ip addr add 10.200.200.2 peer 10.200.200.1 dev wg0 + # wg set wg0 private-key ... peer ... allowed-ips 10.200.200.1/32 + # ./client 1.2.3.4 wg0 + # ping 10.200.200.1 + +And voila! Client A and Client B can speak from behind NAT. + + + +----- +Keep in mind that this is proof-of-concept example code. It is not code that +should be used in production, ever. It is woefully insecure, and is unsuitable +for any real usage. With that said, this is useful as a learning example of +how NAT hole punching might work within a more developed solution. diff --git a/contrib/nat-hole-punching/nat-punch-client.c b/contrib/nat-hole-punching/nat-punch-client.c new file mode 100644 index 0000000..a72b5f6 --- /dev/null +++ b/contrib/nat-hole-punching/nat-punch-client.c @@ -0,0 +1,203 @@ +/* Example only. Do not run in production. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum { MAX_PEERS = 65536, PORT = 49918 }; + +static struct { + uint8_t base64_key[45]; + bool have_seen; +} peers[MAX_PEERS]; +static unsigned int total_peers; + +static const char *cmd(const char *line, ...) +{ + static char buf[2048]; + char full_cmd[2048] = { 0 }; + size_t len; + FILE *f; + va_list args; + va_start(args, line); + vsnprintf(full_cmd, 2047, line, args); + va_end(args); + f = popen(full_cmd, "r"); + if (!f) { + perror("popen"); + exit(errno); + } + if (!fgets(buf, 2048, f)) { + pclose(f); + return NULL; + } + pclose(f); + len = strlen(buf); + if (!len) + return NULL; + if (buf[len - 1] == '\n') + buf[len - 1] = '\0'; + return buf; +} + +static void read_peers(const char *interface) +{ + char full_cmd[2048] = { 0 }; + size_t len; + FILE *f; + snprintf(full_cmd, 2047, "wg show %s peers", interface); + f = popen(full_cmd, "r"); + if (!f) { + perror("popen"); + exit(errno); + } + for (;;) { + if (!fgets(peers[total_peers].base64_key, 45, f)) + break; + len = strlen(peers[total_peers].base64_key); + if (len != 44 && len != 45) + continue; + if (peers[total_peers].base64_key[len - 1] == '\n') + peers[total_peers].base64_key[len - 1] = '\0'; + ++total_peers; + } + pclose(f); +} + +static void unbase64(uint8_t dstkey[32], const char *srckey) +{ + uint8_t buf[33]; + if (b64_pton(srckey, buf, 33) != 32) { + fprintf(stderr, "Could not parse base64 key: %s\n", srckey); + exit(EINVAL); + } + memcpy(dstkey, buf, 32); +} + +static void apply_bpf(int sock, uint16_t port, uint32_t ip) +{ + struct sock_filter filter[] = { + BPF_STMT(BPF_LD + BPF_W + BPF_ABS, 12 /* src ip */), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ip, 0, 5), + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 20 /* src port */), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, PORT, 0, 3), + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 22 /* dst port */), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, port, 0, 1), + BPF_STMT(BPF_RET + BPF_K, -1), + BPF_STMT(BPF_RET + BPF_K, 0) + }; + struct sock_fprog filter_prog = { + .len = sizeof(filter) / sizeof(filter[0]), + .filter = filter + }; + if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &filter_prog, sizeof(filter_prog)) < 0) { + perror("setsockopt(bpf)"); + exit(errno); + } +} + +int main(int argc, char *argv[]) +{ + struct sockaddr_in addr = { + .sin_family = AF_INET + }; + struct { + struct udphdr udp; + uint8_t my_pubkey[32]; + uint8_t their_pubkey[32]; + } __attribute__((packed)) packet = { + .udp = { + .len = htons(sizeof(packet)), + .dest = htons(PORT) + } + }; + struct { + struct iphdr iphdr; + struct udphdr udp; + uint32_t ip; + uint16_t port; + } __attribute__((packed)) reply; + ssize_t len; + int sock, i; + bool repeat; + struct hostent *ent; + const char *server = argv[1], *interface = argv[2]; + + if (argc < 3) { + fprintf(stderr, "Usage: %s SERVER WIREGUARD_INTERFACE\nExample:\n %s demo.wireguard.io wg0\n", argv[0], argv[0]); + return EINVAL; + } + + if (getuid() != 0) { + fprintf(stderr, "Must be root!\n"); + return EPERM; + } + + ent = gethostbyname2(server, AF_INET); + if (!ent) { + herror("gethostbyname2"); + return h_errno; + } + addr.sin_addr = *(struct in_addr *)ent->h_addr; + read_peers(interface); + cmd("ip link set %s up", interface); + unbase64(packet.my_pubkey, cmd("wg show %s public-key", interface)); + packet.udp.source = htons(atoi(cmd("wg show %s listen-port", interface))); + + /* We use raw sockets so that the WireGuard interface can actually own the real socket. */ + sock = socket(AF_INET, SOCK_RAW, IPPROTO_UDP); + if (sock < 0) { + perror("socket"); + return errno; + } + apply_bpf(sock, ntohs(packet.udp.source), ntohl(addr.sin_addr.s_addr)); + +check_again: + repeat = false; + for (i = 0; i < total_peers; ++i) { + if (peers[i].have_seen) + continue; + printf("[+] Requesting IP and port of %s: ", peers[i].base64_key); + unbase64(packet.their_pubkey, peers[i].base64_key); + if (sendto(sock, &packet, sizeof(packet), 0, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + putchar('\n'); + perror("sendto"); + return errno; + } + len = recv(sock, &reply, sizeof(reply), 0); + if (len < 0) { + putchar('\n'); + perror("recv"); + return errno; + } + if (len != sizeof(reply)) { + printf("server does not yet have it\n"); + repeat = true; + } else { + printf("%s:%d\n", inet_ntoa(*(struct in_addr *)&reply.ip), ntohs(reply.port)); + peers[i].have_seen = true; + cmd("wg set %s peer %s persistent-keepalive 25 endpoint %s:%d", interface, peers[i].base64_key, inet_ntoa(*(struct in_addr *)&reply.ip), ntohs(reply.port)); + } + } + if (repeat) { + sleep(2); + goto check_again; + } + + close(sock); + return 0; +} diff --git a/contrib/nat-hole-punching/nat-punch-server.c b/contrib/nat-hole-punching/nat-punch-server.c new file mode 100644 index 0000000..198e0f8 --- /dev/null +++ b/contrib/nat-hole-punching/nat-punch-server.c @@ -0,0 +1,110 @@ +/* Example only. Do not run in production. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct entry { + uint8_t pubkey[32]; + uint32_t ip; + uint16_t port; +}; + +enum { MAX_ENTRIES = 65536, PORT = 49918 }; + +static struct entry entries[MAX_ENTRIES]; +static unsigned int next_entry; + +/* XX: this should use a hash table */ +static struct entry *find_entry(uint8_t key[32]) +{ + int i; + for (i = 0; i < MAX_ENTRIES; ++i) { + if (!memcmp(entries[i].pubkey, key, 32)) + return &entries[i]; + } + return NULL; +} + +/* XX: this is obviously vulnerable to DoS */ +static struct entry *find_or_insert_entry(uint8_t key[32]) +{ + struct entry *entry = find_entry(key); + if (!entry) { + entry = &entries[next_entry++ % MAX_ENTRIES]; + memcpy(entry->pubkey, key, 32); + } + return entry; +} + +int main(int argc, char *argv[]) +{ + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_addr = { .s_addr = htonl(INADDR_ANY) }, + .sin_port = htons(PORT) + }; + struct { + uint8_t my_pubkey[32]; + uint8_t their_pubkey[32]; + } __attribute__((packed)) packet; + struct { + uint32_t ip; + uint16_t port; + } __attribute__((packed)) reply; + struct entry *entry; + socklen_t len; + ssize_t retlen; + int optval; + int sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) { + perror("socket"); + return errno; + } + + optval = 1; + if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0) { + perror("setsockopt"); + return errno; + } + + if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + perror("bind"); + return errno; + } + + for (;;) { + len = sizeof(addr); + if (recvfrom(sock, &packet, sizeof(packet), 0, (struct sockaddr *)&addr, &len) != sizeof(packet)) { + perror("recvfrom"); + continue; + } + entry = find_or_insert_entry(packet.my_pubkey); + entry->ip = addr.sin_addr.s_addr; + entry->port = addr.sin_port; + entry = find_entry(packet.their_pubkey); + if (entry) { + reply.ip = entry->ip; + reply.port = entry->port; + if (sendto(sock, &reply, sizeof(reply), 0, (struct sockaddr *)&addr, len) < 0) { + perror("sendto"); + continue; + } + } else { + if (sendto(sock, NULL, 0, 0, (struct sockaddr *)&addr, len) < 0) { + perror("sendto"); + continue; + } + } + } + + close(sock); + return 0; +}