Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixos/systemd-networkd: add NFTSet related options #332777

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Address documentation: https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#NFTSet=source:family:table:set

Do you think you need a validator? One seems possible given the documentation above.

])
(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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

])
(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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

])
(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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

])
(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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

])
(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 (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manual specifies a different way of defining tests, based on runTest instead of importing this.
See https://nixos.org/manual/nixos/stable/#sec-call-nixos-test-in-nixos

{ 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");
'';
}
)