-
-
Notifications
You must be signed in to change notification settings - Fork 14k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding crab hole #341598
base: master
Are you sure you want to change the base?
Adding crab hole #341598
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi there! Great work. I've added a couple of suggestions
description = "Crab-holes data directory."; | ||
}; | ||
|
||
config = mkOption { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks overly complicated to me.
Is there a reason it's not a regular RFC 42-style settings option?
config = mkOption { | |
settings = mkOption { |
warnings = | ||
( | ||
if (cfg.configFile != null) && (cfg.config != null) then | ||
[ | ||
'' | ||
crab-hole: Config File and settings options are both set. | ||
Config File will used instead! | ||
'' | ||
] | ||
else | ||
[ ] | ||
) | ||
++ ( | ||
if (cfg.config.downstream == [ ]) then | ||
[ | ||
'' | ||
crab-hole: Empty downstream specified. Server will not be accessible | ||
'' | ||
] | ||
else | ||
[ ] | ||
) | ||
++ ( | ||
if (cfg.config.upstream.name_servers == [ ]) then | ||
[ | ||
'' | ||
crab-hole: Empty upstream specified. Server will not be able to resolve | ||
'' | ||
] | ||
else | ||
[ ] | ||
); | ||
|
||
assertions = [ | ||
{ | ||
assertion = (cfg.configFile != null) || (cfg.config != null); | ||
message = "crab-hole: Need to set settings or config file"; | ||
} | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With RFC-42-style settings this is not required:
warnings = | |
( | |
if (cfg.configFile != null) && (cfg.config != null) then | |
[ | |
'' | |
crab-hole: Config File and settings options are both set. | |
Config File will used instead! | |
'' | |
] | |
else | |
[ ] | |
) | |
++ ( | |
if (cfg.config.downstream == [ ]) then | |
[ | |
'' | |
crab-hole: Empty downstream specified. Server will not be accessible | |
'' | |
] | |
else | |
[ ] | |
) | |
++ ( | |
if (cfg.config.upstream.name_servers == [ ]) then | |
[ | |
'' | |
crab-hole: Empty upstream specified. Server will not be able to resolve | |
'' | |
] | |
else | |
[ ] | |
); | |
assertions = [ | |
{ | |
assertion = (cfg.configFile != null) || (cfg.config != null); | |
message = "crab-hole: Need to set settings or config file"; | |
} | |
]; | |
services.crab-hole.configFile = lib.mkDefault (settingsFormat.generate "crab-hole.toml" cfg.settings); | |
environment.etc."crab-hole.toml".source = cfg.configFile; |
|
||
rustPlatform.buildRustPackage rec { | ||
pname = "crab-hole"; | ||
version = "0.1.9"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
version = "0.1.9"; | |
version = "0.1.9-unstable-2024-09-05"; |
|
||
meta.maintainers = [ | ||
lib.maintainers.NiklasVousten | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A meta.doc
would be great
pkgs, | ||
... | ||
}: | ||
with lib; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an anti-pattern
with lib; |
type = types.nullOr ( | ||
types.submodule { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
type = types.nullOr ( | |
types.submodule { | |
type = types.submodule { | |
freeformType = settingsFormat.type; |
CRAB_HOLE_DIR = cfg.workDir; | ||
}; | ||
serviceConfig = { | ||
Type = "simple"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the default
Type = "simple"; |
Group = cfg.group; | ||
WorkingDirectory = cfg.workDir; | ||
|
||
ExecStart = ''${pkgs.crab-hole}/bin/crab-hole''; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ExecStart = ''${pkgs.crab-hole}/bin/crab-hole''; | |
ExecStart = lib.getExe cfg.package; |
options = { | ||
services.crab-hole = { | ||
enable = mkEnableOption "Crab-hole Service"; | ||
|
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
meta.platforms = with lib.platforms; [ | ||
linux | ||
windows | ||
darwin | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
meta.platforms = with lib.platforms; [ | |
linux | |
windows | |
darwin | |
]; | |
platforms = lib.platforms.all; |
preStart = | ||
let | ||
settings = | ||
{ | ||
blocklist = { | ||
include_subdomains = cfg.config.blocklist.include_subdomains; | ||
lists = lists.forEach cfg.config.blocklist.lists ( | ||
v: if isPath v then ("file://${cfg.workDir}/${filename v}") else v | ||
); | ||
allow_list = lists.forEach cfg.config.blocklist.allow_list ( | ||
v: if isPath v then ("file://${cfg.workDir}/${filename v}") else v | ||
); | ||
}; | ||
downstream = forEach cfg.config.downstream ( | ||
x: | ||
if x.protocol == "udp" then | ||
{ | ||
protocol = x.protocol; | ||
listen = x.listen; | ||
port = x.port; | ||
} | ||
else if (x.protocol == "https" && x.dns_hostname != null) then | ||
x | ||
else | ||
{ | ||
protocol = x.protocol; | ||
listen = x.listen; | ||
port = x.port; | ||
certificate = x.certificate; | ||
key = x.key; | ||
timeout = x.timeout; | ||
} | ||
); | ||
upstream.name_servers = cfg.config.upstream.name_servers; | ||
} | ||
// ( | ||
if (cfg.config.api == null) then | ||
{ } | ||
else | ||
{ | ||
api = | ||
if (cfg.config.api.admin_key != null) then | ||
cfg.config.api | ||
else | ||
{ | ||
port = cfg.config.api.port; | ||
listen = cfg.config.api.listen; | ||
show_doc = cfg.config.api.show_doc; | ||
}; | ||
} | ||
); | ||
|
||
blockListFiles = builtins.filter (v: isPath v) cfg.config.blocklist.lists; | ||
allowListFiles = builtins.filter (v: isPath v) cfg.config.blocklist.allow_list; | ||
|
||
selectedConfig = | ||
if cfg.configFile != null then cfg.configFile else (configFormat.generate "config.toml" settings); | ||
in | ||
'' | ||
cp -f '${selectedConfig}' '${cfg.workDir}/config.toml' | ||
${builtins.concatStringsSep "\n" ( | ||
map (file: "cp -f '${file}' '${cfg.workDir}/${filename file}'") blockListFiles | ||
)} | ||
${builtins.concatStringsSep "\n" ( | ||
map (file: "cp -f '${file}' '${cfg.workDir}/${filename file}'") allowListFiles | ||
)} | ||
''; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
preStart = | |
let | |
settings = | |
{ | |
blocklist = { | |
include_subdomains = cfg.config.blocklist.include_subdomains; | |
lists = lists.forEach cfg.config.blocklist.lists ( | |
v: if isPath v then ("file://${cfg.workDir}/${filename v}") else v | |
); | |
allow_list = lists.forEach cfg.config.blocklist.allow_list ( | |
v: if isPath v then ("file://${cfg.workDir}/${filename v}") else v | |
); | |
}; | |
downstream = forEach cfg.config.downstream ( | |
x: | |
if x.protocol == "udp" then | |
{ | |
protocol = x.protocol; | |
listen = x.listen; | |
port = x.port; | |
} | |
else if (x.protocol == "https" && x.dns_hostname != null) then | |
x | |
else | |
{ | |
protocol = x.protocol; | |
listen = x.listen; | |
port = x.port; | |
certificate = x.certificate; | |
key = x.key; | |
timeout = x.timeout; | |
} | |
); | |
upstream.name_servers = cfg.config.upstream.name_servers; | |
} | |
// ( | |
if (cfg.config.api == null) then | |
{ } | |
else | |
{ | |
api = | |
if (cfg.config.api.admin_key != null) then | |
cfg.config.api | |
else | |
{ | |
port = cfg.config.api.port; | |
listen = cfg.config.api.listen; | |
show_doc = cfg.config.api.show_doc; | |
}; | |
} | |
); | |
blockListFiles = builtins.filter (v: isPath v) cfg.config.blocklist.lists; | |
allowListFiles = builtins.filter (v: isPath v) cfg.config.blocklist.allow_list; | |
selectedConfig = | |
if cfg.configFile != null then cfg.configFile else (configFormat.generate "config.toml" settings); | |
in | |
'' | |
cp -f '${selectedConfig}' '${cfg.workDir}/config.toml' | |
${builtins.concatStringsSep "\n" ( | |
map (file: "cp -f '${file}' '${cfg.workDir}/${filename file}'") blockListFiles | |
)} | |
${builtins.concatStringsSep "\n" ( | |
map (file: "cp -f '${file}' '${cfg.workDir}/${filename file}'") allowListFiles | |
)} | |
''; |
environment = { | ||
CRAB_HOLE_DIR = cfg.workDir; | ||
}; | ||
serviceConfig = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
serviceConfig = { | |
restartTriggers = [ cfg.configFile ]; | |
serviceConfig = { |
systemd.tmpfiles.rules = [ | ||
"d '${cfg.workDir}' 0750 ${cfg.user} ${cfg.group} - -" | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be replaced by StateDirectory
# Adding crab-hole user and group | ||
users.users = optionalAttrs (cfg.user == "crab-hole") { | ||
crab-hole = { | ||
description = "Crab-hole service"; | ||
home = cfg.workDir; | ||
group = cfg.group; | ||
isSystemUser = true; | ||
}; | ||
}; | ||
|
||
users.groups = optionalAttrs (cfg.group == "crab-hole") { | ||
crab-hole = { }; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be replaced by DynamicUser
environment.systemPackages = [ | ||
pkgs.crab-hole | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it really needed in PATH for everyone?
environment.systemPackages = [ | |
pkgs.crab-hole | |
]; |
Type = "simple"; | ||
User = cfg.user; | ||
Group = cfg.group; | ||
WorkingDirectory = cfg.workDir; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WorkingDirectory = cfg.workDir; | |
StateDirectory = "crab-hole"; | |
WorkingDirectory = "/var/lib/crab-hole"; |
User = cfg.user; | ||
Group = cfg.group; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
User = cfg.user; | |
Group = cfg.group; | |
DynamicUser = true; |
environment = { | ||
CRAB_HOLE_DIR = cfg.workDir; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be better to just set the home directory.
environment = { | |
CRAB_HOLE_DIR = cfg.workDir; | |
}; | |
environment.HOME = "/var/lib/crab-hole"; |
wantedBy = [ "multi-user.target" ]; | ||
after = [ "network-online.target" ]; | ||
wants = [ "network-online.target" ]; | ||
description = "Start the crab-hole dns server"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
description = "Start the crab-hole dns server"; | |
description = "crab-hole dns server"; |
config = mkOption { | ||
default = null; | ||
description = "Crab-holes config"; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An example would be great. You can reuse upstream example here using https://pseitz.github.io/toml-to-json-online-converter/ and https://json-to-nix.pages.dev
It's better to make it smaller though
example = { | |
downstream = [ | |
{ | |
listen = "localhost"; | |
port = 8080; | |
protocol = "udp"; | |
} | |
{ | |
listen = "[::]"; | |
port = 8053; | |
protocol = "udp"; | |
} | |
{ | |
certificate = "dns.example.com.crt"; | |
key = "dns.example.com.key"; | |
listen = "[::]"; | |
port = 8054; | |
protocol = "tls"; | |
timeout_ms = 3000; | |
} | |
{ | |
certificate = "dns.example.com.crt"; | |
dns_hostname = "dns.example.com"; | |
key = "dns.example.com.key"; | |
listen = "[::]"; | |
port = 8055; | |
protocol = "https"; | |
timeout_ms = 3000; | |
} | |
{ | |
certificate = "dns.example.com.crt"; | |
dns_hostname = "dns.example.com"; | |
key = "dns.example.com.key"; | |
listen = "127.0.0.1"; | |
port = 8055; | |
protocol = "quic"; | |
timeout_ms = 3000; | |
} | |
]; | |
api = { | |
admin_key = "1234"; | |
listen = "127.0.0.1"; | |
port = 8080; | |
show_doc = true; | |
}; | |
blocklist = { | |
allow_list = [ | |
"file:///allowed.txt" | |
]; | |
include_subdomains = true; | |
lists = [ | |
"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-porn/hosts" | |
"https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt" | |
"file:///blocked.txt" | |
]; | |
}; | |
upstream = { | |
name_servers = [ | |
{ | |
protocol = "tls"; | |
socket_addr = "[2606:4700:4700::1111]:853"; | |
tls_dns_name = "1dot1dot1dot1.cloudflare-dns.com"; | |
trust_nx_responses = false; | |
} | |
{ | |
protocol = "tls"; | |
socket_addr = "[2606:4700:4700::1001]:853"; | |
tls_dns_name = "1dot1dot1dot1.cloudflare-dns.com"; | |
trust_nx_responses = false; | |
} | |
{ | |
protocol = "tls"; | |
socket_addr = "1.1.1.1:853"; | |
tls_dns_name = "1dot1dot1dot1.cloudflare-dns.com"; | |
trust_nx_responses = false; | |
} | |
{ | |
protocol = "tls"; | |
socket_addr = "1.0.0.1:853"; | |
tls_dns_name = "1dot1dot1dot1.cloudflare-dns.com"; | |
trust_nx_responses = false; | |
} | |
]; | |
options = { | |
validate = true; | |
}; | |
}; | |
}; |
ExecStart = ''${pkgs.crab-hole}/bin/crab-hole''; | ||
|
||
AmbientCapabilities = "CAP_NET_BIND_SERVICE"; | ||
CapabilityBoundingSet = "CAP_NET_BIND_SERVICE"; |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
Thank you for your Feedback. This was very helpful! About some of the suggestions: In my understanding, the following line ensures, that the service can only get the specified capabilities. This would improve security. So I think it is sensible to keep this line. Now on the prestart block. It can be solved with an apply for the downstream options, similar to the prestart. Do you have a solution in mind for this case? Or can nudge me to a helpful resource? |
I'd need an example to give a useful advice, but I think just removing the offending options should fix the issue. It would still allow to set the options, but they won't be type-checked. On the other hand, you won't need to update the module when they change upstream. The nuclear solution would be to only leave |
3215570
to
6d03ac6
Compare
The I guess a user then has to check if the service started correctly and fix the config if this failed. It would be awesome if you could review the changes again @misuzu I struggled briefly with getting the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! I've added a couple of suggestions
lib.maintainers.NiklasVousten | ||
]; | ||
# Readme from upstream | ||
meta.doc = pkgs.writeText "crab-hole.md" '' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move the doc to its own file
meta.doc = pkgs.writeText "crab-hole.md" '' | |
meta.doc = ./crab-hole.md; |
|
||
## Configuration: {#module-services-crab-hole-configuration} | ||
Example config file using cloudflare as dot (dns-over-tls) upstream. | ||
```toml |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better to just leave a link to an example TOML file instead.
What would actually be useful here is a working configuration example of the crab-hole module, ready to be copy-pasted and customized.
warnings = | ||
if (cfg.settings.upstream.options != { }) && (cfg.settings.upstream.options.validate) then | ||
[ | ||
'' | ||
Validate options will ONLY allow DNSSec domains. See https://github.com/LuckyTurtleDev/crab-hole/issues/29 | ||
'' | ||
] | ||
else | ||
[ ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
warnings = | |
if (cfg.settings.upstream.options != { }) && (cfg.settings.upstream.options.validate) then | |
[ | |
'' | |
Validate options will ONLY allow DNSSec domains. See https://github.com/LuckyTurtleDev/crab-hole/issues/29 | |
'' | |
] | |
else | |
[ ]; | |
warnings = | |
lib.optional (cfg.settings.upstream.options.validate or false) | |
'' | |
Validate options will ONLY allow DNSSec domains. See https://github.com/LuckyTurtleDev/crab-hole/issues/29 | |
''; |
|
||
configFile = lib.mkOption { | ||
type = lib.types.path; | ||
description = "The config file of crab-hole. If files are added via url, make sure the service has access to them"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
description = "The config file of crab-hole. If files are added via url, make sure the service has access to them"; | |
description = "The config file of crab-hole. If files are added via url, make sure the service has access to them. Setting this option will override any configuration applied by the settings option."; |
6d03ac6
to
aeb3a7e
Compare
Documentation is now in a separate file. Should I add you as coauthor, because I used a lot of your suggestions? |
aeb3a7e
to
c926a15
Compare
You have to remove DynamicUser and set User & Group for them to be actually used
No need |
***Note:** This also opens a TCP port* | ||
```nix | ||
{ | ||
services.crab-hole.downstream = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
services.crab-hole.downstream = [ | |
services.crab-hole.settings.downstream = [ |
Additionally you can set an optional timeout value, but this is not needed. | ||
```nix | ||
{ | ||
services.crab-hole.downstream = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
services.crab-hole.downstream = [ | |
services.crab-hole.settings.downstream = [ |
***Note:** this config is untested* | ||
```nix | ||
{ | ||
services.crab-hole.downstream = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
services.crab-hole.downstream = [ | |
services.crab-hole.settings.downstream = [ |
Make sure the service has permissions to access the certificate and key. | ||
```nix | ||
{ | ||
services.crab-hole.downstream = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
services.crab-hole.downstream = [ | |
services.crab-hole.settings.downstream = [ |
This can look like the following example. | ||
```nix | ||
{ | ||
services.crab-hole.upstream.options = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
services.crab-hole.upstream.options = { | |
services.crab-hole.settings.upstream.options = { |
|
||
```nix | ||
{ | ||
services.crab-hole.api = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
services.crab-hole.api = { | |
services.crab-hole.settings.api = { |
For ACME for example: | ||
```nix | ||
{ | ||
users.users."${config.services.crab-hole.user}".extraGroups = [ "acme" ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should work with DynamicUser without adding user and group manually:
users.users."${config.services.crab-hole.user}".extraGroups = [ "acme" ]; | |
systemd.services.crab-hole.serviceConfig.SupplementaryGroups = "acme"; |
Alright
Apparently I forgot to copy it from my local test.
Would it be cleaner to add the nice work in spotting the missing |
If you think it will be useful I don't see why not |
e3af164
to
57b3676
Compare
Looks good! I'll test it out later on my machine. |
Should this be added as an extra commit or quashed into the service commit? |
Squashing to the service commit should be ok judging from commit history |
57b3676
to
8664676
Compare
8664676
to
a68546c
Compare
{ | ||
protocol = "udp"; | ||
listen = "localhost"; | ||
port = 53; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This configuration only binds to ::1
and doesn't work with 127.0.0.1, it took a while to figure out what's wrong.
{ | |
protocol = "udp"; | |
listen = "localhost"; | |
port = 53; | |
} | |
{ | |
protocol = "udp"; | |
listen = "127.0.0.1"; | |
port = 53; | |
} | |
{ | |
protocol = "udp"; | |
listen = "::1"; | |
port = 53; | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! I'd like to see a NixOS test, but it's optional and could be added later.
maintainers = [ | ||
lib.maintainers.NiklasVousten | ||
]; | ||
platforms = lib.platforms.all; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't build on macOS right now, could be fixed later if it's actually supported.
platforms = lib.platforms.all; | |
platforms = lib.platforms.linux; |
e6a0b26
to
607cc2e
Compare
607cc2e
to
d7066ec
Compare
d7066ec
to
dc098df
Compare
Description of changes
This change added crab-hole, a pi-hole clone written in Rust.
Additionally a service for crab-hole was added.
Things done
nix.conf
? (See Nix manual)sandbox = relaxed
sandbox = true
nix-shell -p nixpkgs-review --run "nixpkgs-review rev HEAD"
. Note: all changes have to be committed, also see nixpkgs-review usage./result/bin/
)Add a 👍 reaction to pull requests you find important.