{ lib, config, flakes, pkgs, ... }: with lib; let cfg = config.local.nspawn.dmz; inherit (config.local) mailHost; hassPort = config.services.home-assistant.config.http.server_port; hassEnable = config.local.home-assistant.enable; conduitPort = config.local.conduit.listenPort; conduitEnable = config.local.conduit.enable; in { options.local.nspawn.dmz = { enable = mkEnableOption "DMZ services in a container"; net = mkOption { type = types.str; }; net6 = mkOption { type = types.str; }; # Solo para IPv4 netBits = mkOption { type = types.enum [ 30 ]; }; dmzAddr = mkOption { type = types.str; readOnly = true; }; hostAddr = mkOption { type = types.str; readOnly = true; }; hostAddr6 = mkOption { type = types.str; readOnly = true; }; system = mkOption { type = types.raw; }; }; # Situación con os-release # # La idea aquí es poder hacer 'btrfs subvol create /var/lib/machines/foo' y # dejar que systemd-nspawn y el activation script creen todo lo demás. Esto # no sirve bien debido a la prueba barata que hace systemd para revisar si el # árbol parece contener una imagen de sistema operativo. Esta prueba falla en # dos momentos distintos: # # 1. Inmediatamente tras crear un árbol vacío, puesto que os-release no existe. # La solución naive es 'mkdir rootfs/etc && touch rootfs/etc/os-release'. # # 2. Luego de reiniciar el contenedor una vez que NixOS ha preparado /etc, ya que # systemd espera un archivo regular y no telera el symlink a la store. # # Resulta ser que systemd revisa tanto /etc/os-release como /usr/lib/os-release. # NixOS evidentemente no usa la segunda ruta por ser FHS, así que la duct tape # final es 'mkdir rootfs/usr/lib && touch rootfs/usr/lib/os-release'. config = mkIf cfg.enable { local = { mailHost.mdaListen = cfg.hostAddr; conduit.listenAddress = mkIf conduitEnable cfg.hostAddr; nspawn.dmz = let incrementIpv4 = bytes: (incrementIpv4' bytes).tail; incrementIpv4' = bytes: let next = incrementIpv4' (tail bytes); byteInc = (head bytes) + next.carry; in if bytes == [ ] then { tail = [ ]; carry = 1; } else if byteInc < 256 then { tail = [ byteInc ] ++ next.tail; carry = 0; } else { tail = [ 0 ] ++ next.tail; carry = 1; }; joinIpv4 = bytes: concatStringsSep "." (map toString bytes); hostBytes = incrementIpv4 (map toInt (splitString "." cfg.net)); in { dmzAddr = joinIpv4 (incrementIpv4 hostBytes); hostAddr = joinIpv4 hostBytes; hostAddr6 = throwIf (! hasSuffix "::" cfg.net6) "Invalid IPv6 /64: ${cfg.net6}" "${cfg.net6}1"; system = let containerModule = { ... }: { #TODO: urgente: bloquear puertos de dovecot a non-postfix con iptables config = { local = { preset.dmz = { enable = true; container = true; }; mta = { mdaAddr = mailHost.mdaListen; inherit (mailHost) saslPort lmtpPort; }; web.sites = { home = { enable = hassEnable; proxyUrl = "http://${cfg.hostAddr}:${toString hassPort}"; }; matrix = { enable = conduitEnable; proxyUrl = "http://${cfg.hostAddr}:${toString conduitPort}"; }; }; }; nixpkgs = { pkgs = mkDefault pkgs; localSystem = mkDefault pkgs.stdenv.hostPlatform; }; }; }; in # Tomado de la definición de pkgs.nixos junto con definición de nixpkgs.{pkgs,localSystem} arriba import "${flakes.nixpkgs}/nixos/lib/eval-config.nix" { modules = [ ../. containerModule ]; system = null; specialArgs = { inherit flakes; }; }; }; }; services = { home-assistant.config.http = mkIf hassEnable { server_host = [ cfg.hostAddr ]; trusted_proxies = [ cfg.dmzAddr ]; use_x_forwarded_for = true; }; }; systemd = { nspawn.dmz = { execConfig.PrivateUsers = "pick"; filesConfig.BindReadOnly = [ # idmap porque algunos hacks en nixpkgs (postfix-setup.service) # asumen que la store es de root "/nix/store:/nix/store:idmap" "${cfg.system.config.system.build.toplevel}/init:/sbin/init" ]; networkConfig.Port = [ "tcp:25" "tcp:80" "tcp:443" "tcp:465" "tcp:587" ]; }; network.networks."40-ve-dmz" = { matchConfig = { Name = "ve-dmz"; Driver = "veth"; }; networkConfig = { Address = [ "${cfg.hostAddr}/${toString cfg.netBits}" "${cfg.hostAddr6}/64" ]; LinkLocalAddressing = "yes"; DHCPServer = "yes"; IPMasquerade = "both"; LLDP = "yes"; EmitLLDP = "customer-bridge"; IPv6SendRA = "yes"; }; # IP de contenedor fijada en hostAddr + 1 dhcpServerConfig = { PoolOffset = 2; PoolSize = 1; }; ipv6Prefixes = [ { ipv6PrefixConfig = { Assign = "yes"; Prefix = "${cfg.net6}/64"; }; } ]; }; services = { dovecot2.after = [ "systemd-nspawn@dmz.service" ]; "systemd-nspawn@dmz" = { overrideStrategy = "asDropin"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; wantedBy = [ "machines.target" ]; }; }; }; networking.firewall = { allowedTCPPorts = [ 25 80 443 ]; interfaces.ve-dmz = { allowedTCPPorts = [ mailHost.saslPort mailHost.lmtpPort ] ++ optional hassEnable hassPort ++ optional conduitEnable conduitPort; allowedUDPPorts = [ 67 ]; # DHCP }; }; }; }