Fixing my Home Network Without Replacing the ISP Router

This weekend, I finally got around to putting that little FreeBSD box to work providing DNS for all machines on the LAN. While nearly all of my web traffic goes through a browser with built-in filters, my kid now occasionally uses an iPad and lots of apps are loaded with obnoxious ads. Having a network-layer filter never hurts, and it also opens up the possibility of static local addresses under .home.arpa.

Unbound configuration

I'm running Unbound as the DNS server. It's well supported and easy to install and configure. Installation was just pkg install unbound. To configure it, I opened up /usr/local/etc/unbound/unbound.conf. The file is pre-populated with extensive documentation. I made my edits inline, but here is a summary of the important parts.

The server block

Most of the configuration happens under the server block.

server:
    # Specify which interfaces to answer queries from
    interface: 127.0.0.1
    interface: ::1
    interface: 192.168.219.42
    interface: fdb8:f5ea:1370::1
    interface: fe80::f:42%bridge0

    # Subtle additional restriction beyond binding... it basically mirrors the above
	access-control: 127.0.0.0/8 allow
	access-control: ::1 allow
    access-control: 192.168.219.0/24 allow
    access-control: fdb8:f5ea:1370::/48 allow
    access-control: fe00::/8 allow

    # Protect against DNS rebinding attacks
	private-address: 10.0.0.0/8
    private-address: 100.64.0.0/10
	private-address: 172.16.0.0/12
	private-address: 192.168.0.0/16
	private-address: 169.254.0.0/16
	private-address: fd00::/8
	private-address: fe80::/10
	private-address: ::ffff:0:0/96

    # Private domain setup
    private-domain: "home.arpa"

    # respip is required for RPZ
	module-config: "respip validator iterator"

    # Local servers
    local-data: "pf.home.arpa. IN A 192.168.219.42"
    local-data-ptr: "192.168.219.42 pf.home.arpa"
    # and so on...

The IPv6 ULA

The fdb8:f5ea:1370::1 address is an IPv6 Unique Local Address. It is not globally routable, and serves a function similar to private IPv4 ranges. The way it works is that you take the prefix fd00::/8, add a 40-bit pseudorandom global ID to prevent collisions, and you've now got a 48-bit prefix (8 + 40). Pretty cool!

You can generate these with various tools, or just generate 10 random hex digits yourself (a hex digit is 4 bits). Then start assigning them to hosts in your network.

There are a few ways to do this. I was initially going to do this with router advertisements, but due to some complications with my network topology (needing to keep the ISP router for the moment), I had to just assign these statically to the few machines that need them.

The static link-local address

