Universal Relay

Universal Relay can be used as a gateway for an arbitrarily complex network topology.

Universal Relay is a simple TCP->TCP relay for (isolated) network namespaces. It supports a variety of modes of operation, such as tunneling all traffic through a SOCKS server, as well as custom routing of network destinations.

Universal Relay was originally intended to be a means of causing all network traffic in a network namespace to be tunneled through a SOCKS proxy (such as the one created by ssh -D). Similar in overall spirit to slirp4netns, but instead of creating a TUN device and using a userspace TCP/IP stack, we reuse the kernel's TCP/IP stack using the following redirection in the newly created network namespace:

ip link set lo up
ip route add local 0.0.0.0/0 dev lo table main
ip route add local ::/0 dev lo table main

and use ctrtool ns_open_file to create listening sockets in the network namespace.

TCP/IP connections are read from the namespace's listening sockets; when a new connection is made, the destination is read (using the same technique as previously used in IPv6 Things and Socketbox) and translated into SOCKS5 CONNECT commands on the host.

Unlike slirp4netns, Universal Relay specifically accounts for the fact that there might be a whole network that it should be applied to, so you can create subnets, static routes, and even BGP sessions in that network namespace to other network namespaces to make all your VMs and (rootless) Docker containers use it, for example. It's also a very effective way to experiment with various routing protocols (BGP, OSPF, RIP, etc.) using network namespaces. Unlike a traditional address translator, this is possible even if the IP addresses conflict with the host network.

Also intended to be a NAT64 CLAT / PLAT by rewriting IPv4 destinations into their IPv6 equivalent with NAT64 prefix, or vice versa. Quite difficult to do on the layer 3, but rather trivial on the application layer. Unix domain socket destinations will also be supported.

Also intended to be used in conjunction with a protocol that will be developed for use with Socket Enhancer, to take "transparent proxying" off the latter's to do list.

Also intended to be a more flexible and general-purpose version of Snippets:Node.js NAT64 TCP relay, inet-relay, and socketbox-relay.

TCP port forwarding as implemented in slirp4netns will not be supported in Universal Relay itself due to fundamental complexities, as Node.js would need to be able to create sockets in multiple network namespaces (which it doesn't really support). Instead, the recommended solution is to use ctrtool ns_open_file or socketbox to create a listening socket in the host network namespace, then use HAProxy (or a similar tool) in the foreign network namespace with bind fd@${CTRTOOL_NS_OPEN_FILE_FD_n} (if using ctrtool ns_open_file) or bind sockpair@${CTRTOOL_NS_OPEN_FILE_FD_n} (if using Socketbox) in a frontend to relay data from the host network namespace over to the foreign namespace.

The name "Universal Relay" comes from the fact that it is technically a socket relay (similar to socat); the term "universal" refers to the fact that it can handle relaying for the entire Internet, or "universe", unlike socat which can only handle one IP address and port, which makes socat useful only for relaying a single service.

With the "transparent_server" enabled, Universal Relay is technically still a NAT device, since it "collapses" a series of private IP addresses into a single IPv4 and IPv6 address, though it still strives to be a much better version of e.g. the NAT in iptables. For example, instead of sending the network traffic directly out into the outbound/WAN interface, it can arbitrarily rewrite network destinations to other places of choosing, such as through a SOCKS server or a Unix domain socket, or a different IP address (including (untested) a link-local IPv6 address), or even from IPv4 to IPv6 or vice versa (there is an ipRewrite module that implements the equivalent of NAT64/464XLAT).

Other advantages of Universal Relay over iptables include:

  • No root access required on host, as long as you can create a new user and network namespace (e.g. with unshare -r -n); very useful for "rootless" containers.
  • No need to have iptables support in your kernel (except for the TPROXY target, which is recommended, but not required.)
  • Logging of connections (for auditing purposes); if you don't like this, just redirect stdout to /dev/null.
  • Implemented in a way that actually blocks inbound connections to the private-side network.
  • Reuses kernel TCP/IP stack for speed, simplicity and security.
  • Deals with IP address conflicts gracefully (i.e. the relaying on the host is as if the private-side network were to be completely disregarded)
  • No raw network protocol access from the private side (this prevents header spoofing in case there are untrusted clients on the private side)
  • When used with Socket Enhancer, it is possible to set the source IP address for individual outbound connections to Internet destinations on-the-fly; this can be combined with policy-based routing.
  • Can still be used with only a subset of the IPv4/IPv6 address space (useful if there is already a default gateway on the private-side network, and Universal Relay is used primarily for address rewriting or NAT64 purposes); in this case, you would set up a (static) route covering that subset of address space that goes to the network namespace associated with Universal Relay's listening socket, and all clients that wish to use it would have to recognize that route (either directly, by adding the route on the device itself, or indirectly, by adding the route on the device's default gateway).

Disadvantages include:

  • Only relays TCP. UDP is not supported.
- For DNS, this is worked around by (untested) putting Unbound on the private side that listens on both TCP and UDP and forwards all DNS requests to the upstream server (through Universal Relay) over TCP. Alternatively, for a single host, it is possible to put options use-vc in /etc/resolv.conf.
- For NTP, this is worked around by (untested) putting an NTP server that has 127.127.1.0 (local/undisciplined clock) as a clock source on the private side, but which is on the same host as Universal Relay itself, and provided that the host already has correct time synchronization (it will appear to clients as a stratum 1 server with refid .LOCL.).
  • Not possible to pass through special TCP options, sequence number offsets, or urgent data.
  • Pings on the private side will appear to always respond, even if the ultimate target host on the Internet does not. This also means that traceroutes to Internet destinations from the private side will only see the private-side routers, and a traceroute to an Internet-side Traceroute Text Generator will not show the actual traceroute text.
  • No loop detection (the TTL does not decrement for connections that pass through Universal Relay; rather, it is reset to the host's default.)
  • May be a little bit slower and take up more memory (due to socket buffers and bookkeeping) than iptables connection tracking, which is in-kernel. An iperf test on the local host shows speeds of about 6-7 Gbps.
  • Can only forward a discrete set of port numbers (if not using TPROXY; TPROXY is still available even on rootless containers (if using iptables-nft) because it only requires CAP_NET_ADMIN in the user-namespace-owned network namespace, and not on the host); without TPROXY, the suggested starting list of port numbers to allow is: 53,80,443,853,993,8080. TPROXY is still preferred, however.

The TPROXY redirection is as follows, to be done in the same network namespace as Universal Relay's listening sockets:

iptables-nft -t mangle -A PREROUTING ! -i lo -p tcp -j TPROXY --on-ip 127.0.0.1 --on-port 53
ip6tables-nft -t mangle -A PREROUTING ! -i lo -p tcp -j TPROXY --on-ip ::ffff:127.0.0.1 --on-port 53

Note that this redirection only applies to other network namespaces and physical or virtual clients connected to the namespace through veth or tun/tap devices. It does not apply to the original network namespace itself; you can remove ! -i lo if you'd like, but this could result in potential glitches; if not, then you will have to bind each port number explicitly to intercept connections in the same network namespace as the listening sockets.

Hint for development: The SOCKS server created by the OpenSSH client (when using -D) supports Unix domain sockets on the client side:

ssh user@host -D /home/sebastian/gitprojects/universal-relay/urelay.sock

(Obviously, if your name is not Sebastian, then you would have to change that path :))

TODO: implement address rewriting / ACLs i.e. ipRewrite(). Need to move all opaque identifiers in the "dest" object to one single property of that object. SOCKS server with domain containing an IPv6 address enclosed in square brackets does not currently work.