Skip to content

Commit

Permalink
nixos/systemd-networkd: add NFTSet related options
Browse files Browse the repository at this point in the history
Includes NixOS test and validation on required fields.
  • Loading branch information
mvnetbiz committed Aug 7, 2024
1 parent eec16b4 commit c5cd3c2
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 0 deletions.
39 changes: 39 additions & 0 deletions nixos/lib/systemd-lib.nix
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ let
mkAfter
mkIf
optional
optionals
optionalAttrs
optionalString
pipe
Expand Down Expand Up @@ -196,6 +197,44 @@ in rec {
optional (attr ? ${name})
"Systemd ${group} field `${name}' has been removed. See ${see}";

# https://github.com/systemd/systemd/blob/af1a6db58fde8f64edcf7d27e1f3b636c999934c/src/basic/parse-util.c#L794
assertNftSet = name: group: attr:
let
splitSet = (splitString ":");
allSets = if isString attr.${name} then [(splitSet attr.${name})] else map splitSet attr.${name};
getField = xs: i: if length xs >= i + 1 then elemAt xs i else null;
assertNotNull = field: val: optional (val == null || val == "")
"\t`${field}' must be specified.";
assertOneOf = field: values: val:
(assertNotNull field val) ++
(optional (val != null && !elem val values)
"\t`${field}' cannot have value `${val}'.");
assertStrLen = field: min: max: val:
(assertNotNull field val) ++
(optional (val != null && val != "" && (stringLength val < min || stringLength val > max))
"\t`${field}' length must be between ${toString min} and ${toString max}.");
assertNameValid = field: val:
optional (val != null && match "[a-zA-Z][a-zA-Z0-9/\\_.]*" val == null)
"\t`${field}' must begin with an alphabetic character (a-z,A-Z), followed by zero or more alphanumeric characters (a-z,A-Z,0-9) and the characters slash (/), backslash (\\), underscore (_) and dot (.).";
check = def: [
(assertOneOf "source" [ "address" "prefix" "ifindex" ] (getField def 0))
(assertOneOf "family" [ "arp" "bridge" "inet" "ip" "ip6" "netdev" ] (getField def 1))
(assertStrLen "table" 1 31 (getField def 2))
(assertNameValid "table" (getField def 2))
(assertStrLen "set" 1 31 (getField def 3))
(assertNameValid "set" (getField def 3))
];
errors = flatten (map check allSets);
assertions =
if ! (isString attr.${name} || isList attr.${name}) then
[ "Systemd ${group} field `${name}' is not a string or list of strings." ]
else
optional (length errors > 0)
"`${name}' must be in the form source:family:table:set."
++ errors;
in
optionals (attr ? ${name}) assertions;

checkUnitConfig = group: checks: attrs: let
# We're applied at the top-level type (attrsOf unitOption), so the actual
# unit options might contain attributes from mkOverride and mkIf that we need to
Expand Down
10 changes: 10 additions & 0 deletions nixos/modules/system/boot/networkd.nix
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,7 @@ let
"ManageTemporaryAddress"
"AddPrefixRoute"
"AutoJoin"
"NFTSet"
])
(assertHasField "Address")
(assertValueOneOf "PreferredLifetime" ["forever" "infinity" "0" 0])
Expand All @@ -754,6 +755,7 @@ let
(assertValueOneOf "ManageTemporaryAddress" boolValues)
(assertValueOneOf "AddPrefixRoute" boolValues)
(assertValueOneOf "AutoJoin" boolValues)
(assertNftSet "NFTSet")
];

sectionRoutingPolicyRule = checkUnitConfigWithLegacyKey "routingPolicyRuleConfig" "RoutingPolicyRule" [
Expand Down Expand Up @@ -871,6 +873,7 @@ let
"FallbackLeaseLifetimeSec"
"Label"
"Use6RD"
"NFTSet"
])
(assertValueOneOf "UseDNS" boolValues)
(assertValueOneOf "RoutesToDNS" boolValues)
Expand All @@ -896,6 +899,7 @@ let
(assertValueOneOf "SendDecline" boolValues)
(assertValueOneOf "FallbackLeaseLifetimeSec" ["forever" "infinity"])
(assertValueOneOf "Use6RD" boolValues)
(assertNftSet "NFTSet")
];

sectionDHCPv6 = checkUnitConfig "DHCPv6" [
Expand All @@ -920,6 +924,7 @@ let
"IAID"
"UseDelegatedPrefix"
"SendRelease"
"NFTSet"
])
(assertValueOneOf "UseAddress" boolValues)
(assertValueOneOf "UseDNS" boolValues)
Expand All @@ -933,6 +938,7 @@ let
(assertInt "IAID")
(assertValueOneOf "UseDelegatedPrefix" boolValues)
(assertValueOneOf "SendRelease" boolValues)
(assertNftSet "NFTSet")
];

