From 5ce70619451a18ba35de9cc9c8ab7af3ee1420a5 Mon Sep 17 00:00:00 2001 From: Matthew Leach Date: Tue, 7 Dec 2021 15:44:00 +0000 Subject: [PATCH] nixos/networking: add options for configuring a GRE tunnel Add `networking.greTunnels` option that allows a GRE tunnel to be configured in NixOS. --- .../tasks/network-interfaces-scripted.nix | 28 ++++++++ .../tasks/network-interfaces-systemd.nix | 20 ++++++ nixos/modules/tasks/network-interfaces.nix | 61 ++++++++++++++++ nixos/tests/networking.nix | 71 +++++++++++++++++++ 4 files changed, 180 insertions(+) diff --git a/nixos/modules/tasks/network-interfaces-scripted.nix b/nixos/modules/tasks/network-interfaces-scripted.nix index e8e2de090b32..19f2be2c4a25 100644 --- a/nixos/modules/tasks/network-interfaces-scripted.nix +++ b/nixos/modules/tasks/network-interfaces-scripted.nix @@ -532,6 +532,33 @@ let ''; }); + createGreDevice = n: v: nameValuePair "${n}-netdev" + (let + deps = deviceDependency v.dev; + in + { description = "GRE Tunnel Interface ${n}"; + wantedBy = [ "network-setup.service" (subsystemDevice n) ]; + bindsTo = deps; + partOf = [ "network-setup.service" ]; + after = [ "network-pre.target" ] ++ deps; + before = [ "network-setup.service" ]; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + path = [ pkgs.iproute2 ]; + script = '' + # Remove Dead Interfaces + ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}" + ip link add name "${n}" type ${v.type} \ + ${optionalString (v.remote != null) "remote \"${v.remote}\""} \ + ${optionalString (v.local != null) "local \"${v.local}\""} \ + ${optionalString (v.dev != null) "dev \"${v.dev}\""} + ip link set "${n}" up + ''; + postStop = '' + ip link delete "${n}" || true + ''; + }); + createVlanDevice = n: v: nameValuePair "${n}-netdev" (let deps = deviceDependency v.interface; @@ -570,6 +597,7 @@ let // mapAttrs' createMacvlanDevice cfg.macvlans // mapAttrs' createFouEncapsulation cfg.fooOverUDP // mapAttrs' createSitDevice cfg.sits + // mapAttrs' createGreDevice cfg.greTunnels // mapAttrs' createVlanDevice cfg.vlans // { network-setup = networkSetup; diff --git a/nixos/modules/tasks/network-interfaces-systemd.nix b/nixos/modules/tasks/network-interfaces-systemd.nix index ccfd7fd4132b..58239ca5452a 100644 --- a/nixos/modules/tasks/network-interfaces-systemd.nix +++ b/nixos/modules/tasks/network-interfaces-systemd.nix @@ -18,6 +18,7 @@ let concatLists (map (bond: bond.interfaces) (attrValues cfg.bonds)) ++ concatLists (map (bridge: bridge.interfaces) (attrValues cfg.bridges)) ++ map (sit: sit.dev) (attrValues cfg.sits) + ++ map (gre: gre.dev) (attrValues cfg.greTunnels) ++ map (vlan: vlan.interface) (attrValues cfg.vlans) # add dependency to physical or independently created vswitch member interface # TODO: warn the user that any address configured on those interfaces will be useless @@ -245,6 +246,25 @@ in } ]); }; }))) + (mkMerge (flip mapAttrsToList cfg.greTunnels (name: gre: { + netdevs."40-${name}" = { + netdevConfig = { + Name = name; + Kind = gre.type; + }; + tunnelConfig = + (optionalAttrs (gre.remote != null) { + Remote = gre.remote; + }) // (optionalAttrs (gre.local != null) { + Local = gre.local; + }); + }; + networks = mkIf (gre.dev != null) { + "40-${gre.dev}" = (mkMerge [ (genericNetwork (mkOverride 999)) { + tunnel = [ name ]; + } ]); + }; + }))) (mkMerge (flip mapAttrsToList cfg.vlans (name: vlan: { netdevs."40-${name}" = { netdevConfig = { diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix index 49901cda848d..62a90c2b4624 100644 --- a/nixos/modules/tasks/network-interfaces.nix +++ b/nixos/modules/tasks/network-interfaces.nix @@ -9,6 +9,7 @@ let interfaces = attrValues cfg.interfaces; hasVirtuals = any (i: i.virtual) interfaces; hasSits = cfg.sits != { }; + hasGres = cfg.greTunnels != { }; hasBonds = cfg.bonds != { }; hasFous = cfg.fooOverUDP != { } || filterAttrs (_: s: s.encapsulation != null) cfg.sits != { }; @@ -996,6 +997,65 @@ in }); }; + networking.greTunnels = mkOption { + default = { }; + example = literalExpression '' + { + greBridge = { + remote = "10.0.0.1"; + local = "10.0.0.22"; + dev = "enp4s0f0"; + type = "tap"; + }; + } + ''; + description = '' + This option allows you to define Generic Routing Encapsulation (GRE) tunnels. + ''; + type = with types; attrsOf (submodule { + options = { + + remote = mkOption { + type = types.nullOr types.str; + default = null; + example = "10.0.0.1"; + description = '' + The address of the remote endpoint to forward traffic over. + ''; + }; + + local = mkOption { + type = types.nullOr types.str; + default = null; + example = "10.0.0.22"; + description = '' + The address of the local endpoint which the remote + side should send packets to. + ''; + }; + + dev = mkOption { + type = types.nullOr types.str; + default = null; + example = "enp4s0f0"; + description = '' + The underlying network device on which the tunnel resides. + ''; + }; + + type = mkOption { + type = with types; enum [ "tun" "tap" ]; + default = "tap"; + example = "tap"; + apply = v: if v == "tun" then "gre" else "gretap"; + description = '' + Whether the tunnel routes layer 2 (tap) or layer 3 (tun) traffic. + ''; + }; + }; + }); + }; + networking.vlans = mkOption { default = { }; example = literalExpression '' @@ -1225,6 +1285,7 @@ in boot.kernelModules = [ ] ++ optional hasVirtuals "tun" ++ optional hasSits "sit" + ++ optional hasGres "gre" ++ optional hasBonds "bonding" ++ optional hasFous "fou"; diff --git a/nixos/tests/networking.nix b/nixos/tests/networking.nix index 647c8942b37d..f46a115a07d4 100644 --- a/nixos/tests/networking.nix +++ b/nixos/tests/networking.nix @@ -489,6 +489,77 @@ let client2.wait_until_succeeds("ping -c 1 fc00::2") ''; }; + gre = let + node = { pkgs, ... }: with pkgs.lib; { + networking = { + useNetworkd = networkd; + useDHCP = false; + }; + }; + in { + name = "GRE"; + nodes.client1 = args@{ pkgs, ... }: + mkMerge [ + (node args) + { + virtualisation.vlans = [ 1 2 ]; + networking = { + greTunnels = { + greTunnel = { + local = "192.168.2.1"; + remote = "192.168.2.2"; + dev = "eth2"; + type = "tap"; + }; + }; + bridges.bridge.interfaces = [ "greTunnel" "eth1" ]; + interfaces.eth1.ipv4.addresses = mkOverride 0 []; + interfaces.bridge.ipv4.addresses = mkOverride 0 [ + { address = "192.168.1.1"; prefixLength = 24; } + ]; + }; + } + ]; + nodes.client2 = args@{ pkgs, ... }: + mkMerge [ + (node args) + { + virtualisation.vlans = [ 2 3 ]; + networking = { + greTunnels = { + greTunnel = { + local = "192.168.2.2"; + remote = "192.168.2.1"; + dev = "eth1"; + type = "tap"; + }; + }; + bridges.bridge.interfaces = [ "greTunnel" "eth2" ]; + interfaces.eth2.ipv4.addresses = mkOverride 0 []; + interfaces.bridge.ipv4.addresses = mkOverride 0 [ + { address = "192.168.1.2"; prefixLength = 24; } + ]; + }; + } + ]; + testScript = { ... }: + '' + start_all() + + with subtest("Wait for networking to be configured"): + client1.wait_for_unit("network.target") + client2.wait_for_unit("network.target") + + # Print diagnostic information + client1.succeed("ip addr >&2") + client2.succeed("ip addr >&2") + + with subtest("Test GRE tunnel bridge over VLAN"): + client1.wait_until_succeeds("ping -c 1 192.168.1.2") + + client2.wait_until_succeeds("ping -c 1 192.168.1.1") + ''; + }; vlan = let node = address: { pkgs, ... }: with pkgs.lib; { #virtualisation.vlans = [ 1 ];