{ config, pkgs, ... }:
with pkgs.lib;
let
cfg = config.networking;
interfaces = attrValues cfg.interfaces;
hasVirtuals = any (i: i.virtual) interfaces;
interfaceOpts = { name, ... }: {
options = {
name = mkOption {
example = "eth0";
type = types.string;
description = "Name of the interface.";
};
ipAddress = mkOption {
default = null;
example = "10.0.0.1";
type = types.nullOr types.string;
description = ''
IP address of the interface. Leave empty to configure the
interface using DHCP.
'';
};
prefixLength = mkOption {
default = null;
example = 24;
type = types.nullOr types.int;
description = ''
Subnet mask of the interface, specified as the number of
bits in the prefix (24).
'';
};
subnetMask = mkOption {
default = "";
example = "255.255.255.0";
type = types.string;
description = ''
Subnet mask of the interface, specified as a bitmask.
This is deprecated; use
instead.
'';
};
macAddress = mkOption {
default = null;
example = "00:11:22:33:44:55";
type = types.nullOr types.string;
description = ''
MAC address of the interface. Leave empty to use the default.
'';
};
virtual = mkOption {
default = false;
type = types.bool;
description = ''
Whether this interface is virtual and should be created by tunctl.
This is mainly useful for creating bridges between a host a virtual
network such as VPN or a virtual machine.
Defaults to tap device, unless interface contains "tun" in its name.
'';
};
virtualOwner = mkOption {
default = "root";
type = types.uniq types.string;
description = ''
In case of a virtual device, the user who owns it.
'';
};
proxyARP = mkOption {
default = false;
type = types.bool;
description = ''
Turn on proxy_arp for this device (and proxy_ndp for ipv6).
This is mainly useful for creating pseudo-bridges between a real
interface and a virtual network such as VPN or a virtual machine for
interfaces that don't support real bridging (most wlan interfaces).
As ARP proxying acts slightly above the link-layer, below-ip traffic
isn't bridged, so things like DHCP won't work. The advantage above
using NAT lies in the fact that no IP addresses are shared, so all
hosts are reachable/routeable.
WARNING: turns on ip-routing, so if you have multiple interfaces, you
should think of the consequence and setup firewall rules to limit this.
'';
};
};
config = {
name = mkDefault name;
};
};
in
{
###### interface
options = {
networking.hostName = mkOption {
default = "nixos";
description = ''
The name of the machine. Leave it empty if you want to obtain
it from a DHCP server (if using DHCP).
'';
};
networking.enableIPv6 = mkOption {
default = true;
description = ''
Whether to enable support for IPv6.
'';
};
networking.defaultGateway = mkOption {
default = "";
example = "131.211.84.1";
description = ''
The default gateway. It can be left empty if it is auto-detected through DHCP.
'';
};
networking.defaultGatewayWindowSize = mkOption {
default = null;
example = 524288;
type = types.nullOr types.int;
description = ''
The window size of the default gateway. It limits maximal data bursts that TCP peers
are allowed to send to us.
'';
};
networking.nameservers = mkOption {
default = [];
example = ["130.161.158.4" "130.161.33.17"];
description = ''
The list of nameservers. It can be left empty if it is auto-detected through DHCP.
'';
};
networking.domain = mkOption {
default = "";
example = "home";
description = ''
The domain. It can be left empty if it is auto-detected through DHCP.
'';
};
networking.localCommands = mkOption {
default = "";
example = "text=anything; echo You can put $text here.";
description = ''
Shell commands to be executed at the end of the
network-setup systemd service. Note that if
you are using DHCP to obtain the network configuration,
interfaces may not be fully configured yet.
'';
};
networking.interfaces = mkOption {
default = {};
example =
{ eth0 = {
ipAddress = "131.211.84.78";
subnetMask = "255.255.255.128";
};
};
description = ''
The configuration for each network interface. If
is true, then every
interface not listed here will be configured using DHCP.
'';
type = types.loaOf types.optionSet;
options = [ interfaceOpts ];
};
networking.bridges = mkOption {
default = { };
example =
{ br0.interfaces = [ "eth0" "eth1" ];
br1.interfaces = [ "eth2" "wlan0" ];
};
description =
''
This option allows you to define Ethernet bridge devices
that connect physical networks together. The value of this
option is an attribute set. Each attribute specifies a
bridge, with the attribute name specifying the name of the
bridge's network interface.
'';
type = types.attrsOf types.optionSet;
options = {
interfaces = mkOption {
example = [ "eth0" "eth1" ];
type = types.listOf types.string;
description =
"The physical network interfaces connected by the bridge.";
};
};
};
networking.useDHCP = mkOption {
default = true;
merge = mergeEnableOption;
description = ''
Whether to use DHCP to obtain an IP adress and other
configuration for all network interfaces that are not manually
configured.
'';
};
};
###### implementation
config = {
boot.kernelModules = optional cfg.enableIPv6 "ipv6" ++ optional hasVirtuals "tun";
environment.systemPackages =
[ pkgs.host
pkgs.iproute
pkgs.iputils
pkgs.nettools
pkgs.wirelesstools
pkgs.iw
pkgs.rfkill
pkgs.openresolv
]
++ optional (cfg.bridges != {}) pkgs.bridge_utils
++ optional hasVirtuals pkgs.tunctl
++ optional cfg.enableIPv6 pkgs.ndisc6;
security.setuidPrograms = [ "ping" "ping6" ];
systemd.targets."network-interfaces" =
{ description = "All Network Interfaces";
wantedBy = [ "network.target" ];
unitConfig.X-StopOnReconfiguration = true;
};
systemd.services =
let
networkSetup =
{ description = "Networking Setup";
after = [ "network-interfaces.target" ];
before = [ "network.target" ];
wantedBy = [ "network.target" ];
path = [ pkgs.iproute ];
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
script =
''
# Set the static DNS configuration, if given.
${pkgs.openresolv}/sbin/resolvconf -m 1 -a static < /proc/sys/net/ipv6/conf/all/disable_ipv6
fi
# Set the default gateway.
${optionalString (cfg.defaultGateway != "") ''
# FIXME: get rid of "|| true" (necessary to make it idempotent).
ip route add default via "${cfg.defaultGateway}" ${
optionalString (cfg.defaultGatewayWindowSize != null)
"window ${cfg.defaultGatewayWindowSize}"} || true
''}
# Turn on forwarding if any interface has enabled proxy_arp.
${optionalString (any (i: i.proxyARP) interfaces) ''
echo 1 > /proc/sys/net/ipv4/ip_forward
''}
# Run any user-specified commands.
${cfg.localCommands}
'';
};
# For each interface , create a job ‘-cfg.service"
# that performs static configuration. It has a "wants"
# dependency on ‘.service’, which is supposed to create
# the interface and need not exist (i.e. for hardware
# interfaces). It has a binds-to dependency on the actual
# network device, so it only gets started after the interface
# has appeared, and it's stopped when the interface
# disappears.
configureInterface = i: nameValuePair "${i.name}-cfg"
(let mask =
if i.prefixLength != null then toString i.prefixLength else
if i.subnetMask != "" then i.subnetMask else "32";
in
{ description = "Configuration of ${i.name}";
wantedBy = [ "network-interfaces.target" ];
bindsTo = [ "sys-subsystem-net-devices-${i.name}.device" ];
after = [ "sys-subsystem-net-devices-${i.name}.device" ];
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
path = [ pkgs.iproute pkgs.gawk ];
script =
''
echo "bringing up interface..."
ip link set "${i.name}" up
''
+ optionalString (i.macAddress != null)
''
echo "setting MAC address to ${i.macAddress}..."
ip link set "${i.name}" address "${i.macAddress}"
''
+ optionalString (i.ipAddress != null)
''
cur=$(ip -4 -o a show dev "${i.name}" | awk '{print $4}')
# Only do a flush/add if it's necessary. This is
# useful when the Nix store is accessed via this
# interface (e.g. in a QEMU VM test).
if [ "$cur" != "${i.ipAddress}/${mask}" ]; then
echo "configuring interface..."
ip -4 addr flush dev "${i.name}"
ip -4 addr add "${i.ipAddress}/${mask}" dev "${i.name}"
# Ensure that the default gateway remains set.
# (Flushing this interface may have removed it.)
${config.systemd.package}/bin/systemctl try-restart --no-block network-setup.service
else
echo "skipping configuring interface"
fi
${config.systemd.package}/bin/systemctl start ip-up.target
''
+ optionalString i.proxyARP
''
echo 1 > /proc/sys/net/ipv4/conf/${i.name}/proxy_arp
''
+ optionalString (i.proxyARP && cfg.enableIPv6)
''
echo 1 > /proc/sys/net/ipv6/conf/${i.name}/proxy_ndp
'';
});
createTunDevice = i: nameValuePair "${i.name}"
{ description = "Virtual Network Interface ${i.name}";
requires = [ "dev-net-tun.device" ];
after = [ "dev-net-tun.device" ];
wantedBy = [ "network.target" ];
requiredBy = [ "sys-subsystem-net-devices-${i.name}.device" ];
serviceConfig =
{ Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.tunctl}/bin/tunctl -t '${i.name}' -u '${i.virtualOwner}'";
ExecStop = "${pkgs.tunctl}/bin/tunctl -d '${i.name}'";
};
};
createBridgeDevice = n: v:
let
deps = map (i: "sys-subsystem-net-devices-${i}.device") v.interfaces;
in
{ description = "Bridge Interface ${n}";
wantedBy = [ "network.target" "sys-subsystem-net-devices-${n}.device" ];
bindsTo = deps;
after = deps;
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
path = [ pkgs.bridge_utils pkgs.iproute ];
script =
''
brctl addbr "${n}"
# Set bridge's hello time to 0 to avoid startup delays.
brctl setfd "${n}" 0
${flip concatMapStrings v.interfaces (i: ''
brctl addif "${n}" "${i}"
ip link set "${i}" up
ip addr flush dev "${i}"
echo "bringing up network device ${n}..."
ip link set "${n}" up
'')}
# !!! Should delete (brctl delif) any interfaces that
# no longer belong to the bridge.
'';
postStop =
''
ip link set "${n}" down
brctl delbr "${n}"
'';
};
in listToAttrs (
map configureInterface interfaces ++
map createTunDevice (filter (i: i.virtual) interfaces))
// mapAttrs createBridgeDevice cfg.bridges
// { "network-setup" = networkSetup; };
# Set the host and domain names in the activation script. Don't
# clear it if it's not configured in the NixOS configuration,
# since it may have been set by dhclient in the meantime.
system.activationScripts.hostname =
optionalString (config.networking.hostName != "") ''
hostname "${config.networking.hostName}"
'';
system.activationScripts.domain =
optionalString (config.networking.domain != "") ''
domainname "${config.networking.domain}"
'';
services.udev.extraRules =
''
KERNEL=="tun", TAG+="systemd"
'';
};
}