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