CLI tool
mullvad/udp-over-tcp avatar
mullvad/udp-over-tcp

udp-over-tcp: a small Rust library for carrying UDP datagrams across a TCP stream

Proxy UDP traffic over a TCP stream

588 stars84 forksRustApache-2.0

At a glance

What is it?
udp-over-tcp is a Rust library and a pair of binaries from Mullvad that tunnels UDP datagrams inside a TCP stream, using a two byte big endian length prefix per datagram. One half connects a UDP socket to a TCP peer, the other accepts TCP connections and forwards them to a UDP service, and the whole thing is small enough to read in an evening.
Who is it for?
udp-over-tcp suits a developer whose protocol only speaks UDP and whose network only permits TCP, and who wants a well defined framing rather than a home grown one, especially since Mullvad ships it as part of a VPN stack. It does not suit traffic that needs the latency characteristics of real UDP, because the TCP stream underneath will head of line block on a loss, and it is not published to crates.io, so you would vendor it.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 67 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Two halves that meet at a length prefix

udp-over-tcp is a library and binaries for tunneling UDP datagrams over a TCP stream, and it exists because of a mismatch that shows up in practice. Some programs and protocols only work over UDP, and some networks only allow TCP.

The library comes in two parts and the split is the design.

`udp2tcp` forwards incoming UDP datagrams over a TCP stream. The return stream is translated back to datagrams and sent back out over UDP again. This part can be easily used as both a library and a binary, so it can be run standalone, but can also easily be included in other Rust programs. The important constraint is that the UDP socket is connected to the peer address of the first incoming datagram, so one `Udp2Tcp` instance can handle traffic from a single peer only.

`tcp2udp` accepts connections over TCP and translates and forwards the incoming stream as UDP datagrams to the destination specified during setup or on the command line. It is designed mostly to be a standalone executable to run on servers, but can be consumed as a Rust library as well. It continues to accept new incoming TCP connections and creates a new UDP socket for each, so a single `tcp2udp` server can service many `udp2tcp` clients.

That asymmetry is the part to understand before deploying anything. The client side is a one to one connection, because a connected UDP socket has a single peer. The server side is a many to one listener, because it opens a fresh socket per incoming TCP connection.

The framing is as simple as it can be while still being unambiguous. Each datagram is preceded with a 16 bit unsigned integer in big endian byte order, specifying the length of the datagram. There is no magic number, no version field and no checksum, which means a stream can be parsed by anything that can read two bytes of length prefix, and it also means the protocol has no way to recover from a corrupted length.

Running the server half from a shell

The server side is the one you run on a machine, and the readme gives a complete example.

Make the server listen for TCP connections that it can then forward to a local UDP service, forwarding anything that comes in to 127.0.0.1:51820 over UDP:

bash
user@server $ RUST_LOG=debug tcp2udp \
    --tcp-listen 10.0.0.0:5001 \
    --udp-forward 127.0.0.1:51820

Two environment variables control the logging, and both are worth knowing about.

`RUST_LOG` sets the logging level, and the readme points at the env_logger documentation for how to use it. There is a condition, though: the crate must be built with the `env_logger` feature for this to be active. That is because env_logger is an optional dependency, so a library consumer who does not enable it gets no logging at all from this path, which is the right default for a library but does mean the variable silently does nothing otherwise.

`REDACT_LOGS=1` redacts the IPs of the peers using the service from the logs. The stated reason is that it allows having logging turned on but without storing potentially user sensitive data to disk. For a VPN operator, the peer addresses in the logs are subscriber data, and this is the switch that makes it possible to run with debug logging without writing customer addresses to a disk.

The two logging variables together cover the two cases: verbose when you are debugging your own deployment, redacted when the deployment is in production.

Embedding the client half in a Rust program

The client side is the one you embed, and the readme shows a complete integration.

This will connect a TCP socket to 1.2.3.4:9000 and bind a UDP socket to a random port on the loopback interface. It will then connect the UDP socket to the socket address of the first incoming datagram and start forwarding all traffic to and from the TCP socket:

rust

let udp_listen_addr = "127.0.0.1:0".parse().unwrap();
let tcp_forward_addr = "1.2.3.4:9000".parse().unwrap();

// Create a UDP -> TCP forwarder. This will connect the TCP socket
// to `tcp_forward_addr`
let udp2tcp = udp_over_tcp::Udp2Tcp::new(
    udp_listen_addr,
    tcp_forward_addr,
    udp_over_tcp::TcpOptions::default(),
)
.await?;

// Read out which address the UDP actually bound to. Useful if you specified port
// zero to get a random port from the OS.
let local_udp_addr = udp2tcp.local_udp_addr()?;

