main
  1{
  2  lib,
  3  den,
  4  ...
  5}: {
  6  den.aspects.services.proxy.dae = {
  7    settings = {
  8      interfaces = {
  9        wan = lib.mkOption {
 10          description = "The WAN interface to bind. Use it if you want to proxy localhost.";
 11          type = lib.types.listOf lib.types.str;
 12          default = ["auto"];
 13        };
 14        lan = lib.mkOption {
 15          description = "The LAN interface to bind. Use it if you want to proxy LAN.";
 16          type = lib.types.listOf lib.types.str;
 17          default = [];
 18        };
 19      };
 20    };
 21
 22    nixos = {
 23      host,
 24      pkgs,
 25      config,
 26      ...
 27    }: let
 28      cfg = host.settings.services.proxy.dae;
 29
 30      allHosts = builtins.concatMap builtins.attrValues (builtins.attrValues den.hosts);
 31      otherHosts = builtins.filter (h: h.name != host.name) allHosts;
 32
 33      hostsWithIpv4ClearText = builtins.filter (h: h.address.ipv4.clearText != null) otherHosts;
 34      hostsWithIpv4Secret = builtins.filter (h: h.address.ipv4.secret.name != null && h.address.ipv4.secret.file != null) otherHosts;
 35      hostsWithIpv6ClearText = builtins.filter (h: h.address.ipv6.clearText != null) otherHosts;
 36      hostsWithIpv6Secret = builtins.filter (h: h.address.ipv6.secret.name != null && h.address.ipv6.secret.file != null) otherHosts;
 37
 38      clearTextIps =
 39        (map (h: h.address.ipv4.clearText) hostsWithIpv4ClearText)
 40        ++ (map (h: h.address.ipv6.clearText) hostsWithIpv6ClearText);
 41
 42      secretIpPlaceholders =
 43        (map (h: config.vaultix.placeholder.${h.address.ipv4.secret.name}) hostsWithIpv4Secret)
 44        ++ (map (h: config.vaultix.placeholder.${h.address.ipv6.secret.name}) hostsWithIpv6Secret);
 45
 46      allHostIps = clearTextIps ++ secretIpPlaceholders;
 47
 48      hostDirectRule =
 49        lib.optionalString (allHostIps != [])
 50        "dip(${builtins.concatStringsSep ", " allHostIps}) -> direct";
 51
 52      v2ray-geoip = pkgs.fetchurl rec {
 53        pname = "v2ray-geoip";
 54        version = "202606262300";
 55        url = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/download/${version}/geoip.dat";
 56        downloadToTemp = true;
 57        recursiveHash = true;
 58        postFetch = ''
 59          install -D "$downloadedFile" "$out/share/v2ray/geoip.dat"
 60        '';
 61        hash = "sha256-0z0qNvvN8I5PY14DWZgk4blZcgLoWIChZefkgeuWM1c=";
 62      };
 63      v2ray-geosite = pkgs.fetchurl rec {
 64        pname = "v2ray-geosite";
 65        version = "202606262300";
 66        url = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/download/${version}/geosite.dat";
 67        downloadToTemp = true;
 68        recursiveHash = true;
 69        postFetch = ''
 70          install -D "$downloadedFile" "$out/share/v2ray/geosite.dat"
 71        '';
 72        hash = "sha256-NytCOfetMwjKFlc8bEz31KskcSnEkvSmvTp+Y8RvVC0=";
 73      };
 74    in {
 75      services.dae = {
 76        enable = true;
 77        package = pkgs.dae.overrideAttrs {
 78          version = "1.1.0";
 79          src = pkgs.fetchFromGitHub {
 80            owner = "daeuniverse";
 81            repo = "dae";
 82            tag = "v1.1.0";
 83            hash = "sha256-Kc51VQuqObxKXVXGv5CnDm4/3XYqjPvrpAQSVb2vxSM=";
 84            fetchSubmodules = true;
 85          };
 86          vendorHash = "sha256-juxIsZt1T33epN8CbzDc02MmlW5PtYa4pcGxuX9OpH4=";
 87        };
 88        assets = [v2ray-geoip v2ray-geosite];
 89        config = ''
 90          include {
 91            # For security reasons, dae requires included files to be under the entry config directory.
 92            routing.dae
 93          }
 94
 95          global {
 96            ${
 97            if cfg.interfaces.wan != []
 98            then "wan_interface: ${builtins.concatStringsSep "," cfg.interfaces.wan}"
 99            else ""
100          }
101            ${
102            if cfg.interfaces.lan != []
103            then "lan_interface: ${builtins.concatStringsSep "," cfg.interfaces.lan}"
104            else ""
105          }
106            log_level: info
107            allow_insecure: false
108            check_interval: 60s
109            check_tolerance: 100ms
110            dial_mode: domain
111          }
112
113          dns {
114            upstream {
115              alidns: 'udp://dns.alidns.com:53'
116              googledns: 'tcp+udp://dns.google:53'
117              tailscale: 'udp://100.100.100.100:53'
118            }
119            routing {
120              request {
121                qname(keyword: _acme-challenge) -> googledns
122                qname(suffix: ts.net, suffix: net.trin.one) -> tailscale
123                qname(geosite:category-ads-all) -> reject
124                qtype(https) -> reject
125                qname(geosite:cn) -> alidns
126                fallback: googledns
127              }
128            }
129          }
130
131          subscription {
132            common: "http://127.0.0.1:${toString host.settings.services.proxy.sub-store.port}/backend/download/collection/common?target=V2Ray"
133            auxiliary: "http://127.0.0.1:${toString host.settings.services.proxy.sub-store.port}/backend/download/collection/auxiliary?target=V2Ray"
134          }
135
136          group {
137            proxy {
138              filter: !subtag(auxiliary) && name(regex: '||||')
139              filter: subtag(auxiliary) && name(regex: '||||') [add_latency: 300ms]
140              policy: min_moving_avg
141            }
142            proxy_download {
143              filter: !subtag(auxiliary) && !name(regex: 'IEPL|') && name(regex: '|||||')
144              policy: min_avg10
145            }
146          }
147        '';
148      };
149
150      systemd.services.dae = {
151        serviceConfig.LoadCredential = ["routing.dae:${config.vaultix.templates.dae-routing.path}"];
152      };
153
154      vaultix.secrets = lib.mkMerge (
155        (map (h: {${h.address.ipv4.secret.name}.file = h.address.ipv4.secret.file;}) hostsWithIpv4Secret)
156        ++ (map (h: {${h.address.ipv6.secret.name}.file = h.address.ipv6.secret.file;}) hostsWithIpv6Secret)
157      );
158
159      vaultix.templates.dae-routing = {
160        content = ''
161          routing {
162            # == System ==
163            pname(NetworkManager, systemd-networkd, systemd-resolved,
164                  dnsmasq, cloudflared) -> must_direct
165            pname(ssh) -> direct
166            dport(22) -> direct
167
168            # == DNS ==
169            dip(223.5.5.5, 223.6.6.6) -> direct
170            domain(full:dns.alidns.com) -> direct
171            dip(119.29.29.29, 119.28.28.28) -> direct
172            domain(full:doh.pub) -> direct
173            dip(8.8.8.8, 8.8.4.4) -> proxy
174            domain(full:dns.google) -> proxy
175            dip(1.1.1.1, 1.0.0.1) -> proxy
176            domain(full:one.one.one.one, full:cloudflare-dns.com) -> proxy
177
178            # == Private ==
179            dip(geoip:private) -> direct
180            domain(geosite:private) -> direct
181
182            # == Tailscale ==
183            dip(100.64.0.0/10) -> direct
184            pname(tailscale, tailscaled) -> direct
185            l4proto(udp) && dport(3478) -> direct
186            l4proto(udp) && sport(41641) -> direct
187
188            # == My Hosts ==
189            ${hostDirectRule}
190
191            # == AI ==
192            domain(geosite:category-ai-!cn) -> proxy
193
194            # == Big Company ==
195            domain(geosite:microsoft@cn) -> direct
196            domain(geosite:microsoft) -> proxy
197            domain(geosite:apple@cn) -> direct
198            domain(geosite:apple) -> proxy
199            domain(geosite:google@cn) -> direct
200            domain(geosite:google) -> proxy
201            domain(geosite:cloudflare) -> proxy
202
203            # == Game ==
204            domain(suffix:cm.steampowered.com) -> direct
205            domain(suffix:steamserver.net) -> direct
206            domain(geosite:steam@cn) -> direct
207            domain(geosite:category-games-cn) -> direct
208            domain(geosite:category-game-platforms-download@cn) -> direct
209
210            # == Download ==
211            domain(geosite:mega) -> proxy_download
212            domain(keyword:dl-a10b-, dl-z01a-, dl-b07-) -> proxy_download
213
214            # == China ==
215            domain(geosite:tld-cn, geosite:geolocation-cn, geosite:cn) -> direct
216            dip(geoip:cn) -> direct
217            domain(geosite:gfw) -> proxy
218
219            # == Other ==
220            domain(suffix: kagi.com, suffix: codeberg.org, suffix: mxrouting.net) -> direct
221
222            # == Fallback ==
223            fallback: proxy
224          }
225        '';
226      };
227    };
228  };
229}