udp-over-tcp: init at 0.4.0 (+ nixos/udp-over-tcp) (#418826)
This commit is contained in:
@@ -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}
|
||||
|
||||
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
|
||||
|
||||
@@ -1404,6 +1404,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
|
||||
|
||||
@@ -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 ];
|
||||
}
|
||||
@@ -1594,6 +1594,7 @@ in
|
||||
tzupdate = runTest ./tzupdate.nix;
|
||||
ucarp = runTest ./ucarp.nix;
|
||||
udisks2 = runTest ./udisks2.nix;
|
||||
udp-over-tcp = runTest ./udp-over-tcp.nix;
|
||||
ulogd = runTest ./ulogd/ulogd.nix;
|
||||
umami = runTest ./web-apps/umami.nix;
|
||||
umurmur = runTest ./umurmur.nix;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
{ lib, ... }:
|
||||
{
|
||||
name = "udp-over-tcp";
|
||||
meta.maintainers = [ lib.maintainers.timschumi ];
|
||||
|
||||
nodes.sender =
|
||||
{ nodes, ... }:
|
||||
{
|
||||
services.udp-over-tcp = {
|
||||
udp2tcp = {
|
||||
"0.0.0.0:51821" = {
|
||||
forward = "${nodes.receiver.networking.primaryIPAddress}:444";
|
||||
openFirewall = true;
|
||||
|
||||
# Remaining options are not tested for behavior, but to cover options passing.
|
||||
recvBufferSize = 16384;
|
||||
sendBufferSize = 16384;
|
||||
recvTimeout = 10;
|
||||
fwmark = 1337;
|
||||
nodelay = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
nodes.receiver =
|
||||
{ nodes, ... }:
|
||||
{
|
||||
services.udp-over-tcp = {
|
||||
tcp2udp = {
|
||||
"0.0.0.0:444" = {
|
||||
forward = "127.0.0.1:51821";
|
||||
openFirewall = true;
|
||||
|
||||
# Remaining options are not tested for behavior, but to cover options passing.
|
||||
threads = 2;
|
||||
bind = "127.0.0.1";
|
||||
recvBufferSize = 16384;
|
||||
sendBufferSize = 16384;
|
||||
recvTimeout = 10;
|
||||
fwmark = 1337;
|
||||
nodelay = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
# TODO: Replace unit wait with waiting on an UDP port.
|
||||
sender.wait_for_unit("udp2tcp-0.0.0.0:51821.service")
|
||||
receiver.wait_for_open_port(444, "0.0.0.0")
|
||||
|
||||
receiver.succeed("nc -w 10 -u -l 127.0.0.1 51821 > transfer.txt &")
|
||||
# We need to delay a short time here because detaching exits the shell before the socket is ready.
|
||||
receiver.execute("sleep 1")
|
||||
sender.succeed("echo 'Hello World!' | nc -w 1 -u 127.0.0.1 51821")
|
||||
receiver.succeed("grep 'Hello World!' transfer.txt")
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
fetchFromGitHub,
|
||||
lib,
|
||||
nixosTests,
|
||||
rustPlatform,
|
||||
}:
|
||||
rustPlatform.buildRustPackage (finalAttrs: {
|
||||
pname = "udp-over-tcp";
|
||||
version = "0.4.0";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "mullvad";
|
||||
repo = "udp-over-tcp";
|
||||
tag = "v${finalAttrs.version}";
|
||||
hash = "sha256-nlLo5/CpieBuRpQyd0eybfkltZyt4qeiupkbtfL/qWE=";
|
||||
};
|
||||
|
||||
cargoHash = "sha256-s72C+7q56dSwrmkUBy871rF1MvPkhg8780S+dN/ETh0=";
|
||||
cargoBuildFlags = [
|
||||
"--bins"
|
||||
"--features"
|
||||
"clap"
|
||||
];
|
||||
|
||||
passthru.tests = {
|
||||
inherit (nixosTests) udp-over-tcp;
|
||||
};
|
||||
|
||||
meta = {
|
||||
homepage = "https://github.com/mullvad/udp-over-tcp";
|
||||
description = "Proxy UDP traffic over a TCP stream";
|
||||
license = with lib.licenses; [
|
||||
asl20
|
||||
mit
|
||||
];
|
||||
maintainers = with lib.maintainers; [ timschumi ];
|
||||
# No single mainProgram is listed here because tcp2udp and udp2tcp are equally important.
|
||||
};
|
||||
})
|
||||
Reference in New Issue
Block a user