Kieran's opinionated (and probably slightly dumb) nix config
at main 5.5 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 cfg = config.atelier.services.frps; 9in 10{ 11 options.atelier.services.frps = { 12 enable = lib.mkEnableOption "frp server for tunneling services"; 13 14 bindAddr = lib.mkOption { 15 type = lib.types.str; 16 default = "0.0.0.0"; 17 description = "Address to bind frp server to"; 18 }; 19 20 bindPort = lib.mkOption { 21 type = lib.types.port; 22 default = 7000; 23 description = "Port for frp control connection"; 24 }; 25 26 vhostHTTPPort = lib.mkOption { 27 type = lib.types.port; 28 default = 7080; 29 description = "Port for HTTP virtual host traffic"; 30 }; 31 32 allowedTCPPorts = lib.mkOption { 33 type = lib.types.listOf lib.types.port; 34 default = lib.lists.range 20000 20099; 35 example = [ 20000 20001 20002 20003 20004 ]; 36 description = "TCP port range to allow for TCP tunnels (default: 20000-20099)"; 37 }; 38 39 allowedUDPPorts = lib.mkOption { 40 type = lib.types.listOf lib.types.port; 41 default = lib.lists.range 20000 20099; 42 example = [ 20000 20001 20002 20003 20004 ]; 43 description = "UDP port range to allow for UDP tunnels (default: 20000-20099)"; 44 }; 45 46 authToken = lib.mkOption { 47 type = lib.types.nullOr lib.types.str; 48 default = null; 49 description = "Authentication token for clients (deprecated: use authTokenFile)"; 50 }; 51 52 authTokenFile = lib.mkOption { 53 type = lib.types.nullOr lib.types.path; 54 default = null; 55 description = "Path to file containing authentication token"; 56 }; 57 58 domain = lib.mkOption { 59 type = lib.types.str; 60 example = "bore.dunkirk.sh"; 61 description = "Base domain for subdomains (e.g., *.bore.dunkirk.sh)"; 62 }; 63 64 enableCaddy = lib.mkOption { 65 type = lib.types.bool; 66 default = true; 67 description = "Automatically configure Caddy reverse proxy for wildcard domain"; 68 }; 69 }; 70 71 config = lib.mkIf cfg.enable { 72 assertions = [ 73 { 74 assertion = cfg.authToken != null || cfg.authTokenFile != null; 75 message = "Either authToken or authTokenFile must be set for frps"; 76 } 77 ]; 78 79 # Open firewall ports for frp control connection and TCP/UDP tunnels 80 networking.firewall.allowedTCPPorts = [ cfg.bindPort ] ++ cfg.allowedTCPPorts; 81 networking.firewall.allowedUDPPorts = cfg.allowedUDPPorts; 82 83 # frp server service 84 systemd.services.frps = 85 let 86 tokenConfig = 87 if cfg.authTokenFile != null then 88 '' 89 auth.tokenSource.type = "file" 90 auth.tokenSource.file.path = "${cfg.authTokenFile}" 91 '' 92 else 93 ''auth.token = "${cfg.authToken}"''; 94 95 configFile = pkgs.writeText "frps.toml" '' 96 bindAddr = "${cfg.bindAddr}" 97 bindPort = ${toString cfg.bindPort} 98 vhostHTTPPort = ${toString cfg.vhostHTTPPort} 99 100 # Dashboard and Prometheus metrics 101 webServer.addr = "127.0.0.1" 102 webServer.port = 7400 103 enablePrometheus = true 104 105 # Authentication token - clients need this to connect 106 auth.method = "token" 107 ${tokenConfig} 108 109 # Subdomain support for *.${cfg.domain} 110 subDomainHost = "${cfg.domain}" 111 112 # Allow port ranges for TCP/UDP tunnels 113 # Format: [[{"start": 20000, "end": 20099}]] 114 allowPorts = [ 115 { start = 20000, end = 20099 } 116 ] 117 118 # Custom 404 page 119 custom404Page = "${./404.html}" 120 121 # Logging 122 log.to = "console" 123 log.level = "info" 124 ''; 125 in 126 { 127 description = "frp server for ${cfg.domain} tunneling"; 128 after = [ "network.target" ]; 129 wantedBy = [ "multi-user.target" ]; 130 serviceConfig = { 131 Type = "simple"; 132 Restart = "on-failure"; 133 RestartSec = "5s"; 134 ExecStart = "${pkgs.frp}/bin/frps -c ${configFile}"; 135 }; 136 }; 137 138 # Automatically configure Caddy for wildcard domain 139 services.caddy = lib.mkIf cfg.enableCaddy { 140 # Dashboard for base domain 141 virtualHosts."${cfg.domain}" = { 142 extraConfig = '' 143 tls { 144 dns cloudflare {env.CLOUDFLARE_API_TOKEN} 145 } 146 header { 147 Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" 148 } 149 150 # Proxy /api/* to frps dashboard 151 handle /api/* { 152 reverse_proxy localhost:7400 153 } 154 155 # Serve dashboard HTML 156 handle { 157 root * ${./.} 158 try_files dashboard.html 159 file_server 160 } 161 ''; 162 }; 163 164 # Wildcard subdomain proxy to frps 165 virtualHosts."*.${cfg.domain}" = { 166 extraConfig = '' 167 tls { 168 dns cloudflare {env.CLOUDFLARE_API_TOKEN} 169 } 170 header { 171 Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" 172 } 173 reverse_proxy localhost:${toString cfg.vhostHTTPPort} { 174 header_up X-Forwarded-Proto {scheme} 175 header_up X-Forwarded-For {remote} 176 header_up Host {host} 177 } 178 handle_errors { 179 @404 expression {http.error.status_code} == 404 180 handle @404 { 181 root * ${./.} 182 rewrite * /404.html 183 file_server 184 } 185 } 186 ''; 187 }; 188 }; 189 }; 190}