/systemd-transparent-udp-forwarderd

Transparent socket-activated UDP Proxy/Forwarder

Primary LanguageC

Transparent socket-activated UDP Proxy/Forwarder for systemd

Written as quick workaround to systemd-socket-proxyd not forwarding UDP datagrams.

Caveats

  • Does not convert between IPv4 and IPv6.
    The family of incoming and outgoing sockets/addresses must match.
    (Just run this a second time to get both families covered.)
  • The maximum permissible payload size is hard-coded, and any larger than that gets silently discarded.

If you don't need the socket-activation you will be better served with a DSTNAT/RNAT scheme and nftables (or tc filter … nat).

Other than that it's fine for all use-cases I've encountered.

Requirements

  • systemd v221 or later (tested with v231)
  • Linux 3.10.0 or later
  • GCC to compile this

Compile

The usual gcc -D_GNU_SOURCE -Os -o systemd-transparent-udp-forwarderd *.c -lsystemd, or use cmake:

mkdir build && cd $_ && \
CFLAGS="-march=silvermont -mtune=intel" cmake -GNinja .. && \
ninja -v

# or, if you prefer make:
mkdir build && cd $_ && \
CFLAGS="-march=silvermont -mtune=intel" cmake .. && \
make

Run

We will use Avorion – a random game – in this example (find its service unit below), which expects UDP packets on ports 27000, 27003, 27020, and 27021; but cannot be socket-activated.

Unlike systemd-socket-proxyd, systemd-transparent-udp-forwarderd can handle more than one socket: (Systemd will listen on them to see when to proceed, but eventually relinquish control of those sockets to this UDP proxy.)

# proxy-to-avorion-udp.socket
[Socket]
ListenDatagram=0.0.0.0:27000
ListenDatagram=0.0.0.0:27003
ListenDatagram=0.0.0.0:27020
ListenDatagram=0.0.0.0:27021
Transparent=true

[Install]
WantedBy=sockets.target

… which will start, by systemd's convention, following similarly named service on incoming datagrams. That service in turn binds to another, and therefore makes systemd schedule its start as well.

# proxy-to-avorion-udp.service
[Unit]
BindsTo=avorion-server.service
After=avorion-server.service

[Service]
Type=notify
ExecStart=/opt/sbin/systemd-transparent-udp-forwarderd \
  172.16.28.240:27000 \
  172.16.28.240:27003 \
  172.16.28.240:27020 \
  172.16.28.240:27021

Avorion's service file looks like this (excerpt). The corresponding container address is the important part:

# avorion-server.service
[Service]
ExecStart=/usr/bin/rkt run \
  --dns=host --net="ptp0:IP=172.16.28.240" \
  blitznote.com/aci/avorion-server

Please Note

If you do not use any containers, i.e. the program runs on your host or with --net=host, this section does not apply and you can skip reading it.

Incoming packets forwarded to the container will not appear to have been sent from the host. They will retain their original source address; and any response will be sent to them directly and not go through this forwarder by design.

Now, whatever is in the container will have an address specific to it, and needs to be changed to the host's. Remember to configure SNAT/MASQUERADE rules to modify the source address of those outgoing packets. (If you forgot this, your server will emit packets that won't have your host's as source, which in turn will most likely result in them being suppressed by your DC/network operator on account of appearing to have been forged or being non-routable.) Kubernetes and other orchestration tools do set those rules automatically.

See for example tc … action nat egress 172.16.28.240 <public host IPv4>.