Sfoglia il codice sorgente

Implement TUN interface

Billy Barrow 1 anno fa
parent
commit
e111c832aa

+ 2 - 0
README.md

@@ -10,3 +10,5 @@ Currently supports forwarding the following protocols:
 - **HTTP-01 Well Known** intercept and redirect HTTP requests for the /.well-known directiory used by LetsEncrypt
 
 When a connection is made to the Astrogate server, it reads data from the client, and uses pattern matching to determine what protocol the client is likely talking. It uses this information to attempt to read out an intended hostname from the data and tunnels the connection to the configured host.
+
+It also supports tunnel mode, where by using the `astrogate-tunnel` client program creates a TUN interface where traffic will appear to be routed from their original IP addresses on the internet. This is useful since Astrogate does not modify any data it proxies, so there are not a HTTP `X-Forwarded-For` header to know where the request came from.

+ 3 - 1
config

@@ -1,5 +1,6 @@
 BIND-TO 127.0.0.1
 BIND-TO 200:3785:eb78:5234:e4f1:d453:86d3:59ff
+TUNNEL 192.168.1.126 6060 tun0 10.60.0.0 255.255.0.0
 
 TLS billy.barrow.nz 443 ROUND-ROBIN {
     192.168.1.232 443
@@ -9,7 +10,8 @@ TLS billy.barrow.nz 443 ROUND-ROBIN {
 }
 
 HTTP billy.barrow.nz 80 {
-    185.112.145.195 80
+    tun/192.168.1.206 8081
+    tun/127.0.0.1 8081
 }
 
 

+ 233 - 0
src/client/client.vala

@@ -0,0 +1,233 @@
+
+namespace AstrogateTunnel {
+
+
+    private static Socket socket;
+    private static int tun_fd;
+
+    private static string ifname;
+    private static InetSocketAddress server_address;
+    private static InetSocketAddress client_address;
+
+    private static InetAddress vpn_network;
+    private static InetAddress vpn_netmask;
+    private static InetAddress vpn_ip;
+    private static int mtu = 1500;
+
+    public static int main(string[] args) {
+        int client_port = 0;
+        int server_port = 0;
+        if(args.length < 6 || !int.try_parse(args[3], out client_port) || !int.try_parse(args[5], out server_port)) {
+            print(@"USAGE: $(args[0]) interface client_ip client_port server_ip server_port\n");
+            return -1;
+        }
+
+        try {
+            server_address = new InetSocketAddress.from_string(args[4], server_port);
+            client_address = new InetSocketAddress.from_string(args[2], client_port);
+            ifname = args[1];
+
+            socket = new Socket(server_address.family, SocketType.DATAGRAM, SocketProtocol.UDP);
+            socket.bind(client_address, false);
+
+            // Open tun device
+            tun_fd = handle_posix_error("opening tun device", Posix.open("/dev/net/tun", Posix.O_RDWR));
+
+            // Create tunnel device
+            var request = generate_ifreq();
+            handle_posix_error("creating tun interface", Linux.ioctl(tun_fd, IfTun.Tun.SETIFF, &request));
+
+            // Register with server
+            var registration_message = new uint8[] { 'R' };
+            socket.connect(server_address);
+            socket.send(registration_message);
+
+            print(@"Waiting for $(server_address)...\n");
+            var ack_message = new uint8[1024];
+            socket.receive(ack_message);
+
+            if(ack_message[0] != 'A') {
+                print("Received invalid reply from server. Exiting.\n");
+                return -2;
+            }
+
+            var mtu_dat = new uint8[2];
+            if(ack_message[1] == '4') {
+                vpn_network = new InetAddress.from_bytes(ack_message[2:6], SocketFamily.IPV4);
+                vpn_netmask = new InetAddress.from_bytes(ack_message[6:10], SocketFamily.IPV4);
+                vpn_ip = new InetAddress.from_bytes(ack_message[10:14], SocketFamily.IPV4);
+                mtu_dat = ack_message[14:16];
+            }
+            else if(ack_message[1] == '6') {
+                vpn_network = new InetAddress.from_bytes(ack_message[2:18], SocketFamily.IPV6);
+                vpn_netmask = new InetAddress.from_bytes(ack_message[18:34], SocketFamily.IPV6);
+                vpn_ip = new InetAddress.from_bytes(ack_message[34:50], SocketFamily.IPV6);
+                mtu_dat = ack_message[50:52];
+            }
+            else {
+                print("Received unsupported network type from server. Exiting.\n");
+                return -3;
+            }
+
+            uint16 mtu_val = 0;
+            Memory.copy(&mtu_val, mtu_dat, sizeof(uint16));
+            mtu = mtu_val.to_big_endian();
+
+            print(@"Registered with Astrogate server at $server_address:\n");
+            print(@"  Network: $vpn_network\n");
+            print(@"  Subnet mask: $vpn_netmask\n");
+            print(@"  Allocated IP: $vpn_ip\n");
+            print(@"  MTU: $mtu\n\n");
+            
+            request.ifr_flags = 0;
+            request.ifr_mtu = mtu;
+            handle_posix_error(@"setting MTU for $ifname", Linux.ioctl(socket.fd, Linux.Network.SIOCSIFMTU, &request));
+
+            request.ifr_flags |= Linux.Network.IfFlag.UP;
+            handle_posix_error(@"bringing $ifname up", Linux.ioctl(socket.fd, Linux.Network.SIOCSIFFLAGS, &request));
+            
+            address_to_posix(vpn_ip, &request.ifr_addr);
+            handle_posix_error(@"setting IP address for $ifname", Linux.ioctl(socket.fd, Linux.Network.SIOCSIFADDR, &request));
+
+            address_to_posix(vpn_netmask, &request.ifr_addr);
+            handle_posix_error(@"setting subnet mask for $ifname", Linux.ioctl(socket.fd, Linux.Network.SIOCSIFNETMASK, &request));
+
+            address_to_posix(vpn_network, &request.ifr_addr);
+            handle_posix_error(@"setting destination network for $ifname", Linux.ioctl(socket.fd, Linux.Network.SIOCSIFDSTADDR, &request));
+        }
+        catch(Error e) {
+            print(@"Fatal error: $(e.message)\n");
+            return -999;
+        }
+
+        new Thread<int>("udp listener", listen_udp);
+        return listen_tun();
+    }
+
+    private int listen_udp() {
+        while(true) {
+            try {
+                // Receive the next datagram
+                var buffer = new uint8[mtu+5];
+                var size = socket.receive(buffer);
+                buffer.length = (int)size;
+
+                if(buffer[0] == 'L') {
+                    link_ip(buffer);
+                    continue;
+                }
+
+                else if(buffer[0] == 'U') {
+                    unlink_ip(buffer);
+                    continue;
+                }
+
+                else if(buffer[0] != 'D') {
+                    print("Ignoring invalid datagram from server\n");
+                    continue;
+                }
+
+                handle_posix_error(@"piping datagram to $ifname", (int)Posix.write(tun_fd, buffer[1:buffer.length], buffer.length));
+            }
+            catch(Error e) {
+                print(@"Exception handling incoming tunnel server datagram: $(e.message)\n");
+            }
+        }
+    }
+
+    private int listen_tun() {
+        while(true) {
+            var buffer = new uint8[mtu+4];
+            var size = Posix.read(tun_fd, buffer, buffer.length);
+            if(size < 0) {
+                print(@"Got $(size) from Posix.read.\n");
+                continue;
+            }
+            buffer.length = (int)size;
+
+            try {
+                var message = new uint8[buffer.length + 1];
+                message[0] = 'D';
+                Memory.copy(message[1:], buffer, buffer.length);
+                socket.send(message);
+            }
+            catch (Error e) {
+                print(@"Error forwarding datagram to server: $(e.message)\n");
+            }
+        }
+    }
+
+    private void link_ip(uint8[] buffer) throws Error {
+        var address = new InetAddress.from_bytes(buffer[1:buffer.length], vpn_ip.family);
+        print(@"Linking IP $(address)\n");
+        var route = build_rtentry(address);
+
+        var res = Linux.ioctl(socket.fd, Linux.Network.SIOCADDRT, &route);
+        if(res < 0 && errno != Posix.EEXIST) {
+            handle_posix_error(@"adding route to $address", res);
+        }
+        else if(errno == Posix.EEXIST) {
+            print(@"Already linked to $(address)\n");
+        }
+    }
+
+    private void unlink_ip(uint8[] buffer) throws Error {
+        var address = new InetAddress.from_bytes(buffer[1:buffer.length], vpn_ip.family);
+        print(@"Uninking IP $(address)\n");
+        var route = build_rtentry(address);
+        handle_posix_error(@"adding route to $address", Linux.ioctl(socket.fd, Linux.Network.SIOCDELRT, &route));
+    }
+
+    private Linux.Network.RtEntry build_rtentry(InetAddress address) {
+        var route = Linux.Network.RtEntry();
+        route.rt_dev = ifname;
+        address_to_posix(address, &route.rt_dst);
+        address_to_posix(new InetAddress.from_string("255.255.255.255"), &route.rt_genmask);
+        route.rt_flags = Linux.Network.RtFlag.UP;
+        return route;
+    }
+
+    private int handle_posix_error(string task, int result) throws Error {
+        if(result < 0) {
+            var code = Posix.errno;
+            var message = Posix.strerror(code);
+            throw new Error.literal(Quark.from_string("posix-error"), code, @"Got result $(result) while $(task): $message");
+        }
+        return result;
+    }
+
+    private void address_to_posix(InetAddress address, Posix.SockAddr *addr) {
+        var addr_str = address.to_string();
+        switch (address.family) {
+            case SocketFamily.IPV4:
+                var sin4 = Posix.SockAddrIn();
+                sin4.sin_family = Posix.AF_INET;
+                Posix.inet_pton(Posix.AF_INET, addr_str, &sin4.sin_addr.s_addr);
+                Memory.copy(addr, &sin4, sizeof(Posix.SockAddr));
+                break;
+            case SocketFamily.IPV6:
+                var sin6 = Posix.SockAddrIn();
+                sin6.sin_family = Posix.AF_INET;
+                Posix.inet_pton(Posix.AF_INET, addr_str, &sin6.sin_addr.s_addr);
+                Memory.copy(addr, &sin6, sizeof(Posix.SockAddr));
+                break;
+            default:
+                assert_not_reached();
+        }
+    }
+
+    private Linux.Network.IfReq generate_ifreq() throws Error {
+        var request = Linux.Network.IfReq();
+        request.ifr_flags = IfTun.IfFlag.TUN;
+        if(ifname.length > request.ifr_name.length) {
+            error(@"Max length for interface name is $(request.ifr_name.length) characters, provided name is $(ifname.length) characters.");
+        }
+        for(int i = 0; i < ifname.length; i++){
+            request.ifr_name[i] = ifname[i];
+        }
+        return request;
+    }
+
+
+
+}

+ 17 - 0
src/client/meson.build

@@ -0,0 +1,17 @@
+
+
+sources = files('client.vala')
+
+
+dependencies = [
+    dependency('glib-2.0'),
+    dependency('gobject-2.0'),
+    dependency('gio-2.0'),
+    dependency('gee-0.8'),
+    dependency('invercargill'),
+    meson.get_compiler('vala').find_library('posix'),
+    meson.get_compiler('vala').find_library('linux'),
+    meson.get_compiler('vala').find_library('if_tun', dirs: vapi_dir),
+]
+
+executable('astrogate-tunnel', sources, dependencies: dependencies, install: true)

+ 2 - 23
src/meson.build

@@ -1,26 +1,5 @@
 project('astrogate', 'vala', 'c')
 vapi_dir = meson.current_source_dir() / 'vapi'
 
-add_project_arguments(['--disable-warnings', '--enable-checking'], language: 'vala')
-
-
-sources = files('server.vala')
-sources += files('header_reader.vala')
-sources += files('configuration.vala')
-sources += files('tunnel.vala')
-
-
-dependencies = [
-    dependency('glib-2.0'),
-    dependency('gobject-2.0'),
-    dependency('gio-2.0'),
-    dependency('gee-0.8'),
-    dependency('libxml-2.0'),
-    dependency('invercargill'),
-    meson.get_compiler('vala').find_library('posix'),
-    meson.get_compiler('vala').find_library('linux'),
-    meson.get_compiler('vala').find_library('if_tun', dirs: vapi_dir),
-    meson.get_compiler('c').find_library('m')
-]
-
-executable('astrogate', sources, dependencies: dependencies, install: true)
+subdir('server')
+subdir('client')

+ 55 - 4
src/configuration.vala → src/server/configuration.vala

@@ -27,9 +27,16 @@ namespace Astrogate {
             return bind_addresses;
         }}
 
+        public bool tunnel_enabled { get; private set; }
+        public InetSocketAddress tunnel_bind { get; private set; }
+        public string tunnel_interface { get; private set; }
+        public InetAddress tunnel_network { get; private set; }
+        public InetAddress tunnel_netmask { get; private set; }
+
         public Configuration() {
             config_items = new Vector<ConfigurationItem>();
             bind_addresses = new Vector<InetAddress>();
+            tunnel_enabled = false;
         }
 
         public async void read_file(File file) throws Error {
@@ -51,12 +58,35 @@ namespace Astrogate {
                 if(type == "BIND-TO") {
                     var address = read_field(line, 1);
                     if(address == null) {
-                        throw new ConfigurationFormatError.MALFORMED_ENTRY("One or more required configuration entry fields are missing");
+                        throw new ConfigurationFormatError.MALFORMED_ENTRY("One or more required configuration entry fields are missing for BIND-TO");
                     }
                     bind_addresses.add(new InetAddress.from_string(address));
                     continue;
                 }
 
+                if(type == "TUNNEL") {
+                    var server_address = read_field(line, 1);
+                    var server_port = read_field(line, 2);
+                    var iface = read_field(line, 3);
+                    var network = read_field(line, 4);
+                    var netmask = read_field(line, 5);
+                    if(server_address == null || server_port == null || iface == null || network == null || netmask == null) {
+                        throw new ConfigurationFormatError.MALFORMED_ENTRY("One or more required configuration entry fields are missing for TUNNEL");
+                    }
+
+                    int port_no;
+                    if(!int.try_parse(server_port, out port_no)) {
+                        throw new ConfigurationFormatError.MALFORMED_ENTRY("Could not parse tunnel server port number");
+                    }
+
+                    tunnel_bind = new InetSocketAddress.from_string(server_address, port_no);
+                    tunnel_interface = iface;
+                    tunnel_network = new InetAddress.from_string(network);
+                    tunnel_netmask = new InetAddress.from_string(netmask);
+                    tunnel_enabled = true;
+                    continue;
+                }
+
                 var host = read_field(line, 1);
                 var port = read_field(line, 2);
                 var mode = read_field(line, 3);
@@ -126,7 +156,7 @@ namespace Astrogate {
                     throw new ConfigurationFormatError.MALFORMED_ENTRY("Entry does not contain \" {\".");
                 }
 
-                entry.destinations = new Series<InetSocketAddress>();
+                entry.destinations = new Series<ConfigurationDestination>();
                 while(true) {
                     var inner = yield data.read_line_async(0);
                     if(inner.chomp().chug() == "") {
@@ -154,7 +184,13 @@ namespace Astrogate {
                         throw new ConfigurationFormatError.MALFORMED_ENTRY("Could not parse destination port number");
                     }
 
-                    entry.destinations.add(new InetSocketAddress.from_string(net_address, net_port_no));
+                    if(net_address.has_prefix("tun/")) {
+                        var tun_address = net_address.split("/", 2)[1];
+                        entry.destinations.add(new ConfigurationDestination(DestinationType.TUNNEL, tun_address, net_port_no));    
+                    }
+                    else {
+                        entry.destinations.add(new ConfigurationDestination(DestinationType.INTERNET, net_address, net_port_no));
+                    }
                 }
 
                 config_items.add(entry);
@@ -199,9 +235,24 @@ namespace Astrogate {
         public ConnectionType protocol {get; set;}
         public string hostname {get; set;}
         public uint16 source_port {get; set;}
-        public Series<InetSocketAddress> destinations {get; set;}
+        public Series<ConfigurationDestination> destinations {get; set;}
         public ForwardMode forward_mode {get; set;}
 
     }
 
+    public class ConfigurationDestination {
+        public DestinationType mode { get; set; }
+        public InetSocketAddress socket_address { get; set; }
+
+        public ConfigurationDestination(DestinationType dest_type, string address, int port) {
+            mode = dest_type;
+            socket_address = new InetSocketAddress.from_string(address, port);
+        }
+    }
+
+    public enum DestinationType {
+        INTERNET,
+        TUNNEL
+    }
+
 }

+ 0 - 0
src/header_reader.vala → src/server/header_reader.vala


+ 28 - 0
src/server/meson.build

@@ -0,0 +1,28 @@
+
+add_project_arguments(['--disable-warnings', '--enable-checking'], language: 'vala')
+
+
+sources = files('server.vala')
+sources += files('header_reader.vala')
+sources += files('configuration.vala')
+sources += files('tunnel/tunnel.vala')
+sources += files('tunnel/address_mapping.vala')
+sources += files('tunnel/client_registration.vala')
+sources += files('tunnel/frame.vala')
+sources += files('tunnel/server.vala')
+
+
+dependencies = [
+    dependency('glib-2.0'),
+    dependency('gobject-2.0'),
+    dependency('gio-2.0'),
+    dependency('gee-0.8'),
+    dependency('libxml-2.0'),
+    dependency('invercargill'),
+    meson.get_compiler('vala').find_library('posix'),
+    meson.get_compiler('vala').find_library('linux'),
+    meson.get_compiler('vala').find_library('if_tun', dirs: vapi_dir),
+    meson.get_compiler('c').find_library('m'),
+]
+
+executable('astrogate', sources, dependencies: dependencies, install: true)

+ 82 - 40
src/server.vala → src/server/server.vala

@@ -3,24 +3,11 @@ using Invercargill;
 namespace Astrogate {
 
     private static Configuration config;
+    private static Tunnel? tunnel;
+    private static Gee.LinkedList<ProxyConnection> closing_connections;
 
     public static int main(string[] args) {
 
-        var tunnel = new Tunnel(
-            args[1],
-            1500,
-            new InetSocketAddress(new InetAddress.from_string("127.0.0.1"), 8080),
-            new InetAddress.from_string("10.60.0.64"),
-            new InetAddress.from_string("255.255.255.192"));
-
-        var res = tunnel.listen_tun();
-        print(@"EXIT: $res\n");
-        return res;
-
-        /// Normal program below
-
-
-
         var config_path = args.length > 1 ? args[1] : "/etc/astrogate.conf";
         run.begin(config_path);
 
@@ -30,6 +17,7 @@ namespace Astrogate {
 
     private async void run(string config_path) {
         config = new Configuration();
+        closing_connections = new Gee.LinkedList<ProxyConnection>();
         print("Reading config...\n");
         yield config.read_file(File.new_for_commandline_arg(config_path));
 
@@ -44,6 +32,19 @@ namespace Astrogate {
             }
         }
 
+        if(config.tunnel_enabled) {
+            tunnel = new Tunnel(config.tunnel_interface, 1500, config.tunnel_bind, config.tunnel_network, config.tunnel_netmask);
+            tunnel.listen_threaded();
+            var tunnel_dests = config.entries
+                .select_many<ConfigurationDestination>(e => e.destinations)
+                .where(d => d.mode == DestinationType.TUNNEL);
+
+            foreach (var dest in tunnel_dests) {
+                print(@"Whitelisting $(dest.socket_address.address) in the tunnel server\n");
+                tunnel.whitelist_client(dest.socket_address.address);
+            }
+        }
+
         service.incoming.connect((c, o) => {
             handle_connection.begin(c);
             return false;
@@ -86,40 +87,38 @@ namespace Astrogate {
     }
 
     private async void forward_connection(SocketConnection connection, ConfigurationItem entry, BinaryData initial_buffer) throws Error {
-        SocketConnection proxy;
-
-        print(@"Proxying connection from $(connection.get_remote_address().to_string()) intended for $(entry.hostname):$(entry.source_port).\n");
+        ProxyConnection proxy;
+        var remote_address = (InetSocketAddress)connection.get_remote_address();
+        print(@"Proxying connection from $(remote_address) intended for $(entry.hostname):$(entry.source_port).\n");
 
         switch (entry.forward_mode) {
             case ForwardMode.ROUND_ROBIN:
-                proxy = yield connect_round_robin(entry);
+                proxy = yield connect_round_robin(entry, remote_address);
                 break;
             case ForwardMode.RANDOM:
-                proxy = yield connect_random(entry);
+                proxy = yield connect_random(entry, remote_address);
                 break;
             case ForwardMode.FIRST_RESPONDER:
-                proxy = yield connect_first_responder(entry);
+                proxy = yield connect_first_responder(entry, remote_address);
                 break;
             default:
                 assert_not_reached();
         }
 
-        print(@"Connection from $(connection.get_remote_address().to_string()) forwarded to $(proxy.get_remote_address()).\n");
+        print(@"Connection from $(connection.get_remote_address().to_string()) forwarded to $(proxy.connection.get_remote_address()).\n");
         
-        yield proxy.output_stream.write_async(initial_buffer.to_array(), 0);
-        yield proxy.output_stream.flush_async(0);
+        yield proxy.connection.output_stream.write_async(initial_buffer.to_array(), 0);
+        yield proxy.connection.output_stream.flush_async(0);
 
-        var cancellation_token = new Cancellable();
-        proxy.output_stream.splice_async.begin(connection.input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE | OutputStreamSpliceFlags.CLOSE_TARGET, 0, cancellation_token, () => cancellation_token.cancel());
-        connection.output_stream.splice_async.begin(proxy.input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE | OutputStreamSpliceFlags.CLOSE_TARGET, 0, cancellation_token, () => cancellation_token.cancel());
+        proxy.connection.output_stream.splice_async.begin(connection.input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE | OutputStreamSpliceFlags.CLOSE_TARGET, 0, proxy.cancellation_token, () => proxy.cleanup());
+        connection.output_stream.splice_async.begin(proxy.connection.input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE | OutputStreamSpliceFlags.CLOSE_TARGET, 0, proxy.cancellation_token, () => proxy.cleanup());
     }
 
-    private async SocketConnection connect_round_robin(ConfigurationItem entry) throws Error {
-        SocketConnection? connection = null;
+    private async ProxyConnection connect_round_robin(ConfigurationItem entry, InetSocketAddress source) throws Error {
+        ProxyConnection? connection = null;
         foreach (var address in entry.destinations) {
             try{
-                var client = new SocketClient();
-                connection = yield client.connect_async(address);
+                connection = yield connect_to_destination(source, address);
             }
             catch (Error e) {
                 print(@"Failure during round-robin: $(e.message)\n");
@@ -131,25 +130,24 @@ namespace Astrogate {
         return connection;
     }
 
-    private async SocketConnection connect_random(ConfigurationItem entry) throws Error {
+    private async ProxyConnection connect_random(ConfigurationItem entry, InetSocketAddress source) throws Error {
         var items = entry.destinations.to_array();
         var i = Random.int_range(0, items.length);
         var address = items[i];
 
         var client = new SocketClient();
-        return yield client.connect_async(address);
+        return yield connect_to_destination(source, address);
     }
 
-    private async SocketConnection connect_first_responder(ConfigurationItem entry) throws Error {
+    private async ProxyConnection connect_first_responder(ConfigurationItem entry, InetSocketAddress source) throws Error {
         var cancel_token = new Cancellable();
-        SocketConnection? connection = null;
+        ProxyConnection? connection = null;
         var in_flight = 0;
 
         AsyncReadyCallback connection_cb = (obj, res) => {
-            var client = (SocketClient)obj;
-            SocketConnection conn;
+            ProxyConnection conn;
             try {
-                conn = client.connect_async.end(res);
+                conn = connect_to_destination.end(res);
                 if(connection == null){
                     connection = conn;
                 }
@@ -167,8 +165,7 @@ namespace Astrogate {
 
         foreach (var address in entry.destinations) {
             in_flight++;
-            var client = new SocketClient();
-            client.connect_async.begin(address, cancel_token, connection_cb);
+            connect_to_destination.begin(source, address, cancel_token, connection_cb);
         }
         while(in_flight > 0 && connection == null) {
             print(@"In flight requests: $(in_flight)\n");
@@ -195,4 +192,49 @@ namespace Astrogate {
         return entry;
     }
 
+    private async ProxyConnection connect_to_destination(InetSocketAddress source, ConfigurationDestination dest, Cancellable? cancellation_token = null) throws Error {
+        var client = new SocketClient();
+        if(dest.mode == DestinationType.INTERNET) {
+            var connection = yield client.connect_async(dest.socket_address, cancellation_token);
+            return new ProxyConnection(connection);
+        }
+        if(tunnel == null) {
+            throw new TunnelError.NOT_CONFIGURED(@"Cannot connect to $(dest.socket_address) via tunnel, tunnel is not configured");
+        }
+        
+        var mapping = tunnel.map(source.address, dest.socket_address.address, (uint16)dest.socket_address.port);
+        var local_addr = new InetSocketAddress(tunnel.proxy_internal_ip, mapping.source_port);
+        client.set_local_address(local_addr);
+        print(@"TUNNEL CONNECTION: $(local_addr) -> $(new InetSocketAddress(mapping.internal_address, mapping.target_port))");
+        var connection = yield client.connect_async(new InetSocketAddress(mapping.internal_address, mapping.target_port), cancellation_token);
+        connection.notify["closed"].connect(() => tunnel.unmap(mapping));
+        return new ProxyConnection(connection, mapping);
+    }
+
+    private class ProxyConnection {
+        public SocketConnection connection { get; private set; }
+        public Cancellable cancellation_token { get; private set; }
+        private TunnelAddressMapping? mapping;
+
+        public ProxyConnection(SocketConnection conn, TunnelAddressMapping? map = null) {
+            connection = conn;
+            mapping = map;
+            cancellation_token = new Cancellable();
+        }
+
+        public void cleanup() {
+            closing_connections.add(this);
+            print("Cleanup connection!\n");
+            cancellation_token.cancel();
+            connection = null;
+            GLib.Timeout.add_seconds_once(10, () => {
+                if(mapping != null) {
+                    tunnel.unmap(mapping);
+                }
+                closing_connections.remove(this);
+            });
+        }
+
+    }
+
 }

+ 12 - 0
src/server/tunnel/address_mapping.vala

@@ -0,0 +1,12 @@
+
+namespace Astrogate {
+
+    public class TunnelAddressMapping {
+        public InetSocketAddress forward_address {get; set;}
+        public InetAddress internal_address {get; set;}
+        public InetAddress external_address {get; set;}
+        public uint16 target_port {get; set;}
+        public uint16 source_port {get; set;}
+    }
+
+}

+ 9 - 0
src/server/tunnel/client_registration.vala

@@ -0,0 +1,9 @@
+
+namespace Astrogate {
+
+    public class TunnelClientRegistration {
+        public InetAddress internal_address {get; set;}
+        public InetSocketAddress forward_address {get; set;}
+    }
+
+}

+ 177 - 0
src/server/tunnel/frame.vala

@@ -0,0 +1,177 @@
+namespace Astrogate {
+
+    public class TunnelFrame {
+        public SocketFamily protocol {get; private set;}
+        public InetAddress source_ip {get; private set;}
+        public InetAddress destination_ip {get; private set;}
+        public uint16 source_port {get; private set;}
+        public uint16 destination_port {get; private set;}
+        public uint8[] raw_frame {get; private set;}
+        public bool is_tcp {get; private set;}
+
+        private int payload_start = -1;
+
+        public TunnelFrame(uint8[] raw_frame) {
+            this.raw_frame = raw_frame;
+            if(raw_frame[2] == 0x08 && raw_frame[3] == 0x00) {
+                protocol = SocketFamily.IPV4;
+                print("Reading IPv4 datagram!\n");
+                read_ipv4_datagram();
+            }
+            else if(raw_frame[2] == 0x86 && raw_frame[3] == 0xdd) {
+                protocol = SocketFamily.IPV6;
+                print("IPv6 Not Implemented!\n");
+            }
+            else {
+                print("Could not determine protocol type!\n");
+                protocol = SocketFamily.INVALID;
+            }
+        }
+
+        private void read_ipv4_datagram() {
+            uint internet_header_length = raw_frame[4] & 0x0F;
+            source_ip = new InetAddress.from_bytes(raw_frame[16:20], SocketFamily.IPV4);
+            destination_ip = new InetAddress.from_bytes(raw_frame[20:22], SocketFamily.IPV4);
+
+            print(@"Src: $(source_ip.to_string()), Dst: $(destination_ip.to_string())\n");
+
+            // TUN header = 4 bytes, internet_header_length = IP header size in 32-bit word sizes
+            payload_start = 4 + (((int)internet_header_length) * 4);
+
+            if(raw_frame[13] == 0x06) {
+                print(@"Is TCP! (offset $payload_start == 4 + $internet_header_length * 4)\n");
+                is_tcp = true;
+                read_tcp_ports();
+            }
+            else {
+                print("Is not TCP!\n");
+            }
+        }
+
+        private void read_tcp_ports() {
+            uint16 src = 0;
+            uint16 dst = 0;
+            Memory.copy(&src, raw_frame[payload_start:payload_start+2], sizeof(int16));
+            Memory.copy(&dst, raw_frame[payload_start+2:payload_start+4], sizeof(int16));
+
+            src = src.to_big_endian();
+            dst = dst.to_big_endian();
+
+            print(@"Src: $(src), Dst: $dst\n");
+
+            source_port = src;
+            destination_port = dst;
+        }
+
+        private void write_tcp_ports(uint16 src, uint16 dst) {
+            var s = src.to_little_endian();
+            var d = dst.to_little_endian();
+            var sbytes = new uint8[sizeof(uint16)];
+            var dbytes = new uint8[sizeof(uint16)];
+            Memory.copy(sbytes, &s, sizeof(uint16));
+            Memory.copy(dbytes, &d, sizeof(uint16));
+
+            copy_into(raw_frame, payload_start, 2, sbytes);
+            copy_into(raw_frame, payload_start+2, 2, dbytes);
+        }
+
+        private void copy_into(uint8[] buffer, int start, int length, uint8[] replacement) {
+            for(int i = start; i < start+length; i++) {
+                buffer[i] = replacement[i-start];
+            }
+        }
+
+        public void replace_source_ip(InetAddress address) {
+            source_ip = address;
+            var bin = get_binary_representation_of_ip(address).copy();
+            if(protocol == SocketFamily.IPV4) {
+                copy_into(raw_frame, 16, 4, bin);
+            }
+            recalculate_ip_checksum();
+            recalculate_tcp_checksum();
+        }
+
+        public void replace_destination_ip(InetAddress address) {
+            destination_ip = address;
+            if(protocol == SocketFamily.IPV4) {
+                copy_into(raw_frame, 20, 4, get_binary_representation_of_ip(address));
+            }
+            recalculate_ip_checksum();
+            recalculate_tcp_checksum();
+        }
+
+        private void recalculate_ip_checksum() {
+            uint32 checksum = 0;
+            int i;
+        
+            // Iterate through the raw data, grouping the bytes into 16-bit words
+            for (i = 4; i < payload_start; i += 2) {
+                if(i == 14) {
+                    // Don't add the checksum field itself
+                    continue;
+                }
+                uint16 word = (raw_frame[i] << 8) | raw_frame[i + 1];
+                checksum += word;
+            }
+
+            if(i < payload_start) {
+                checksum += raw_frame[i+1];
+            }
+        
+            // Fold the 32-bit checksum into 16 bits
+            while (checksum >> 16 != 0) {
+                checksum = (checksum & 0xFFFF) + (checksum >> 16);
+            }
+        
+            var result = (uint16) ~checksum;
+            result = result.to_big_endian();
+
+            Memory.copy(raw_frame[14:16], &result, sizeof(uint16));
+        }
+
+        private void recalculate_tcp_checksum() {
+            var segment = new Invercargill.BinaryData();
+            segment.endianness = Invercargill.BinaryData.Endianness.BigEndian;
+
+            // Pseudo-header
+            segment.append_byte_array(get_binary_representation_of_ip(source_ip));
+            segment.append_byte_array(get_binary_representation_of_ip(destination_ip));
+            segment.append_byte_array(new uint8[] { 0x00, 0x06 });
+            var tcp_length = (uint16)(raw_frame.length - payload_start);
+            segment.push_uint16(tcp_length);
+
+            // Real segment
+            segment.append_byte_array(raw_frame[payload_start:raw_frame.length]);
+
+            var merged = segment.to_array();
+        
+            uint32 checksum = 0;
+            int i;
+            // TCP segment checksum
+            for (i = 0; i < merged.length; i += 2) {
+                if (i == 28) {
+                    // Don't add the checksum field itself
+                    continue;
+                }
+                uint16 word = (merged[i] << 8) | merged[i + 1];
+                checksum += word;
+            }
+        
+            if (i < merged.length) {
+                checksum += merged[i];
+            }
+        
+            // Fold the 32-bit checksum into 16 bits
+            while (checksum >> 16 != 0) {
+                checksum = (checksum & 0xFFFF) + (checksum >> 16);
+            }
+        
+            var result = (uint16) ~checksum;
+            result = result.to_big_endian();
+        
+            Memory.copy(raw_frame[payload_start + 16:payload_start + 18], &result, sizeof(uint16));
+        }
+
+    }
+
+}

+ 97 - 0
src/server/tunnel/server.vala

@@ -0,0 +1,97 @@
+
+namespace Astrogate {
+
+    public class TunnelServer {
+
+        private Socket socket;
+        private InetSocketAddress server_address;
+        private Tunnel tunnel;
+
+        public TunnelServer(InetSocketAddress address, Tunnel tunnel) throws Error {
+            this.tunnel = tunnel;
+            server_address = address;
+            socket = new Socket (address.family, SocketType.DATAGRAM, SocketProtocol.UDP);
+            socket.bind (address, true);
+        }
+
+        public int listen() {
+            while(true) {
+                try {
+                    // Receive the next datagram
+                    var buffer = new uint8[tunnel.mtu+5];
+                    SocketAddress address;
+                    var size = socket.receive_from(out address, buffer);
+                    var from = (InetSocketAddress)address;
+                    buffer.length = (int)size;
+
+                    if(buffer[0] == 'D') {
+                        handle_data(buffer, from);
+                        continue;
+                    }
+                    if(buffer[0] == 'R') {
+                        handle_register(buffer, from);
+                        continue;
+                    }
+
+                    print("Ignoring invalid datagram\n");
+                }
+                catch(Error e) {
+                    print(@"Exception handling incoming tunnel server datagram: $(e.message)\n");
+                }
+            }
+        }
+
+        private void handle_register(uint8[] buffer, InetSocketAddress source) throws Error {
+            var reg = tunnel.register_client(source);
+            var message = new Invercargill.BinaryData();
+            message.endianness = Invercargill.BinaryData.Endianness.BigEndian;
+            var header = new uint8[2];
+            header[0] = 'A';
+
+            if(reg.internal_address.family == SocketFamily.IPV4) {
+                header[1] = '4';
+            }
+            else {
+                header[1] = '6';
+            }
+            message.append_byte_array(header);
+            message.append_byte_array(get_binary_representation_of_ip(tunnel.internal_network_ip));
+            message.append_byte_array(get_binary_representation_of_ip(tunnel.internal_network_mask));
+            message.append_byte_array(get_binary_representation_of_ip(reg.internal_address));
+            message.push_uint16((uint16)tunnel.mtu);
+
+            print(@"Replying with $(message.to_escaped_string())\n");
+
+            socket.send_to(source, message.to_array());
+        }
+
+        private void handle_data(uint8[] buffer, InetSocketAddress source) throws Error {
+            var frame = new TunnelFrame(buffer[1:buffer.length]);
+            tunnel.inject(frame, source);
+        }
+
+        public void send_to(InetSocketAddress address, uint8[] buffer) throws Error {
+            var message = new uint8[buffer.length + 1];
+            message[0] = 'D';
+            Memory.copy(message[1:], buffer, buffer.length);
+            socket.send_to(address, message);
+        }
+
+        public void link_ip(InetSocketAddress client, InetAddress ip) throws Error {
+            var addr = get_binary_representation_of_ip(ip);
+            var message = new uint8[addr.length + 1];
+            message[0] = 'L';
+            Memory.copy(message[1:], addr, addr.length);
+            socket.send_to(client, message);
+        }
+
+        public void unlink_ip(InetSocketAddress client, InetAddress ip) throws Error {
+            var addr = get_binary_representation_of_ip(ip);
+            var message = new uint8[addr.length + 1];
+            message[0] = 'U';
+            Memory.copy(message[1:], addr, addr.length);
+            socket.send_to(client, message);
+        }
+    }
+
+}

+ 326 - 0
src/server/tunnel/tunnel.vala

@@ -0,0 +1,326 @@
+using Invercargill;
+
+namespace Astrogate {
+
+    public errordomain TunnelError {
+        IFACE_NAME_TOO_LONG,
+        POSIX_ERROR,
+        ADDRESS_SPACE_EXHAUSTED,
+        PORT_SPACE_EXHAUSTED,
+        CLIENT_NOT_REGISTERED,
+        NOT_CONFIGURED,
+        CLIENT_NOT_WHITELISTED
+    }
+
+    public class Tunnel {
+
+        public InetAddress proxy_internal_ip {get; private set;}
+        public InetAddress internal_network_ip {get; private set;}
+        public InetAddress internal_network_mask {get; private set;}
+        public InetSocketAddress proxy_socket_address {get; private set;}
+        public int mtu { get; private set; }
+        public string iface { get; private set; }
+
+        private int address_allocations = 0;
+        private Socket socket;
+        private Vector<TunnelAddressMapping> address_mapping = new Vector<TunnelAddressMapping> ();
+        private Vector<InetAddress> allowed_clients = new Vector<InetAddress>();
+        private Vector<TunnelClientRegistration> registered_clients = new Vector<TunnelClientRegistration>();
+        private bool[] available_ports = new bool[65536];
+        private int fd;
+        private TunnelServer udp_server;
+
+        public Tunnel(string iface, int mtu, InetSocketAddress server_address, InetAddress subnet, InetAddress subnet_mask) throws Error {
+            this.iface = iface;
+            this.mtu = mtu;
+            proxy_socket_address = server_address;
+            internal_network_ip = subnet;
+            internal_network_mask = subnet_mask;
+            proxy_internal_ip = allocate_address();
+
+            var socket = new Socket(proxy_socket_address.family, SocketType.DATAGRAM, SocketProtocol.UDP);
+
+            // Open tun device
+            fd = handle_posix_error("opening tun device", Posix.open("/dev/net/tun", Posix.O_RDWR));
+
+            // Create tunnel device
+            var request = generate_ifreq();
+            
+            handle_posix_error("creating tun interface", Linux.ioctl(fd, IfTun.Tun.SETIFF, &request));
+            
+            request.ifr_flags = 0;
+            request.ifr_mtu = mtu;
+            handle_posix_error(@"setting MTU for $iface", Linux.ioctl(socket.fd, Linux.Network.SIOCSIFMTU, &request));
+            
+            request.ifr_flags |= Linux.Network.IfFlag.UP;
+            handle_posix_error(@"bringing $iface up", Linux.ioctl(socket.fd, Linux.Network.SIOCSIFFLAGS, &request));
+
+            address_to_posix(proxy_internal_ip, &request.ifr_addr);
+            handle_posix_error(@"setting IP address for $iface", Linux.ioctl(socket.fd, Linux.Network.SIOCSIFADDR, &request));
+            
+            address_to_posix(internal_network_mask, &request.ifr_addr);
+            handle_posix_error(@"setting subnet mask for $iface", Linux.ioctl(socket.fd, Linux.Network.SIOCSIFNETMASK, &request));
+            
+            address_to_posix(internal_network_ip, &request.ifr_addr);
+            handle_posix_error(@"setting destination network for $iface", Linux.ioctl(socket.fd, Linux.Network.SIOCSIFDSTADDR, &request));
+            
+            udp_server = new TunnelServer(proxy_socket_address, this);
+        }
+
+        public void whitelist_client(InetAddress address) {
+            allowed_clients.add(address);
+        }
+
+        public TunnelAddressMapping map(InetAddress public_ip, InetAddress forward_ip, uint16 port) throws Error {
+            var mapping = new TunnelAddressMapping();
+            mapping.external_address = public_ip;
+            mapping.target_port = port;
+            
+            var client = registered_clients.first_or_default(c => c.forward_address.address.equal(forward_ip));
+            if(client == null) {
+                throw new TunnelError.CLIENT_NOT_REGISTERED(@"The requested client $(forward_ip) is not registered with the tunnel.");
+            }
+
+            mapping.forward_address = client.forward_address;
+            mapping.internal_address = client.internal_address;
+            mapping.source_port = allocate_port();
+
+
+            mapping.external_address = new InetAddress.from_string("8.8.8.8");
+
+            address_mapping.add(mapping);
+            udp_server.link_ip(mapping.forward_address, mapping.external_address);
+            return mapping;
+        }
+
+        public void unmap(TunnelAddressMapping mapping) throws Error {
+            print("Unmapping!");
+            lock(available_ports) {
+                var index = address_mapping.index_of(m => m == mapping);
+                if(index == -1) {
+                    return;
+                }
+
+                address_mapping.remove(index);
+                available_ports[mapping.source_port] = false;
+
+                // Only send unlink to client if there are no other mappings to this address
+                if(address_mapping.no(m => m.external_address.equal(mapping.external_address) && m.forward_address.address.equal(mapping.forward_address.address))) {
+                    udp_server.unlink_ip(mapping.forward_address, mapping.external_address);
+                }
+            }
+        }
+
+        public void listen_threaded() {
+            new Thread<int>("Tunnel interface thread", listen_tun);
+            new Thread<int>("Tunnel udp server thread", listen_udp);
+        }
+
+        public int listen_udp() {
+            return udp_server.listen();
+        }
+
+        public int listen_tun() {
+
+            while(true) {
+                var buffer = new uint8[mtu+4];
+                var size = Posix.read(fd, buffer, buffer.length);
+                if(size < 0) {
+                    print(@"Got $(size) from Posix.read.\n");
+                    continue;
+                }
+                buffer.length = (int)size;
+
+                var frame = new TunnelFrame(buffer);
+
+                if(frame.protocol == SocketFamily.INVALID) {
+                    print("Ignoring non IP frame");
+                }
+
+                if(!frame.is_tcp) {
+                    print("Ignoring non-tcp datagram\n");
+                    continue;
+                }
+
+                if(!frame.source_ip.equal(proxy_internal_ip)) {
+                    print("Ignoring datagram from non-proxy address\n");
+                    continue;
+                }
+
+                var mapping = address_mapping
+                    .where(m => m.internal_address.equal(frame.destination_ip))
+                    .where(m => m.source_port == frame.source_port)
+                    .first_or_default();
+
+                if(mapping == null) {
+                    print("Ignoring non-mapped destination IP and port\n");
+                    continue;
+                }
+
+                print(@"Remapping $(proxy_internal_ip) to $(mapping.external_address)\n");
+                frame.replace_source_ip(mapping.external_address);
+                udp_server.send_to(mapping.forward_address, frame.raw_frame);
+            }
+        }
+
+        private Linux.Network.IfReq generate_ifreq() throws TunnelError {
+            var request = Linux.Network.IfReq();
+            request.ifr_flags = IfTun.IfFlag.TUN;
+            if(iface.length > request.ifr_name.length) {
+                throw new TunnelError.IFACE_NAME_TOO_LONG(@"Max length for interface name is $(request.ifr_name.length) characters, provided name is $(iface.length) characters.");
+            }
+            for(int i = 0; i < iface.length; i++){
+                request.ifr_name[i] = iface[i];
+            }
+            return request;
+        }
+
+        private int handle_posix_error(string task, int result) throws TunnelError {
+            if(result < 0) {
+                var code = Posix.errno;
+                var message = Posix.strerror(code);
+                throw new TunnelError.POSIX_ERROR(@"Got result code $(result) while $(task): ($(code)) $message");
+            }
+            return result;
+        }
+
+        private void address_to_posix(InetAddress address, Posix.SockAddr *addr) {
+            var addr_str = address.to_string();
+            switch (address.family) {
+                case SocketFamily.IPV4:
+                    var sin4 = Posix.SockAddrIn();
+                    sin4.sin_family = Posix.AF_INET;
+                    Posix.inet_pton(Posix.AF_INET, addr_str, &sin4.sin_addr.s_addr);
+                    Memory.copy(addr, &sin4, sizeof(Posix.SockAddr));
+                    break;
+                case SocketFamily.IPV6:
+                    var sin6 = Posix.SockAddrIn();
+                    sin6.sin_family = Posix.AF_INET;
+                    Posix.inet_pton(Posix.AF_INET, addr_str, &sin6.sin_addr.s_addr);
+                    Memory.copy(addr, &sin6, sizeof(Posix.SockAddr));
+                    break;
+                default:
+                    assert_not_reached();
+            }
+        }
+
+        public TunnelClientRegistration register_client(InetSocketAddress socket_address) throws TunnelError {
+            var client = allowed_clients.first_or_default(c => c.equal(socket_address.address));
+            if(client == null) {
+                throw new TunnelError.CLIENT_NOT_WHITELISTED(@"Cannot register client $(socket_address) as it is not in the allowed clients list");
+            }
+
+            var registration = registered_clients.first_or_default(c => c.forward_address.address.equal(socket_address.address));
+            var pre_registered = registration != null;
+            if(!pre_registered) {
+                registration = new TunnelClientRegistration();
+                registration.internal_address = allocate_address();
+                registration.forward_address = socket_address;
+                registered_clients.add(registration);
+            }
+            else {
+                registration.forward_address = socket_address;
+            }
+
+            return registration;
+        }
+
+        public InetAddress allocate_address() throws TunnelError {
+            int index;
+            lock(address_allocations) {
+                index = address_allocations;
+                address_allocations++;
+            }
+            // First address is reserved
+            index++;
+
+            if(index > get_num_possible_addresses()) {
+                throw new TunnelError.ADDRESS_SPACE_EXHAUSTED("Could not allocate new IP address, IP address space exhausted");
+            }
+
+            var address = get_binary_representation_of_ip(internal_network_ip);
+            var mask = get_binary_representation_of_ip(internal_network_mask);
+
+            var pos = address.length-1;
+            var carry = index;
+            while(carry != 0) {
+                var remaining = carry % 256;
+                carry = carry / 256;
+                address[pos] = (address[pos] & mask[pos]) | (remaining & ~mask[pos]);
+                pos--;
+            }
+
+            return new InetAddress.from_bytes(address, internal_network_ip.family);
+        }
+
+        public uint16 allocate_port() throws TunnelError {
+            lock(available_ports) {
+                for(int i = 10; i < available_ports.length; i++) {
+                    if(available_ports[i] == false) {
+                        available_ports[i] = true;
+                        return (uint16)i;
+                    }
+                }
+            }
+            throw new TunnelError.PORT_SPACE_EXHAUSTED("Could not allocate new port, port space exhausted");
+        }
+
+        public int get_num_possible_addresses() {
+            int set_bits = 0;
+            var subnet_mask = get_binary_representation_of_ip(internal_network_mask);
+            for (int i = 0; i < subnet_mask.length; i++) {
+                uint8 mask = subnet_mask[i];
+                while (mask != 0) {
+                    set_bits += mask & 1;
+                    mask >>= 1;
+                }
+            }
+            return (int) Math.pow (2, 32 - set_bits) - 2;
+        }
+
+        public void inject(TunnelFrame frame, InetSocketAddress source) throws Error {
+
+            if(frame.protocol == SocketFamily.INVALID) {
+                print("Refusing to inject non IP frame\n");
+                return;
+            }
+
+            if(!frame.is_tcp) {
+                print("Refusing to inject non-tcp datagram\n");
+                return;
+            }
+
+            var client = registered_clients.first_or_default(c => c.forward_address.address.equal(source.address) && c.forward_address.port == source.port);
+            if(client == null) {
+                print("Refusing to inject frame from non-registered client.\n");
+                return;
+            }
+
+            if(!frame.source_ip.equal(client.internal_address)) {
+                print("Refusing to inject frame from an address that does not belong to the sending client.\n");
+            }
+            
+            var mapping = address_mapping
+                .where(m => m.internal_address.equal(frame.source_ip))
+                .where(m => m.target_port == frame.source_port)
+                .first_or_default();
+
+            if(mapping == null) {
+                print("Refusing to inject frame with non-mapped destination IP.\n");
+                return;
+            }
+
+            print(@"INJECT Remapping $(frame.destination_ip) to $(proxy_internal_ip)\n");
+            frame.replace_destination_ip(tunnel.proxy_internal_ip);
+            handle_posix_error("injecting frame", (int)Posix.write(fd, frame.raw_frame, frame.raw_frame.length));
+        }
+    }
+
+    private static uint8[] get_binary_representation_of_ip(InetAddress address) {
+        var a = new uint8[address.get_native_size()];
+        Memory.copy(a, address.bytes, a.length);
+        return a;
+    }
+
+
+}

+ 0 - 286
src/tunnel.vala

@@ -1,286 +0,0 @@
-using Invercargill;
-
-namespace Astrogate {
-
-    public errordomain TunnelError {
-        IFACE_NAME_TOO_LONG,
-        POSIX_ERROR,
-        ADDRESS_SPACE_EXHAUSTED
-    }
-
-    public class Tunnel {
-
-        public InetAddress proxy_internal_ip {get; private set;}
-        public InetAddress internal_network_ip {get; private set;}
-        public InetAddress internal_network_mask {get; private set;}
-        public InetSocketAddress proxy_socket_address {get; private set;}
-        public int mtu { get; private set; }
-        public string iface { get; private set; }
-
-        private int address_allocations = 0;
-        private Socket socket;
-        private Vector<TunnelAddressMapping> address_mapping = new Vector<TunnelAddressMapping> ();
-        private int fd;
-
-        public Tunnel(string iface, int mtu, InetSocketAddress server_address, InetAddress subnet, InetAddress subnet_mask) throws Error {
-            this.iface = iface;
-            this.mtu = mtu;
-            proxy_socket_address = server_address;
-            internal_network_ip = subnet;
-            internal_network_mask = subnet_mask;
-
-            var socket = new Socket(proxy_socket_address.family, SocketType.DATAGRAM, SocketProtocol.UDP);
-            socket.bind(proxy_socket_address, true);
-
-            // Open tun device
-            fd = handle_posix_error("opening tun device", Posix.open("/dev/net/tun", Posix.O_RDWR));
-
-            // Create tunnel device
-            var request = generate_ifreq();
-            request.ifr_mtu = mtu;
-            //  address_to_posix(internal_network_ip, request.ifr_dstaddr);
-            //  address_to_posix(internal_network_mask, request.ifr_netmask);
-            while(true){
-                proxy_internal_ip = allocate_address();
-                print(@"Allocated $(proxy_internal_ip)\n");
-
-            }
-            //  address_to_posix(proxy_internal_ip, request.ifr_addr);
-
-            handle_posix_error("Setting ioctl for /dev/net/tun", Linux.ioctl(fd, IfTun.Tun.SETIFF, &request));
-        }
-
-        public int listen_tun() throws Error {
-
-            while(true) {
-                var buffer = new uint8[mtu+4];
-                var size = Posix.read(fd, buffer, buffer.length);
-                if(size < 0) {
-                    print(@"Got $(size) from Posix.read.\n");
-                    continue;
-                }
-                buffer.length = (int)size;
-
-                var frame = new TunnelFrame(buffer);
-
-                if(frame.protocol == SocketFamily.INVALID) {
-                    print("Ignoring non IP frame");
-                }
-
-                if(!frame.is_tcp) {
-                    print("Ignoring non-tcp datagram\n");
-                    continue;
-                }
-
-                if(!frame.source_ip.equal(proxy_internal_ip)) {
-                    print("Ignoring datagram from non-proxy address\n");
-                    continue;
-                }
-
-                var mapping = address_mapping
-                    .where(m => m.internal_address.address.equal(frame.destination_ip))
-                    .where(m => m.internal_address.port == frame.destination_port)
-                    .first_or_default();
-
-                if(mapping == null) {
-                    print("Ignoring non-mapped destination IP and port\n");
-                    continue;
-                }
-
-                frame.replace_source_ip(mapping.external_address);
-                frame.replace_source_port(mapping.target_port);
-
-                socket.send_to(mapping.forward_address, frame.raw_frame);
-            }
-        }
-
-        private Linux.Network.IfReq generate_ifreq() throws TunnelError {
-            var request = Linux.Network.IfReq();
-            request.ifr_flags = IfTun.IfFlag.TUN;
-            if(iface.length > request.ifr_name.length) {
-                throw new TunnelError.IFACE_NAME_TOO_LONG(@"Max length for interface name is $(request.ifr_name.length) characters, provided name is $(iface.length) characters.");
-            }
-            for(int i = 0; i < iface.length; i++){
-                request.ifr_name[i] = iface[i];
-            }
-            return request;
-        }
-
-        private int handle_posix_error(string task, int result) throws TunnelError {
-            if(result < 0) {
-                var code = Posix.errno;
-                var message = Posix.strerror(code);
-                throw new TunnelError.POSIX_ERROR(@"Got result code $(result) while $(task): ($(code)) $message");
-            }
-            return result;
-        }
-
-        private void address_to_posix(InetAddress address, Posix.SockAddr addr) {
-            switch (address.family) {
-                case SocketFamily.IPV4:
-                    addr.sa_family = Posix.AF_INET;
-                    break;
-                case SocketFamily.IPV6:
-                    addr.sa_family = Posix.AF_INET6;
-                    break;
-                default:
-                    assert_not_reached();
-            }
-
-            var data = get_binary_representation(address);
-            for(int i = 0; i < data.length; i++){
-                addr.sa_data[i] = (char)data[i];
-            }
-        }
-
-        private InetAddress allocate_address() throws TunnelError {
-            int index;
-            lock(address_allocations) {
-                index = address_allocations;
-                address_allocations++;
-            }
-            // First address is reserved
-            index++;
-
-            if(index > get_num_possible_addresses()) {
-                throw new TunnelError.ADDRESS_SPACE_EXHAUSTED("Exhausted IP address space");
-            }
-
-            var address = get_binary_representation(internal_network_ip);
-            var mask = get_binary_representation(internal_network_mask);
-
-            var pos = address.length-1;
-            var carry = index;
-            while(carry != 0) {
-                var remaining = carry % 256;
-                carry = carry / 256;
-                address[pos] = (address[pos] & mask[pos]) | (remaining & ~mask[pos]);
-                pos--;
-            }
-
-            return new InetAddress.from_bytes(address, internal_network_ip.family);
-        }
-
-        public int get_num_possible_addresses() {
-            int set_bits = 0;
-            var subnet_mask = get_binary_representation(internal_network_mask);
-            for (int i = 0; i < subnet_mask.length; i++) {
-                uint8 mask = subnet_mask[i];
-                while (mask != 0) {
-                    set_bits += mask & 1;
-                    mask >>= 1;
-                }
-            }
-            return (int) Math.pow (2, 32 - set_bits) - 2;
-        }
-    }
-
-    private static uint8[] get_binary_representation(InetAddress address) {
-        var a = new uint8[address.get_native_size()];
-        Memory.copy(a, address.bytes, a.length);
-        return a;
-    }
-
-    public class TunnelAddressMapping {
-        public InetSocketAddress internal_address {get; set;}
-        public InetSocketAddress forward_address {get; set;}
-        public InetAddress external_address {get; set;}
-        public uint16 target_port {get; set;}
-    }
-
-    private class TunnelFrame {
-        public SocketFamily protocol {get; private set;}
-        public InetAddress source_ip {get; private set;}
-        public InetAddress destination_ip {get; private set;}
-        public uint16 source_port {get; private set;}
-        public uint16 destination_port {get; private set;}
-        public uint8[] raw_frame {get; private set;}
-        public bool is_tcp {get; private set;}
-
-        private int payload_start = -1;
-
-        public TunnelFrame(uint8[] raw_frame) {
-            this.raw_frame = raw_frame;
-            if(raw_frame[2] == 0x08 && raw_frame[3] == 0x00) {
-                protocol = SocketFamily.IPV4;
-                print("Reading IPv4 datagram!\n");
-                read_ipv4_datagram();
-            }
-            else if(raw_frame[2] == 0x86 && raw_frame[3] == 0xdd) {
-                protocol = SocketFamily.IPV6;
-                print("IPv6 Not Implemented!\n");
-            }
-            else {
-                print("Could not determine protocol type!\n");
-                protocol = SocketFamily.INVALID;
-            }
-        }
-
-        private void read_ipv4_datagram() {
-            uint internet_header_length = raw_frame[4] & 0x0F;
-            source_ip = new InetAddress.from_bytes(raw_frame[16:20], SocketFamily.IPV4);
-            destination_ip = new InetAddress.from_bytes(raw_frame[20:22], SocketFamily.IPV4);
-
-            print(@"Src: $(source_ip.to_string()), Dst: $(destination_ip.to_string())\n");
-
-            // TUN header = 4 bytes, internet_header_length = IP header size in 32-bit word sizes
-            payload_start = 4 + (((int)internet_header_length) * 4);
-
-            if(raw_frame[13] == 0x06) {
-                print(@"Is TCP! (offset $payload_start == 4 + $internet_header_length * 4)\n");
-                is_tcp = true;
-                read_tcp_ports();
-            }
-            else {
-                print("Is not TCP!\n");
-            }
-        }
-
-        private void read_tcp_ports() {
-            uint16 src = 0;
-            uint16 dst = 0;
-            Memory.copy(&src, raw_frame[payload_start:payload_start+2], sizeof(int16));
-            Memory.copy(&dst, raw_frame[payload_start+2:payload_start+4], sizeof(int16));
-
-            src = src.to_big_endian();
-            dst = dst.to_big_endian();
-
-            print(@"Src: $(src), Dst: $dst\n");
-
-            source_port = src;
-            destination_port = dst;
-        }
-
-        private void write_tcp_ports(uint16 src, uint16 dst) {
-            var s = src.to_little_endian();
-            var d = dst.to_little_endian();
-            var sbytes = new uint8[sizeof(uint16)];
-            var dbytes = new uint8[sizeof(uint16)];
-            Memory.copy(sbytes, &s, sizeof(uint16));
-            Memory.copy(dbytes, &d, sizeof(uint16));
-
-            copy_into(raw_frame, payload_start, 2, sbytes);
-            copy_into(raw_frame, payload_start+2, 2, dbytes);
-        }
-
-        private void copy_into(uint8[] buffer, int start, int length, uint8[] replacement) {
-            for(int i = start; i < start+length; i++) {
-                buffer[i] = replacement[i];
-            }
-        }
-
-        public void replace_source_ip(InetAddress address) {
-            source_ip = address;
-            if(protocol == SocketFamily.IPV4) {
-                copy_into(raw_frame, 16, 4, get_binary_representation(address));
-            }
-        }
-
-        public void replace_source_port(uint16 port) {
-            source_port = port;
-            write_tcp_ports(source_port, destination_port);
-        }
-
-    }
-
-}