OpenVPN Gateway with PiHole and DNSCrypt

The goal is to set up a server with dnsmasq as DNS and DHCP server, Pi-Hole to manage the host entries, DNS-Crypt to encrypt the DNS requests and a VPN gateway.
The whole network should be able to be managed centrally via the RaspberryPi.

  • For Windows I would recommend the Bitvise SSH Client, which also implements SFTP for file transfer.
  • I also assume that all commands are executed as root and Raspbian Buster is used as operating system.
  • I use Windscribe, because I like their log policy, they offer a Stealth VPN protocol and I was able to get a Lifetime Premium license. The offer is sold out, but you should always look for similar ones if you have projects like this in mind. BTW.: This post is not sponsored.

This how-to is also available in german.

Enforce system:

1.Install updates:

apt-get update && apt-get upgrade

2.Create a new user:

adduser <Username>

3.Add the new user to group sudo:

adduser <Username> sudo

4.Delete the “pi” user :
Reconnect to the Raspberry, using the new user you just created.

deluser pi

5.Change the password of the new user and root:

passwd <Username>
passwd root

6.Change SSH config :
Uncomment/paste in /etc/ssh/sshd_config

Port <Change freely>
Protocol 2
PermitRootLogin no
DebianBanner no

Configure and install DNSCrypt:

1.Download and extract DNSCrypt-Proxy:
Newest release : https://github.com/jedisct1/dnscrypt-proxy/releases/

cd /etc
wget https://github.com/DNSCrypt/dnscrypt-proxy/releases/download/<Release>.tar.gz
tar -xf dnscrypt-proxy-linux_arm-x.x.x.tar.gz
mv linux-arm dnscrypt-proxy
rm dnscrypt-proxy-linux_arm-x.x.x.tar.gz

2.Configure DNSCrypt:
You can either use lists or pic every server yourself.
Using the require_ filters you can filter the lists (declarative).
Using DNS relays anonymizes your DNS requests by routing them to the actual server via the specified relay server. You should choose a relay server maintained by a different entity than the DNS server.

cd dnscrypt-proxy
cp example-dnscrypt-proxy.toml dnscrypt-proxy.toml
/etc/dnscrypt-proxy/dnscrypt-proxy.toml (CHANGES)

server_names = [’ <SERVER NAME1>’, ’ <SERVER NAME2>’] ## If you want to use specific servers
ipv6_servers = false ## “true” if your VPN service supports IPv6
listen_addresses = [‘127.0.0.1:5300’, ‘[::1]:5300’] ## Pi-Hole already uses Port 53
require_dnssec = true # I would recommend it for security reasons
require_nolog = true ## I would recommend it for privacy reasons
fallback_resolver = ‘176.126.70.119:53’ ## OpenNIC; You can use any server you trust
ignore_system_dns = true ## Pi-Hole is our system resolver, where we used DNSCrypt-Proxy (Loop)

##Insert below “[sources]”:
[sources.’<LIST NAME>’]
urls = [’<List URL>’, ‘<BACKUP URL>’]
minisign_key = ‘<MINISIGN_KEY>’
cache_file = ‘<CACHE FILE (CUSTOM)>’

##Insert below “[anonymized_dns]”:
routes = [
{ server_name=’ <SERVER NAME1>’, via=[’ <RELAY NAME>’,<…>] },
{ server_name=’<SERVER NAME2>’, via=[’ <RELAY NAME>’,<…>] }
]

Server/Relay lists (“Sources”)
More info about the servers

3.Install and start the service:

./dnscrypt-proxy -service install
./dnscrypt-proxy -service start

4.Check your configuration:

./dnscrypt-proxy -resolve google.com

The output should look something like this:

Resolving [google.com]
Domain exists:  yes, 4 name servers found
Canonical name: google.com.
IP addresses:   216.58.206.110, 2a00:1450:4009:810::200e
TXT records:    docusign=05958488-4752-4ef2-95eb-aa7ba8a3bd0e v=spf1 include:_spf.google.com ~all facebook-domain-verification=22rm551cu4k0ab0bxsw536tlds4h95
Resolver IP:    xxx.xxx.xxx.xxx

