at master 12 kB view raw
1# This is a distributed test of the Network Address Translation involving a topology 2# with a router inbetween three separate virtual networks: 3# - "external" -- i.e. the internet, 4# - "internal" -- i.e. an office LAN, 5# 6# This test puts one server on each of those networks and its primary goal is to ensure that: 7# - server (named client in the code) in internal network can reach server (named server in the code) on the external network, 8# - server in external network can not reach server in internal network (skipped in some cases), 9# - when using externalIP, only the specified IP is used for NAT, 10# - port forwarding functionality behaves correctly 11# 12# The client is behind the nat (read: protected by the nat) and the server is on the external network, attempting to access services behind the NAT. 13 14import ./make-test-python.nix ( 15 { 16 pkgs, 17 lib, 18 withFirewall ? false, 19 nftables ? false, 20 ... 21 }: 22 let 23 unit = if nftables then "nftables" else (if withFirewall then "firewall" else "nat"); 24 25 routerAlternativeExternalIp = "192.168.2.234"; 26 27 makeNginxConfig = hostname: { 28 enable = true; 29 virtualHosts."${hostname}" = { 30 root = "/etc"; 31 locations."/".index = "hostname"; 32 listen = [ 33 { 34 addr = "0.0.0.0"; 35 port = 80; 36 } 37 { 38 addr = "0.0.0.0"; 39 port = 8080; 40 } 41 ]; 42 }; 43 }; 44 45 makeCommonConfig = hostname: { 46 services.nginx = makeNginxConfig hostname; 47 services.vsftpd = { 48 enable = true; 49 anonymousUser = true; 50 localRoot = "/etc/"; 51 extraConfig = '' 52 pasv_min_port=51000 53 pasv_max_port=51999 54 ''; 55 }; 56 57 # Disable eth0 autoconfiguration 58 networking.useDHCP = false; 59 60 environment.systemPackages = [ 61 (pkgs.writeScriptBin "check-connection" '' 62 #!/usr/bin/env bash 63 64 set -e 65 66 if [[ "$2" == "" || "$3" == "" || "$1" == "--help" || "$1" == "-h" ]]; 67 then 68 echo "check-connection <target-address> <target-hostname> <[expect-success|expect-failure]>" 69 exit 1 70 fi 71 72 ADDRESS="$1" 73 HOSTNAME="$2" 74 75 function test_icmp() { timeout 3 ping -c 1 $ADDRESS; } 76 function test_http() { [[ `timeout 3 curl $ADDRESS` == "$HOSTNAME" ]]; } 77 function test_ftp() { timeout 3 curl ftp://$ADDRESS; } 78 79 if [[ "$3" == "expect-success" ]]; 80 then 81 test_icmp; test_http; test_ftp 82 else 83 ! test_icmp; ! test_http; ! test_ftp 84 fi 85 '') 86 (pkgs.writeScriptBin "check-last-clients-ip" '' 87 #!/usr/bin/env bash 88 set -e 89 90 [[ `cat /var/log/nginx/access.log | tail -n1 | awk '{print $1}'` == "$1" ]] 91 '') 92 ]; 93 }; 94 95 in 96 # VLANS: 97 # 1 -- simulates the internal network 98 # 2 -- simulates the external network 99 { 100 name = 101 "nat" 102 + (lib.optionalString nftables "Nftables") 103 + (if withFirewall then "WithFirewall" else "Standalone"); 104 meta = with pkgs.lib.maintainers; { 105 maintainers = [ 106 tne 107 rob 108 ]; 109 }; 110 111 nodes = { 112 client = 113 { pkgs, nodes, ... }: 114 lib.mkMerge [ 115 (makeCommonConfig "client") 116 { 117 virtualisation.vlans = [ 1 ]; 118 networking.defaultGateway = 119 (pkgs.lib.head nodes.router.networking.interfaces.eth1.ipv4.addresses).address; 120 networking.nftables.enable = nftables; 121 networking.firewall.enable = false; 122 } 123 ]; 124 125 router = 126 { nodes, ... }: 127 lib.mkMerge [ 128 (makeCommonConfig "router") 129 { 130 virtualisation.vlans = [ 131 1 132 2 133 ]; 134 networking.firewall = { 135 enable = withFirewall; 136 filterForward = nftables; 137 allowedTCPPorts = [ 138 21 139 80 140 8080 141 ]; 142 # For FTP passive mode 143 allowedTCPPortRanges = [ 144 { 145 from = 51000; 146 to = 51999; 147 } 148 ]; 149 }; 150 networking.nftables.enable = nftables; 151 networking.nat = 152 let 153 clientIp = (pkgs.lib.head nodes.client.networking.interfaces.eth1.ipv4.addresses).address; 154 serverIp = (pkgs.lib.head nodes.router.networking.interfaces.eth2.ipv4.addresses).address; 155 in 156 { 157 enable = true; 158 internalIPs = [ "${clientIp}/24" ]; 159 # internalInterfaces = [ "eth1" ]; 160 externalInterface = "eth2"; 161 externalIP = serverIp; 162 163 forwardPorts = [ 164 { 165 destination = "${clientIp}:8080"; 166 proto = "tcp"; 167 sourcePort = 8080; 168 169 loopbackIPs = [ serverIp ]; 170 } 171 ]; 172 }; 173 174 networking.interfaces.eth2.ipv4.addresses = lib.mkOrder 10000 [ 175 { 176 address = routerAlternativeExternalIp; 177 prefixLength = 24; 178 } 179 ]; 180 181 services.nginx.virtualHosts.router.listen = lib.mkOrder (-1) [ 182 { 183 addr = routerAlternativeExternalIp; 184 port = 8080; 185 } 186 ]; 187 188 specialisation.no-nat.configuration = { 189 networking.nat.enable = lib.mkForce false; 190 }; 191 } 192 ]; 193 194 server = 195 { nodes, ... }: 196 lib.mkMerge [ 197 (makeCommonConfig "server") 198 { 199 virtualisation.vlans = [ 2 ]; 200 networking.firewall.enable = false; 201 202 networking.defaultGateway = 203 (pkgs.lib.head nodes.router.networking.interfaces.eth2.ipv4.addresses).address; 204 } 205 ]; 206 }; 207 208 testScript = 209 { nodes, ... }: 210 let 211 clientIp = (pkgs.lib.head nodes.client.networking.interfaces.eth1.ipv4.addresses).address; 212 serverIp = (pkgs.lib.head nodes.server.networking.interfaces.eth1.ipv4.addresses).address; 213 routerIp = (pkgs.lib.head nodes.router.networking.interfaces.eth2.ipv4.addresses).address; 214 in 215 '' 216 def wait_for_machine(m): 217 m.wait_for_unit("network.target") 218 m.wait_for_unit("nginx.service") 219 220 client.start() 221 router.start() 222 server.start() 223 224 wait_for_machine(router) 225 wait_for_machine(client) 226 wait_for_machine(server) 227 228 # We assume we are isolated from layer 2 attacks or are securely configured (like disabling forwarding by default) 229 # Relevant moby issue describing the problem allowing bypassing of NAT: https://github.com/moby/moby/issues/14041 230 ${lib.optionalString (!nftables) '' 231 router.succeed("iptables -P FORWARD DROP") 232 ''} 233 234 # Sanity checks. 235 ## The router should have direct access to the server 236 router.succeed("check-connection ${serverIp} server expect-success") 237 ## The server should have direct access to the router 238 server.succeed("check-connection ${routerIp} router expect-success") 239 240 # The client should be also able to connect via the NAT router... 241 client.succeed("check-connection ${serverIp} server expect-success") 242 # ... but its IP should be rewritten to be that of the router. 243 server.succeed("check-last-clients-ip ${routerIp}") 244 245 # Active FTP (where the FTP server connects back to us via a random port) should work directly... 246 router.succeed("timeout 3 curl -P eth2:51000-51999 ftp://${serverIp}") 247 # ... but not from behind NAT. 248 client.fail("timeout 3 curl -P eth1:51000-51999 ftp://${serverIp};") 249 250 # If using nftables without firewall, filterForward can't be used and L2 security can't easily be simulated like with iptables, skipping. 251 # See moby github issue mentioned above. 252 ${lib.optionalString (nftables && withFirewall) '' 253 # The server should not be able to reach the client directly... 254 server.succeed("check-connection ${clientIp} client expect-failure") 255 ''} 256 # ... but the server should be able to reach a port forwarded address of the client 257 server.succeed('[[ `timeout 3 curl http://${routerIp}:8080` == "client" ]]') 258 # The IP address the client sees should not be rewritten to be that of the router (#277016) 259 client.succeed("check-last-clients-ip ${serverIp}") 260 261 # But this forwarded port shouldn't intercept communication with 262 # other IPs than externalIp. 263 server.succeed('[[ `timeout 3 curl http://${routerAlternativeExternalIp}:8080` == "router" ]]') 264 265 # The loopback should allow the router itself to access the forwarded port 266 # Note: The reason we use routerIp here is because only routerIp is listed for reflection in networking.nat.forwardPorts.loopbackIPs 267 # The purpose of loopbackIPs is to allow things inside of the NAT to for example access their own public domain when a service has to make a request 268 # to itself/another service on the same NAT through a public address 269 router.succeed('[[ `timeout 3 curl http://${routerIp}:8080` == "client" ]]') 270 # The loopback should also allow the client to access its own forwarded port 271 client.succeed('[[ `timeout 3 curl http://${routerIp}:8080` == "client" ]]') 272 273 # If we turn off NAT, nothing should work 274 router.succeed( 275 "systemctl stop ${unit}.service" 276 ) 277 278 # If using nftables and firewall, this makes no sense. We deactivated the firewall after all, 279 # so we are once again affected by the same issue as the moby github issue mentioned above. 280 # If using nftables without firewall, filterForward can't be used and L2 security can't easily be simulated like with iptables, skipping. 281 # See moby github issue mentioned above. 282 ${lib.optionalString (!nftables) '' 283 client.succeed("check-connection ${serverIp} server expect-failure") 284 server.succeed("check-connection ${clientIp} client expect-failure") 285 ''} 286 # These should revert to their pre-NATed versions 287 server.succeed('[[ `timeout 3 curl http://${routerIp}:8080` == "router" ]]') 288 router.succeed('[[ `timeout 3 curl http://${routerIp}:8080` == "router" ]]') 289 290 # Reverse the effect of nat stop 291 router.succeed( 292 "systemctl start ${unit}.service" 293 ) 294 295 # Switch to a config without NAT at all, again nothing should work 296 router.succeed( 297 "/run/booted-system/specialisation/no-nat/bin/switch-to-configuration test 2>&1" 298 ) 299 300 # If using nftables without firewall, filterForward can't be used and L2 security can't easily be simulated like with iptables, skipping. 301 # See moby github issue mentioned above. 302 ${lib.optionalString (nftables && withFirewall) '' 303 client.succeed("check-connection ${serverIp} server expect-failure") 304 server.succeed("check-connection ${clientIp} client expect-failure") 305 ''} 306 307 # These should revert to their pre-NATed versions 308 server.succeed('[[ `timeout 3 curl http://${routerIp}:8080` == "router" ]]') 309 router.succeed('[[ `timeout 3 curl http://${routerIp}:8080` == "router" ]]') 310 ''; 311 } 312)