Tag: FreeBSD

  • Configuring DNS and DHCP For A LAN In 2025

    Configuring DNS and DHCP For A LAN In 2025

    14 years ago I described how to configure ISC BIND and DHCP(v6) Server on FreeBSD to get DHCP with local domain updates working on a dual stack LAN. However, ISC DHCP Server went End of Life on October 5th, 2022, replaced with their new Kea DHCP server. I also wouldn’t recommend running a non-filtering DNS resolver for your LAN any longer.

    AdGuard Home

    If you’re reading this blog, you’ve almost certainly heard of Pi-hole. However, I’ve found that I prefer AdGuard Home. AdGuard offers paid DNS filtering apps (I happily pay for AdGuard Pro for my iPhone), however their Home product is open source (GPL3) and free. I wont repeat the official Getting started docs, except to point out that AdGuard Home is available in FreeBSD ports so go ahead and install it with pkg install adguardhome.

    There are some configuration changes we’re going to make that cannot be done in the web UI and have to be done directly in the AdGuardHome.yaml config file. I wonder cover everything in the file, just the interesting bits.

    First, we’re going to be specific about which IPs to bind to, so we don’t accidentally create a public resolver, and also because there are LAN IPs on the router we don’t want to bind to (more on this in just a moment).

    http:
      pprof:
        port: 6060
        enabled: false
      address: 172.16.2.1:3000
      session_ttl: 720h
    ...
    dns:
      bind_hosts:
        - 127.0.0.1
        - ::1
        - 172.16.2.1
        - 2001:db8:ffff:aa02::1

    Your choice of upstream resolver is of course personal preference, but I wanted a non-filtering upstream since I want to the control and visibility into why requests are passing/failing. I’m also Canadian, so I prefer (but don’t require) that my queries stay domestic. I’m also sending requests for my LAN domains to the authoritative DNS server, which you can see is configured on local host IP 127.0.0.53 and on the similarly numbered alias IPs on my LAN interface (hence why I had to be specific about which IPs I wanted AdGuard to bind to).

      upstream_dns:
        - '# public resolvers'
        - https://private.canadianshield.cira.ca/dns-query
        - https://unfiltered.adguard-dns.com/dns-query
        - '# local network'
        - '[/lan.example.com/]127.0.0.53 172.16.2.53 2001:db8:ffff:aa02::53'
    ...
      trusted_proxies:
        - 127.0.0.0/8
        - ::1/128
        - 172.16.2.1/32
    ...
      local_ptr_upstreams:
        - 172.16.2.53
        - 2001:db8:ffff:aa02::53

    Lastly, we’re going to configure the webserver for HTTPS and DNS-over-HTTPS (DoH). I use dehydrated to manage my Let’s Encrypt certs, but any tool will do (and is outside the scope of this doc). The important thing to note is that the web UI will now run on port 8453, and will answer DoH queries.

    tls:
      enabled: true
      server_name: router.lan.example.com
      force_https: true
      port_https: 8453
      port_dns_over_tls: 853
      port_dns_over_quic: 853
      port_dnscrypt: 0
      dnscrypt_config_file: ""
      allow_unencrypted_doh: false
      certificate_chain: ""
      private_key: ""
      certificate_path: /usr/local/etc/dehydrated/certs/router.lan.example.com/fullchain.pem
      private_key_path: /usr/local/etc/dehydrated/certs/router.lan.example.com/privkey.pem
      strict_sni_check: false

    The rest of the configuration should be done to taste in the web UI. Personally, I find this filter list is effective while still having a very low false positive rate:

    • AdGuard DNS filter
    • AdAway Default Blocklist
    • AdGuard DNS popup Hosts filter
    • HaGeZi’s Threat Intelligence Feeds
    • HaGeZi’s Pro++ Blocklist
    • OISD Blocklist Big

    More than that and it just becomes unwieldy.

    BIND

    Good old BIND. It’ll outlive us all. This part is basically unchanged I first described it in 2011, except that I’m going to have BIND listen on 127.0.0.53 and alias IPs I created on my LAN networks (also using the .53 address) by setting this in my /etc/rc.conf:

    ifconfig_igb1="inet 172.16.2.1 netmask 255.255.255.0"
    ifconfig_igb1_ipv6="inet6 2001:db8:ffff:aa02::1 prefixlen 64"
    ifconfig_igb1_aliases="\
      inet 172.16.2.53 netmask 255.255.255.0 \
      inet6 2001:db8:ffff:aa02::53 prefixlen 64"

    Next, create an rndc key with rndc-confgen -a -c /usr/local/etc/namedb/rndc.example.com and configure BIND with the following in /usr/local/etc/named/named.conf (don’t remove the logging or zones at the bottom of the default named.conf).

    "acl_self" {
      127.0.0.1;
      127.0.0.53;
      172.16.2.1;
      172.16.2.53;
      ::1;
      2001:db8:ffff:aa02::1;
      2001:db8:ffff:aa02::53;
    };
    
    acl "acl_lan" {
      10.42.0.0/16;
      10.43.0.0/16;
      172.16.2.0/24;
      2001:db8:ffff:aa02::/64;
      fe80::/10;
    };
    
    options {
      directory             "/usr/local/etc/namedb/working";
      pid-file              "/var/run/named/pid";
      dump-file             "/var/dump/named_dump.db";
      statistics-file       "/var/stats/named.stats";
      allow-transfer        { acl_lan; };
      allow-notify          { "none"; };
      allow-recursion       { "none"; };
      dnssec-validation     auto;
      auth-nxdomain         no;
      recursion             no;
      listen-on             { 127.0.0.53; 172.16.2.53; };
      listen-on-v6          { 2001:db8:ffff:aa02::53; };
      disable-empty-zone "255.255.255.255.IN-ADDR.ARPA";
      disable-empty-zone "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.IP6.ARPA";
      disable-empty-zone "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.IP6.ARPA";
      version "BIND";
    };
    
    include "/usr/local/etc/namedb/rndc.example.com";
    
    controls {
      inet 127.0.0.53 allow { "acl_self"; "acl_lan"; } keys { "rndc.example.com";};
      inet 172.16.2.53 allow { "acl_self"; "acl_lan"; } keys { "rndc.example.com";};
      inet 2001:db8:ffff:aa02::53 allow { "acl_self"; "acl_lan"; } keys { "rndc.example.com";};
    };
    
    include "/usr/local/etc/namedb/named.zones.local";

    The local zones are configured in /usr/local/etc/namedb/named.zones.local:

    acl zone "lan.example.com" {
      type master;
      file "../dynamic/lan.example.com";
      update-policy { grant rndc.example.com zonesub ANY; };
    };
    
    zone "2.16.172.in-addr.arpa" {
      type master;
      file "../dynamic/2.16.172.in-addr.arpa";
      update-policy { grant rndc.example.com zonesub ANY; };
    };
    
    zone "2.0.a.a.f.f.f.f.8.B.D.0.1.0.0.2.ip6.arpa" {
      type master;
      file "../dynamic/2001.0db8.ffff.aa02.ip6.arpa";
      update-policy { grant rndc.example.com zonesub ANY; };
    };

    Here’s a starter zone for lan.example.com:

    $ORIGIN .
    $TTL 1200       ; 20 minutes
    lan.example.com      IN SOA  ns0.lan.example.com. admin.example.com. (
                                    2020138511 ; serial
                                    1200       ; refresh (20 minutes)
                                    1200       ; retry (20 minutes)
                                    2419200    ; expire (4 weeks)
                                    3600       ; minimum (1 hour)
                                    )
                            NS      ns0.lan.example.com.
                            A       172.16.2.53
                            AAAA    2001:db8:ffff:aa02::53
    $ORIGIN lan.example.com.
    router                  A       172.16.2.1
                            AAAA    2001:db8:ffff:aa02::1

    An IPv4 reverse zone:

    $ORIGIN .
    $TTL 1200       ; 20 minutes
    2.16.172.in-addr.arpa IN SOA ns0.lan.example.com. admin.example.com. (
                                    2020051192 ; serial
                                    1200       ; refresh (20 minutes)
                                    1200       ; retry (20 minutes)
                                    2419200    ; expire (4 weeks)
                                    3600       ; minimum (1 hour)
                                    )
                            NS      ns0.lan.example.com.
    $ORIGIN 2.16.172.in-addr.arpa.
    1                       PTR     router.lan.example.com.

    And an IPv6 reverse zone:

    $ORIGIN .
    $TTL 1200       ; 20 minutes
    2.0.a.a.f.f.f.f.8.B.D.0.1.0.0.2.ip6.arpa IN SOA ns0.lan.example.com. mikemacleod.gmail.com. (
                                    2020049273 ; serial
                                    1200       ; refresh (20 minutes)
                                    1200       ; retry (20 minutes)
                                    2419200    ; expire (4 weeks)
                                    3600       ; minimum (1 hour)
                                    )
                            NS      ns0.lan.example.com.
    $ORIGIN 0.0.0.0.0.0.0.0.0.0.0.0.0.0.2.0.a.a.f.f.f.f.8.B.D.0.1.0.0.2.ip6.arpa.
    1.0                     PTR     router.lan.example.com.

    Kea DHCP Server

    The final piece to this is the Kea DHCP server. It’s still from ISC, but this is a from-scratch implementation of DHCP and DHCPv6 that to use modern designs and tools. We won’t be using many of the new bells and whistles, but there’s a couple things we can do now that we couldn’t with ISC DHCP.

    The first thing you’ll notice is that the Kea config files are JSON, and there are four of them. First up is kea-dhcp4.conf, where we configure our IPv4 DHCP options and pool, and also the options necessary to enable dynamic updating of our LAN domain via RFC2136 DDNS updates. Note that because I had an existing zone that had been updated by ISC DHCP and other stuff, I set "ddns-conflict-resolution-mode": "no-check-with-dhcid". You can find more info here.

    {
      "Dhcp4": {
        "ddns-send-updates": true,
        "ddns-conflict-resolution-mode": "no-check-with-dhcid",
        "hostname-char-set": "[^A-Za-z0-9.-]",
        "hostname-char-replacement": "x",
        "interfaces-config": {
          "interfaces": [
            "igb1/172.16.2.1"
          ]
        },
        "dhcp-ddns": {
          "enable-updates": true
        },
        "subnet4": [
          {
            "id": 1,
            "subnet": "172.16.2.0/24",
            "authoritative": true,
            "interface": "igb1",
            "ddns-qualifying-suffix": "lan.example.com",
            "pools": [
              {
                "pool": "172.16.2.129 - 172.16.2.254"
              }
            ],
            "option-data": [
              {
                "name": "routers",
                "data": "172.16.2.1"
              },
              {
                "name": "domain-name-servers",
                "data": "172.16.2.1"
              },
              {
                "name": "domain-name",
                "data": "lan.example.com"
              },
              {
                "name": "ntp-servers",
                "data": "172.16.2.1"
              }
            ],
            "reservations": [
              {
    
                "hw-address": "aa:bb:cc:dd:ee:ff",
                "ip-address": "172.16.2.2",
                "hostname": "foobar"
              }
            ]
          }
        ],
        "loggers": [
          {
            "name": "kea-dhcp4",
            "output-options": [
              {
                "output": "syslog"
              }
            ],
            "severity": "INFO",
            "debuglevel": 0
          }
        ]
      }
    }

    The kea-dhcp6.conf file is basically identical, except IPv6 flavoured. One nice thing about Kea is you can set a DHCPv6 reservation by MAC address, which is something you could not do with ISC DHCPv6 Server.

    {
      "Dhcp6": {
        "ddns-send-updates": true,
        "ddns-conflict-resolution-mode": "no-check-with-dhcid",
        "hostname-char-set": "[^A-Za-z0-9.-]",
        "hostname-char-replacement": "x",
        "dhcp-ddns": {
          "enable-updates": true
        },
        "interfaces-config": {
          "interfaces": [
            "igb1"
          ]
        },
        "subnet6": [
          {
            "id": 1,
            "subnet": "2001:db8:ffff:aa02::/64",
            "interface": "igb1",
            "rapid-commit": true,
            "ddns-qualifying-suffix": "lan.example.com",
            "pools": [
              {
                "pool": "2001:db8:ffff:aa02:ffff::/80"
              }
            ],
            "option-data": [
              {
                "name": "dns-servers",
                "data": "2001:db8:ffff:aa02::1"
              }
            ],
            "reservations": [
              {
                "hw-address": "aa:bb:cc:dd:ee:ff",
                "ip-addresses": [
                  "2001:db8:ffff:aa02::2"
                ],
                "hostname": "foobar"
              }
              }
            ]
          }
        ],
        "loggers": [
          {
            "name": "kea-dhcp6",
            "output-options": [
              {
                "output": "syslog"
              }
            ],
            "severity": "INFO",
            "debuglevel": 0
          }
        ]
      }
    }

    Lastly, we have kea-dhcp-ddns.conf, which configures how the zones will actuall be updated. Note that I’m connecting to BIND on 127.0.0.53.

    {
      "DhcpDdns": {
        "tsig-keys": [
          {
            "name": "rndc.example.com",
            "algorithm": "hmac-sha256",
            "secret": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
          }
        ],
        "forward-ddns": {
          "ddns-domains": [
            {
              "name": "lan.example.com.",
              "key-name": "rndc.example.com",
              "dns-servers": [
                {
                  "ip-address": "127.0.0.53",
                  "port": 53
                }
              ]
            }
          ]
        },
        "reverse-ddns": {
          "ddns-domains": [
            {
              "name": "2.16.172.in-addr.arpa.",
              "key-name": "rndc.example.com",
              "dns-servers": [
                {
                  "ip-address": "127.0.0.53",
                  "port": 53
                }
              ]
            },
            {
              "name": "2.0.a.a.f.f.f.f.8.B.D.0.1.0.0.2.ip6.arpa.",
              "key-name": "rndc.example.com",
              "dns-servers": [
                {
                  "ip-address": "127.0.0.53",
                  "port": 53
                }
              ]
            }
          ]
        },
        "loggers": [
          {
            "name": "kea-dhcp-ddns",
            "output-options": [
              {
                "output": "syslog"
              }
            ],
            "severity": "INFO",
            "debuglevel": 0
          }
        ]
      }
    }

    Extra Credit: Mobile DNS over HTTPS (DoH)

    I mentioned earlier that I pay for AdGuard Pro on my phone. Part of why I do that is it uses the MDM API in iOS to let you force your DNS to a DoH provider, including a custom one. Perhaps one you’re hosting yourself.

    I’m already running an nginx reverse proxy on my router, so let’s get mobile DoH setup. This is a simplified configuration and you’ll need to ensure you’ve got HTTPS properly configured, which is (again) outside the scope of this post.

    Note that I proxy the request to router.lan.example.com which will resolve to the LAN IP 172.16.2.1 rather than localhost, because we configured AdGuard Home to run it’s HTTP server on 172.16.2.1.

        server {
            listen 443 ssl;
            server_name  dns.example.com;
            location / {
                return 418;
            }
            location /dns-query {
                proxy_pass https://router.lan.example.com:8453/dns-query;
                proxy_set_header X-Real-IP  $proxy_protocol_addr;
                proxy_set_header X-Forwarded-For $proxy_protocol_addr;
            }
        }

    Conclusion

    That should do it. You’ve now got filtered DNS resolution for the LAN. You’ve got an authoritative LAN domain. You’ve got a modern DHCP service. And you’ve even got filtered DNS resolution when you’re out of the house.

  • FreeBSD 9.2 on Jetway NF9H-525

    I wanted a new system to run as my router, so I set out to find a small system with at least three gigabit NICs. I settled on a Jetway NF9H-525 (also referenced as a NF9HQL-525), which is a mini-ITX board geared for networking applications with four onboard gigabit NICs and a dual core atom 1.8Ghz processor.  I also purchased an LGX MC500 case, two gigabytes of RAM, and pulled a drive out of a recently retired laptop. Bottom line: everything worked great and I would recommend it.

    The board has four onboard realtek gigabit NICs (RTL8111/8168B) which use the re driver. They’re not the beloved Intel em NICs, but they’ll do. Especially for the types of applications this board is likely to be used for – like my home networking. Same deal with the intel atom processor. The system can be a bit louder than I would have really liked, but the fan speed stepping does work so it’s only loud when it’s under a bit of load.

    I’m not going to bother including any build notes, mostly because I didn’t encounter any gotchas or oddities during the installation or configuration of the system. I read reports online that the NICs require a recent version of FreeBSD, but I didn’t have any issues with 9.2-RELEASE.

  • VDSL + MLPPP + FreeBSD + Xen = Awesome

    I signed up for the new 25Mbps VDSL services that are becoming available through TekSavvy, now that Bell has to provide speed matching profiles to other providers instead of just the staid old 5Mbps profiles they used to offer.

    The techs were done by the time I got home, but one of them was nice enough to install a proper POTS splitter for me, which was nice. According to the person present at the time, he said something to the effect of “I don’t know if I’m supposed to do this, but I think Mike will appreciate it”.

    My router is a Xen virtual machine, running a hardware virtualized FreeBSD instance, with three NICs passed to it using PCI passthrough. I use packet filter for the firewall and traffic shaping, and MPD5 to handle the actual MLPPP tunnel.

    Once I got home I connected the router to the cellpipe modem, and right away the PPPoE came up. Subsequent testing showed that I actually got slightly better performance by configuring mpd to bring up two full tunnels on the single line and then bond them together than I did by having mpd bring up just a single MLPPP enabled tunnel.

    Speedtest Result

    I had been concerned that the virtual machine wouldn’t be up to the task, but it appears that isn’t much of a concern. I haven’t done any testing with new MTU/MRU values yet, so there’s still a possibility of improving performance slightly from here, but I’m already getting pretty much what was promised, so I don’t know how much further it could go.

  • IPv6 Part 7: Securing And Optimizing An IPv6 Home Network Using FreeBSD

    Welcome to part seven of my multipart series on IPv6. In this post I’ll cover how to use packet filter (pf) on BSD to run a small network. The router is running FreeBSD 8.2, but this should apply equally well to either OpenBSD or FreeBSD with pf.
    (more…)

  • IPv6 Part 6: Configuring An IPv6 Network Assigned Over PPPoE Using FreeBSD

    Welcome to part six of my multipart series on IPv6. In this post I’ll cover how to configure a dual-stacked PPPoE tunnel on FreeBSD using mpd. The host is a FreeBSD 8.2 box using mpd5 from ports.
    (more…)