Setting up Pi-Hole in LXC

I already have AdGuard installed on the Windows PC (at the network layer), but I’m starting to reconsider running browser extensions and their ilk – they’re executing with potentially full rights in the DOM (AdGuard injects JS to manage things for instance), and all it takes is one of them being sold/pwned, and my data could be flowing somewhere I don’t want it to. An alternative is to run a DNS server that can reject requests for known “bad” hostnames, so that requests from all devices on the network get a layer of protection. It’s nuanced though, because DNS level blocks can only deny a request based on a name, while extensions in the browser can do full DOM inspection and manipulation.

In case that pwned link ever goes away:

The official Chrome extension for the MEGA.nz file sharing service has been compromised with malicious code that steals usernames and passwords, but also private keys for cryptocurrency accounts, ZDNet has learned.

The malicious behavior was found in the source code of the MEGA.nz Chrome extension version 3.39.4, released as an update earlier today.

Google engineers have already intervened and removed the extension from the official Chrome Web Store, and also disabled the extension for existing users.

So, what is Pi-hole?

Pi-hole is, at the heart of it, a DNS service that you can run on your own network. It has one or more deny lists of hostnames that should not resolve, and those hostnames are typically ad networks (which have been used many, many times to serve compromised Javascript to unsuspecting browsers). The install documentation for Pi-hole either has you using docker, or curling a script and passing it right to bash; they say “here, look at the script”, but there’s documented evidence that server-side processing can be used to detect that curl is piping to bash, and send different content over the connection.

So, rather than use docker, I’m going to attempt to get Pi-hole running in LXC – Linux Containers. Of course, this means having to remember just how to set up LXC containers and get the network bound correctly. From previous experiments with PBXs and so on, my LXC install is configured in macvlan mode.

lxc.network.type = macvlan
lxc.network.macvlan.mode = bridge
lxc.network.link = eno1
lxc.network.flags = up

Step 1: Create a new container

I want to be able to SSH to this container, and having vim, curl, and ping will be useful.

lxc-create -t debian -n pi-hole -- -r jessie --package=cron,curl,wget,openssh-server,vim,ping

Step 2: Start it up and attach to it from the console

lxc-start -n pi-hole
lxc-attach -n pi-hole

Step 3: Network?

root@pi-hole:/etc# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
5: eth0@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether d2:19:a1:0f:38:eb brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.32/24 brd 192.168.0.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::d019:a1ff:fe0f:38eb/64 scope link
       valid_lft forever preferred_lft forever

root@pi-hole:/etc# ping 192.168.0.1 -c 1
PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data.
64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=1.46 ms

--- 192.168.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.460/1.460/1.460/0.000 ms

Yep, network is up and running.

Step 4: Download the script for inspection

root@pi-hole:~# curl -sSL https://install.pi-hole.net -o pihole.sh
curl: (60) SSL certificate problem: unable to get local issuer certificate

Ah, that’s a problem. This probably means that my container is missing the ca-certificates package (and this could be added to the lxc-create invocation).

apt install ca-certificates

That fixes the warning, and the -k argument to curl isn’t needed. The script can now be inspected, and it looks reasonably sane; uses set -e, which is nice.

Step 5: Install Pi-hole

root@pi-hole:~# bash pihole.sh

My personal choices:

  • Cloudflare’s 1.1.1.1 resolver for upstream DNS.
  • All the third party lists.
  • Block IPv4 and IPv6, though my ISP doesn’t offer working IPv6 anyway.
  • Install the web interface, using the preferred lighttpd.
  • Log queries – I’m curious what my network is doing. If you’re you’re doing this anywhere other than at home, maybe you’ll care about GDPR.

As the install was running, the moment it said lighttpd had been installed and started, I pointed a browser at the IP that’s allocated to this container.


That looks good.

Step 6: Configure my ISP-provided router to hand out the Pi-hole IP as the DNS server

Turns out I can’t do this. Virgin Media, in their sage and infinite wisdom, have decided that customers with a Media Hub 3.0 are not to be trusted with DNS settings (even though you’ve got DMZ, Firewall, port triggering etc control). This means I can’t make the Pi-hole the default DNS server for my network. Well, I can, but now I’m going to need to disable DHCP on the Media Hub (if I can, without disabling the whole router mode) and get a DHCP service up and running on one of the other computers in the network. The one that’s running DNS can probably do it, but I’ll need to configure it to boot on a static IP.

Step 7: Remember to make this start automatically

By default, my LXC installation won’t start containers automatically. In /var/lib/lxc/<containername>/config, two lines are needed – well, the first one is needed, the second one lets you control boot sequences a bit (and there’s also .order).

lxc.start.auto = 1
lxc.start.delay = 1

Overall, that was easier than I expected.

Step 8: Tweak things as I go along

I’m bound to hit a few DNS denies that I’d actually like to have work, but the admin panel for Pi-hole lets me fix that.

The container, in macvlan mode, requested a DHCP lease from the Virgin hub. The server should really be on a static address, and I’ll probably put it on .53 just for ease of remembering that it’s the DNS server (since port 53 is DNS).