소스 검색

Initial commit

Billy Barrow 1 년 전
커밋
42e6c41f0b
7개의 변경된 파일404개의 추가작업 그리고 0개의 파일을 삭제
  1. 1 0
      .gitignore
  2. 3 0
      README.md
  3. 8 0
      config
  4. 146 0
      src/configuration.vala
  5. 139 0
      src/header_reader.vala
  6. 20 0
      src/meson.build
  7. 87 0
      src/server.vala

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+/build

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+# Astrogate
+
+Astrologue uses the Yggdrasil network as a backbone of some of its peer-to-peer infrastructre. Astrogate allows configuring gateways that can forward traffic from the clear internet onto the Yggdrasil network and vice-versa following a simple configuration.

+ 8 - 0
config

@@ -0,0 +1,8 @@
+
+TLS billy.barrow.nz 443 {
+    185.112.145.195 443
+}
+
+HTTP billy.barrow.nz 80 {
+    185.112.145.195 80
+}

+ 146 - 0
src/configuration.vala

@@ -0,0 +1,146 @@
+
+using Invercargill;
+
+namespace Astrogate {
+
+    errordomain ConfigurationFormatError {
+        MALFORMED_ENTRY,
+        UNRECOGNISED_PROTOCOL,
+    }
+ 
+    public class Configuration {
+
+        private Vector<ConfigurationItem> config_items;
+        public Enumerable<ConfigurationItem> entries { get {
+            return config_items;
+        }}
+
+        public Configuration() {
+            config_items = new Vector<ConfigurationItem>();
+        }
+
+        public async void read_file(File file) throws Error {
+            var data = new DataInputStream(yield file.read_async(0));
+
+            while(true) {
+                var line = yield data.read_line_async(0);
+                if(line == null) {
+                    return;
+                }
+                if(line.chomp().chug() == "") {
+                    continue;
+                }
+                var type = read_field(line, 0);
+                var host = read_field(line, 1);
+                var port = read_field(line, 2);
+                var delimiter = read_field(line, 3);
+
+                if(type == null || host == null || port == null || delimiter == null) {
+                    throw new ConfigurationFormatError.MALFORMED_ENTRY("One or more required configuration entry fields are missing");
+                }
+
+                var entry = new ConfigurationItem();
+                switch (type) {
+                    case "TLS":
+                        entry.protocol = ConnectionType.TLS;
+                        break;
+                    case "HTTP":
+                        entry.protocol = ConnectionType.HTTP;
+                        break;
+                    case "ASTROGATE":
+                        entry.protocol = ConnectionType.ASTORGATE;
+                        break;
+                    default:
+                        throw new ConfigurationFormatError.UNRECOGNISED_PROTOCOL(@"Unrecognised protocol type $(type)");
+                }
+
+                entry.hostname = host;
+
+                int port_no;
+                if(!int.try_parse(port, out port_no)) {
+                    throw new ConfigurationFormatError.MALFORMED_ENTRY("Could not parse source port number");
+                }
+                entry.source_port = (uint16)port_no;
+
+                if(delimiter != "{") {
+                    throw new ConfigurationFormatError.MALFORMED_ENTRY("Entry does not contain \" {\".");
+                }
+
+                entry.destinations = new Series<InetSocketAddress>();
+                while(true) {
+                    var inner = yield data.read_line_async(0);
+                    if(inner.chomp().chug() == "") {
+                        continue;
+                    }
+                    if(inner == null) {
+                        if(inner == null) {
+                            throw new ConfigurationFormatError.MALFORMED_ENTRY("Unexpected end of file reading entry");
+                        }
+                        continue;
+                    }
+                    if(read_field(inner, 0) == "}") {
+                        break;
+                    }
+
+                    var net_address = read_field(inner, 0);
+                    var net_port = read_field(inner, 1);
+                                    
+                    if(net_address == null || net_port == null) {
+                        throw new ConfigurationFormatError.MALFORMED_ENTRY("One or more required destination entry fields are missing");
+                    }
+
+                    int net_port_no;
+                    if(!int.try_parse(net_port, out net_port_no)) {
+                        throw new ConfigurationFormatError.MALFORMED_ENTRY("Could not parse destination port number");
+                    }
+
+                    entry.destinations.add(new InetSocketAddress.from_string(net_address, net_port_no));
+                }
+
+                config_items.add(entry);
+            }
+        }
+
+        private static string? read_field(string line, int field) {
+            var text = "";
+            var fields = 0;
+            var in_field = false;
+            var text_populated = false;
+            for(var i = 0; i < line.length; i++){
+                var c = line[i];
+                var is_whitespace = (c == ' ' || c == ' ');
+                if(is_whitespace && !in_field) {
+                    continue;
+                }
+                if(is_whitespace) {
+                    in_field = false;
+                    fields++;
+                    if(fields > field) {
+                        break;
+                    }
+                    continue;
+                }
+                in_field = true;
+                if(fields == field){
+                    text_populated = true;
+                    text += @"$(c)";
+                }
+            }
+            if(text_populated) {
+                return text;
+            }
+            return null;
+        }
+
+    }
+
+    public class ConfigurationItem {
+
+        public ConnectionType protocol {get; set;}
+        public string hostname {get; set;}
+        public uint16 source_port {get; set;}
+        public Series<InetSocketAddress> destinations {get; set;}
+
+    }
+
+}

