This is a poorly documented yet really useful feature of Openssh. It allows you to connect two tun/tap interfaces together, to create a layer-2 or layer-3 network between remote machines. This results in OpenVPN-like VPNs (but much simpler and, admittedly, less scalable).
The ssh server must be configured to support tunnels. In practice, this means adding the directive
to the /etc/ssh/sshd_config file and restarting the ssh server daemon. You can also use the keyword ethernet or point-to-point instead of yes if you want to permit only a specific type of tunnel (see below for the details).
As I said before, the tunnel can be layer-2 (ethernet) or layer-3 (point to point). To specify what kind of tunnel you want on the client side, you have two choices:
- Use the -o Tunnel=<tunnel_type> option when running ssh to connect to the server; or
- Use the Tunnel <tunnel_type> directive in the /etc/ssh/ssh_config client configuration file (or, equivalently, in the per-user ~/.ssh/config file).
In both cases, the <tunnel_type> can be ethernet for an L2 link, or point-to-point for a L3 (ie, IP) link.
Openssh assumes an interface name of "tap" if you create an ethernet tunnel, and "tun" if you create an IP tunnel. AFAICT (but corrections are welcome) there is currently no means of specifying a different interface name.
Please note that, for the following examples to work, you must disable any iptables rule that might prevent tun/tap interfaces from sending or receiving traffic.
Creating an ethernet tunnel
Let's say you want to create an L2 tunnel between host local and host remote (the ssh server is running on remote), and you want to connect interface tap4 on local to interface tap6 on remote.
First of all, the necessary tap interfaces must already be present on both hosts, so you have to create them beforehand, for example with the following commands:
local # tunctl -t tap4 Set 'tap4' persistent and owned by uid 0 remote # tunctl -t tap6 Set 'tap6' persistent and owned by uid 0
tunctl is part of the uml-utilities package (the name might be slightly different depending on the distribution, but in any case it's the set of utilities that come with User Mode Linux). Another command that can create tun/tap interfaces is openvpn, for example:
local # openvpn --mktun --dev tap4 Thu Nov 12 21:59:13 2009 TUN/TAP device tap4 opened Thu Nov 12 21:59:13 2009 Persist state set to: ON remote # openvpn --mktun --dev tap6 Thu Nov 12 22:11:51 2009 TUN/TAP device tap6 opened Thu Nov 12 22:11:51 2009 Persist state set to: ON
Of course, if you want a regular user to be able to use the interfaces, use the -u <owner> option with tunctl, or the --user <owner> option with openvpn. To remove the interface, use
# tunctl -d <ifname>
# openvpn --rmtun <ifname>
Now, to test the tunnel, let's assign IP addresses to the interfaces:
local # ifconfig tap4 10.0.0.1 netmask 255.255.255.0 up remote # ifconfig tap6 10.0.0.2 netmask 255.255.255.0 up
A better way is to use the ip command, part of the iproute2 package:
local # ip link set tap4 up local # ip addr add 10.0.0.1/24 dev tap4 local # ip route add 10.0.0.0/24 dev tap4 # this might not be necessary remote # ip link set tap6 up remote # ip addr add 10.0.0.2/24 dev tap6 remote # ip route add 10.0.0.0/24 dev tap6 # this might not be necessary
Now we can connect the two interfaces using Openssh:
local # ssh -o Tunnel=ethernet -w 4:6 remote Password: ********* remote #
Test the tunnel:
local # ping -c 4 10.0.0.2 PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data. 64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=79.5 ms 64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=37.3 ms 64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=39.5 ms 64 bytes from 10.0.0.2: icmp_seq=4 ttl=64 time=38.5 ms --- 10.0.0.2 ping statistics --- 4 packets transmitted, 4 received, 0% packet loss, time 3005ms rtt min/avg/max/mdev = 37.311/48.759/79.407/17.792 ms local #
Although not apparent from the above, since we are using an L2 tunnel, full ethernet frames (ie, up to 1514 bytes long if using the default MTU) are passing through the encrypted tunnel.
Creating a point to point IP tunnel
The scenario is the same as above, but this time we want to setup an IP tunnel. This means that IP packets (up to 1500 bytes long if using the default MTU), and not ethernet frames, will be flowing through the tunnel.
The steps needed to create and bring up the interfaces are the same as the previous example. Just change the names of the interfaces to tun4 and tun6 in all commands.
To connect the two interfaces with Openssh, use the following command:
local # ssh -o Tunnel=point-to-point -w 4:6 remote Password: ********* remote #
Test the tunnel:
local # ping -c 4 10.0.0.2 PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data. 64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=36.4 ms 64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=39.0 ms 64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=37.4 ms 64 bytes from 10.0.0.2: icmp_seq=4 ttl=64 time=44.8 ms --- 10.0.0.2 ping statistics --- 4 packets transmitted, 4 received, 0% packet loss, time 3005ms rtt min/avg/max/mdev = 36.405/39.439/44.847/3.261 ms local #
The output looks exactly the same as the previous example, but, as I said above, the difference is in the type and size of packets passing through the tunnel. Now the tunnel is carrying IP packets rather than ethernet frames.
Some example applications
Those that follow are certainly not the only useful applications of Openssh tun/tap tunnels. They are only meant to demonstrate some examples of what can be done, and can (and should) be extended in the way you like.
VPN using a point-to-point tunnel
Suppose your LAN at work uses IP subnet 172.16.10.0/24, so it's not directly reachable from the Internet. You are at home, and the only thing you can do is login via ssh onto a box on the LAN which uses a public IP address (this will probably be a router or a firewall, but not necessarily). What you can do is set up a L3 tunnel between your box and the remote host, and access the internal LAN without having to resort to nasty tricks.
You need to choose an IP subnet that will be used for the point-to-point link between your box and the remote host (better: between the tun interface on your box and the tun interface on the remote host); for this example, we'll use 192.168.0.0/30, which leaves only two usable IP addresses. The tun1 interface on your box will use 192.168.0.1, and the tun1 interface on the remote box will use 192.168.0.2.
Create the interfaces on your box and on the remote host as described in the Creating a point to point IP tunnel example above, assigning the correct interface names and IP addresses (local tun1: 192.168.0.1, remote tun1: 192.168.0.2). Here, of course, local is your home box and remote is the remote host. Now connect to the remote host:
homebox # ssh -o Tunnel=point-to-point -w 1:1 remotehost Password: ********* remotehost #
So far you just have a working and pingable link between your box's tun1 interface and the remote tun1 interface. To access the internal remote LAN, you have to add some static routes. At a minimum, you need to tell your home box that network 172.16.10.0/24 is reachable through interface tun1, and you must enable routing on the remote box (if it's not already enabled):
homebox # ip route add 172.16.10.0/24 via 192.168.0.2 dev tun1 remotehost # echo 1 > /proc/sys/net/ipv4/conf/all/forwarding
Now you should be able to ping a host internal to the LAN from your home box:
homebox # ping -c 4 172.16.10.44 PING 172.16.10.44 (172.16.10.44) 56(84) bytes of data. 64 bytes from 172.16.10.4: icmp_seq=1 ttl=64 time=37.8 ms 64 bytes from 172.16.10.4: icmp_seq=2 ttl=64 time=36.4 ms 64 bytes from 172.16.10.4: icmp_seq=3 ttl=64 time=40.5 ms 64 bytes from 172.16.10.4: icmp_seq=4 ttl=64 time=40.2 ms --- 172.16.10.44 ping statistics --- 4 packets transmitted, 4 received, 0% packet loss, time 2998ms rtt min/avg/max/mdev = 36.449/38.754/40.530/1.696 ms
This means that you can now access and use your work LAN's internal resources, such as HTTP servers, mail severs, network disks, etc., all using their private IP addresses, as they appear on the LAN.
Note that this example assumes that the hosts in the remote LAN use the remote host as their default gateway (which will generally be true if it's a router or a firewall). If this is not true, then they will receive the packets you send them from home, but they won't be able to reply, since they have no route for the VPN link (192.168.0.0/30). A possible solution is to add a route to 192.168.0.0/30 via the remote box to each internal host you need to access, but it's a bit awkward. A better solution is to use NAT towards the internal LAN on the remote host, so packets from your home box will appear on the remote LAN as originating from the remote box (which other hosts in the LAN know how to reach). For example, do something like the following:
remotehost # iptables -t nat -A POSTROUTING -s 192.168.0.0/30 -o eth0 -j MASQUERADE
(if the remote host's LAN interface is not eth0, substitute with the appropriate interface name). This should get things going even when the remote box is not the default gateway for hosts in the remote LAN.
IPv6 at home
Again, suppose your LAN at work has IPv6 connectivity (either native or tunneled). Your home ISP service does not (yet) offer IPv6 connectivity, but you want or need to use IPv6 from home. What we are going to do is to connect to the work LAN at the ethernet level, so our home box will be able to receive the ethernet multicast messages needed to perform IPv6 autoconfiguration. To do this, we'll use a tap interface and an Openssh ethernet tunnel.
The remote machine we connect to need not be the default gateway on the remote LAN. In fact, we can choose any machine that is reachable via ssh (over IPv4 of course), is IPv6-enabled, and has ethernet bridging support in the kernel. On the home box, we just need to create a tap interface:
homebox # tunctl -t tap1 Set 'tap1' persistent and owned by uid 0 homebox # ip link set tap1 up
On the remote host, we need to create a tap interface, and bridge it with the host's interface on the LAN (we assume eth0 in this example). WARNING: don't run the following commands through a remote session (eg, ssh) connected through interface eth0, since we need to remove IP addresses from the eth0 interface for a brief period of time, and you'll be disconnected. Creating a script that does all the operations at once might work, but you have been warned. First, create the tap interface:
remotehost # tunctl -t tap1 Set 'tap1' persistent and owned by uid 0 remotehost # ip link set tap1 up
Now, create the bridge (br0) and add interfaces tap1 and eth0 to the bridge. But before doing that, we must remove all the IP addresses from it, and set it to be in promiscuous mode. We assume the IPv4 address of the remote host's eth0 interface is 10.0.0.1/24 (with a default gateway of 10.0.0.254), and that its IPv6 address is 2001:db8:1:1::1/64, but you must use the actual addresses.
remotehost # ip addr del 10.0.0.1/24 dev eth0 remotehost # ip -6 addr del 2001:db8:1:1::1/64 dev eth0 remotehost # ip -6 route del 2001:db8:1:1::/64 dev eth0 remotehost # ip -6 route del default dev eth0 remotehost # ip link set eth0 promisc on remotehost # ip link set tap1 promisc on remotehost # brctl addbr br0 remotehost # brctl addif br0 eth0 tap1 remotehost # ip addr add 10.0.0.1/24 dev br0 remotehost # ip route add default via 10.0.0.254 dev br0
We don't readd IPv6 addresses to interface br0, since it will autoconfigure them again as soon as a router advertisement packet is received.
What we have now is an ethernet bridge (or switch if you prefer) that joins together the ethernet segments connected to the interfaces eth0 and tap1. This is what enables us to send ethernet frames (including router advertisements and neighbor discovery) to the home box. For more information about linux kernel bridge read the official documentation. Note that we don't even need to assign IPv4 addresses to tap interfaces.
Now, connect the tap interface on the home box to the tap interface on the remote box:
homebox # ssh -o Tunnel=ethernet -w 1:1 remotehost Password: ********* remotehost #
If we execute ifconfig tap1 a few times on the home box, we can (hopefully) see the RX packet counter incrementing (meaning that ethernet frames are being received from the remote LAN):
homebox # ifconfig tap1 tap1 Link encap:Ethernet HWaddr 00:FF:AF:04:C7:92 inet6 addr: fe80::2ff:afff:fe04:c792/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:24 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:6 overruns:0 carrier:0 collisions:0 txqueuelen:500 RX bytes:1652 (1.6 Kb) TX bytes:0 (0.0 b) homebox # ifconfig tap1 tap1 Link encap:Ethernet HWaddr 00:FF:AF:04:C7:92 inet6 addr: fe80::2ff:afff:fe04:c792/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:37 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:6 overruns:0 carrier:0 collisions:0 txqueuelen:500 RX bytes:3970 (3.8 Kb) TX bytes:0 (0.0 b)
After a while (depending on the configuration of the IPv6 router on the remote LAN) the tap interface on your home box will receive a router advertisement frame (through the Openssh ethernet tunnel), and will autoconfigure its IPv6 address and default gateway:
homebox # ifconfig tap1 tap1 Link encap:Ethernet HWaddr 00:FF:AF:04:C7:92 inet6 addr: 2001:db8:1:1:2ff:afff:fe04:c792/64 Scope:Global inet6 addr: fe80::2ff:afff:fe04:c792/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:64 errors:0 dropped:0 overruns:0 frame:0 TX packets:1 errors:0 dropped:6 overruns:0 carrier:0 collisions:0 txqueuelen:500 RX bytes:5862 (5.7 Kb) TX bytes:78 (78.0 b)
Note the single packet sent, which represents a neighbor solicitation frame (used for duplicate address detection).
That's it. Now try pinging an IPv6 site from home:
homebox # ping6 -c 4 -n www.ipv6.org PING www.ipv6.org(2001:6b0:1:ea:202:a5ff:fecd:13a6) 56 data bytes 64 bytes from 2001:6b0:1:ea:202:a5ff:fecd:13a6: icmp_seq=1 ttl=51 time=150 ms 64 bytes from 2001:6b0:1:ea:202:a5ff:fecd:13a6: icmp_seq=2 ttl=51 time=115 ms 64 bytes from 2001:6b0:1:ea:202:a5ff:fecd:13a6: icmp_seq=3 ttl=51 time=116 ms 64 bytes from 2001:6b0:1:ea:202:a5ff:fecd:13a6: icmp_seq=4 ttl=51 time=111 ms --- www.ipv6.org ping statistics --- 4 packets transmitted, 4 received, 0% packet loss, time 3000ms rtt min/avg/max/mdev = 111.829/123.484/150.095/15.474 ms
Figuring out the path taken by IPv6 packets and frames sent from the home box to the IPv6 Internet is left as an exercise for the reader :-)
Of course, you can do many other things with Openssh VPN tunnels beyond those described here. This document is meant to be just an introduction to the feature, which I feel is not covered terribly well by the official documentation and man page.
One thing to keep in mind is that Openssh VPNs, as those described in this document, use TCP (unlike OpenVPN, which uses UDP - by default). This might lead to some problems, as described here: Why TCP Over TCP Is A Bad Idea. However, usually this is an issue only if your connection is slow, and YMMV.