6.Allows the service to bind to the specified port while being run as non-root user:

setcap cap_net_bind_service=+pe dnscrypt-proxy

5.Start the service on boot:

systemctl enable dnscrypt-proxy

Setup Pi-Hole :

1.Install Pi-Hole:

curl -sSL https://install.pi-hole.net | bash

Our code is completely open, but piping to bash can be dangerous. For a safer install, review the code and then run the installer locally.

Interface : eth0
DNS Server : Doesn’t really matter
Protocol : Activate IPv4 and IPv6
IP : IP of eth0 (usually already properly configured)
Gateway : IP of your Router (usually already properly configured)
Log Queries : Doesn’t really matter
Web interface : ON

2.Activate DNSSEC :
If DNSSEC is activated in dnscrypt-proxy, you should activate it in dnsmasq too.

echo "proxy-dnssec" >> /etc/dnsmasq.d/dnscrypt.conf

3.Change the web interface’s password:

pihole -a -p

Setup the OpenVPN gateway:

1.Configure the Interface:
In case you don’t already have your interface set up during the Pi-Hole setup, paste at the and overwrite any existing static configuration for eth0 in /etc/dhcpcd.conf:

interface eth0
static ip_address=192.168.0.2/24 ##IP for this server
static routers=192.168.0.1 ##IP of your router
static domain_name_servers=127.0.0.1

2.Restart the network interfaces:

service networking restart

3.Install OpenVPN:

apt install openvpn

4.Download the OpenVPN config files from your VPN provider

5.Move your config files to /etc/openvpn/:

If your server configuration is saved as <Server.conf> in /etc/openvpn/ the connection as automatically initialized during boot.

cp <Server.ovpn> /etc/openvpn/*Server*.conf
mkdir /etc/openvpn/server

If those files exist:

mv <CACertificate.crt> /etc/openvpn/server/
mv <UserCertificate.crt> /etc/openvpn/server/
mv <PrivateKey.key> /etc/openvpn/server/

6.You probably don’t want to enter your VPN credentails every time:

Insert in /etc/openvpn/<Server>.conf:

auth-user-pass /etc/openvpn/server/passwd.conf

Create passwd.conf:

/etc/openvpn/server/passwd.conf

<LOGIN NAME>
<LOGIN PASSWORT>

Only allow root to access the file:

chown root:root /etc/openvpn/server/passwd.conf
chmod 600 /etc/openvpn/server/passwd.conf

7.Depending on the files you got from your VPN provider you may have to configure certificates or keys:

Insert in <Server>.ovpn:

ca /etc/openvpn/server/<CACertificate>.crt
cert /etc/openvpn/server/<UserCertificate>.crt
key /etc/openvpn/server/<PrivateKey>.key

If you are using Windscribe you can skip step 3 to 7 and install the linux client. You should definitely deactivate the inbuilt firewall.

8.Activate IP forwarding and some security features:

Uncomment/paste in /etc/sysctl.conf:

#IP Forwarding
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding=1 ##If your VPN service supports IPv6
#IP Spoofing protection
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
#Ignore ICMP broadcast requests
net.ipv4.icmp_echo_ignore_broadcasts = 1
#Ignore ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
#Ignore send redirects
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
#Disable source packet routing
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0
# Block SYN attacks
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 2048
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 5

More info about the settings:
IP Spoofing
ICMP broadcast requests
ICMP redirects
Source packet routing
SYN attacks

Load these changes:

sysctl -p /etc/sysctl.conf

9.Configure iptables:

iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A FORWARD -i eth0 -o eth0 -j REJECT
iptables -A FORWARD -s 192.168.0.0/24 -o tun0 -j ACCEPT
iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE

##If your VPN service supports IPv6
ip6tables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
ip6tables -A FORWARD -i eth0 -o eth0 -j REJECT
ip6tables -A FORWARD -s fe80::/10 -o tun0 -j ACCEPT
ip6tables -t nat -A POSTROUTING -o tun0 -j MASQUERADE

10.Save this configuration:

Install ‘iptables-persistent’

apt install iptables-persistent

Save the current iptables configuration (In case you didn’t already do it during the installation)

iptables-save > /etc/iptables/rules.v4
iptables-save > /etc/iptables/rules.v6 ## If your VPN service supports IPv6 

11.Configure DHCP and DNS:

Turn off the DHCP server of your router first.
Change in /etc/pihole/setupVars.conf:

DHCP_ACTIVE=true
DHCP_ROUTER=<IP of the VPN Gateway (eth0)>
DHCP_rapid_commit=true
PIHOLE_DNS_1=127.0.0.1#5300
PIHOLE_DNS_2=::1#5300
DNSSEC=false

Restart pihole to apply all changes:

service pihole-FTL restart

To test everything, open ipleak.net in your Browser to check your IP address and DNS servers.

                            Optional settings:

System:

unattended-upgrades:

Once the server is set up you shouldn’t have to worry about security updates.
unattended-upgrades frequently checks for - and installs - security updates.

apt-get install unattended-upgrades apt-listchanges
dpkg-reconfigure -plow unattended-upgrades

dnsmasq :

Exclude Devices:

/etc/dnsmasq.d/02-pihole-dhcp.conf

dhcp-authoritative

#IP Range 1
dhcp-range=192.168.0.X,192.168.0.X,24h

#IP Range 2
dhcp-range=set:tag0,192.168.0.X,192.168.0.X,24h

#Settings of IP Range 1
dhcp-option=3,192.168.0.2
dhcp-option=6,192.168.0.2

#Settings of IP Range 2
dhcp-option=tag:tag0,3,192.168.0.1
dhcp-option=tag:tag0,6,192.168.0.1

dhcp-leasefile=/etc/pihole/dhcp.leases
#quiet-dhcp
domain=lan

dhcp-rapid-commit

Write-protect this file so you can’t accidentally overwrite it using the Pi-Hole DHCP configurator.
To unlock the file replace the ‘+’ by a ‘-’.

chattr +i 02-pihole-dhcp.conf

Align IP addresses in the IP range’s to the devices.

04-pihole-static-dhcp.conf

dhcp-host=<MAC address>,<IP address>,<COOSE A NAME>
dhcp-host=XX:XX:XX:XX:XX:XX,192.168.0.1,Computer

Example

dhcp-authoritative

#Guests (VPN/Adguard DNS)
dhcp-range=192.168.0.200,192.168.0.225,24h
dhcp-option=3,192.168.0.2
dhcp-option=6,176.103.130.130,176.103.130.131

#VPN (Local DNS)
dhcp-range=set:tag0,92.168.0.10,192.168.0.99,24h
dhcp-option-force=tag:tag0,3,192.168.0.2
dhcp-option=tag:tag0,6,192.168.0.2

#Direct (Local DNS)
dhcp-range=set:tag1,192.168.0.100,192.168.0.199,24h
dhcp-option=tag:tag1,3,192.168.0.1
dhcp-option=tag:tag1,6,192.168.0.2

dhcp-leasefile=/etc/pihole/dhcp.leases
#quiet-dhcp
domain=lan

dhcp-rapid-commit

IP-Tables :
Set up a (relatively) simple Firewall:

Thanks to iptables-persistent editing the configuration is simple:

/etc/iptables/rules.v4

*filter
:INPUT DROP
:FORWARD DROP
:OUTPUT ACCEPT

-A INPUT -i lo -j ACCEPT
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT

-A INPUT -s 192.168.178.0/24 -p udp --dport 53 -j ACCEPT
-A INPUT -p icmp -j ACCEPT
-A INPUT -p udp --sport 67:68 --dport 67:68 -j ACCEPT
-A INPUT -s 192.168.178.0/24 -p tcp --dport 80 -j ACCEPT
-A INPUT -s 192.168.178.0/24 -p tcp --dport <SSH Port> -j ACCEPT
-A INPUT -s 192.168.178.0/24 -p tcp --dport 53 -j ACCEPT

-A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT

#Allow internet connections only trough following ports (HTTP,HTTPS,DNS,FTP,SFTP,IMAP,IMAP-TLS)
#-A FORWARD --match multiport ! --dports 80,443,53,20,115,143,993 -s 192.168.178.0/24 -o tun0 -j #DROP

-A FORWARD -s 192.168.178.0/24 -o tun0 -j ACCEPT

COMMIT

*nat
:PREROUTING ACCEPT
:INPUT ACCEPT
:POSTROUTING ACCEPT
:OUTPUT ACCEPT

-A POSTROUTING -o tun0 -j MASQUERADE

COMMIT

/etc/iptables/rules.v6

*filter
:INPUT DROP
:FORWARD DROP
:OUTPUT ACCEPT

-A INPUT -i lo -j ACCEPT
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT

-A INPUT -m state --state NEW -s fe80::/10 -p udp --dport 53 -j ACCEPT
-A INPUT -p ipv6-icmp -j ACCEPT
-A INPUT -m state --state NEW -p udp --sport 67:68 --dport 67:68 -j ACCEPT
-A INPUT -m state --state NEW -s fe80::/10 -p tcp --dport 80 -j ACCEPT
-A INPUT -m state --state NEW -s fe80::/10 -p tcp --dport <SSH Port> -j ACCEPT
-A INPUT -m state --state NEW -s fe80::/10 -p tcp --dport 53 -j ACCEPT

-A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT

#Allow outgoing connections only trough following ports (HTTP,HTTPS,DNS,FTP,SFTP,IMAP,IMAP-TLS)
#-A FORWARD -s fe80::/10 --match multiport ! --dports 80,443,53,20,115,143,993 -o tun0 -j DROP

-A FORWARD -s fe80::/10 -o tun0 -j ACCEPT

COMMIT

*nat
:PREROUTING ACCEPT
:INPUT ACCEPT
:POSTROUTING ACCEPT
:OUTPUT ACCEPT

-A POSTROUTING -o tun0 -j MASQUERADE

COMMIT

What does it do?
  • Drop incoming connections by default
  • Allow packages being sent to the loopback interface
  • Allow packages of self-established or to those related connections
  • Allow packet forwarding from your LAN to eth0
  • Allow packages of incoming DNS requests (UDP) from your LAN
  • Allow ICMP packages
  • Allow DHCP packages
  • Allow http connections from your LAN (PiHole web interface)
  • Allow SSH connections from your LAN (If you often experiment with your network, you should consider removing “-s 192.168.0.0/24” to be always able to access your server)
  • Allow packages of incoming DNS requests (TCP) from your LAN
  • Only allow packages from your LAN, sent over one of the specified ports (optional)
  • Accept packets by default
  • Replace your local by your public IP and the other way around (Essentially how all routers work)

Create rules for specific devices:

Since the devices connect to the internet over the Raspberry your routers filters are being bypassed.

Important: IP-Tables rules are processed in the given order. To block a request, the corresponding rule must be set before the rule that generally allows requests of this type.

Specify the device:

The safest way is to use the module ‘mac’, because the MAC address usually (Windows, Android, IOS, Linux) is ether not changeable at all, only with administrator rights or root access.

This rule blocks a request if the MAC address of the requesting device is “81:7d:22:a2:3e:7d”.

-A INPUT -m mac --mac-source 81:7d:22:a2:3e:7d -j REJECT

Time limit:

The most direct solution is via the ‘time’ module.
This rule blocks a request if it’s between 22:00 and 06:00, Sunday and Thursday:

-A INPUT -m time --timestart 22:00 --timestop 06:00 --weekdays Sun,Mon,Tue,Wed,Thu -j REJECT

Another possibility would be to create several files and load them via cron.

Sample configuration for /etc/cron.d/iptables

00 00 * * * root PATH="$PATH:/usr/sbin/iptables-restore" iptables-restore /etc/iptables/rules-test.v4
00 00 * * * root PATH="$PATH:/usr/sbin/ip6tables-restore" ip6tables-restore /etc/iptables/rules-test.v6

  • *     *     *     *  Command to be executed
    

| | | | |

| | | | ±---- Weekday (0 - 7) (Sunday corresponds to 0 and 7)

| | | ±------ month (1 - 12)

| | ±-------- day (1 - 31)

| ±---------- hour (0 - 23)

±------------ minute (0 - 59)

Don’t forget to set your local time-zone first. That made me desperate.

Block certain Domains:

This rule discards all UDP DNS requests other than “wikipedia.com”. The DNS queries are usually made exclusively over UDP, but TCP requests are also possible.

-A INPUT -p upd --dport 53 -m string ! --string "wikipedia.com" --algo bm -j DROP

It is also possible to filter using Regex. To do this, you must first install the Algorithm. The Regex algorithm is much more complex than the Boyer Moore algorithm. The filter option should therefore be placed as far back as possible rule, so that it can be skipped for non-applicable packets without even checking for the Regex string.

This rule filters all UDP DNS requests for “.googlevideo.” and “googlevideo.*”. The syntax of the string is “/<REGEX>/”.

-A INPUT -p udp --dport 53 -m string --string "/(.*\.|)googlevideo\..*/" --algo regex -j DROP
Sample configuration

*filter
:INPUT DROP
:FORWARD DROP
:OUTPUT ACCEPT

-A INPUT -i lo -j ACCEPT
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT

-A INPUT -p udp --dport 53 -m mac --mac-source 81:7d:22:a2:3e:7d -m time --timestart 00:00 --timestop 06:00 --weekdays Sun,Mon,Tue,Wed,Thu -m string ! --string “/(..|)whatsapp.(net|com)|(..|)(signal|whispersystems).org/” --algo regex -j DROP

-A INPUT -s 192.168.0.0/24 -p udp --dport 53 -j ACCEPT
-A INPUT -p icmp -j ACCEPT
-A INPUT -p udp --sport 67:68 --dport 67:68 -j ACCEPT
-A INPUT -s 192.168.0.0/24 -p tcp --dport 80 -j ACCEPT
-A INPUT -s 192.168.0.0/24 -p tcp --dport 22 -j ACCEPT
#-A INPUT -s 192.168.0.0/24 -p tcp --dport 53 -j ACCEPT

#Allow outgoing connections only trough following ports (HTTP,HTTPS,DNS,FTP,SFTP,IMAP,IMAP-TLS)
#-A FORWARD --match multiport ! --dports 80,443,53,20,115,143,993 -s 192.168.178.0/24 -o tun0 -j #DROP

-A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT

#Force Local DNS on Target
-A FORWARD -p udp --dport 53 -m mac --mac-source 81:7d:22:a2:3e:7d -j REJECT
-A FORWARD -p tcp --dport 53 -m mac --mac-source 81:7d:22:a2:3e:7d -j REJECT

-A FORWARD -s 192.168.0.0/24 -o tun0 -j ACCEPT

COMMIT

*nat
:PREROUTING ACCEPT
:INPUT ACCEPT
:POSTROUTING ACCEPT
:OUTPUT ACCEPT

-A POSTROUTING -o tun0 -j MASQUERADE

COMMIT

Pi-Hole :

In case IPv6 is disabled:

It is often simply not needed.
But if IPv6 was not active during the installation and is now needed there are two possibilities to get the IPv6 address to be displayed in the web interface again and be reachable via pi.hole:

  • “run ifconfig” to determine your IPv6 address and insert it in “/etc/pihole/setupVars.conf” at “IPV6_ADDRESS=” and in /etc/pihole/local.list as “<IPv4> <IPv6> pi.hole”.
  • run “pihole -r” and choose “Reconfigure” (Your DNS settings and hosts sources will get overwritten).

If you want the blocking page back:

The Blocking Page is especially useful if you use many or very large filter lists which may contain false positives. There you also have the option to whitelist pages that were blocked by mistake without first logging in to the web interface. However, this could slow down the general browsing experience.

Insert into /etc/pihole/pihole-FTL.conf:

BLOCKINGMODE=IP-NODATA-AAAA
service pihole-FTL restart

Block- and Whitelists:

Adjust update frequency (default is every sunday):

Change the time schedule in /etc/cron.d/pihole as with the iptables cron job:

00 00   * * *   root    PATH="$PATH:/usr/local/bin/" pihole updateGravity >/var/log/pihole_updateGravity.log || cat /var/log/pihole_updateGravity.log

To prevent the changes from simply being overwritten by the PiHole during an update, we write-protect the file. To unlock the file simply replace the ‘+’ with a ‘-’.

chattr +i /etc/cron.d/pihole