On 2008-02-21 I wrote about the usage of FreeBSD as a VPN server using PPTP (holy fuck, I have this site up for more than 10 years by now?!). Times are changing, PoPToP lost its maintainer and even worse is considered insecure these days. Time to move on. Please note that this article reiterates lots of stuff that is already present on the Internet. I wrote this anyway since redundancy is good (and good) and these days you never know for how long Internet resources will stay available.

The Basics or How we got here

I needed a VPN. Not in the way most people mean it when they say "I needed a VPN", when what they mean is "So I wanted to get away with watching this movie for free". I wanted to dial into my home network from an off-site. So I worked my way up. I started with the use of SSH-VPN, that is, I used a BSD on boch sides on which I created and configured tun-devices. Then I used a root-privileged instance of ssh with the parameter -w to create a tunnel between those devices.As soon as the tunnel is up and running, there's an ssh-encrypted network connection available. While this works pretty well (and is quite easy set up, all you need is a sshd with root-privileges on both sides), it has its kinks. Starting with the need for some kind of an Unix on both ends. Unix in a virtualization does work for sure, but that's not the point. Also, tunneling TCP in TCP is inherently a nightmare in terms of efficiency.

At some point I took a closer look at the data I panned to send over this line and came to the conclusion that super-strong encryption is not as important as the convenience of using it with Windows w/o any third-party software (i.e. OpenVPN). So I took a step back and looked back at using PPTP -- at which point I got PPTP to work with mpd5. However, as my ultimate goal is to have the VPN server positioned in a DMZ and behind a NAT, I found that I run into a bit of a trouble with GRE packages (PPTP would use TCP to establish a control channel and then use GRE to transmit the actual data packages). While that makes 4 kinds of sense, it sucked for my scenario as I cannot portforward GRE. Then I took a look at SSTP. It's not as if I have a general problem with using something not officially standardized. But the lack of open source support for the procotol as such made me a bit worried about the future-readiness of this solution. Don't get me wrong, I know, there's always "well, if you want it, you should just provide and maintain it"... yeah... go fuck yourself. For two main reasons: a. I don't have the necessary experience for that, b. I want to solve a resource bottleneck and not create another one.
Eventually I realized that there's only a small number of VPN services which are in the intersection of "Supported by vanilla Microsoft Windows", "Server runs on FreeBSD", and "Can easily sit behind a NAT". I would have loved to play with a fake Cisco AnyConnect server as I have to admit that their protocol idea is quite elegant. But for real, there's only L2TP/ipsec to go. It's quite secure from what is said in public; Windows has native support; it would even work with mobile devices as most of them support this kind of VPN, same goes for a number of consumer wifi-routers.

Setting up PPTP with mpd5 on FreeBSD (a side note)

As I wrote above, I re-tried going for PPTP again, this time using mpd5 contrary to my last attempt which in 2008 used PoPToP pptp.html. Without any further ado, this would be a config for mpd5 if you want to setup a quick-and-dirty PPTP server, assuming you can accept GRE packages:

root@blacklight:/usr/local/etc/mpd5 # cat mpd.conf
startup:
default:
    load pptp_server
pptp_server:
    set ippool add pool1 192.168.11.50 192.168.11.99
    create bundle template B
    set iface enable proxy-arp
    set iface idle 1800
    set iface enable tcpmssfix
    set iface route 192.168.11.1
    set ipcp yes vjcomp
    set ipcp ranges 192.168.11.1/32 ippool pool1
    set ipcp dns 8.8.8.8
    set ipcp dns 9.9.9.9
    set ipcp nbns 192.168.11.1
    set bundle enable compression
    set ccp yes mppc
    set mppc yes e40
    set mppc yes e128
    set mppc yes stateless
    create link template L pptp
    set link fsm-timeout 5
    set link action bundle B
    set link enable multilink
    set link yes acfcomp protocomp
    set link no pap chap eap chap-msv2
    set link enable chap chap-msv2 eap
    set link accept chap-msv2
    set link keep-alive 10 60
    set link mtu 1460
    set pptp self 0.0.0.0
    set link enable incoming
What this configuration does is setting the pool of IP addresses handed out to clients to 192.168.11.50-192.168.11.99, enabling Proxy-ARP (otherwise the whole forwarding thing for nameserver etc would be a bigger issue in the first place), then setting the default-route to be this pptp-server's own 192.168.11.1. DNS (and even nbns -- net bios name service) are set, then the link-chitchat is configured and finally we come to the highlight -- mpd5 will listen for incoming connections from any IP and actually do so (enable incoming). Of course we need to provide some credentials to authentificate against, so here we go:
root@blacklight:/usr/local/etc/mpd5 # cat mpd.secret
muffin  cookie
This enables the user "muffin" to log in using the super-snackable password "cookie".

L2TP and IPsec

Enough with the foreplay, here's the reason for this article. This configuration will enable L2TP connects.

I start with a FreeBSD 11.2. More precisely, it's a

FreeBSD blacklight 11.2-RELEASE FreeBSD 11.2-RELEASE #0 r335510: Fri Jun 22 04:32:14 UTC 2018     root@releng2.nyi.freebsd.org:/usr/obj/usr/src/sys/GENERIC  amd64
...which fortunately has all the fun stuff you need for IPSEC in its kernel. That is, this article doesn't start with the "and now you compile your kernel as follows"-section featured by numerous older articles on this topic. So we can go ahead and just start by installing the tools necessary:
shell# pkg install mpd5
shell# pkg install ipsec-tools
Note however, with the installation of the racoon utility as part of the ipsec-tools package, we block ourselves from modifying its source code. And this is one of the cases in which having the actual source code available to patch it is an advantage as we cannot have wildcard-PSKs in this case. To be clear about what this implies: Either we need an extensive psk file or have to know the IP addresses of our VPN clients at configuration time. That is, we make it virtually impossible to use this VPN server from a dialup or free WiFi connection as we typically can't predict those offers' public IP addresses.
In case you want to go for patching, the patch is pretty straight forward -- quoting [2]:
diff -rup srca/racoon/localconf.c srcb/racoon/localconf.c
--- src/racoon/localconf.c      2008-12-23 12:04:42.000000000 -0200
+++ src/racoon/localconf.c      2011-04-25 15:44:24.000000000 -0300
@@ -207,7 +207,8 @@ getpsk(str, len)
                if (*p == '\0')
                        continue;       /* no 2nd parameter */
                p--;
