There are cases, when you need to change the source ip address of a client, but can't do it in the kernel. Either because you don't have a firewall running or because it doesn't work, or because there's no firewall available.
In such cases the usual aproach is to use a port forwarder. This is a small piece of software, which opens a port on the inside, visible to the client and establishes a new connection to the intended destination, which is otherwise unreachable for the client. In fact, a port forwarder implements hiding nat or masquerading in userspace. Tools like this are also usefull for testing things, to simulate requests when the tool you need to use for the test are unable to set their source ip address (binding address).
For TCP there are LOTs of solutions available, e.g. tcpxd. Unfortunately there are not so many solutions available if you want to do port forwarding with UDP. One tool I found was udp-redirect. But it is just a dirty hack and didn't work for me. Also it doesn't track clients and is therefore not able to handle multiple clients simultaneusly.
So I decided to enhance it. The - current - result is udpxd, a "general purpose UDP relay/port forwarder/proxy". It supports multiple concurrent clients, it is able to bind to a specific source ip address and it is small and simple. Just check out the repository, enter "make" and "sudo make install" and you're done.
How does it work: I'll explain it on a concrete example. On the server where this website is running there are multiple ip addresses configured on the outside interface, because I'm using jails to separate services. One of those jail ip addresses is 18.104.22.168. Now if I want to send a DNS query to the Hetzner nameserver 22.214.171.124 from the root system (not inside the jail with the .33 ip address!), then the packet will go out with the first interface address of the system. Let's say I don't want that (either for testing or because the remote end does only allow me to use the .33 address). In this szenario I could use udpxd like this:
udpxd -l 172.16.0.3:53 -b 126.96.36.199 -t 188.8.131.52:53
The ip address 172.16.0.3 is configured on the loopback interface lo0. Now I can use dig to send a dns query to 172.16.0.3:53:
dig +nocmd +noall +answer google.de a @172.16.0.3 google.de. 135 IN A 184.108.40.206 google.de. 135 IN A 220.127.116.11 google.de. 135 IN A 18.104.22.168 google.de. 135 IN A 22.214.171.124
When we look with tcpdump on our external interface, we see:
IP 126.96.36.199.24239 > 188.8.131.52.53: 4552+ A? google.de. (27) IP 184.108.40.206.53 > 220.127.116.11.24239: 4552 4/0/0 A 18.104.22.168, A 22.214.171.124, A 126.96.36.199, A 188.8.131.52 (91)
And this is how the same request looks on the loopback interface:
IP 172.16.0.3.24239 > 172.16.0.3.53: 4552+ A? google.de. (27) IP 172.16.0.3.53 > 172.16.0.3.24239: 4552 4/0/0 A 184.108.40.206, A 220.127.116.11, A 18.104.22.168, A 22.214.171.124 (91)
As you can see, dig sent the packet to 172.16.0.3:53, udpxd took it, opened a new outgoing socket, bound to 126.96.36.199 and sent the packet with that source ip address to the hetzner nameserver. It also remembered which socket the particular client (source ip and source port) has used. Then the response came back from hetzner, udpxd took it, looked up its internal cache for the matching internal client and sent it back to it with the source ip on the loopback interface.
As I already said, udpxd is a general purpose udp port forwarder, so you can use it with almost any udp protocol. Here's another example using ntp: now I set up a tunnel between two systems, because udpxd cannot bind to any interface with port 123 while ntpd is running (ntpd binds to *.123). So on system A I have 3 udpxd's running:
udpxd -l 172.17.0.1:123 -b 188.8.131.52 -t 184.108.40.206:123 udpxd -l 172.17.0.2:123 -b 220.127.116.11 -t 18.104.22.168:123 udpxd -l 172.17.0.3:123 -b 22.214.171.124 -t 126.96.36.199:123
Here I forward ntp queries on 172.16.0.1-3:123 to the real ntp pool servers and I'm using the .33 source ip to bind for outgoing packets again. On the other system B, which is able to reach 172.17.0.0/24 via the tunnel, I reconfigured the ntpd like so:
server 172.17.0.1 iburst dynamic server 172.17.0.2 iburst dynamic server 172.17.0.3 iburst dynamic
After restarting, let's look how it works:
ntpq> peers remote refid st t when poll reach delay offset jitter ============================================================================== *172.17.0.1 188.8.131.52 2 u 12 64 1 18.999 2.296 1.395 172.17.0.2 184.108.40.206 2 u 11 64 1 0.710 1.979 0.136 172.17.0.3 220.127.116.11 2 u 10 64 1 12.073 5.836 0.089
Seems to work :), here's what we see in the tunnel interface:
14:13:32.534832 IP 10.10.10.1.123 > 172.17.0.1.123: NTPv4, Client, length 48 14:13:32.556627 IP 172.17.0.1.123 > 10.10.10.1.123: NTPv4, Server, length 48 14:13:33.535081 IP 10.10.10.1.123 > 172.17.0.2.123: NTPv4, Client, length 48 14:13:33.535530 IP 172.17.0.2.123 > 10.10.10.1.123: NTPv4, Server, length 48 14:13:34.535166 IP 10.10.10.1.123 > 172.17.0.1.123: NTPv4, Client, length 48 14:13:34.535278 IP 10.10.10.1.123 > 172.17.0.3.123: NTPv4, Client, length 48 14:13:34.544585 IP 172.17.0.3.123 > 10.10.10.1.123: NTPv4, Server, length 48 14:13:34.556956 IP 172.17.0.1.123 > 10.10.10.1.123: NTPv4, Server, length 48 14:13:35.535308 IP 10.10.10.1.123 > 172.17.0.2.123: NTPv4, Client, length 48 14:13:35.535742 IP 172.17.0.2.123 > 10.10.10.1.123: NTPv4, Server, length 48 14:13:36.535363 IP 10.10.10.1.123 > 172.17.0.1.123: NTPv4, Client, length 48 ...
And the forwarded traffic on the public interface:
14:13:32.534944 IP 18.104.22.168.63956 > 22.214.171.124.123: NTPv4, Client, length 48 14:13:32.556586 IP 126.96.36.199.123 > 188.8.131.52.63956: NTPv4, Server, length 48 14:13:33.535188 IP 184.108.40.206.48131 > 220.127.116.11.123: NTPv4, Client, length 48 14:13:33.535500 IP 18.104.22.168.123 > 22.214.171.124.48131: NTPv4, Server, length 48 14:13:34.535255 IP 126.96.36.199.56807 > 188.8.131.52.123: NTPv4, Client, length 48 14:13:34.535337 IP 184.108.40.206.56554 > 220.127.116.11.123: NTPv4, Client, length 48 14:13:34.544543 IP 18.104.22.168.123 > 22.214.171.124.56554: NTPv4, Server, length 48 14:13:34.556932 IP 126.96.36.199.123 > 188.8.131.52.56807: NTPv4, Server, length 48 14:13:35.535379 IP 184.108.40.206.22968 > 220.127.116.11.123: NTPv4, Client, length 48 14:13:35.535717 IP 18.104.22.168.123 > 22.214.171.124.22968: NTPv4, Server, length 48 14:13:36.535442 IP 126.96.36.199.24583 > 188.8.131.52.123: NTPv4, Client, length 48 ...
You see, the ntp server gets the time via the tunnel via udpxd which in turn forwards it to real ntp servers.
Note, if you left the parameter -b out, then the first ip address of the outgoing interface will be used.
I made a couple of enhancements to udpxd:</p>
- added ipv6 support (see below)
- udpxd now forks and logs to syslog if -d (-v) is specified
So the most important change is ip version 6 support. udpxd can now listen on an ipv6 address and forward to an ipv4 address (or vice versa), or listen and forward on ipv6. Examples:
Listen on ipv6 loopback and forward to ipv6 google:
udpxd -l [::1]:53 -t [2001:4860:4860::8888]:53Listen on ipv6 loopback and forward to ipv4 google:
udpxd -l [::1]:53 -t 184.108.40.206:53Listen on ipv4 loopback and forward to ipv6 google:
udpxd -l 127.0.0.1:53 -t [2001:4860:4860::8888]:53
Of course it is possble to set the bind ip address (-b) using ipv6 as well.
And I added a Travis-CI test, which runs successful. So, udpxd is supported on FreeBSD and Linux. I didn't test other platforms yet.