+ 139 - 0
src/header_reader.vala

@@ -0,0 +1,139 @@
+
+using Invercargill;
+
+namespace Astrogate {
+
+    public enum ConnectionType {
+        INVALID,
+        TLS,
+        HTTP,
+        ASTORGATE
+    }
+ 
+    public class HeaderReader {
+
+        private static int MIN_TLS_SNI_DATA_SIZE = 9;
+
+        public static async BinaryData read_sample(InputStream stream) throws Error{
+            var buffer = new uint8[8192];
+            size_t bytes_read = yield stream.read_async(buffer);
+            buffer.length = (int)bytes_read;
+            return new BinaryData.from_byte_array(buffer);
+        }
+
+        public static ConnectionType determine_type(BinaryData data) {
+            // TLS
+            if(data.first_or_default() == 22) {
+                return ConnectionType.TLS;
+            }
+
+            // Astrogate
+            var ast_header = new BinaryData();
+            ast_header.append_string("astrogate-dial: ", true);
+            if(ast_header.equals(data.take(16))) {
+                return ConnectionType.ASTORGATE;
+            }
+
+            // HTTP
+            var str_data = data.to_raw_string();
+            var id_start = str_data.index_of("HTTP/");
+            if(id_start != -1) {
+                var headers = str_data.split("\r\n");
+                foreach (var header in headers) {
+                    if(header.has_prefix("Host:")) {
+                        return ConnectionType.HTTP;
+                    }
+                }
+            }
+
+            return ConnectionType.INVALID;
+        }
+
+        public static string? read_name(BinaryData data, ConnectionType type) {
+            switch(type) {
+                case ConnectionType.HTTP:
+                    return read_http_host(data);
+                case ConnectionType.ASTORGATE:
+                    return read_astrogate_name(data);
+                case ConnectionType.TLS:
+                    return read_tls_sni(data);
+                default:
+                    return null;
+            }
+        }
+
+        public static string? read_http_host(BinaryData data) {
+            var str_data = data.to_raw_string();
+            var id_start = str_data.index_of("HTTP/", 0);
+            if(id_start != -1) {
+                var headers = str_data.split("\r\n");
+                foreach (var header in headers) {
+                    if(header.has_prefix("Host:")) {
+                        var host = header[5:header.length].chomp().chug();
+                        return host.split(":", 2)[0];
+                    }
+                }
+            }
+            return null;
+        }
+
+        public static string? read_astrogate_name(BinaryData data) {
+            var ast_header = new BinaryData();
+            ast_header.append_string("astrogate-dial: ", true);
+            if(!ast_header.equals(data.take(16))) {
+                return null;
+            }
+
+            var str_data = data.to_escaped_string();
+            var header = str_data.split("\n", 2);
+            if(header.length != 2) {
+                return null;
+            }
+
+            return header[0][16:header.length];
+        }
+
+        public static string? read_tls_sni(BinaryData data) {
+            var buffer = data.to_array();
+            while(buffer.length >= MIN_TLS_SNI_DATA_SIZE) {
+                var result = try_read_sni(buffer);
+                if(result != null) {
+                    return result;
+                }
+                buffer = buffer[1:buffer.length];
+            }
+            return null;
+        }
+
+        private static string? try_read_sni(uint8[] data) {
+
+            if(data[0] != 0 || data[1] != 0 || data[6] != 0) {
+                return null;
+            }
+
+            var size_1 = read_uint16(data[2:4]);
+            var size_2 = read_uint16(data[4:6]);
+            if(size_1 != size_2 + 2) {
+                return null;
+            }
+            
+            var name_size = read_uint16(data[7:9]);
+            if(size_2 != name_size + 3) {
+                return null;
+            }
+
+            var name = new uint8[name_size + 1];
+            Memory.copy(name, data[9:name_size], name_size);
+            name[name_size] = 0;
+            return (string)name;
+        }
+
+        private static uint16 read_uint16(uint8[] data) {
+            uint16 val = 0;
+            Memory.copy(&val, data, sizeof(int16));
+            return val.to_big_endian();
+        }
+
+    }
+
+}

