diff --git a/modules/retiolum/default.nix b/modules/retiolum/default.nix index 4937150..c86a802 100644 --- a/modules/retiolum/default.nix +++ b/modules/retiolum/default.nix @@ -6,6 +6,7 @@ let netname = "retiolum"; cfg = config.networking.retiolum; hosts = ../../hosts; + genipv6 = import ./genipv6.nix { inherit lib; }; in { options = { networking.retiolum.ipv4 = mkOption { @@ -17,6 +18,9 @@ in { }; networking.retiolum.ipv6 = mkOption { type = types.str; + default = (genipv6 "retiolum" "external" { + hostName = config.networking.hostName; + }).address; description = '' own ipv6 address ''; diff --git a/modules/retiolum/genipv6.nix b/modules/retiolum/genipv6.nix new file mode 100644 index 0000000..b359232 --- /dev/null +++ b/modules/retiolum/genipv6.nix @@ -0,0 +1,198 @@ +# stolen from krebs/stockholm +{ lib ? (import {}).lib }: +with lib; +let { + body = netname: subnetname: suffixSpec: rec { + address = let + suffix' = prependZeros suffixLength suffix; + in + normalize-ip6-addr + (checkAddress addressLength (joinAddress subnetPrefix suffix')); + addressCIDR = "${address}/${toString addressLength}"; + addressLength = 128; + + inherit netname; + netCIDR = "${netAddress}/${toString netPrefixLength}"; + netAddress = + normalize-ip6-addr (appendZeros addressLength netPrefix); + netHash = toString { + retiolum = 0; + wiregrill = 1; + }.${netname}; + netPrefix = "42:${netHash}"; + netPrefixLength = { + retiolum = 32; + wiregrill = 32; + }.${netname}; + + inherit subnetname; + subnetCIDR = "${subnetAddress}/${toString subnetPrefixLength}"; + subnetAddress = + normalize-ip6-addr (appendZeros addressLength subnetPrefix); + subnetHash = hashToLength 4 subnetname; + subnetPrefix = joinAddress netPrefix subnetHash; + subnetPrefixLength = netPrefixLength + 16; + + suffix = getAttr (builtins.typeOf suffixSpec) { + set = + concatStringsSep + ":" + (stringToGroupsOf + 4 + (hashToLength (suffixLength / 4) suffixSpec.hostName)); + string = suffixSpec; + }; + suffixLength = addressLength - subnetPrefixLength; + }; + + appendZeros = n: s: let + n' = n / 16; + zeroCount = n' - length parsedaddr; + parsedaddr = parseAddress s; + in + formatAddress (parsedaddr ++ map (const "0") (range 1 zeroCount)); + + prependZeros = n: s: let + n' = n / 16; + zeroCount = n' - length parsedaddr; + parsedaddr = parseAddress s; + in + formatAddress (map (const "0") (range 1 zeroCount) ++ parsedaddr); + + hasEmptyPrefix = xs: take 2 xs == ["" ""]; + hasEmptySuffix = xs: takeLast 2 xs == ["" ""]; + hasEmptyInfix = xs: any (x: x == "") (trimEmpty 2 xs); + + hasEmptyGroup = xs: + any (p: p xs) [hasEmptyPrefix hasEmptyInfix hasEmptySuffix]; + + hashToLength = n: s: substring 0 n (builtins.hashString "sha256" s); + + ltrimEmpty = n: xs: if hasEmptyPrefix xs then drop n xs else xs; + rtrimEmpty = n: xs: if hasEmptySuffix xs then dropLast n xs else xs; + trimEmpty = n: xs: rtrimEmpty n (ltrimEmpty n xs); + + parseAddress = splitString ":"; + formatAddress = concatStringsSep ":"; + + check = s: c: if !c then throw "${s}" else true; + + checkAddress = maxaddrlen: addr: let + parsedaddr = parseAddress addr; + normalizedaddr = trimEmpty 1 parsedaddr; + in + assert (check "address malformed; lone leading colon: ${addr}" ( + head parsedaddr == "" -> tail (take 2 parsedaddr) == "" + )); + assert (check "address malformed; lone trailing colon ${addr}" ( + last parsedaddr == "" -> head (takeLast 2 parsedaddr) == "" + )); + assert (check "address malformed; too many successive colons: ${addr}" ( + length (filter (x: x == "") normalizedaddr) > 1 -> addr == [""] + )); + assert (check "address malformed: ${addr}" ( + all (test "[0-9a-f]{0,4}") parsedaddr + )); + assert (check "address is too long: ${addr}" ( + length normalizedaddr * 16 <= maxaddrlen + )); + addr; + + joinAddress = prefix: suffix: let + parsedPrefix = parseAddress prefix; + parsedSuffix = parseAddress suffix; + normalizePrefix = rtrimEmpty 2 parsedPrefix; + normalizeSuffix = ltrimEmpty 2 parsedSuffix; + delimiter = + optional (length (normalizePrefix ++ normalizeSuffix) < 8 && + (hasEmptySuffix parsedPrefix || hasEmptyPrefix parsedSuffix)) + ""; + in + formatAddress (normalizePrefix ++ delimiter ++ normalizeSuffix); + + # https://tools.ietf.org/html/rfc5952 + normalize-ip6-addr = + let + max-run-0 = + let + both = v: { off = v; pos = v; }; + gt = a: b: a.pos - a.off > b.pos - b.off; + + chkmax = ctx: { + cur = both (ctx.cur.pos + 1); + max = if gt ctx.cur ctx.max then ctx.cur else ctx.max; + }; + + incpos = ctx: recursiveUpdate ctx { + cur.pos = ctx.cur.pos + 1; + }; + + f = ctx: blk: (if blk == "0" then incpos else chkmax) ctx; + z = { cur = both 0; max = both 0; }; + in + blks: (chkmax (foldl' f z blks)).max; + + group-zeros = a: + let + blks = splitString ":" a; + max = max-run-0 blks; + lhs = take max.off blks; + rhs = drop max.pos blks; + in + if max.pos == 0 + then a + else let + sep = + if 8 - (length lhs + length rhs) == 1 + then ":0:" + else "::"; + in + "${concatStringsSep ":" lhs}${sep}${concatStringsSep ":" rhs}"; + + drop-leading-zeros = + let + f = block: + let + res = builtins.match "0*(.+)" block; + in + if res == null + then block # empty block + else elemAt res 0; + in + a: concatStringsSep ":" (map f (splitString ":" a)); + in + a: + toLower + (if test ".*::.*" a + then a + else group-zeros (drop-leading-zeros a)); + + # Split string into list of chunks where each chunk is at most n chars long. + # The leftmost chunk might shorter. + # Example: stringToGroupsOf "123456" -> ["12" "3456"] + stringToGroupsOf = n: s: let + acc = + foldl' + (acc: c: if stringLength acc.chunk < n then { + chunk = acc.chunk + c; + chunks = acc.chunks; + } else { + chunk = c; + chunks = acc.chunks ++ [acc.chunk]; + }) + { + chunk = ""; + chunks = []; + } + (stringToCharacters s); + in + filter (x: x != []) ([acc.chunk] ++ acc.chunks); + + + takeLast = n: xs: reverseList (take n (reverseList xs)); + + test = re: x: isString x && testString re x; + + testString = re: x: builtins.match re x != null; + +}