spin_up_some_udp_thing(local_udp_addr);

// Run the forwarder until the TCP socket disconnects or an error happens.
udp2tcp.run().await?;

The interesting part is the ordering. The forwarder is constructed first and only then does the code ask which address the UDP socket actually bound to, which is the pattern to copy when the port is zero and the real port comes from the operating system. A program that needs to know the port to advertise it has to ask the forwarder rather than assume it.

The API is async throughout, with the constructor awaited and the run loop awaited, and the run loop is documented as continuing until the TCP socket disconnects or an error happens. The options argument is a TcpOptions value with a default constructor, so the defaults are the configuration you get unless you say otherwise.

The library exposes the local address as a fallible call returning a Result, which is the shape to expect for anything that touches a socket, and the whole flow is three calls: construct, ask for the address, run.

A small crate with a careful build profile

The package manifest shows a project that treats its build and its features carefully, and a few decisions there are worth reading.

The crate is named udp-over-tcp at version 0.4.0, authored by Mullvad VPN, licensed MIT OR Apache-2.0, on edition 2024 with a rust-version floor of 1.87.0. The most consequential line is `publish = false`, which means the crate is not published to crates.io. If you want to use it, you vendor it or point at the git repository, and that is a deliberate choice by a vendor whose dependencies are part of its own product.

Both binaries are behind a feature gate. The `tcp2udp` and `udp2tcp` binaries each require the `clap` feature, and clap is an optional dependency with the derive feature enabled. The optional dependencies are also a performance choice: env_logger and cadence are marked optional so that they are not pulled in when the crate is built as a library, with a comment in the manifest saying exactly that.

The `statsd` feature enables the tcp2udp binary reporting metrics over statsd, and it pulls in cadence. For a VPN operator running thousands of tunnels, per tunnel counters are a real requirement, and making it a feature means a library consumer does not pay for it.

The release profile is tuned. opt-level 3 with lto enabled and codegen-units set to 1 is the maximum optimisation configuration, which costs build time and buys a smaller and faster binary. For a network path where latency is the product, that is the right trade.

The runtime dependencies are minimal. Tokio provides the multi-threaded runtime with the macros, net, time and io-util features, the log crate does the logging, futures brings the async combinators, and err-context adds error context. The Linux-specific target section adds nix with the socket feature, which is how the crate gets at the socket level on Linux specifically.

The repository also carries a tcp2udp.service file, which is a systemd unit for running the server side, and a build-static-bins.sh script, which suggests the release artefacts include statically linked binaries. There is a CHANGELOG.md, a tests/ directory, and both LICENSE-APACHE and LICENSE-MIT at the root.

Editorial conclusion

udp-over-tcp suits a developer whose protocol only speaks UDP and whose network only permits TCP, and who wants a well defined framing rather than a home grown one, especially since Mullvad ships it as part of a VPN stack. It does not suit traffic that needs the latency characteristics of real UDP, because the TCP stream underneath will head of line block on a loss, and it is not published to crates.io, so you would vendor it. Before building on it, read the framing section, check whether your protocol tolerates a stream rather than datagrams, and decide which side of the tunnel you want to run, since the two halves have quite different deployment shapes.

Frequently asked questions

What is udp-over-tcp?

It is a Rust library and a pair of binaries for tunneling UDP datagrams over a TCP stream. udp2tcp forwards incoming UDP datagrams over TCP and translates the return stream back to datagrams, while tcp2udp accepts TCP connections and forwards them as UDP datagrams to a configured destination.

How is the data framed inside the TCP stream?

Each datagram is preceded with a 16 bit unsigned integer in big endian byte order, specifying the length of the datagram. There is no magic number, version field or checksum, so a corrupted length cannot be recovered from.

How do I run the tcp2udp server?

Run the binary with the listen and forward addresses, for example RUST_LOG=debug tcp2udp with --tcp-listen 10.0.0.0:5001 and --udp-forward 127.0.0.1:51820. The RUST_LOG variable sets the log level and requires the crate to be built with the env_logger feature, and REDACT_LOGS=1 redacts peer IP addresses from the logs.

Can I use udp-over-tcp as a library from crates.io?

No. The Cargo manifest sets publish to false, so the crate is not published to crates.io and has to be vendored or used from the git repository. The version is 0.4.0, it is licensed MIT OR Apache-2.0, and it requires Rust 1.87 or newer.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. mullvad/udp-over-tcp on GitHub
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/mullvad-udp-over-tcp.svg)](https://hysenlabs.com/projects/mullvad-udp-over-tcp)

Was this useful?