2024-07-21 05:28:21 +00:00
|
|
|
# This is a distributed test of the Network Address Translation involving a topology
|
|
|
|
# with a router inbetween three separate virtual networks:
|
|
|
|
# - "external" -- i.e. the internet,
|
|
|
|
# - "internal" -- i.e. an office LAN,
|
|
|
|
#
|
|
|
|
# This test puts one server on each of those networks and its primary goal is to ensure that:
|
|
|
|
# - server (named client in the code) in internal network can reach server (named server in the code) on the external network,
|
|
|
|
# - server in external network can not reach server in internal network (skipped in some cases),
|
|
|
|
# - when using externalIP, only the specified IP is used for NAT,
|
|
|
|
# - port forwarding functionality behaves correctly
|
|
|
|
#
|
|
|
|
# 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.
|
|
|
|
|
|
|
|
import ./make-test-python.nix ({ pkgs, lib, withFirewall ? false, nftables ? false, ... }:
|
2014-09-18 20:34:29 +00:00
|
|
|
let
|
2022-12-22 16:23:23 +00:00
|
|
|
unit = if nftables then "nftables" else (if withFirewall then "firewall" else "nat");
|
2024-07-21 05:28:21 +00:00
|
|
|
|
|
|
|
routerAlternativeExternalIp = "192.168.2.234";
|
|
|
|
|
|
|
|
makeNginxConfig = hostname: {
|
|
|
|
enable = true;
|
|
|
|
virtualHosts."${hostname}" = {
|
|
|
|
root = "/etc";
|
|
|
|
locations."/".index = "hostname";
|
|
|
|
listen = [
|
|
|
|
{
|
|
|
|
addr = "0.0.0.0";
|
|
|
|
port = 80;
|
|
|
|
}
|
|
|
|
{
|
|
|
|
addr = "0.0.0.0";
|
|
|
|
port = 8080;
|
|
|
|
}
|
|
|
|
];
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
makeCommonConfig = hostname: {
|
|
|
|
services.nginx = makeNginxConfig hostname;
|
|
|
|
services.vsftpd = {
|
|
|
|
enable = true;
|
|
|
|
anonymousUser = true;
|
|
|
|
localRoot = "/etc/";
|
|
|
|
extraConfig = ''
|
|
|
|
pasv_min_port=51000
|
|
|
|
pasv_max_port=51999
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
# Disable eth0 autoconfiguration
|
|
|
|
networking.useDHCP = false;
|
|
|
|
|
|
|
|
environment.systemPackages = [
|
|
|
|
(pkgs.writeScriptBin "check-connection"
|
|
|
|
''
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
|
|
|
set -e
|
|
|
|
|
|
|
|
if [[ "$2" == "" || "$3" == "" || "$1" == "--help" || "$1" == "-h" ]];
|
|
|
|
then
|
|
|
|
echo "check-connection <target-address> <target-hostname> <[expect-success|expect-failure]>"
|
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
|
|
|
|
ADDRESS="$1"
|
|
|
|
HOSTNAME="$2"
|
|
|
|
|
|
|
|
function test_icmp() { timeout 3 ping -c 1 $ADDRESS; }
|
|
|
|
function test_http() { [[ `timeout 3 curl $ADDRESS` == "$HOSTNAME" ]]; }
|
|
|
|
function test_ftp() { timeout 3 curl ftp://$ADDRESS; }
|
|
|
|
|
|
|
|
if [[ "$3" == "expect-success" ]];
|
|
|
|
then
|
|
|
|
test_icmp; test_http; test_ftp
|
|
|
|
else
|
|
|
|
! test_icmp; ! test_http; ! test_ftp
|
|
|
|
fi
|
|
|
|
''
|
|
|
|
)
|
|
|
|
(pkgs.writeScriptBin "check-last-clients-ip"
|
|
|
|
''
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
set -e
|
|
|
|
|
|
|
|
[[ `cat /var/log/nginx/access.log | tail -n1 | awk '{print $1}'` == "$1" ]]
|
|
|
|
''
|
|
|
|
)
|
|
|
|
];
|
|
|
|
};
|
|
|
|
|
|
|
|
# VLANS:
|
|
|
|
# 1 -- simulates the internal network
|
|
|
|
# 2 -- simulates the external network
|
2014-09-18 20:34:29 +00:00
|
|
|
in
|
|
|
|
{
|
2022-12-22 16:23:23 +00:00
|
|
|
name = "nat" + (lib.optionalString nftables "Nftables")
|
2023-03-04 07:50:38 +00:00
|
|
|
+ (if withFirewall then "WithFirewall" else "Standalone");
|
2021-01-10 19:08:30 +00:00
|
|
|
meta = with pkgs.lib.maintainers; {
|
2024-07-21 05:28:21 +00:00
|
|
|
maintainers = [ tne rob ];
|
2015-07-12 10:09:40 +00:00
|
|
|
};
|
2010-05-20 21:07:32 +00:00
|
|
|
|
2014-09-18 20:34:29 +00:00
|
|
|
nodes =
|
2024-07-21 05:28:21 +00:00
|
|
|
{ client =
|
|
|
|
{ pkgs, nodes, ... }:
|
|
|
|
lib.mkMerge [
|
|
|
|
( makeCommonConfig "client" )
|
|
|
|
{ virtualisation.vlans = [ 1 ];
|
|
|
|
networking.defaultGateway =
|
|
|
|
(pkgs.lib.head nodes.router.networking.interfaces.eth1.ipv4.addresses).address;
|
|
|
|
networking.nftables.enable = nftables;
|
|
|
|
networking.firewall.enable = false;
|
|
|
|
}
|
|
|
|
];
|
2010-05-20 21:07:32 +00:00
|
|
|
|
2024-07-21 05:28:21 +00:00
|
|
|
router =
|
|
|
|
{ nodes, ... }: lib.mkMerge [
|
|
|
|
( makeCommonConfig "router" )
|
|
|
|
{ virtualisation.vlans = [ 1 2 ];
|
|
|
|
networking.firewall = {
|
|
|
|
enable = withFirewall;
|
|
|
|
filterForward = nftables;
|
|
|
|
allowedTCPPorts = [ 21 80 8080 ];
|
|
|
|
# For FTP passive mode
|
|
|
|
allowedTCPPortRanges = [ { from = 51000; to = 51999; } ];
|
|
|
|
};
|
|
|
|
networking.nftables.enable = nftables;
|
|
|
|
networking.nat =
|
|
|
|
let
|
|
|
|
clientIp = (pkgs.lib.head nodes.client.networking.interfaces.eth1.ipv4.addresses).address;
|
|
|
|
serverIp = (pkgs.lib.head nodes.router.networking.interfaces.eth2.ipv4.addresses).address;
|
|
|
|
in
|
|
|
|
{
|
|
|
|
enable = true;
|
|
|
|
internalIPs = [ "${clientIp}/24" ];
|
|
|
|
# internalInterfaces = [ "eth1" ];
|
|
|
|
externalInterface = "eth2";
|
|
|
|
externalIP = serverIp;
|
|
|
|
|
|
|
|
forwardPorts = [
|
|
|
|
{
|
|
|
|
destination = "${clientIp}:8080";
|
|
|
|
proto = "tcp";
|
|
|
|
sourcePort = 8080;
|
|
|
|
|
|
|
|
loopbackIPs = [ serverIp ];
|
|
|
|
}
|
|
|
|
];
|
|
|
|
};
|
|
|
|
|
|
|
|
networking.interfaces.eth2.ipv4.addresses =
|
|
|
|
lib.mkOrder 10000 [ { address = routerAlternativeExternalIp; prefixLength = 24; } ];
|
|
|
|
|
|
|
|
services.nginx.virtualHosts.router.listen = lib.mkOrder (-1) [ {
|
|
|
|
addr = routerAlternativeExternalIp;
|
|
|
|
port = 8080;
|
|
|
|
} ];
|
|
|
|
|
|
|
|
specialisation.no-nat.configuration = {
|
|
|
|
networking.nat.enable = lib.mkForce false;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
];
|
2010-05-20 21:07:32 +00:00
|
|
|
|
2014-09-18 20:34:29 +00:00
|
|
|
server =
|
2024-07-21 05:28:21 +00:00
|
|
|
{ nodes, ... }: lib.mkMerge [
|
|
|
|
( makeCommonConfig "server" )
|
|
|
|
{ virtualisation.vlans = [ 2 ];
|
|
|
|
networking.firewall.enable = false;
|
|
|
|
|
|
|
|
networking.defaultGateway =
|
|
|
|
(pkgs.lib.head nodes.router.networking.interfaces.eth2.ipv4.addresses).address;
|
|
|
|
}
|
|
|
|
];
|
2014-09-18 20:34:29 +00:00
|
|
|
};
|
2010-05-20 21:07:32 +00:00
|
|
|
|
2024-07-21 05:28:21 +00:00
|
|
|
testScript =
|
|
|
|
{ nodes, ... }: let
|
|
|
|
clientIp = (pkgs.lib.head nodes.client.networking.interfaces.eth1.ipv4.addresses).address;
|
|
|
|
serverIp = (pkgs.lib.head nodes.server.networking.interfaces.eth1.ipv4.addresses).address;
|
|
|
|
routerIp = (pkgs.lib.head nodes.router.networking.interfaces.eth2.ipv4.addresses).address;
|
|
|
|
in ''
|
|
|
|
def wait_for_machine(m):
|
|
|
|
m.wait_for_unit("network.target")
|
|
|
|
m.wait_for_unit("nginx.service")
|
|
|
|
|
2019-11-30 23:20:28 +00:00
|
|
|
client.start()
|
|
|
|
router.start()
|
|
|
|
server.start()
|
2010-05-20 21:07:32 +00:00
|
|
|
|
2024-07-21 05:28:21 +00:00
|
|
|
wait_for_machine(router)
|
|
|
|
wait_for_machine(client)
|
|
|
|
wait_for_machine(server)
|
|
|
|
|
|
|
|
# We assume we are isolated from layer 2 attacks or are securely configured (like disabling forwarding by default)
|
|
|
|
# Relevant moby issue describing the problem allowing bypassing of NAT: https://github.com/moby/moby/issues/14041
|
|
|
|
${lib.optionalString (!nftables) ''
|
|
|
|
router.succeed("iptables -P FORWARD DROP")
|
|
|
|
''}
|
|
|
|
|
|
|
|
# Sanity checks.
|
|
|
|
## The router should have direct access to the server
|
|
|
|
router.succeed("check-connection ${serverIp} server expect-success")
|
|
|
|
## The server should have direct access to the router
|
|
|
|
server.succeed("check-connection ${routerIp} router expect-success")
|
2010-05-20 21:07:32 +00:00
|
|
|
|
2024-07-21 05:28:21 +00:00
|
|
|
# The client should be also able to connect via the NAT router...
|
|
|
|
client.succeed("check-connection ${serverIp} server expect-success")
|
|
|
|
# ... but its IP should be rewritten to be that of the router.
|
|
|
|
server.succeed("check-last-clients-ip ${routerIp}")
|
2011-03-10 12:08:39 +00:00
|
|
|
|
2024-07-21 05:28:21 +00:00
|
|
|
# Active FTP (where the FTP server connects back to us via a random port) should work directly...
|
|
|
|
router.succeed("timeout 3 curl -P eth2:51000-51999 ftp://${serverIp}")
|
|
|
|
# ... but not from behind NAT.
|
|
|
|
client.fail("timeout 3 curl -P eth1:51000-51999 ftp://${serverIp};")
|
2011-09-14 18:20:50 +00:00
|
|
|
|
2024-07-21 05:28:21 +00:00
|
|
|
# If using nftables without firewall, filterForward can't be used and L2 security can't easily be simulated like with iptables, skipping.
|
|
|
|
# See moby github issue mentioned above.
|
|
|
|
${lib.optionalString (nftables && withFirewall) ''
|
|
|
|
# The server should not be able to reach the client directly...
|
|
|
|
server.succeed("check-connection ${clientIp} client expect-failure")
|
|
|
|
''}
|
|
|
|
# ... but the server should be able to reach a port forwarded address of the client
|
|
|
|
server.succeed('[[ `timeout 3 curl http://${routerIp}:8080` == "client" ]]')
|
|
|
|
# The IP address the client sees should not be rewritten to be that of the router (#277016)
|
|
|
|
client.succeed("check-last-clients-ip ${serverIp}")
|
|
|
|
|
|
|
|
# But this forwarded port shouldn't intercept communication with
|
|
|
|
# other IPs than externalIp.
|
|
|
|
server.succeed('[[ `timeout 3 curl http://${routerAlternativeExternalIp}:8080` == "router" ]]')
|
2017-01-22 18:42:59 +00:00
|
|
|
|
2024-07-21 05:28:21 +00:00
|
|
|
# The loopback should allow the router itself to access the forwarded port
|
|
|
|
# Note: The reason we use routerIp here is because only routerIp is listed for reflection in networking.nat.forwardPorts.loopbackIPs
|
|
|
|
# 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
|
|
|
|
# to itself/another service on the same NAT through a public address
|
|
|
|
router.succeed('[[ `timeout 3 curl http://${routerIp}:8080` == "client" ]]')
|
|
|
|
# The loopback should also allow the client to access its own forwarded port
|
|
|
|
client.succeed('[[ `timeout 3 curl http://${routerIp}:8080` == "client" ]]')
|
2011-03-10 13:03:47 +00:00
|
|
|
|
2024-07-21 05:28:21 +00:00
|
|
|
# If we turn off NAT, nothing should work
|
2019-11-30 23:20:28 +00:00
|
|
|
router.succeed(
|
2024-07-21 05:28:21 +00:00
|
|
|
"systemctl stop ${unit}.service"
|
2019-11-30 23:20:28 +00:00
|
|
|
)
|
2011-09-14 18:20:50 +00:00
|
|
|
|
2024-07-21 05:28:21 +00:00
|
|
|
# If using nftables and firewall, this makes no sense. We deactivated the firewall after all,
|
|
|
|
# so we are once again affected by the same issue as the moby github issue mentioned above.
|
|
|
|
# If using nftables without firewall, filterForward can't be used and L2 security can't easily be simulated like with iptables, skipping.
|
|
|
|
# See moby github issue mentioned above.
|
|
|
|
${lib.optionalString (!nftables) ''
|
|
|
|
client.succeed("check-connection ${serverIp} server expect-failure")
|
|
|
|
server.succeed("check-connection ${clientIp} client expect-failure")
|
|
|
|
''}
|
|
|
|
# These should revert to their pre-NATed versions
|
|
|
|
server.succeed('[[ `timeout 3 curl http://${routerIp}:8080` == "router" ]]')
|
|
|
|
router.succeed('[[ `timeout 3 curl http://${routerIp}:8080` == "router" ]]')
|
|
|
|
|
|
|
|
# Reverse the effect of nat stop
|
2019-11-30 23:20:28 +00:00
|
|
|
router.succeed(
|
2024-07-21 05:28:21 +00:00
|
|
|
"systemctl start ${unit}.service"
|
|
|
|
)
|
|
|
|
|
|
|
|
# Switch to a config without NAT at all, again nothing should work
|
|
|
|
router.succeed(
|
|
|
|
"/run/booted-system/specialisation/no-nat/bin/switch-to-configuration test 2>&1"
|
2019-11-30 23:20:28 +00:00
|
|
|
)
|
2024-07-21 05:28:21 +00:00
|
|
|
|
|
|
|
# If using nftables without firewall, filterForward can't be used and L2 security can't easily be simulated like with iptables, skipping.
|
|
|
|
# See moby github issue mentioned above.
|
|
|
|
${lib.optionalString (nftables && withFirewall) ''
|
|
|
|
client.succeed("check-connection ${serverIp} server expect-failure")
|
|
|
|
server.succeed("check-connection ${clientIp} client expect-failure")
|
2017-08-04 15:52:42 +00:00
|
|
|
''}
|
2024-07-21 05:28:21 +00:00
|
|
|
|
|
|
|
# These should revert to their pre-NATed versions
|
|
|
|
server.succeed('[[ `timeout 3 curl http://${routerIp}:8080` == "router" ]]')
|
|
|
|
router.succeed('[[ `timeout 3 curl http://${routerIp}:8080` == "router" ]]')
|
2014-09-18 20:34:29 +00:00
|
|
|
'';
|
2024-06-12 11:24:42 +00:00
|
|
|
})
|