Commit 5662f1c
Changed files (8)
modules
services
direct-dns
mihomo
modules/services/caddy/default.nix
@@ -106,14 +106,15 @@
- secret IPv4/IPv6 (address.ipv4.secret / address.ipv6.secret)
'';
- entries =
+ entries = lib.unique (
lib.concatMap (
r:
lib.mapAttrsToList (mkEntry (resolveAddress r.source.host)) (
lib.filterAttrs (_: conf: !(conf.tailscale or false)) r.value
)
)
- (lib.filter (r: r.source.host.name == host.name) reverseProxy);
+ (lib.filter (r: r.source.host.name == host.name) reverseProxy)
+ );
importEntries = lib.filter (e: e.resolution.kind == "import") entries;
modules/services/caddy/quirks.nix
@@ -16,6 +16,7 @@ in {
path = "/foo/bar" # Path to reverse proxy to
stripPath = false; # Whether to strip path prefix ("/foo/bar")
upstream = "/"; Path to reverse proxy from
+ prefer = "direct"; # Connection policy override: null (host default), "cdn", "direct" or "tailscale"
}
'';
@@ -25,11 +26,10 @@ in {
inherit (den.lib.policy) pipe;
in [
(pipe.from "reverseProxy" [
- (pipe.collect ({host, ...}: true))
- (pipe.transform (value:
- lib.mapAttrs
+ (pipe.collectAll ({host, ...}: true))
+ (pipe.transform (value: (lib.mapAttrs
(domain: conf: conf // {tailscale = lib.hasSuffix ".${tailnetDomain}" domain;})
- value))
+ value)))
pipe.withProvenance
])
];
modules/services/caddy/tailscale.nix
@@ -17,7 +17,7 @@
forwardAuthBlock
;
- entries =
+ entries = lib.unique (
lib.concatMap (
r:
lib.mapAttrsToList (mkEntry {
@@ -25,7 +25,8 @@
address = "127.0.0.1";
}) (lib.filterAttrs (_: conf: conf.tailscale or false) r.value)
)
- (lib.filter (r: r.source.host.name == host.name) reverseProxy);
+ (lib.filter (r: r.source.host.name == host.name) reverseProxy)
+ );
enabled = builtins.length entries > 0;
modules/services/direct-dns/_lib.nix
@@ -0,0 +1,59 @@
+{lib}: let
+ fromClearOrSecret = addr:
+ if addr.clearText != null
+ then {
+ kind = "clear";
+ value = addr.clearText;
+ }
+ else if addr.secret.name != null && addr.secret.file != null
+ then {
+ kind = "secret";
+ name = addr.secret.name;
+ file = addr.secret.file;
+ }
+ else null;
+
+ mkClear = value:
+ if value != null
+ then {
+ kind = "clear";
+ inherit value;
+ }
+ else null;
+
+ resolve = host: domain: conf: let
+ prefer = conf.prefer or null;
+ mode =
+ if prefer != null
+ then prefer
+ else host.settings.services.direct-dns.mode or "cdn";
+ v4 =
+ if mode == "direct"
+ then fromClearOrSecret host.address.ipv4
+ else if mode == "tailscale"
+ then mkClear host.address.ipv4.tailscale
+ else null;
+ v6 =
+ if mode == "direct"
+ then fromClearOrSecret host.address.ipv6
+ else if mode == "tailscale"
+ then mkClear host.address.ipv6.tailscale
+ else null;
+ in
+ assert lib.assertMsg (mode != "direct" || v4 != null || v6 != null)
+ "direct-dns: domain '${domain}' on host '${host.name}' prefers 'direct' but the host has no public IPv4/IPv6 configured."; {
+ inherit domain mode v4 v6;
+ hostName = host.name;
+ };
+in {
+ mkDirectEntries = reverseProxy:
+ lib.unique (
+ lib.concatMap (
+ r:
+ lib.mapAttrsToList (resolve r.source.host) (
+ lib.filterAttrs (_: conf: !(conf.tailscale or false)) r.value
+ )
+ )
+ reverseProxy
+ );
+}
modules/services/direct-dns/default.nix
@@ -0,0 +1,19 @@
+{lib, ...}: {
+ den.aspects.services.direct-dns = {
+ settings.host = {
+ mode = lib.mkOption {
+ type = lib.types.enum ["cdn" "direct" "tailscale"];
+ default = "cdn";
+ description = ''
+ Default connection policy for reverse proxy domains served by this host.
+
+ - `cdn`: resolve through public DNS (unchanged).
+ - `direct`: resolve to the host's public IP.
+ - `tailscale`: resolve to the host's tailscale IP.
+
+ Individual reverse proxy entries can override this with `prefer`.
+ '';
+ };
+ };
+ };
+}
modules/services/mihomo/dns.nix
@@ -1,16 +1,68 @@
{
+ den,
+ lib,
+ ...
+}: let
+ inherit (import ../direct-dns/_lib.nix {inherit lib;}) mkDirectEntries;
+
+ templateName = domain: "mihomo-hosts-${lib.replaceStrings ["." ":"] ["-" "-"] domain}";
+in {
den.aspects.services.mihomo.dns = {
- nixos = {host, ...}: let
+ nixos = {
+ host,
+ reverseProxy,
+ config,
+ ...
+ }: let
cfg = host.settings.services.mihomo;
+
+ directEntries = mkDirectEntries reverseProxy;
+
+ directV4Clear = lib.listToAttrs (lib.concatMap (e:
+ lib.optional (e.v4 != null && e.v4.kind == "clear") {
+ name = e.domain;
+ value = [e.v4.value];
+ })
+ directEntries);
+
+ directV4Secret = lib.listToAttrs (lib.concatMap (e:
+ lib.optional (e.v4 != null && e.v4.kind == "secret") {
+ name = e.domain;
+ value = {
+ _secret = config.vaultix.templates.${templateName e.domain}.path;
+ quote = false;
+ };
+ })
+ directEntries);
+
+ secretEntries = lib.filter (e: e.v4 != null && e.v4.kind == "secret") directEntries;
in {
+ vaultix.secrets = lib.listToAttrs (map (e: {
+ name = e.v4.name;
+ value.file = e.v4.file;
+ })
+ secretEntries);
+ vaultix.templates = lib.listToAttrs (map (e: {
+ name = templateName e.domain;
+ value = {
+ content = builtins.toJSON [config.vaultix.placeholder.${e.v4.name}];
+ owner = "mihomo";
+ group = "mihomo";
+ mode = "0400";
+ };
+ })
+ secretEntries);
+
services.mihomo = {
config = {
- hosts = {
- "dns.alidns.com" = ["223.5.5.5" "223.6.6.6" "2400:3200::1" "2400:3200:baba::1"];
- "doh.pub" = ["1.12.12.12" "1.12.12.21" "120.53.53.53"];
- "dns.google" = ["8.8.8.8" "8.8.4.4" "2001:4860:4860::8888" "2001:4860:4860::8844"];
- "cloudflare-dns.com" = ["1.1.1.1" "1.0.0.1" "2606:4700:4700::1111" "2606:4700:4700::1001"];
- };
+ hosts =
+ {
+ "dns.alidns.com" = ["223.5.5.5" "223.6.6.6" "2400:3200::1" "2400:3200:baba::1"];
+ "doh.pub" = ["1.12.12.12" "1.12.12.21" "120.53.53.53"];
+ "dns.google" = ["8.8.8.8" "8.8.4.4" "2001:4860:4860::8888" "2001:4860:4860::8844"];
+ "cloudflare-dns.com" = ["1.1.1.1" "1.0.0.1" "2606:4700:4700::1111" "2606:4700:4700::1001"];
+ }
+ // directV4Clear // directV4Secret;
dns = {
enable = true;
listen = ":${toString cfg.ports.dns}";
modules/services/headscale.nix
@@ -63,6 +63,8 @@
tailnetDomain = den.aspects.services.caddy.tailnetDomain;
+ inherit (import ./direct-dns/_lib.nix {inherit lib;}) mkDirectEntries;
+
derpHosts = let
allHosts = builtins.concatMap builtins.attrValues (builtins.attrValues den.hosts);
in
@@ -161,17 +163,24 @@
# Tailnet entries (from all hosts): emit A/AAAA records pointing at the
# owning host's tailscale addresses so MagicDNS resolves them.
- tailnetEntries =
- lib.concatMap (
- r:
- lib.mapAttrsToList (domain: conf: {
- inherit domain;
- source = r.source.host;
- }) (
- lib.filterAttrs (_: conf: conf.tailscale or false) r.value
- )
- )
- reverseProxy;
+ tailnetEntries = let
+ uniqueByDomain = lib.foldl' (acc: e:
+ if builtins.any (x: x.domain == e.domain) acc
+ then acc
+ else acc ++ [e]) [];
+ in
+ uniqueByDomain (
+ lib.concatMap (
+ r:
+ lib.mapAttrsToList (domain: conf: {
+ inherit domain;
+ source = r.source.host;
+ }) (
+ lib.filterAttrs (_: conf: conf.tailscale or false) r.value
+ )
+ )
+ reverseProxy
+ );
duplicateDomains =
lib.filter (domain: builtins.length (lib.filter (e: e.domain == domain) tailnetEntries) > 1)
@@ -211,6 +220,35 @@
}
])
tailnetEntries;
+
+ directEntries = mkDirectEntries reverseProxy;
+
+ mkRecordValue = ip:
+ if ip.kind == "clear"
+ then ip.value
+ else config.vaultix.placeholder.${ip.name};
+
+ directRecords = lib.concatMap (e:
+ lib.optionals (e.v4 != null) [
+ {
+ name = e.domain;
+ type = "A";
+ value = mkRecordValue e.v4;
+ }
+ ]
+ ++ lib.optionals (e.v6 != null) [
+ {
+ name = e.domain;
+ type = "AAAA";
+ value = mkRecordValue e.v6;
+ }
+ ])
+ directEntries;
+
+ directSecretIps = lib.unique (lib.concatMap (e:
+ lib.optional (e.v4 != null && e.v4.kind == "secret") e.v4
+ ++ lib.optional (e.v6 != null && e.v6.kind == "secret") e.v6)
+ directEntries);
in {
services.headscale = {
enable = true;
@@ -261,6 +299,7 @@
vaultix.secrets = lib.mkMerge (
(map (h: {${h.address.ipv4.secret.name}.file = h.address.ipv4.secret.file;}) hostsWithIpv4Secret)
++ (map (h: {${h.address.ipv6.secret.name}.file = h.address.ipv6.secret.file;}) hostsWithIpv6Secret)
+ ++ (map (s: {${s.name}.file = s.file;}) directSecretIps)
);
vaultix.templates.headscale-derp-map = {
content = derpMapYaml;
@@ -268,6 +307,12 @@
group = config.services.headscale.group;
mode = "0400";
};
+ vaultix.templates.headscale-direct-records = {
+ content = builtins.toJSON directRecords;
+ owner = config.services.headscale.user;
+ group = config.services.headscale.group;
+ mode = "0400";
+ };
systemd.services.headscale = let
nixConfig = config.services.headscale.configFile;
@@ -293,16 +338,17 @@
fi
# Merge DNS extra records
- if [ -f "${dnsRecordsFilePath}" ]; then
- ${lib.getExe pkgs.yq-go} eval-all '. as $item ireduce ([]; . + $item) | unique_by(.name + "|" + .type)' \
- "${nixDnsRecords}" \
- "${dnsRecordsFilePath}" \
- > "${dnsRecordsFilePath}.tmp" \
- && mv "${dnsRecordsFilePath}.tmp" "${dnsRecordsFilePath}"
- else
+ if [ ! -f "${dnsRecordsFilePath}" ]; then
cp "${nixDnsRecords}" "${dnsRecordsFilePath}"
chmod 0640 "${dnsRecordsFilePath}"
fi
+
+ ${lib.getExe pkgs.yq-go} eval-all '. as $item ireduce ([]; . + $item) | unique_by(.name + "|" + .type)' \
+ "${nixDnsRecords}" \
+ "${config.vaultix.templates.headscale-direct-records.path}" \
+ "${dnsRecordsFilePath}" \
+ > "${dnsRecordsFilePath}.tmp" \
+ && mv "${dnsRecordsFilePath}.tmp" "${dnsRecordsFilePath}"
'';
in {
serviceConfig = {
modules/services/tailscale-derp.nix
@@ -15,6 +15,7 @@
in {
${cfg.domain} = {
port = cfg.port;
+ prefer = "direct";
};
};