@ -4,51 +4,28 @@ with lib;
let
cfg = config . services . unbound ;
stateDir = " / v a r / l i b / u n b o u n d " ;
access = concatMapStringsSep " \n " ( x : " a c c e s s - c o n t r o l : ${ x } a l l o w " ) cfg . allowedAccess ;
interfaces = concatMapStringsSep " \n " ( x : " i n t e r f a c e : ${ x } " ) cfg . interfaces ;
isLocalAddress = x : substring 0 3 x == " : : 1 " || substring 0 9 x == " 1 2 7 . 0 . 0 . 1 " ;
forward =
optionalString ( any isLocalAddress cfg . forwardAddresses ) ''
do-not-query-localhost : no
''
+ optionalString ( cfg . forwardAddresses != [ ] ) ''
forward-zone :
name : .
''
+ concatMapStringsSep " \n " ( x : " f o r w a r d - a d d r : ${ x } " ) cfg . forwardAddresses ;
rootTrustAnchorFile = " ${ stateDir } / r o o t . k e y " ;
trustAnchor = optionalString cfg . enableRootTrustAnchor
" a u t o - t r u s t - a n c h o r - f i l e : ${ rootTrustAnchorFile } " ;
confFile = pkgs . writeText " u n b o u n d . c o n f " ''
server :
ip-freebind : yes
directory : " ${ stateDir } "
username : unbound
chroot : " "
pidfile : " "
# when running under systemd there is no need to daemonize
do-daemonize : no
$ { interfaces }
$ { access }
$ { trustAnchor }
$ { lib . optionalString ( cfg . localControlSocketPath != null ) ''
remote-control :
control-enable : yes
control-interface : $ { cfg . localControlSocketPath }
'' }
$ { cfg . extraConfig }
$ { forward }
'' ;
in
{
yesOrNo = v : if v then " y e s " else " n o " ;
toOption = indent : n : v : " ${ indent } ${ toString n } : ${ v } " ;
toConf = indent : n : v :
if builtins . isFloat v then ( toOption indent n ( builtins . toJSON v ) )
else if isInt v then ( toOption indent n ( toString v ) )
else if isBool v then ( toOption indent n ( yesOrNo v ) )
else if isString v then ( toOption indent n v )
else if isList v then ( concatMapStringsSep " \n " ( toConf indent n ) v )
else if isAttrs v then ( concatStringsSep " \n " (
[ " ${ indent } ${ n } : " ] ++ (
mapAttrsToList ( toConf " ${ indent } " ) v
)
) )
else throw ( traceSeq v " s e r v i c e s . u n b o u n d . s e t t i n g s : u n e x p e c t e d t y p e " ) ;
confFile = pkgs . writeText " u n b o u n d . c o n f " ( concatStringsSep " \n " ( ( mapAttrsToList ( toConf " " ) cfg . settings ) ++ [ " " ] ) ) ;
rootTrustAnchorFile = " ${ cfg . stateDir } / r o o t . k e y " ;
in {
###### interface
@ -64,25 +41,30 @@ in
description = " T h e u n b o u n d p a c k a g e t o u s e " ;
} ;
allowedAccess = mkOption {
default = [ " 1 2 7 . 0 . 0 . 0 / 2 4 " ] ;
type = types . listOf types . str ;
description = " W h a t n e t w o r k s a r e a l l o w e d t o u s e u n b o u n d a s a r e s o l v e r . " ;
user = mkOption {
type = types . str ;
default = " u n b o u n d " ;
description = " U s e r a c c o u n t u n d e r w h i c h u n b o u n d r u n s . " ;
} ;
interfaces = mkOption {
default = [ " 1 2 7 . 0 . 0 . 1 " ] ++ optional config . networking . enableIPv6 " : : 1 " ;
type = types . listOf types . str ;
description = ''
What addresses the server should listen on . This supports the interface syntax documented in
<citerefentry> <refentrytitle> unbound . conf < /refentrytitle > <manvolnum> 8 < /manvolnum > < /citerefentry > .
'' ;
group = mkOption {
type = types . str ;
default = " u n b o u n d " ;
description = " G r o u p u n d e r w h i c h u n b o u n d r u n s . " ;
} ;
forwardAddresses = mkOption {
default = [ ] ;
type = types . listOf types . str ;
description = " W h a t s e r v e r s t o f o r w a r d q u e r i e s t o . " ;
stateDir = mkOption {
default = " / v a r / l i b / u n b o u n d " ;
description = " D i r e c t o r y h o l d i n g a l l s t a t e f o r u n b o u n d t o r u n . " ;
} ;
resolveLocalQueries = mkOption {
type = types . bool ;
default = true ;
description = ''
Whether unbound should resolve local queries ( i . e . add 127 .0 .0 .1 to
/etc/resolv.conf ) .
'' ;
} ;
enableRootTrustAnchor = mkOption {
@ -106,23 +88,66 @@ in
and group will be <literal> nogroup < /literal > .
Users that should be permitted to access the socket must be in the
<literal> unbound < /literal > group .
<literal> config . services . unbound . group < /literal > group .
If this option is <literal> null < /literal > remote control will not be
configured at all . Unbounds default values apply .
enabled . Unbounds default values apply .
'' ;
} ;
extraConfig = mkOption {
default = " " ;
type = types . lines ;
settings = mkOption {
default = { } ;
type = with types ; submodule {
freeformType = let
validSettingsPrimitiveTypes = oneOf [ int str bool float ] ;
validSettingsTypes = oneOf [ validSettingsPrimitiveTypes ( listOf validSettingsPrimitiveTypes ) ] ;
settingsType = ( attrsOf validSettingsTypes ) ;
in attrsOf ( oneOf [ string settingsType ( listOf settingsType ) ] )
// { description = ''
unbound . conf configuration type . The format consist of an attribute
set of settings . Each settings can be either one value , a list of
values or an attribute set . The allowed values are integers ,
strings , booleans or floats .
'' ;
} ;
options = {
remote-control . control-enable = mkOption {
type = bool ;
default = false ;
internal = true ;
} ;
} ;
} ;
example = literalExample ''
{
server = {
interface = [ " 1 2 7 . 0 . 0 . 1 " ] ;
} ;
forward-zone = [
{
name = " . " ;
forward-addr = " 1 . 1 . 1 . 1 @ 8 5 3 # c l o u d f l a r e - d n s . c o m " ;
}
{
name = " e x a m p l e . o r g . " ;
forward-addr = [
" 1 . 1 . 1 . 1 @ 8 5 3 # c l o u d f l a r e - d n s . c o m "
" 1 . 0 . 0 . 1 @ 8 5 3 # c l o u d f l a r e - d n s . c o m "
] ;
}
] ;
remote-control . control-enable = true ;
} ;
'' ;
description = ''
Extra unbound config . See
<citerefentry> <refentrytitle> unbound . conf < /refentrytitle > <manvolnum> 8
< /manvolnum > < /citerefentry > .
Declarative Unbound configuration
See the <citerefentry> <refentrytitle> unbound . conf < /refentrytitle >
<manvolnum> 5 < /manvolnum > < /citerefentry > manpage for a list of
available options .
'' ;
} ;
} ;
} ;
@ -130,23 +155,56 @@ in
config = mkIf cfg . enable {
services . unbound . settings = {
server = {
directory = mkDefault cfg . stateDir ;
username = cfg . user ;
chroot = '' " " '' ;
pidfile = '' " " '' ;
# when running under systemd there is no need to daemonize
do-daemonize = false ;
interface = mkDefault ( [ " 1 2 7 . 0 . 0 . 1 " ] ++ ( optional config . networking . enableIPv6 " : : 1 " ) ) ;
access-control = mkDefault ( [ " 1 2 7 . 0 . 0 . 0 / 8 a l l o w " ] ++ ( optional config . networking . enableIPv6 " : : 1 / 1 2 8 a l l o w " ) ) ;
auto-trust-anchor-file = mkIf cfg . enableRootTrustAnchor rootTrustAnchorFile ;
tls-cert-bundle = mkDefault " / e t c / s s l / c e r t s / c a - c e r t i f i c a t e s . c r t " ;
# prevent race conditions on system startup when interfaces are not yet
# configured
ip-freebind = mkDefault true ;
} ;
remote-control = {
control-enable = mkDefault false ;
control-interface = mkDefault ( [ " 1 2 7 . 0 . 0 . 1 " ] ++ ( optional config . networking . enableIPv6 " : : 1 " ) ) ;
server-key-file = mkDefault " ${ cfg . stateDir } / u n b o u n d _ s e r v e r . k e y " ;
server-cert-file = mkDefault " ${ cfg . stateDir } / u n b o u n d _ s e r v e r . p e m " ;
control-key-file = mkDefault " ${ cfg . stateDir } / u n b o u n d _ c o n t r o l . k e y " ;
control-cert-file = mkDefault " ${ cfg . stateDir } / u n b o u n d _ c o n t r o l . p e m " ;
} // optionalAttrs ( cfg . localControlSocketPath != null ) {
control-enable = true ;
control-interface = cfg . localControlSocketPath ;
} ;
} ;
environment . systemPackages = [ cfg . package ] ;
users . users . unbound = {
description = " u n b o u n d d a e m o n u s e r " ;
isSystemUser = true ;
group = lib . mkIf ( cfg . localControlSocketPath != null ) ( lib . mkDefault " u n b o u n d " ) ;
users . users = mkIf ( cfg . user == " u n b o u n d " ) {
unbound = {
description = " u n b o u n d d a e m o n u s e r " ;
isSystemUser = true ;
group = cfg . group ;
} ;
} ;
# We need a group so that we can give users access to the configured
# control socket. Unbound allows access to the socket only to the unbound
# user and the primary group.
users . groups = lib . mkIf ( cfg . localControlSocketPath != null ) {
users . groups = mkIf ( cfg . group == " u n b o u n d " ) {
unbound = { } ;
} ;
networking . resolvconf . useLocalResolver = mkDefault true ;
networking = mkIf cfg . resolveLocalQueries {
resolvconf = {
useLocalResolver = mkDefault true ;
} ;
networkmanager . dns = " u n b o u n d " ;
} ;
environment . etc . " u n b o u n d / u n b o u n d . c o n f " . source = confFile ;
@ -156,8 +214,15 @@ in
before = [ " n s s - l o o k u p . t a r g e t " ] ;
wantedBy = [ " m u l t i - u s e r . t a r g e t " " n s s - l o o k u p . t a r g e t " ] ;
preStart = lib . mkIf cfg . enableRootTrustAnchor ''
$ { cfg . package } /bin/unbound-anchor - a $ { rootTrustAnchorFile } || echo " R o o t a n c h o r u p d a t e d ! "
path = mkIf cfg . settings . remote-control . control-enable [ pkgs . openssl ] ;
preStart = ''
$ { optionalString cfg . enableRootTrustAnchor ''
$ { cfg . package } /bin/unbound-anchor - a $ { rootTrustAnchorFile } || echo " R o o t a n c h o r u p d a t e d ! "
'' }
$ { optionalString cfg . settings . remote-control . control-enable ''
$ { cfg . package } /bin/unbound-control-setup - d $ { cfg . stateDir }
'' }
'' ;
restartTriggers = [
@ -181,8 +246,8 @@ in
" C A P _ S Y S _ R E S O U R C E "
] ;
User = " u n b o u n d " ;
Group = lib . mkIf ( cfg . localControlSocketPath != null ) ( lib . mkDefault " u n b o u n d " ) ;
User = cfg . user ;
Group = cfg . group ;
MemoryDenyWriteExecute = true ;
NoNewPrivileges = true ;
@ -211,9 +276,29 @@ in
RestrictNamespaces = true ;
LockPersonality = true ;
RestrictSUIDSGID = true ;
Restart = " o n - f a i l u r e " ;
RestartSec = " 5 s " ;
} ;
} ;
# If networkmanager is enabled, ask it to interface with unbound.
networking . networkmanager . dns = " u n b o u n d " ;
} ;
imports = [
( mkRenamedOptionModule [ " s e r v i c e s " " u n b o u n d " " i n t e r f a c e s " ] [ " s e r v i c e s " " u n b o u n d " " s e t t i n g s " " s e r v e r " " i n t e r f a c e " ] )
( mkChangedOptionModule [ " s e r v i c e s " " u n b o u n d " " a l l o w e d A c c e s s " ] [ " s e r v i c e s " " u n b o u n d " " s e t t i n g s " " s e r v e r " " a c c e s s - c o n t r o l " ] (
config : map ( value : " ${ value } a l l o w " ) ( getAttrFromPath [ " s e r v i c e s " " u n b o u n d " " a l l o w e d A c c e s s " ] config )
) )
( mkRemovedOptionModule [ " s e r v i c e s " " u n b o u n d " " f o r w a r d A d d r e s s e s " ] ''
Add a new setting :
services . unbound . settings . forward-zone = [ {
name = " . " ;
forward-addr = [ # Your current services.unbound.forwardAddresses ];
} ] ;
If any of those addresses are local addresses ( 127 .0 .0 .1 or : : 1 ) , you must
also set services . unbound . settings . server . do-not-query-localhost to false .
'' )
( mkRemovedOptionModule [ " s e r v i c e s " " u n b o u n d " " e x t r a C o n f i g " ] ''
You can use services . unbound . settings to add any configuration you want .
'' )
] ;
}