{ config , pkgs , lib , ... }: let fsTypes = [ "f2fs" "ext" "exfat" "vfat" ]; domain = "home.arpa"; ts_domain = "hydra-pinecone.ts.net"; in { imports = [ ./nanopi-hardware.nix ]; age.secrets = { dyndns.file = ../secrets/dyndns.age; acme.file = ../secrets/acme.age; syncthing.file = ../secrets/syncthing.age; }; boot = { supportedFilesystems = fsTypes; initrd.supportedFilesystems = fsTypes; loader.timeout = 1; kernelPatches = lib.mkForce [ ]; }; systemd.services."irqbalance-oneshot" = { enable = true; description = "Distribute interrupts after boot using \"irqbalance --oneshot\""; documentation = [ "man:irqbalance" ]; wantedBy = [ "sysinit.target" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; ExecStart = "${pkgs.irqbalance.out}/bin/irqbalance --foreground --oneshot"; }; }; systemd.tmpfiles.settings."leds-off" = { "/sys/class/leds/green:*/brightness" = { w = { argument = "0"; }; }; }; services.udev.extraRules = '' # set scheduler for NVMe ACTION=="add|change", KERNEL=="nvme[0-9]n[0-9]", ATTR{queue/scheduler}="kyber" # set scheduler for SSD and eMMC ACTION=="add|change", KERNEL=="sd[a-z]|mmcblk[0-9]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline" # set scheduler for rotating disks ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="kyber" ''; systemd.services.dynamic-dns-update = { enable = true; startAt = [ "hourly" ]; description = "Update IP addresses"; path = with pkgs; [ curl iproute2 dig.dnsutils miller ]; after = [ "sys-devices-platform-fe2a0000.ethernet-net-wan0.device" ]; bindsTo = [ "sys-devices-platform-fe2a0000.ethernet-net-wan0.device" ]; serviceConfig = { Type = "oneshot"; ExecStart = "/bin/sh /etc/nixos/update-ip ${config.age.secrets.dyndns.path}"; }; }; systemd.services.backup-golink = { enable = true; startAt = "daily"; description = "Export short links from golink"; path = with pkgs; [ curl gitMinimal ]; script = '' [ -d golink ] || git init --quiet golink --initial-branch=main --shared=world git config --global user.email linde@alanpearce.eu cd golink curl https://go.${ts_domain}/.export > links.json git add links.json git commit -m $(date +%F) ''; serviceConfig = { Type = "oneshot"; User = "linde"; WorkingDirectory = config.users.users.linde.home; }; }; services.journald.extraConfig = '' MaxRetentionSec=1 month ''; environment.enableAllTerminfo = true; environment.systemPackages = with pkgs; [ htop lsof usbutils lzop zstd sqlite ]; systemd.network.config.networkConfig = { SpeedMeter = true; }; networking = { hostName = "nanopi"; domain = domain; search = [ domain ]; hosts = { "fd7a:115c:a1e0::53" = [ "tailscale" "ts" ]; }; useDHCP = false; useNetworkd = true; firewall = { enable = true; rejectPackets = true; logRefusedConnections = false; pingLimit = "5/second"; filterForward = true; # we are a router trustedInterfaces = [ "bridge0" "tailscale0" ]; interfaces.wan0 = { allowedTCPPorts = [ 6980 # aria2c ]; allowedUDPPorts = [ 6976 6980 41641 ]; }; extraForwardRules = '' iifname { "wan0", "wlan0", "wwan0" } oifname { "lan1", "lan2", "bridge0" } icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-request, mld-listener-query, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept iifname { "lan1", "lan2", "bridge0" } oifname { "wan0", "wlan0", "wwan0" } accept iifname "tailscale0" oifname "bridge0" accept iifname "bridge0" oifname "tailscale0" accept ''; }; nftables = { enable = true; tables = { firewall = { family = "inet"; content = '' chain postrouting { type nat hook postrouting priority srcnat; policy accept; oifname { "wan0", "wlan0", "wwan0" } masquerade } chain prerouting { type nat hook prerouting priority dstnat; iifname "wan0" tcp dport { 6922, 51413 } dnat ip to 10.0.0.42 } ''; }; }; }; wireless = { enable = true; # iwd = { # enable = true; # settings = { # Network = { # RoutePriorityOffset = 300; # }; # }; # }; }; }; networking = { resolvconf = { # having this enabled (the default) is pointless # a) this device has fixed upstream nameservers enable = false; # b) it makes tailscale think it should change the search domains for MagicDNS # ... due to this: # useLocalResolver = false; # which is set by kresd?! # https://github.com/NixOS/nixpkgs/blob/7780e5160e011b39019797a4c4b1a4babc80d1bf/nixos/modules/services/networking/kresd.nix#L113 }; nameservers = lib.optionals config.services.dnsmasq.enable [ "::1" "127.0.0.1" ]; }; services.resolved = { # this allows link-specific DNS configuration, which is useful. enable = true; # why use simple boolean when string do trick? llmnr = "false"; dnssec = "true"; fallbackDns = [ "9.9.9.9" "149.112.112.112" "2620::fe:fe" "2620::fe:9" "116.203.248.56" "2a01:4f8:c012:23a4::1" ]; }; # leaving this here just in case I ever think about disabling both `resolvconf` and `resolved` # I thought that there would have been a fallback that does this anyway, but apparently not. environment.etc."resolv.conf".text = lib.mkDefault (lib.optionalString ( !config.networking.resolvconf.enable && !config.services.resolved.enable ) '' search ${domain} ${ts_domain} nameserver ::1 nameserver 127.0.0.1 options edns0 ''); services.tailscale = { enable = true; extraUpFlags = [ "--accept-dns=false" "--advertise-routes=10.0.0.0/20,fd12:d04f:65d:42::/56" ]; }; programs.command-not-found.enable = false; services.openssh = { enable = true; openFirewall = true; startWhenNeeded = false; settings = { PasswordAuthentication = false; KbdInteractiveAuthentication = false; }; }; programs.mosh.enable = true; services.sshguard = { enable = true; services = [ "sshd" ]; }; systemd.network = { enable = true; wait-online = { extraArgs = [ "--interface" "bridge0" ]; }; links = { "10-name-lan1" = { matchConfig.Path = "platform-3c0000000.pcie-pci-0000:01:00.0"; linkConfig = { Name = "lan1"; MACAddress = "a8:95:85:0d:67:38"; }; }; "10-name-lan2" = { matchConfig.Path = "platform-3c0400000.pcie-pci-0001:01:00.0"; linkConfig = { Name = "lan2"; MACAddress = "a8:95:85:0d:67:39"; }; }; "10-name-wan0" = { matchConfig.Path = "platform-fe2a0000.ethernet"; linkConfig = { Name = "wan0"; MACAddress = "a8:95:85:0d:67:3a"; }; }; "10-name-wlan0" = { matchConfig.MACAddress = "9c:53:22:33:bf:e9"; linkConfig = { Name = "wlan0"; }; }; "10-name-wwan0" = { matchConfig.MACAddress = "34:4b:50:00:00:00"; linkConfig = { Name = "wwan0"; }; }; }; netdevs = { "20-bridge" = { netdevConfig = { Kind = "bridge"; Name = "bridge0"; }; }; }; networks = { "30-lan-ports" = { matchConfig.Name = "lan*"; bridge = [ "bridge0" ]; linkConfig = { MACAddress = "82:E0:06:9C:8E:7C"; }; networkConfig.LinkLocalAddressing = "no"; }; "40-bridge0" = { matchConfig.Name = "bridge0"; linkConfig.RequiredForOnline = "routable"; address = [ "10.0.0.1/20" "fd12:d04f:65d:42::1/56" ]; networkConfig = { IPv6AcceptRA = false; IPv6SendRA = true; DHCPPrefixDelegation = true; ConfigureWithoutCarrier = true; MulticastDNS = true; BindCarrier = [ "lan0" "lan1" ]; Domains = [ domain ]; }; dhcpPrefixDelegationConfig = { UplinkInterface = "wan0"; SubnetId = "42"; Assign = true; Token = "::1"; }; ipv6SendRAConfig = { RouterLifetimeSec = 1800; EmitDNS = true; DNS = "fd12:d04f:65d:42::1"; EmitDomains = true; Domains = [ config.networking.domain ]; }; }; "50-wwan0" = { matchConfig.Name = "wwan0"; networkConfig = { DHCP = "yes"; IPv6AcceptRA = true; IPForward = "yes"; }; dhcpV4Config = { UseDNS = false; SendHostname = false; UseRoutes = false; }; ipv6AcceptRAConfig.UseDNS = false; routes = [ { routeConfig = { Gateway = "_dhcp4"; Metric = 2048; QuickAck = true; InitialCongestionWindow = 30; InitialAdvertisedReceiveWindow = 30; }; } ]; cakeConfig = { Bandwidth = "1M"; OverheadBytes = 18; MPUBytes = 64; CompensationMode = "none"; NAT = true; PriorityQueueingPreset = "diffserv8"; }; }; "50-wan" = { matchConfig.Name = "wan0"; networkConfig = { DHCP = "yes"; IPv6AcceptRA = true; IPForward = "yes"; }; dhcpV4Config = { UseDNS = false; UseRoutes = false; SendHostname = false; SendRelease = false; UseHostname = false; # Label = "wan0:1"; }; dhcpV6Config = { UseDNS = false; RapidCommit = true; PrefixDelegationHint = "::/56"; }; dhcpPrefixDelegationConfig = { UplinkInterface = ":self"; }; ipv6AcceptRAConfig = { UseDNS = false; UseGateway = false; }; addresses = [ { addressConfig = { Address = "192.168.100.10/24"; # Peer = "192.168.100.1/32"; Label = "wan0:0"; # Scope = "link"; }; } ]; routes = [ { routeConfig = { Gateway = "_dhcp4"; QuickAck = true; InitialCongestionWindow = 30; InitialAdvertisedReceiveWindow = 30; }; } { routeConfig = { Gateway = "_ipv6ra"; QuickAck = true; InitialCongestionWindow = 30; InitialAdvertisedReceiveWindow = 30; }; } ]; cakeConfig = { Bandwidth = "24M"; OverheadBytes = 18; MPUBytes = 64; CompensationMode = "none"; NAT = true; PriorityQueueingPreset = "diffserv8"; }; }; "60-wlan" = { matchConfig.MACAddress = "9c:53:22:33:bf:e9"; networkConfig = { DHCP = "yes"; IPForward = "yes"; IgnoreCarrierLoss = "3s"; }; dhcpV4Config = { UseDNS = false; SendHostname = false; SendRelease = true; UseHostname = false; RouteMetric = 2048; }; routes = [ { routeConfig = { Metric = 2048; Gateway = "_dhcp4"; QuickAck = true; InitialCongestionWindow = 30; InitialAdvertisedReceiveWindow = 30; }; } ]; cakeConfig = { Bandwidth = "1M"; OverheadBytes = 18; MPUBytes = 64; CompensationMode = "none"; NAT = true; PriorityQueueingPreset = "diffserv8"; }; }; }; }; boot.kernelModules = [ "tcp_lp" ]; boot.kernel.sysctl = { "net.ipv4.conf.bridge0.send_redirects" = 1; "net.ipv4.conf.bridge0.accept_source_route" = 1; "net.ipv4.tcp_slow_start_after_idle" = 0; "net.ipv4.tcp_ecn" = 1; "net.ipv4.tcp_fastopen" = "0x3"; "net.ipv4.tcp_allowed_congestion_control" = "reno cubic lp"; "net.core.default_qdisc" = "fq"; }; services.dnsmasq = { enable = true; # let systemd-resolved.do this resolveLocalQueries = false; alwaysKeepRunning = true; settings = { local-ttl = 60; domain = domain; dhcp-fqdn = true; domain-needed = true; bogus-priv = true; no-resolv = true; no-negcache = true; strict-order = true; log-queries = false; server = [ "9.9.9.9" "149.112.112.112" "2620::fe:fe" "2620::fe:9" "116.203.248.56" "2a01:4f8:c012:23a4::1" # kresd "127.0.0.1#5553" "::1#5553" # smartdns # "127.0.0.1#5533" # "::1#5533" "/ts.net/tailscale" ]; localise-queries = true; cname = [ "ha,home-assistant" ]; interface-name = [ "nanopi.${domain},bridge0" "wan.${domain},wan0" "wlan.${domain},wlan0" "wwan.${domain},wwan0" ]; interface = [ "lo" "bridge0" ]; no-dhcp-interface = [ "tailscale0" ]; # auth-zone = "lan,wan0"; # auth-server = [ # "nanopi.alanpearce.eu,wan0" # ]; bind-interfaces = true; # if this is false, a remote query for nanopi returns 127.0.0.2, because that's in /etc/hosts no-hosts = true; dnssec = true; trust-anchor = ".,20326,8,2,E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D"; enable-ra = true; dhcp-lease-max = 240; dhcp-authoritative = true; dhcp-rapid-commit = true; dhcp-range = [ "10.0.1.0,10.0.1.250,12h" "::,constructor:bridge0,ra-stateless,ra-names,48h" ]; dhcp-host = [ "00:a0:de:b3:0c:01,10.0.0.50,wxa-50" "10:f0:68:12:b1:e0,10.0.0.11,Ruckus" "9c:93:4e:ad:05:c8,10.0.0.210,xerox-b210" "00:08:9b:f5:b8:25,10.0.0.42,dontpanic" "d8:3a:dd:34:85:cc,d8:3a:dd:34:85:cd,10.0.0.81,home-assistant" ]; dhcp-option = [ "option:ntp-server,0.0.0.0" "option:tftp-server,0.0.0.0" "option:ip-forward-enable,0" # ip-forwarding "252,\"\\n\"" ]; dhcp-name-match = "set:wpad-ignore,wpad"; dhcp-ignore-names = "tag:wpad-ignore"; tftp-root = "/srv/tftp/"; dhcp-boot = [ "tag:bios,netboot.xyz.kpxe" "tag:efi32,netboot.xyz.efi" "tag:efi32-1,netboot.xyz.efi" "tag:efi64,netboot.xyz.efi" "tag:efi64-1,netboot.xyz.efi" "tag:efi64-2,netboot.xyz.efi" ]; dhcp-match = [ "set:bios,60,PXEClient:Arch:00000" "set:efi32,60,PXEClient:Arch:00002" "set:efi32-1,60,PXEClient:Arch:00006" "set:efi64,60,PXEClient:Arch:00007" "set:efi64-1,60,PXEClient:Arch:00008" "set:efi64-2,60,PXEClient:Arch:00009" ]; }; }; systemd.services.dnsmasq.wants = [ "network-online.target" ]; services.networkd-dispatcher = { # broken? enable = true; rules = { update-home-address = { onState = [ "configured" "configuring" ]; script = '' #!${pkgs.runtimeShell} set -eu if [[ $IFACE == "wan0" && $OperationalState == "routable" ]] then systemctl start dynamic-dns-update.service fi exit 0 ''; }; tailscale-subnet-router-optimisation = { onState = [ "routable" ]; script = '' #!${pkgs.runtimeShell} set -eu if [[ $IFACE == "wan0" && $OperationalState == "routable" ]] then ${pkgs.ethtool}/bin/ethtool -K $IFACE rx-udp-gro-forwarding on rx-gro-list off fi ''; }; }; }; system.stateVersion = "23.05"; programs.fish = { enable = true; }; programs.neovim = { enable = true; defaultEditor = true; vimAlias = true; viAlias = true; }; users.users.root.shell = "${pkgs.fish}/bin/fish"; users.users.alan = { description = "Alan Pearce"; isNormalUser = true; extraGroups = [ "wheel" "lp" "scanner" "dialout" ]; shell = "${pkgs.fish}/bin/fish"; home = "/home/alan"; uid = 1000; openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMvcW4Z9VxOQgEJjsRC1uSMwEJ4vru9BwjT+Z50nawp4 lan" ]; }; home-manager.users.alan = import ../user/nanopi.nix; users.groups = { linde.members = [ ]; }; users.users = { linde = { group = "linde"; description = "Backup user for system 'linde'"; isSystemUser = true; shell = "/bin/sh"; home = "/srv/backup/linde"; homeMode = "755"; createHome = true; packages = with pkgs; [ rdiff-backup ]; openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ74cPdIX9OlDkzHb6Y1E5sWqtIqMaf0z/SN3Tfy1Fjl root@linde" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINNXwIdGcP1vKyjmgeLw/sJntn7lajaZivepgdzaXvOt rdiff-backup" ]; }; }; nix = { distributedBuilds = true; buildMachines = [ { protocol = "ssh-ng"; sshUser = "nixremote"; hostName = "linde.alanpearce.eu"; system = "aarch64-linux"; sshKey = "/root/.ssh/id_buche.alanpearce.eu_nixremote"; maxJobs = 2; speedFactor = 4; supportedFeatures = [ ]; } ]; settings = { builders-use-substitutes = true; max-jobs = 2; auto-optimise-store = true; experimental-features = [ "nix-command" "flakes" ]; substituters = [ "https://binarycache.alanpearce.eu" ]; trusted-public-keys = [ "mba-1:CxokFjx7YAQWPWMJJKcP50ZpcPUCAFEOrtWdNUMTVjw=" "binarycache.alanpearce.eu:ZwqO3XMuajPictjwih8OY2+RXnOKpjZEZFHJjGSxAI4=" ]; }; daemonCPUSchedPolicy = "batch"; daemonIOSchedPriority = 6; gc = { automatic = true; dates = "weekly"; options = "--delete-older-than 30d"; }; optimise = { automatic = true; dates = [ "04:00" ]; }; }; nixpkgs.config.allowUnfree = true; system.autoUpgrade = { enable = true; dates = "04:15"; randomizedDelaySec = "59 min"; flake = "git+https://git.alanpearce.eu/nixfiles"; allowReboot = true; rebootWindow = { lower = "01:00"; upper = "06:00"; }; flags = [ "--no-write-lock-file" "--update-input" "nixpkgs-small" ]; }; services.miniupnpd = { enable = false; natpmp = true; internalIPs = [ "bridge0" ]; externalInterface = "wan0"; }; users.groups.videos = { members = [ "alan" "jellyfin" ]; }; services.jellyfin = { enable = false; openFirewall = true; }; users.users.syncthing = { isSystemUser = true; group = "syncthing"; homeMode = "0755"; }; users.groups.syncthing.members = [ "alan" ]; services.syncthing = { enable = true; openDefaultPorts = true; dataDir = "/srv/syncthing"; user = "syncthing"; group = "syncthing"; key = config.age.secrets.syncthing.path; cert = toString (pkgs.writeText "syncthing.crt" '' -----BEGIN CERTIFICATE----- MIIBmjCCASCgAwIBAgIIUOEmXGFrrX0wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ c3luY3RoaW5nMB4XDTIyMDcxMzEwMzIxOVoXDTQ5MTIzMTIzNTk1OVowFDESMBAG A1UEAxMJc3luY3RoaW5nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEPiJT41NqucQf UXiBwt+yPYnMg9G8oTt9XNA72V99K46D7mIs1F/5oESlDiCSAngXPsajxRY7wyZV VoiWegfiaBOGZmq+TyaLlQ5bq/hm/Mp/jVED/rUA+BggohoZZMa2oz8wPTAOBgNV HQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1Ud EwEB/wQCMAAwCgYIKoZIzj0EAwIDaAAwZQIwLp4Gv5EEmjRO9EphbYJ4jxEJks7E oblgnTmhfWmVWmf9avJyeGB212VYu4X8cCKDAjEAn7tTB9Y6LZvYPaLSwUKY3EzF hKTYCb7VA/P1dU3tTR1vSQxnu1DsiliD/XcKe2IK -----END CERTIFICATE----- ''); overrideFolders = false; overrideDevices = false; settings = { options = { maxRecvKbps = 10240; maxSendKbps = 1024; globalAnnounceEnabled = false; relaysEnabled = false; natEnabled = false; urAccepted = 4; trafficClass = 1; }; }; }; time.timeZone = "Europe/Berlin"; services.chrony = { enable = true; extraConfig = '' rtcdevice /dev/rtc0 rtcfile /var/lib/chrony/rtc rtcautotrim 30 allow 10.0.0.0/8 allow fd12:d04f:65d:42::0/56 ''; }; services.samba = { enable = true; enableNmbd = false; extraConfig = '' log level = 1 interfaces = bridge0 min protocol = SMB2 disable netbios = yes smb ports = 445 socket options = IPTOS_LOWDELAY TCP_NODELAY SO_KEEPALIVE SO_RCVBUF=65536 SO_SNDBUF=65536 max xmit = 131072 min receivefile size = 131072 aio read size = 1 aio write size = 1 load printers = no disable spoolss = yes mdns name = mdns follow symlinks = yes veto files = /Thumbs.db/.DS_Store/._.DS_Store/.apdisk/ delete veto files = yes ''; shares = { public = { path = "/srv/public"; browseable = "yes"; "guest ok" = "yes"; "create mask" = "0666"; "directory mask" = "0777"; "read only" = "no"; }; Homes = { "read only" = "no"; "valid users" = "%S"; "inherit acls" = "yes"; }; Videos = { path = "/srv/videos"; "valid users" = "alan"; "create mask" = "0664"; "directory mask" = "0775"; "writeable" = "yes"; }; }; }; services.samba-wsdd = { enable = true; interface = "bridge0"; }; services.smartdns = { enable = false; bindPort = "5533"; settings = { bind = "[::]:5533"; address = [ "/use-application-dns.net/#" ]; server = [ "[::1]:5553" "10.0.0.1:53 -group lan -exclude-default-group" ]; nameserver = [ "/${domain}/${domain}" ]; dualstack-ip-selection = true; dualstack-ip-selection-threshold = 10; dualstack-ip-allow-force-AAAA = false; dnsmasq-lease-file = "/var/lib/dnsmasq/dnsmasq.leases"; mdns-lookup = true; }; }; services.kresd = { enable = true; instances = 4; listenPlain = [ "[::1]:5553" ]; # listenTLS = [ "853" ]; # listenDoH = [ "[::1]:5443" ]; extraConfig = '' -- Load useful modules modules = { 'serve_stale < cache', 'workarounds < iterate', 'hints > iterate', 'nsid', } local systemd_instance = os.getenv("SYSTEMD_INSTANCE") nsid.name(systemd_instance) -- Cache size cache.size = 500 * MB local internalDomains = policy.todnames({'lan.alanpearce.eu.', '10.in-addr.arpa.', '.172.in-addr.arpa.', '.168.192.in-addr.arpa.'}) policy.add(policy.suffix(policy.FLAGS({'NO_CACHE'}), internalDomains)) policy.add(policy.suffix(policy.STUB({'10.0.0.1'}), internalDomains)) -- disable duplicate DNSSEC validation when using Quad9 or private trust_anchors.remove('.') -- policy.add(policy.all(policy.TLS_FORWARD({ -- { "23.88.111.219", hostname="dns.alanpearce.eu" }, -- { "2a01:4f8:c0c:d9ce::1", hostname="dns.alanpearce.eu" }, -- }))) policy.add(policy.all(policy.TLS_FORWARD({ {'9.9.9.11', hostname='dns11.quad9.net'}, {'149.112.122.11', hostname='dns11.quad9.net'}, {'2620:fe::11', hostname='dns11.quad9.net'}, {'2620:fe::fe:11', hostname='dns11.quad9.net'} }))) policy.add(policy.domains(policy.REFUSE, policy.todnames({ 'use-application-dns.net', 'telemetry.astro.build', }))) -- policy.add(policy.rpz( -- policy.DENY_MSG('domain blocked by hblock'), -- '/etc/knot-resolver/blocklist.rpz', -- true -- )) ''; }; }