IfState
IfState is a python 3 utility designed for declarative management of Linux network interfaces. It acts as a frontend to the kernel's Netlink interface, using the pyroute2
library to configure network settings such as IP addresses, bridges, traffic control, and WireGuard in an idempotent manner—much like an iproute2
/ethtool
/tc
/wg
wrapper.
It will be probably be available with NixOS 25.11 (see https://github.com/NixOS/nixpkgs/pull/431047).
Examples
You can find several examples on the IfState website. Due to the fact that these are yaml examples, I decided to provide some nix examples here too:
{
networking.ifstate = {
enable = true;
settings = {
interfaces.enp3s0 = {
addresses = [
"192.0.2.10/24"
"2001:db8:dead:c0de::10/64"
];
link = {
state = "up";
kind = "physical";
};
identify.perm_address = "00:00:5e:00:53:00";
};
routing.routes = [
{
to = "0.0.0.0/0";
via = "192.0.2.1";
}
{
to = "::/0";
via = "fe80::1";
}
];
};
};
}
Network Namespaces (netns)
Network namespaces are a powerful feature in Linux that allow you to create isolated network environments. Each namespace has its own network interfaces, IP addresses, routing tables, and firewall rules. This isolation is particularly useful for scenarios like running systemd services or NixOS containers in separate network environments, enabling better control and security.
to isolate services / nixos-containers
You can bind specific systemd services to a network namespace, ensuring they operate in a controlled network environment without affecting the host or other services.
{
systemd.services."<name>".serviceConfig.NetworkNamespacePath = "/var/run/netns/<netnsName>";
}
When using nixos-containers, network namespaces allow you to configure the network outside the container. This separation simplifies management and ensures the container’s network setup is independent of its internal configuration.
{
containers."<name>".networkNamespace = "/var/run/netns/<netnsName>";
}
Network namespaces can be connected to other namespaces or the Global Routing Table (GRT) using virtual Ethernet interfaces (veth). This enables traffic forwarding and communication between isolated environments
The following example creates a pair of veth interfaces. One interface is placed inside the example
network namespace and assigned a single IPv4 address. Its peer remains in the global routing table. The GRT is configured to reach the namespace’s IPv4 address via IPv6 link-local routing. To make this possible, both veth interfaces are assigned specific MAC addresses chosen so that the automatically generated IPv6 link-local (EUI-64) addresses are predictable.
let
address = "192.0.2.0/32";
netns = "example";
in
{
networking.ifstate.settings = {
namespaces."${netns}" = {
interfaces.veth0 = {
addresses = [ address ];
link = {
state = "up";
kind = "veth";
peer = "veth1";
peer_netns = null; # grt
address = "12:de:ad:be:ef:00";
};
};
};
interfaces.veth1 = {
link = {
state = "up";
kind = "veth";
peer = "veth0";
peer_netns = netns;
address = "12:de:ad:be:ef:10";
};
};
routing.routes = [
{
to = address;
# generated eui64 local link address for mac address of container
via = "fe80::10de:adff:febe:ef00";
dev = "veth1";
}
];
};
}
to separate provider network from grt with igp
Another practical application could involve setting up a VPN gateway on a virtual server hosted by a provider. Imagine you’re using a WireGuard tunnel (wg0
) to connect to a network that provides internet access, alongside a client peer WireGuard endpoint (wg1
) that allows your personal devices to connect.
In this setup, you’d need to configure a default route through wg0
to forward internet traffic via the first WireGuard tunnel. At the same time, you’d also require a default route to your provider’s network to ensure your server remains accessible from the internet—essential for your devices to connect to wg1
and for wg0
to maintain its peer connection.
To achieve this, you might want to isolate the provider network from your Global Routing Table (GRT) and bind the WireGuard endpoints. The IfState
tool offers a link configuration option called bind_netns
, which can be used with tunnel links (such as WireGuard, GRE, SIT, etc.) to implement this separation.
Important Note: If enp0s3
is your provider interface, this configuration will move it into an external network namespace that contains nothing except the bound WireGuard endpoint. As a result, you won’t be able to access systemd services like your SSH server without an active WireGuard connection. Plan accordingly to avoid losing access to critical services.
{
networking.ifstate = {
enable = true;
settings = {
namespaces.outside = {
interfaces.enp0s3 = {
# public ip addresses from your upstream provider
addresses = [
"10.20.30.10/26"
"2001:db8::10/64"
];
link = {
state = "up";
kind = "physical";
};
identify.perm_address = "00:00:5e:00:53:00";
};
routing.routes = [
{
to = "0.0.0.0/0";
via = "10.20.30.62";
}
{
to = "::/0";
via = "fe80::1";
}
];
};
};
interfaces = {
wg0 = {
link = {
state = "up";
kind = "wireguard";
# the connection to the peer will be established via the netns
bind_netns = "outside";
};
# the tunnel addresses for your upstream wireguard
addresses = [
"172.16.0.100/32"
"2001:db8:bad:c0de::100/128"
];
wireguard = {
listen_port = 40608;
private_key = "!include /keys/wg0.priv";
peers."!include /keys/wg0-peer.pub" = {
allowedips = [
"0.0.0.0/0"
"::/0"
];
endpoint = "10.10.10.10:51820";
};
};
};
wg1 = {
link = {
state = "up";
kind = "wireguard";
# the wireguard listener will be bound in the netns
bind_netns = "outside";
};
# the tunnel addresses for your upstream wireguard
addresses = [
"172.16.0.100/32"
"2001:db8:bad:c0de::100/128"
];
wireguard = {
listen_port = 20406;
private_key = "!include /keys/wg1.priv";
peers."!include /keys/wg1-notebook.pub" = {
allowedips = [
"172.16.1.2/32"
"2001:db8:dead:beef::2/128"
];
};
};
};
};
};
}