#+TITLE: Creating a VPN Gateway with OpenBSD 6.7 #+DATE: 2020-08-16T16:35:47-04:00 #+DRAFT: true #+DESCRIPTION: #+TAGS[]: openbsd openvpn #+KEYWORDS[]: openbsd openvpn #+SHOWTOC: true #+SLUG: #+SUMMARY: * The Problem Say you have an account with a VPN provider. Maybe there are a limit to how many connections you can have with one account. Maybe you want to put more machines than you have connections on the account. Or maybe you want to put a large number of machines of the connection, maybe some FreeBSD Jails, LXC containers, or VMs, and you don't want to download the VPN profiles, sign in and configure them all individually. * The Solution The solution I came up with to this problem is to setup a VPN gateway on my network using [[https://www.openbsd.org/faq/pf/][OpenBSD]]. Any device that sets that machine as it's gateway will automatically get its traffic tunnelled through the VPN connection. Because I'm setting the VPN up as a second gateway on an existing network, all devices on the network will still be able to talk to each other normally, regardless of which gateway they use. It's setup such that if the VPN connection ever drops or gets killed for any reason, the traffic will stop and won't be able to reach the internet. Thanks to this I don't have to worry about the traffic ever leaking out through my residential gateway should OpenVPN decide to close the connection. Sort of like a "kill switch", as some companies market it. * Our Network In this post the machine will have a single network interface called =vio0=. We'll set it up with a static IP of =192.168.0.11= and a =/24= subnet. Our network's router is located at =192.168.0.1=. The interface and IPs in your case will differ. * Hardware To follow this setup you'll need a dedicated machine running OpenBSD. You'll have to choose an appropriate host, taking into consideration how much traffic you plan to put through it, the speed of you VPN connection, and the speed of your home internet connection. Anything from a virtual machine or a low power single board PC will do in most cases. If your internet connection is fast enough though, you may consider [[https://blog.lambda.cx/posts/installing-openbsd-on-pcengines/][installing OpenBSD]] on a [[https://blog.lambda.cx/posts/pcengines-comparison/][PC Engines APU2]]. They're affordable, have gigabit Ethernet, and great OpenBSD driver support. In my case I created a virtual machine on a server running [[https://www.proxmox.com/en/][Proxmox]]. The machine only has 1 vCPU and 512 MB RAM, which is more than enough for my needs. * Documentation I highly recommend you check out the man pages for the firewall configuration file format [[https://man.openbsd.org/man5/pf.conf.5][=pf.conf(5)=]], and the pf control command [[https://man.openbsd.org/man8/pfctl.8][ =pfctl(8)=]] if you plan on setting something like this up. They're all very well written and explain a lot of what I'm doing in very clear detail. You should also read the excellent [[https://www.openbsd.org/faq/pf/][PF FAQ]] from the OpenBSD website. * Installing OpenBSD I won't be covering installing OpenBSD here, although it's extremely simple and straight forward. You can pick up the disk =.iso= image or USB =.fs= image from the [[https://www.openbsd.org/faq/faq4.html#Download][download]] page on OpenBSD website. If this is your first time installing OpenBSD, you should check out the [[https://www.openbsd.org/faq/faq4.html][installation guide]], which goes over the process in detail. I'd also highly recommend checking out my [[{{< ref openbsd-introduction-talk >}}][Introduction to OpenBSD]] talk. * Configuring a Static IP It's very important to set a static IP on our VPN gateway. We do this so we always know where to find it on the network. We'll do this first. Setting a static IP in OpenBSD couldn't be simpler. For each interface on the machine, you can create a [[http://man.openbsd.org/man5/hostname.if.5][=hostname.if(5)=]] file with the name =/etc/hostname.=, where == is the name of the interface. Since we want to set a configure the interface =vio0=, the file we want is =/etc/hostname.vio0=. If your box was configured with DHCP, the file might contain a single line saying =dhcp=. We want to give the interface the static IP =192.168.0.11= with a =/24= subnet. We use [[https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing][CIDR notation]] here for convenience, but it's also possible to write out the full subnet mask after our IP, separated by a space. We open =/etc/hostname.vio0= and replace its contents with the following. #+BEGIN_SRC inet 192.168.0.11/24 #+END_SRC We'll also need to enter the IP of our network's router into the [[https://man.openbsd.org/man5/myname.5][=mygate(5)=]] file at =/etc/mygate=. #+BEGIN_SRC 192.168.0.1 #+END_SRC Then we set our desired DNS servers in [[https://man.openbsd.org/man5/resolv.conf.5][=resolv.conf(5)=]] located at =/etc/resolv.conf=. #+BEGIN_SRC lookup file bind nameserver 8.8.8.8 nameserver 8.8.4.4 #+END_SRC Now we run [[https://man.openbsd.org/man8/netstart.8][=netstart(8)=]] to reconfigure the interface according to the file we've just edited. #+BEGIN_SRC shell doas sh /etc/netstat #+END_SRC Now if we check [[http://man.openbsd.org/man8/ifconfig.8][=ifconfig(8)=]], we should see the interface has the correct IP. #+BEGIN_SRC shell ifconfig vio0 #+END_SRC #+RESULTS: : vio0: flags=8843 mtu 1500 : lladdr AA:BB:CC:DD:EE:FF : index 1 priority 0 llprio 3 : groups: egress : media: Ethernet autoselect : status: active : inet 192.168.0.11 netmask 0xffffff00 broadcast 192.168.0.255 * Configuring OpenVPN ** Installation First we have to install [[https://openvpn.net/][OpenVPN]], which is provided by the OpenBSD package manager. #+BEGIN_SRC shell doas pkg_add openvpn #+END_SRC ** VPN Profile Let's assume the VPN profile we've downloaded from our provider exists in =/root/profile.ovpn=. This could have been downloaded using [[https://man.openbsd.org/man1/ftp.1][=ftp(1)=]] or transferred on using [[https://man.openbsd.org/man1/sftp.1][=sftp(1)=]]. Let's say it also requires a username and password supplied by the user. For this example, the username is =user@example.com= and the password is =password=. To allow OpenVPN to login without us having to enter our password, we can modify the =auth-user-pass= directive to our =profile.ovpn= file. This will allow us run OpenVPN as a daemon. To do this we'll create a file called =/root/vpnpasswd.txt= containing our username, followed my our password on a separate line. #+BEGIN_SRC user@examples.com password #+END_SRC We then edit our VPN profile, adding the name of our password file after =auth-user-pass=. #+BEGIN_SRC auth-user-pass vpnpass.txt #+END_SRC Now we change their permissions to make sure they cannot be read or modified by other users on the system. #+BEGIN_SRC shell doas chmod 600 /root/profile.ovpn /root/vpnpasswd.txt #+END_SRC ** rcctl We can now set the OpenVPN daemon to launch at boot with our modified profile using =rcctl=. =rcctl= is a tool that comes with OpenBSD which modifies =/etc/rc.conf.local= on our behalf to ensure it's done properly. The use of =rcctl= is not strictly required, but highly recommended. #+BEGIN_SRC shell doas rcctl enable openvpn doas rcctl set openvpn flags --config /root/profile.ovpn doas rcctl start openvpn #+END_SRC - =rcctl enable openvpn=, enables the daemon at boot. - =rcctl set openvpn flags --config /root/profile.ovpn= sets the launch flags for =openvpn= to =--config /root/profile.ovpn=. This is an OpenVPN option that tells it to load its config from =/root/profile.ovpn=. - =rcctl start openvpn= starts the =openvpn= daemon. If things are configured correctly, we should now see a =tun= device in our =ifconfig=, and our traffic should be going through the VPN. To check this we can make a request to a service like https://icanhazip.com or https://ifconfig.so using the =ftp= command. #+BEGIN_SRC shell ftp -o- https://canhazip.com 2>/dev/null #+END_SRC It should output an IP that belongs to our VPN provider. We can also check =/var/log/daemon= to check that OpenVPN is outputting logs. OpenVPN should have already reconfigured the our routing table to send all of our traffic over the VPN connection, but how do we pass incoming traffic through it? * sysctl The first step is to allow the kernel to forward IP packets destined for other hosts. To set this option in the kernel we use the [[https://man.openbsd.org/man8/sysctl.8][=sysctl(8)=]] command. #+BEGIN_SRC shell doas sysctl net.inet.ip.forwarding=1 #+END_SRC We're also going to want to make this option persistent, so it remains even after rebooting. To do this we add the option to our [[https://man.openbsd.org/man5/sysctl.conf.5][=sysctl.conf(5)=]]. #+BEGIN_SRC shell doas sh -c 'echo "net.inet.ip.forwarding=1" >> /etc/sysctl.conf' #+END_SRC This can of course also be done with a text editor like =vi= or =mg=. * PF Rules At this point, we're forwarding the incoming packets out the VPN tunnel, but they have no method to find their way back to us. This is because when we're forwarding them, they still have their [[https://en.wikipedia.org/wiki/Local_area_network][LAN]] IP addresses (=192.168.0.X=) as the sender address. In order for these to successfully traverse the internet, they're going to need a [[https://en.wikipedia.org/wiki/Wide_area_network][WAN]] address. That's what you might call an external IP. To accomplish this, we use something called a [[https://en.wikipedia.org/wiki/Network_address_translation][NAT]] (Network Address Translation). This allows us to map many local (LAN) IP addresses to a single external (WAN) IP address. We do this using OpenBSD's firewall, PF. This is what our new [[http://man.openbsd.org/man5/pf.conf.5][=pf.conf(5)=]] will look like. #+BEGIN_SRC c set skip on lo block return # block stateless traffic # pass # establish keep-state # By default, do not permit remote connections to X11 block return in on ! lo0 proto tcp to port 6000:6010 # Port build user does not need network block return out log proto {tcp udp} user _pbuild ##################################### # VPN ##################################### ext_if = "vio0" vpn_if = "tun0" pass in on $ext_if pass out on $ext_if from self match out on $vpn_if from $ext_if:network to any nat-to ($vpn_if) pass out on $vpn_if #+END_SRC Let's go through this line by line to see what's going on. Something that's important to note with PF is that the last matching rule determines the fate of a packet. This means that if a packet matches a =block= rule, but then matches a =pass= rule afterwards and is not blocked again, the packet is allowed through, and vice versa. - =set skip on lo= [[https://man.openbsd.org/OpenBSD-6.7/pf.conf.5#set~14][Do not filter]] traffic coming over [[https://man.openbsd.org/man4/lo.4][loopback]] devices, this is a default rule and we can leave it. - =block return= [[https://man.openbsd.org/man5/pf.conf.5#block][Block]] any packet that doesn't match any =pass= rule. The =return= tells pf to block packets, but issue a =TCP RST= for [[https://en.wikipedia.org/wiki/Transmission_Control_Protocol][TCP]] packets, and =ICMP UNREACHABLE= for [[https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol][ICMP]] packets, instead of just dropping them. - =# pass= This rule is commented out, but left in for illustrative purposes. The default =pf.conf= passes any traffic that isn't explicitly blocked. By commenting this line out we are inverting that. Everything is blocked unless we explicitly pass it. - =block return in on ! lo0 proto tcp to port 6000:6010= This is a default rule, left in for security reasons. It stops other machines from being able to reach our X11 session, should we be running one. - =block return out log proto {tcp udp} user _pbuild= This is another default rule, left in for security reasons. It stops the =_pbuild= user from accessing the internet. This is to stop ports builds from accessing any resources online. - =ext_if = "vio0"= We use this macro to set the external interface name. This is done so we only have to set the name of the interface in one place. - =vpn_if = "tun0"= This is similar to the macro above, except it's for the VPN tunnel interface. - =pass in on $ext_if= [[https://man.openbsd.org/OpenBSD-6.7/pf.conf.5#pass][Pass]] all traffic coming in on our external interface. This is how we receive traffic from the network. - =pass out on $ext_if from self= Pass all traffic /originating from the VPN gateway/ out on our external interface. This will allow OpenVPN to communicate with the VPN server, but will not allow forwarded traffic out. Because of this, if the VPN connection ever fails, forwarded traffic will be unable to leave the gateway. This provides us with a sort of "kill switch". [[https://man.openbsd.org/OpenBSD-6.7/pf.conf.5#self][=self=]] expands to all IPs belonging to interfaces on our host machine. - =match out on $vpn_if from $ext_if:network to any nat-to ($vpn_if)= This is a big rule, let's break it down into smaller pieces. - =match= A [[https://man.openbsd.org/OpenBSD-6.7/pf.conf.5#match][match]] rule is usually used to apply options to a packet. It does not block or pass a packet itself, but lets PF know how to handle a packet once it is blocked or passed. Unlike =block= or =pass= rules, a single packet can match many =match= rules, and have them all apply. - =out on $vpn_if from $ext_if:network to any= This tells the =match= command which packets it should apply the option to. - =out on $vpn_if= Packets going out on =$vpn_if=, which gets evaluated to =tun0=. - =from $ext_if:network= Packets coming from =$ext_if:network=. Since =$ext_if= gets evaluated to =vio0=, it becomes =vio0:network=. [[https://man.openbsd.org/OpenBSD-6.7/pf.conf.5#:network][=:network=]] evaluates to the network attached to an interface. In our case, it becomes =192.168.0.0/24=. - =to any= Packets with any destination. - =nat-to ($vpn_if)= [[https://man.openbsd.org/OpenBSD-6.7/pf.conf.5#nat-to][Translate the IP addresses]] on the matched packets to the address on =$vpn_if=. In this case =$vpn_if= evaluates to =tun0=. Notice that =($vpn_if)= is in parentheses. This tells PF to re-evaluate the rule when the status of =$vpn_if= changes. Without this, if the VPN has to restart, and OpenVPN gets assigned a new IP, the entire firewall configuration would have to be manually reloaded. Even worse, if OpenVPN starts after PF and there was no IP assigned to =tun0=, the entire rule set would fail to load. With the parentheses, this rule will get updated as =tun0= get updated. This way PF is always using the IP address currently assigned to the interface, even if it changes. You might be wondering why we only apply the NAT on outbound connections. Since PF is a stateful firewall, we apply the NAT when we are establishing the outbound connection, and it will remember the mapping for returning packets automatically, including in UDP connections. - =pass out on $vpn_if= Pass packets out on the VPN tunnel interface. If you want to configure a more complex NAT, the PF FAQ has an [[https://www.openbsd.org/faq/pf/nat.html][excellent section]] covering different setups. After writing new PF rules, we can check our file for syntax errors before loading it using the =pfctl= command. #+BEGIN_SRC shell doas pfctl -nf /etc/pf.conf #+END_SRC Assuming there are no errors, we can then load the rule set. #+BEGIN_SRC shell dosa pfctl -f /etc/pf.conf #+END_SRC * Connecting Clients The final piece in the puzzle is connecting client to the VPN gateway. The method to do this varies depending on OS, and situation. Most tutorials online covering setting a static IP will also mention how to set the gateway. It's also possible to set a custom gateway on machines using DHCP with the =routes= command and various config options, but I won't go through that here to keep this section somewhat brief. ** OpenBSD On OpenBSD, if we have a static IP setup on our OpenBSD machine, like we did as part of this tutorial, it's as simple as replacing the contents of =/etc/mygate= with the IP of our new VPN gateway, and then either running ~doas sh /etc/netstart~ or rebooting. This is covered more in depth on the [[https://www.openbsd.org/faq/faq6.html][OpenBSD FAQ]]. ** FreeBSD FreeBSD's [[https://www.freebsd.org/doc/handbook/config-network-setup.html][handbook]] covers this topic very well. The basic steps are involve adding the following lines to your =/etc/rc.conf=: - ~ifconfig_[if]="inet XXX.XXX.XXX.XXX netmask YYY.YYY.YYY.YYY~, where =[if]= is the interface, and =X= and =Y= are your IP and netmask. - ~defaultrouter=~ where == is the gateway's IP. - ~nameserver ZZZ.ZZZ.ZZZ.ZZZ~ where =Z= is the DNS IP. ** Linux Most graphical interfaces for Linux desktop environments will have a networking section that will allow you to set the gateway without too much fuss. [[https://linuxconfig.org/how-to-configure-static-ip-address-on-ubuntu-20-04-focal-fossa-desktop-server][Here]]'s a walk through from https://linuxconfig.org. The situation for Linux servers is a bit more of a mess. As covered in the previously linked article, Ubuntu now likes to use the =netplan= framework, while others like Fedora may prefer =nmcli= as stated in [[https://linuxconfig.org/how-to-configure-static-ip-address-on-fedora-31][this]] article, or =network-scripts= as states [[https://www.systutorials.com/how-to-set-the-static-ip-address-using-cli-in-fedoracentos-linux/][here]]. If you take this path it's recommended you look into how it should be done on your specific Linux distribution. ** Containers/Jails Most graphical LXC container or Jail host software like [[https://www.proxmox.com/en/proxmox-ve][Proxmox-VE]] or [[https://www.freenas.org/][FreeNAS]] should have a fairly obvious place to put the IP when setting up the containers/jails. Otherwise you may want to consult the documentation of the container/jail management software you may be using ([[https://docs.docker.com/][docker]], [[https://github.com/iocage/iocage][iocage]], etc.) * Wrapping up After following these steps we should now have a properly setup VPN gateway on our network. All traffic flowing through it should go over the VPN, without us having to worry about the connection dropping. We also have the added benefit of everything still being on the same subnet, so the computers on the network can communicate with each other, regardless of which gateway they use.