Skip to content

NAT-Traversal Testing with testnet.polykey.io #159

@CMCDragonkai

Description

@CMCDragonkai

Specification

To automatically test for NAT-busting is a bit complex, you need to simulate the existence of multiple machines, and then simulate full-cone nat, restricted cone nat and then symmetric nat.

Since we don't have a relay proxy enabled yet, symmetric NATs is going to be left out for now. So we'll focus on all the NAT architectures except for symmetric NAT.

Additional context

Actually I don't think we should bother with QEMU or NixOS here. It's too complicated. QEMU might be good choice for being able to run the test cross-platform, but lacking expertise on QEMU here (I've already worked on it with respect to netboot work), and more experience with network namespaces should mean we can do this tests on just Linux. NixOS limits our environment even more and requires running in a NixOS environment.

Note that network namespaces with Linux stateful firewalls should be perfectly capable of simulating a port-restricted firewall.

Old context follows...

The best way to do this is with VM system using QEMU.

NixOS has a multi-machine testing system that can be used to do this, however such tests can only run on NixOS: https://nixos.org/manual/nixos/unstable/index.html#sec-nixos-tests We have pre-existing code for this:

NixOS NAT Module Test
# here is the testing base file: https://github.com/NixOS/nixpkgs/blob/master/nixos/lib/testing-python.nix
with import ../../pkgs.nix {};
let
  pk = (callPackage ../../nix/default.nix {}).package;
in
  import <nixpkgs/nixos/tests/make-test-python.nix> {
    nodes =
      {
        privateNode1 =
          { nodes, pkgs, ... }:
          {
            virtualisation.vlans = [ 1 ];
            environment.variables = {
              PK_PATH = "$HOME/polykey";
            };
            environment.systemPackages = [ pk pkgs.tcpdump ];
            networking.firewall.enable = false;
            networking.defaultGateway = (pkgs.lib.head nodes.router1.config.networking.interfaces.eth1.ipv4.addresses).address;
          };
        privateNode2 =
          { nodes, pkgs, ... }:
          {
            virtualisation.vlans = [ 2 ];
            environment.variables = {
              PK_PATH = "$HOME/polykey";
            };
            environment.systemPackages = [ pk pkgs.tcpdump ];
            networking.firewall.enable = false;
            networking.defaultGateway = (pkgs.lib.head nodes.router2.config.networking.interfaces.eth1.ipv4.addresses).address;
          };
        router1 =
          { pkgs, ... }:
          {
            virtualisation.vlans = [ 1 3 ];
            environment.systemPackages = [ pkgs.tcpdump ];
            networking.firewall.enable = false;
            networking.nat.externalInterface = "eth2";
            networking.nat.internalIPs = [ "192.168.1.0/24" ];
            networking.nat.enable = true;
          };
        router2 =
          { ... }:
          {
            virtualisation.vlans = [ 2 3 ];
            environment.systemPackages = [ pkgs.tcpdump ];
            networking.firewall.enable = false;
            networking.nat.externalInterface = "eth2";
            networking.nat.internalIPs = [ "192.168.2.0/24" ];
            networking.nat.enable = true;
          };
        publicNode =
          { config, pkgs, ... }:
          {
            virtualisation.vlans = [ 3 ];
            environment.variables = {
              PK_PATH = "$HOME/polykey";
            };
            environment.systemPackages = [ pk pkgs.tcpdump ];
            networking.firewall.enable = false;
          };
      };
    testScript =''
      start_all()
      # can start polykey-agent in both public and private nodes
      publicNode.succeed("pk agent start")
      privateNode1.succeed("pk agent start")
      privateNode2.succeed("pk agent start")
      # can create a new keynode in both public and private nodes
      create_node_command = "pk agent create -n {name} -e {name}@email.com -p passphrase"
      publicNode.succeed(create_node_command.format(name="publicNode"))
      privateNode1.succeed(create_node_command.format(name="privateNode1"))
      privateNode2.succeed(create_node_command.format(name="privateNode2"))
      # can add privateNode node info to publicNode
      publicNodeNodeInfo = publicNode.succeed("pk nodes get -c -b")
      privateNode1.succeed("pk nodes add -b '{}'".format(publicNodeNodeInfo))
      privateNode2.succeed("pk nodes add -b '{}'".format(publicNodeNodeInfo))
      # can add publicNode node info to privateNodes
      privateNode1NodeInfo = privateNode1.succeed("pk nodes get -c -b")
      privateNode2NodeInfo = privateNode2.succeed("pk nodes get -c -b")
      publicNode.succeed("pk nodes add -b '{}'".format(privateNode1NodeInfo))
      publicNode.succeed("pk nodes add -b '{}'".format(privateNode2NodeInfo))
      # copy public keys over to node machines
      publicNodePublicKey = publicNode.succeed("cat $HOME/.polykey/.keys/public_key")
      privateNode1PublicKey = privateNode1.succeed("cat $HOME/.polykey/.keys/public_key")
      privateNode2PublicKey = privateNode2.succeed("cat $HOME/.polykey/.keys/public_key")
      privateNode1.succeed("echo '{}' > $HOME/publicNode.pub".format(publicNodePublicKey))
      privateNode1.succeed("echo '{}' > $HOME/privateNode2.pub".format(privateNode2PublicKey))
      privateNode2.succeed("echo '{}' > $HOME/publicNode.pub".format(publicNodePublicKey))
      privateNode2.succeed("echo '{}' > $HOME/privateNode1.pub".format(privateNode1PublicKey))
      publicNode.succeed("echo '{}' > $HOME/privateNode1.pub".format(privateNode1PublicKey))
      publicNode.succeed("echo '{}' > $HOME/privateNode2.pub".format(privateNode2PublicKey))
      # modify node info to match node machines' host address
      publicNode.succeed("pk nodes update -p $HOME/privateNode1.pub -ch privateNode1")
      publicNode.succeed("pk nodes update -p $HOME/privateNode2.pub -ch privateNode2")
      privateNode1.succeed(
          "pk nodes update -p $HOME/publicNode.pub -ch publicNode -r $HOME/publicNode.pub"
      )
      privateNode2.succeed(
          "pk nodes update -p $HOME/publicNode.pub -ch publicNode -r $HOME/publicNode.pub"
      )
      # privateNodes can ping publicNode
      privateNode1.succeed("pk nodes ping -p $HOME/publicNode.pub")
      privateNode2.succeed("pk nodes ping -p $HOME/publicNode.pub")
      # can create a new vault in publicNode and clone it from both privateNodes
      publicNode.succeed("pk vaults new publicVault")
      publicNode.succeed("echo 'secret content' > $HOME/secret")
      publicNode.succeed("pk secrets new publicVault:Secret -f $HOME/secret")
      privateNode1.succeed("pk vaults clone -n publicVault -p $HOME/publicNode.pub")
      privateNode2.succeed("pk vaults clone -n publicVault -p $HOME/publicNode.pub")
      # can create a new vault in privateNode1
      privateNode1.succeed("pk vaults new privateVault1")
      # can create a new secret in privateNode1
      privateNode1.succeed("echo 'secret content' > $HOME/secret")
      privateNode1.succeed("pk secrets new privateVault1:Secret -f $HOME/secret")
      # setup a relay between privateNode1 and publicNode
      privateNode1.succeed("pk nodes relay -p $HOME/publicNode.pub")
      # add privateNode1 node info to privateNode2
      privateNode1NodeInfo = privateNode1.succeed("pk nodes get -c -b")
      privateNode2.succeed("pk nodes add -b '{}'".format(privateNode1NodeInfo))
      # add privateNode2 node info to privateNode1
      privateNode2NodeInfo = privateNode2.succeed("pk nodes get -c -b")
      privateNode1.succeed("pk nodes add -b '{}'".format(privateNode2NodeInfo))
      # can ping privateNode1 to privateNode2
      privateNode2.succeed("pk nodes ping -p ~/privateNode1.pub")
      # can pull a vault from privateNode1 to privateNode2
      privateNode2.succeed("pk vaults clone -p ~/privateNode1.pub -n privateVault1")
    '';
  }

Tasks

  1. - Create test harness/fixture utilities that create a multi-node situation
  2. - Simulate a NAT table situation by making use of network namespaces
  3. - This test can only run on Linux that supports virtual network namespaces.
    4. [ ] - The test will have to be run separately from npm test which runs jest. This test can be done inside Gitlab CI/CD if the CI/CD on Linux supports creating network namespaces. If not, it's a manual test. Using conditional testing instead Conditional testing for platform-specific tests #380
  4. - Review my gist https://gist.github.com/CMCDragonkai/3f3649d7f1be9c7df36f which explains how to use network namespaces. The Linux iptables firewall has to be used that simulates a NAT that allows outgoing packets but denies incoming packets except for the connections that are already live. This is called a "stateful firewall". I've done this before, but I forgot the details.
  5. - You'll need to use https://stackabuse.com/executing-shell-commands-with-node-js/ to run the ip netns commands. Remember to check whether the OS is linux before allowing one to run these tests.
  6. [ ] - Add in testing involving testnet.polykey.io which should run only during integration testing after the integration:deployment job (because it has to deploy to the testnet in that job). - Reissued Integration Tests for testnet.polykey.com Polykey-CLI#71

Metadata

Metadata

Assignees

Labels

developmentStandard developmentepicBig issue with multiple subissuesr&d:polykey:core activity 4End to End Networking behind Consumer NAT Devices

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions