1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
|
#+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, and 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,
including maybe 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 tunneled through the VPN
connection. It's also 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= with a desired static IP of =192.168.0.11= and a =/24=
subnet, although the interface and IP in your case will be differ.
* Hardware
To replicate my 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, as home internet connections
generally aren't the fastest. 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]], as they're affordable, have gigabit Ethernet, and great
OpenBSD driver support. In my case I created a virtual machine on a
server in my house running [[https://www.proxmox.com/en/][Proxmox]]. In my case, 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, which covers many more PF configuration examples.
* Install 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#Download][ installation guide]], which goes over the process in detail. You may
also want to consider checking out my [[{{< ref openbsd-introduction-talk >}}][Introduction to OpenBSD]] talk.
* Configuring a Static IP
The most important thing is to set a static IP, so it can be set as
the gateway for client machines. We'll set 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.<if>=, where =<if>= 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 =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. We open the file and replace its
contents with the following.
#+BEGIN_SRC
inet 192.168.0.11/24
#+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
dosa sh /etc/netstat vio0
#+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<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> 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. Normally we would install the =openvpn= package,
but due to an [[{{< ref "openvpn-issues-openbsd" >}}][issue with libressl]], we'll be installing the =mbedtls=
version. This problem should hopefully be resolved soon, so we'll
likely be able to use regular =openvpn= in the future.
#+BEGIN_SRC shell
doas pkg_add openvpn--mbedtls
#+END_SRC
Note: The =--mbedtls= is required to get the =mbedtls= flavour of
the =openvpn= package.
** 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, as most
commercial VPN providers authenticate users in this way. For this
example, the username is =user@example.com= and the password is
=password=.
To allow OpenVPN to login to the VPN without the us having to enter
our password, we can add the =auth-user-pass= directive to our
=profile.ovpn= file. This will allow us run OpenVPN as a daemon,
and restart it without having to type our username and password in.
To do this we can 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 following line somewhere.
#+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 profile.ovpn 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 your behalf to
ensure it's done properly. The use of =rcctl= is not strictly
required, but highly recommended.
#+BEGIN_SRC shell
doas rcctl set openvpn flags --config /root/profile.ovpn
doas rcctl enable openvpn
doas rcctl start openvpn
#+END_SRC
- =rcctl set openvpn flags --config /root/profile.ovpn= tells
=rcctl= to set the launch flags to the =openvpn= daemon to
=--config /root/profile.ovpn=. This is an OpenVPN option that
tells it to load its config from =/root/profile.ovpn=.
- =rcctl enable openvpn=, enables the daemon at boot.
- =rcctl start openvpn= starts the =openvpn= daemon.
If things are configured correctly, you should now see a =tun=
device in your =ifconfig=, and your traffic should be going through
the VPN. To easily check this you 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
This should output your current external IP address, which should
belong to your VPN provider.
* sysctl
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?
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)=]], which re-applies =sysctl= options on boot.
#+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=.
Setting =net.inet.ip.forwarding= to =1= tells the kernel to
forward any packets it receives that aren't destined for any of its
interfaces according to its routing table and firewall rules.
* 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 LAN
(Local Area Network) 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 WAN (Wide Area Network) address. That's
what you might call an external IP.
To accomplish this, we use something called a 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 X Windows 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, should we ever need to change it.
- =vpn_if = "tun0"= This is similar to the rule above, except 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
us/ out on our external interface, this will allow OpenVPN to
communicate with the VPN server without us having to worry about
accidentally passing forwarded traffic to the open internet
outside of the VPN connection, should OpenVPN ever fail. =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 either 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.
- =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.
|