summaryrefslogtreecommitdiffstats
path: root/content/posts/openbsd-vpn-gateway/index.org
blob: c8005e5a2ec8305ad8ce0f138e279ebf2943bd8a (plain) (blame)
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
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
#+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 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=. 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.<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 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 the file 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 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= package 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. 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 add 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 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 /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.

  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's [[https://www.freebsd.org/doc/handbook/config-network-setup.html][handbook]] covers this topic very well. The basic steps are:
  - Add a line ~ifconfig_<if>="inet

  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.

  Most graphical

** Linux

*** Docker
** FreeBSD
*** Jails