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:
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.
Another fine point¶
If you have two VPN nodes behind the same NAT device, tinc’s outside the NAT can get confused by them using same IP+PORT.
The obvious solution is to give them different port numbers, in which case you need to add the “extra” port numbers to tinc_non_services above.
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