Installing DNSCrypt in pfSsense

I don’t think it takes a lot of brainpower to realize why Internet users need to proactively make every effort to protect their online privacy.

Every piece of data that can be extracted with regards to our behavior, will be harvested and sold, with various purposes, from behavioral modeling for targeted advertisement, to potential profiling by 3-letter government agencies that may not agree with that you are trying to view. On the same note, storage has become so cheap over time, that this data could be available for years to come, so searches made today, could conceivable put you in some list years from now.

And one of the most overlooked of all these data sources is DNS. Why do you think Cisco paid $635 million in cash to acquire OpenDNS? or why do you think Google has created an extremely fast and efficient public DNS servers? Out of the goodness of their heart?

ISPs also have vested interest in this data. It’s perfectly feasible for them to snoop on the content of DNS queries and use that data for their own purposes.

Introduction

In order to help mitigating these situations, the DNSCrypt protocol was created. DNSCrypt basically encrypts all DNS queries between your point of request and the DNS provider, making effectively impossible (or extremely expensive) for the interested party in obtaining that data.

Of course, that’s not the whole picture. You need to find a DNS provider that will also respect your privacy. The OpenNIC Project has taken that role for quite some time now, and they maintain a list of Tier-2 DNS servers for both, IPv4 and IPv6 that anyone can reach. A subset of those are DNSCrypt-enabled, and yet another subset, claims to keep no logs whatsoever, another important recommendation if security is your primary motivation.

Now, my router of choice these days is pfSense, but there isn’t a plugin available out of the box, so this guide will show you how to set it up manually. This has been validated in version 2.3.2.

Environment

My pfSense gateway is running as a VM on an ESXi box, connected to a Cisco SG200-20 that provides the VLAN segmentation. For this design, there are 3 VLANs, one external (along with the cable modem), one internal for the local systems, and one dedicated to guests and other untrusted devices (TVs, mobile phones, etc) for both, wired and wireless clients. The guest network does not provide IPv6, while the internal network is dual stack.

Since I use Comcast Business Class, I can request a /56 for prefix delegation, but for the moment, I’m sticking to a /64 to make things simpler to start.

Procedure

  1. From the main dashboard, execute all necessary updates before proceeding.
  2. ssh into the gateway. When prompted with the text-based menu, press 8.
  3. We first put the filesystem in rw mode with /etc/rc.conf_mount_rw.
  4. Next, we allow FreeBSD packages to be installed from non-pfSense repos by editing /usr/local/etc/pkg/repos/pfSense.conf and /usr/local/etc/pkg/repos/FreeBSD.conf. In both cases, change FreeBSD: { enabled: no } to yes. You should also verify that enabled: yes is present in /etc/pkg/FreeBSD.conf.
  5. We now install DNSCrypt with: pkg install dnscrypt-proxy. Say yes to the dependencies.
  6. Optionally, you can get back to no the FreeBSD repos you changed above if you want to be more strict.

At this point we have DNSCrypt installed, so the next step is to test it. For this, we’ll use 4 DNS servers, two with IPv4 and two with IPv6.

Given the ping round trip from my location near San Francisco, it took me a while to find 4 servers that 1) are properly working with DNSCrypt, 2) are stable enough for daily use, and 3) claim to keep no logs.

Keep open the list here. I selected the ones with the following DNSCrypt names:

  • 2.fvz-rec-us-ca-01.dnscrypt-cert.meo.ws (IPv4, Freemont, CA)
  • 2.fvz-rec-gb-eng-01.dnscrypt-cert.meo.ws (IPv4, England)
  • 2.fvz-rec-us-ga-01.dnscrypt-cert.meo.ws (IPv6, Atlanta, GA)
  • 2.fvz-rec-gb-eng-01.dnscrypt-cert.meo.ws (IPv6, England – same as above but on v6)

The list contains also all the relevant data, such as their actual IP addresses, DNSCrypt keys, and available ports. I’ve settled on using port 443 for everything since the chance of that port ever been blocked is close to zero.

So let’s test the first one:

/usr/local/sbin/dnscrypt-proxy \
  --user=_dnscrypt-proxy
  --local-address=127.0.0.1:54 \
  --pidfile=/var/run/dnscrypt-proxy.pid \
  --resolver-address=74.207.241.202:443 \
  --provider-name=2.fvz-rec-us-ca-01.dnscrypt-cert.meo.ws \
  --provider-key=FAC6:3B37:8485:5E43:3CC3:8BBC:FA84:5DCB:8DF0:B683:3BB3:A116:126D:0C29:95CD:899F

This command runs DNSCrypt in the foreground, with all relevant data being forwarded to the stdout.

If everything is working properly, you should see a message like this:

[NOTICE] Starting dnscrypt-proxy 1.4.3
[INFO] Initializing libsodium for optimal performance
[INFO] Generating a new key pair
[INFO] Done
[INFO] Server certificate #808464433 received
[INFO] This certificate looks valid
[INFO] Chosen certificate #808464433 is valid from [2015-02-15] to [2016-02-15]
[INFO] Server key fingerprint is 5B7F:9954:EBFD:8BC5:235D:698C:6F15:1E9F:22AC:5925:79DC:32C6:381A:D12A:8FA0:9179
[NOTICE] Proxying from 127.0.0.1:54 to 74.207.241.202:443

At this point we want to test DNS resolution, so either from a separate SSH session, or in the same after issuing a ^Z and bg, we test a single-server resolution: dig -4 @127.0.0.1 -p 54 slashdot.org. The output should be:

; <<>> DiG 9.10.2-P2 <<>> -4 @127.0.0.1 -p 54 slashdot.org
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 22564
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;slashdot.org.			IN	A

;; ANSWER SECTION:
slashdot.org.		300	IN	A	216.34.181.45

;; Query time: 19 msec
;; SERVER: 127.0.0.1#54(127.0.0.1)
;; WHEN: Tue Jul 14 16:44:38 PDT 2015
;; MSG SIZE  rcvd: 46

which demonstrate this is actually working. We can now kill the instance and test each one of the other ones. For IPv6, the testing syntax is as follows:

/usr/local/sbin/dnscrypt-proxy --user=_dnscrypt-proxy \
  --local-address='[::1]:5353' \
  --pidfile=/var/run/dnscrypt-proxy3.pid \
  --resolver-address='[2600:3c02::f03c:91ff:fe84:dac2]:443' \
  --provider-name=2.fvz-rec-us-ga-01.dnscrypt-cert.meo.ws \
  --provider-key=F584:75C4:5693:317A:1D8E:4232:76E7:9E63:D286:B12D:B472:5FA2:92D3:E714:1E46:459B

This should provide a similar message as the one above. The testing command should be like this: dig aaaa -6 @::1 -p 5353 ipv6.google.com, and the result should be:

; <<>> DiG 9.10.2-P2 <<>> aaaa -6 @::1 -p 5353 ipv6.google.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 14842
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;ipv6.google.com.		IN	AAAA

;; ANSWER SECTION:
ipv6.google.com.	77630	IN	CNAME	ipv6.l.google.com.
ipv6.l.google.com.	170	IN	AAAA	2607:f8b0:4002:802::1000

;; Query time: 76 msec
;; SERVER: ::1#5353(::1)
;; WHEN: Tue Jul 14 16:57:40 PDT 2015
;; MSG SIZE  rcvd: 82

Surviving a Reboot

At this point, we have tested all our 4 DNSCrypt connections and verified that they work fine. What we need to do now is to daemonize them, produce a static log file we can monitor, and make sure they will all come up upon reboot.

In order to run them as a daemon, all we need to do is add a -d to the command above. In order to add a log file, we need to append --logfile=/var/log/dnscrypt-1.log. Use different filenames for each daemon.

Next, create a startup script named dnscrypt-start.sh in the directory /etc/rc.conf.d and make it executable. This should contain the commands needed upon reboot to start all the daemons. Mine looks like this:

/usr/local/sbin/dnscrypt-proxy -d --user=_dnscrypt-proxy \
  --local-address=127.0.0.1:54 \
  --pidfile=/var/run/dnscrypt-proxy.pid \
  --resolver-address=74.207.241.202:443 \
  --provider-name=2.fvz-rec-us-ca-01.dnscrypt-cert.meo.ws \
  --provider-key=FAC6:3B37:8485:5E43:3CC3:8BBC:FA84:5DCB:8DF0:B683:3BB3:A116:126D:0C29:95CD:899F \
  --logfile=/var/log/dnscrypt1.log

/usr/local/sbin/dnscrypt-proxy -d --user=_dnscrypt-proxy \
  --local-address=127.0.0.1:55 \
  --pidfile=/var/run/dnscrypt-proxy2.pid \
  --resolver-address=178.79.174.162:443 \
  --provider-name=2.fvz-rec-gb-eng-01.dnscrypt-cert.meo.ws \
  --provider-key=8E14:C710:FA49:6F5E:18D7:DA15:6E80:5CDD:F666:299F:438E:5798:63F1:4513:68D1:9B3A \
  --logfile=/var/log/dnscrypt2.log

