Skip to content
bsoudanPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

udpdup

A symmetric UDP tunnel that duplicates a stream across every network link that can reach a destination, and deduplicates it on the far side — in both directions. Useful for making a latency-sensitive UDP flow (telemetry, video, control channels) survive the loss of any single link: every packet travels over all available paths, and the other end forwards whichever copy arrives first.

            sender host                              receiver host
 app ◀─udp─▶ udpdup send ══╦═ eth0 ═══╗
                           ╠═ wwan0 ══╬══▶ udpdup recv ◀─udp─▶ app
                           ╚═ wlan0 ══╝      (dedup)

Both directions are duplicated and deduplicated; each direction has its own sequence space.

Modes

send

Listens on a UDP port for the local app. Each received datagram gets a 20-byte header (magic, version, type, random epoch, sequence number) and is transmitted to -dest once per eligible link, using a socket bound to that link with SO_BINDTODEVICE so the kernel cannot collapse everything onto the default route.

A link is used when it is up, running, not loopback (unless explicitly listed in -ifaces), passes the -ifaces/-exclude filters, and has a route to the destination (the per-device connect fails fast when it does not). Links are re-evaluated every -refresh (default 5s), so interfaces coming up or going down are picked up automatically.

Reverse path: packets the far side sends back arrive on the per-link sockets, are deduplicated, and the payload is delivered to the local app's last-seen address on the listen socket. A keepalive is sent over every link each -keepalive interval (default 2s) so the far side always knows the current per-link source addresses — this is what keeps the reverse path working across NATs, link re-dials, and idle periods.

udpdup send -listen :5000 -dest 192.0.2.10:5001
udpdup send -listen 127.0.0.1:5000 -dest 192.0.2.10:5001 -ifaces eth0,wwan0

SO_BINDTODEVICE requires CAP_NET_RAW: run as root, grant the binary the capability (sudo setcap cap_net_raw+ep ./udpdup), or use AmbientCapabilities in the systemd unit as below.

recv

Listens on a UDP port for udpdup traffic, strips the header, drops duplicates, and forwards each unique payload to -forward.

Reverse path: every source address udpdup traffic arrives from (one per sender link) is tracked as a peer, expiring after -peer-timeout (default 15s) of silence. When the local app replies — by writing back to the source address its packets came from, i.e. the forward socket — each reply is headered and transmitted to every live peer, one copy per sender link. The sender deduplicates and delivers to its app.

udpdup recv -listen :5001 -forward 127.0.0.1:6000

Deduplication

Two methods are available via -dedup; both ends must use the same one (a mismatch shows up as a climbing bad_header counter on the seq end, or headers delivered as payload on the other).

-dedup seq (default, exact)

Each packet gets a 20-byte header carrying a random per-run epoch and a sequence number. The receiver keeps a sliding bitmap window of -window sequence numbers (default 8192) behind the highest seen, per epoch — so out-of-order arrival within the window is handled, a restarted peer starts clean, and legitimately repeated payloads (heartbeats, fixed telemetry) are never wrongly dropped. Packets older than the window are dropped as stale.

-dedup hash (transparent, best-effort)

No header: packets travel byte-identical to what the app sent, and the receiver suppresses payloads whose (seeded 64-bit) hash was already seen within -hash-window (default 250ms — cover your worst inter-link delay skew with margin). Keepalives become zero-length datagrams.

This is semantically safe for any app that is correct over plain UDP, because UDP itself already permits duplication and loss: a wrongly suppressed identical repeat is indistinguishable from packet loss, and a copy delayed past the window is ordinary duplication. For traffic that is unique per packet anyway (RTP, QUIC, WireGuard, anything encrypted) it is just as exact as seq mode. Caveats:

  • A steady stream of byte-identical payloads is thinned to roughly one per window (never starved entirely).
  • Protocols that retransmit identical packets for loss recovery (DNS retries, TFTP) can have a retransmit eaten if it lands inside the window; keep the window well below their retransmit timeout.
  • Zero-length app datagrams are treated as keepalives and not carried.

Upside: zero overhead, no MTU loss, and either end interoperates with plain UDP peers.

-dedup fec (exact, plus loss recovery)

Everything seq mode does, plus forward error correction: packets are grouped, and after every k data packets, r Reed-Solomon parity packets are transmitted (-fec k:r, default 4:1). Any k of a group's k+r packets reconstruct the whole group, so up to r data packets whose every link copy was lost are recovered — protection beyond what duplication alone provides, at the cost of r/k extra bandwidth and a 22-byte header.

Latency is unaffected: data packets are forwarded immediately on first arrival, and parity is only consulted when something is missing. Recovered packets are delivered as soon as enough of the group has arrived (so they may be reordered — normal UDP behavior), and a real copy arriving after its packet was recovered is dropped as a duplicate.

If traffic pauses mid-group, the partial group's parity is flushed after -fec-flush (default 20ms; 0 disables) so the last packets before an idle period are still protected. The group shape rides in every header, so the receive direction adapts to the sender's -fec setting automatically; each side's own flag governs the direction it transmits.

Pick k:r by loss pattern: 4:1 tolerates one loss per 4-packet group (25% overhead); 8:2 the same average overhead but survives two losses in a burst. Larger groups amortize overhead better but need more packets in flight before recovery can happen.

Notes

  • Per-packet overhead: seq 20 bytes, fec 22 bytes (24 for the largest payload's parity), hash zero. Oversized datagrams are dropped and counted; keep payloads under the path MTU minus the overhead to avoid fragmentation.
  • Packets are forwarded in arrival order, not sequence order; udpdup removes duplicates but does not reorder. UDP consumers should already tolerate reordering.
  • Reverse delivery on the send side goes to the last local address that sent to the listen port — one local app per send instance.
  • The app behind recv must reply to the source address its packets arrive from (normal request/response UDP behavior).
  • Both modes log counters every -stats interval (default 30s; 0 disables) covering both directions: received, copies sent, forwarded, duplicates, stale/malformed, and drops with no usable link/peer/app.

Build & test

go build -o udpdup .
go test ./...

The test suite includes a full bidirectional pipeline test over loopback; it is skipped automatically where SO_BINDTODEVICE needs privileges the test run doesn't have.

systemd

/etc/systemd/system/udpdup-send.service on the sender:

[Unit]
Description=udpdup sender
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/udpdup send -listen 127.0.0.1:5000 -dest 192.0.2.10:5001
AmbientCapabilities=CAP_NET_RAW
DynamicUser=yes
Restart=on-failure

[Install]
WantedBy=multi-user.target

/etc/systemd/system/udpdup-recv.service on the receiver:

[Unit]
Description=udpdup receiver
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/udpdup recv -listen :5001 -forward 127.0.0.1:6000
DynamicUser=yes
Restart=on-failure

[Install]
WantedBy=multi-user.target

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages