Self-hosted IPsec server for Android smartphone

Self-hosted IPsec server for Android smartphone

As censorship in Russia tighten, it become hard to access information on Internet. Depending on how ISP implements Russian laws it can not only block list of URLs provided by government, but also do a collateral damage, implementing some rough methods like blocking by IP. And since encrypted client hello (ECH) [1] coming to web servers all over the world, it probably will be worse.
So from time to time I found that some links that I follow from HN, Reddit, etc. is not working. Quite annoying! After some frustration and thinking I decided that it’s finally time to get a VPN!

Technical requirements

You probably think: “Why IPsec?” or “Why bother with self-hosted solution?”. Of course you can go and get ProtonVPN subscription and be done, but hey, that’s not fun! How about spend hours reading documentation, fighting bugs and correct your own errors, ha? So let’s get into some details on what I wanted to get and how I get there.
First of all, I am privacy concerned person. Not the one who wears a tinfoil hat and live in the woods, but if privacy-based solution require little to no effort – I’ll for it! I believe that there are respectable and trustable players on VPN market, but since self-hosted VPN server is not very hard thing, I choose it. For the same reason I like to have as less apps installed on my phone as I can. Also, I use an F-Droid [2] repository, so no Google or vendor (Samsung in my case) accounts registered. That’s why I wanted to use built-in Android VPN. Since it provide only PPTP and IPsec, I went for latter as my VPN solution.
And of course doing something by hand is a great way to learn things! Also, you can use blogging as some form of documentation that reinforce this learning process.
Based on that I decided to do this:

* spin up some cheap virtual private server (VPS)
* install modern stack of software on it, so it can act as an IPsec server
* use no NAT
* use Android built-in IPsec implementation

Turns out I can’t reach all that goals at the same time. That’s OK, you can’t know everything beforehand, so adapting your solution in the process is a normal way of life. So let’s talk a little bit about failures and what I’ve changed in my requirements on the way, before going into final solution.
To get some quick refresh of your IPsec knowledge (or get a quick introduction into the topic) I highly recommend this guide [3].


Network address translation is a technique familiar to any network engineer. And while most engineers agree that it is a hack, i.e. short-term solution for IPv4 scalability problem [4], some people still believe that NAT is a security mechanism [5]. Anyway, NAT is another layer of abstraction and complexity which make troubleshooting harder and configuration more error-prone.
So I thought I’ll get additional IPv4 and IPv6 addresses on my VPS and assign them to clients (smartphone). I chose Hetzner Cloud [6] not only for it’s price, but because from their documentation I thought that I can have additional IP addresses assigned to a VPS [7]. Turns out I was looking at the wrong page, I need other one [8]. This one says that additional addresses can be assigned only to dedicated servers. Since it makes price tag 10 times higher, I turned “no NAT” requirement into “as less NAT as I can”.
Since IPv6 on Hetzner turns out usable (more on it later), I had options of either:

* IPv6-only with NAT64 
* global IPv6 address + private IPv4 address with NAT44

I decided not go into NAT64 (maybe I return on it later) and go with old-fashioned NAT44.

Android VPN

For about 5 years or so I used Android smartphone with LineageOS [9]. It’s a FOSS version of Android, with privacy and security on the goals list. It’s a successor of famous CyanogenMod. You can’t buy smartphone with it, you need to install it by yourself. They support small number of devices, and there are unofficial builds for some more. That allows you not only to have no Google account, but even not have any Google app installed, including Google Play services [10]. I must say that it can brake some apps, but not ones came from F-Droid. Of course if you want you can have all Google stuff installed, most users do exactly this.
Unfortunately, early this year my smartphone broke and I needed to buy a new one. LineageOS supports mostly old phones, bringing them some extended longevity, so it’s hard (not impossible) to get a new LineageOS compatible smartphone. So I just bought new shiny middle-range Samsung smartphone with Android 11, uninstalled all bloatware I can, disabled many other apps and features and installed F-Droid.
After such a long introduction into my smartphone habits let’s turn to VPNs. Android 11 is first version that finally supports IKEv2! Remember, I want to use modern software? I can’t find any official changelog which mentions this, but here [11] are an article on XDA which covers new features of Android 11 developer preview 2. So, before 11th version, built-in IPsec support was restricted to IKEv1. Now, you have IKEv2 options of PSK, RSA or EAP-MSCHAPv2 authentication.
So why that fail? I was able to successfully connect to a server with PSK auth, but it looks like Android put VPN to sleep while phone is not in use. It makes it fail to answer dead peer detection (DPD) requests from server. After smartphone woke up it still thinks it’s connected, while server already deleted this connection. DPD can be disabled, but what to do when smartphone really lost Internet connection?
Another problem is that Android, by default, just drops VPN on network change, switching from Wi-Fi to mobile carrier for example. You can enable ‘always on VPN’ in settings, but when you need to disable such a connection, you need to reconfigure it! Not very handy.
I also faced some weird problem with fragmentation (UDP used for NAT traversal), but I will not go into details.
In the end it turns out that built-in IPsec implementation is almost unusable. At that point I thought about switching to OpenVPN or even WireGuard, but since I was already deep into IPsec I chose not to go this way and use 3rd party app for a VPN connection.

Refined technical requirements

Let’s revisit what was desired and update it then.

* spin up some cheap virtual private server (VPS)
* install modern stack of software on it, so it can act as an IPsec server
* use no NAT use global address for IPv6 and private address for IPv4 with NAT44
* use Android built-in IPsec implementation use 3rd party app for IPsec on Android

A little bit more details. I will authenticate server with a certificate, so we need to get one. Although it’s possible to authenticate client using certificate too, at that time I do not want to complicate things and will use username and password pair. Now let’s delve into some implementation details.


As I said earlier, I chose Hetzner Cloud for it’s price tag. Smallest VPS available (CX11 plan) is less than 3 EUR per month! Also I found that they manage IPv6 addressing pretty good. Since it’s 2021 and IPv6 numbers continues to grow [12], I think that IPv6 support is a must. Although not available at every home (I’m fortunate that my ISP does provide me with IPv6), situation with different clouds looks much better.
Still, I pictured in my head some ideal world where cloud provider gives your VPS with SLAAC for “main” address configuration and routes additional /64 by DHCPv6-PD, static configuration or some other means. Turns out that not the case. For example Digital Ocean, another cloud provider that I use, provides you /124 prefix. That’s 16 addresses, one of which assigned to VPS interface by cloud-init [13]. On the other hand Hetzner routes proper /64 prefix on your VPS’ link-local address, which can be used for containers or other means, but no “main” address to assign to VPS Internet-faced interface. That’s still much better than many other options on market.
I will not describe registration and VPS initialization process here, it’s pretty much straightforward, but let’s stop on OS choice for a moment. Hetzner provides you with some popular Linux and BSD distributions like Ubuntu, Fedora and FreeBSD. But I found interesting that there are also easy method of installing Arch Linux [14]. I’m very much like Arch Linux and use it as daily driver on my laptop/desktop, but always hesitates to try it on server. Looks like time has come! Of course you can go with other option, like more popular Ubuntu. In that case this guide will still mostly apply, but some file paths may be different, and of course package managers is not same (pacman vs apt).
Let’s turn to software and configuration of a server.


To build an IPsec server we need to install and configure a firewall/NAT app, and IPsec app. Also, since our server will authenticate with certificate we need to handle certificate installation and renewal. Let’s start with IPsec itself.
Looks like that there are 3 active implementations of IPsec/IKE stack available: Openswan, Libreswan and strongSwan, all of them in some way a descendant of FreeS/WAN. I didn’t spent much time choosing one, because I touched one of them at work a little bit (strongSwan), and almost every guide on Internet suggest using strongSwan app on Android. And it turns out that Arch Linux officially supports only strongSwan, but of course there are ways to install others. But if you want some bullet points to look at, here are some good, but not very recent comparison of Libreswan to strongSwan [15].
strongSwan comes with two configuration options as it’s documentation [16] points out: legacy stroke-based and modern vici-based. Again, searching through Internet you will find many good guides on strongSwan configuration using legacy method. And I found zero which goes through modern one. Since I try to use new and shiny things here I will go with modern vici method. Anyway, documentation goes thoroughly through both of them and provides many useful examples.
So let’s install strongSwan

sudo pacman -S strongswan

Configuration split up in two parts: overall strongSwan config and actual IPsec configuration. Let’s go through former first:

yman@anderstorp ~ > cat /etc/strongswan.conf
swanctl {
  load = pem pkcs1 x509 revocation constraints pubkey openssl random

charon-systemd {
  load = random nonce aes des md4 sha1 sha2 fips-prf pem pkcs1 curve25519 gmp x509 curl revocation hmac vici kernel-netlink socket-default eap-identity eap-mschapv2 updown openssl

This one is very simple, it’s configure set of plugins that charon (IKE daemon) and swanctl (IPSec management tool) will load upon startup. I found important to note ‘eap-*’ and ‘openssl’ plugins in charon-systemd part, which will be used for client authentication with EAP-MSCHAPv2 and server authentication with certificate. List can be made smaller or bigger, depends on what you need. I, for example, will not use updown plugin at the moment (calls external scripts on up and down events), but still has it on list.
Now to more complicated part:

yman@anderstorp ~ > sudo cat /etc/swanctl/swanctl.conf
connections {

   nat-t {
      # set of addresses server will listen to (comma separated)
      local_addrs =,2001:db8:644a:83d::2

      # method wich server will use to authenticate to client
      local {
         auth = pubkey
         certs = certificate.pem
         id =
      # method which client will use to authenticate to server
      remote {
         auth = eap-mschapv2
         eap_id = %any
      # SA configurations
      children {
         nat-t {
            local_ts =,::/0
            esp_proposals = aes192gcm16-aes128gcm16-ecp256-modp3072,aes192-sha256-ecp256-modp3072
      # server will always send it's certificate
      send_cert = always
      # server will not request certificates
      send_certreq = no
      # pools to assign addresses to clients (comma separated)
      pools = localv4,localv6
      # use IKEv2
      version = 2
      # force usage of encapsulation (ESP in UDP)
      encap = yes
      # DPD config
      dpd_delay = 30s
      dpd_timeout = 90s
      # IKE security proposals
      proposals = aes192gcm16-aes128gcm16-prfsha256-ecp256-ecp521,aes192-sha256-modp3072

# secrets used to authenticate clients, one section per client
secrets {
   eap-client {
      id = $CLIENT_LOGIN
      secret = $CLIENT_PASS

# IP pools to assign addresses to clients from, one section per pool
pools {
    localv4 {
        addrs =
        dns =,
    localv6 {
        addrs = 2001:db8:644a:83d::8-2001:db8:644a:83d::f
        dns = 2620:fe::fe,2620:fe::9

This config has 3 sections: connections, secrets and pools. Let’s start from the beginning.
In the ‘connections’ section you can define different types of connections, which use different authentication methods, traffic selectors (TS), security associations (SA), etc… Since I will have only one client there are only one connection named ‘nat-t’, which is only a name of a connection here, you can name it ‘dinosaur’, for example. It defines many different things, let’s stop on interesting ones.
‘local’ defines how server will authenticate itself to clients. I use certificate here, which we will acquire later.
For clients (‘remote’ part) EAP-MSCHAPv2 will be used, and server will accept any client ID.
‘pools’ define IP pools that will be used to assign addresses to clients. Since I use both IPv4 and IPv6 there are two pools. In case of IPv6 I use a range which is a subset of /64 provided by cloud.
‘encap’ here will force encapsulation for every connection, producing ESP in UDP packets. Despite strongSwan can detect NAT on it’s way and enable encapsulation, I will force it since smartphone most of the time will be placed behind NAT (or maybe couple of NATs).
‘proposals’ at the bottom defines list of sets of security algorithms used to form IKE connection between server and client. Elements in list is comma separated, while elements in set is hyphen separated. If you familiar with Cisco terminology this will be a ‘transform-set’, used to build phase 1 tunnel. You can experiment with this set or just omit it at all in which case it will be set to ‘default’ and use some predefined set, which considered safe. Note that your clients may not support latest and greatest algorithms or can consume to much CPU and/or power doing them.
DPD statements configure dead peer detection – mechanism that delete connections for unreachable clients, very handy when you clients is not servers with five nines availability.
Now to that ‘children’ subsection. It actually defines security associations and since you can have many – ‘nat-t’ here is again a variable name of a section/SA. We already talked about proposals, so let’s look at other statement here, it’s very important. ‘local_ts’ defines a traffic selector which client will use to decide whether to send traffic into a tunnel. So if you put here, for example, ‘’, client will only send traffic to through this connection. Since we provide client with full Internet access there are default routes for both IPv4 and IPv6. I actually spent couple of hours trying to debug my connection, where client successfully established tunnel, set default route pointing to it (!), but not sending anything anywhere, because I just omitted this line. If it’s omitted TS will be equal to server IP. There are ‘remote_ts’ counterpart, but since there are nothing behind client it’s OK to omit it. It’s required for site-to-site scenarios. One caveat with strongSwan Android app I hit is that if your client has IPv6 connectivity, but you do not provide it through IPsec, client will use it through carrier/Wi-Fi. And since IPv6 is preferred protocol, some of your traffic will not go into a tunnel. Looks like built-in Android IPsec block this behaviour. So if you do not have IPv6 on server, or do not want to provide it to clients it’s probably better to still advertise it, but block in firewall. Good-written apps can deal with it.
Section ‘secrets’ define client secrets, one subsection per client. Again ‘eap-client’ is just a name, replace it with whatever you want. Devise some user login (id) and password (secret). Don’t forget to make your password strong.
Finally, ‘pools’ identify available IP address pools. Again, one subsection per pool, and ‘localv4’, ‘localv6’ are just a names. You can provide some additional options for your pools, as you can see I assigned Quad9 [17] DNS resolvers here.
And one important detail, since it didn’t specified, strongSwan will use tunnel mode by default.
It’s better to tweak permissions on this file with sudo chmod 600 /etc/swanctl/swanctl.conf since it’s contain sensitive information.


Now let’s deal with our little IPv6 problem. As I mentioned before, Hetzner provides only /64 IPv6 prefix. Since I need IPv6 address on a VPS and want to provide clients with IPv6 I need two subnets, and forwarding between them. Or do I? After little experiment I settled on a scheme, that someone can dub ‘unnumbered’. I moved an IP address from Internet-faced interface to dummy interface (loopback), and then assigned a subset range of addresses to IPsec IPv6 pool (which we saw before). Let’s look at a network config, which managed by systemd-networkd there.

yman@anderstorp ~ > cat /etc/systemd/network/
### Hetzner Online GmbH installimage


yman@anderstorp ~ > cat /etc/systemd/network/15-dummy0.netdev
# always match

Description=loopback interface

yman@anderstorp ~ > cat /etc/systemd/network/


There are three files lying in /etc/systemd/network. First one ( was already there and defined IP configuration for Internet-faced (or main) interface of VPS. I just commented out ‘Address’ statement, which removes IPv6 global address from this interface. Next two files was created by me. 15-dummy0.netdev defines new virtual interface of dummy type with name dummy0, and that’s all. defines IP configuration for dummy0. There I assigned same address that was commented out in first file, but use /128 length.
Since routing in IPv6 done with help of link-local addresses, there are no need to put global address on main interface. Link-local addresses configured by default using modified EUI-64 [18].
After changing network configuration we must reload it.

sudo networkctl reload

One more thing we must deal with – is enable traffic routing, making this server a router!

sudo sysctl net.ipv6.conf.all.forwarding=1
sudo sysctl net.ipv4.ip_forward=1

To make this permanent put this config in some file in /etc/sysctl.d directory, like /etc/sysctl.d/10-routing.conf.



We got to this part only now, but in general it’s a bad idea running anything on Internet without a firewall. So better do it’s first actually.
OK, remember I wanted to use modern software, huh? If you deal with Linux servers on daily basis you probably use iptables as your firewall (not really correct term, because iptables is a framework for firewall configuration). But for quite sometime now iptables and friends (ip6tables, ebtables, arptables) considered as legacy tools. World slowly moving towards using nftables now [19]. Debian 10 by default use transitional packages, which translates iptables into nftables [20] and thinking about switching to pure nftables in near future. Ubuntu switched to nftables since 20.10 [21]. RHEL and Fedora also doing this for sometime now.
I myself use nftables on all my personal machines running Arch Linux, Debian and Ubuntu, so it’s a natural choice for me. If you see nftables for first time it’s better to read introduction on their wiki [22]. One strange thing that struck me on Arch, is that it comes with iptables, not nftables. Moreover you can’t uninstall iptables, because for some unknown reason it’s required by iproute2 package. And I can hardly imagine usable server without iproute2. So to not turn your firewall into a mess – it’s better to disable iptables. But first let’s install nftables.

sudo pacman -S nftables

I will not explain here how to use nft tool to configure your firewall, use documentation if you want to learn this. As of now you can just put firewall config into /etc/nftables.conf.

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;
        iif "lo" accept
        ct state established,related accept
        ct state invalid drop
        icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-request, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
        icmp type { destination-unreachable, echo-request, router-advertisement, time-exceeded, parameter-problem } accept
        tcp dport ssh accept
        udp dport 500 accept
        udp dport 4500 accept
        udp dport 33434-33625 reject
        counter drop

    chain output {
        type filter hook output priority 0; policy accept;

    chain forward {
        type filter hook forward priority 0; policy drop;
        ip daddr iif "ens3" accept
        ip saddr oif "ens3" accept
        ip6 daddr 2001:db8:644a:83d::/64 iif "ens3" accept
        ip6 saddr 2001:db8:644a:83d::/64 oif "ens3" accept


table ip nat {
    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;
        ip saddr oif "ens3" snat to

    chain prerouting {
        type nat hook prerouting priority filter; policy accept;

Not so many lines, but there certainly some interesting things to look at. As you can see there are two tables: ‘inet filter’ and ‘ip nat’. ‘inet’ and ‘ip’ defines address family here, former for both IPv4 and IPv6 and latter for IPv4 only. Yes, you can have single table in one place for both IPv4 and IPv6! That’s one of cool features of nftables. ‘filter’ and ‘nat’ just a names here, they can be whatever you want, but good names clearly identifies purpose, right?
Table ‘inet filter’ defines three chains: ‘input’, ‘output’ and ‘forward’. Again this is just a names. Critical part in all of them is the next line of the form ‘type filter hook * priority 0; policy *;’. That statement actually define where in Netfilter this chain will be connected [23]. In this statement ‘hook output’, for example, defines exact Netfilter hook, and not just random name. That’s how you can have chain ‘pizza’ attached to hook ‘input’. Main idea here is that you have multiple tables with multiple different chains attached to same hook with different priorities. But we will keep things simple here. ‘policy’ in that statement defines what to do with packets by default. For ‘input’ and ‘forward’ we use ‘drop’, for ‘output’ – ‘accept’.
Chain ‘input’ here defines accept rule for local traffic (‘iif “lo”‘), enables stateful firewalling through connection tracking (lines with ‘ct state’), choose some ICMP messages to accept (note how there are different lines for v4 and v6) and allows SSH and IPsec (UDP ports 500 and 4500) traffic from anyone. Next to last line rejecting traceroute requests instead of dropping them, so this server can be visible in standard UDP traceroute. Last line enables counter for all other (dropped) traffic. Unlike iptables, nftables do not count traffic for every rule by default.
Chain ‘output’ is empty and ‘forward’ is pretty simple – there are just our networks used for IPsec clients.
Table ‘ip nat’ must be easy for you now. We just have SNAT rule in postrouting chain, which translates our IPsec pool into address on main interface.
After that file is saved, we can disable iptables and enable nftables.

sudo systemctl stop iptables.service
sudo systemctl disable iptables.service
sudo systemctl enable nftables.service
sudo systemctl start nftables.service

Server certificate

Time to get a certificate for our IPsec server. Of course for this purpose it’s OK to generate self-signed one, but since there amazing Let’s Encrypt [24], why not use it? To get free Let’s Encrypt certificate we need certbot – a certificate handling tool.

sudo pacman -S certbot

To get a certificate our server must be accessible to outside world on port 80. Since this is not web server, port 80 is blocked by firewall. At the moment we need to add simple line in ‘table inet filter chain input’ before final ‘count drop’ statement.

tcp dport 80 accept

Put it in /etc/nftables.conf and reload ruleset.

sudo systemctl reload nftables.service

Now, run certbot

sudo cerbot certonly --standalone

Answer it’s questions (nothing hard) and it will put certificates into /etc/letsencrypt/live/FQDN/ directory. For them be used by strongSwan let’s just make some symlinks.

sudo ln -s /etc/letsencrypt/live/FQDN/chain.pem /etc/swanctl/x509/ca.pem
sudo ln -s /etc/letsencrypt/live/FQDN/cert.pem /etc/swanctl/x509/certificate.pem
sudo ln -s /etc/letsencrypt/live/FQDN/privkey.pem /etc/swanctl/private/key.pem

Now strongSwan can finally get to work. Let’s start it.

sudo systemctl enable strongswan.service
sudo systemctl start strongswan.service

Great! But we are not done yet. Certificates has expire time, and Let’s Encrypt certificates expire quickly. So we need to automate it’s renewal. That’s not very hard, I use systemd timers for it, since I very like them! But of course it can be done with cron. Here’s the files.

yman@anderstorp ~ > cat /etc/systemd/system/certbot_renew.service
Description=renew lets encrypt certificate



yman@anderstorp ~ > cat /etc/systemd/system/certbot_renew.timer
Description=Weekly renewal of lets encrypt certificate

OnCalendar=Wed 06:30:00


They are pretty simple, so I will not go too deep. Service file defines action that will be run, while timer file defines when it will be run. As you can see, I schedule it for every Wednesday 6:30 AM. Actual heavy lifting outsourced to a bash script in my home directory, here it is:

#!/usr/bin/env bash

set -euo pipefail


"$NFT_BIN" insert rule inet filter input handle "$BOTTOM_HANDLE" tcp dport 80 accept
"$CERTBOT_BIN" renew --verbose
systemctl reload nftables.service

You can actually omit this script and just put /usr/bin/certbot renew --verbose at ExecStart= line of service file instead. You need to keep port 80 opened by firewall to do so. But I prefer keep it closed, since I have no intention to serve Web stuff from this machine. What this script done is put port 80 rule before count drop line in firewall using a ‘handle’ number, which identifies this drop rule. It renew certificate afterwards, and reload ruleset closing port 80 again. You can see rules handles with

sudo nft -a -n list table inet filter

And if will use this script, don’t forget to make it executable with chmod. Timer must be enabled before it can do something.

sudo systemctl daemon-reload
sudo systemctl enable certbot_renew.timer
sudo systemctl start certbot_renew.timer

‘daemon-reload’ line here required so systemd reread files on disk, since we changed them by adding new ones. You can also get current status of timers with

systemctl list-timers

Now we can finally turn to client, an Android smartphone.

Android strongSwan

Unfortunately strongSwan Android app is not in F-Droid repo. Sure, you can grab it from Google Play Store, but since I don’t use it I went with other option. Great news is that app available to download from strongSwan site itself [25]. Installing it by hand put some maintenance hurdle on me, but I’m OK with it.
Configuration is pretty simple. In the app, tap on big ‘ADD VPN PROFILE’ button and fill available fields:
* Server – FQDN or IP of server
* VPN Type – IKEv2 EAP (Username/Password)
* Username and Password
* Profile name – just a name of your profile
Done! Now taping on a profile will connect to it – your traffic is now IPsec’ed!
On a server side you can see current status and some data with this commands:

sudo swanctl --list-conns
sudo swanctl --list-sas
sudo ip xfrm state


If you went through the whole guide you can now see how it’s different from most others available on the net. To sum it up:
* strongSwan configured through modern vici/swanctl mechanism
* nftables used instead of iptables
* IPv6 support included
* certificate renewal done with systemd timer
I can’t say that it better than others, and sometimes it can be even harder to understand if you never use swanctl or nftables. That’s just another way of doing it. Hope it can help someone. All configs can be found on my GitHub [26], but remember to replace variables with yours.


[1] ECH on Wikipedia
[2] F-Droid
[3] An illustrated guide to IPsec
[5] Is NAT a security feature?
[6] Hetzner Cloud
[7] Additional IP addresses on Hetzner docs
[8] IP addresses on Hetzner docs
[9] LineageOS
[10] Google Play services
[11] Android 11 DP2 changes
[12] An IPv6 update for 2020
[13] cloud-init
[14] Install Arch Linux on Hetzner Cloud
[15] Libreswan to strongSwan comparison
[16] strongSwan user documentation
[17] Quad9
[18] Modified EUI-64
[19] nftables
[20] Debian 10 relese notes/network filtering
[21] Ubuntu 20.10 release notes
[22] nftables quick reference
[23] Netfilter hooks
[24] Let’s Encrypt
[25] strongSwan for Android downloads
[26] configs on GitHub

Comments are closed.