+ 20 - 0
src/meson.build

@@ -0,0 +1,20 @@
+project('astrogate', 'vala', 'c')
+
+add_project_arguments(['--disable-warnings', '--enable-checking'], language: 'vala')
+
+
+sources = files('server.vala')
+sources += files('header_reader.vala')
+sources += files('configuration.vala')
+
+
+dependencies = [
+    dependency('glib-2.0'),
+    dependency('gobject-2.0'),
+    dependency('gio-2.0'),
+    dependency('gee-0.8'),
+    dependency('invercargill'),
+    meson.get_compiler('c').find_library('m', required: false),
+]
+
+executable('astrogate', sources, dependencies: dependencies, install: true)

+ 87 - 0
src/server.vala

@@ -0,0 +1,87 @@
+using Invercargill;
+
+namespace Astrogate {
+
+    private static Configuration config;
+
+    public static int main(string[] args) {
+        var config_path = args.length > 1 ? args[1] : "/etc/astrogate.conf";
+        run.begin(config_path);
+
+        new MainLoop().run();
+        return 0;
+    }
+
+    private async void run(string config_path) throws Error {
+        config = new Configuration();
+        print("Reading config...\n");
+        yield config.read_file(File.new_for_commandline_arg(config_path));
+
+        var service = new SocketService ();
+        var ports = config.entries
+            .select<uint16>(e => e.source_port)
+            .unique((a, b) => a == b);
+
+        foreach (var port in ports) {
+            print(@"Adding port $(port)\n");
+            service.add_inet_port (port, null);
+        }
+
+        service.incoming.connect((c, o) => {
+            handle_connection.begin(c);
+            return false;
+        });
+        service.start ();
+    }
+
+    private async void handle_connection(SocketConnection connection) {
+        try {
+            var port = ((InetSocketAddress)connection.get_local_address()).port;
+            print(@"New connection (port $(port))\n");
+            var buffer = yield HeaderReader.read_sample(connection.input_stream);
+            var type = HeaderReader.determine_type(buffer);
+            if(type == ConnectionType.INVALID) {
+                print("Invalid connection type.\n");
+                yield connection.close_async();
+                return;
+            }
+            var name = HeaderReader.read_name(buffer, type);
+            if(name != null) {
+                var entry = config.entries
+                    .where(e => e.protocol == type)
+                    .where(e => e.source_port == port)
+                    .where(e => e.hostname == name)
+                    .single_or_default();
+
+                if(entry == null) {
+                    print(@"No matching configuration entry found\n");
+                    yield connection.close_async();
+                    return;
+                }
+
+                print(@"Proxying $(entry.protocol) connection from $(connection.get_remote_address().to_string()) intended for $(entry.hostname):$(entry.source_port) to $(entry.destinations.first().to_string()).\n");
+                yield forward_connection(connection, entry.destinations.first(), buffer);
+            }
+            else {
+                print(@"Could not determine destination hostname\n");
+                yield connection.close_async();
+                return;
+            }
+        }
+        catch(Error e) {
+            warning(@"Error servicing connection: $(e.message)");
+        }
+    }
+
+    private async void forward_connection(SocketConnection connection, InetSocketAddress destination, BinaryData initial_buffer) throws Error {
+        var client = new SocketClient();
+        var proxy = yield client.connect_async(destination);
+        
+        yield proxy.output_stream.write_async(initial_buffer.to_array(), 0);
+        yield proxy.output_stream.flush_async(0);
+
+        proxy.output_stream.splice_async.begin(connection.input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE | OutputStreamSpliceFlags.CLOSE_TARGET, 0);
+        connection.output_stream.splice_async.begin(proxy.input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE | OutputStreamSpliceFlags.CLOSE_TARGET, 0);
+    }
+
+}