Some finer points of Tinc VPN on FreeBSD

I know this will sound like a tall tale, but there was a time when the Inter-Net, yes, it was hyphenated back then, was largely devoid of idiots.

Not devoid of people doing stupid things and of course the first idiots soon materialized, but still…

If you want to experience that kind of peace and calm these days, you have to build a VPN, which of course I have, but I ran into some interesting corners, which deserves to be documented.

I use the refreshingly competent VPN implementation Tinc, available from all well stocked ports collections as security/tinc-devel.

Tinc is a Mesh network, which means that if you can get to just one node in the network, you can get traffic through to all the nodes in the network. (port 53 and 443 he said knowingly, nudge, nudge, wink, wink, know what a’ mean ?)

Tinc and FreeBSD

Load if_tap at boot:

$ grep if_tap /boot/loader.conf
if_tap_load=YES

You need a tinc.conf file, mine looks like this:

$ cd /usr/local/etc/tinc/VPN
$ cat tinc.conf
Name = $HOST
Device = /dev/tap21
Forwarding = kernel
ConnectTo = firewall_example_com

I picked a high if_tap number to reduce the likelyhood of accidentally occupying it with any other stuff I do on the machine.

Next you need a tinc-up script to configure the interface:

$ cd /usr/local/etc/tinc/VPN
$ cat tinc-up
#!/bin/sh
/sbin/ifconfig ${INTERFACE} inet 192.168.21.3/24
/sbin/ifconfig ${INTERFACE} inet6 -ifdisabled
/sbin/ifconfig ${INTERFACE} inet6 -auto_linklocal
/sbin/ifconfig ${INTERFACE} inet6 fc00:192:168:21::3/120

You will want to fix the IP-numbers if you copy & paste.

It would be nice of /etc/rc.conf could do that, but it almost but not quite entirely works in practice.

You also want a tinc-down script to tear the interface down again:

$ cd /usr/local/etc/tinc/VPN
$ cat tinc-down
#!/bin/sh
/sbin/ifconfig ${INTERFACE} down || true
/sbin/ifconfig ${INTERFACE} delete || true
/sbin/ifconfig ${INTERFACE} destroy &

The destroy call goes in background, because it does not complete until tincd closes the tap device.

Remember to make the scripts excutable.

You also need to explicitly announce your hosts IP# to the rest of the VPN:

$ cd /usr/local/etc/tinc/VPN
$ grep Subnet hosts/laptop
Subnet = 192.168.21.3
Subnet = fc00:192:168:21::3

You will want to fix the IP-numbers if you copy & paste.

You of course still need to do all the usual non-FreeBSD specific Tinc stuff, generate keys, add Address = ... lines &c.

If you have unattended or exposed nodes on your VPN, you should add firewall rules on the VPN interface. Just because somebody manages to break into your beach-house or remote data collection computer, should not give them free run of your network.

Tinc and extra networks

If a Tinc node wants to advertise a subnet, it does so, but unless you have a subnet-up (and subnet-down) script, you will not see those routes.

However, just because some node advertises a subnet is not the same as you should accept it, so subnet-{up|down} should be a little bit paranoid.

This is how I have done it:

$ cd /usr/local/etc/tinc/VPN
$ cat subnet-up
#!/bin/sh

set -e
# exec >/dev/console  2>&1

ME="`basename $0`"

if [ "x${NODE}" = "x${NAME}" ] ; then
        # logger -t tinc "$ME ${NETNAME} OWN ${SUBNET}"
        exit 0
elif ! grep -iq "^[     ]*Subnet[       ]*=[    ]*${SUBNET}[    ]*$" hosts/${NODE} ; then
        # Only install routes to networks if they are in the local hosts/* files
        logger -t tinc "$ME ${NETNAME} UNAUTH ${SUBNET} ${NODE}/${REMOTEADDRESS}"
        exit 0
else
        logger -t tinc "${ME} ${NETNAME} OK $SUBNET ${NODE}/${REMOTEADDRESS}"
fi

if expr "x${SUBNET}" : 'x.*:.*' > /dev/null ; then
        IPV=6
else
        IPV=4
fi

if [ "${ME}" = "subnet-up" ] ; then
        /sbin/route add -${IPV} ${SUBNET} -iface ${INTERFACE}
else
        /sbin/route delete -${IPV} ${SUBNET} -iface ${INTERFACE} || true
fi

The result is that routes will only be installed to subnets listed in the /usr/local/etc/tinc/$NETNAME/hosts/$NODE files local to the machine. This way other nodes can advertize all the subnets they want, you get to decide which ones you adopt.

Why StrictSubnets is not the same

Tinc has an option called StrictSubnets which sounds like it does the same thing, but it is a little bit too brutal for my taste.

Say you have three machines without full reachability, for instance two laptops on the internet who cannot connect directly to each other, but both connected to the same firewall.

If one laptop advertises a subnet, which the firewall does not want to adopt, and the firewall uses StrictSubnets, the firewall will not pass this traffic through to the other laptop which does want to adopt that subnet.

At least, that is how I think it works…

And now for the tricky bit

Assume the following network topology:

../../_images/tincery_01.svg

Sometimes my laptop is on one of the two home networks, sometimes I leave the house and God knows what I am connected to.

Because my laptop can be on either of the two local networks and because I advertise those as subnets with tinc in the VPN, things can go amusingly wrong.

On my laptop I have a hosts file for the firewall:

$ cd /usr/local/etc/tinc/VPN
$ cat hosts/firewall_example_com
Ed25519PublicKey = BlaBlaBlaBlaBlaBlaBlaBlaBlaBlaBlaBlaBlaBlab

Subnet = 192.168.60.0/24
Subnet = 192.168.87.0/24

Address = <public IP address>
Address = 192.168.87.1
Address = 192.168.60.1

Assume my laptop is somewhere out on the internet, it uses the public IP address to get hold of the firewall, the firewall announces the two subnets which the laptop accepts and everything is just nice and wonderful.

Until, at some point, for reasons not fully understood, my laptop decides that it is a better idea to contact the firewall using one of the other two addresses.

Tl;dr: Recursion: See recursion.

To prevent this from happening, I had to add Forwarding = kernel to the tinc.conf file (see above), and add PF rules to stop these bogo-packets:

$ grep tinc_non_services /etc/pf.conf
tinc_non_services = "{ 655 }"
block in on tap21 proto tcp from any to any port $tinc_non_services
block in on tap21 proto udp from any to any port $tinc_non_services
block out on tap21 proto tcp from any to any port $tinc_non_services
block out on tap21 proto udp from any to any port $tinc_non_services

I suspect the trigger for this trouble may be meta-data packet loss which makes tincd try the next Address = line, to see if that works better, but I have not had patience to fully prove that.

Ideally tincd should never send metadata through the tunnel the metadata is about, but I saw no sensible way of adding that check, so PF got the job instead.

Ohh, and one final thing…

Release 1.1.pre17 of tinc has an annoying bug where it only tries the first Address = record. Hopefully a new release will fix that soon-ish.

phk