-               if (strncmp(buf, str, len) == 0 && buf[len] == '\0') {
+               if (strcmp(buf, "*") == 0 ||
+                   (strncmp(buf, str, len) == 0 && buf[len] == '\0')) {
                        p++;
                        keylen = 0;
                        for (q = p; *q != '\0' && *q != '\n'; q++)

Put this piece of magic into /usr/ports/security/ipsec-tools/files/patch-zz-local-1.diff once you checked out the portstree and then build the ipsec-tools from scratch. Note however, I haven't tested it. Worst case you'd have to do some C-reading to re-identify this position at which the PSK is checked for identity with the connection ID (which defaults to the peer IP).

Now we can move on to configure racoon, the program which will take care of the whole key management process. Its configuration file is expected to be found at /usr/local/etc/racoon/racoon.conf. Mine looks like this:

path pre_shared_key "/usr/local/etc/racoon/psk.txt";

listen {
        isakmp          192.168.0.44 [500];
        isakmp_natt     192.168.0.44 [4500];
        strict_address;
}

remote anonymous
{
        exchange_mode   main;
        passive         on;
        proposal_check  obey;
        support_proxy   on;
        nat_traversal   on;
        ike_frag        on;
        dpd_delay       20;

        proposal
        {
                encryption_algorithm    aes;
                hash_algorithm          sha1;
                authentication_method   pre_shared_key;
                dh_group                modp1024;
        }

        proposal
        {
                encryption_algorithm    3des;
                hash_algorithm          sha1;
                authentication_method   pre_shared_key;
                dh_group                modp1024;
        }
}

sainfo anonymous
{
        encryption_algorithm            aes,3des;
        authentication_algorithm        hmac_sha1;
        compression_algorithm           deflate;
        pfs_group                       modp1024;
}
After declaring the path of the file containing the pre-shared keys, the listen-block defines what address and port (in square brackets) racoon will be listening on. Mind the semicolons... that took me a while.

As we just referenced a file with the pre shared keys, we should also create that -- it's a straight forward file, first the identifier, then a whitespace, then the psk (which again might include whitespaces). So it may look like this:

shell# cat /usr/local/etc/racoon/psk.txt
56.78.154.33 What_Is-Love?
89.99.122.4 Baby!Don't_Hurt0Me.
...which defines two PSKs for the given IPs. If you used the patch sketched above, you could of course also go for sth like this:
shell# cat /usr/local/etc/racoon/psk.txt
* WhoooooooAreYou_Who-ho_who-ho?
Which would define the same (admittedly hateworthy) passphrase for all incoming connections. I guess I don't have to talk about the security implication of this option... it essentially weakenes the authentification by taking away one factor which could be given out to individual peers to enable them to start the negotiation at all before we get to the whole username/password section of the negotiation.

The rights for the psk file must be set to 600.

As part of the connection initiation, we need to give the setkey utility some configuration. It will ensure that there are security associations (SA) are governed by security association policies, kept in the security policy database (SPD) -- in this case, ensuring that they are defined to be transport mode, i.e. tunnel and using UDP.

shell# cat /usr/local/etc/racoon/setkey.conf
flush;
spdflush;
spdadd 0.0.0.0/0[0] 0.0.0.0/0[1701] udp -P in  ipsec esp/transport//require;
spdadd 0.0.0.0/0[1701] 0.0.0.0/0[0] udp -P out ipsec esp/transport//require;

And finally we come to the configuration of mpd the deamon which takes care of the higher level configuration.

root@blacklight:~ # cat /usr/local/etc/mpd5/mpd.conf
startup:

default:
        load l2tp_server

l2tp_server:
        set ippool add pool_l2tp 192.168.11.150 192.168.11.199

        create bundle template B_l2tp
        set iface enable proxy-arp
        set iface enable tcpmssfix
        set ipcp yes vjcomp

        set ipcp ranges 192.168.11.1/32 ippool pool_l2tp
        set ipcp dns 9.9.9.9
		
        create link template L_l2tp l2tp
        set link action bundle B_l2tp
        set link mtu 1230
        set link keep-alive 0 0
        set link yes acfcomp protocomp
        set link no pap chap eap
        set link enable chap

        set l2tp self 192.168.0.44
        set l2tp disable dataseq

        set link enable incoming

With this configuration, we declare the IP configuration, well, see above for the params; l2tp and pptp are quite siilar in terms of their config params regarding IP. Also we should define the accounts allowed to log in to the VPN, those are declared in the mpd.secrets file with a simple user-password-list:
shell# cat /usr/local/etc/mpd5/mpd.secret
muffin 	chocoholicschocolatechips
If you want to use spaces in the password, you need to surround the password with quotes.

Finally you might want to make some extensions to your rc.conf for IPSEC and racoon:

ipsec_enable="YES"
ipsec_program="/usr/local/sbin/setkey"
ipsec_file="/usr/local/etc/racoon/setkey.conf"
racoon_enable="YES"
racoon_flags="-l /var/log/racoon.log"
mpd_enable="YES"
So that the VPN stuff comes up after a system reboot and the racoon log will be written to /var/log/racoon.log.

Given all this worked out, you should now be left with a system which is holding up two ports after a reboot:

shell# sockstat -l4
USER     COMMAND    PID   FD PROTO  LOCAL ADDRESS         FOREIGN ADDRESS
root     racoon     24226 4  udp4   192.168.0.44:4500     *:*
root     racoon     24226 5  udp4   192.168.0.44:500      *:*

Housekeeping

As usual, there's a small amount of housekeeping things to track after a connection has been established. Most scenarios afford more than just access to ressources on the dialin-server; usually VPNs are used to enter a network -- so some kind of routing or NATting will make sense. The question of routing vs NATting is mainly decided by the rest of the setup. Does the rest of you network know that a certain IP range is to be routed using this FreeBSD box? Then routing might be the way to go. OTOH, if you want to keep efforts low, go for NAT. In this case you won't have to configure a route and mess with your address concept; unless you experience a high number of users (and thus exceed the portrange) you should be fine. Regardless of what you do, in the first place this is not as much of a security question as sometimes insinuated1.

If you decide to go for the way of NATting everything coming from the tunnel, this is one way to do it using ipnat. First, tell the kernel that we actually want to do some kind of routing stuff on this machine:

shell# sysctl net.inet.ip.forwarding=1
Next, define the ruleset for the IPNAT natting and get them into effect:
shell# cat ipnat.rules
map em0 192.168.11.0/24 to 0/0 -> 0/32
shell# kldload ipl
shell# ipnat -vCf ipnat.rules
Which essentially says "nat things coming from the net 192.168.11.0/24 to anywhere to whereever they want using the device em0". With the kldload we ensure that the respective kernel module is present and finally we use the ipnat tool to make things work.

Note that all of this is not VPN related at all, you can do this with any network device and net.

References

There's some sites which helped me putting this together:


1 NAT is not a security feature; it never will and it never has been. Your attacker might loose track of your internal IPs but his devious package contents are still the very same except for them now running around with one of your internal IPs which you've been stupid enough to grant them. External IPs are a red flag when occurring in certain network zones and that is a good thing as it makes them easy to spot in any log analysis. Converting them to internal IPs makes them less obvious while letting them keep their potentially harmful payload.

Stichworte:


Impressum