module-based DNS and DHCP config

This commit is contained in:
Ellis Rahhal 2024-11-14 20:52:54 -08:00
parent 81436b4192
commit e21105f241
6 changed files with 369 additions and 175 deletions

View file

@ -7,6 +7,7 @@
../../profiles/common.nix
../../profiles/config-editor.nix
../../profiles/ddclient.nix
../../profiles/dnsmasq.nix
../../profiles/home-assistant
../../profiles/git.nix
../../profiles/gitea.nix
@ -15,6 +16,7 @@
../../profiles/nixvim.nix
../../profiles/postgres.nix
../../profiles/router.nix
../../profiles/unbound.nix
../../profiles/unifi.nix
../../profiles/vaultwarden.nix
../../profiles/wireguard.nix

View file

@ -33,6 +33,15 @@
description = "Default locale for the system";
};
localDomain = lib.mkOption {
type = lib.types.str;
## @TODO: Should this be "local"?
default = "localdomain";
description = "local lan domain";
};
## @TODO: Deduplicate this with localDomain
## recursive?
searchDomainsLocal = lib.mkOption {
type = lib.types.listOf lib.types.str;
## @TODO: Should this be "local"?
@ -46,6 +55,12 @@
description = "Domain for the system";
};
additionalDomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = "Additional zones for the system";
};
adminUsername = lib.mkOption {
type = lib.types.str;
default = "homefree";
@ -69,6 +84,9 @@
};
};
## @TODO: Add default subnet and gateway config, e.g. 10.0.0.0/24, 10.0.0.1
## @TODO: This section doesn't make sense. Some network config is in "system" above
## and some is in separate services, e.g. unbound and ddns
network = {
## @TODO: Detect during setup
wan-interface = lib.mkOption {
@ -83,55 +101,128 @@
default = "ens5";
description = "Internal interface to the local network";
};
};
ddclient = {
enable = lib.mkOption {
static-ip-expiration = lib.mkOption {
type = lib.types.str;
default = "3d";
description = "Expiration time of static IPs";
};
static-ips = lib.mkOption {
default = [];
description = "Static IP mappings";
type = with lib.types; listOf (submodule {
options = {
mac-address = lib.mkOption {
type = lib.types.str;
description = "MAC address to assign IP to";
};
hostname = lib.mkOption {
type = lib.types.str;
description = "Hostname to assign to IP";
};
ip = lib.mkOption {
type = lib.types.str;
description = "IP Address";
};
};
});
};
## @TODO: Make type for dns override entry
dns-overrides = lib.mkOption {
description = "dns hostname to IP overrides";
default = [];
type = with lib.types; listOf (submodule {
options = {
hostname = lib.mkOption {
type = lib.types.str;
description = "Hostname of override";
};
domain = lib.mkOption {
type = lib.types.str;
description = "Domain of override";
};
ip = lib.mkOption {
type = lib.types.str;
description = "IP Address";
};
};
});
};
enable-adblock = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable dynamic DNS client";
};
protocol = lib.mkOption {
type = lib.types.str;
default = "hetzner";
description = "Protocol for dynamic DNS client";
description = "enable ad blocking";
};
username = lib.mkOption {
type = lib.types.str;
default = "erahhal";
description = "Username for dynamic DNS client";
blocked-domains = lib.mkOption {
type = lib.typse.listOf lib.types.str;
default = [];
description = "list of domains to block";
};
};
zone = lib.mkOption {
type = lib.types.str;
default = "homefree.host";
description = "Zone for dynamic DNS client";
};
dynamic-dns = lib.mkOption {
description = "Dynamic DNS Config";
default = [];
type = with lib.types; listOf (submodule {
options = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "enable dynamic dns for zone";
};
interval = lib.mkOption {
type = lib.types.str;
default = "10m";
description = "Interval for dynamic DNS client";
};
## @TODO: validate against network.domain and network.additionalDomains
zone = lib.mkOption {
type = lib.types.str;
default = "homefree.host";
description = "Zone for dynamic DNS client";
};
domains = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "@" "*" "www" "dev" ];
description = "Domains for dynamic DNS client";
};
protocol = lib.mkOption {
type = lib.types.str;
default = "hetzner";
description = "Protocol for dynamic DNS client";
};
usev4 = lib.mkOption {
type = lib.types.str;
default = "web, web=ipinfo.io/ip";
description = "Use format for obtaining ipv4 for dynamic DNS client";
};
username = lib.mkOption {
type = lib.types.str;
default = "erahhal";
description = "Username for dynamic DNS client";
};
usev6 = lib.mkOption {
type = lib.types.str;
default = "web, web=v6.ipinfo.io/ip";
description = "Use format for obtaining ipv6 for dynamic DNS client";
};
interval = lib.mkOption {
type = lib.types.str;
default = "10m";
description = "Interval for dynamic DNS client";
};
domains = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "@" "*" "www" "dev" ];
description = "Domains for dynamic DNS client";
};
usev4 = lib.mkOption {
type = lib.types.str;
default = "web, web=ipinfo.io/ip";
description = "Use format for obtaining ipv4 for dynamic DNS client";
};
usev6 = lib.mkOption {
type = lib.types.str;
default = "web, web=v6.ipinfo.io/ip";
description = "Use format for obtaining ipv6 for dynamic DNS client";
};
};
});
};
wireguard = {

View file

@ -1,5 +1,8 @@
{ config, inputs, pkgs, ... }:
let
## @TODO: Update to support multiple zones
ddclientConfig = builtins.elemAt config.homefree.dynamic-dns 0;
in
{
#-----------------------------------------------------------------------------------------------------
# Dynamic DNS
@ -7,14 +10,14 @@
services.ddclient = {
enable = true;
interval = config.homefree.ddclient.interval;
protocol = config.homefree.ddclient.protocol;
username = config.homefree.ddclient.username;
zone = config.homefree.ddclient.zone;
domains = config.homefree.ddclient.domains;
interval = ddclientConfig.interval;
protocol = ddclientConfig.protocol;
username = ddclientConfig.username;
zone = ddclientConfig.zone;
domains = ddclientConfig.domains;
passwordFile = "/run/secrets/ddclient/ddclient-password";
usev4 = config.homefree.ddclient.usev4;
usev6 = config.homefree.ddclient.usev6;
usev4 = ddclientConfig.usev4;
usev6 = ddclientConfig.usev6;
verbose = true;
};

53
profiles/dnsmasq.nix Normal file
View file

@ -0,0 +1,53 @@
{ config, lib, ... }:
{
services.dnsmasq = {
enable = true;
settings = {
## @TODO
## @WARNING - changes to this do not clear out old entries from /etc/dnsmasq-conf.conf
## Only DHCP server on network
dhcp-authoritative = true;
## Enable Router Advertising for ipv6
enable-ra = true;
## DNS servers to pass to clients
## @TODO: Make this based on configured gateway IP
server = [ "10.1.1.1" ];
## Which interfaces to bind to
interface = [
# "${lan-interface}.${builtins.toString vlan-lan-id}"
# "${lan-interface}.${builtins.toString vlan-iot-id}"
# "${lan-interface}.${builtins.toString vlan-guest-id}"
config.homefree.network.lan-interface
];
## IP ranges to hand out
dhcp-range = [
# "lan,10.1.1.100,10.1.1.254,255.255.255.0,8h"
# "iot,10.2.1.100,10.2.1.254,255.255.255.0,8h"
# "guest,10.3.1.100,10.3.1.254,255.255.255.0,8h"
"${config.homefree.network.lan-interface},10.1.1.100,10.1.1.254,255.255.255.0,8h"
];
## Disable DNS, since Unbound is handling DNS
port = 0;
cache-size = 500;
## Additional DHCP options
dhcp-option = [
"option6:dns-server,[::]" # @TODO: point this at Unbound when ipv6 is setup
"option:dns-server,10.1.1.1"
];
dhcp-host = lib.map (ip-config:
"${ip-config.mac-address},${ip-config.hostname},${ip-config.ip},${config.homefree.network.static-ip-expiration}")
config.homefree.network.static-ips;
};
};
}

View file

@ -8,9 +8,6 @@ let
vlan-lan-id = 200;
vlan-iot-id = 201;
vlan-guest-id = 202;
# lan-interface = "ens3";
dns-servers = [ "1.1.1.1" "1.0.0.1" ];
adlist = homefree-inputs.adblock-unbound.packages.${pkgs.system};
in
{
@ -49,7 +46,8 @@ in
#-----------------------------------------------------------------------------------------------------
useDHCP = false;
nameservers = dns-servers;
## @TODO: Base on config for lan gateway
nameservers = [ "10.1.1.1" ];
# resolvconf = {
# };
@ -207,7 +205,7 @@ in
};
#-----------------------------------------------------------------------------------------------------
# DHCP
# DHCP/DNS
#-----------------------------------------------------------------------------------------------------
# See: https://nixos.wiki/wiki/Systemd-resolved
@ -222,130 +220,6 @@ in
'';
};
services.dnsmasq = {
enable = true;
settings = {
## @TODO
## @WARNING - changes to this do not clear out old entries from /etc/dnsmasq-conf.conf
## Only DHCP server on network
dhcp-authoritative = true;
## Enable Router Advertising for ipv6
enable-ra = true;
## DNS servers to pass to clients
server = dns-servers;
## Which interfaces to bind to
interface = [
# "${lan-interface}.${builtins.toString vlan-lan-id}"
# "${lan-interface}.${builtins.toString vlan-iot-id}"
# "${lan-interface}.${builtins.toString vlan-guest-id}"
lan-interface
];
## IP ranges to hand out
dhcp-range = [
# "lan,10.1.1.100,10.1.1.254,255.255.255.0,8h"
# "iot,10.2.1.100,10.2.1.254,255.255.255.0,8h"
# "guest,10.3.1.100,10.3.1.254,255.255.255.0,8h"
"${lan-interface},10.1.1.100,10.1.1.254,255.255.255.0,8h"
];
## Disable DNS, since Unbound is handling DNS
port = 0;
## Additional DHCP options
dhcp-option = [
"option6:dns-server,[::]" # @TODO: point this at Unbound when ipv6 is setup
"option:dns-server,10.1.1.1"
];
cache-size = 500;
};
};
#-----------------------------------------------------------------------------------------------------
# DNS
#-----------------------------------------------------------------------------------------------------
## @TODO - Setup Unbound
## See: https://blog.josefsson.org/2015/10/26/combining-dnsmasq-and-unbound/
services.unbound = {
enable = true;
user = "root";
resolveLocalQueries = true;
settings = {
server = {
include = [
"\"${adlist.unbound-adblockStevenBlack}\""
];
port = 5353;
interface = [
"127.0.0.1"
"::1"
"10.1.1.1"
];
access-control = [
"127.0.0.1/8 allow"
"::1 allow"
"10.1.1.1/8 allow"
# @TODO: need ipv6 address
];
outgoing-interface = [
## @TODO: should be WAN IP - how to get this automatically?
"10.0.2.15"
# @TODO: need ipv6 address
];
local-zone = [
"\"homefree.lan.\" static"
];
local-data = [
"\"radicale.lan. IN A 10.1.1.1\""
];
local-data-ptr = [
"\"10.1.1.1 radicale.lan\""
];
hide-identity = true;
hide-version = true;
# Based on recommended settings in https://doc.pi-hole.net/guides/dns/unbound/#configure-unbound
harden-glue = true;
harden-dnssec-stripped = true;
use-caps-for-id = false;
prefetch = true;
edns-buffer-size = 1232;
};
forward-zone = [
{
name = ".";
forward-addr = [
"9.9.9.9#dns.quad9.net"
"1.1.1.1@853#cloudflare-dns.com"
"1.0.0.1@853#cloudflare-dns.com"
];
forward-tls-upstream = "yes";
}
# {
# name = "example.org.";
# forward-addr = [
# "1.1.1.1@853#cloudflare-dns.com"
# "1.0.0.1@853#cloudflare-dns.com"
# ];
# }
];
remote-control.control-enable = true;
};
};
#-----------------------------------------------------------------------------------------------------
# Service Discovery
#-----------------------------------------------------------------------------------------------------

171
profiles/unbound.nix Normal file
View file

@ -0,0 +1,171 @@
{ homefree-inputs, config, lib, pkgs, ... }:
let
adlist = homefree-inputs.adblock-unbound.packages.${pkgs.system};
zones = [config.homefree.system.domain] ++ config.homefree.system.additionalDomains;
in
{
## See: https://blog.josefsson.org/2015/10/26/combining-dnsmasq-and-unbound/
## Unbound is a caching resolver, not meant to be used as authoritative.
## nbound does support simple authoritative hosting with local-zone config.
## For a proper authoritative DNS, look at NSD.
services.unbound = {
enable = true;
user = "root";
resolveLocalQueries = true;
settings = {
server = {
include = [
"\"${adlist.unbound-adblockStevenBlack}\""
];
port = 53530;
interface = [
"127.0.0.1"
"::1"
"10.1.1.1"
];
access-control = [
"127.0.0.1/8 allow"
"::1 allow"
"10.1.1.1/8 allow"
# @TODO: need ipv6 address
];
outgoing-interface = [
## @TODO: should be WAN IP - how to get this automatically?
"10.0.2.15"
# @TODO: need ipv6 address
];
local-zone = [
"\"homefree.lan\" static"
"\"homefree.host\" transparent"
"\"rahh.al\" transparent"
];
## @TODO: Add config.homefree.network.blocked-domains as such:
# local-zone: "example.org" always_nxdomain
## Record format:
## NAME CLASS (default: IN) TYPE RDATA
## localhost IN A 127.0.0.1
local-data =
[
"\"localhost A 127.0.0.1\""
"\"localhost AAAA ::1\""
]
++
(lib.map (zone: "\"localhost.${zone} IN A 127.0.0.1\"") zones)
++
(lib.map (zone: "\"${config.homefree.system.hostName}.${zone} IN A 127.0.0.1\"") zones)
++
(lib.map (local-data-config:
if builtins.hasAttr "domain" local-data-config then
"\"${local-data-config.hostname}.${local-data-config.domain} IN A ${local-data-config.ip}\""
else
"\"${local-data-config.hostname} IN A ${local-data-config.ip}\""
) config.homefree.network.dns-overrides
)
++
## router lan ip with public domains
(lib.map (zone: "\"${config.homefree.system.hostName}.${zone} IN A 10.0.0.1\"") zones)
++
## router vpn ip with public domains
(lib.map (zone: "\"${config.homefree.system.hostName}.${zone} IN A 192.168.2.1\"") zones)
++
## @TODO: Move to config for gateway IP
[
## router lan IP
"\"${config.homefree.system.hostName} IN A 10.0.0.1\""
## router lan IP with local domain
"\"${config.homefree.system.hostName}.${config.homefree.system.localDomain} IN A 10.0.0.1\""
## router vpn IP
"\"${config.homefree.system.hostName} IN A 192.168.2.1\""
## router vpn IP with local domain
"\"${config.homefree.system.hostName}.${config.homefree.system.localDomain} IN A 192.168.2.1\""
]
++
## @TODO: How to configure these at runtime?
## router wan IP with public domain
(lib.map (zone: "\"${config.homefree.system.hostName}.${zone} IN A 104.182.229.64\"") zones)
++
## Bare hostname maps
[
## router wan IP
"\"${config.homefree.system.hostName} IN A 104.182.229.64\""
## router wan ipv6 IP
"\"${config.homefree.system.hostName} IN AAAA 2600:1700:ab00:4650:2e0:67ff:fe22:3e62\""
## ??
"\"${config.homefree.system.hostName} IN AAAA 2600:1700:ab00:465f:2e0:67ff:fe22:3e63\""
]
++
## router wan IPv6 with public domain
(lib.map (zone: "\"${config.homefree.system.hostName}.${zone} IN AAAA 2600:1700:ab00:4650:2e0:67ff:fe22:3e62\"") zones)
++
(lib.map (zone: "\"${config.homefree.system.hostName}.${zone} IN AAAA 2600:1700:ab00:465f:2e0:67ff:fe22:3e64\"") zones)
++
(lib.map (ip-config:
"\"${ip-config.hostname}.${config.homefree.system.localDomain} IN A ${ip-config.ip}\"")
config.homefree.network.static-ips)
## @TODO: Add caddy domains to zones, e.g.:
## "auth.rahh.al IN A 10.0.0.1"
;
local-data-ptr = [
"\"::1 localhost\""
"\"127.0.0.1 localhost\""
]
++
(lib.concatLists
(lib.map (zone:
(lib.map (ip-config: "\"${ip-config.ip} ${ip-config.hostname}.${zone}\"") config.homefree.network.static-ips)
) zones)
)
## @TODO: Add caddy domains to zones, e.g.:
## "10.0.0.1 auth.rahh.al"
;
hide-identity = true;
hide-version = true;
# Based on recommended settings in https://doc.pi-hole.net/guides/dns/unbound/#configure-unbound
harden-glue = true;
harden-dnssec-stripped = true;
use-caps-for-id = false;
prefetch = true;
edns-buffer-size = 1232;
};
#
# range-lan = {
# start = "10.0.0.200";
# end = "10.0.0.254";
# domain = "localdomain";
# };
forward-zone = [
{
name = ".";
forward-addr = [
"9.9.9.9#dns.quad9.net"
"1.1.1.1@853#cloudflare-dns.com"
"1.0.0.1@853#cloudflare-dns.com"
];
forward-tls-upstream = "yes";
}
# {
# name = "example.org.";
# forward-addr = [
# "1.1.1.1@853#cloudflare-dns.com"
# "1.0.0.1@853#cloudflare-dns.com"
# ];
# }
];
remote-control.control-enable = true;
};
};
}