/usr/local/sbin/dnscrypt-proxy -d --user=_dnscrypt-proxy \
  --local-address='[::1]:5353' \
  --pidfile=/var/run/dnscrypt-proxy3.pid \
  --resolver-address='[2600:3c02::f03c:91ff:fe84:dac2]:443' \
  --provider-name=2.fvz-rec-us-ga-01.dnscrypt-cert.meo.ws \
  --provider-key=F584:75C4:5693:317A:1D8E:4232:76E7:9E63:D286:B12D:B472:5FA2:92D3:E714:1E46:459B \
  --logfile=/var/log/dnscrypt-v6-1.log

/usr/local/sbin/dnscrypt-proxy -d --user=_dnscrypt-proxy \
  --local-address='[::1]:5354' \le
  --pidfile=/var/run/dnscrypt-proxy4.pid \
  --resolver-address='[2a01:7e00::f03c:91ff:fe84:da61]:443' \
  --provider-name=2.fvz-rec-gb-eng-01.dnscrypt-cert.meo.ws \
  --provider-key=8E14:C710:FA49:6F5E:18D7:DA15:6E80:5CDD:F666:299F:438E:5798:63F1:4513:68D1:9B3A \
  --logfile=/var/log/dnscrypt-v6-2.log

I added mine manually to the end of /etc/rc. I have no doubt this can be done in a nicer way with variables and such, but I wanted to have an MVP1 and this qualifies. Refactoring comes later.

To test it, reboot the system. The dig commands should work just as before.

Local Resolver Integration

At this point, we have DNSCrypt up and running. We now need to setup a local DNS server that can act as a proxy against these instances, so our clients in the network don’t need to know anything about it.

For that, we’ll use the dnsmasq instance included in pfSense.

In the Web UI, select Services -> DNS Forwarder. Make sure the following check boxes are enabled:

  • Enable
  • DHCP Registration
  • Static DHCP
  • Prefer DHCP

This is assuming you use pfSense as a DHCP server for your network of course.

Under Interfaces, manually select:

  • LAN
  • LAN IPv6 Link-local
  • Localhost

My guests and untrusted devices don’t get service from this DNS system. They are forced out to Google.

Under Advanced, add the following lines:

local=/home/
rebind-domain-ok=/xip.io/
selfmx
cache-size=16384
log-async=5
domain=home
addn-hosts=/var/etc/dnsmasq/hosts
server=127.0.0.1#54
server=127.0.0.1#55
server=::1#5353
server=::1#5354

Now, not all of these entries are useful to everybody. .home for example, is the domain name I use in my home network. I also explicitly allow xip.io since I rely on it heavily for my home lab’s Pivotal Cloud Foundry installation.

The cache size will most likely be truncated to 10,000 due to a limitation of dnsmasq. I also add addn-hosts to manually select a number of host names and to include the PGL Yoyo list for ad servers.

The important part is the server= statements since they point to our local DNSCrypt instances.

Now go ahead and save the configuration. You can test it with the same dig commands against the selected interfaces from within the gateway and it should work like a charm.

Changing the Firewall Configuration

When you try to dig from a separate machine in the same LAN however, you will get no response. That’s because the firewall is only forwarding traffic. We need to add a rule that allows the local LAN to query the DNS server running on the LAN interface.

Add an IPv4 rule that Pass traffic from your local LAN (expressed as CIDR, for example 192.168.10.0/24) specifically into LAN Net on port 53, for both, UDP and TCP. Add a separate rule for IPv6 traffic, using your /64 prefix (or different if you have larger).

Try now a dig command from a separate machine in the local LAN and everything should work fine. For IPv6, you should test this site also: http://test-ipv6.com/ that should give you a 1010, particularly on the DNS side.

Awesome! but how can we prevent that some specific OSs or devices ignore our DNS settings and use their own? simple: add another firewall rule in the LAN interface denying access to any external server on port 53 TCP and UDP. If you want to be flexible, you can setup an exception rule with !allowed_devices that you should define as an alias with the IP addresses of the devices that are allowed to query external DNSs for whatever reason.

Conclusion

This setup provides a good shield against DNS visibility by third parties, potential censorship and domain blacklisting.

There is a lot of room for improvement. I would certainly like to see which DNS servers also support Namecoin with the .bit TLD. I would also like to eventually automate all this, in the sense that if one DNS server stops working, I can automatically recover setting up another one. That would be a great microservice to have as a resource in the Internet.

For the pfSense developers, I’d love to see DNSCrypt formally integrated in the system, not only with dnsmasq, but also with unbound to provide DNSSEC on top of it. It would also be great to have IPv6-compatible aliases, which currently don’t seem to work (IPv6 addresses are, well ugly). And finally, I’d love to have the YoYo lists (above) integrated as a standard option.

I hope this article is useful to someone. My next line of work will be How to avoid IPv6 leaking your location and ID when using a VPN