Here's where things turn a bit weird. Due to what looks like a bug, I can't send out an RA advertising the ULA prefix. But I COULD do a simple announcement that I am here on the local link. As long as you are on the same network segment (you can't cross a router but bridges and switches are fair game!).

So, this is basically a hack because I can't easily replace the ISP's router yet. But it's perfectly valid to statically claim a link-local address, so that's how I hacked around it.

The rpz block

And now we get to the fun part: the blocklist! Unbound supports loading these via the Response Policy Zone mechanism. I'm using the Hagezi "multi-normal" blocklist. I have no opinion on whether it's better than any of the others; it's something. NOTE: They recommend the "multi pro" but this one blocks legitimate bug tracking systems, like Sentry, which I need for work.

The config took me a bit longer than I expected to grok. The name is a DNS zone name. You can basically make one up, I think? The zonefile is the location on disk where it will store the blocklist. You need to create this directory with the appropriate permissions. The URL is what you'd guess. You can insert any of the RPZ format links here. Finally, you need to specify the action. In our case, we return an NXDOMAIN message, which halts resolution for most operating systems.

rpz:
    name: "rpz.hagezi.multi-normal.home.arpa"
    zonefile: "/usr/local/etc/unbound/zonefiles/hagezi.zone"
    url: "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/rpz/multi.txt"
    rpz-action-override: nxdomain

That's pretty much it for Unbound. It will automatically refresh the list per the parameters in the SOA record.

Additional networking setup on the mini PC

The mini PC is already configured as a bridge, but it needed some additional setup. As noted, I've added two new static v6 addresses. I made the assignments permanent with sysrc as before:

sysrc ifconfig_bridge0_alias1="inet6 fdb8:f5ea:1370::1 prefixlen 64"
sysrc ifconfig_bridge0_alias2="inet6 fe80::f:42 prefixlen 64"

Then I set up rtadvd on the bridge so that it will advertise recursive DNS to anyone listening that way.

sysrc rtadvd_enable="YES"
sysrc rtadvd_interfaces="bridge0"
sysrc rtadvd_flags="-s"

And the config in /etc/rtadvd.conf:

bridge0:\
        :rltime#0:\
        :noifprefix:\
        :rdnss="fe80::f:42":

Ideally this would also advertise the ULA prefix, but as noted before that's not possible. Oh well.

Fixing the ISP's equipment

I have not just one, but three appliances from my ISP, LG U+.

Router (HGWD-1G)

The router is a little grey box that just does routing. It's actually alright, besides the mediocre management interface which is common to all LG hardware. Fortunately the access codes are always printed on the boxes, so you don't even need to reset them.

The only thing I had to change here was the DNS options (under 네트워크 설정 > 유선 네트워크 설정). But this took a few more hours of trial-and-error than I expected.

The form is a bit funny and you MUST enter both a primary and backup DNS for both IPv4 and IPv6. For IPv4, I set both of them to my Unbound server's local v4 address. But for some reason, unless I set one of the v6 DNS servers to a globally routable address, I had no IPv4 routing outside the LAN. Not even ping 1.1.1.1 worked.

This is one of the most bizarre things I have ever seen. I really don't have any theories on why this might happen, but I was able to reproduce the issue very reliably by setting both DNS servers to the link-local fe80::f:42. I ended up setting the backup to Quad9 (2620:fe::fe).

This made all DHCP and DHCPv6 clients happy. And they mostly got the right DNS. For some reason, the gateway feels it must insert its own IPv4 address at the front. Presumably it has a caching recursive resolver of its own on the device. Bizarrely, it does NOT use the DNS servers you specify (dig @192.168.219.1 pf.home.arpa yields no results). I guess it uses whatever the LG DHCP-configured server is. The one I overrode in the settings. I have no idea.

Fortunately, clients are not all stupid. They seem to (mostly?) ignore that entry, and pick the first IPv6 address. Presumably it will win out but I guess I can't guarantee that. Oh well.

Wireless (GAPD-7500R and GAPK-7600)

And now the other two LG appliances... These are included in our plan. I could replace them if I wanted, but I just can't be bothered. I have no complaints on reliability or throughput. The reason we have 2 is that the apartment construction makes it physically impossible to cover with a single AP. So they gave us two, which was pretty sweet!

Unfortunately, the techs installed both of them in NAT mode. I did not realize this before, but all wireless clients were actually double-NAT'd! Not to mention, each of those was acting as its own DNS cache, and wasn't providing IPv6.

The fix is obvious: turn them into bridges. To do this, you need to go to 공유기 설정 > 상태 변경. Then change the mode to 수동(브릿지), which is manual bridge mode.

Save and restart. And a few minutes later, I finally(!) had IPv6 and lovely filtered local DNS from wireless clients.

Testing?

My larger devices were easy enough to test by pulling up the blocklist, looking for trouble, and issuing dig. But I wanted to make sure it worked on my phone (remember, the DHCP from the router STILL inserts itself as an IPv4 DNS server). I haven't seen ads in years, since my web browser filters everything out.

So the first thing I did was open up Safari again to get that raw, unfiltered browsing experience. But then I had to think "what sort of websites show ads?" My browsing habits don't take me to those corners of the internet much. Then I remembered what some poor Windows users are bombarded with: MSN 🤣 It did indeed have a big fat banner at at the top of the page. Now satisfactorily replaced with a placeholder reserving the space.

Posts from blogs I follow

Capture Clauses as Effects

IntroductionIn my post on Hoisting Expressions I discussed the move($expr) feature and how it works much like an inverse of the defer feature many languages have. Instead of creating expressions which run after the scope ends, move($expr) r...

via Yosh Wuyts — Blog

Against Chat

The most prevalent interface of recent fame is undeniably the chat window. Today it feels ubiquitous, to the extent that almost all interactions gravitate toward a single indistinguishable blob. This seems due in large part to an assumpti...

via Max's Homepage

Generated by openring-rs from my blogroll.