{ lib, config, pkgs, ... }: let utils = import ./utils.nix { inherit lib; }; inherit (lib) mkOption mkDefault types assertMsg optional optionalString getExe concatStringsSep filterAttrs listToAttrs attrsToList imap0 attrNames nameValuePair ; inherit (utils) naming; cfg = config.foundation; # this could be moved out into library functions, it's pretty useful. # mapAttrsIndexed' :: (Int -> String -> AttrSet -> { name:: String; value :: Any; }) -> AttrSet -> AttrSet mapAttrsIndexed' = f: attrs: listToAttrs (imap0 (i: v: f i v.name v.value) (attrsToList attrs)); # mapAttrsIndexed :: (Int -> String -> AttrSet -> Any) -> AttrSet -> AttrSet mapAttrsIndexed = f: attrs: mapAttrsIndexed' ( i: n: v: nameValuePair v.name (f i v.name v.value) ) attrs; in { options.foundation = let networkSubmodule = with types; submodule { options = { enable = mkOption { type = types.bool; default = true; description = "Should this network be created?"; }; default = mkOption { type = types.bool; default = false; description = '' Should this network be the default for all services that don't have an explicit network defined? ''; }; # we rely on docker to give us our # ipv4 settings automatically, but # maybe we want another module like # this for ipv4? ipv6 = mkOption { type = types.submodule { options = { enable = mkOption { type = types.bool; default = true; description = "Should this network have IPv6?"; }; subnet = mkOption { type = types.nullOr types.str; description = '' IPv6 subnet for this network in CIDR notation. Don't set to get a random subnet assigned to you within the subnet defined in `defaultIPv6SubnetPrefix`. ''; example = "2001:d0c:123::/64"; default = null; }; gateway = mkOption { type = types.nullOr types.str; description = '' IPv6 gateway for this network. Should match the subnet. ''; example = "2001:d0c:123::1"; default = null; }; }; }; description = "IPv6 settings for network"; default = { }; }; driver = mkOption { type = types.str; default = "bridge"; description = "Docker network driver to use"; }; mtu = mkOption { type = types.nullOr types.int; default = null; example = 1400; description = '' The MTU for this network. If null, we use the Docker default. ''; }; options = mkOption { type = types.listOf types.str; default = [ ]; description = "Additional options to pass to docker network create"; }; serviceGroup = mkOption { type = types.nullOr types.str; default = null; description = '' The group of the network, if it belongs to one. This makes the network service a part of the group target. ''; }; }; }; in { networks = mkOption { type = types.attrsOf networkSubmodule; description = "List of service networks to create"; default = { }; }; defaultNetwork = mkOption { type = types.nullOr types.str; description = '' The default network for services. If at least one network is marked as default, you can find it's name here. ''; }; defaultIPv6SubnetPrefix = mkOption { type = types.str; description = '' Default network subnet assigned to networks without a set subnet. Prefix length defined by `defaultIPv6SubnetLength`. ''; default = "2001:d0c"; }; defaultIPv6SubnetLength = mkOption { type = types.int; description = '' Default network subnet length assigned to networks without a set subnet. This should always be the length of the `defaultIPv6SubnetPrefix` + 16. ''; default = 48; }; }; config = lib.mkIf (cfg.networks != { }) { # other modules can use this to look up which network # they can use as the default! foundation.defaultNetwork = let networksLabeledDefault = attrNames (filterAttrs (name: net: net.default) cfg.networks); defaultsCount = builtins.length networksLabeledDefault; in assert assertMsg (defaultsCount <= 1) "multiple service networks labeled default"; assert assertMsg (defaultsCount != 0) "no default service network"; builtins.head networksLabeledDefault; # we need these settings to allow our networks to be IPv6-enabled. # we also ignore the default bridge, because it's setup # is a lot more complicated... virtualisation.docker.daemon.settings = { experimental = true; ipv6 = true; ip6tables = true; fixed-cidr-v6 = "${cfg.defaultIPv6SubnetPrefix}:255::/${toString cfg.defaultIPv6SubnetLength}"; }; # turn these ipv6 forwarding on, if it isn't already. boot.kernel.sysctl = { "net.ipv6.conf.all.forwarding" = lib.mkDefault 1; "net.ipv6.conf.default.forwarding" = lib.mkDefault 1; }; networking.firewall = { # both options should work together to let all packets coming from # docker bridges through. trustedInterfaces = [ "br-*" ]; extraCommands = '' # allow inbound packets ip6tables -A nixos-fw -i br-+ -j nixos-fw-accept 2>/dev/null || true # allow outbound to the docker bridge ip6tables -A nixos-fw -o br-+ -j nixos-fw-accept 2>/dev/null || true # allow forwarding between bridges+external interfaces ip6tables -I FORWARD -i br-+ -j ACCEPT 2>/dev/null || true ip6tables -I FORWARD -o br-+ -j ACCEPT 2>/dev/null || true # allow return traffic # note: nothing works without this! ip6tables -I FORWARD ! -i br-+ -o br-+ -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true ''; }; systemd.services = let prefixOffset = 100; prefixByIndex = i: "${cfg.defaultIPv6SubnetPrefix}:${toString (prefixOffset + i)}"; subnetByIndex = i: "${prefixByIndex i}::/${toString cfg.defaultIPv6SubnetLength}"; gatewayByIndex = i: "${prefixByIndex i}::1"; networkService = index: name: network: let inherit (network) ipv6 driver mtu options serviceGroup ; docker = getExe pkgs.docker; subnet = if ipv6.subnet == null then subnetByIndex index else ipv6.subnet; gateway = if ipv6.gateway == null then gatewayByIndex index else ipv6.gateway; extraOptions = concatStringsSep " " options; ipv6Options = concatStringsSep " " [ "--ipv6" "--subnet=${subnet}" "--gateway=${gateway}" ]; in { description = "Docker service network '${name}'"; after = [ "docker.service" ]; requires = [ "docker.service" ]; wantedBy = [ "multi-user.target" ]; partOf = optional (network.serviceGroup != null) "${naming.groupTarget serviceGroup}.target"; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; ExecStart = pkgs.writeShellScript "create-docker-network-${name}" '' if ${docker} network inspect ${name} >/dev/null 2>&1; then ${docker} network rm ${name} fi ${docker} network create \ --driver=${driver} \ ${optionalString (ipv6.enable) ipv6Options} \ ${optionalString (mtu != null) "--opt com.docker.network.driver.mtu=${toString network.mtu}"} \ ${extraOptions} \ ${name} ''; ExecStop = pkgs.writeShellScript "delete-docker-network-${name}" '' ${docker} network rm -f ${name} ''; }; }; mkNetwork = index: name: network: { name = naming.networkService name; value = networkService index name network; }; in mapAttrsIndexed' mkNetwork (filterAttrs (name: net: net.enable) cfg.networks); }; }