at 23.11-beta 6.0 kB view raw
1let 2 certs = import ./snakeoil-certs.nix; 3in 4import ../make-test-python.nix ({ pkgs, ... }: { 5 name = "nginx-proxyprotocol"; 6 7 meta = { 8 maintainers = with pkgs.lib.maintainers; [ raitobezarius ]; 9 }; 10 11 nodes = { 12 webserver = { pkgs, lib, ... }: { 13 environment.systemPackages = [ pkgs.netcat ]; 14 security.pki.certificateFiles = [ 15 certs.ca.cert 16 ]; 17 18 networking.extraHosts = '' 19 127.0.0.5 proxy.test.nix 20 127.0.0.5 noproxy.test.nix 21 127.0.0.3 direct-nossl.test.nix 22 127.0.0.4 unsecure-nossl.test.nix 23 127.0.0.2 direct-noproxy.test.nix 24 127.0.0.1 direct-proxy.test.nix 25 ''; 26 services.nginx = { 27 enable = true; 28 defaultListen = [ 29 { addr = "127.0.0.1"; proxyProtocol = true; ssl = true; } 30 { addr = "127.0.0.2"; } 31 { addr = "127.0.0.3"; ssl = false; } 32 { addr = "127.0.0.4"; ssl = false; proxyProtocol = true; } 33 ]; 34 commonHttpConfig = '' 35 log_format pcombined '(proxy_protocol=$proxy_protocol_addr) - (remote_addr=$remote_addr) - (realip=$realip_remote_addr) - (upstream=) - (remote_user=$remote_user) [$time_local] ' 36 '"$request" $status $body_bytes_sent ' 37 '"$http_referer" "$http_user_agent"'; 38 access_log /var/log/nginx/access.log pcombined; 39 error_log /var/log/nginx/error.log; 40 ''; 41 virtualHosts = 42 let 43 commonConfig = { 44 locations."/".return = "200 '$remote_addr'"; 45 extraConfig = '' 46 set_real_ip_from 127.0.0.5/32; 47 real_ip_header proxy_protocol; 48 ''; 49 }; 50 in 51 { 52 "*.test.nix" = commonConfig // { 53 sslCertificate = certs."*.test.nix".cert; 54 sslCertificateKey = certs."*.test.nix".key; 55 forceSSL = true; 56 }; 57 "direct-nossl.test.nix" = commonConfig; 58 "unsecure-nossl.test.nix" = commonConfig // { 59 extraConfig = '' 60 real_ip_header proxy_protocol; 61 ''; 62 }; 63 }; 64 }; 65 66 services.sniproxy = { 67 enable = true; 68 config = '' 69 error_log { 70 syslog daemon 71 } 72 access_log { 73 syslog daemon 74 } 75 listener 127.0.0.5:443 { 76 protocol tls 77 source 127.0.0.5 78 } 79 table { 80 ^proxy\.test\.nix$ 127.0.0.1 proxy_protocol 81 ^noproxy\.test\.nix$ 127.0.0.2 82 } 83 ''; 84 }; 85 }; 86 }; 87 88 testScript = '' 89 def check_origin_ip(src_ip: str, dst_url: str, failure: bool = False, proxy_protocol: bool = False, expected_ip: str | None = None): 90 check = webserver.fail if failure else webserver.succeed 91 if expected_ip is None: 92 expected_ip = src_ip 93 94 return check(f"curl {'--haproxy-protocol' if proxy_protocol else '''} --interface {src_ip} --fail -L {dst_url} | grep '{expected_ip}'") 95 96 webserver.wait_for_unit("nginx") 97 webserver.wait_for_unit("sniproxy") 98 # This should be closed by virtue of ssl = true; 99 webserver.wait_for_closed_port(80, "127.0.0.1") 100 # This should be open by virtue of no explicit ssl 101 webserver.wait_for_open_port(80, "127.0.0.2") 102 # This should be open by virtue of ssl = true; 103 webserver.wait_for_open_port(443, "127.0.0.1") 104 # This should be open by virtue of no explicit ssl 105 webserver.wait_for_open_port(443, "127.0.0.2") 106 # This should be open by sniproxy 107 webserver.wait_for_open_port(443, "127.0.0.5") 108 # This should be closed by sniproxy 109 webserver.wait_for_closed_port(80, "127.0.0.5") 110 111 # Sanity checks for the NGINX module 112 # direct-HTTP connection to NGINX without TLS, this checks that ssl = false; works well. 113 check_origin_ip("127.0.0.10", "http://direct-nossl.test.nix/") 114 # webserver.execute("openssl s_client -showcerts -connect direct-noproxy.test.nix:443") 115 # direct-HTTP connection to NGINX with TLS 116 check_origin_ip("127.0.0.10", "http://direct-noproxy.test.nix/") 117 check_origin_ip("127.0.0.10", "https://direct-noproxy.test.nix/") 118 # Well, sniproxy is not listening on 80 and cannot redirect 119 check_origin_ip("127.0.0.10", "http://proxy.test.nix/", failure=True) 120 check_origin_ip("127.0.0.10", "http://noproxy.test.nix/", failure=True) 121 122 # Actual PROXY protocol related tests 123 # Connecting through sniproxy should passthrough the originating IP address. 124 check_origin_ip("127.0.0.10", "https://proxy.test.nix/") 125 # Connecting through sniproxy to a non-PROXY protocol enabled listener should not pass the originating IP address. 126 check_origin_ip("127.0.0.10", "https://noproxy.test.nix/", expected_ip="127.0.0.5") 127 128 # Attack tests against spoofing 129 # Let's try to spoof our IP address by connecting direct-y to the PROXY protocol listener. 130 # FIXME(RaitoBezarius): rewrite it using Python + (Scapy|something else) as this is too much broken unfortunately. 131 # Or wait for upstream curl patch. 132 # def generate_attacker_request(original_ip: str, target_ip: str, dst_url: str): 133 # return f"""PROXY TCP4 {original_ip} {target_ip} 80 80 134 # GET / HTTP/1.1 135 # Host: {dst_url} 136 137 # """ 138 # def spoof(original_ip: str, target_ip: str, dst_url: str, tls: bool = False, expect_failure: bool = True): 139 # method = webserver.fail if expect_failure else webserver.succeed 140 # port = 443 if tls else 80 141 # print(webserver.execute(f"cat <<EOF | nc {target_ip} {port}\n{generate_attacker_request(original_ip, target_ip, dst_url)}\nEOF")) 142 # return method(f"cat <<EOF | nc {target_ip} {port} | grep {original_ip}\n{generate_attacker_request(original_ip, target_ip, dst_url)}\nEOF") 143 144 # check_origin_ip("127.0.0.10", "http://unsecure-nossl.test.nix", proxy_protocol=True) 145 # spoof("1.1.1.1", "127.0.0.4", "direct-nossl.test.nix") 146 # spoof("1.1.1.1", "127.0.0.4", "unsecure-nossl.test.nix", expect_failure=False) 147 ''; 148})