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