sectionDHCPPrefixDelegation = checkUnitConfig "DHCPPrefixDelegation" [
Expand All @@ -944,11 +950,13 @@ let
"Token"
"ManageTemporaryAddress"
"RouteMetric"
"NFTSet"
])
(assertValueOneOf "Announce" boolValues)
(assertValueOneOf "Assign" boolValues)
(assertValueOneOf "ManageTemporaryAddress" boolValues)
(assertRange "RouteMetric" 0 4294967295)
(assertNftSet "NFTSet")
];

sectionIPv6AcceptRA = checkUnitConfig "IPv6AcceptRA" [
Expand All @@ -971,6 +979,7 @@ let
"UseRoutePrefix"
"Token"
"UsePREF64"
"NFTSet"
])
(assertValueOneOf "UseDNS" boolValues)
(assertValueOneOf "UseDomains" (boolValues ++ ["route"]))
Expand All @@ -982,6 +991,7 @@ let
(assertValueOneOf "UseGateway" boolValues)
(assertValueOneOf "UseRoutePrefix" boolValues)
(assertValueOneOf "UsePREF64" boolValues)
(assertNftSet "NFTSet")
];

sectionDHCPServer = checkUnitConfig "DHCPServer" [
Expand Down
132 changes: 132 additions & 0 deletions nixos/tests/systemd-networkd-nftset.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# This tests systemd-networkd NFTSet option. The interface's statically
# configured address is added to an nft set, and the DHCP configured address is
# added to another. The sets are used by one rule that blocks connections to
# the static address, and one rule that blocks connections to the DHCP address.
# It is tested that the expected connections succeed or fail from another host.
import ./make-test-python.nix (
{ pkgs, ... }:
{
name = "systemd-networkd-nftset";
meta = with pkgs.lib.maintainers; {
maintainers = [ mvnetbiz ];
};
nodes = {
router =
{ ... }:
{
virtualisation.vlans = [ 1 ];
systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";
networking = {
useNetworkd = true;
useDHCP = false;
firewall.enable = false;
};
systemd.network = {
networks = {
# systemd-networkd will load the first network unit file
# that matches, ordered lexiographically by filename.
# /etc/systemd/network/{40-eth1,99-main}.network already
# exists. This network unit must be loaded for the test,
# however, hence why this network is named such.
"01-eth1" = {
name = "eth1";
networkConfig = {
DHCPServer = true;
IPv6AcceptRA = "no";
Address = "10.0.0.1/24";
};
dhcpServerConfig = {
PoolOffset = 100;
PoolSize = 1;
};
};
};
};
};

client =
{ ... }:
{
virtualisation.vlans = [ 1 ];
systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";
networking = {
useNetworkd = true;
useDHCP = false;
firewall.enable = false;
nftables = {
enable = true;
flushRuleset = true;
ruleset = ''
table inet mytable {
set dhcp_set {
type ipv4_addr
}
set static_set {
type ipv4_addr
}
chain input {
type filter hook input priority filter; policy accept;
ip daddr @dhcp_set tcp dport 80 reject with tcp reset
ip daddr @static_set tcp dport 8080 reject with tcp reset
}
}
'';
};
};
systemd.network.networks."01-eth" = {
name = "eth1";
networkConfig = {
DHCP = "ipv4";
IPv6AcceptRA = "no";
};
addresses = [
{
Address = "10.0.0.2/24";
NFTSet = "address:inet:mytable:static_set";
}
];
dhcpV4Config = {
NFTSet = "address:inet:mytable:dhcp_set";
};
};
services.nginx = {
enable = true;
virtualHosts.localhost.listen = [
{
addr = "0.0.0.0";
port = 80;
}
{
addr = "0.0.0.0";
port = 8080;
}
];
};
};
};
testScript =
{ ... }:
''
start_all()
router.systemctl("start network-online.target")
client.systemctl("start network-online.target")
router.wait_for_unit("systemd-networkd-wait-online.service")
client.wait_for_unit("systemd-networkd-wait-online.service")
# should be able to ping both IPs
router.wait_until_succeeds("ping -c 5 10.0.0.2")
router.wait_until_succeeds("ping -c 5 10.0.0.100")
client.wait_for_unit("nginx.service")
client.wait_for_unit("nftables.service")
# should be able to get static IP, but not the DHCP IP on port 80
router.wait_until_succeeds("curl 10.0.0.2")
router.wait_until_fails("curl 10.0.0.100");
# vice versa on port 8080
router.wait_until_succeeds("curl 10.0.0.100:8080")
router.wait_until_fails("curl 10.0.0.2:8080");
'';
}
)

0 comments on commit c5cd3c2

Please sign in to comment.