mirror of
https://github.com/NixOS/nixpkgs.git
synced 2024-12-29 09:04:17 +00:00
12f0948900
This code is mostly from #279629, the uninvoled client checks were removed (since they are the same as the direct connection to the client test) and the tests were adjusted to work as intended as well as bugs fixed. In some cases, some tests are skipped when they do not make sense for the specific configuration that is being tested.
279 lines
11 KiB
Nix
279 lines
11 KiB
Nix
# 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, ... }:
|
|
let
|
|
unit = if nftables then "nftables" else (if withFirewall then "firewall" else "nat");
|
|
|
|
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
|
|
in
|
|
{
|
|
name = "nat" + (lib.optionalString nftables "Nftables")
|
|
+ (if withFirewall then "WithFirewall" else "Standalone");
|
|
meta = with pkgs.lib.maintainers; {
|
|
maintainers = [ tne rob ];
|
|
};
|
|
|
|
nodes =
|
|
{ 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;
|
|
}
|
|
];
|
|
|
|
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;
|
|
};
|
|
}
|
|
];
|
|
|
|
server =
|
|
{ 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;
|
|
}
|
|
];
|
|
};
|
|
|
|
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")
|
|
|
|
client.start()
|
|
router.start()
|
|
server.start()
|
|
|
|
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")
|
|
|
|
# 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}")
|
|
|
|
# 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};")
|
|
|
|
# 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" ]]')
|
|
|
|
# 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" ]]')
|
|
|
|
# If we turn off NAT, nothing should work
|
|
router.succeed(
|
|
"systemctl stop ${unit}.service"
|
|
)
|
|
|
|
# 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
|
|
router.succeed(
|
|
"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"
|
|
)
|
|
|
|
# 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")
|
|
''}
|
|
|
|
# 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" ]]')
|
|
'';
|
|
})
|