at 25.11-pre 17 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 cfg = config.services.wstunnel; 10 11 hostPortToString = { host, port }: "${host}:${toString port}"; 12 13 hostPortSubmodule = { 14 options = { 15 host = lib.mkOption { 16 description = "The hostname."; 17 type = lib.types.str; 18 }; 19 port = lib.mkOption { 20 description = "The port."; 21 type = lib.types.port; 22 }; 23 }; 24 }; 25 26 commonOptions = { 27 enable = lib.mkEnableOption "this `wstunnel` instance" // { 28 default = true; 29 }; 30 31 package = lib.mkPackageOption pkgs "wstunnel" { }; 32 33 autoStart = lib.mkEnableOption "starting this wstunnel instance automatically" // { 34 default = true; 35 }; 36 37 extraArgs = lib.mkOption { 38 description = '' 39 Extra command line arguments to pass to `wstunnel`. 40 Attributes of the form `argName = true;` will be translated to `--argName`, 41 and `argName = \"value\"` to `--argName value`. 42 ''; 43 type = with lib.types; attrsOf (either str bool); 44 default = { }; 45 example = { 46 "someNewOption" = true; 47 "someNewOptionWithValue" = "someValue"; 48 }; 49 }; 50 51 # The original argument name `websocketPingFrequency` is a misnomer, as the frequency is the inverse of the interval. 52 websocketPingInterval = lib.mkOption { 53 description = "Frequency at which the client will send websocket ping to the server."; 54 type = lib.types.nullOr lib.types.ints.unsigned; 55 default = null; 56 }; 57 58 loggingLevel = lib.mkOption { 59 description = '' 60 Passed to --log-lvl 61 62 Control the log verbosity. i.e: TRACE, DEBUG, INFO, WARN, ERROR, OFF 63 For more details, checkout [EnvFilter](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax) 64 ''; 65 type = lib.types.nullOr lib.types.str; 66 example = "INFO"; 67 default = null; 68 }; 69 70 environmentFile = lib.mkOption { 71 description = '' 72 Environment file to be passed to the systemd service. 73 Useful for passing secrets to the service to prevent them from being 74 world-readable in the Nix store. 75 Note however that the secrets are passed to `wstunnel` through 76 the command line, which makes them locally readable for all users of 77 the system at runtime. 78 ''; 79 type = lib.types.nullOr lib.types.path; 80 default = null; 81 example = "/var/lib/secrets/wstunnelSecrets"; 82 }; 83 }; 84 85 serverSubmodule = 86 { config, ... }: 87 { 88 options = commonOptions // { 89 listen = lib.mkOption { 90 description = '' 91 Address and port to listen on. 92 Setting the port to a value below 1024 will also give the process 93 the required `CAP_NET_BIND_SERVICE` capability. 94 ''; 95 type = lib.types.submodule hostPortSubmodule; 96 default = { 97 host = "0.0.0.0"; 98 port = if config.enableHTTPS then 443 else 80; 99 }; 100 defaultText = lib.literalExpression '' 101 { 102 host = "0.0.0.0"; 103 port = if enableHTTPS then 443 else 80; 104 } 105 ''; 106 }; 107 108 restrictTo = lib.mkOption { 109 description = '' 110 Accepted traffic will be forwarded only to this service. 111 ''; 112 type = lib.types.listOf (lib.types.submodule hostPortSubmodule); 113 default = [ ]; 114 example = [ 115 { 116 host = "127.0.0.1"; 117 port = 51820; 118 } 119 ]; 120 }; 121 122 enableHTTPS = lib.mkOption { 123 description = "Use HTTPS for the tunnel server."; 124 type = lib.types.bool; 125 default = true; 126 }; 127 128 tlsCertificate = lib.mkOption { 129 description = '' 130 TLS certificate to use instead of the hardcoded one in case of HTTPS connections. 131 Use together with `tlsKey`. 132 ''; 133 type = lib.types.nullOr lib.types.path; 134 default = null; 135 example = "/var/lib/secrets/cert.pem"; 136 }; 137 138 tlsKey = lib.mkOption { 139 description = '' 140 TLS key to use instead of the hardcoded on in case of HTTPS connections. 141 Use together with `tlsCertificate`. 142 ''; 143 type = lib.types.nullOr lib.types.path; 144 default = null; 145 example = "/var/lib/secrets/key.pem"; 146 }; 147 148 useACMEHost = lib.mkOption { 149 description = '' 150 Use a certificate generated by the NixOS ACME module for the given host. 151 Note that this will not generate a new certificate - you will need to do so with `security.acme.certs`. 152 ''; 153 type = lib.types.nullOr lib.types.str; 154 default = null; 155 example = "example.com"; 156 }; 157 }; 158 }; 159 160 clientSubmodule = 161 { config, ... }: 162 { 163 options = commonOptions // { 164 connectTo = lib.mkOption { 165 description = "Server address and port to connect to."; 166 type = lib.types.str; 167 example = "https://wstunnel.server.com:8443"; 168 }; 169 170 localToRemote = lib.mkOption { 171 description = "Listen on local and forwards traffic from remote."; 172 type = lib.types.listOf (lib.types.str); 173 default = [ ]; 174 example = [ 175 "tcp://1212:google.com:443" 176 "unix:///tmp/wstunnel.sock:g.com:443" 177 ]; 178 }; 179 180 remoteToLocal = lib.mkOption { 181 description = "Listen on remote and forwards traffic from local. Only tcp is supported"; 182 type = lib.types.listOf lib.types.str; 183 default = [ ]; 184 example = [ 185 "tcp://1212:google.com:443" 186 "unix://wstunnel.sock:g.com:443" 187 ]; 188 }; 189 190 addNetBind = lib.mkEnableOption "Whether add CAP_NET_BIND_SERVICE to the tunnel service, this should be enabled if you want to bind port < 1024"; 191 192 httpProxy = lib.mkOption { 193 description = '' 194 Proxy to use to connect to the wstunnel server (`USER:PASS@HOST:PORT`). 195 196 ::: {.warning} 197 Passwords specified here will be world-readable in the Nix store! 198 To pass a password to the service, point the `environmentFile` option 199 to a file containing `PROXY_PASSWORD=<your-password-here>` and set 200 this option to `<user>:$PROXY_PASSWORD@<host>:<port>`. 201 Note however that this will also locally leak the passwords at 202 runtime via e.g. /proc/<pid>/cmdline. 203 ::: 204 ''; 205 type = lib.types.nullOr lib.types.str; 206 default = null; 207 }; 208 209 soMark = lib.mkOption { 210 description = '' 211 Mark network packets with the SO_MARK sockoption with the specified value. 212 Setting this option will also enable the required `CAP_NET_ADMIN` capability 213 for the systemd service. 214 ''; 215 type = lib.types.nullOr lib.types.ints.unsigned; 216 default = null; 217 }; 218 219 upgradePathPrefix = lib.mkOption { 220 description = '' 221 Use a specific HTTP path prefix that will show up in the upgrade 222 request to the `wstunnel` server. 223 Useful when running `wstunnel` behind a reverse proxy. 224 ''; 225 type = lib.types.nullOr lib.types.str; 226 default = null; 227 example = "wstunnel"; 228 }; 229 230 tlsSNI = lib.mkOption { 231 description = "Use this as the SNI while connecting via TLS. Useful for circumventing hostname-based firewalls."; 232 type = lib.types.nullOr lib.types.str; 233 default = null; 234 }; 235 236 tlsVerifyCertificate = lib.mkOption { 237 description = "Whether to verify the TLS certificate of the server. It might be useful to set this to `false` when working with the `tlsSNI` option."; 238 type = lib.types.bool; 239 default = true; 240 }; 241 242 upgradeCredentials = lib.mkOption { 243 description = '' 244 Use these credentials to authenticate during the HTTP upgrade request 245 (Basic authorization type, `USER:[PASS]`). 246 247 ::: {.warning} 248 Passwords specified here will be world-readable in the Nix store! 249 To pass a password to the service, point the `environmentFile` option 250 to a file containing `HTTP_PASSWORD=<your-password-here>` and set this 251 option to `<user>:$HTTP_PASSWORD`. 252 Note however that this will also locally leak the passwords at runtime 253 via e.g. /proc/<pid>/cmdline. 254 ::: 255 ''; 256 type = lib.types.nullOr lib.types.str; 257 default = null; 258 }; 259 260 customHeaders = lib.mkOption { 261 description = "Custom HTTP headers to send during the upgrade request."; 262 type = lib.types.attrsOf lib.types.str; 263 default = { }; 264 example = { 265 "X-Some-Header" = "some-value"; 266 }; 267 }; 268 }; 269 }; 270 271 generateServerUnit = name: serverCfg: { 272 name = "wstunnel-server-${name}"; 273 value = 274 let 275 certConfig = config.security.acme.certs.${serverCfg.useACMEHost}; 276 in 277 { 278 description = "wstunnel server - ${name}"; 279 requires = [ 280 "network.target" 281 "network-online.target" 282 ]; 283 after = [ 284 "network.target" 285 "network-online.target" 286 ]; 287 wantedBy = lib.optional serverCfg.autoStart "multi-user.target"; 288 289 environment.RUST_LOG = serverCfg.loggingLevel; 290 291 serviceConfig = { 292 Type = "exec"; 293 EnvironmentFile = lib.optional (serverCfg.environmentFile != null) serverCfg.environmentFile; 294 DynamicUser = true; 295 SupplementaryGroups = lib.optional (serverCfg.useACMEHost != null) certConfig.group; 296 PrivateTmp = true; 297 AmbientCapabilities = lib.optionals (serverCfg.listen.port < 1024) [ "CAP_NET_BIND_SERVICE" ]; 298 NoNewPrivileges = true; 299 RestrictNamespaces = "uts ipc pid user cgroup"; 300 ProtectSystem = "strict"; 301 ProtectHome = true; 302 ProtectKernelTunables = true; 303 ProtectKernelModules = true; 304 ProtectControlGroups = true; 305 PrivateDevices = true; 306 RestrictSUIDSGID = true; 307 308 Restart = "on-failure"; 309 RestartSec = 2; 310 RestartSteps = 20; 311 RestartMaxDelaySec = "5min"; 312 }; 313 314 script = with serverCfg; '' 315 ${lib.getExe package} \ 316 server \ 317 ${ 318 lib.cli.toGNUCommandLineShell { } ( 319 lib.recursiveUpdate { 320 restrict-to = map hostPortToString restrictTo; 321 websocket-ping-frequency-sec = websocketPingInterval; 322 tls-certificate = 323 if !enableHTTPS then 324 null 325 else if useACMEHost != null then 326 "${certConfig.directory}/fullchain.pem" 327 else 328 "${tlsCertificate}"; 329 tls-private-key = 330 if !enableHTTPS then 331 null 332 else if useACMEHost != null then 333 "${certConfig.directory}/key.pem" 334 else 335 "${tlsKey}"; 336 } extraArgs 337 ) 338 } \ 339 ${lib.escapeShellArg "${if enableHTTPS then "wss" else "ws"}://${hostPortToString listen}"} 340 ''; 341 }; 342 }; 343 344 generateClientUnit = name: clientCfg: { 345 name = "wstunnel-client-${name}"; 346 value = { 347 description = "wstunnel client - ${name}"; 348 requires = [ 349 "network.target" 350 "network-online.target" 351 ]; 352 after = [ 353 "network.target" 354 "network-online.target" 355 ]; 356 wantedBy = lib.optional clientCfg.autoStart "multi-user.target"; 357 358 environment.RUST_LOG = clientCfg.loggingLevel; 359 360 serviceConfig = { 361 Type = "exec"; 362 EnvironmentFile = lib.optional (clientCfg.environmentFile != null) clientCfg.environmentFile; 363 DynamicUser = true; 364 PrivateTmp = true; 365 AmbientCapabilities = 366 (lib.optionals clientCfg.addNetBind [ "CAP_NET_BIND_SERVICE" ]) 367 ++ (lib.optionals (clientCfg.soMark != null) [ "CAP_NET_ADMIN" ]); 368 NoNewPrivileges = true; 369 RestrictNamespaces = "uts ipc pid user cgroup"; 370 ProtectSystem = "strict"; 371 ProtectHome = true; 372 ProtectKernelTunables = true; 373 ProtectKernelModules = true; 374 ProtectControlGroups = true; 375 PrivateDevices = true; 376 RestrictSUIDSGID = true; 377 378 Restart = "on-failure"; 379 RestartSec = 2; 380 RestartSteps = 20; 381 RestartMaxDelaySec = "5min"; 382 }; 383 384 script = with clientCfg; '' 385 ${lib.getExe package} \ 386 client \ 387 ${ 388 lib.cli.toGNUCommandLineShell { } ( 389 lib.recursiveUpdate { 390 local-to-remote = localToRemote; 391 remote-to-local = remoteToLocal; 392 http-headers = lib.mapAttrsToList (n: v: "${n}:${v}") customHeaders; 393 http-proxy = httpProxy; 394 socket-so-mark = soMark; 395 http-upgrade-path-prefix = upgradePathPrefix; 396 tls-sni-override = tlsSNI; 397 tls-verify-certificate = tlsVerifyCertificate; 398 websocket-ping-frequency-sec = websocketPingInterval; 399 http-upgrade-credentials = upgradeCredentials; 400 } extraArgs 401 ) 402 } \ 403 ${lib.escapeShellArg connectTo} 404 ''; 405 }; 406 }; 407in 408{ 409 options.services.wstunnel = { 410 enable = lib.mkEnableOption "wstunnel"; 411 412 servers = lib.mkOption { 413 description = "`wstunnel` servers to set up."; 414 type = lib.types.attrsOf (lib.types.submodule serverSubmodule); 415 default = { }; 416 example = { 417 "wg-tunnel" = { 418 listen = { 419 host = "0.0.0.0"; 420 port = 8080; 421 }; 422 enableHTTPS = true; 423 tlsCertificate = "/var/lib/secrets/fullchain.pem"; 424 tlsKey = "/var/lib/secrets/key.pem"; 425 restrictTo = [ 426 { 427 host = "127.0.0.1"; 428 port = 51820; 429 } 430 ]; 431 }; 432 }; 433 }; 434 435 clients = lib.mkOption { 436 description = "`wstunnel` clients to set up."; 437 type = lib.types.attrsOf (lib.types.submodule clientSubmodule); 438 default = { }; 439 example = { 440 "wg-tunnel" = { 441 connectTo = "wss://wstunnel.server.com:8443"; 442 localToRemote = [ 443 "tcp://1212:google.com:443" 444 "tcp://2:n.lan:4?proxy_protocol" 445 ]; 446 remoteToLocal = [ 447 "socks5://[::1]:1212" 448 "unix://wstunnel.sock:g.com:443" 449 ]; 450 }; 451 }; 452 }; 453 }; 454 455 config = lib.mkIf cfg.enable { 456 systemd.services = 457 (lib.mapAttrs' generateServerUnit (lib.filterAttrs (n: v: v.enable) cfg.servers)) 458 // (lib.mapAttrs' generateClientUnit (lib.filterAttrs (n: v: v.enable) cfg.clients)); 459 460 assertions = 461 (lib.mapAttrsToList (name: serverCfg: { 462 assertion = !(serverCfg.useACMEHost != null && serverCfg.tlsCertificate != null); 463 message = '' 464 Options services.wstunnel.servers."${name}".useACMEHost and services.wstunnel.servers."${name}".{tlsCertificate, tlsKey} are mutually exclusive. 465 ''; 466 }) cfg.servers) 467 ++ 468 469 (lib.mapAttrsToList (name: serverCfg: { 470 assertion = 471 serverCfg.enableHTTPS 472 -> 473 (serverCfg.useACMEHost != null) || (serverCfg.tlsCertificate != null && serverCfg.tlsKey != null); 474 message = '' 475 If services.wstunnel.servers."${name}".enableHTTPS is set to true, either services.wstunnel.servers."${name}".useACMEHost or both services.wstunnel.servers."${name}".tlsKey and services.wstunnel.servers."${name}".tlsCertificate need to be set. 476 ''; 477 }) cfg.servers) 478 ++ 479 480 (lib.mapAttrsToList (name: clientCfg: { 481 assertion = !(clientCfg.localToRemote == [ ] && clientCfg.remoteToLocal == [ ]); 482 message = '' 483 Either one of services.wstunnel.clients."${name}".localToRemote or services.wstunnel.clients."${name}".remoteToLocal must be set. 484 ''; 485 }) cfg.clients); 486 }; 487 488 meta.maintainers = with lib.maintainers; [ 489 alyaeanyx 490 raylas 491 rvdp 492 neverbehave 493 ]; 494}