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