nixos/networking-interfaces: add IPIP tunnels

This commit is contained in:
rnhmjoj
2025-07-11 09:44:49 +02:00
parent 1c5b9429ea
commit 4e5205a68a
4 changed files with 313 additions and 0 deletions
@@ -107,6 +107,7 @@ let
|| (hasAttr dev cfg.bonds)
|| (hasAttr dev cfg.macvlans)
|| (hasAttr dev cfg.sits)
|| (hasAttr dev cfg.ipips)
|| (hasAttr dev cfg.vlans)
|| (hasAttr dev cfg.greTunnels)
|| (hasAttr dev cfg.vswitches)
@@ -662,6 +663,52 @@ let
}
);
createIpipDevice =
n: v:
nameValuePair "${n}-netdev" (
let
deps = deviceDependency v.dev;
in
{
description = "IP in IP Tunnel Interface ${n}";
wantedBy = [
"network-setup.service"
(subsystemDevice n)
];
bindsTo = deps;
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 dev "${n}" >/dev/null 2>&1 && ip link delete dev "${n}"
ip tunnel add name "${n}" ${
formatIpArgs {
inherit (v)
remote
local
ttl
dev
;
mode =
{
"4in6" = "ipip6";
"ipip" = "ipip";
}
.${v.encapsulation.type};
encaplimit = if v.encapsulation.type == "ipip" then null else v.encapsulation.limit;
}
}
ip link set dev "${n}" up
'';
postStop = ''
ip link delete dev "${n}" || true
'';
}
);
createGreDevice =
n: v:
nameValuePair "${n}-netdev" (
@@ -743,6 +790,7 @@ let
// mapAttrs' createMacvlanDevice cfg.macvlans
// mapAttrs' createFouEncapsulation cfg.fooOverUDP
// mapAttrs' createSitDevice cfg.sits
// mapAttrs' createIpipDevice cfg.ipips
// mapAttrs' createGreDevice cfg.greTunnels
// mapAttrs' createVlanDevice cfg.vlans
// {
@@ -24,6 +24,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 (ipip: ipip.dev) (attrValues cfg.ipips)
++ map (gre: gre.dev) (attrValues cfg.greTunnels)
++ map (vlan: vlan.interface) (attrValues cfg.vlans)
# add dependency to physical or independently created vswitch member interface
@@ -457,6 +458,38 @@ in
}
)
))
(mkMerge (
flip mapAttrsToList cfg.ipips (
name: ipip: {
netdevs."40-${name}" = {
netdevConfig = {
Name = name;
Kind = if ipip.encapsulation.type == "ipip" then "ipip" else "ip6tnl";
};
tunnelConfig =
(optionalAttrs (ipip.remote != null) {
Remote = ipip.remote;
})
// (optionalAttrs (ipip.local != null) {
Local = ipip.local;
})
// (optionalAttrs (ipip.ttl != null) {
TTL = ipip.ttl;
})
// (optionalAttrs (ipip.encapsulation.type != "ipip") {
# IPv6 tunnel options
Mode = if ipip.encapsulation.type == "4in6" then "ipip6" else "ip6ip6";
EncapsulationLimit = ipip.encapsulation.type;
});
};
networks = mkIf (ipip.dev != null) {
"40-${ipip.dev}" = {
tunnel = [ name ];
};
};
}
)
))
(mkMerge (
flip mapAttrsToList cfg.greTunnels (
name: gre: {
@@ -1137,6 +1137,104 @@ in
});
};
networking.ipips = mkOption {
default = { };
example = literalExpression ''
{
wan4in6 = {
remote = "2001:db8::1";
local = "2001:db8::3";
dev = "wan6";
encapsulation.type = "4in6";
encapsulation.limit = 0;
};
}
'';
description = ''
This option allows you to define interfaces encapsulating IP
packets within IP packets; which should be automatically created.
For example, this allows you to create 4in6 (RFC 2473)
or IP within IP (RFC 2003) tunnels.
'';
type =
with types;
attrsOf (submodule {
options = {
remote = mkOption {
type = types.str;
example = "2001:db8::1";
description = ''
The address of the remote endpoint to forward traffic over.
'';
};
local = mkOption {
type = types.str;
example = "2001:db8::3";
description = ''
The address of the local endpoint which the remote
side should send packets to.
'';
};
ttl = mkOption {
type = types.nullOr types.int;
default = null;
example = 255;
description = ''
The time-to-live of the connection to the remote tunnel endpoint.
'';
};
dev = mkOption {
type = types.nullOr types.str;
default = null;
example = "wan6";
description = ''
The underlying network device on which the tunnel resides.
'';
};
encapsulation.type = mkOption {
type = types.enum [
"ipip"
"4in6"
"ip6ip6"
];
default = "ipip";
description = ''
Select the encapsulation type:
- `ipip` to create an IPv4 within IPv4 tunnel (RFC 2003).
- `4in6` to create a 4in6 tunnel (RFC 2473);
- `ip6ip6` to create an IPv6 within IPv6 tunnel (RFC 2473);
::: {.note}
For encapsulating IPv6 within IPv4 packets, see
the ad-hoc {option}`networking.sits` option.
:::
'';
};
encapsulation.limit = mkOption {
type = types.either (types.enum [ "none" ]) types.ints.unsigned;
default = 4;
example = "none";
description = ''
For an IPv6-based tunnel, the maximum number of nested
encapsulation to allow. 0 means no nesting, "none" unlimited.
'';
};
};
});
};
networking.sits = mkOption {
default = { };
example = literalExpression ''
@@ -659,6 +659,140 @@ let
client2.wait_until_succeeds("ping -c 1 fc00::2")
'';
};
ipip-4in6 =
let
node =
{
address4,
remote,
address6,
}:
{
virtualisation.interfaces.enp1s0.vlan = 1;
networking = {
useNetworkd = networkd;
useDHCP = false;
ipips."4in6" = {
inherit remote;
local = address6;
dev = "enp1s0";
encapsulation.type = "4in6";
};
firewall.enable = false;
nftables.enable = true;
firewall.extraInputRules = "meta l4proto ipip accept";
interfaces.enp1s0.ipv6.addresses = lib.mkOverride 0 [
{
address = address6;
prefixLength = 64;
}
];
interfaces."4in6".ipv4.addresses = lib.mkOverride 0 [
{
address = address4;
prefixLength = 24;
}
];
};
};
in
{
name = "ipip-4in6";
nodes.client1 = node {
address6 = "fc00::1";
address4 = "192.168.1.1";
remote = "fc00::2";
};
nodes.client2 = node {
address6 = "fc00::2";
address4 = "192.168.1.2";
remote = "fc00::1";
};
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 ipv6"):
client1.wait_until_succeeds("ping -c 1 192.168.1.1")
client1.wait_until_succeeds("ping -c 1 192.168.1.2")
client2.wait_until_succeeds("ping -c 1 192.168.1.1")
client2.wait_until_succeeds("ping -c 1 192.168.1.2")
'';
};
ipip =
let
node =
{
local,
remote,
address,
}:
{
virtualisation.interfaces.enp1s0.vlan = 1;
networking = {
useNetworkd = networkd;
useDHCP = false;
ipips.ipip = {
inherit local remote;
dev = "enp1s0";
encapsulation.type = "ipip";
};
nftables.enable = true;
firewall.extraInputRules = "meta l4proto 4 accept";
interfaces.enp1s0.ipv4.addresses = lib.mkOverride 0 [
{
address = local;
prefixLength = 24;
}
];
interfaces.ipip.ipv4.addresses = lib.mkOverride 0 [
{
inherit address;
prefixLength = 24;
}
];
};
};
in
{
name = "ipip";
nodes.client1 = node {
local = "192.168.1.1";
remote = "192.168.1.2";
address = "192.168.10.1";
};
nodes.client2 = node {
local = "192.168.1.2";
remote = "192.168.1.1";
address = "192.168.10.2";
};
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 IPIP tunnel"):
client1.wait_until_succeeds("ping -c 1 192.168.10.1")
client1.wait_until_succeeds("ping -c 1 192.168.10.2")
client2.wait_until_succeeds("ping -c 1 192.168.10.1")
client2.wait_until_succeeds("ping -c 1 192.168.10.2")
'';
};
gre =
let
node =