Flevo CFD

Ultimate Guide to Tunneling in Linux

There are cases where your machine does not have access to the destination server, or you don’t want to expose the destination server to the internet. In these cases, you can use a relay server to tunnel the traffic to the destination server.

A relay server simply forwards the incoming traffic to the destination server and forwards the response back to the client.

For the sake of this article, we want to relay openvpn traffic from machine A to machine B.

All the examples in this article are for TCP traffic thus they operate at the transport layer.

iptables

iptables is a powerful tool that allows you to configure the IP packet filter rules of the Linux kernel firewall.

First we need to enable ip forwarding

1
2
3
echo net.ipv4.ip_forward=1 >> /etc/sysctl.conf
# Reload the configuration
sysctl -p

Then we need to add the following rules

1
2
iptables -t nat -A PREROUTING -p tcp --dport 8443 -j DNAT --to-destination machine_b:443
iptables -t nat -A POSTROUTING -j MASQUERADE 

The first rule tells iptables to forward the incoming traffic on port 8443 to machine B on port 443.

The second rule masquerade the IP address of the incoming packets with the IP address of the outgoing network interface of the relay server. so the client will see the relay server IP address as the source IP address.

iptables rules are not persistent, so you need to save them to make them persistent using a tool like iptables-persistent on Ubuntu/Debian.

1
iptables-save > /etc/iptables/rules.v4

This is a simple soltuion with low overhead, but it’s not very flexible.

socat

Another great program that can be used to tunnel traffic is socat.

1
apt install socat
1
socat -dd tcp:8443,fork,reuseaddr tcp:machine_b:443

Let’s break down the command:

This first part tells socat on which host and port to listen, the second part specifies where to forward the traffic.

It listens on port 8443 tcp and forwards the traffic to machine B on port 443 tcp.

The fork option create a new process for each connection by calling fork(), so that multiple connections can be handled.

The reuseaddr option allows other sockets to bind to an address to which another socket has already bound.

it’s enabled by default on most systems so It’s not necessary to specify it.

The -dd option enables debugging output.

SSH

We want to use SSH Local Port Forwarding feature to tunnel the traffic.

1
ssh -C -N -L 0.0.0.0:8443:127.0.0.1:443 machine_b_ssh_user@machine_b

This command simply forwards traffic from port 8443 (on all interfaces meaning 0.0.0.0) on the system that the command is executed (machine A) to port 443 on 127.0.0.1 on machine B. it’s the address that the traffic will be forwarded to which is the openvpn server running on machine B.

Note: you can omit 0.0.0.0 since it’s the default value.

let’s break down the command:

-L option specifies the port forwarding

-C option enables compression

-N option tells ssh not to execute a remote command.

In order to send keep-alive packets to the server to prevent the connection from being closed by some firewalls because of connection inactivity, you can use the following options:

  • ServerAliveInterval sets a timeout interval in seconds after which if no data has been received from the server, ssh will send a message through the encrypted channel to request a response from the server.

  • ServerAliveCountMax sets the number of server alive messages which may be sent without ssh receiving any messages back from the server.

so the command becomes:

1
ssh -C -N -o "ServerAliveInterval 10" -o "ServerAliveCountMax 3" -L 8443:127.0.0.1:443 machine_b_ssh_user@machine_b

There’s another great program called autossh that can be used to automatically restart the ssh connection if it fails.

1
autossh -f -M 0 -C -N -o "ServerAliveInterval 10" -o "ServerAliveCountMax 3" -L 8443:127.0.0.1:443 machine_b_ssh_user@machine_b

autossh comes with a monitoring feature but we disabled it with the -M 0 option. you can experiment with it to see if it fits your needs.

The -f option tells autossh to run in the background.

stunnel

This progmram adds TLS encryption to the TCP traffic.

1
apt install stunnel4

Then we need create a certificate to encrypt the traffic. this command creates a private key and a self-signed certificate:

1
openssl req -new -x509 -days 365 -noenc -out cert.pem -keyout key.pem

-new create a new certificate request.

-x509 create a self-signed certificate.

-days specifies the number of days the certificate is valid.

-noenc tells openssl not to encrypt the private key.

-out specifies the output file for the certificate.

-keyout specifies the output file for the private key.

Combine the key and the certificate into a single file as stunnel requires it:

1
cat key.pem cert.pem > /etc/stunnel/stunnel.pem

Now we need to setup a client and server. the client is machine A and the server is machine B. client sends the traffic to the server and the server forwards it to the openvpn service running on port 443.

We need to create a configuration file for machine A /etc/stunnel/stunnel.conf as client

1
2
3
4
5
6
cert = /etc/stunnel/stunnel.pem
client = yes

[openvpn]
accept = 8443
connect = machine_b:8443

accept is the port that stunnel listens on. connect is where the traffic is forwarded to.

On machine B which recieves the traffic and forwards it to the openvpn server running on port 443

1
2
3
4
5
6
client = no

[openvpn]
accept = 8443
connect = 127.0.0.1:443
cert = /etc/stunnel/stunnel.pem
1
systemctl start stunnel4

HAProxy

HAProxy is a very fast and reliable solution offering high availability, load balancing, and proxying for TCP and HTTP-based applications. here we will use it as a forward proxy.

HAProxy has two main sections: frontend and backend. the frontend recieves the traffic and based on some rules, forwards it to the backend.

Install HAProxy:

1
apt install haproxy

Let’s define our frontend and backend in the configuration file /etc/haproxy/haproxy.cfg

frontend default
	log stdout format raw local0 info  # log to stdout
	mode tcp
	bind :8443
	default_backend mybackend

backend mybackend
	mode tcp
	server server1 machine_b:443

server1 is a tag that HAProxy uses to identify the server. you can use any name you want.

We could also encrypt this communication between the two machines using SSL

Let’s create a self-signed certificate:

1
openssl req -new -x509 -days 365 -noenc -out cert.pem -keyout key.pem

Combine the key and the certificate into a single file as HAProxy requires it:

1
cat key.pem cert.pem > /etc/haproxy/haproxy.pem

On machine A

frontend default
	log stdout format raw local0 info
	mode tcp
	bind :8443
	default_backend mybackend

backend mybackend
	mode tcp
	server server1 machine_b:8443 ssl crt /etc/haproxy/haproxy.pem verify none

On machine B, the traffic is recived on port 8443 and be forwarded to the openvpn server running on port 443.

frontend default
	log stdout format raw local0 info
	mode tcp
	bind :8443 ssl crt /etc/haproxy/haproxy.pem verify none
	default_backend mybackend

backend mybackend
	mode tcp
	server server1 127.0.0.1:443

Note that only the communication between the HAProxy instances is encrypted.

The verify none option tells haproxy not to verify the certificate of the destination server. you could add the certificate to the operating system trust store so you don’t have to use this option. run the following commands on both machines:

1
2
3
4
5
6
7
# Ubuntu/Debian
cp /etc/haproxy/haproxy.pem /usr/local/share/ca-certificates/haproxy.pem
update-ca-certificates

# Fedora/CentOS
cp /etc/haproxy/haproxy.pem /etc/pki/ca-trust/source/anchors/haproxy.pem
update-ca-trust

Restart the HAProxy service:

1
systemctl restart haproxy

Conclusion

We have seen different ways to tunnel traffic in Linux. Each method has its own use case and advantages.

iptables, socat are simple and have low overhead, but they are not very flexible and they don’t provide encryption.

stunnel, SSH and HAProxy provide encryption between relay and destination server and are more flexible but they have more overhead.

You can choose the method that fits your needs.