ab2308e765
IPv6 temporary (privacy) addresses rotate an interface's stable source address out from under long-lived connected sockets. nginx's resolver on middleman binds a UDP socket to the preferred temporary address at worker start; once that address expires and is removed, the socket can no longer send, so every upstream lookup fails with "could not be resolved (timed out)" until nginx is restarted (which then binds the next temporary address, so it recurs). These are servers with no need for privacy addresses, so disable them in networkdAssignment for every RA-accepting interface. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
291 lines
9.9 KiB
Nix
291 lines
9.9 KiB
Nix
{ inputs, lib }:
|
|
let
|
|
inherit (builtins) length match elemAt filter replaceStrings substring;
|
|
inherit (lib)
|
|
genAttrs mapAttrsToList filterAttrsRecursive nameValuePair types
|
|
mkOption mkOverride mkForce mkIf mergeEqualOption optional
|
|
showWarnings concatStringsSep flatten unique optionalAttrs
|
|
mkBefore toLower splitString last;
|
|
inherit (lib.flake) defaultSystems;
|
|
in
|
|
rec {
|
|
pow =
|
|
let
|
|
pow' = base: exponent: value:
|
|
# FIXME: It will silently overflow on values > 2**62 :(
|
|
# The value will become negative or zero in this case
|
|
if exponent == 0
|
|
then 1
|
|
else if exponent <= 1
|
|
then value
|
|
else (pow' base (exponent - 1) (value * base));
|
|
in base: exponent: pow' base exponent base;
|
|
|
|
attrsToNVList = mapAttrsToList nameValuePair;
|
|
|
|
inherit ((import "${inputs.libnetRepo}/lib/netu.nix" { inherit lib; }).lib) net;
|
|
dns = import ./dns.nix { inherit lib; };
|
|
c = import ./constants.nix { inherit lib; };
|
|
|
|
# Yoinked from nixpkgs/nixos/modules/services/networking/nat.nix
|
|
isIPv6 = ip: length (lib.splitString ":" ip) > 2;
|
|
parseIPPort = ipp:
|
|
let
|
|
v6 = isIPv6 ipp;
|
|
matchIP = if v6 then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)";
|
|
m = match "${matchIP}:(.+)" ipp;
|
|
checked = v: if m == null then throw "bad ip:ports `${ipp}'" else v;
|
|
in
|
|
{
|
|
inherit v6;
|
|
ip = checked (elemAt m 0);
|
|
ports = checked (elemAt m 1);
|
|
};
|
|
|
|
netBroadcast = net': net.cidr.host ((pow 2 (net.cidr.size net')) - 1) net';
|
|
|
|
mkDefaultSystemsPkgs = path: args': genAttrs defaultSystems (system: import path ((args' system) // { inherit system; }));
|
|
mkApp = program: { type = "app"; inherit program; };
|
|
mkShellApp = pkgs: name: text: mkApp (pkgs.writeShellScript name text).outPath;
|
|
mkShellApp' = pkgs: args:
|
|
let
|
|
app = pkgs.writeShellApplication args;
|
|
in mkApp "${app}/bin/${app.meta.mainProgram}";
|
|
flakePackageOverlay' = flake: pkg: system: (final: prev:
|
|
let
|
|
pkg' = if pkg != null then flake.packages.${system}.${pkg} else flake.packages.${system}.default;
|
|
name = if pkg != null then pkg else pkg'.name;
|
|
in
|
|
{
|
|
${name} = pkg';
|
|
});
|
|
flakePackageOverlay = flake: flakePackageOverlay' flake null;
|
|
|
|
inlineModule' = path: module: {
|
|
_file = path;
|
|
imports = [ module ];
|
|
};
|
|
inlineModule = path: inlineModule' path (import path);
|
|
|
|
# Merge together modules which are defined as functions with others that aren't
|
|
naiveModule = with types; (coercedTo (attrsOf anything) (conf: { ... }: conf) (functionTo (attrsOf anything)));
|
|
|
|
mkOpt = type: default: mkOption { inherit type default; };
|
|
mkOpt' = type: default: description: mkOption { inherit type default description; };
|
|
mkBoolOpt = default: mkOption {
|
|
inherit default;
|
|
type = types.bool;
|
|
example = true;
|
|
};
|
|
mkBoolOpt' = default: description: mkOption {
|
|
inherit default description;
|
|
type = types.bool;
|
|
example = true;
|
|
};
|
|
nullOrOpt' = type: description: mkOpt' (types.nullOr type) null description;
|
|
dummyOption = mkOption { };
|
|
|
|
# Slightly higher precedence than mkDefault
|
|
mkDefault' = mkOverride 900;
|
|
mkVMOverride' = mkOverride 9;
|
|
|
|
# This is shocking...
|
|
duplicates = l:
|
|
flatten
|
|
(map
|
|
(e:
|
|
optional
|
|
((length (filter (e2: e2 == e) l)) > 1)
|
|
e)
|
|
(unique l));
|
|
|
|
applyAssertions = config: res:
|
|
let
|
|
failedAssertions = map (x: x.message) (filter (x: !x.assertion) config.assertions);
|
|
in
|
|
if failedAssertions != []
|
|
then throw "\nFailed assertions:\n${concatStringsSep "\n" (map (x: "- ${x}") failedAssertions)}"
|
|
else showWarnings config.warnings res;
|
|
|
|
homeStateVersion' = hmBranch: (if (hmBranch == "stable" || hmBranch == "mine-stable") then "22.11" else "23.05");
|
|
homeStateVersion = hmBranch: {
|
|
# The flake passes a default setting, but we don't care about that
|
|
home.stateVersion = mkForce (homeStateVersion' hmBranch);
|
|
};
|
|
|
|
commonOpts = with types; {
|
|
moduleType = mkOptionType {
|
|
name = "Inline flake-exportable module";
|
|
merge = loc: defs: inlineModule (mergeEqualOption loc defs);
|
|
};
|
|
|
|
system = mkOpt' (enum defaultSystems) null "Nix-style system string.";
|
|
nixpkgs = mkOpt' (enum [ "unstable" "stable" "mine" "mine-stable" ]) "unstable" "Branch of nixpkgs to use.";
|
|
home-manager = mkOpt' (enum [ "unstable" "stable" "mine" "mine-stable" ]) "unstable" "Branch of home-manager to use.";
|
|
};
|
|
|
|
nft = rec {
|
|
ipEscape = replaceStrings ["." ":"] ["-" "-"];
|
|
natFilterChain = ip: "filter-fwd-${ipEscape ip}";
|
|
dnatChain = ip: "fwd-${ipEscape ip}";
|
|
};
|
|
|
|
mkVLAN = name: vid: {
|
|
"25-${name}" = {
|
|
netdevConfig = {
|
|
Name = name;
|
|
Kind = "vlan";
|
|
};
|
|
vlanConfig.Id = vid;
|
|
};
|
|
};
|
|
networkdAssignment = iface: a: {
|
|
matchConfig.Name = iface;
|
|
address =
|
|
[ "${a.ipv4.address}/${toString a.ipv4.mask}" ] ++
|
|
(optional (a.ipv6.address != null && a.ipv6.iid == null) "${a.ipv6.address}/${toString a.ipv6.mask}");
|
|
gateway =
|
|
(optional (a.ipv4.gateway != null) a.ipv4.gateway) ++
|
|
(optional (a.ipv6.gateway != null) a.ipv6.gateway);
|
|
networkConfig = {
|
|
IPv6AcceptRA = a.ipv6.gateway == null || a.ipv6.iid != null;
|
|
# These are servers: temporary (privacy) addresses only rotate our stable source
|
|
# address out from under long-lived connected sockets (e.g. nginx's resolver, which
|
|
# wedges permanently when the address it bound to expires).
|
|
IPv6PrivacyExtensions = "no";
|
|
# NOTE: LLDP emission / reception is ignored on bridge interfaces
|
|
LLDP = true;
|
|
EmitLLDP = "customer-bridge";
|
|
};
|
|
linkConfig = optionalAttrs (a.mtu != null) {
|
|
MTUBytes = toString a.mtu;
|
|
};
|
|
ipv6AcceptRAConfig = {
|
|
Token = mkIf (a.ipv6.iid != null) "static:${a.ipv6.iid}";
|
|
UseDNS = true;
|
|
UseDomains = true;
|
|
};
|
|
};
|
|
dockerNetAssignment =
|
|
assignments: name: with assignments."${name}".internal; "ip=${ipv4.address},ip=${ipv6.address}";
|
|
|
|
systemdAwaitPostgres = pkg: host: {
|
|
after = [ "systemd-networkd-wait-online.service" ];
|
|
preStart = mkBefore ''
|
|
until ${pkg}/bin/pg_isready -h ${host}; do
|
|
sleep 0.5
|
|
done
|
|
'';
|
|
};
|
|
|
|
vm = rec {
|
|
lvmDisk'' = name: vg: lv: {
|
|
inherit name;
|
|
backend = {
|
|
driver = "host_device";
|
|
filename = "/dev/${vg}/${lv}";
|
|
# It appears this needs to be set on the backend _and_ the format
|
|
discard = "unmap";
|
|
};
|
|
format = {
|
|
driver = "raw";
|
|
discard = "unmap";
|
|
};
|
|
frontend = "virtio-blk";
|
|
};
|
|
lvmDisk' = vg: lv: lvmDisk'' lv vg lv;
|
|
lvmDisk = lvmDisk' "main";
|
|
disk = vm: lv: lvmDisk'' lv "main" "vm-${vm}-${lv}";
|
|
};
|
|
|
|
deploy-rs =
|
|
with types;
|
|
let
|
|
globalOpts = {
|
|
sshUser = nullOrOpt' str "Username deploy-rs will deploy with.";
|
|
user = nullOrOpt' str "Username deploy-rs will deploy with.";
|
|
sudo = nullOrOpt' str "Command to elevate privileges with (used if the deployment user != profile user).";
|
|
sshOpts = mkOpt' (listOf str) [ ]
|
|
"Options deploy-rs will pass to ssh. Note: overriding at a lower level _merges_ options.";
|
|
fastConnection = nullOrOpt' bool "Whether to copy the whole closure instead of using substitution.";
|
|
autoRollback = nullOrOpt' bool "Whether to roll back the profile if activation fails.";
|
|
magicRollback = nullOrOpt' bool "Whether to roll back the profile if connectivity to the deployer is lost.";
|
|
confirmTimeout = nullOrOpt' ints.u16 "Timeout for confirming activation succeeded.";
|
|
tempPath = nullOrOpt' str "Path that deploy-rs will use for temporary files.";
|
|
};
|
|
|
|
profileOpts = {
|
|
path = mkOpt' package "" "Derivation to build (should include activation script).";
|
|
profilePath = nullOrOpt' str "Path to profile location";
|
|
} // globalOpts;
|
|
profileType = submodule { options = profileOpts; };
|
|
|
|
nodeOpts = {
|
|
hostname = mkOpt' str "" "Hostname deploy-rs will connect to.";
|
|
profilesOrder = nullOrOpt' (listOf str)
|
|
"Order to deploy profiles in (remainder will be deployed in arbitrary order).";
|
|
profiles = mkOpt' (attrsOf profileType) { } "Profiles to deploy.";
|
|
} // globalOpts;
|
|
nodeType = submodule { options = nodeOpts; };
|
|
|
|
deployOpts = {
|
|
nodes = mkOption {
|
|
type = attrsOf nodeType;
|
|
default = { };
|
|
internal = true;
|
|
description = "deploy-rs node configurations.";
|
|
};
|
|
} // globalOpts;
|
|
deployType = submodule { options = deployOpts; };
|
|
in
|
|
{
|
|
inherit globalOpts;
|
|
node = mkOpt' nodeType { } "deploy-rs node configuration.";
|
|
deploy = mkOpt' deployType { } "deploy-rs configuration.";
|
|
|
|
filterOpts = filterAttrsRecursive (_: v: v != null);
|
|
};
|
|
|
|
versionOverlay = { self, pkgsFlake }: final: prev:
|
|
let
|
|
date = substring 0 8 (self.lastModifiedDate or self.lastModified or "19700101");
|
|
revCode = flake: flake.shortRev or "dirty";
|
|
in
|
|
{
|
|
trivial = prev.trivial // {
|
|
release = "26.06:u-${prev.trivial.release}";
|
|
codeName = "Irritating";
|
|
revisionWithDefault = default: self.rev or default;
|
|
versionSuffix = ".${date}.${revCode self}:u-${revCode pkgsFlake}";
|
|
};
|
|
};
|
|
upstreamRelease = last (splitString "-" lib.trivial.release);
|
|
|
|
netbootKeaClientClasses = { tftpIP, hostname, systems }:
|
|
let
|
|
testIPXE = "substring(option[user-class].hex, 0, 4) == 'iPXE'";
|
|
in
|
|
(mapAttrsToList (name: mac: {
|
|
name = "nixos-${name}";
|
|
test = "(${testIPXE}) and (hexstring(pkt4.mac, ':') == '${toLower mac}')";
|
|
next-server = tftpIP;
|
|
server-hostname = hostname;
|
|
boot-file-name = "http://${hostname}/systems/${name}/menu.ipxe";
|
|
}) systems) ++ [
|
|
{
|
|
name = "ipxe";
|
|
test = testIPXE;
|
|
next-server = tftpIP;
|
|
server-hostname = hostname;
|
|
boot-file-name = "http://${hostname}/boot.ipxe";
|
|
}
|
|
{
|
|
name = "efi-x86_64";
|
|
test = "option[client-system].hex == 0x0007";
|
|
next-server = tftpIP;
|
|
server-hostname = hostname;
|
|
boot-file-name = "ipxe-x86_64.efi";
|
|
}
|
|
];
|
|
}
|