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

View file

@ -33,6 +33,15 @@
description = "Default locale for the system"; 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 { searchDomainsLocal = lib.mkOption {
type = lib.types.listOf lib.types.str; type = lib.types.listOf lib.types.str;
## @TODO: Should this be "local"? ## @TODO: Should this be "local"?
@ -46,6 +55,12 @@
description = "Domain for the system"; 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 { adminUsername = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "homefree"; 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 = { network = {
## @TODO: Detect during setup ## @TODO: Detect during setup
wan-interface = lib.mkOption { wan-interface = lib.mkOption {
@ -83,14 +101,91 @@
default = "ens5"; default = "ens5";
description = "Internal interface to the local network"; description = "Internal interface to the local network";
}; };
static-ip-expiration = lib.mkOption {
type = lib.types.str;
default = "3d";
description = "Expiration time of static IPs";
}; };
ddclient = { 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 ad blocking";
};
blocked-domains = lib.mkOption {
type = lib.typse.listOf lib.types.str;
default = [];
description = "list of domains to block";
};
};
dynamic-dns = lib.mkOption {
description = "Dynamic DNS Config";
default = [];
type = with lib.types; listOf (submodule {
options = {
enable = lib.mkOption { enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
description = "Enable dynamic DNS client"; description = "enable dynamic dns for zone";
}; };
## @TODO: validate against network.domain and network.additionalDomains
zone = lib.mkOption {
type = lib.types.str;
default = "homefree.host";
description = "Zone for dynamic DNS client";
};
protocol = lib.mkOption { protocol = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "hetzner"; default = "hetzner";
@ -103,12 +198,6 @@
description = "Username for dynamic DNS client"; description = "Username for dynamic DNS client";
}; };
zone = lib.mkOption {
type = lib.types.str;
default = "homefree.host";
description = "Zone for dynamic DNS client";
};
interval = lib.mkOption { interval = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "10m"; default = "10m";
@ -133,6 +222,8 @@
description = "Use format for obtaining ipv6 for dynamic DNS client"; description = "Use format for obtaining ipv6 for dynamic DNS client";
}; };
}; };
});
};
wireguard = { wireguard = {
peers = lib.mkOption { peers = lib.mkOption {

View file

@ -1,5 +1,8 @@
{ config, inputs, pkgs, ... }: { config, inputs, pkgs, ... }:
let
## @TODO: Update to support multiple zones
ddclientConfig = builtins.elemAt config.homefree.dynamic-dns 0;
in
{ {
#----------------------------------------------------------------------------------------------------- #-----------------------------------------------------------------------------------------------------
# Dynamic DNS # Dynamic DNS
@ -7,14 +10,14 @@
services.ddclient = { services.ddclient = {
enable = true; enable = true;
interval = config.homefree.ddclient.interval; interval = ddclientConfig.interval;
protocol = config.homefree.ddclient.protocol; protocol = ddclientConfig.protocol;
username = config.homefree.ddclient.username; username = ddclientConfig.username;
zone = config.homefree.ddclient.zone; zone = ddclientConfig.zone;
domains = config.homefree.ddclient.domains; domains = ddclientConfig.domains;
passwordFile = "/run/secrets/ddclient/ddclient-password"; passwordFile = "/run/secrets/ddclient/ddclient-password";
usev4 = config.homefree.ddclient.usev4; usev4 = ddclientConfig.usev4;
usev6 = config.homefree.ddclient.usev6; usev6 = ddclientConfig.usev6;
verbose = true; 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-lan-id = 200;
vlan-iot-id = 201; vlan-iot-id = 201;
vlan-guest-id = 202; 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 in
{ {
@ -49,7 +46,8 @@ in
#----------------------------------------------------------------------------------------------------- #-----------------------------------------------------------------------------------------------------
useDHCP = false; useDHCP = false;
nameservers = dns-servers; ## @TODO: Base on config for lan gateway
nameservers = [ "10.1.1.1" ];
# resolvconf = { # resolvconf = {
# }; # };
@ -207,7 +205,7 @@ in
}; };
#----------------------------------------------------------------------------------------------------- #-----------------------------------------------------------------------------------------------------
# DHCP # DHCP/DNS
#----------------------------------------------------------------------------------------------------- #-----------------------------------------------------------------------------------------------------
# See: https://nixos.wiki/wiki/Systemd-resolved # 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 # 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;
};
};
}