diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index dfc1d694e976..27fbe55cd429 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -388,6 +388,7 @@ ./services/networking/prayer.nix ./services/networking/privoxy.nix ./services/networking/prosody.nix + ./services/networking/quagga.nix ./services/networking/quassel.nix ./services/networking/racoon.nix ./services/networking/radicale.nix diff --git a/nixos/modules/services/networking/quagga.nix b/nixos/modules/services/networking/quagga.nix new file mode 100644 index 000000000000..ac83da920638 --- /dev/null +++ b/nixos/modules/services/networking/quagga.nix @@ -0,0 +1,187 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.quagga; + + services = [ "babel" "bgp" "isis" "ospf6" "ospf" "pim" "rip" "ripng" ]; + allServices = services ++ [ "zebra" ]; + + isEnabled = service: cfg.${service}.enable; + + daemonName = service: if service == "zebra" then service else "${service}d"; + + configFile = service: + let + scfg = cfg.${service}; + in + if scfg.configFile != null then scfg.configFile + else pkgs.writeText "${daemonName service}.conf" + '' + ! Quagga ${daemonName service} configuration + ! + hostname ${config.networking.hostName} + log syslog + service password-encryption + ! + ${scfg.config} + ! + end + ''; + + serviceOptions = service: + { + enable = mkEnableOption "the Quagga ${toUpper service} routing protocol"; + + configFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/etc/quagga/${daemonName service}.conf"; + description = '' + Configuration file to use for Quagga ${daemonName service}. + By default the NixOS generated files are used. + ''; + }; + + config = mkOption { + type = types.lines; + default = ""; + example = + let + examples = { + rip = '' + router rip + network 10.0.0.0/8 + ''; + + ospf = '' + router ospf + network 10.0.0.0/8 area 0 + ''; + + bgp = '' + router bgp 65001 + neighbor 10.0.0.1 remote-as 65001 + ''; + }; + in + examples.${service} or ""; + description = '' + ${daemonName service} configuration statements. + ''; + }; + + vtyListenAddress = mkOption { + type = types.str; + default = "127.0.0.1"; + description = '' + Address to bind to for the VTY interface. + ''; + }; + + vtyListenPort = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + TCP Port to bind to for the VTY interface. + ''; + }; + }; + +in + +{ + + ###### interface + + options.services.quagga = + { + + zebra = (serviceOptions "zebra") // { + + enable = mkOption { + type = types.bool; + default = any isEnabled services; + example = true; + description = '' + Whether to enable the Zebra routing manager. + + The Zebra routing manager is automatically enabled + if any routing protocols are configured. + ''; + }; + + }; + + } // (genAttrs services serviceOptions); + + ###### implementation + + config = mkIf (any isEnabled allServices) { + + environment.systemPackages = [ + pkgs.quagga # for the vtysh tool + ]; + + users.users.quagga = { + description = "Quagga daemon user"; + isSystemUser = true; + group = "quagga"; + }; + + users.groups = { + quagga = {}; + # Members of the quaggavty group can use vtysh to inspect the Quagga daemons + quaggavty = {}; + }; + + systemd.services = + let + quaggaService = service: + let + scfg = cfg.${service}; + daemon = daemonName service; + in + nameValuePair daemon ({ + wantedBy = [ "multi-user.target" ]; + restartTriggers = [ (configFile service) ]; + + serviceConfig = { + Type = "forking"; + PIDFile = "/run/quagga/${daemon}.pid"; + ExecStart = "@${pkgs.quagga}/libexec/quagga/${daemon} ${daemon} -d -f ${configFile service}" + + optionalString (scfg.vtyListenAddress != "") " -A ${scfg.vtyListenAddress}" + + optionalString (scfg.vtyListenPort != null) " -P ${toString scfg.vtyListenPort}"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + Restart = "on-abort"; + }; + } // ( + if service == "zebra" then + { + description = "Quagga Zebra routing manager"; + unitConfig.Documentation = "man:zebra(8)"; + after = [ "network.target" ]; + preStart = '' + install -m 0755 -o quagga -g quagga -d /run/quagga + + ${pkgs.iproute}/bin/ip route flush proto zebra + ''; + } + else + { + description = "Quagga ${toUpper service} routing daemon"; + unitConfig.Documentation = "man:${daemon}(8) man:zebra(8)"; + bindsTo = [ "zebra.service" ]; + after = [ "network.target" "zebra.service" ]; + } + )); + in + listToAttrs (map quaggaService (filter isEnabled allServices)); + + }; + + meta.maintainers = with lib.maintainers; [ tavyc ]; + +} diff --git a/nixos/release.nix b/nixos/release.nix index 70a7ba5af89d..4dd1311e2d0c 100644 --- a/nixos/release.nix +++ b/nixos/release.nix @@ -279,6 +279,7 @@ in rec { tests.printing = callTest tests/printing.nix {}; tests.proxy = callTest tests/proxy.nix {}; tests.pumpio = callTest tests/pump.io.nix {}; + tests.quagga = callTest tests/quagga.nix {}; tests.quake3 = callTest tests/quake3.nix {}; tests.runInMachine = callTest tests/run-in-machine.nix {}; tests.sddm = callTest tests/sddm.nix {}; diff --git a/nixos/tests/quagga.nix b/nixos/tests/quagga.nix new file mode 100644 index 000000000000..b9644b4768c0 --- /dev/null +++ b/nixos/tests/quagga.nix @@ -0,0 +1,97 @@ +# This test runs Quagga and checks if OSPF routing works. +# +# Network topology: +# [ client ]--net1--[ router1 ]--net2--[ router2 ]--net3--[ server ] +# +# All interfaces are in OSPF Area 0. + +import ./make-test.nix ({ pkgs, ... }: + let + + ifAddr = node: iface: (pkgs.lib.head node.config.networking.interfaces.${iface}.ip4).address; + + ospfConf = '' + interface eth2 + ip ospf hello-interval 1 + ip ospf dead-interval 5 + ! + router ospf + network 192.168.0.0/16 area 0 + ''; + + in + { + name = "quagga"; + + meta = with pkgs.stdenv.lib.maintainers; { + maintainers = [ tavyc ]; + }; + + nodes = { + + client = + { config, pkgs, nodes, ... }: + { + virtualisation.vlans = [ 1 ]; + networking.defaultGateway = ifAddr nodes.router1 "eth1"; + }; + + router1 = + { config, pkgs, nodes, ... }: + { + virtualisation.vlans = [ 1 2 ]; + boot.kernel.sysctl."net.ipv4.ip_forward" = "1"; + networking.firewall.extraCommands = "iptables -A nixos-fw -i eth2 -p ospf -j ACCEPT"; + services.quagga.ospf = { + enable = true; + config = ospfConf; + }; + }; + + router2 = + { config, pkgs, nodes, ... }: + { + virtualisation.vlans = [ 3 2 ]; + boot.kernel.sysctl."net.ipv4.ip_forward" = "1"; + networking.firewall.extraCommands = "iptables -A nixos-fw -i eth2 -p ospf -j ACCEPT"; + services.quagga.ospf = { + enable = true; + config = ospfConf; + }; + }; + + server = + { config, pkgs, nodes, ... }: + { + virtualisation.vlans = [ 3 ]; + networking.defaultGateway = ifAddr nodes.router2 "eth1"; + networking.firewall.allowedTCPPorts = [ 80 ]; + networking.firewall.allowPing = true; + services.httpd.enable = true; + services.httpd.adminAddr = "foo@example.com"; + }; + }; + + testScript = + { nodes, ... }: + '' + startAll; + + # Wait for the networking to start on all machines + $_->waitForUnit("network.target") foreach values %vms; + + # Wait for OSPF to form adjacencies + for my $gw ($router1, $router2) { + $gw->waitForUnit("ospfd"); + $gw->waitUntilSucceeds("vtysh -c 'show ip ospf neighbor' | grep Full"); + $gw->waitUntilSucceeds("vtysh -c 'show ip route' | grep '^O>'"); + } + + # Test ICMP. + $client->succeed("ping -c 3 server >&2"); + + # Test whether HTTP works. + $server->waitForUnit("httpd"); + $client->succeed("curl --fail http://server/ >&2"); + ''; + })