From f86b249ad203554a81b5edc8d424b5b13126aab1 Mon Sep 17 00:00:00 2001 From: Ryan Lahfa Date: Wed, 25 Feb 2026 22:14:46 +0100 Subject: [PATCH 1/2] state: pass default backend from settings This makes the upstream chaining feature functional for default targets. Signed-off-by: Ryan Lahfa --- src/config.rs | 3 +++ src/state.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index faa7359..964bcd8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -53,6 +53,9 @@ pub struct Settings { #[serde(default)] pub tcp_nodelay: bool, + /// Whether to set a default upstream. + pub default_backend: Option, + /// List of backends to which we can route proxy queries. #[serde(default)] pub backends: HashMap, diff --git a/src/state.rs b/src/state.rs index 94d6259..998cbc3 100644 --- a/src/state.rs +++ b/src/state.rs @@ -30,7 +30,7 @@ pub struct Statistics { pub fn init(settings: &Settings) -> State { State { - default_backend: None, + default_backend: settings.default_backend.clone(), acl_rules: crate::acl::load_rules_from_file(&settings.filter_acl_rules_path.clone().unwrap()).unwrap(), root_store: None, client_cert_resolver: None From fcc95b966115380b81495a69675e1ff9694b6fa8 Mon Sep 17 00:00:00 2001 From: Ryan Lahfa Date: Wed, 25 Feb 2026 16:10:29 +0100 Subject: [PATCH 2/2] nix/tests/e2e: init E2E connection via proxy This is two tests: - direct connect (autonomous operations) - chained connect (chain to another proxy) Signed-off-by: Ryan Lahfa --- nix/tests/default.nix | 176 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 173 insertions(+), 3 deletions(-) diff --git a/nix/tests/default.nix b/nix/tests/default.nix index bcd8154..955b724 100644 --- a/nix/tests/default.nix +++ b/nix/tests/default.nix @@ -1,19 +1,189 @@ { pkgs, ... }: +let + # Import nixpkgs snakeoil certs. + mkVirtualHost = svcName: { + extraConfig = '' + location / { + default_type application/json; + return 200 '{"remote_addr":"$remote_addr","service": "${svcName}"}'; + } + ''; + }; + mkServiceNode = {}: { + networking.interfaces.eth1.ipv4.addresses = [ + { + address = "192.168.1.200"; + prefixLength = 24; + } + ]; + + services.nginx = { + enable = true; + virtualHosts."hello.corp.example.com" = mkVirtualHost "hello.corp"; + virtualHosts."bad.corp.example.com" = mkVirtualHost "bad.corp"; + }; + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + }; +in { - daemon = pkgs.testers.nixosTest { - name = "daemon"; + exit-node = pkgs.testers.nixosTest { + name = "exit-node"; + nodes = { + corp-server = mkServiceNode { }; + node = { nodes, ... }: { + imports = [ ../module.nix ]; + + networking.hosts."${nodes.corp-server.networking.primaryIPAddress}" = [ "hello.corp.example.com" "bad.corp.example.com" ]; + + networking.interfaces.eth1.ipv4.addresses = [ + { + address = "192.168.1.100"; + prefixLength = 24; + } + ]; + services.portail = { + enable = true; + enableAtBoot = true; + acl.filter.rules = [ + "hello.corp.example.com -> allow" + ".* -> deny" + ]; + }; + }; + }; + testScript = '' + import json + + start_all() + + node.wait_for_unit("multi-user.target") + node.wait_for_unit("portail.service") + + self_ip = "192.168.1.100" + + # Wait for the server to be ready. + corp_server.wait_for_unit("multi-user.target") + + # Wait for NGINX to be ready. + corp_server.wait_for_open_port(80) + # corp_server.wait_for_open_port(443) + + # Wait for the SOCK5 server to be ready. + node.wait_for_open_port(8080) + + # Let's test SOCKS5 to hello.corp.example.com. + + # This tests without DNS resolution. + result = json.loads(node.succeed( + "curl --fail --socks5 127.0.0.1:8080 http://hello.corp.example.com" + )) + assert result['service'] == 'hello.corp' and result['remote_addr'] == self_ip, "Unexpected result from the web service: {}".format(json.dumps(result)) + # This exercise the SOCKS5 DNS resolution. + result = json.loads(node.succeed( + "curl --fail --socks5-hostname 127.0.0.1:8080 http://hello.corp.example.com" + )) + assert result['service'] == 'hello.corp' and result['remote_addr'] == self_ip, "Unexpected result from the web service: {}".format(json.dumps(result)) + # TODO: Test HTTPS as well. + + # This exercises rejections and ACLs. + # TODO: once ACLs are stabilized, uncomment. + # This tests without DNS resolution. + # node.fail( + # "curl --fail --socks5 127.0.0.1:8080 http://bad.corp.example.com" + # ) + # This exercise the SOCKS5 DNS resolution. + # node.fail( + # "curl --fail --socks5-hostname 127.0.0.1:8080 http://bad.corp.example.com" + # ) + ''; + }; + + # This tests Portail connecting to microsocks as an upstream. + microsocks-upstream = pkgs.testers.nixosTest { + name = "microsocks-upstream"; nodes = { - node = { + microsocks = { nodes, ... }: { + networking.interfaces.eth1.ipv4.addresses = [ + { + address = "192.168.1.50"; + prefixLength = 24; + } + ]; + + services.microsocks = { + enable = true; + ip = "0.0.0.0"; + port = 8080; + }; + + networking.hosts."${nodes.corp-server.networking.primaryIPAddress}" = [ "hello.corp.example.com" "bad.corp.example.com" ]; + networking.firewall.allowedTCPPorts = [ 8080 ]; + }; + corp-server = mkServiceNode { }; + node = { nodes, ... }: { imports = [ ../module.nix ]; + networking.hosts."${nodes.corp-server.networking.primaryIPAddress}" = [ "hello.corp.example.com" "bad.corp.example.com" ]; + networking.interfaces.eth1.ipv4.addresses = [ + { + address = "192.168.1.100"; + prefixLength = 24; + } + ]; services.portail = { enable = true; enableAtBoot = true; + settings = { + default-backend = "default"; + backends.default = { + target-address = "192.168.1.50:8080"; + }; + }; + acl.filter.rules = [ + "hello.corp.example.com -> allow" + ]; }; }; }; testScript = '' + import json + + start_all() + node.wait_for_unit("multi-user.target") node.wait_for_unit("portail.service") + + self_ip = "192.168.1.100" + + # Wait for the server to be ready. + corp_server.wait_for_unit("multi-user.target") + + # Wait for NGINX to be ready. + corp_server.wait_for_open_port(80) + # corp_server.wait_for_open_port(443) + + # Wait for microsocks to be ready. + microsocks.wait_for_unit("microsocks.service") + microsocks.wait_for_open_port(8080) + + # Wait for the SOCK5 server to be ready. + node.wait_for_open_port(8080) + + # Let's test SOCKS5 to hello.corp.example.com. + + # This tests without DNS resolution. + result = json.loads(node.succeed( + "curl --fail --socks5 127.0.0.1:8080 http://hello.corp.example.com" + )) + assert result['service'] == 'hello.corp' and result['remote_addr'] != self_ip, "Unexpected result from the web service: {}".format(json.dumps(result)) + # This exercise the SOCKS5 DNS resolution. + result = json.loads(node.succeed( + "curl --fail --socks5-hostname 127.0.0.1:8080 http://hello.corp.example.com" + )) + assert result['service'] == 'hello.corp' and result['remote_addr'] != self_ip, "Unexpected result from the web service: {}".format(json.dumps(result)) ''; }; + }