From b5fff63550c857553dee65766d4eb7514c10e088 Mon Sep 17 00:00:00 2001 From: Tim Schumacher Date: Thu, 26 Jun 2025 23:55:52 +0200 Subject: [PATCH] nixos/udp-over-tcp: init --- .../manual/release-notes/rl-2605.section.md | 2 + nixos/modules/module-list.nix | 1 + .../services/networking/udp-over-tcp.nix | 281 ++++++++++++++++++ 3 files changed, 284 insertions(+) create mode 100644 nixos/modules/services/networking/udp-over-tcp.nix diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index ab81b72aaa61..9cfb4e223933 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -12,6 +12,8 @@ - [knot-resolver](https://www.knot-resolver.cz/) in version 6. Available as `services.knot-resolver`. A module for knot-resolver 5 was already available as `services.kresd`. +- [udp-over-tcp](https://github.com/mullvad/udp-over-tcp), a tunnel for proxying UDP traffic over a TCP stream. Available as `services.udp-over-tcp`. + ## Backward Incompatibilities {#sec-release-26.05-incompatibilities} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 7d79b5132253..10910a203f74 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1403,6 +1403,7 @@ ./services/networking/trickster.nix ./services/networking/twingate.nix ./services/networking/ucarp.nix + ./services/networking/udp-over-tcp.nix ./services/networking/umurmur.nix ./services/networking/unbound.nix ./services/networking/unifi.nix diff --git a/nixos/modules/services/networking/udp-over-tcp.nix b/nixos/modules/services/networking/udp-over-tcp.nix new file mode 100644 index 000000000000..d4a63406f1e4 --- /dev/null +++ b/nixos/modules/services/networking/udp-over-tcp.nix @@ -0,0 +1,281 @@ +{ + config, + lib, + pkgs, + ... +}: +let + inherit (lib) + attrNames + escapeShellArgs + filterAttrs + getExe' + last + literalExpression + maintainers + mapAttrs' + mkOption + mkPackageOption + optionals + splitString + toInt + types + ; + + cfg = config.services.udp-over-tcp; + + commonOptions = { + openFirewall = mkOption { + type = types.bool; + default = false; + description = '' + Open the appropriate ports in the firewall. + ''; + }; + + # Options and descriptions as indicated by `tcp2udp --help` and `udp2tcp --help`. + forward = mkOption { + type = types.str; + description = '' + The IP and port to forward all traffic to. + ''; + }; + recvBufferSize = mkOption { + type = types.nullOr types.ints.positive; + description = '' + If given, sets the SO_RCVBUF option on the TCP socket to the given number of bytes. + Changes the size of the operating system's receive buffer associated with the socket. + ''; + default = null; + }; + sendBufferSize = mkOption { + type = types.nullOr types.ints.positive; + description = '' + If given, sets the SO_SNDBUF option on the TCP socket to the given number of bytes. + Changes the size of the operating system's send buffer associated with the socket. + ''; + default = null; + }; + recvTimeout = mkOption { + type = types.nullOr types.ints.positive; + description = '' + An application timeout on receiving data from the TCP socket. + ''; + default = null; + }; + fwmark = mkOption { + type = types.nullOr types.ints.u32; + description = '' + If given, sets the SO_MARK option on the TCP socket. + ''; + default = null; + }; + nodelay = mkOption { + type = types.bool; + description = '' + Enables TCP_NODELAY on the TCP socket. + ''; + default = false; + }; + }; + tcp2udpSubmodule = { + options = commonOptions // { + threads = mkOption { + type = types.nullOr types.ints.positive; + description = '' + Sets the number of worker threads to use. + The default value is the number of cores available to the system. + ''; + default = null; + }; + bind = mkOption { + type = types.nullOr types.str; + description = '' + Which local IP to bind the UDP socket to. + ''; + default = null; + }; + }; + }; + udp2tcpSubmodule = { + options = commonOptions; + }; + + configToService = type: buildCmdline: listen: conf: { + name = "${type}-${listen}"; + value = { + description = "${type} tunnel from ${listen} to ${conf.forward}"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + reloadIfChanged = true; + + serviceConfig = { + Type = "exec"; + ExecStart = "${getExe' cfg.package type} " + escapeShellArgs (buildCmdline listen conf); + + DynamicUser = true; + User = "udp-over-tcp"; + + # CAP_NET_BIND_SERVICE in case we are binding to ports < 1024, CAP_NET_ADMIN only covers addresses. + # CAP_NET_ADMIN for setting SO_MARK on the socket. + AmbientCapabilities = [ + "CAP_NET_ADMIN" + "CAP_NET_BIND_SERVICE" + ]; + CapabilityBoundingSet = [ + "CAP_NET_ADMIN" + "CAP_NET_BIND_SERVICE" + ]; + + Restart = "on-failure"; + RestartSec = 10; + + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateMounts = true; + PrivateTmp = true; + PrivateUsers = false; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = "@system-service"; + UMask = "077"; + }; + }; + }; + + buildCommonCmdline = + listen: conf: + optionals (conf.recvBufferSize != null) [ + "--recv-buffer" + conf.recvBufferSize + ] + ++ optionals (conf.sendBufferSize != null) [ + "--send-buffer" + conf.sendBufferSize + ] + ++ optionals (conf.recvTimeout != null) [ + "--tcp-recv-timeout" + conf.recvTimeout + ] + ++ optionals (conf.fwmark != null) [ + "--fwmark" + conf.fwmark + ] + ++ optionals conf.nodelay [ + "--nodelay" + ]; + buildTcp2udpCmdline = + listen: conf: + [ + "--tcp-listen" + listen + "--udp-forward" + conf.forward + ] + ++ optionals (conf.threads != null) [ + "--threads" + conf.threads + ] + ++ optionals (conf.bind != null) [ + "--udp-bind" + conf.bind + ] + ++ buildCommonCmdline listen conf; + buildUdp2tcpCmdline = + listen: conf: + [ + "--udp-listen" + listen + "--tcp-forward" + conf.forward + ] + ++ buildCommonCmdline listen conf; + + getFirewallPorts = + instances: + map (e: toInt (last (splitString ":" e))) ( + attrNames (filterAttrs (_: e: e.openFirewall) instances) + ); +in +{ + options.services.udp-over-tcp = { + package = mkPackageOption pkgs "udp-over-tcp" { }; + tcp2udp = mkOption { + type = types.attrsOf (types.submodule tcp2udpSubmodule); + example = literalExpression '' + { + "0.0.0.0:443" = { + forward = "127.0.0.1:51820"; + openFirewall = true; + }; + "0.0.0.0:444" = { + threads = 2; + forward = "127.0.0.1:51821"; + bind = "127.0.0.1"; + recvBufferSize = 16384; + sendBufferSize = 16384; + recvTimeout = 10; + fwmark = 1337; + nodelay = true; + }; + } + ''; + description = '' + Mapping of TCP listening ports to UDP forwarding ports or configurations. + ''; + default = { }; + }; + udp2tcp = mkOption { + type = types.attrsOf (types.submodule udp2tcpSubmodule); + example = literalExpression '' + { + "0.0.0.0:51820" = { + forward = "10.0.0.1:443"; + openFirewall = true; + }; + "0.0.0.0:51821" = { + forward = "10.0.0.1:444"; + recvBufferSize = 16384; + sendBufferSize = 16384; + recvTimeout = 10; + fwmark = 1337; + nodelay = true; + }; + } + ''; + description = '' + Mapping of UDP listening ports to TCP forwarding ports or configurations. + ''; + default = { }; + }; + }; + + config = { + systemd.services = + (mapAttrs' (configToService "tcp2udp" buildTcp2udpCmdline) cfg.tcp2udp) + // (mapAttrs' (configToService "udp2tcp" buildUdp2tcpCmdline) cfg.udp2tcp); + + networking.firewall.allowedTCPPorts = getFirewallPorts cfg.tcp2udp; + networking.firewall.allowedUDPPorts = getFirewallPorts cfg.udp2tcp; + }; + + meta.maintainers = with maintainers; [ timschumi ]; +}