push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

View File

@@ -0,0 +1,416 @@
{
config,
lib,
pkgs,
...
}:
let
pkg = pkgs._3proxy;
cfg = config.services._3proxy;
optionalList = list: if list == [ ] then "*" else lib.concatMapStringsSep "," toString list;
in
{
options.services._3proxy = {
enable = lib.mkEnableOption "3proxy";
confFile = lib.mkOption {
type = lib.types.path;
example = "/var/lib/3proxy/3proxy.conf";
description = ''
Ignore all other 3proxy options and load configuration from this file.
'';
};
usersFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/var/lib/3proxy/3proxy.passwd";
description = ''
Load users and passwords from this file.
Example users file with plain-text passwords:
```
test1:CL:password1
test2:CL:password2
```
Example users file with md5-crypted passwords:
```
test1:CR:$1$tFkisVd2$1GA8JXkRmTXdLDytM/i3a1
test2:CR:$1$rkpibm5J$Aq1.9VtYAn0JrqZ8M.1ME.
```
You can generate md5-crypted passwords via <https://unix4lyfe.org/crypt/>
Note that htpasswd tool generates incompatible md5-crypted passwords.
Consult [documentation](https://github.com/z3APA3A/3proxy/wiki/How-To-%28incomplete%29#USERS) for more information.
'';
};
services = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {
options = {
type = lib.mkOption {
type = lib.types.enum [
"proxy"
"socks"
"pop3p"
"ftppr"
"admin"
"dnspr"
"tcppm"
"udppm"
];
example = "proxy";
description = ''
Service type. The following values are valid:
- `"proxy"`: HTTP/HTTPS proxy (default port 3128).
- `"socks"`: SOCKS 4/4.5/5 proxy (default port 1080).
- `"pop3p"`: POP3 proxy (default port 110).
- `"ftppr"`: FTP proxy (default port 21).
- `"admin"`: Web interface (default port 80).
- `"dnspr"`: Caching DNS proxy (default port 53).
- `"tcppm"`: TCP portmapper.
- `"udppm"`: UDP portmapper.
'';
};
bindAddress = lib.mkOption {
type = lib.types.str;
default = "[::]";
example = "127.0.0.1";
description = ''
Address used for service.
'';
};
bindPort = lib.mkOption {
type = lib.types.nullOr lib.types.port;
default = null;
example = 3128;
description = ''
Override default port used for service.
'';
};
maxConnections = lib.mkOption {
type = lib.types.int;
default = 100;
example = 1000;
description = ''
Maximum number of simulationeous connections to this service.
'';
};
auth = lib.mkOption {
type = lib.types.listOf (
lib.types.enum [
"none"
"iponly"
"strong"
]
);
example = [
"iponly"
"strong"
];
description = ''
Authentication type. The following values are valid:
- `"none"`: disables both authentication and authorization. You can not use ACLs.
- `"iponly"`: specifies no authentication. ACLs authorization is used.
- `"strong"`: authentication by username/password. If user is not registered their access is denied regardless of ACLs.
Double authentication is possible, e.g.
```
{
auth = [ "iponly" "strong" ];
acl = [
{
rule = "allow";
targets = [ "192.168.0.0/16" ];
}
{
rule = "allow"
users = [ "user1" "user2" ];
}
];
}
```
In this example strong username authentication is not required to access 192.168.0.0/16.
'';
};
acl = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {
options = {
rule = lib.mkOption {
type = lib.types.enum [
"allow"
"deny"
];
example = "allow";
description = ''
ACL rule. The following values are valid:
- `"allow"`: connections allowed.
- `"deny"`: connections not allowed.
'';
};
users = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"user1"
"user2"
"user3"
];
description = ''
List of users, use empty list for any.
'';
};
sources = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"127.0.0.1"
"192.168.1.0/24"
];
description = ''
List of source IP range, use empty list for any.
'';
};
targets = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"127.0.0.1"
"192.168.1.0/24"
];
description = ''
List of target IP ranges, use empty list for any.
May also contain host names instead of addresses.
It's possible to use wildmask in the beginning and in the the end of hostname, e.g. `*badsite.com` or `*badcontent*`.
Hostname is only checked if hostname presents in request.
'';
};
targetPorts = lib.mkOption {
type = lib.types.listOf lib.types.port;
default = [ ];
example = [
80
443
];
description = ''
List of target ports, use empty list for any.
'';
};
};
}
);
default = [ ];
example = lib.literalExpression ''
[
{
rule = "allow";
users = [ "user1" ];
}
{
rule = "allow";
sources = [ "192.168.1.0/24" ];
}
{
rule = "deny";
}
]
'';
description = ''
Use this option to limit user access to resources.
'';
};
extraArguments = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "-46";
description = ''
Extra arguments for service.
Consult "Options" section in [documentation](https://github.com/z3APA3A/3proxy/wiki/3proxy.cfg) for available arguments.
'';
};
extraConfig = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
description = ''
Extra configuration for service. Use this to configure things like bandwidth limiter or ACL-based redirection.
Consult [documentation](https://github.com/z3APA3A/3proxy/wiki/3proxy.cfg) for available options.
'';
};
};
}
);
default = [ ];
example = lib.literalExpression ''
[
{
type = "proxy";
bindAddress = "192.168.1.24";
bindPort = 3128;
auth = [ "none" ];
}
{
type = "proxy";
bindAddress = "10.10.1.20";
bindPort = 3128;
auth = [ "iponly" ];
}
{
type = "socks";
bindAddress = "172.17.0.1";
bindPort = 1080;
auth = [ "strong" ];
}
]
'';
description = ''
Use this option to define 3proxy services.
'';
};
denyPrivate = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to deny access to private IP ranges including loopback.
'';
};
privateRanges = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
"0.0.0.0/8"
"127.0.0.0/8"
"10.0.0.0/8"
"100.64.0.0/10"
"172.16.0.0/12"
"192.168.0.0/16"
"::"
"::1"
"fc00::/7"
];
description = ''
What IP ranges to deny access when denyPrivate is set tu true.
'';
};
resolution = lib.mkOption {
type = lib.types.submodule {
options = {
nserver = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"127.0.0.53"
"192.168.1.3:5353/tcp"
];
description = ''
List of nameservers to use.
Up to 5 nservers may be specified. If no nserver is configured,
default system name resolution functions are used.
'';
};
nscache = lib.mkOption {
type = lib.types.int;
default = 65535;
description = "Set name cache size for IPv4.";
};
nscache6 = lib.mkOption {
type = lib.types.int;
default = 65535;
description = "Set name cache size for IPv6.";
};
nsrecord = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = lib.literalExpression ''
{
"files.local" = "192.168.1.12";
"site.local" = "192.168.1.43";
}
'';
description = "Adds static nsrecords.";
};
};
};
default = { };
description = ''
Use this option to configure name resolution and DNS caching.
'';
};
extraConfig = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
description = ''
Extra configuration, appended to the 3proxy configuration file.
Consult [documentation](https://github.com/z3APA3A/3proxy/wiki/3proxy.cfg) for available options.
'';
};
};
config = lib.mkIf cfg.enable {
services._3proxy.confFile = lib.mkDefault (
pkgs.writeText "3proxy.conf" ''
# log to stdout
log
${lib.concatMapStringsSep "\n" (x: "nserver " + x) cfg.resolution.nserver}
nscache ${toString cfg.resolution.nscache}
nscache6 ${toString cfg.resolution.nscache6}
${lib.concatMapStringsSep "\n" (x: "nsrecord " + x) (
lib.mapAttrsToList (name: value: "${name} ${value}") cfg.resolution.nsrecord
)}
${lib.optionalString (cfg.usersFile != null) ''users $"${cfg.usersFile}"''}
${lib.concatMapStringsSep "\n" (service: ''
auth ${lib.concatStringsSep " " service.auth}
${lib.optionalString (cfg.denyPrivate) "deny * * ${optionalList cfg.privateRanges}"}
${lib.concatMapStringsSep "\n" (
acl:
"${acl.rule} ${
lib.concatMapStringsSep " " optionalList [
acl.users
acl.sources
acl.targets
acl.targetPorts
]
}"
) service.acl}
maxconn ${toString service.maxConnections}
${lib.optionalString (service.extraConfig != null) service.extraConfig}
${service.type} -i${toString service.bindAddress} ${
lib.optionalString (service.bindPort != null) "-p${toString service.bindPort}"
} ${lib.optionalString (service.extraArguments != null) service.extraArguments}
flush
'') cfg.services}
${lib.optionalString (cfg.extraConfig != null) cfg.extraConfig}
''
);
systemd.services."3proxy" = {
description = "Tiny free proxy server";
documentation = [ "https://github.com/z3APA3A/3proxy/wiki" ];
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
StateDirectory = "3proxy";
ExecStart = "${pkg}/bin/3proxy ${cfg.confFile}";
Restart = "on-failure";
};
};
};
meta.maintainers = with lib.maintainers; [ misuzu ];
}

View File

@@ -0,0 +1,179 @@
{
lib,
config,
pkgs,
...
}:
let
cfg = config.services.acme-dns;
format = pkgs.formats.toml { };
inherit (lib)
literalExpression
mkEnableOption
mkOption
mkPackageOption
types
;
domain = "acme-dns.example.com";
in
{
options.services.acme-dns = {
enable = mkEnableOption "acme-dns";
package = mkPackageOption pkgs "acme-dns" { };
settings = mkOption {
description = ''
Free-form settings written directly to the `acme-dns.cfg` file.
Refer to <https://github.com/joohoi/acme-dns/blob/master/README.md#configuration> for supported values.
'';
default = { };
type = types.submodule {
freeformType = format.type;
options = {
general = {
listen = mkOption {
type = types.str;
description = "IP+port combination to bind and serve the DNS server on.";
default = "[::]:53";
example = "127.0.0.1:53";
};
protocol = mkOption {
type = types.enum [
"both"
"both4"
"both6"
"udp"
"udp4"
"udp6"
"tcp"
"tcp4"
"tcp6"
];
description = "Protocols to serve DNS responses on.";
default = "both";
};
domain = mkOption {
type = types.str;
description = "Domain name to serve the requests off of.";
example = domain;
};
nsname = mkOption {
type = types.str;
description = "Zone name server.";
example = domain;
};
nsadmin = mkOption {
type = types.str;
description = "Zone admin email address for `SOA`.";
example = "admin.example.com";
};
records = mkOption {
type = types.listOf types.str;
description = "Predefined DNS records served in addition to the `_acme-challenge` TXT records.";
example = literalExpression ''
[
# replace with your acme-dns server's public IPv4
"${domain}. A 198.51.100.1"
# replace with your acme-dns server's public IPv6
"${domain}. AAAA 2001:db8::1"
# ${domain} should resolve any *.${domain} records
"${domain}. NS ${domain}."
]
'';
};
};
database = {
engine = mkOption {
type = types.enum [
"sqlite3"
"postgres"
];
description = "Database engine to use.";
default = "sqlite3";
};
connection = mkOption {
type = types.str;
description = "Database connection string.";
example = "postgres://user:password@localhost/acmedns";
default = "/var/lib/acme-dns/acme-dns.db";
};
};
api = {
ip = mkOption {
type = types.str;
description = "IP to bind the HTTP API on.";
default = "[::]";
example = "127.0.0.1";
};
port = mkOption {
type = types.port;
description = "Listen port for the HTTP API.";
default = 8080;
# acme-dns expects this value to be a string
apply = toString;
};
disable_registration = mkOption {
type = types.bool;
description = "Whether to disable the HTTP registration endpoint.";
default = false;
example = true;
};
tls = mkOption {
type = types.enum [
"letsencrypt"
"letsencryptstaging"
"cert"
"none"
];
description = "TLS backend to use.";
default = "none";
};
};
logconfig = {
loglevel = mkOption {
type = types.enum [
"error"
"warning"
"info"
"debug"
];
description = "Level to log on.";
default = "info";
};
};
};
};
};
};
config = lib.mkIf cfg.enable {
systemd.packages = [ cfg.package ];
systemd.services.acme-dns = {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = [
""
"${lib.getExe cfg.package} -c ${format.generate "acme-dns.toml" cfg.settings}"
];
StateDirectory = "acme-dns";
WorkingDirectory = "%S/acme-dns";
DynamicUser = true;
};
};
};
}

View File

@@ -0,0 +1,209 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.adguardhome;
settingsFormat = pkgs.formats.yaml { };
args = lib.concatStringsSep " " (
[
"--no-check-update"
"--pidfile /run/AdGuardHome/AdGuardHome.pid"
"--work-dir /var/lib/AdGuardHome/"
"--config /var/lib/AdGuardHome/AdGuardHome.yaml"
]
++ cfg.extraArgs
);
settings =
if (cfg.settings != null) then
cfg.settings
// (
if cfg.settings.schema_version < 23 then
{
bind_host = cfg.host;
bind_port = cfg.port;
}
else
{
http.address = "${cfg.host}:${toString cfg.port}";
}
)
else
null;
configFile = (settingsFormat.generate "AdGuardHome.yaml" settings).overrideAttrs (_: {
checkPhase = "${cfg.package}/bin/AdGuardHome -c $out --check-config";
});
in
{
options.services.adguardhome = with lib.types; {
enable = lib.mkEnableOption "AdGuard Home network-wide ad blocker";
package = lib.mkOption {
type = package;
default = pkgs.adguardhome;
defaultText = lib.literalExpression "pkgs.adguardhome";
description = ''
The package that runs adguardhome.
'';
};
openFirewall = lib.mkOption {
default = false;
type = bool;
description = ''
Open ports in the firewall for the AdGuard Home web interface. Does not
open the port needed to access the DNS resolver.
'';
};
allowDHCP = lib.mkOption {
default = settings.dhcp.enabled or false;
defaultText = lib.literalExpression "config.services.adguardhome.settings.dhcp.enabled or false";
type = bool;
description = ''
Allows AdGuard Home to open raw sockets (`CAP_NET_RAW`), which is
required for the integrated DHCP server.
The default enables this conditionally if the declarative configuration
enables the integrated DHCP server. Manually setting this option is only
required for non-declarative setups.
'';
};
mutableSettings = lib.mkOption {
default = true;
type = bool;
description = ''
Allow changes made on the AdGuard Home web interface to persist between
service restarts.
'';
};
host = lib.mkOption {
default = "0.0.0.0";
type = str;
description = ''
Host address to bind HTTP server to.
'';
};
port = lib.mkOption {
default = 3000;
type = port;
description = ''
Port to serve HTTP pages on.
'';
};
settings = lib.mkOption {
default = null;
type = nullOr (submodule {
freeformType = settingsFormat.type;
options = {
schema_version = lib.mkOption {
default = cfg.package.schema_version;
defaultText = lib.literalExpression "cfg.package.schema_version";
type = int;
description = ''
Schema version for the configuration.
Defaults to the `schema_version` supplied by `cfg.package`.
'';
};
};
});
description = ''
AdGuard Home configuration. Refer to
<https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#configuration-file>
for details on supported values.
::: {.note}
On start and if {option}`mutableSettings` is `true`,
these options are merged into the configuration file on start, taking
precedence over configuration changes made on the web interface.
Set this to `null` (default) for a non-declarative configuration without any
Nix-supplied values.
Declarative configurations are supplied with a default `schema_version`, and `http.address`.
:::
'';
};
extraArgs = lib.mkOption {
default = [ ];
type = listOf str;
description = ''
Extra command line parameters to be passed to the adguardhome binary.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.settings != null -> !(lib.hasAttrByPath [ "bind_host" ] cfg.settings);
message = "AdGuard option `settings.bind_host' has been superseded by `services.adguardhome.host'";
}
{
assertion = cfg.settings != null -> !(lib.hasAttrByPath [ "bind_port" ] cfg.settings);
message = "AdGuard option `settings.bind_port' has been superseded by `services.adguardhome.port'";
}
{
assertion =
settings != null -> cfg.mutableSettings || lib.hasAttrByPath [ "dns" "bootstrap_dns" ] settings;
message = "AdGuard setting dns.bootstrap_dns needs to be configured for a minimal working configuration";
}
{
assertion =
settings != null
->
cfg.mutableSettings
|| lib.hasAttrByPath [ "dns" "bootstrap_dns" ] settings && lib.isList settings.dns.bootstrap_dns;
message = "AdGuard setting dns.bootstrap_dns needs to be a list";
}
];
systemd.services.adguardhome = {
description = "AdGuard Home: Network-level blocker";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
unitConfig = {
StartLimitIntervalSec = 5;
StartLimitBurst = 10;
};
preStart = lib.optionalString (settings != null) ''
if [ -e "$STATE_DIRECTORY/AdGuardHome.yaml" ] \
&& [ "${toString cfg.mutableSettings}" = "1" ]; then
# First run a schema_version update on the existing configuration
# This ensures that both the new config and the existing one have the same schema_version
# Note: --check-config has the side effect of modifying the file at rest!
${lib.getExe cfg.package} -c "$STATE_DIRECTORY/AdGuardHome.yaml" --check-config
# Writing directly to AdGuardHome.yaml results in empty file
${lib.getExe pkgs.yaml-merge} "$STATE_DIRECTORY/AdGuardHome.yaml" "${configFile}" > "$STATE_DIRECTORY/AdGuardHome.yaml.tmp"
mv "$STATE_DIRECTORY/AdGuardHome.yaml.tmp" "$STATE_DIRECTORY/AdGuardHome.yaml"
else
cp --force "${configFile}" "$STATE_DIRECTORY/AdGuardHome.yaml"
chmod 600 "$STATE_DIRECTORY/AdGuardHome.yaml"
fi
'';
serviceConfig = {
DynamicUser = true;
ExecStart = "${lib.getExe cfg.package} ${args}";
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ] ++ lib.optionals cfg.allowDHCP [ "CAP_NET_RAW" ];
Restart = "always";
RestartSec = 10;
RuntimeDirectory = "AdGuardHome";
StateDirectory = "AdGuardHome";
};
};
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ];
};
}

View File

@@ -0,0 +1,103 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.alice-lg;
settingsFormat = pkgs.formats.ini { };
in
{
options = {
services.alice-lg = {
enable = lib.mkEnableOption "Alice Looking Glass";
package = lib.mkPackageOption pkgs "alice-lg" { };
settings = lib.mkOption {
type = settingsFormat.type;
default = { };
description = ''
alice-lg configuration, for configuration options see the example on [github](https://github.com/alice-lg/alice-lg/blob/main/etc/alice-lg/alice.example.conf)
'';
example = lib.literalExpression ''
{
server = {
# configures the built-in webserver and provides global application settings
listen_http = "127.0.0.1:7340";
enable_prefix_lookup = true;
asn = 9033;
store_backend = postgres;
routes_store_refresh_parallelism = 5;
neighbors_store_refresh_parallelism = 10000;
routes_store_refresh_interval = 5;
neighbors_store_refresh_interval = 5;
};
postgres = {
url = "postgres://postgres:postgres@localhost:5432/alice";
min_connections = 2;
max_connections = 128;
};
pagination = {
routes_filtered_page_size = 250;
routes_accepted_page_size = 250;
routes_not_exported_page_size = 250;
};
}
'';
};
};
};
config = lib.mkIf cfg.enable {
environment = {
etc."alice-lg/alice.conf".source = settingsFormat.generate "alice-lg.conf" cfg.settings;
};
systemd.services = {
alice-lg = {
wants = [ "network.target" ];
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
description = "Alice Looking Glass";
serviceConfig = {
DynamicUser = true;
Type = "simple";
Restart = "on-failure";
RestartSec = 15;
ExecStart = "${cfg.package}/bin/alice-lg";
StateDirectoryMode = "0700";
UMask = "0007";
CapabilityBoundingSet = "";
NoNewPrivileges = true;
ProtectSystem = "strict";
PrivateTmp = true;
PrivateDevices = true;
PrivateUsers = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [ "AF_INET AF_INET6" ];
LockPersonality = true;
MemoryDenyWriteExecute = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
PrivateMounts = true;
SystemCallArchitectures = "native";
SystemCallFilter = "~@clock @privileged @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @reboot @setuid @swap";
BindReadOnlyPaths = [
"-/etc/resolv.conf"
"-/etc/nsswitch.conf"
"-/etc/ssl/certs"
"-/etc/static/ssl/certs"
"-/etc/hosts"
"-/etc/localtime"
];
};
};
};
};
}

View File

@@ -0,0 +1,89 @@
{
config,
lib,
options,
pkgs,
...
}:
let
cfg = config.services.amule;
opt = options.services.amule;
user = if cfg.user != null then cfg.user else "amule";
in
{
###### interface
options = {
services.amule = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to run the AMule daemon. You need to manually run "amuled --ec-config" to configure the service for the first time.
'';
};
dataDir = lib.mkOption {
type = lib.types.str;
default = "/home/${user}/";
defaultText = lib.literalExpression ''
"/home/''${config.${opt.user}}/"
'';
description = ''
The directory holding configuration, incoming and temporary files.
'';
};
user = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
The user the AMule daemon should run as.
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
users.users = lib.mkIf (cfg.user == null) [
{
name = "amule";
description = "AMule daemon";
group = "amule";
uid = config.ids.uids.amule;
}
];
users.groups = lib.mkIf (cfg.user == null) [
{
name = "amule";
gid = config.ids.gids.amule;
}
];
systemd.services.amuled = {
description = "AMule daemon";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
preStart = ''
mkdir -p ${cfg.dataDir}
chown ${user} ${cfg.dataDir}
'';
script = ''
${pkgs.su}/bin/su -s ${pkgs.runtimeShell} ${user} \
-c 'HOME="${cfg.dataDir}" ${pkgs.amule-daemon}/bin/amuled'
'';
};
};
}

View File

@@ -0,0 +1,64 @@
# Anubis {#module-services-anubis}
[Anubis](https://anubis.techaro.lol) is a scraper defense software that blocks AI scrapers. It is designed to sit
between a reverse proxy and the service to be protected.
## Quickstart {#module-services-anubis-quickstart}
This module is designed to use Unix domain sockets as the socket paths can be automatically configured for multiple
instances, but TCP sockets are also supported.
A minimal configuration with [nginx](#opt-services.nginx.enable) may look like the following:
```nix
{ config, ... }:
{
services.anubis.instances.default.settings.TARGET = "http://localhost:8000";
# required due to unix socket permissions
users.users.nginx.extraGroups = [ config.users.groups.anubis.name ];
services.nginx.virtualHosts."example.com" = {
locations = {
"/".proxyPass = "http://unix:${config.services.anubis.instances.default.settings.BIND}";
};
};
}
```
If Unix domain sockets are not needed or desired, this module supports operating with only TCP sockets.
```nix
{
services.anubis = {
instances.default = {
settings = {
TARGET = "http://localhost:8080";
BIND = ":9000";
BIND_NETWORK = "tcp";
METRICS_BIND = "127.0.0.1:9001";
METRICS_BIND_NETWORK = "tcp";
};
};
};
}
```
## Configuration {#module-services-anubis-configuration}
It is possible to configure default settings for all instances of Anubis, via {option}`services.anubis.defaultOptions`.
```nix
{
services.anubis.defaultOptions = {
botPolicy = {
dnsbl = false;
};
settings.DIFFICULTY = 3;
};
}
```
Note that at the moment, a custom bot policy is not merged with the baked-in one. That means to only override a setting
like `dnsbl`, copying the entire bot policy is required. Check
[the upstream repository](https://github.com/TecharoHQ/anubis/blob/1509b06cb921aff842e71fbb6636646be6ed5b46/cmd/anubis/botPolicies.json)
for the policy.

View File

@@ -0,0 +1,347 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) types;
jsonFormat = pkgs.formats.json { };
cfg = config.services.anubis;
enabledInstances = lib.filterAttrs (_: conf: conf.enable) cfg.instances;
instanceName = name: if name == "" then "anubis" else "anubis-${name}";
commonSubmodule =
isDefault:
let
mkDefaultOption =
path: opts:
lib.mkOption (
opts
// lib.optionalAttrs (!isDefault && opts ? default) {
default =
lib.attrByPath (lib.splitString "." path)
(throw "This is a bug in the Anubis module. Please report this as an issue.")
cfg.defaultOptions;
defaultText = lib.literalExpression "config.services.anubis.defaultOptions.${path}";
}
);
in
{ name, ... }:
{
options = {
enable = lib.mkEnableOption "this instance of Anubis" // {
default = true;
};
user = mkDefaultOption "user" {
default = "anubis";
description = ''
The user under which Anubis is run.
This module utilizes systemd's DynamicUser feature. See the corresponding section in
{manpage}`systemd.exec(5)` for more details.
'';
type = types.str;
};
group = mkDefaultOption "group" {
default = "anubis";
description = ''
The group under which Anubis is run.
This module utilizes systemd's DynamicUser feature. See the corresponding section in
{manpage}`systemd.exec(5)` for more details.
'';
type = types.str;
};
botPolicy = mkDefaultOption "botPolicy" {
default = null;
description = ''
Anubis policy configuration in Nix syntax. Set to `null` to use the baked-in policy which should be
sufficient for most use-cases.
This option has no effect if `settings.POLICY_FNAME` is set to a different value, which is useful for
importing an existing configuration.
See [the documentation](https://anubis.techaro.lol/docs/admin/policies) for details.
'';
type = types.nullOr jsonFormat.type;
};
extraFlags = mkDefaultOption "extraFlags" {
default = [ ];
description = "A list of extra flags to be passed to Anubis.";
example = [ "-metrics-bind \"\"" ];
type = types.listOf types.str;
};
settings = lib.mkOption {
default = { };
description = ''
Freeform configuration via environment variables for Anubis.
See [the documentation](https://anubis.techaro.lol/docs/admin/installation) for a complete list of
available environment variables.
'';
type = types.submodule [
{
freeformType =
with types;
attrsOf (
nullOr (oneOf [
str
int
bool
])
);
options = {
# BIND and METRICS_BIND are defined in instance specific options, since global defaults don't make sense
BIND_NETWORK = mkDefaultOption "settings.BIND_NETWORK" {
default = "unix";
description = ''
The network family that Anubis should bind to.
Accepts anything supported by Go's [`net.Listen`](https://pkg.go.dev/net#Listen).
Common values are `tcp` and `unix`.
'';
example = "tcp";
type = types.str;
};
METRICS_BIND_NETWORK = mkDefaultOption "settings.METRICS_BIND_NETWORK" {
default = "unix";
description = ''
The network family that the metrics server should bind to.
Accepts anything supported by Go's [`net.Listen`](https://pkg.go.dev/net#Listen).
Common values are `tcp` and `unix`.
'';
example = "tcp";
type = types.str;
};
DIFFICULTY = mkDefaultOption "settings.DIFFICULTY" {
default = 4;
description = ''
The difficulty required for clients to solve the challenge.
Currently, this means the amount of leading zeros in a successful response.
'';
type = types.int;
example = 5;
};
SERVE_ROBOTS_TXT = mkDefaultOption "settings.SERVE_ROBOTS_TXT" {
default = false;
description = ''
Whether to serve a default robots.txt that denies access to common AI bots by name and all other
bots by wildcard.
'';
type = types.bool;
};
OG_PASSTHROUGH = mkDefaultOption "settings.OG_PASSTHROUGH" {
default = false;
description = ''
Whether to enable Open Graph tag passthrough.
This enables social previews of resources protected by
Anubis without having to exempt each scraper individually.
'';
type = types.bool;
};
WEBMASTER_EMAIL = mkDefaultOption "settings.WEBMASTER_EMAIL" {
default = null;
description = ''
If set, shows a contact email address when rendering error pages.
This email address will be how users can get in contact with administrators.
'';
example = "alice@example.com";
type = types.nullOr types.str;
};
# generated by default
POLICY_FNAME = mkDefaultOption "settings.POLICY_FNAME" {
default = null;
description = ''
The bot policy file to use. Leave this as `null` to respect the value set in
{option}`services.anubis.instances.<name>.botPolicy`.
'';
type = types.nullOr types.path;
};
};
}
(lib.optionalAttrs (!isDefault) (instanceSpecificOptions name))
];
};
};
};
instanceSpecificOptions = name: {
options = {
# see other options above
BIND = lib.mkOption {
default = "/run/anubis/${instanceName name}.sock";
description = ''
The address that Anubis listens to. See Go's [`net.Listen`](https://pkg.go.dev/net#Listen) for syntax.
Defaults to Unix domain sockets. To use TCP sockets, set this to a TCP address and `BIND_NETWORK` to `"tcp"`.
'';
example = ":8080";
type = types.str;
};
METRICS_BIND = lib.mkOption {
default = "/run/anubis/${instanceName name}-metrics.sock";
description = ''
The address Anubis' metrics server listens to. See Go's [`net.Listen`](https://pkg.go.dev/net#Listen) for
syntax.
The metrics server is enabled by default and may be disabled. However, due to implementation details, this is
only possible by setting a command line flag. See {option}`services.anubis.defaultOptions.extraFlags` for an
example.
Defaults to Unix domain sockets. To use TCP sockets, set this to a TCP address and `METRICS_BIND_NETWORK` to
`"tcp"`.
'';
example = "127.0.0.1:8081";
type = types.str;
};
TARGET = lib.mkOption {
description = ''
The reverse proxy target that Anubis is protecting. This is a required option.
The usage of Unix domain sockets is supported by the following syntax: `unix:///path/to/socket.sock`.
'';
example = "http://127.0.0.1:8000";
type = types.str;
};
};
};
in
{
options.services.anubis = {
package = lib.mkPackageOption pkgs "anubis" { };
defaultOptions = lib.mkOption {
default = { };
description = "Default options for all instances of Anubis.";
type = types.submodule (commonSubmodule true);
};
instances = lib.mkOption {
default = { };
description = ''
An attribute set of Anubis instances.
The attribute name may be an empty string, in which case the `-<name>` suffix is not added to the service name
and socket paths.
'';
type = types.attrsOf (types.submodule (commonSubmodule false));
# Merge defaultOptions into each instance
apply = lib.mapAttrs (_: lib.recursiveUpdate cfg.defaultOptions);
};
};
config = lib.mkIf (enabledInstances != { }) {
users.users = lib.mkIf (cfg.defaultOptions.user == "anubis") {
anubis = {
isSystemUser = true;
group = cfg.defaultOptions.group;
};
};
users.groups = lib.mkIf (cfg.defaultOptions.group == "anubis") {
anubis = { };
};
systemd.services = lib.mapAttrs' (
name: instance:
lib.nameValuePair "${instanceName name}" {
description = "Anubis (${if name == "" then "default" else name} instance)";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
environment = lib.mapAttrs (lib.const (lib.generators.mkValueStringDefault { })) (
lib.filterAttrs (_: v: v != null) (
instance.settings
// {
POLICY_FNAME =
if instance.settings.POLICY_FNAME != null then
instance.settings.POLICY_FNAME
else if instance.botPolicy != null then
jsonFormat.generate "${instanceName name}-botPolicy.json" instance.botPolicy
else
null;
}
)
);
serviceConfig = {
User = instance.user;
Group = instance.group;
DynamicUser = true;
ExecStart = lib.concatStringsSep " " (
(lib.singleton (lib.getExe cfg.package)) ++ instance.extraFlags
);
RuntimeDirectory =
if
lib.any (lib.hasPrefix "/run/anubis") (
with instance.settings;
[
BIND
METRICS_BIND
]
)
then
"anubis"
else
null;
# hardening
NoNewPrivileges = true;
CapabilityBoundingSet = null;
SystemCallFilter = [
"@system-service"
"~@privileged"
];
SystemCallArchitectures = "native";
MemoryDenyWriteExecute = true;
AmbientCapabilities = "";
PrivateMounts = true;
PrivateUsers = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectHome = true;
ProtectClock = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
ProtectControlGroups = "strict";
LockPersonality = true;
RemoveIPC = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RestrictNamespaces = true;
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
};
}
) enabledInstances;
};
meta.maintainers = with lib.maintainers; [
soopyc
nullcube
];
meta.doc = ./anubis.md;
}

View File

@@ -0,0 +1,238 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.aria2;
homeDir = "/var/lib/aria2";
defaultRpcListenPort = 6800;
defaultDir = "${homeDir}/Downloads";
portRangesToString =
ranges:
lib.concatStringsSep "," (
map (
x:
if x.from == x.to then
builtins.toString x.from
else
builtins.toString x.from + "-" + builtins.toString x.to
) ranges
);
customToKeyValue = lib.generators.toKeyValue {
mkKeyValue = lib.generators.mkKeyValueDefault {
mkValueString =
v: if builtins.isList v then portRangesToString v else lib.generators.mkValueStringDefault { } v;
} "=";
};
in
{
imports = [
(lib.mkRemovedOptionModule [
"services"
"aria2"
"rpcSecret"
] "Use services.aria2.rpcSecretFile instead")
(lib.mkRemovedOptionModule [
"services"
"aria2"
"extraArguments"
] "Use services.aria2.settings instead")
(lib.mkRenamedOptionModule
[ "services" "aria2" "downloadDir" ]
[ "services" "aria2" "settings" "dir" ]
)
(lib.mkRenamedOptionModule
[ "services" "aria2" "listenPortRange" ]
[ "services" "aria2" "settings" "listen-port" ]
)
(lib.mkRenamedOptionModule
[ "services" "aria2" "rpcListenPort" ]
[ "services" "aria2" "settings" "rpc-listen-port" ]
)
];
options = {
services.aria2 = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether or not to enable the headless Aria2 daemon service.
Aria2 daemon can be controlled via the RPC interface using one of many
WebUIs (http://localhost:${toString defaultRpcListenPort}/ by default).
Targets are downloaded to `${defaultDir}` by default and are
accessible to users in the `aria2` group.
'';
};
openPorts = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Open listen and RPC ports found in `settings.listen-port` and
`settings.rpc-listen-port` options in the firewall.
'';
};
rpcSecretFile = lib.mkOption {
type = lib.types.path;
example = "/run/secrets/aria2-rpc-token.txt";
description = ''
A file containing the RPC secret authorization token.
Read <https://aria2.github.io/manual/en/html/aria2c.html#rpc-auth> to know how this option value is used.
'';
};
downloadDirPermission = lib.mkOption {
type = lib.types.str;
default = "0770";
description = ''
The permission for `settings.dir`.
The default is 0770, which denies access for users not in the `aria2`
group.
You may want to adjust `serviceUMask` as well, which further restricts
the file permission for newly created files (i.e. the downloads).
'';
};
serviceUMask = lib.mkOption {
type = lib.types.str;
default = "0022";
example = "0002";
description = ''
The file mode creation mask for Aria2 service.
The default is 0022 for compatibility reason, as this is the default
used by systemd. However, this results in file permission 0644 for new
files, and denies `aria2` group member from modifying the file.
You may want to set this value to `0002` so you can manage the file
more easily.
'';
};
settings = lib.mkOption {
description = ''
Generates the `aria2.conf` file. Refer to [the documentation][0] for
all possible settings.
[0]: <https://aria2.github.io/manual/en/html/aria2c.html#synopsis>
'';
default = { };
type = lib.types.submodule {
freeformType =
with lib.types;
attrsOf (oneOf [
bool
int
float
singleLineStr
]);
options = {
save-session = lib.mkOption {
type = lib.types.singleLineStr;
default = "${homeDir}/aria2.session";
description = "Save error/unfinished downloads to FILE on exit.";
};
dir = lib.mkOption {
type = lib.types.singleLineStr;
default = defaultDir;
description = "Directory to store downloaded files.";
};
conf-path = lib.mkOption {
type = lib.types.singleLineStr;
default = "${homeDir}/aria2.conf";
description = "Configuration file path.";
};
enable-rpc = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable JSON-RPC/XML-RPC server.";
};
listen-port = lib.mkOption {
type = with lib.types; listOf (attrsOf port);
default = [
{
from = 6881;
to = 6999;
}
];
description = "Set UDP listening port range used by DHT(IPv4, IPv6) and UDP tracker.";
};
rpc-listen-port = lib.mkOption {
type = lib.types.port;
default = defaultRpcListenPort;
description = "Specify a port number for JSON-RPC/XML-RPC server to listen to. Possible Values: 1024-65535";
};
};
};
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.settings.enable-rpc;
message = "RPC has to be enabled, the default module option takes care of that.";
}
{
assertion = !(cfg.settings ? rpc-secret);
message = "Set the RPC secret through services.aria2.rpcSecretFile so it will not end up in the world-readable nix store.";
}
];
# Need to open ports for proper functioning
networking.firewall = lib.mkIf cfg.openPorts {
allowedUDPPortRanges = config.services.aria2.settings.listen-port;
allowedTCPPorts = [ config.services.aria2.settings.rpc-listen-port ];
};
users.users.aria2 = {
group = "aria2";
uid = config.ids.uids.aria2;
description = "aria2 user";
home = homeDir;
createHome = false;
};
users.groups.aria2.gid = config.ids.gids.aria2;
systemd.tmpfiles.rules = [
"d '${homeDir}' 0770 aria2 aria2 - -"
"d '${config.services.aria2.settings.dir}' ${config.services.aria2.downloadDirPermission} aria2 aria2 - -"
];
systemd.services.aria2 = {
description = "aria2 Service";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
preStart = ''
if [[ ! -e "${cfg.settings.save-session}" ]]
then
touch "${cfg.settings.save-session}"
fi
cp -f "${pkgs.writeText "aria2.conf" (customToKeyValue cfg.settings)}" "${cfg.settings.conf-path}"
chmod +w "${cfg.settings.conf-path}"
echo "rpc-secret=$(cat "$CREDENTIALS_DIRECTORY/rpcSecretFile")" >> "${cfg.settings.conf-path}"
'';
serviceConfig = {
Restart = "on-abort";
ExecStart = "${pkgs.aria2}/bin/aria2c --conf-path=${cfg.settings.conf-path}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
User = "aria2";
Group = "aria2";
LoadCredential = "rpcSecretFile:${cfg.rpcSecretFile}";
UMask = cfg.serviceUMask;
};
};
};
meta.maintainers = [ lib.maintainers.timhae ];
}

View File

@@ -0,0 +1,265 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.asterisk;
asteriskUser = "asterisk";
asteriskGroup = "asterisk";
varlibdir = "/var/lib/asterisk";
spooldir = "/var/spool/asterisk";
logdir = "/var/log/asterisk";
# Add filecontents from files of useTheseDefaultConfFiles to confFiles, do not override
defaultConfFiles = lib.subtractLists (lib.attrNames cfg.confFiles) cfg.useTheseDefaultConfFiles;
allConfFiles = {
# Default asterisk.conf file
"asterisk.conf".text = ''
[directories]
astetcdir => /etc/asterisk
astmoddir => ${cfg.package}/lib/asterisk/modules
astvarlibdir => /var/lib/asterisk
astdbdir => /var/lib/asterisk
astkeydir => /var/lib/asterisk
astdatadir => /var/lib/asterisk
astagidir => /var/lib/asterisk/agi-bin
astspooldir => /var/spool/asterisk
astrundir => /run/asterisk
astlogdir => /var/log/asterisk
astsbindir => ${cfg.package}/sbin
${cfg.extraConfig}
'';
# Loading all modules by default is considered sensible by the authors of
# "Asterisk: The Definitive Guide". Secure sites will likely want to
# specify their own "modules.conf" in the confFiles option.
"modules.conf".text = ''
[modules]
autoload=yes
'';
# Use syslog for logging so logs can be viewed with journalctl
"logger.conf".text = ''
[general]
[logfiles]
syslog.local0 => notice,warning,error
'';
}
// lib.mapAttrs (name: text: { inherit text; }) cfg.confFiles
// lib.listToAttrs (
map (x: lib.nameValuePair x { source = cfg.package + "/etc/asterisk/" + x; }) defaultConfFiles
);
in
{
options = {
services.asterisk = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable the Asterisk PBX server.
'';
};
extraConfig = lib.mkOption {
default = "";
type = lib.types.lines;
example = ''
[options]
verbose=3
debug=3
'';
description = ''
Extra configuration options appended to the default
`asterisk.conf` file.
'';
};
confFiles = lib.mkOption {
default = { };
type = lib.types.attrsOf lib.types.str;
example = lib.literalExpression ''
{
"extensions.conf" = '''
[tests]
; Dial 100 for "hello, world"
exten => 100,1,Answer()
same => n,Wait(1)
same => n,Playback(hello-world)
same => n,Hangup()
[softphones]
include => tests
[unauthorized]
''';
"sip.conf" = '''
[general]
allowguest=no ; Require authentication
context=unauthorized ; Send unauthorized users to /dev/null
srvlookup=no ; Don't do DNS lookup
udpbindaddr=0.0.0.0 ; Listen on all interfaces
nat=force_rport,comedia ; Assume device is behind NAT
[softphone](!)
type=friend ; Match on username first, IP second
context=softphones ; Send to softphones context in
; extensions.conf file
host=dynamic ; Device will register with asterisk
disallow=all ; Manually specify codecs to allow
allow=g722
allow=ulaw
allow=alaw
[myphone](softphone)
secret=GhoshevFew ; Change this password!
''';
"logger.conf" = '''
[general]
[logfiles]
; Add debug output to log
syslog.local0 => notice,warning,error,debug
''';
}
'';
description = ''
Sets the content of config files (typically ending with
`.conf`) in the Asterisk configuration directory.
Note that if you want to change `asterisk.conf`, it
is preferable to use the {option}`services.asterisk.extraConfig`
option over this option. If `"asterisk.conf"` is
specified with the {option}`confFiles` option (not recommended),
you must be prepared to set your own `astetcdir`
path.
See
<https://www.asterisk.org/community/documentation/>
for more examples of what is possible here.
'';
};
useTheseDefaultConfFiles = lib.mkOption {
default = [
"ari.conf"
"acl.conf"
"agents.conf"
"amd.conf"
"calendar.conf"
"cdr.conf"
"cdr_syslog.conf"
"cdr_custom.conf"
"cel.conf"
"cel_custom.conf"
"cli_aliases.conf"
"confbridge.conf"
"dundi.conf"
"features.conf"
"hep.conf"
"iax.conf"
"pjsip.conf"
"pjsip_wizard.conf"
"phone.conf"
"phoneprov.conf"
"queues.conf"
"res_config_sqlite3.conf"
"res_parking.conf"
"statsd.conf"
"udptl.conf"
"unistim.conf"
];
type = lib.types.listOf lib.types.str;
example = [
"sip.conf"
"dundi.conf"
];
description = ''
Sets these config files to the default content. The default value for
this option contains all necesscary files to avoid errors at startup.
This does not override settings via {option}`services.asterisk.confFiles`.
'';
};
extraArguments = lib.mkOption {
default = [ ];
type = lib.types.listOf lib.types.str;
example = [
"-vvvddd"
"-e"
"1024"
];
description = ''
Additional command line arguments to pass to Asterisk.
'';
};
package = lib.mkPackageOption pkgs "asterisk" { };
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
environment.etc = lib.mapAttrs' (
name: value: lib.nameValuePair "asterisk/${name}" value
) allConfFiles;
users.users.asterisk = {
name = asteriskUser;
group = asteriskGroup;
uid = config.ids.uids.asterisk;
description = "Asterisk daemon user";
home = varlibdir;
};
users.groups.asterisk = {
name = asteriskGroup;
gid = config.ids.gids.asterisk;
};
systemd.services.asterisk = {
description = ''
Asterisk PBX server
'';
wantedBy = [ "multi-user.target" ];
# Do not restart, to avoid disruption of running calls. Restart unit by yourself!
restartIfChanged = false;
preStart = ''
# Copy skeleton directory tree to /var
for d in '${varlibdir}' '${spooldir}' '${logdir}'; do
# TODO: Make exceptions for /var directories that likely should be updated
if [ ! -e "$d" ]; then
mkdir -p "$d"
cp --recursive ${cfg.package}/"$d"/* "$d"/
chown --recursive ${asteriskUser}:${asteriskGroup} "$d"
find "$d" -type d | xargs chmod 0755
fi
done
'';
serviceConfig = {
ExecStart =
let
# FIXME: This doesn't account for arguments with spaces
argString = lib.concatStringsSep " " cfg.extraArguments;
in
"${cfg.package}/bin/asterisk -U ${asteriskUser} -C /etc/asterisk/asterisk.conf ${argString} -F";
ExecReload = ''
${cfg.package}/bin/asterisk -C /etc/asterisk/asterisk.conf -x "core reload"
'';
Type = "forking";
PIDFile = "/run/asterisk/asterisk.pid";
};
};
};
}

View File

@@ -0,0 +1,18 @@
# atalkd {#module-services-atalkd}
atalkd (AppleTalk daemon) is a component inside of the suite of software provided by Netatalk. It allows for the creation of AppleTalk networks, typically speaking over a Linux ethernet network interface, that can still be seen by classic macintosh computers. Using the NixOS module, you can specify a set of network interfaces that you wish to speak AppleTalk on, and the corresponding ATALKD.CONF(5) values to go along with it.
## Basic Usage {#module-services-atalkd-basic-usage}
A minimal configuration looks like this:
```nix
{
services.atalkd = {
enable = true;
interfaces.wlan0.config = ''-router -phase 2 -net 1 -addr 1.48 -zone "Default"'';
};
}
```
It is also valid to use atalkd without setting `services.netatalk.interfaces` to any value, only providing `services.atalkd.enable = true`. In this case it will inherit the behavior of the upstream application when an empty config file is found, which is to listen on and use all interfaces.

View File

@@ -0,0 +1,98 @@
{
config,
pkgs,
lib,
utils,
...
}:
let
cfg = config.services.atalkd;
# Generate atalkd.conf only if configFile isn't manually specified
atalkdConfFile = pkgs.writeText "atalkd.conf" (
lib.concatStringsSep "\n" (
lib.mapAttrsToList (
iface: ifaceCfg: iface + (if ifaceCfg.config != null then " ${ifaceCfg.config}" else "")
) cfg.interfaces
)
);
in
{
options.services.atalkd = {
enable = lib.mkEnableOption "the AppleTalk daemon";
configFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = atalkdConfFile;
defaultText = "/nix/store/xxx-atalkd.conf";
description = ''
Optional path to a custom `atalkd.conf` file. When set, this overrides the generated
configuration from `services.atalkd.interfaces`.
'';
};
interfaces = lib.mkOption {
description = "Per-interface configuration for atalkd.";
type = lib.types.attrsOf (
lib.types.submodule {
options.config = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Optional configuration string for this interface.";
};
}
);
default = { };
};
};
config =
let
interfaces = map (iface: "sys-subsystem-net-devices-${utils.escapeSystemdPath iface}.device") (
builtins.attrNames cfg.interfaces
);
in
lib.mkIf cfg.enable {
system.requiredKernelConfig = [
(config.lib.kernelConfig.isEnabled "APPLETALK")
];
systemd.services.netatalk.partOf = [ "atalkd.service" ];
systemd.services.netatalk.after = interfaces;
systemd.services.netatalk.requires = interfaces;
systemd.services.atalkd =
let
interfaces = map (iface: "sys-subsystem-net-devices-${utils.escapeSystemdPath iface}.device") (
builtins.attrNames cfg.interfaces
);
in
{
description = "atalkd AppleTalk daemon";
unitConfig.Documentation = "man:atalkd.conf(5) man:atalkd(8)";
after = interfaces;
wants = [ "network.target" ];
before = [ "netatalk.service" ];
requires = interfaces;
wantedBy = [ "multi-user.target" ];
path = [ pkgs.netatalk ];
serviceConfig = {
Type = "forking";
GuessMainPID = "no";
DynamicUser = true;
AmbientCapabilities = [ "CAP_NET_ADMIN" ];
RuntimeDirectory = "atalkd";
PIDFile = "/run/atalkd/atalkd";
BindPaths = [ "/run/atalkd:/run/lock" ];
ExecStart = "${pkgs.netatalk}/bin/atalkd -f ${cfg.configFile}";
Restart = "always";
};
};
};
meta.maintainers = with lib.maintainers; [ matthewcroughan ];
meta.doc = ./atalkd.md;
}

View File

@@ -0,0 +1,66 @@
# NixOS module for atftpd TFTP server
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.atftpd;
in
{
options = {
services.atftpd = {
enable = lib.mkOption {
default = false;
type = lib.types.bool;
description = ''
Whether to enable the atftpd TFTP server. By default, the server
binds to address 0.0.0.0.
'';
};
extraOptions = lib.mkOption {
default = [ ];
type = lib.types.listOf lib.types.str;
example = lib.literalExpression ''
[ "--bind-address 192.168.9.1"
"--verbose=7"
]
'';
description = ''
Extra command line arguments to pass to atftp.
'';
};
root = lib.mkOption {
default = "/srv/tftp";
type = lib.types.path;
description = ''
Document root directory for the atftpd.
'';
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.atftpd = {
description = "TFTP Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
# runs as nobody
serviceConfig.ExecStart = "${pkgs.atftp}/sbin/atftpd --daemon --no-fork ${lib.concatStringsSep " " cfg.extraOptions} ${cfg.root}";
};
};
}

View File

@@ -0,0 +1,234 @@
{
lib,
pkgs,
config,
...
}:
let
inherit (lib) types;
cfg = config.services.atticd;
format = pkgs.formats.toml { };
checkedConfigFile =
pkgs.runCommand "checked-attic-server.toml"
{
configFile = format.generate "server.toml" cfg.settings;
}
''
export ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64="$(${lib.getExe pkgs.openssl} genrsa -traditional 4096 | ${pkgs.coreutils}/bin/base64 -w0)"
export ATTIC_SERVER_DATABASE_URL="sqlite://:memory:"
${lib.getExe cfg.package} --mode check-config -f $configFile
cat <$configFile >$out
'';
atticadmShim = pkgs.writeShellScript "atticadm" ''
if [ -n "$ATTICADM_PWD" ]; then
cd "$ATTICADM_PWD"
if [ "$?" != "0" ]; then
>&2 echo "Warning: Failed to change directory to $ATTICADM_PWD"
fi
fi
exec ${cfg.package}/bin/atticadm -f ${checkedConfigFile} "$@"
'';
atticadmWrapper = pkgs.writeShellScriptBin "atticd-atticadm" ''
exec systemd-run \
--quiet \
--pipe \
--pty \
--same-dir \
--wait \
--collect \
--service-type=exec \
--property=EnvironmentFile=${cfg.environmentFile} \
--property=DynamicUser=yes \
--property=User=${cfg.user} \
--property=Environment=ATTICADM_PWD=$(pwd) \
--working-directory / \
-- \
${atticadmShim} "$@"
'';
hasLocalPostgresDB =
let
url = cfg.settings.database.url or "";
localStrings = [
"localhost"
"127.0.0.1"
"/run/postgresql"
];
hasLocalStrings = lib.any (lib.flip lib.hasInfix url) localStrings;
in
config.services.postgresql.enable && lib.hasPrefix "postgresql://" url && hasLocalStrings;
in
{
options = {
services.atticd = {
enable = lib.mkEnableOption "the atticd, the Nix Binary Cache server";
package = lib.mkPackageOption pkgs "attic-server" { };
environmentFile = lib.mkOption {
description = ''
Path to an EnvironmentFile containing required environment
variables:
- ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64: The base64-encoded RSA PEM PKCS1 of the
RS256 JWT secret. Generate it with `openssl genrsa -traditional 4096 | base64 -w0`.
'';
type = types.nullOr types.path;
default = null;
};
user = lib.mkOption {
description = ''
The user under which attic runs.
'';
type = types.str;
default = "atticd";
};
group = lib.mkOption {
description = ''
The group under which attic runs.
'';
type = types.str;
default = "atticd";
};
settings = lib.mkOption {
description = ''
Structured configurations of atticd.
See <https://github.com/zhaofengli/attic/blob/main/server/src/config-template.toml>
'';
type = format.type;
default = { };
};
mode = lib.mkOption {
description = ''
Mode in which to run the server.
'monolithic' runs all components, and is suitable for single-node deployments.
'api-server' runs only the API server, and is suitable for clustering.
'garbage-collector' only runs the garbage collector periodically.
A simple NixOS-based Attic deployment will typically have one 'monolithic' and any number of 'api-server' nodes.
There are several other supported modes that perform one-off operations, but these are the only ones that make sense to run via the NixOS module.
'';
type = lib.types.enum [
"monolithic"
"api-server"
"garbage-collector"
];
default = "monolithic";
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.environmentFile != null;
message = ''
<option>services.atticd.environmentFile</option> is not set.
Run `openssl genrsa -traditional 4096 | base64 -w0` and create a file with the following contents:
ATTIC_SERVER_TOKEN_RS256_SECRET="output from command"
Then, set `services.atticd.environmentFile` to the quoted absolute path of the file.
'';
}
];
services.atticd.settings = {
chunking = lib.mkDefault {
nar-size-threshold = 65536;
min-size = 16384; # 16 KiB
avg-size = 65536; # 64 KiB
max-size = 262144; # 256 KiB
};
database.url = lib.mkDefault "sqlite:///var/lib/atticd/server.db?mode=rwc";
# "storage" is internally tagged
# if the user sets something the whole thing must be replaced
storage = lib.mkDefault {
type = "local";
path = "/var/lib/atticd/storage";
};
};
systemd.services.atticd = {
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ] ++ lib.optionals hasLocalPostgresDB [ "postgresql.target" ];
requires = lib.optionals hasLocalPostgresDB [ "postgresql.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
ExecStart = "${lib.getExe cfg.package} -f ${checkedConfigFile} --mode ${cfg.mode}";
EnvironmentFile = cfg.environmentFile;
StateDirectory = "atticd"; # for usage with local storage and sqlite
DynamicUser = true;
User = cfg.user;
Group = cfg.group;
Restart = "on-failure";
RestartSec = 10;
CapabilityBoundingSet = [ "" ];
DeviceAllow = "";
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
ReadWritePaths =
let
path = cfg.settings.storage.path;
isDefaultStateDirectory = path == "/var/lib/atticd" || lib.hasPrefix "/var/lib/atticd/" path;
in
lib.optionals (cfg.settings.storage.type or "" == "local" && !isDefaultStateDirectory) [ path ];
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@resources"
"~@privileged"
];
UMask = "0077";
};
};
environment.systemPackages = [
atticadmWrapper
];
};
}

View File

@@ -0,0 +1,120 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.autossh;
in
{
###### interface
options = {
services.autossh = {
sessions = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {
options = {
name = lib.mkOption {
type = lib.types.str;
example = "socks-peer";
description = "Name of the local AutoSSH session";
};
user = lib.mkOption {
type = lib.types.str;
example = "bill";
description = "Name of the user the AutoSSH session should run as";
};
monitoringPort = lib.mkOption {
type = lib.types.port;
default = 0;
example = 20000;
description = ''
Port to be used by AutoSSH for peer monitoring. Note, that
AutoSSH also uses mport+1. Value of 0 disables the keep-alive
style monitoring
'';
};
extraArguments = lib.mkOption {
type = lib.types.separatedString " ";
example = "-N -D4343 bill@socks.example.net";
description = ''
Arguments to be passed to AutoSSH and retransmitted to SSH
process. Some meaningful options include -N (don't run remote
command), -D (open SOCKS proxy on local port), -R (forward
remote port), -L (forward local port), -v (Enable debug). Check
ssh manual for the complete list.
'';
};
};
}
);
default = [ ];
description = ''
List of AutoSSH sessions to start as systemd services. Each service is
named 'autossh-{session.name}'.
'';
example = [
{
name = "socks-peer";
user = "bill";
monitoringPort = 20000;
extraArguments = "-N -D4343 billremote@socks.host.net";
}
];
};
};
};
###### implementation
config = lib.mkIf (cfg.sessions != [ ]) {
systemd.services =
lib.foldr (
s: acc:
acc
// {
"autossh-${s.name}" =
let
mport = if s ? monitoringPort then s.monitoringPort else 0;
in
{
description = "AutoSSH session (" + s.name + ")";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
# To be able to start the service with no network connection
environment.AUTOSSH_GATETIME = "0";
# How often AutoSSH checks the network, in seconds
environment.AUTOSSH_POLL = "30";
serviceConfig = {
User = "${s.user}";
# AutoSSH may exit with 0 code if the SSH session was
# gracefully terminated by either local or remote side.
Restart = "on-success";
ExecStart = "${pkgs.autossh}/bin/autossh -M ${toString mport} ${s.extraArguments}";
};
};
}
) { } cfg.sessions;
environment.systemPackages = [ pkgs.autossh ];
};
}

View File

@@ -0,0 +1,410 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.avahi;
yesNo = yes: if yes then "yes" else "no";
avahiDaemonConf =
with cfg;
pkgs.writeText "avahi-daemon.conf" ''
[server]
${
# Users can set `networking.hostName' to the empty string, when getting
# a host name from DHCP. In that case, let Avahi take whatever the
# current host name is; setting `host-name' to the empty string in
# `avahi-daemon.conf' would be invalid.
lib.optionalString (hostName != "") "host-name=${hostName}"
}
browse-domains=${lib.concatStringsSep ", " browseDomains}
use-ipv4=${yesNo ipv4}
use-ipv6=${yesNo ipv6}
${lib.optionalString (
allowInterfaces != null
) "allow-interfaces=${lib.concatStringsSep "," allowInterfaces}"}
${lib.optionalString (
denyInterfaces != null
) "deny-interfaces=${lib.concatStringsSep "," denyInterfaces}"}
${lib.optionalString (domainName != null) "domain-name=${domainName}"}
allow-point-to-point=${yesNo allowPointToPoint}
${lib.optionalString (cacheEntriesMax != null) "cache-entries-max=${toString cacheEntriesMax}"}
[wide-area]
enable-wide-area=${yesNo wideArea}
[publish]
disable-publishing=${yesNo (!publish.enable)}
disable-user-service-publishing=${yesNo (!publish.userServices)}
publish-addresses=${yesNo (publish.userServices || publish.addresses)}
publish-hinfo=${yesNo publish.hinfo}
publish-workstation=${yesNo publish.workstation}
publish-domain=${yesNo publish.domain}
[reflector]
enable-reflector=${yesNo reflector}
${extraConfig}
'';
in
{
imports = [
(lib.mkRenamedOptionModule
[ "services" "avahi" "interfaces" ]
[ "services" "avahi" "allowInterfaces" ]
)
(lib.mkRenamedOptionModule [ "services" "avahi" "nssmdns" ] [ "services" "avahi" "nssmdns4" ])
];
options.services.avahi = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to run the Avahi daemon, which allows Avahi clients
to use Avahi's service discovery facilities and also allows
the local machine to advertise its presence and services
(through the mDNS responder implemented by `avahi-daemon`).
'';
};
package = lib.mkPackageOption pkgs "avahi" { };
hostName = lib.mkOption {
type = lib.types.str;
default = config.networking.hostName;
defaultText = lib.literalExpression "config.networking.hostName";
description = ''
Host name advertised on the LAN. If not set, avahi will use the value
of {option}`config.networking.hostName`.
'';
};
domainName = lib.mkOption {
type = lib.types.str;
default = "local";
description = ''
Domain name for all advertisements.
'';
};
browseDomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"0pointer.de"
"zeroconf.org"
];
description = ''
List of non-local DNS domains to be browsed.
'';
};
ipv4 = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to use IPv4.";
};
ipv6 = lib.mkOption {
type = lib.types.bool;
default = config.networking.enableIPv6;
defaultText = lib.literalExpression "config.networking.enableIPv6";
description = "Whether to use IPv6.";
};
allowInterfaces = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
description = ''
List of network interfaces that should be used by the {command}`avahi-daemon`.
Other interfaces will be ignored. If `null`, all local interfaces
except loopback and point-to-point will be used.
'';
};
denyInterfaces = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
description = ''
List of network interfaces that should be ignored by the
{command}`avahi-daemon`. Other unspecified interfaces will be used,
unless {option}`allowInterfaces` is set. This option takes precedence
over {option}`allowInterfaces`.
'';
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to open the firewall for UDP port 5353.
Disabling this setting also disables discovering of network devices.
'';
};
allowPointToPoint = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to use POINTTOPOINT interfaces. Might make mDNS unreliable due to usually large
latencies with such links and opens a potential security hole by allowing mDNS access from Internet
connections.
'';
};
wideArea = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to enable wide-area service discovery.";
};
reflector = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Reflect incoming mDNS requests to all allowed network interfaces.";
};
extraServiceFiles = lib.mkOption {
type = with lib.types; attrsOf (either str path);
default = { };
example = lib.literalExpression ''
{
ssh = "''${pkgs.avahi}/etc/avahi/services/ssh.service";
smb = '''
<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
<name replace-wildcards="yes">%h</name>
<service>
<type>_smb._tcp</type>
<port>445</port>
</service>
</service-group>
''';
}
'';
description = ''
Specify custom service definitions which are placed in the avahi service directory.
See the {manpage}`avahi.service(5)` manpage for detailed information.
'';
};
publish = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to allow publishing in general.";
};
userServices = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to publish user services. Will set `addresses=true`.";
};
addresses = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to register mDNS address records for all local IP addresses.";
};
hinfo = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to register a mDNS HINFO record which contains information about the
local operating system and CPU.
'';
};
workstation = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to register a service of type "_workstation._tcp" on the local LAN.
'';
};
domain = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to announce the locally used domain name for browsing by other hosts.";
};
};
nssmdns4 = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable the mDNS NSS (Name Service Switch) plug-in for IPv4.
Enabling it allows applications to resolve names in the `.local`
domain by transparently querying the Avahi daemon.
'';
};
nssmdns6 = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable the mDNS NSS (Name Service Switch) plug-in for IPv6.
Enabling it allows applications to resolve names in the `.local`
domain by transparently querying the Avahi daemon.
::: {.note}
Due to the fact that most mDNS responders only register local IPv4 addresses,
most user want to leave this option disabled to avoid long timeouts when applications first resolve the none existing IPv6 address.
:::
'';
};
cacheEntriesMax = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
description = ''
Number of resource records to be cached per interface. Use 0 to
disable caching. Avahi daemon defaults to 4096 if not set.
'';
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Extra config to append to avahi-daemon.conf.
'';
};
};
config = lib.mkIf cfg.enable {
users.users.avahi = {
description = "avahi-daemon privilege separation user";
home = "/var/empty";
group = "avahi";
isSystemUser = true;
};
users.groups.avahi = { };
system.nssModules = lib.optional (cfg.nssmdns4 || cfg.nssmdns6) pkgs.nssmdns;
system.nssDatabases.hosts =
let
mdns =
if (cfg.nssmdns4 && cfg.nssmdns6) then
"mdns"
else if (!cfg.nssmdns4 && cfg.nssmdns6) then
"mdns6"
else if (cfg.nssmdns4 && !cfg.nssmdns6) then
"mdns4"
else
"";
in
lib.optionals (cfg.nssmdns4 || cfg.nssmdns6) (
lib.mkMerge [
(lib.mkBefore [ "${mdns}_minimal [NOTFOUND=return]" ]) # before resolve
(lib.mkAfter [ "${mdns}" ]) # after dns
]
);
environment.systemPackages = [ cfg.package ];
environment.etc = (
lib.mapAttrs' (
n: v:
lib.nameValuePair "avahi/services/${n}.service" {
${if lib.types.path.check v then "source" else "text"} = v;
}
) cfg.extraServiceFiles
);
systemd.sockets.avahi-daemon = {
description = "Avahi mDNS/DNS-SD Stack Activation Socket";
listenStreams = [ "/run/avahi-daemon/socket" ];
wantedBy = [ "sockets.target" ];
after = [
# Ensure that `/run/avahi-daemon` owned by `avahi` is created by `systemd.tmpfiles.rules` before the `avahi-daemon.socket`,
# otherwise `avahi-daemon.socket` will automatically create it owned by `root`, which will cause `avahi-daemon.service` to fail.
"systemd-tmpfiles-setup.service"
];
};
systemd.tmpfiles.rules = [ "d /run/avahi-daemon - avahi avahi -" ];
systemd.services.avahi-daemon = {
description = "Avahi mDNS/DNS-SD Stack";
wantedBy = [ "multi-user.target" ];
requires = [ "avahi-daemon.socket" ];
documentation = [
"man:avahi-daemon(8)"
"man:avahi-daemon.conf(5)"
"man:avahi.hosts(5)"
"man:avahi.service(5)"
];
# Make NSS modules visible so that `avahi_nss_support ()' can
# return a sensible value.
environment.LD_LIBRARY_PATH = config.system.nssModules.path;
path = [
pkgs.coreutils
cfg.package
];
serviceConfig = {
NotifyAccess = "main";
BusName = "org.freedesktop.Avahi";
Type = "dbus";
ExecStart = "${cfg.package}/sbin/avahi-daemon --syslog -f ${avahiDaemonConf}";
ConfigurationDirectory = "avahi/services";
# Hardening
CapabilityBoundingSet = [
# https://github.com/avahi/avahi/blob/v0.9-rc1/avahi-daemon/caps.c#L38
"CAP_SYS_CHROOT"
"CAP_SETUID"
"CAP_SETGID"
];
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = false;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
"@chown setgroups setresuid"
];
UMask = "0077";
};
};
services.dbus.enable = true;
services.dbus.packages = [ cfg.package ];
networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall [ 5353 ];
};
}

View File

@@ -0,0 +1,57 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
types
;
inherit (lib.modules)
mkIf
;
inherit (lib.options)
mkEnableOption
mkOption
mkPackageOption
;
cfg = config.services.ax25.axlisten;
in
{
options = {
services.ax25.axlisten = {
enable = mkEnableOption "AX.25 axlisten daemon";
package = mkPackageOption pkgs "ax25-apps" { };
config = mkOption {
type = types.str;
default = "-art";
description = ''
Options that will be passed to the axlisten daemon.
'';
};
};
};
config = mkIf cfg.enable {
systemd.services.axlisten = {
description = "AX.25 traffic monitor";
wantedBy = [ "multi-user.target" ];
after = [ "ax25-axports.target" ];
requires = [ "ax25-axports.target" ];
serviceConfig = {
Type = "exec";
ExecStart = "${cfg.package}/bin/axlisten ${cfg.config}";
};
};
};
}

View File

@@ -0,0 +1,153 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
types
;
inherit (lib.strings)
concatStringsSep
optionalString
;
inherit (lib.attrsets)
filterAttrs
mapAttrsToList
mapAttrs'
;
inherit (lib.modules)
mkIf
;
inherit (lib.options)
mkEnableOption
mkOption
mkPackageOption
;
cfg = config.services.ax25.axports;
enabledAxports = filterAttrs (ax25Name: cfg: cfg.enable) cfg;
axportsOpts = {
options = {
enable = mkEnableOption "Enables the axport interface";
package = mkPackageOption pkgs "ax25-tools" { };
tty = mkOption {
type = types.str;
example = "/dev/ttyACM0";
description = ''
Location of hardware kiss tnc for this interface.
'';
};
callsign = mkOption {
type = types.str;
example = "WB6WLV-7";
description = ''
The callsign of the physical interface to bind to.
'';
};
description = mkOption {
type = types.str;
# This cannot be empty since some ax25 tools cant parse /etc/ax25/axports without it
default = "NixOS managed tnc";
description = ''
Free format description of this interface.
'';
};
baud = mkOption {
type = types.int;
example = 57600;
description = ''
The serial port speed of this interface.
'';
};
paclen = mkOption {
type = types.int;
default = 255;
description = ''
Default maximum packet size for this interface.
'';
};
window = mkOption {
type = types.int;
default = 7;
description = ''
Default window size for this interface.
'';
};
kissParams = mkOption {
type = types.nullOr types.str;
default = null;
example = "-t 300 -l 10 -s 12 -r 80 -f n";
description = ''
Kissattach parameters for this interface.
'';
};
};
};
in
{
options = {
services.ax25.axports = mkOption {
type = types.attrsOf (types.submodule axportsOpts);
default = { };
description = "Specification of one or more AX.25 ports.";
};
};
config = mkIf (enabledAxports != { }) {
system.requiredKernelConfig = [
(config.lib.kernelConfig.isEnabled "ax25")
];
environment.etc."ax25/axports" = {
text = concatStringsSep "\n" (
mapAttrsToList (
portName: portCfg:
"${portName} ${portCfg.callsign} ${toString portCfg.baud} ${toString portCfg.paclen} ${toString portCfg.window} ${portCfg.description}"
) enabledAxports
);
mode = "0644";
};
systemd.targets.ax25-axports = {
description = "AX.25 axports group target";
};
systemd.services = mapAttrs' (portName: portCfg: {
name = "ax25-kissattach-${portName}";
value = {
description = "AX.25 KISS attached interface for ${portName}";
wantedBy = [ "multi-user.target" ];
before = [ "ax25-axports.target" ];
partOf = [ "ax25-axports.target" ];
serviceConfig = {
Type = "exec";
ExecStart = "${portCfg.package}/bin/kissattach ${portCfg.tty} ${portName}";
};
postStart = optionalString (portCfg.kissParams != null) ''
${portCfg.package}/bin/kissparms -p ${portName} ${portCfg.kissParams}
'';
};
}) enabledAxports;
};
}

View File

@@ -0,0 +1,163 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.babeld;
conditionalBoolToString =
value: if (lib.isBool value) then (lib.boolToString value) else (toString value);
paramsString =
params:
lib.concatMapStringsSep " " (name: "${name} ${conditionalBoolToString (lib.getAttr name params)}") (
lib.attrNames params
);
interfaceConfig =
name:
let
interface = lib.getAttr name cfg.interfaces;
in
"interface ${name} ${paramsString interface}\n";
configFile =
with cfg;
pkgs.writeText "babeld.conf" (
''
skip-kernel-setup true
''
+ (lib.optionalString (cfg.interfaceDefaults != null) ''
default ${paramsString cfg.interfaceDefaults}
'')
+ (lib.concatMapStrings interfaceConfig (lib.attrNames cfg.interfaces))
+ extraConfig
);
in
{
meta.maintainers = with lib.maintainers; [ hexa ];
###### interface
options = {
services.babeld = {
enable = lib.mkEnableOption "the babeld network routing daemon";
interfaceDefaults = lib.mkOption {
default = null;
description = ''
A set describing default parameters for babeld interfaces.
See {manpage}`babeld(8)` for options.
'';
type = lib.types.nullOr (lib.types.attrsOf lib.types.unspecified);
example = {
type = "tunnel";
split-horizon = true;
};
};
interfaces = lib.mkOption {
default = { };
description = ''
A set describing babeld interfaces.
See {manpage}`babeld(8)` for options.
'';
type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified);
example = {
enp0s2 = {
type = "wired";
hello-interval = 5;
split-horizon = "auto";
};
};
};
extraConfig = lib.mkOption {
default = "";
type = lib.types.lines;
description = ''
Options that will be copied to babeld.conf.
See {manpage}`babeld(8)` for details.
'';
};
};
};
###### implementation
config = lib.mkIf config.services.babeld.enable {
boot.kernel.sysctl = {
"net.ipv6.conf.all.forwarding" = 1;
"net.ipv6.conf.all.accept_redirects" = 0;
"net.ipv4.conf.all.forwarding" = 1;
"net.ipv4.conf.all.rp_filter" = 0;
}
// lib.mapAttrs' (
ifname: _: lib.nameValuePair "net.ipv4.conf.${ifname}.rp_filter" (lib.mkDefault 0)
) config.services.babeld.interfaces;
systemd.services.babeld = {
description = "Babel routing daemon";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${pkgs.babeld}/bin/babeld -c ${configFile} -I /run/babeld/babeld.pid -S /var/lib/babeld/state";
AmbientCapabilities = [ "CAP_NET_ADMIN" ];
CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
DevicePolicy = "closed";
DynamicUser = true;
IPAddressAllow = [
"fe80::/64"
"ff00::/8"
"::1/128"
"127.0.0.0/8"
];
IPAddressDeny = "any";
LockPersonality = true;
NoNewPrivileges = true;
MemoryDenyWriteExecute = true;
ProtectSystem = "strict";
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [
"AF_NETLINK"
"AF_INET6"
"AF_INET"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RemoveIPC = true;
ProtectHome = true;
ProtectHostname = true;
ProtectProc = "invisible";
PrivateMounts = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateUsers = false; # kernel_route(ADD): Operation not permitted
ProcSubset = "pid";
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged @resources"
];
UMask = "0177";
RuntimeDirectory = "babeld";
StateDirectory = "babeld";
};
};
};
}

View File

@@ -0,0 +1,142 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.bee;
format = pkgs.formats.yaml { };
configFile = format.generate "bee.yaml" cfg.settings;
in
{
meta = {
# doc = ./bee.xml;
maintainers = [ ];
};
### interface
options = {
services.bee = {
enable = lib.mkEnableOption "Ethereum Swarm Bee";
package = lib.mkPackageOption pkgs "bee" {
example = "bee-unstable";
};
settings = lib.mkOption {
type = format.type;
description = ''
Ethereum Swarm Bee configuration. Refer to
<https://gateway.ethswarm.org/bzz/docs.swarm.eth/docs/installation/configuration/>
for details on supported values.
'';
};
daemonNiceLevel = lib.mkOption {
type = lib.types.int;
default = 0;
description = ''
Daemon process priority for bee.
0 is the default Unix process priority, 19 is the lowest.
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "bee";
description = ''
User the bee binary should execute under.
'';
};
group = lib.mkOption {
type = lib.types.str;
default = "bee";
description = ''
Group the bee binary should execute under.
'';
};
};
};
### implementation
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = (lib.hasAttr "password" cfg.settings) != true;
message = ''
`services.bee.settings.password` is insecure. Use `services.bee.settings.password-file` or `systemd.services.bee.serviceConfig.EnvironmentFile` instead.
'';
}
{
assertion =
(lib.hasAttr "swap-endpoint" cfg.settings) || (cfg.settings.swap-enable or true == false);
message = ''
In a swap-enabled network a working Ethereum blockchain node is required. You must specify one using `services.bee.settings.swap-endpoint`, or disable `services.bee.settings.swap-enable` = false.
'';
}
];
services.bee.settings = {
data-dir = lib.mkDefault "/var/lib/bee";
password-file = lib.mkDefault "/var/lib/bee/password";
clef-signer-enable = lib.mkDefault true;
swap-endpoint = lib.mkDefault "https://rpc.slock.it/goerli";
};
systemd.packages = [ cfg.package ]; # include the upstream bee.service file
systemd.tmpfiles.rules = [
"d '${cfg.settings.data-dir}' 0750 ${cfg.user} ${cfg.group}"
];
systemd.services.bee = {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Nice = cfg.daemonNiceLevel;
User = cfg.user;
Group = cfg.group;
ExecStart = [
"" # this hides/overrides what's in the original entry
"${cfg.package}/bin/bee --config=${configFile} start"
];
};
preStart = with cfg.settings; ''
if ! test -f ${password-file}; then
< /dev/urandom tr -dc _A-Z-a-z-0-9 2> /dev/null | head -c32 | install -m 600 /dev/stdin ${password-file}
echo "Initialized ${password-file} from /dev/urandom"
fi
if [ ! -f ${data-dir}/keys/libp2p.key ]; then
${cfg.package}/bin/bee init --config=${configFile} >/dev/null
echo "
Logs: journalctl -f -u bee.service
Bee has SWAP enabled by default and it needs ethereum endpoint to operate.
It is recommended to use external signer with bee.
Check documentation for more info:
- SWAP https://docs.ethswarm.org/docs/installation/manual#swap-bandwidth-incentives
After you finish configuration run 'sudo bee-get-addr'."
fi
'';
};
users.users = lib.optionalAttrs (cfg.user == "bee") {
bee = {
group = cfg.group;
home = cfg.settings.data-dir;
isSystemUser = true;
description = "Daemon user for Ethereum Swarm Bee";
};
};
users.groups = lib.optionalAttrs (cfg.group == "bee") {
bee = { };
};
};
}

View File

@@ -0,0 +1,308 @@
{
config,
lib,
pkgs,
options,
...
}:
let
cfg = config.services.biboumi;
inherit (config.environment) etc;
rootDir = "/run/biboumi/mnt-root";
stateDir = "/var/lib/biboumi";
settingsFile = pkgs.writeText "biboumi.cfg" (
lib.generators.toKeyValue {
mkKeyValue = k: v: lib.optionalString (v != null) (lib.generators.mkKeyValueDefault { } "=" k v);
} cfg.settings
);
need_CAP_NET_BIND_SERVICE = cfg.settings.identd_port != 0 && cfg.settings.identd_port < 1024;
in
{
options = {
services.biboumi = {
enable = lib.mkEnableOption "the Biboumi XMPP gateway to IRC";
package = lib.mkPackageOption pkgs "biboumi" { };
settings = lib.mkOption {
description = ''
See [biboumi 9.0](https://doc.biboumi.louiz.org/9.0/admin.html#configuration)
for documentation.
'';
default = { };
type = lib.types.submodule {
freeformType =
with lib.types;
(attrsOf (
nullOr (oneOf [
str
int
bool
])
))
// {
description = "settings option";
};
options.admin = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
example = [ "admin@example.org" ];
apply = lib.concatStringsSep ":";
description = ''
The bare JID of the gateway administrator. This JID will have more
privileges than other standard users, for example some administration
ad-hoc commands will only be available to that JID.
'';
};
options.ca_file = lib.mkOption {
type = lib.types.path;
default = config.security.pki.caBundle;
defaultText = lib.literalExpression "config.security.pki.caBundle";
description = ''
Specifies which file should be used as the list of trusted CA
when negotiating a TLS session.
'';
};
options.db_name = lib.mkOption {
type = with lib.types; nullOr (either path str);
default = "${stateDir}/biboumi.sqlite";
description = ''
The name of the database to use.
Set it to null and use [credentialsFile](#opt-services.biboumi.credentialsFile)
if you do not want this connection string to go into the Nix store.
'';
example = "postgresql://user:secret@localhost";
};
options.hostname = lib.mkOption {
type = lib.types.str;
example = "biboumi.example.org";
description = ''
The hostname served by the XMPPgateway.
This domain must be configured in the XMPP server
as an external component.
'';
};
options.identd_port = lib.mkOption {
type = lib.types.port;
default = 113;
example = 0;
description = ''
The TCP port on which to listen for identd queries.
'';
};
options.log_level = lib.mkOption {
type = lib.types.ints.between 0 3;
default = 1;
description = ''
Indicate what type of log messages to write in the logs.
0 is debug, 1 is info, 2 is warning, 3 is error.
'';
};
options.password = lib.mkOption {
type = with lib.types; nullOr str;
description = ''
The password used to authenticate the XMPP component to your XMPP server.
This password must be configured in the XMPP server,
associated with the external component on
[hostname](#opt-services.biboumi.settings.hostname).
Set it to null and use [credentialsFile](#opt-services.biboumi.credentialsFile)
if you do not want this password to go into the Nix store.
'';
};
options.persistent_by_default = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether all rooms will be persistent by default:
the value of the persistent option in the global configuration of each
user will be true, but the value of each individual room will still
default to false. This means that a user just needs to change the global
persistent configuration option to false in order to override this.
'';
};
options.policy_directory = lib.mkOption {
type = lib.types.path;
default = "${cfg.package}/etc/biboumi";
defaultText = lib.literalExpression ''"''${pkgs.biboumi}/etc/biboumi"'';
description = ''
A directory that should contain the policy files,
used to customize Botans behaviour
when negotiating the TLS connections with the IRC servers.
'';
};
options.port = lib.mkOption {
type = lib.types.port;
default = 5347;
description = ''
The TCP port to use to connect to the local XMPP component.
'';
};
options.realname_customization = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether the users will be able to use
the ad-hoc commands that lets them configure
their realname and username.
'';
};
options.realname_from_jid = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether the realname and username of each biboumi
user will be extracted from their JID.
Otherwise they will be set to the nick
they used to connect to the IRC server.
'';
};
options.xmpp_server_ip = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = ''
The IP address to connect to the XMPP server on.
The connection to the XMPP server is unencrypted,
so the biboumi instance and the server should
normally be on the same host.
'';
};
};
};
credentialsFile = lib.mkOption {
type = lib.types.path;
description = ''
Path to a configuration file to be merged with the settings.
Beware not to surround "=" with spaces when setting biboumi's options in this file.
Useful to merge a file which is better kept out of the Nix store
because it contains sensible data like
[password](#opt-services.biboumi.settings.password).
'';
default = "/dev/null";
example = "/run/keys/biboumi.cfg";
};
openFirewall = lib.mkEnableOption "opening of the identd port in the firewall";
};
};
config = lib.mkIf cfg.enable {
networking.firewall = lib.mkIf (cfg.openFirewall && cfg.settings.identd_port != 0) {
allowedTCPPorts = [ cfg.settings.identd_port ];
};
systemd.services.biboumi = {
description = "Biboumi, XMPP to IRC gateway";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "notify";
# Biboumi supports systemd's watchdog.
WatchdogSec = 20;
Restart = "always";
# Use "+" because credentialsFile may not be accessible to User= or Group=.
ExecStartPre = [
(
"+"
+ pkgs.writeShellScript "biboumi-prestart" ''
set -eux
cat ${settingsFile} '${cfg.credentialsFile}' |
install -m 644 /dev/stdin /run/biboumi/biboumi.cfg
''
)
];
ExecStart = "${lib.getExe cfg.package} /run/biboumi/biboumi.cfg";
ExecReload = "${pkgs.coreutils}/bin/kill -USR1 $MAINPID";
# Firewalls needing opening for output connections can still do that
# selectively for biboumi with:
# users.users.biboumi.isSystemUser = true;
# and, for example:
# networking.nftables.ruleset = ''
# add rule inet filter output meta skuid biboumi tcp accept
# '';
DynamicUser = true;
RootDirectory = rootDir;
RootDirectoryStartOnly = true;
InaccessiblePaths = [ "-+${rootDir}" ];
RuntimeDirectory = [
"biboumi"
(lib.removePrefix "/run/" rootDir)
];
RuntimeDirectoryMode = "700";
StateDirectory = "biboumi";
StateDirectoryMode = "700";
MountAPIVFS = true;
UMask = "0066";
BindPaths = [
stateDir
# This is for Type="notify"
# See https://github.com/systemd/systemd/issues/3544
"/run/systemd/notify"
"/run/systemd/journal/socket"
];
BindReadOnlyPaths = [
builtins.storeDir
"/etc"
];
# The following options are only for optimizing:
# systemd-analyze security biboumi
AmbientCapabilities = [ (lib.optionalString need_CAP_NET_BIND_SERVICE "CAP_NET_BIND_SERVICE") ];
CapabilityBoundingSet = [ (lib.optionalString need_CAP_NET_BIND_SERVICE "CAP_NET_BIND_SERVICE") ];
# ProtectClock= adds DeviceAllow=char-rtc r
DeviceAllow = "";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateNetwork = lib.mkDefault false;
PrivateTmp = true;
# PrivateUsers=true breaks AmbientCapabilities=CAP_NET_BIND_SERVICE
# See https://bugs.archlinux.org/task/65921
PrivateUsers = !need_CAP_NET_BIND_SERVICE;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
RemoveIPC = true;
# AF_UNIX is for /run/systemd/notify
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallFilter = [
"@system-service"
# Groups in @system-service which do not contain a syscall
# listed by perf stat -e 'syscalls:sys_enter_*' biboumi biboumi.cfg
# in tests, and seem likely not necessary for biboumi.
# To run such a perf in ExecStart=, you have to:
# - AmbientCapabilities="CAP_SYS_ADMIN"
# - mount -o remount,mode=755 /sys/kernel/debug/{,tracing}
"~@aio"
"~@chown"
"~@ipc"
"~@keyring"
"~@resources"
"~@setuid"
"~@timer"
];
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
};
};
};
meta.maintainers = with lib.maintainers; [ julm ];
}

View File

@@ -0,0 +1,377 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.bind;
bindPkg = config.services.bind.package;
bindUser = "named";
bindZoneCoerce =
list:
builtins.listToAttrs (
lib.forEach list (zone: {
name = zone.name;
value = zone;
})
);
bindZoneOptions =
{ name, config, ... }:
{
options = {
name = lib.mkOption {
type = lib.types.str;
default = name;
description = "Name of the zone.";
};
master = lib.mkOption {
description = "Master=false means slave server";
type = lib.types.bool;
};
file = lib.mkOption {
type = lib.types.either lib.types.str lib.types.path;
description = "Zone file resource records contain columns of data, separated by whitespace, that define the record.";
};
masters = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "List of servers for inclusion in stub and secondary zones.";
};
slaves = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "Addresses who may request zone transfers.";
default = [ ];
};
allowQuery = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = ''
List of address ranges allowed to query this zone. Instead of the address(es), this may instead
contain the single string "any".
'';
default = [ "any" ];
};
extraConfig = lib.mkOption {
type = lib.types.lines;
description = "Extra zone config to be appended at the end of the zone section.";
default = "";
};
};
};
confFile = pkgs.writeText "named.conf" ''
include "/etc/bind/rndc.key";
controls {
inet 127.0.0.1 allow {localhost;} keys {"rndc-key";};
};
acl cachenetworks { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.cacheNetworks} };
acl badnetworks { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.blockedNetworks} };
options {
listen-on port ${toString cfg.listenOnPort} { ${
lib.concatMapStrings (entry: " ${entry}; ") cfg.listenOn
} };
listen-on-v6 port ${toString cfg.listenOnIpv6Port} { ${
lib.concatMapStrings (entry: " ${entry}; ") cfg.listenOnIpv6
} };
allow-query-cache { cachenetworks; };
blackhole { badnetworks; };
forward ${cfg.forward};
forwarders { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.forwarders} };
directory "${cfg.directory}";
pid-file "/run/named/named.pid";
${cfg.extraOptions}
};
${cfg.extraConfig}
${lib.concatMapStrings (
{
name,
file,
master ? true,
slaves ? [ ],
masters ? [ ],
allowQuery ? [ ],
extraConfig ? "",
}:
''
zone "${name}" {
type ${if master then "master" else "slave"};
file "${file}";
${
if master then
''
allow-transfer {
${lib.concatMapStrings (ip: "${ip};\n") slaves}
};
''
else
''
masters {
${lib.concatMapStrings (ip: "${ip};\n") masters}
};
''
}
allow-query { ${lib.concatMapStrings (ip: "${ip}; ") allowQuery}};
${extraConfig}
};
''
) (lib.attrValues cfg.zones)}
'';
in
{
###### interface
options = {
services.bind = {
enable = lib.mkEnableOption "BIND domain name server";
package = lib.mkPackageOption pkgs "bind" { };
cacheNetworks = lib.mkOption {
default = [
"127.0.0.0/24"
"::1/128"
];
type = lib.types.listOf lib.types.str;
description = ''
What networks are allowed to use us as a resolver. Note
that this is for recursive queries -- all networks are
allowed to query zones configured with the `zones` option
by default (although this may be overridden within each
zone's configuration, via the `allowQuery` option).
It is recommended that you limit cacheNetworks to avoid your
server being used for DNS amplification attacks.
'';
};
blockedNetworks = lib.mkOption {
default = [ ];
type = lib.types.listOf lib.types.str;
description = ''
What networks are just blocked.
'';
};
ipv4Only = lib.mkOption {
default = false;
type = lib.types.bool;
description = ''
Only use ipv4, even if the host supports ipv6.
'';
};
forwarders = lib.mkOption {
default = config.networking.nameservers;
defaultText = lib.literalExpression "config.networking.nameservers";
type = lib.types.listOf lib.types.str;
description = ''
List of servers we should forward requests to.
'';
};
forward = lib.mkOption {
default = "first";
type = lib.types.enum [
"first"
"only"
];
description = ''
Whether to forward 'first' (try forwarding but lookup directly if forwarding fails) or 'only'.
'';
};
listenOn = lib.mkOption {
default = [ "any" ];
type = lib.types.listOf lib.types.str;
description = ''
Interfaces to listen on.
'';
};
listenOnPort = lib.mkOption {
default = 53;
type = lib.types.port;
description = ''
Port to listen on.
'';
};
listenOnIpv6 = lib.mkOption {
default = [ "any" ];
type = lib.types.listOf lib.types.str;
description = ''
Ipv6 interfaces to listen on.
'';
};
listenOnIpv6Port = lib.mkOption {
default = 53;
type = lib.types.port;
description = ''
Ipv6 port to listen on.
'';
};
directory = lib.mkOption {
type = lib.types.str;
default = "/run/named";
description = "Working directory of BIND.";
};
zones = lib.mkOption {
default = [ ];
type =
with lib.types;
coercedTo (listOf attrs) bindZoneCoerce (attrsOf (lib.types.submodule bindZoneOptions));
description = ''
List of zones we claim authority over.
'';
example = {
"example.com" = {
master = false;
file = "/var/dns/example.com";
masters = [ "192.168.0.1" ];
slaves = [ ];
extraConfig = "";
};
};
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Extra lines to be added verbatim to the generated named configuration file.
'';
};
extraOptions = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Extra lines to be added verbatim to the options section of the
generated named configuration file.
'';
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Additional command-line arguments to pass to named.
'';
example = [
"-n"
"4"
];
};
configFile = lib.mkOption {
type = lib.types.path;
default = confFile;
defaultText = lib.literalExpression "confFile";
description = ''
Overridable config file to use for named. By default, that
generated by nixos.
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
networking.resolvconf.useLocalResolver = lib.mkDefault true;
users.users.${bindUser} = {
group = bindUser;
description = "BIND daemon user";
isSystemUser = true;
};
users.groups.${bindUser} = { };
systemd.tmpfiles.settings."bind" = lib.mkIf (cfg.directory != "/run/named") {
${cfg.directory} = {
d = {
user = bindUser;
group = bindUser;
age = "-";
};
};
};
systemd.services.bind = {
description = "BIND Domain Name Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
preStart = ''
if ! [ -f "/etc/bind/rndc.key" ]; then
${bindPkg.out}/sbin/rndc-confgen -c /etc/bind/rndc.key -a -A hmac-sha256 2>/dev/null
fi
'';
serviceConfig = {
Type = "forking"; # Set type to forking, see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=900788
ExecStart = "${bindPkg.out}/sbin/named ${lib.optionalString cfg.ipv4Only "-4"} -c ${cfg.configFile} ${lib.concatStringsSep " " cfg.extraArgs}";
ExecReload = "${bindPkg.out}/sbin/rndc -k '/etc/bind/rndc.key' reload";
ExecStop = "${bindPkg.out}/sbin/rndc -k '/etc/bind/rndc.key' stop";
User = bindUser;
RuntimeDirectory = "named";
RuntimeDirectoryPreserve = "yes";
ConfigurationDirectory = "bind";
ReadWritePaths = [
(lib.mapAttrsToList (
name: config: if (lib.hasPrefix "/" config.file) then "-${dirOf config.file}" else ""
) cfg.zones)
cfg.directory
];
CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
# Security
NoNewPrivileges = true;
# Sandboxing
ProtectSystem = "strict";
ReadOnlyPaths = "/sys";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateMounts = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
ProtectProc = "invisible";
ProcSubset = "pid";
RemoveIPC = true;
RestrictAddressFamilies = [ "AF_UNIX AF_INET AF_INET6 AF_NETLINK" ];
LockPersonality = true;
MemoryDenyWriteExecute = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RestrictNamespaces = true;
# System Call Filtering
SystemCallArchitectures = "native";
SystemCallFilter = "~@mount @debug @clock @reboot @resources @privileged @obsolete acct modify_ldt add_key adjtimex clock_adjtime delete_module fanotify_init finit_module get_mempolicy init_module io_destroy io_getevents iopl ioperm io_setup io_submit io_cancel kcmp kexec_load keyctl lookup_dcookie migrate_pages move_pages open_by_handle_at perf_event_open process_vm_readv process_vm_writev ptrace remap_file_pages request_key set_mempolicy swapoff swapon uselib vmsplice";
};
unitConfig.Documentation = "man:named(8)";
};
};
}

View File

@@ -0,0 +1,332 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.bird-lg;
stringOrConcat = sep: v: if builtins.isString v then v else lib.concatStringsSep sep v;
frontend_args =
let
fe = cfg.frontend;
in
{
"--servers" = lib.concatStringsSep "," fe.servers;
"--domain" = fe.domain;
"--listen" = stringOrConcat "," fe.listenAddresses;
"--proxy-port" = fe.proxyPort;
"--whois" = fe.whois;
"--dns-interface" = fe.dnsInterface;
"--bgpmap-info" = lib.concatStringsSep "," cfg.frontend.bgpMapInfo;
"--title-brand" = fe.titleBrand;
"--navbar-brand" = fe.navbar.brand;
"--navbar-brand-url" = fe.navbar.brandURL;
"--navbar-all-servers" = fe.navbar.allServers;
"--navbar-all-url" = fe.navbar.allServersURL;
"--net-specific-mode" = fe.netSpecificMode;
"--protocol-filter" = lib.concatStringsSep "," cfg.frontend.protocolFilter;
};
proxy_args =
let
px = cfg.proxy;
in
{
"--allowed" = lib.concatStringsSep "," px.allowedIPs;
"--bird" = px.birdSocket;
"--listen" = stringOrConcat "," px.listenAddresses;
"--traceroute_bin" = px.traceroute.binary;
"--traceroute_flags" = lib.concatStringsSep " " px.traceroute.flags;
"--traceroute_raw" = px.traceroute.rawOutput;
};
mkArgValue =
value:
if lib.isString value then
lib.escapeShellArg value
else if lib.isBool value then
lib.boolToString value
else
toString value;
filterNull = lib.filterAttrs (_: v: v != "" && v != null && v != [ ]);
argsAttrToList =
args: lib.mapAttrsToList (name: value: "${name} " + mkArgValue value) (filterNull args);
in
{
imports = [
(lib.mkRenamedOptionModule
[ "services" "bird-lg" "frontend" "listenAddress" ]
[ "services" "bird-lg" "frontend" "listenAddresses" ]
)
(lib.mkRenamedOptionModule
[ "services" "bird-lg" "proxy" "listenAddress" ]
[ "services" "bird-lg" "proxy" "listenAddresses" ]
)
];
options = {
services.bird-lg = {
package = lib.mkPackageOption pkgs "bird-lg" { };
user = lib.mkOption {
type = lib.types.str;
default = "bird-lg";
description = "User to run the service.";
};
group = lib.mkOption {
type = lib.types.str;
default = "bird-lg";
description = "Group to run the service.";
};
frontend = {
enable = lib.mkEnableOption "Bird Looking Glass Frontend Webserver";
listenAddresses = lib.mkOption {
type = with lib.types; either str (listOf str);
default = "127.0.0.1:5000";
description = "Address to listen on.";
};
proxyPort = lib.mkOption {
type = lib.types.port;
default = 8000;
description = "Port bird-lg-proxy is running on.";
};
domain = lib.mkOption {
type = lib.types.str;
example = "dn42.lantian.pub";
description = "Server name domain suffixes.";
};
servers = lib.mkOption {
type = lib.types.listOf lib.types.str;
example = [
"gigsgigscloud"
"hostdare"
];
description = "Server name prefixes.";
};
whois = lib.mkOption {
type = lib.types.str;
default = "whois.verisign-grs.com";
description = "Whois server for queries.";
};
dnsInterface = lib.mkOption {
type = lib.types.str;
default = "asn.cymru.com";
description = "DNS zone to query ASN information.";
};
bgpMapInfo = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
"asn"
"as-name"
"ASName"
"descr"
];
description = "Information displayed in bgpmap.";
};
titleBrand = lib.mkOption {
type = lib.types.str;
default = "Bird-lg Go";
description = "Prefix of page titles in browser tabs.";
};
netSpecificMode = lib.mkOption {
type = lib.types.str;
default = "";
example = "dn42";
description = "Apply network-specific changes for some networks.";
};
protocolFilter = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "ospf" ];
description = "Information displayed in bgpmap.";
};
nameFilter = lib.mkOption {
type = lib.types.str;
default = "";
example = "^ospf";
description = "Protocol names to hide in summary tables (RE2 syntax),";
};
timeout = lib.mkOption {
type = lib.types.int;
default = 120;
description = "Time before request timed out, in seconds.";
};
navbar = {
brand = lib.mkOption {
type = lib.types.str;
default = "Bird-lg Go";
description = "Brand to show in the navigation bar .";
};
brandURL = lib.mkOption {
type = lib.types.str;
default = "/";
description = "URL of the brand to show in the navigation bar.";
};
allServers = lib.mkOption {
type = lib.types.str;
default = "ALL Servers";
description = "Text of 'All server' button in the navigation bar.";
};
allServersURL = lib.mkOption {
type = lib.types.str;
default = "all";
description = "URL of 'All servers' button.";
};
};
extraArgs = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
description = ''
Extra parameters documented [here](https://github.com/xddxdd/bird-lg-go#frontend).
:::{.note}
Passing lines (plain strings) is deprecated in favour of passing lists of strings.
:::
'';
};
};
proxy = {
enable = lib.mkEnableOption "Bird Looking Glass Proxy";
listenAddresses = lib.mkOption {
type = with lib.types; either str (listOf str);
default = "127.0.0.1:8000";
description = "Address to listen on.";
};
allowedIPs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"192.168.25.52"
"192.168.25.53"
"192.168.0.0/24"
];
description = "List of IPs or networks to allow (default all allowed).";
};
birdSocket = lib.mkOption {
type = lib.types.str;
default = "/var/run/bird/bird.ctl";
description = "Bird control socket path.";
};
traceroute = {
binary = lib.mkOption {
type = lib.types.str;
default = "${pkgs.traceroute}/bin/traceroute";
defaultText = lib.literalExpression ''"''${pkgs.traceroute}/bin/traceroute"'';
description = "Traceroute's binary path.";
};
flags = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
description = "Flags for traceroute process";
};
rawOutput = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Display traceroute output in raw format.";
};
};
extraArgs = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
description = ''
Extra parameters documented [here](https://github.com/xddxdd/bird-lg-go#proxy).
'';
};
};
};
};
###### implementation
config = {
systemd.services = {
bird-lg-frontend = lib.mkIf cfg.frontend.enable {
enable = true;
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
description = "Bird Looking Glass Frontend Webserver";
serviceConfig = {
Type = "simple";
Restart = "on-failure";
ProtectSystem = "full";
ProtectHome = "yes";
MemoryDenyWriteExecute = "yes";
User = cfg.user;
Group = cfg.group;
};
script = ''
${cfg.package}/bin/frontend \
${lib.concatStringsSep " \\\n " (argsAttrToList frontend_args)} \
${stringOrConcat " " cfg.frontend.extraArgs}
'';
};
bird-lg-proxy = lib.mkIf cfg.proxy.enable {
enable = true;
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
description = "Bird Looking Glass Proxy";
serviceConfig = {
Type = "simple";
Restart = "on-failure";
ProtectSystem = "full";
ProtectHome = "yes";
MemoryDenyWriteExecute = "yes";
User = cfg.user;
Group = cfg.group;
};
script = ''
${cfg.package}/bin/proxy \
${lib.concatStringsSep " \\\n " (argsAttrToList proxy_args)} \
${stringOrConcat " " cfg.proxy.extraArgs}
'';
};
};
users = lib.mkIf (cfg.frontend.enable || cfg.proxy.enable) {
groups."bird-lg" = lib.mkIf (cfg.group == "bird-lg") { };
users."bird-lg" = lib.mkIf (cfg.user == "bird-lg") {
description = "Bird Looking Glass user";
extraGroups = lib.optionals (config.services.bird.enable) [ "bird" ];
group = cfg.group;
isSystemUser = true;
};
};
};
meta.maintainers = with lib.maintainers; [
e1mo
tchekda
];
}

View File

@@ -0,0 +1,131 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
mkEnableOption
mkIf
mkOption
optionalString
types
;
cfg = config.services.bird;
caps = [
"CAP_NET_ADMIN"
"CAP_NET_BIND_SERVICE"
"CAP_NET_RAW"
];
in
{
###### interface
options = {
services.bird = {
enable = mkEnableOption "BIRD Internet Routing Daemon";
package = lib.mkPackageOption pkgs "bird3" { };
config = mkOption {
type = types.lines;
description = ''
BIRD Internet Routing Daemon configuration file.
<http://bird.network.cz/>
'';
};
autoReload = mkOption {
type = types.bool;
default = true;
description = ''
Whether bird should be automatically reloaded when the configuration changes.
'';
};
checkConfig = mkOption {
type = types.bool;
default = true;
description = ''
Whether the config should be checked at build time.
When the config can't be checked during build time, for example when it includes
other files, either disable this option or use `preCheckConfig` to create
the included files before checking.
'';
};
preCheckConfig = mkOption {
type = types.lines;
default = "";
example = ''
echo "cost 100;" > include.conf
'';
description = ''
Commands to execute before the config file check. The file to be checked will be
available as `bird.conf` in the current directory.
Files created with this option will not be available at service runtime, only during
build time checking.
'';
};
};
};
imports = [
(lib.mkRemovedOptionModule [ "services" "bird2" ]
"Use services.bird instead. bird3 is the new default bird package. You can choose to remain with bird2 by setting the service.bird.package option."
)
(lib.mkRemovedOptionModule [ "services" "bird6" ] "Use services.bird instead")
];
###### implementation
config = mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
environment.etc."bird/bird.conf".source = pkgs.writeTextFile {
name = "bird";
text = cfg.config;
derivationArgs.nativeBuildInputs = lib.optional cfg.checkConfig cfg.package;
checkPhase = optionalString cfg.checkConfig ''
ln -s $out bird.conf
${cfg.preCheckConfig}
bird -d -p -c bird.conf || { exit=$?; cat -n bird.conf; exit $exit; }
'';
};
systemd.services.bird = {
description = "BIRD Internet Routing Daemon";
wantedBy = [ "multi-user.target" ];
reloadTriggers = lib.optional cfg.autoReload config.environment.etc."bird/bird.conf".source;
serviceConfig = {
Type = "forking";
Restart = "on-failure";
User = "bird";
Group = "bird";
ExecStart = "${lib.getExe' cfg.package "bird"} -c /etc/bird/bird.conf";
ExecReload = "${lib.getExe' cfg.package "birdc"} configure";
ExecStop = "${lib.getExe' cfg.package "birdc"} down";
RuntimeDirectory = "bird";
CapabilityBoundingSet = caps;
AmbientCapabilities = caps;
ProtectSystem = "full";
ProtectHome = "yes";
ProtectKernelTunables = true;
ProtectControlGroups = true;
PrivateTmp = true;
PrivateDevices = true;
SystemCallFilter = "~@cpu-emulation @debug @keyring @module @mount @obsolete @raw-io";
MemoryDenyWriteExecute = "yes";
};
};
users = {
users.bird = {
description = "BIRD Internet Routing Daemon user";
group = "bird";
isSystemUser = true;
};
groups.bird = { };
};
};
meta = {
maintainers = with lib.maintainers; [ herbetom ];
};
}

View File

@@ -0,0 +1,131 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.birdwatcher;
in
{
options = {
services.birdwatcher = {
package = lib.mkPackageOption pkgs "birdwatcher" { };
enable = lib.mkEnableOption "Birdwatcher";
flags = lib.mkOption {
default = [ ];
type = lib.types.listOf lib.types.str;
example = [
"-worker-pool-size 16"
"-6"
];
description = ''
Flags to append to the program call
'';
};
settings = lib.mkOption {
type = lib.types.lines;
default = { };
description = ''
birdwatcher configuration, for configuration options see the example on [github](https://github.com/alice-lg/birdwatcher/blob/master/etc/birdwatcher/birdwatcher.conf)
'';
example = lib.literalExpression ''
[server]
allow_from = []
allow_uncached = false
modules_enabled = ["status",
"protocols",
"protocols_bgp",
"protocols_short",
"routes_protocol",
"routes_peer",
"routes_table",
"routes_table_filtered",
"routes_table_peer",
"routes_filtered",
"routes_prefixed",
"routes_noexport",
"routes_pipe_filtered_count",
"routes_pipe_filtered"
]
[status]
reconfig_timestamp_source = "bird"
reconfig_timestamp_match = "# created: (.*)"
filter_fields = []
[bird]
listen = "0.0.0.0:29184"
config = "/etc/bird/bird.conf"
birdc = "''${pkgs.bird2}/bin/birdc"
ttl = 5 # time to live (in minutes) for caching of cli output
[parser]
filter_fields = []
[cache]
use_redis = false # if not using redis cache, activate housekeeping to save memory!
[housekeeping]
interval = 5
force_release_memory = true
'';
};
};
};
config =
let
flagsStr = lib.escapeShellArgs cfg.flags;
in
lib.mkIf cfg.enable {
environment.etc."birdwatcher/birdwatcher.conf".source = pkgs.writeTextFile {
name = "birdwatcher.conf";
text = cfg.settings;
};
systemd.services = {
birdwatcher = {
wants = [ "network.target" ];
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
description = "Birdwatcher";
serviceConfig = {
Type = "simple";
Restart = "on-failure";
RestartSec = 15;
ExecStart = "${cfg.package}/bin/birdwatcher";
StateDirectoryMode = "0700";
UMask = "0117";
NoNewPrivileges = true;
ProtectSystem = "strict";
PrivateTmp = true;
PrivateDevices = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [ "AF_UNIX AF_INET AF_INET6" ];
LockPersonality = true;
MemoryDenyWriteExecute = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
PrivateMounts = true;
SystemCallArchitectures = "native";
SystemCallFilter = "~@clock @privileged @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @reboot @setuid @swap";
BindReadOnlyPaths = [
"-/etc/resolv.conf"
"-/etc/nsswitch.conf"
"-/etc/ssl/certs"
"-/etc/static/ssl/certs"
"-/etc/hosts"
"-/etc/localtime"
];
};
};
};
};
}

View File

@@ -0,0 +1,285 @@
{
config,
pkgs,
lib,
...
}:
with lib;
let
eachBitcoind = filterAttrs (bitcoindName: cfg: cfg.enable) config.services.bitcoind;
rpcUserOpts =
{ name, ... }:
{
options = {
name = mkOption {
type = types.str;
example = "alice";
description = ''
Username for JSON-RPC connections.
'';
};
passwordHMAC = mkOption {
type = types.uniq (types.strMatching "[0-9a-f]+\\$[0-9a-f]{64}");
example = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
description = ''
Password HMAC-SHA-256 for JSON-RPC connections. Must be a string of the
format \<SALT-HEX\>$\<HMAC-HEX\>.
Tool (Python script) for HMAC generation is available here:
<https://github.com/bitcoin/bitcoin/blob/master/share/rpcauth/rpcauth.py>
'';
};
};
config = {
name = mkDefault name;
};
};
bitcoindOpts =
{
config,
lib,
name,
...
}:
{
options = {
enable = mkEnableOption "Bitcoin daemon";
package = mkPackageOption pkgs "bitcoind" { };
configFile = mkOption {
type = types.nullOr types.path;
default = null;
example = "/var/lib/${name}/bitcoin.conf";
description = "The configuration file path to supply bitcoind.";
};
extraConfig = mkOption {
type = types.lines;
default = "";
example = ''
par=16
rpcthreads=16
logips=1
'';
description = "Additional configurations to be appended to {file}`bitcoin.conf`.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/bitcoind-${name}";
description = "The data directory for bitcoind.";
};
user = mkOption {
type = types.str;
default = "bitcoind-${name}";
description = "The user as which to run bitcoind.";
};
group = mkOption {
type = types.str;
default = config.user;
description = "The group as which to run bitcoind.";
};
rpc = {
port = mkOption {
type = types.nullOr types.port;
default = null;
description = "Override the default port on which to listen for JSON-RPC connections.";
};
users = mkOption {
default = { };
example = literalExpression ''
{
alice.passwordHMAC = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
bob.passwordHMAC = "b2dd077cb54591a2f3139e69a897ac$4e71f08d48b4347cf8eff3815c0e25ae2e9a4340474079f55705f40574f4ec99";
}
'';
type = types.attrsOf (types.submodule rpcUserOpts);
description = "RPC user information for JSON-RPC connections.";
};
};
pidFile = mkOption {
type = types.path;
default = "${config.dataDir}/bitcoind.pid";
description = "Location of bitcoind pid file.";
};
testnet = mkOption {
type = types.bool;
default = false;
description = "Whether to use the testnet instead of mainnet.";
};
port = mkOption {
type = types.nullOr types.port;
default = null;
description = "Override the default port on which to listen for connections.";
};
dbCache = mkOption {
type = types.nullOr (types.ints.between 4 16384);
default = null;
example = 4000;
description = "Override the default database cache size in MiB.";
};
prune = mkOption {
type = types.nullOr (
types.coercedTo (types.enum [
"disable"
"manual"
]) (x: if x == "disable" then 0 else 1) types.ints.unsigned
);
default = null;
example = 10000;
description = ''
Reduce storage requirements by enabling pruning (deleting) of old
blocks. This allows the pruneblockchain RPC to be called to delete
specific blocks, and enables automatic pruning of old blocks if a
target size in MiB is provided. This mode is incompatible with -txindex
and -rescan. Warning: Reverting this setting requires re-downloading
the entire blockchain. ("disable" = disable pruning blocks, "manual"
= allow manual pruning via RPC, >=550 = automatically prune block files
to stay under the specified target size in MiB).
'';
};
extraCmdlineOptions = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Extra command line options to pass to bitcoind.
Run bitcoind --help to list all available options.
'';
};
};
};
in
{
options = {
services.bitcoind = mkOption {
type = types.attrsOf (types.submodule bitcoindOpts);
default = { };
description = "Specification of one or more bitcoind instances.";
};
};
config = mkIf (eachBitcoind != { }) {
assertions = flatten (
mapAttrsToList (bitcoindName: cfg: [
{
assertion =
(cfg.prune != null)
-> (
builtins.elem cfg.prune [
"disable"
"manual"
0
1
]
|| (builtins.isInt cfg.prune && cfg.prune >= 550)
);
message = ''
If set, services.bitcoind.${bitcoindName}.prune has to be "disable", "manual", 0 , 1 or >= 550.
'';
}
{
assertion = (cfg.rpc.users != { }) -> (cfg.configFile == null);
message = ''
You cannot set both services.bitcoind.${bitcoindName}.rpc.users and services.bitcoind.${bitcoindName}.configFile
as they are exclusive. RPC user setting would have no effect if custom configFile would be used.
'';
}
]) eachBitcoind
);
environment.systemPackages = flatten (
mapAttrsToList (bitcoindName: cfg: [
cfg.package
]) eachBitcoind
);
systemd.services = mapAttrs' (
bitcoindName: cfg:
(nameValuePair "bitcoind-${bitcoindName}" (
let
configFile = pkgs.writeText "bitcoin.conf" ''
# If Testnet is enabled, we need to add [test] section
# otherwise, some options (e.g.: custom RPC port) will not work
${optionalString cfg.testnet "[test]"}
# RPC users
${concatMapStringsSep "\n" (rpcUser: "rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC}") (
attrValues cfg.rpc.users
)}
# Extra config options (from bitcoind nixos service)
${cfg.extraConfig}
'';
in
{
description = "Bitcoin daemon";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStart = ''
${cfg.package}/bin/bitcoind \
${if (cfg.configFile != null) then "-conf=${cfg.configFile}" else "-conf=${configFile}"} \
-datadir=${cfg.dataDir} \
-pid=${cfg.pidFile} \
${optionalString cfg.testnet "-testnet"}\
${optionalString (cfg.port != null) "-port=${toString cfg.port}"}\
${optionalString (cfg.prune != null) "-prune=${toString cfg.prune}"}\
${optionalString (cfg.dbCache != null) "-dbcache=${toString cfg.dbCache}"}\
${optionalString (cfg.rpc.port != null) "-rpcport=${toString cfg.rpc.port}"}\
${toString cfg.extraCmdlineOptions}
'';
Restart = "on-failure";
# Hardening measures
PrivateTmp = "true";
ProtectSystem = "full";
NoNewPrivileges = "true";
PrivateDevices = "true";
MemoryDenyWriteExecute = "true";
};
}
))
) eachBitcoind;
systemd.tmpfiles.rules = flatten (
mapAttrsToList (bitcoindName: cfg: [
"d '${cfg.dataDir}' 0770 '${cfg.user}' '${cfg.group}' - -"
]) eachBitcoind
);
users.users = mapAttrs' (
bitcoindName: cfg:
(nameValuePair "bitcoind-${bitcoindName}" {
name = cfg.user;
group = cfg.group;
description = "Bitcoin daemon user";
home = cfg.dataDir;
isSystemUser = true;
})
) eachBitcoind;
users.groups = mapAttrs' (bitcoindName: cfg: (nameValuePair "${cfg.group}" { })) eachBitcoind;
};
meta.maintainers = with maintainers; [ _1000101 ];
}

View File

@@ -0,0 +1,196 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.bitlbee;
bitlbeeUid = config.ids.uids.bitlbee;
bitlbeePkg = pkgs.bitlbee.override {
enableLibPurple = cfg.libpurple_plugins != [ ];
enablePam = cfg.authBackend == "pam";
};
bitlbeeConfig = pkgs.writeText "bitlbee.conf" ''
[settings]
RunMode = Daemon
ConfigDir = ${cfg.configDir}
DaemonInterface = ${cfg.interface}
DaemonPort = ${toString cfg.portNumber}
AuthMode = ${cfg.authMode}
AuthBackend = ${cfg.authBackend}
Plugindir = ${pkgs.bitlbee-plugins cfg.plugins}/lib/bitlbee
${lib.optionalString (cfg.hostName != "") "HostName = ${cfg.hostName}"}
${lib.optionalString (cfg.protocols != "") "Protocols = ${cfg.protocols}"}
${cfg.extraSettings}
[defaults]
${cfg.extraDefaults}
'';
purple_plugin_path = lib.concatMapStringsSep ":" (
plugin: "${plugin}/lib/pidgin/:${plugin}/lib/purple-2/"
) cfg.libpurple_plugins;
in
{
###### interface
options = {
services.bitlbee = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to run the BitlBee IRC to other chat network gateway.
Running it allows you to access the MSN, Jabber, Yahoo! and ICQ chat
networks via an IRC client.
'';
};
interface = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = ''
The interface the BitlBee daemon will be listening to. If `127.0.0.1`,
only clients on the local host can connect to it; if `0.0.0.0`, clients
can access it from any network interface.
'';
};
portNumber = lib.mkOption {
default = 6667;
type = lib.types.port;
description = ''
Number of the port BitlBee will be listening to.
'';
};
authBackend = lib.mkOption {
default = "storage";
type = lib.types.enum [
"storage"
"pam"
];
description = ''
How users are authenticated
storage -- save passwords internally
pam -- Linux PAM authentication
'';
};
authMode = lib.mkOption {
default = "Open";
type = lib.types.enum [
"Open"
"Closed"
"Registered"
];
description = ''
The following authentication modes are available:
Open -- Accept connections from anyone, use NickServ for user authentication.
Closed -- Require authorization (using the PASS command during login) before allowing the user to connect at all.
Registered -- Only allow registered users to use this server; this disables the register- and the account command until the user identifies himself.
'';
};
hostName = lib.mkOption {
default = "";
type = lib.types.str;
description = ''
Normally, BitlBee gets a hostname using getsockname(). If you have a nicer
alias for your BitlBee daemon, you can set it here and BitlBee will identify
itself with that name instead.
'';
};
plugins = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
example = lib.literalExpression "[ pkgs.bitlbee-facebook ]";
description = ''
The list of bitlbee plugins to install.
'';
};
libpurple_plugins = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
example = lib.literalExpression "[ pkgs.purple-discord ]";
description = ''
The list of libpurple plugins to install.
'';
};
configDir = lib.mkOption {
default = "/var/lib/bitlbee";
type = lib.types.path;
description = ''
Specify an alternative directory to store all the per-user configuration
files.
'';
};
protocols = lib.mkOption {
default = "";
type = lib.types.str;
description = ''
This option allows to remove the support of protocol, even if compiled
in. If nothing is given, there are no restrictions.
'';
};
extraSettings = lib.mkOption {
default = "";
type = lib.types.lines;
description = ''
Will be inserted in the Settings section of the config file.
'';
};
extraDefaults = lib.mkOption {
default = "";
type = lib.types.lines;
description = ''
Will be inserted in the Default section of the config file.
'';
};
};
};
###### implementation
config = lib.mkMerge [
(lib.mkIf config.services.bitlbee.enable {
systemd.services.bitlbee = {
environment.PURPLE_PLUGIN_PATH = purple_plugin_path;
description = "BitlBee IRC to other chat networks gateway";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
StateDirectory = "bitlbee";
ReadWritePaths = [ cfg.configDir ];
ExecStart = "${bitlbeePkg}/sbin/bitlbee -F -n -c ${bitlbeeConfig}";
};
};
environment.systemPackages = [ bitlbeePkg ];
})
(lib.mkIf (config.services.bitlbee.authBackend == "pam") {
security.pam.services.bitlbee = { };
})
];
}

View File

@@ -0,0 +1,303 @@
{
config,
lib,
pkgs,
...
}:
let
eachBlockbook = config.services.blockbook-frontend;
blockbookOpts =
{
config,
lib,
name,
...
}:
{
options = {
enable = lib.mkEnableOption "blockbook-frontend application";
package = lib.mkPackageOption pkgs "blockbook" { };
user = lib.mkOption {
type = lib.types.str;
default = "blockbook-frontend-${name}";
description = "The user as which to run blockbook-frontend-${name}.";
};
group = lib.mkOption {
type = lib.types.str;
default = "${config.user}";
description = "The group as which to run blockbook-frontend-${name}.";
};
certFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/etc/secrets/blockbook-frontend-${name}/certFile";
description = ''
To enable SSL, specify path to the name of certificate files without extension.
Expecting {file}`certFile.crt` and {file}`certFile.key`.
'';
};
configFile = lib.mkOption {
type = with lib.types; nullOr path;
default = null;
example = "${config.dataDir}/config.json";
description = "Location of the blockbook configuration file.";
};
coinName = lib.mkOption {
type = lib.types.str;
default = "Bitcoin";
description = ''
See <https://github.com/trezor/blockbook/blob/master/bchain/coins/blockchain.go#L61>
for current of coins supported in master (Note: may differ from release).
'';
};
cssDir = lib.mkOption {
type = lib.types.path;
default = "${config.package}/share/css/";
defaultText = lib.literalExpression ''"''${package}/share/css/"'';
example = lib.literalExpression ''"''${dataDir}/static/css/"'';
description = ''
Location of the dir with {file}`main.css` CSS file.
By default, the one shipped with the package is used.
'';
};
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/blockbook-frontend-${name}";
description = "Location of blockbook-frontend-${name} data directory.";
};
debug = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Debug mode, return more verbose errors, reload templates on each request.";
};
internal = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = ":9030";
description = "Internal http server binding `[address]:port`.";
};
messageQueueBinding = lib.mkOption {
type = lib.types.str;
default = "tcp://127.0.0.1:38330";
description = "Message Queue Binding `address:port`.";
};
public = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = ":9130";
description = "Public http server binding `[address]:port`.";
};
rpc = {
url = lib.mkOption {
type = lib.types.str;
default = "http://127.0.0.1";
description = "URL for JSON-RPC connections.";
};
port = lib.mkOption {
type = lib.types.port;
default = 8030;
description = "Port for JSON-RPC connections.";
};
user = lib.mkOption {
type = lib.types.str;
default = "rpc";
description = "Username for JSON-RPC connections.";
};
password = lib.mkOption {
type = lib.types.str;
default = "rpc";
description = ''
RPC password for JSON-RPC connections.
Warning: this is stored in cleartext in the Nix store!!!
Use `configFile` or `passwordFile` if needed.
'';
};
passwordFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
File containing password of the RPC user.
Note: This options is ignored when `configFile` is used.
'';
};
};
sync = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Synchronizes until tip, if together with zeromq, keeps index synchronized.";
};
templateDir = lib.mkOption {
type = lib.types.path;
default = "${config.package}/share/templates/";
defaultText = lib.literalExpression ''"''${package}/share/templates/"'';
example = lib.literalExpression ''"''${dataDir}/templates/static/"'';
description = "Location of the HTML templates. By default, ones shipped with the package are used.";
};
extraConfig = lib.mkOption {
type = lib.types.attrs;
default = { };
example = lib.literalExpression ''
{
"alternative_estimate_fee" = "whatthefee-disabled";
"alternative_estimate_fee_params" = "{\"url\": \"https://whatthefee.io/data.json\", \"periodSeconds\": 60}";
"fiat_rates" = "coingecko";
"fiat_rates_params" = "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"bitcoin\", \"periodSeconds\": 60}";
"coin_shortcut" = "BTC";
"coin_label" = "Bitcoin";
"parse" = true;
"subversion" = "";
"address_format" = "";
"xpub_magic" = 76067358;
"xpub_magic_segwit_p2sh" = 77429938;
"xpub_magic_segwit_native" = 78792518;
"mempool_workers" = 8;
"mempool_sub_workers" = 2;
"block_addresses_to_keep" = 300;
}'';
description = ''
Additional configurations to be appended to {file}`coin.conf`.
Overrides any already defined configuration options.
See <https://github.com/trezor/blockbook/tree/master/configs/coins>
for current configuration options supported in master (Note: may differ from release).
'';
};
extraCmdLineOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"-workers=1"
"-dbcache=0"
"-logtosderr"
];
description = ''
Extra command line options to pass to Blockbook.
Run blockbook --help to list all available options.
'';
};
};
};
in
{
# interface
options = {
services.blockbook-frontend = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule blockbookOpts);
default = { };
description = "Specification of one or more blockbook-frontend instances.";
};
};
# implementation
config = lib.mkIf (eachBlockbook != { }) {
systemd.services = lib.mapAttrs' (
blockbookName: cfg:
(lib.nameValuePair "blockbook-frontend-${blockbookName}" (
let
configFile =
if cfg.configFile != null then
cfg.configFile
else
pkgs.writeText "config.conf" (
builtins.toJSON (
{
coin_name = "${cfg.coinName}";
rpc_user = "${cfg.rpc.user}";
rpc_pass = "${cfg.rpc.password}";
rpc_url = "${cfg.rpc.url}:${toString cfg.rpc.port}";
message_queue_binding = "${cfg.messageQueueBinding}";
}
// cfg.extraConfig
)
);
in
{
description = "blockbook-frontend-${blockbookName} daemon";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
preStart = ''
ln -sf ${cfg.templateDir} ${cfg.dataDir}/static/
ln -sf ${cfg.cssDir} ${cfg.dataDir}/static/
${lib.optionalString (cfg.rpc.passwordFile != null && cfg.configFile == null) ''
CONFIGTMP=$(mktemp)
${pkgs.jq}/bin/jq ".rpc_pass = \"$(cat ${cfg.rpc.passwordFile})\"" ${configFile} > $CONFIGTMP
mv $CONFIGTMP ${cfg.dataDir}/${blockbookName}-config.json
''}
'';
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStart = ''
${cfg.package}/bin/blockbook \
${
if (cfg.rpc.passwordFile != null && cfg.configFile == null) then
"-blockchaincfg=${cfg.dataDir}/${blockbookName}-config.json"
else
"-blockchaincfg=${configFile}"
} \
-datadir=${cfg.dataDir} \
${lib.optionalString (cfg.sync != false) "-sync"} \
${lib.optionalString (cfg.certFile != null) "-certfile=${toString cfg.certFile}"} \
${lib.optionalString (cfg.debug != false) "-debug"} \
${lib.optionalString (cfg.internal != null) "-internal=${toString cfg.internal}"} \
${lib.optionalString (cfg.public != null) "-public=${toString cfg.public}"} \
${toString cfg.extraCmdLineOptions}
'';
Restart = "on-failure";
WorkingDirectory = cfg.dataDir;
LimitNOFILE = 65536;
};
}
))
) eachBlockbook;
systemd.tmpfiles.rules = lib.flatten (
lib.mapAttrsToList (blockbookName: cfg: [
"d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} - -"
"d ${cfg.dataDir}/static 0750 ${cfg.user} ${cfg.group} - -"
]) eachBlockbook
);
users.users = lib.mapAttrs' (
blockbookName: cfg:
(lib.nameValuePair "blockbook-frontend-${blockbookName}" {
name = cfg.user;
group = cfg.group;
home = cfg.dataDir;
isSystemUser = true;
})
) eachBlockbook;
users.groups = lib.mapAttrs' (
instanceName: cfg: (lib.nameValuePair "${cfg.group}" { })
) eachBlockbook;
};
meta.maintainers = with lib.maintainers; [ _1000101 ];
}

View File

@@ -0,0 +1,85 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.blocky;
format = pkgs.formats.yaml { };
configFile = format.generate "config.yaml" cfg.settings;
in
{
options.services.blocky = {
enable = lib.mkEnableOption "blocky, a fast and lightweight DNS proxy as ad-blocker for local network with many features";
package = lib.mkPackageOption pkgs "blocky" { };
settings = lib.mkOption {
type = format.type;
default = { };
description = ''
Blocky configuration. Refer to
<https://0xerr0r.github.io/blocky/configuration/>
for details on supported values.
'';
};
};
config = lib.mkIf cfg.enable {
systemd.services.blocky = {
description = "A DNS proxy and ad-blocker for the local network";
wants = [
"network-online.target"
"nss-lookup.target"
];
before = [
"nss-lookup.target"
];
wantedBy = [
"multi-user.target"
];
serviceConfig = {
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
DynamicUser = true;
ExecStart = "${lib.getExe cfg.package} --config ${configFile}";
LockPersonality = true;
LogsDirectory = "blocky";
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
NonBlocking = true;
PrivateDevices = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
Restart = "on-failure";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RuntimeDirectory = "blocky";
StateDirectory = "blocky";
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"@chown"
"~@aio"
"~@keyring"
"~@memlock"
"~@setuid"
"~@timer"
];
};
};
};
meta.maintainers = with lib.maintainers; [ paepcke ];
}

View File

@@ -0,0 +1,53 @@
{
lib,
config,
pkgs,
...
}:
let
cfg = config.services.byedpi;
in
{
options.services.byedpi = {
enable = lib.mkEnableOption "the ByeDPI service";
package = lib.mkPackageOption pkgs "byedpi" { };
extraArgs = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
example = [
"--split"
"1"
"--disorder"
"3+s"
"--mod-http=h,d"
"--auto=torst"
"--tlsrec"
"1+s"
];
description = "Extra command line arguments.";
};
};
config = lib.mkIf cfg.enable {
systemd.services.byedpi = {
description = "ByeDPI";
wantedBy = [ "default.target" ];
wants = [ "network-online.target" ];
after = [
"network-online.target"
"nss-lookup.target"
];
serviceConfig = {
ExecStart = lib.escapeShellArgs ([ (lib.getExe cfg.package) ] ++ cfg.extraArgs);
NoNewPrivileges = "yes";
StandardOutput = "null";
StandardError = "journal";
TimeoutStopSec = "5s";
PrivateTmp = "true";
ProtectSystem = "full";
};
};
};
meta.maintainers = with lib.maintainers; [ wozrer ];
}

View File

@@ -0,0 +1,75 @@
{
config,
pkgs,
lib,
...
}:
let
inherit (lib) mkIf mkEnableOption mkPackageOption;
cfg = config.services.cato-client;
in
{
options.services.cato-client = {
enable = mkEnableOption "cato-client service";
package = mkPackageOption pkgs "cato-client" { };
};
config = mkIf cfg.enable {
users = {
groups.cato-client = { };
};
environment.systemPackages = [
cfg.package
];
systemd.services.cato-client = {
enable = true;
description = "Cato Networks Linux client - connects tunnel to Cato cloud";
after = [ "network.target" ];
serviceConfig = {
Type = "simple";
User = "root"; # Note: daemon runs as root, tools sticky to group
Group = "cato-client";
ExecStart = "${cfg.package}/bin/cato-clientd systemd";
WorkingDirectory = "${cfg.package}";
Restart = "always";
# Cato client seems to do the following:
# - Look in each user's ~/.cato/ for configuration and keys
# - Write to /var/log/cato-client.log
# - Create and use sockets /var/run/cato-sdp.i, /var/run/cato-sdp.o
# - Read and Write to /opt/cato/ for runtime settings
# - Read /etc/systemd/resolved.conf (but fine if fails)
# - Restart systemd-resolved (also fine if doesn't exist)
NoNewPrivileges = true;
PrivateTmp = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
ProtectSystem = true;
};
wantedBy = [ "multi-user.target" ];
};
# set up Security wrapper Same as intended in deb post install
security.wrappers.cato-clientd = {
source = "${cfg.package}/bin/cato-clientd";
owner = "root";
group = "cato-client";
permissions = "u+rwx,g+rwx"; # 770
setgid = true;
};
security.wrappers.cato-sdp = {
source = "${cfg.package}/bin/cato-sdp";
owner = "root";
group = "cato-client";
permissions = "u+rwx,g+rx,a+rx"; # 755
setgid = true;
};
};
}

View File

@@ -0,0 +1,136 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.centrifugo;
settingsFormat = pkgs.formats.json { };
configFile = settingsFormat.generate "centrifugo.json" cfg.settings;
in
{
options.services.centrifugo = {
enable = lib.mkEnableOption "Centrifugo messaging server";
package = lib.mkPackageOption pkgs "centrifugo" { };
settings = lib.mkOption {
type = settingsFormat.type;
default = { };
description = ''
Declarative Centrifugo configuration. See the [Centrifugo
documentation] for a list of options.
[Centrifugo documentation]: https://centrifugal.dev/docs/server/configuration
'';
};
credentials = lib.mkOption {
type = lib.types.attrsOf lib.types.path;
default = { };
example = {
CENTRIFUGO_UNI_GRPC_TLS_KEY = "/run/keys/centrifugo-uni-grpc-tls.key";
};
description = ''
Environment variables with absolute paths to credentials files to load
on service startup.
'';
};
environmentFiles = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ ];
description = ''
Files to load environment variables from. Options set via environment
variables take precedence over {option}`settings`.
See the [Centrifugo documentation] for the environment variable name
format.
[Centrifugo documentation]: https://centrifugal.dev/docs/server/configuration#os-environment-variables
'';
};
extraGroups = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "redis-centrifugo" ];
description = ''
Additional groups for the systemd service.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion =
(lib.versionAtLeast cfg.package.version "6") -> (!(cfg.settings ? name) && !(cfg.settings ? port));
message = "`services.centrifugo.settings` is v5 config, must be compatible with centrifugo v6 config format";
}
];
systemd.services.centrifugo = {
description = "Centrifugo messaging server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
Type = "exec";
ExecStartPre = "${lib.getExe cfg.package} checkconfig --config ${configFile}";
ExecStart = "${lib.getExe cfg.package} --config ${configFile}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
Restart = "always";
RestartSec = "1s";
# Copy files to the credentials directory with file name being the
# environment variable name. Note that "%d" specifier expands to the
# path of the credentials directory.
LoadCredential = lib.mapAttrsToList (name: value: "${name}:${value}") cfg.credentials;
Environment = lib.mapAttrsToList (name: _: "${name}=%d/${name}") cfg.credentials;
EnvironmentFile = cfg.environmentFiles;
SupplementaryGroups = cfg.extraGroups;
DynamicUser = true;
UMask = "0077";
ProtectHome = true;
ProtectProc = "invisible";
ProcSubset = "pid";
ProtectClock = true;
ProtectHostname = true;
ProtectControlGroups = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
PrivateUsers = true;
PrivateDevices = true;
RestrictRealtime = true;
RestrictNamespaces = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
DeviceAllow = [ "" ];
DevicePolicy = "closed";
CapabilityBoundingSet = [ "" ];
MemoryDenyWriteExecute = true;
LockPersonality = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
};
};
};
}

View File

@@ -0,0 +1,286 @@
{
config,
lib,
pkgs,
...
}:
let
cfgs = config.services.cgit;
settingType =
with lib.types;
oneOf [
bool
int
str
];
repeatedSettingType =
with lib.types;
oneOf [
settingType
(listOf settingType)
];
genAttrs' = names: f: lib.listToAttrs (map f names);
regexEscape =
let
# taken from https://github.com/python/cpython/blob/05cb728d68a278d11466f9a6c8258d914135c96c/Lib/re.py#L251-L266
special = [
"("
")"
"["
"]"
"{"
"}"
"?"
"*"
"+"
"-"
"|"
"^"
"$"
"\\"
"."
"&"
"~"
"#"
" "
"\t"
"\n"
"\r"
" " # \v / 0x0B
" " # \f / 0x0C
];
in
lib.replaceStrings special (map (c: "\\${c}") special);
stripLocation = cfg: lib.removeSuffix "/" cfg.nginx.location;
regexLocation = cfg: regexEscape (stripLocation cfg);
mkFastcgiPass = name: cfg: ''
${
if cfg.nginx.location == "/" then
''
fastcgi_param PATH_INFO $uri;
''
else
''
fastcgi_split_path_info ^(${regexLocation cfg})(/.+)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
''
}fastcgi_pass unix:${config.services.fcgiwrap.instances."cgit-${name}".socket.address};
'';
cgitrcLine =
name: value:
"${name}=${
if value == true then
"1"
else if value == false then
"0"
else
toString value
}";
# list value as multiple lines (for "readme" for example)
cgitrcEntry =
name: value: if lib.isList value then map (cgitrcLine name) value else [ (cgitrcLine name value) ];
mkCgitrc =
cfg:
pkgs.writeText "cgitrc" ''
# global settings
${lib.concatStringsSep "\n" (
lib.flatten (
lib.mapAttrsToList cgitrcEntry ({ virtual-root = cfg.nginx.location; } // cfg.settings)
)
)}
${lib.optionalString (cfg.scanPath != null) (cgitrcLine "scan-path" cfg.scanPath)}
# repository settings
${lib.concatStrings (
lib.mapAttrsToList (url: settings: ''
${cgitrcLine "repo.url" url}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: cgitrcLine "repo.${name}") settings)}
'') cfg.repos
)}
# extra config
${cfg.extraConfig}
'';
fcgiwrapUnitName = name: "fcgiwrap-cgit-${name}";
fcgiwrapRuntimeDir = name: "/run/${fcgiwrapUnitName name}";
gitProjectRoot =
name: cfg: if cfg.scanPath != null then cfg.scanPath else "${fcgiwrapRuntimeDir name}/repos";
in
{
options = {
services.cgit = lib.mkOption {
description = "Configure cgit instances.";
default = { };
type = lib.types.attrsOf (
lib.types.submodule (
{ config, ... }:
{
options = {
enable = lib.mkEnableOption "cgit";
package = lib.mkPackageOption pkgs "cgit" { };
nginx.virtualHost = lib.mkOption {
description = "VirtualHost to serve cgit on, defaults to the attribute name.";
type = lib.types.str;
default = config._module.args.name;
example = "git.example.com";
};
nginx.location = lib.mkOption {
description = "Location to serve cgit under.";
type = lib.types.str;
default = "/";
example = "/git/";
};
repos = lib.mkOption {
description = "cgit repository settings, see {manpage}`cgitrc(5)`";
type = with lib.types; attrsOf (attrsOf settingType);
default = { };
example = {
blah = {
path = "/var/lib/git/example";
desc = "An example repository";
};
};
};
scanPath = lib.mkOption {
description = "A path which will be scanned for repositories.";
type = lib.types.nullOr lib.types.path;
default = null;
example = "/var/lib/git";
};
settings = lib.mkOption {
description = "cgit configuration, see {manpage}`cgitrc(5)`";
type = lib.types.attrsOf repeatedSettingType;
default = { };
example = lib.literalExpression ''
{
enable-follow-links = true;
source-filter = "''${pkgs.cgit}/lib/cgit/filters/syntax-highlighting.py";
}
'';
};
extraConfig = lib.mkOption {
description = "These lines go to the end of cgitrc verbatim.";
type = lib.types.lines;
default = "";
};
user = lib.mkOption {
description = "User to run the cgit service as.";
type = lib.types.str;
default = "cgit";
};
group = lib.mkOption {
description = "Group to run the cgit service as.";
type = lib.types.str;
default = "cgit";
};
};
}
)
);
};
};
config = lib.mkIf (lib.any (cfg: cfg.enable) (lib.attrValues cfgs)) {
assertions = lib.mapAttrsToList (vhost: cfg: {
assertion = !cfg.enable || (cfg.scanPath == null) != (cfg.repos == { });
message = "Exactly one of services.cgit.${vhost}.scanPath or services.cgit.${vhost}.repos must be set.";
}) cfgs;
users = lib.mkMerge (
lib.flip lib.mapAttrsToList cfgs (
_: cfg: {
users.${cfg.user} = {
isSystemUser = true;
inherit (cfg) group;
};
groups.${cfg.group} = { };
}
)
);
services.fcgiwrap.instances = lib.flip lib.mapAttrs' cfgs (
name: cfg:
lib.nameValuePair "cgit-${name}" {
process = { inherit (cfg) user group; };
socket = { inherit (config.services.nginx) user group; };
}
);
systemd.services = lib.flip lib.mapAttrs' cfgs (
name: cfg:
lib.nameValuePair (fcgiwrapUnitName name) (
lib.mkIf (cfg.repos != { }) {
serviceConfig.RuntimeDirectory = fcgiwrapUnitName name;
preStart = ''
GIT_PROJECT_ROOT=${lib.escapeShellArg (gitProjectRoot name cfg)}
mkdir -p "$GIT_PROJECT_ROOT"
cd "$GIT_PROJECT_ROOT"
${lib.concatLines (
lib.flip lib.mapAttrsToList cfg.repos (
name: repo: ''
ln -s ${lib.escapeShellArg repo.path} ${lib.escapeShellArg name}
''
)
)}
'';
}
)
);
services.nginx.enable = true;
services.nginx.virtualHosts = lib.mkMerge (
lib.mapAttrsToList (name: cfg: {
${cfg.nginx.virtualHost} = {
locations =
(genAttrs' [ "cgit.css" "cgit.png" "favicon.ico" "robots.txt" ] (
fileName:
lib.nameValuePair "= ${stripLocation cfg}/${fileName}" {
alias = lib.mkDefault "${cfg.package}/cgit/${fileName}";
}
))
// {
"~ ${regexLocation cfg}/.+/(info/refs|git-upload-pack)" = {
fastcgiParams = rec {
SCRIPT_FILENAME = "${pkgs.git}/libexec/git-core/git-http-backend";
GIT_HTTP_EXPORT_ALL = "1";
GIT_PROJECT_ROOT = gitProjectRoot name cfg;
HOME = GIT_PROJECT_ROOT;
};
extraConfig = mkFastcgiPass name cfg;
};
"${stripLocation cfg}/" = {
fastcgiParams = {
SCRIPT_FILENAME = "${cfg.package}/cgit/cgit.cgi";
QUERY_STRING = "$args";
HTTP_HOST = "$server_name";
CGIT_CONFIG = mkCgitrc cfg;
};
extraConfig = mkFastcgiPass name cfg;
};
};
};
}) cfgs
);
};
}

View File

@@ -0,0 +1,126 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
mkEnableOption
mkIf
mkOption
singleton
types
;
inherit (pkgs) coreutils charybdis;
cfg = config.services.charybdis;
configFile = pkgs.writeText "charybdis.conf" ''
${cfg.config}
'';
in
{
###### interface
options = {
services.charybdis = {
enable = mkEnableOption "Charybdis IRC daemon";
config = mkOption {
type = types.str;
description = ''
Charybdis IRC daemon configuration file.
'';
};
statedir = mkOption {
type = types.path;
default = "/var/lib/charybdis";
description = ''
Location of the state directory of charybdis.
'';
};
user = mkOption {
type = types.str;
default = "ircd";
description = ''
Charybdis IRC daemon user.
'';
};
group = mkOption {
type = types.str;
default = "ircd";
description = ''
Charybdis IRC daemon group.
'';
};
motd = mkOption {
type = types.nullOr types.lines;
default = null;
description = ''
Charybdis MOTD text.
Charybdis will read its MOTD from /etc/charybdis/ircd.motd .
If set, the value of this option will be written to this path.
'';
};
};
};
###### implementation
config = mkIf cfg.enable (
lib.mkMerge [
{
users.users.${cfg.user} = {
description = "Charybdis IRC daemon user";
uid = config.ids.uids.ircd;
group = cfg.group;
};
users.groups.${cfg.group} = {
gid = config.ids.gids.ircd;
};
systemd.tmpfiles.settings."10-charybdis".${cfg.statedir}.d = {
inherit (cfg) user group;
};
environment.etc."charybdis/ircd.conf".source = configFile;
systemd.services.charybdis = {
description = "Charybdis IRC daemon";
wantedBy = [ "multi-user.target" ];
reloadIfChanged = true;
restartTriggers = [
configFile
];
environment = {
BANDB_DBPATH = "${cfg.statedir}/ban.db";
};
serviceConfig = {
ExecStart = "${charybdis}/bin/charybdis -foreground -logfile /dev/stdout -configfile /etc/charybdis/ircd.conf";
ExecReload = "${coreutils}/bin/kill -HUP $MAINPID";
Group = cfg.group;
User = cfg.user;
};
};
}
(mkIf (cfg.motd != null) {
environment.etc."charybdis/ircd.motd".text = cfg.motd;
})
]
);
}

View File

@@ -0,0 +1,108 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.chisel-server;
in
{
options = {
services.chisel-server = {
enable = lib.mkEnableOption "Chisel Tunnel Server";
host = lib.mkOption {
description = "Address to listen on, falls back to 0.0.0.0";
type = with lib.types; nullOr str;
default = null;
example = "[::1]";
};
port = lib.mkOption {
description = "Port to listen on, falls back to 8080";
type = with lib.types; nullOr port;
default = null;
};
authfile = lib.mkOption {
description = "Path to auth.json file";
type = with lib.types; nullOr path;
default = null;
};
keepalive = lib.mkOption {
description = "Keepalive interval, falls back to 25s";
type = with lib.types; nullOr str;
default = null;
example = "5s";
};
backend = lib.mkOption {
description = "HTTP server to proxy normal requests to";
type = with lib.types; nullOr str;
default = null;
example = "http://127.0.0.1:8888";
};
socks5 = lib.mkOption {
description = "Allow clients access to internal SOCKS5 proxy";
type = lib.types.bool;
default = false;
};
reverse = lib.mkOption {
description = "Allow clients reverse port forwarding";
type = lib.types.bool;
default = false;
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.chisel-server = {
description = "Chisel Tunnel Server";
wantedBy = [ "network-online.target" ];
serviceConfig = {
ExecStart =
"${pkgs.chisel}/bin/chisel server "
+ lib.concatStringsSep " " (
lib.optional (cfg.host != null) "--host ${cfg.host}"
++ lib.optional (cfg.port != null) "--port ${builtins.toString cfg.port}"
++ lib.optional (cfg.authfile != null) "--authfile ${cfg.authfile}"
++ lib.optional (cfg.keepalive != null) "--keepalive ${cfg.keepalive}"
++ lib.optional (cfg.backend != null) "--backend ${cfg.backend}"
++ lib.optional cfg.socks5 "--socks5"
++ lib.optional cfg.reverse "--reverse"
);
# Security Hardening
# Refer to systemd.exec(5) for option descriptions.
CapabilityBoundingSet = "";
# implies RemoveIPC=, PrivateTmp=, NoNewPrivileges=, RestrictSUIDSGID=,
# ProtectSystem=strict, ProtectHome=read-only
DynamicUser = true;
LockPersonality = true;
PrivateDevices = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectProc = "invisible";
ProtectKernelModules = true;
ProtectKernelTunables = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
SystemCallFilter = "~@clock @cpu-emulation @debug @mount @obsolete @reboot @swap @privileged @resources";
UMask = "0077";
};
};
};
meta.maintainers = with lib.maintainers; [ clerie ];
}

View File

@@ -0,0 +1,328 @@
{
config,
lib,
pkgs,
...
}:
let
pkg = pkgs.cjdns;
cfg = config.services.cjdns;
connectToSubmodule =
{ ... }:
{
options = {
password = lib.mkOption {
type = lib.types.str;
description = "Authorized password to the opposite end of the tunnel.";
};
login = lib.mkOption {
default = "";
type = lib.types.str;
description = "(optional) name your peer has for you";
};
peerName = lib.mkOption {
default = "";
type = lib.types.str;
description = "(optional) human-readable name for peer";
};
publicKey = lib.mkOption {
type = lib.types.str;
description = "Public key at the opposite end of the tunnel.";
};
hostname = lib.mkOption {
default = "";
example = "foobar.hype";
type = lib.types.str;
description = "Optional hostname to add to /etc/hosts; prevents reverse lookup failures.";
};
};
};
# Additional /etc/hosts entries for peers with an associated hostname
cjdnsExtraHosts = pkgs.runCommand "cjdns-hosts" { } ''
exec >$out
${lib.concatStringsSep "\n" (
lib.mapAttrsToList (
k: v:
lib.optionalString (
v.hostname != ""
) "echo $(${pkgs.cjdns}/bin/cjdnstool util key2ip6 ${v.publicKey}) ${v.hostname}"
) (cfg.ETHInterface.connectTo // cfg.UDPInterface.connectTo)
)}
'';
parseModules =
x:
x
// {
connectTo = lib.mapAttrs (name: value: { inherit (value) password publicKey; }) x.connectTo;
};
cjdrouteConf = builtins.toJSON (
lib.recursiveUpdate {
admin = {
bind = cfg.admin.bind;
password = "@CJDNS_ADMIN_PASSWORD@";
};
authorizedPasswords = map (p: { password = p; }) cfg.authorizedPasswords;
interfaces = {
ETHInterface = if (cfg.ETHInterface.bind != "") then [ (parseModules cfg.ETHInterface) ] else [ ];
UDPInterface = if (cfg.UDPInterface.bind != "") then [ (parseModules cfg.UDPInterface) ] else [ ];
};
privateKey = "@CJDNS_PRIVATE_KEY@";
resetAfterInactivitySeconds = 100;
router = {
interface = {
type = "TUNInterface";
};
ipTunnel = {
allowedConnections = [ ];
outgoingConnections = [ ];
};
};
security = [
{
exemptAngel = 1;
setuser = "nobody";
}
];
} cfg.extraConfig
);
in
{
options = {
services.cjdns = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable the cjdns network encryption
and routing engine. A file at /etc/cjdns.keys will
be created if it does not exist to contain a random
secret key that your IPv6 address will be derived from.
'';
};
extraConfig = lib.mkOption {
type = lib.types.attrs;
default = { };
example = {
router.interface.tunDevice = "tun10";
};
description = ''
Extra configuration, given as attrs, that will be merged recursively
with the rest of the JSON generated by this module, at the root node.
'';
};
confFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/etc/cjdroute.conf";
description = ''
Ignore all other cjdns options and load configuration from this file.
'';
};
authorizedPasswords = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"snyrfgkqsc98qh1y4s5hbu0j57xw5s0"
"z9md3t4p45mfrjzdjurxn4wuj0d8swv"
"49275fut6tmzu354pq70sr5b95qq0vj"
];
description = ''
Any remote cjdns nodes that offer these passwords on
connection will be allowed to route through this node.
'';
};
admin = {
bind = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1:11234";
description = ''
Bind the administration port to this address and port.
'';
};
};
UDPInterface = {
bind = lib.mkOption {
type = lib.types.str;
default = "";
example = "192.168.1.32:43211";
description = ''
Address and port to bind UDP tunnels to.
'';
};
connectTo = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule connectToSubmodule);
default = { };
example = lib.literalExpression ''
{
"192.168.1.1:27313" = {
hostname = "homer.hype";
password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM";
publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k";
};
}
'';
description = ''
Credentials for making UDP tunnels.
'';
};
};
ETHInterface = {
bind = lib.mkOption {
type = lib.types.str;
default = "";
example = "eth0";
description = ''
Bind to this device for native ethernet operation.
`all` is a pseudo-name which will try to connect to all devices.
'';
};
beacon = lib.mkOption {
type = lib.types.int;
default = 2;
description = ''
Auto-connect to other cjdns nodes on the same network.
Options:
0: Disabled.
1: Accept beacons, this will cause cjdns to accept incoming
beacon messages and try connecting to the sender.
2: Accept and send beacons, this will cause cjdns to broadcast
messages on the local network which contain a randomly
generated per-session password, other nodes which have this
set to 1 or 2 will hear the beacon messages and connect
automatically.
'';
};
connectTo = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule connectToSubmodule);
default = { };
example = lib.literalExpression ''
{
"01:02:03:04:05:06" = {
hostname = "homer.hype";
password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM";
publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k";
};
}
'';
description = ''
Credentials for connecting look similar to UDP credientials
except they begin with the mac address.
'';
};
};
addExtraHosts = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to add cjdns peers with an associated hostname to
{file}`/etc/hosts`. Beware that enabling this
incurs heavy eval-time costs.
'';
};
};
};
config = lib.mkIf cfg.enable {
boot.kernelModules = [ "tun" ];
# networking.firewall.allowedUDPPorts = ...
systemd.services.cjdns = {
description = "cjdns: routing engine designed for security, scalability, speed and ease of use";
wantedBy = [
"multi-user.target"
"sleep.target"
];
after = [ "network-online.target" ];
bindsTo = [ "network-online.target" ];
preStart = lib.optionalString (cfg.confFile == null) ''
[ -e /etc/cjdns.keys ] && source /etc/cjdns.keys
if [ -z "$CJDNS_PRIVATE_KEY" ]; then
shopt -s lastpipe
${pkg}/bin/cjdnstool util keygen | { read private ipv6 public; }
install -m 600 <(echo "CJDNS_PRIVATE_KEY=$private") /etc/cjdns.keys
install -m 444 <(echo -e "CJDNS_IPV6=$ipv6\nCJDNS_PUBLIC_KEY=$public") /etc/cjdns.public
fi
if [ -z "$CJDNS_ADMIN_PASSWORD" ]; then
echo "CJDNS_ADMIN_PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 32)" \
>> /etc/cjdns.keys
fi
'';
script = (
if cfg.confFile != null then
"${pkg}/bin/cjdroute < ${cfg.confFile}"
else
''
source /etc/cjdns.keys
(cat <<'EOF'
${cjdrouteConf}
EOF
) | sed \
-e "s/@CJDNS_ADMIN_PASSWORD@/$CJDNS_ADMIN_PASSWORD/g" \
-e "s/@CJDNS_PRIVATE_KEY@/$CJDNS_PRIVATE_KEY/g" \
| ${pkg}/bin/cjdroute
''
);
startLimitIntervalSec = 0;
serviceConfig = {
Type = "forking";
Restart = "always";
RestartSec = 1;
CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_RAW CAP_SETUID";
ProtectSystem = true;
# Doesn't work on i686, causing service to fail
MemoryDenyWriteExecute = !pkgs.stdenv.hostPlatform.isi686;
ProtectHome = true;
PrivateTmp = true;
};
};
networking.hostFiles = lib.mkIf cfg.addExtraHosts [ cjdnsExtraHosts ];
assertions = [
{
assertion = (cfg.ETHInterface.bind != "" || cfg.UDPInterface.bind != "" || cfg.confFile != null);
message = "Neither cjdns.ETHInterface.bind nor cjdns.UDPInterface.bind defined.";
}
{
assertion = config.networking.enableIPv6;
message = "networking.enableIPv6 must be enabled for CJDNS to work";
}
];
};
}

View File

@@ -0,0 +1,105 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.clatd;
settingsFormat = pkgs.formats.keyValue { };
configFile = settingsFormat.generate "clatd.conf" cfg.settings;
in
{
options = {
services.clatd = {
enable = lib.mkEnableOption "clatd";
package = lib.mkPackageOption pkgs "clatd" { };
enableNetworkManagerIntegration = lib.mkEnableOption "NetworkManager integration" // {
default = config.networking.networkmanager.enable;
defaultText = "config.networking.networkmanager.enable";
};
settings = lib.mkOption {
type = lib.types.submodule (
{ name, ... }:
{
freeformType = settingsFormat.type;
}
);
default = { };
example = lib.literalExpression ''
{
plat-prefix = "64:ff9b::/96";
}
'';
description = ''
Configuration of clatd. See [clatd Documentation](https://github.com/toreanderson/clatd/blob/master/README.pod#configuration).
'';
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.clatd = {
description = "464XLAT CLAT daemon";
documentation = [ "man:clatd(8)" ];
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
startLimitIntervalSec = 0;
serviceConfig = {
ExecStart = "${cfg.package}/bin/clatd -c ${configFile}";
# Hardening
CapabilityBoundingSet = [
"CAP_NET_ADMIN"
];
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateTmp = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectProc = "invisible";
ProtectSystem = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@network-io"
"@system-service"
"~@privileged"
"~@resources"
];
};
};
networking.networkmanager.dispatcherScripts = lib.optionals cfg.enableNetworkManagerIntegration [
{
type = "basic";
# https://github.com/toreanderson/clatd/blob/master/scripts/clatd.networkmanager
source = pkgs.writeShellScript "restart-clatd" ''
[ "$DEVICE_IFACE" = "${cfg.settings.clat-dev or "clat"}" ] && exit 0
[ "$2" != "up" ] && [ "$2" != "down" ] && exit 0
${pkgs.systemd}/bin/systemctl restart clatd.service
'';
}
];
};
}

View File

@@ -0,0 +1,327 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.cloudflare-ddns;
boolToString = b: if b then "true" else "false";
formatList = l: lib.concatStringsSep "," l;
formatDuration = d: d.String;
in
{
options.services.cloudflare-ddns = {
enable = lib.mkEnableOption "Cloudflare Dynamic DNS service";
package = lib.mkPackageOption pkgs "cloudflare-ddns" { };
credentialsFile = lib.mkOption {
type = lib.types.path;
description = ''
Path to a file containing the Cloudflare API authentication token.
The file content should be in the format `CLOUDFLARE_API_TOKEN=YOUR_SECRET_TOKEN`.
The service user `${cfg.user}` needs read access to this file.
Ensure permissions are secure (e.g., `0400` or `0440`) and ownership is appropriate
(e.g., `owner = root`, `group = ${cfg.group}`).
Using `CLOUDFLARE_API_TOKEN` is preferred over the deprecated `CF_API_TOKEN`.
'';
example = "/run/secrets/cloudflare-ddns-token";
};
domains = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
List of domain names (FQDNs) to manage. Wildcards like `*.example.com` are supported.
These domains will be managed for both IPv4 and IPv6 unless overridden by
`ip4Domains` or `ip6Domains`, or if the respective providers are disabled.
This corresponds to the `DOMAINS` environment variable.
'';
example = [
"home.example.com"
"*.dynamic.example.org"
];
};
ip4Domains = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
description = ''
Explicit list of domains to manage only for IPv4. If set, overrides `domains` for IPv4.
Corresponds to the `IP4_DOMAINS` environment variable.
'';
example = [ "ipv4.example.com" ];
};
ip6Domains = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
description = ''
Explicit list of domains to manage only for IPv6. If set, overrides `domains` for IPv6.
Corresponds to the `IP6_DOMAINS` environment variable.
'';
example = [ "ipv6.example.com" ];
};
wafLists = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
List of WAF IP Lists to manage, in the format `account-id/list-name`.
(Experimental feature as of cloudflare-ddns 1.14.0).
'';
example = [ "YOUR_ACCOUNT_ID/allowed_dynamic_ips" ];
};
provider = {
ipv4 = lib.mkOption {
type = lib.types.str;
default = "cloudflare.trace";
description = ''
IP detection provider for IPv4. Common values: `cloudflare.trace`, `cloudflare.doh`, `local`, `url:URL`, `none`.
Use `none` to disable IPv4 updates.
See cloudflare-ddns documentation for all options.
'';
};
ipv6 = lib.mkOption {
type = lib.types.str;
default = "cloudflare.trace";
description = ''
IP detection provider for IPv6. Common values: `cloudflare.trace`, `cloudflare.doh`, `local`, `url:URL`, `none`.
Use `none` to disable IPv6 updates.
See cloudflare-ddns documentation for all options.
'';
};
};
updateCron = lib.mkOption {
type = lib.types.str;
default = "@every 5m";
description = ''
Cron expression for how often to check and update IPs.
Use "@once" to run only once and then exit.
'';
example = "@hourly";
};
updateOnStart = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to perform an update check immediately on service start.";
};
deleteOnStop = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to delete the managed DNS records and clear WAF lists when the service is stopped gracefully.
Warning: Setting this to true with `updateCron = "@once"` will cause immediate deletion.
'';
};
cacheExpiration = lib.mkOption {
type = lib.types.str;
default = "6h";
description = ''
Duration for which API responses (like Zone ID, Record IDs) are cached.
Uses Go's duration format (e.g., "6h", "1h30m").
'';
};
ttl = lib.mkOption {
type = lib.types.ints.positive;
default = 1;
description = ''
Time To Live (TTL) for the DNS records in seconds.
Must be 1 (for automatic) or between 30 and 86400.
'';
};
proxied = lib.mkOption {
type = lib.types.str;
default = "false";
description = ''
Whether the managed DNS records should be proxied through Cloudflare ('orange cloud').
Accepts boolean values (`true`, `false`) or a domain expression.
See cloudflare-ddns documentation for expression syntax (e.g., "is(a.com) || sub(b.org)").
'';
example = "true";
};
recordComment = lib.mkOption {
type = lib.types.str;
default = "";
description = "Comment to add to managed DNS records.";
};
wafListDescription = lib.mkOption {
type = lib.types.str;
default = "";
description = "Description for managed WAF lists (used when creating or verifying lists).";
};
detectionTimeout = lib.mkOption {
type = lib.types.str;
default = "5s";
description = "Timeout for detecting the public IP address.";
};
updateTimeout = lib.mkOption {
type = lib.types.str;
default = "30s";
description = "Timeout for updating records via the Cloudflare API.";
};
healthchecks = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "URL for Healthchecks.io monitoring endpoint (optional).";
example = "https://hc-ping.com/your-uuid";
};
uptimeKuma = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "URL for Uptime Kuma push monitor endpoint (optional).";
example = "https://status.example.com/api/push/tag?status=up&msg=OK&ping=";
};
shoutrrr = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
description = "List of Shoutrrr notification service URLs (optional).";
example = [
"discord://token@id"
"gotify://host/token"
];
};
user = lib.mkOption {
type = lib.types.str;
default = "cloudflare-ddns";
description = "User account under which the service runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "cloudflare-ddns";
description = "Group under which the service runs.";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.ttl == 1 || (cfg.ttl >= 30 && cfg.ttl <= 86400);
message = "services.cloudflare-ddns.ttl must be 1 or between 30 and 86400";
}
{
assertion = cfg.updateCron == "@once" -> !cfg.deleteOnStop;
message = "services.cloudflare-ddns.deleteOnStop cannot be true when updateCron is \"@once\"";
}
{
assertion =
cfg.domains != [ ] || cfg.ip4Domains != null || cfg.ip6Domains != null || cfg.wafLists != [ ];
message = "services.cloudflare-ddns requires at least one domain (domains, ip4Domains, ip6Domains) or WAF list (wafLists) to be specified";
}
{
assertion = cfg.provider.ipv4 != "none" || cfg.provider.ipv6 != "none";
message = "services.cloudflare-ddns requires at least one provider (ipv4 or ipv6) to be enabled (not 'none')";
}
];
users.users.${cfg.user} = {
description = "Cloudflare DDNS service user";
isSystemUser = true;
group = cfg.group;
home = "/var/lib/${cfg.user}";
};
users.groups.${cfg.group} = { };
systemd.tmpfiles.settings."cloudflare-ddns" = {
"/var/lib/${cfg.user}".d = {
mode = "0750";
user = cfg.user;
group = cfg.group;
};
};
systemd.services.cloudflare-ddns = {
description = "Cloudflare Dynamic DNS Client Service (favonia)";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
User = cfg.user;
Group = cfg.group;
WorkingDirectory = "/var/lib/${cfg.user}";
EnvironmentFile = cfg.credentialsFile;
Environment =
let
toEnv = name: value: "${name}=\"${toString value}\"";
toEnvList = name: value: "${name}=\"${formatList value}\"";
toEnvDuration = name: value: "${name}=\"${formatDuration value}\"";
toEnvBool = name: value: "${name}=\"${boolToString value}\"";
toEnvMaybe =
pred: name: value:
lib.optionalString pred (toEnv name value);
toEnvMaybeList =
pred: name: value:
lib.optionalString pred (toEnvList name value);
in
lib.filter (envVar: envVar != "") [
(toEnvList "DOMAINS" cfg.domains)
(toEnvMaybeList (cfg.ip4Domains != null) "IP4_DOMAINS" cfg.ip4Domains)
(toEnvMaybeList (cfg.ip6Domains != null) "IP6_DOMAINS" cfg.ip6Domains)
(toEnv "IP4_PROVIDER" cfg.provider.ipv4)
(toEnv "IP6_PROVIDER" cfg.provider.ipv6)
(toEnvMaybeList (cfg.wafLists != [ ]) "WAF_LISTS" cfg.wafLists)
(toEnvMaybe (cfg.wafListDescription != "") "WAF_LIST_DESCRIPTION" cfg.wafListDescription)
(toEnv "UPDATE_CRON" cfg.updateCron)
(toEnvBool "UPDATE_ON_START" cfg.updateOnStart)
(toEnvBool "DELETE_ON_STOP" cfg.deleteOnStop)
(toEnv "CACHE_EXPIRATION" cfg.cacheExpiration)
(toEnv "TTL" cfg.ttl)
(toEnv "PROXIED" cfg.proxied)
(toEnvMaybe (cfg.recordComment != "") "RECORD_COMMENT" cfg.recordComment)
(toEnv "DETECTION_TIMEOUT" cfg.detectionTimeout)
(toEnv "UPDATE_TIMEOUT" cfg.updateTimeout)
(toEnvMaybe (cfg.healthchecks != null) "HEALTHCHECKS" cfg.healthchecks)
(toEnvMaybe (cfg.uptimeKuma != null) "UPTIMEKUMA" cfg.uptimeKuma)
(toEnvMaybeList (cfg.shoutrrr != null) "SHOUTRRR" (lib.concatStringsSep "\n" cfg.shoutrrr))
];
ExecStart = lib.getExe cfg.package;
Restart = "on-failure";
RestartSec = "30s";
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
NoNewPrivileges = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
};
};
};
}

View File

@@ -0,0 +1,130 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.cloudflare-dyndns;
in
{
options = {
services.cloudflare-dyndns = {
enable = lib.mkEnableOption "Cloudflare Dynamic DNS Client";
package = lib.mkPackageOption pkgs "cloudflare-dyndns" { };
apiTokenFile = lib.mkOption {
type = lib.types.pathWith {
absolute = true;
inStore = false;
};
description = ''
The path to a file containing the CloudFlare API token.
'';
};
domains = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
List of domain names to update records for.
'';
};
frequency = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = "*:0/5";
description = ''
Run cloudflare-dyndns with the given frequency (see
{manpage}`systemd.time(7)` for the format).
If null, do not run automatically.
'';
};
proxied = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether this is a DNS-only record, or also being proxied through CloudFlare.
'';
};
ipv4 = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to enable setting IPv4 A records.
'';
};
ipv6 = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable setting IPv6 AAAA records.
'';
};
deleteMissing = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to delete the record when no IP address is found.
'';
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.cloudflare-dyndns = {
description = "CloudFlare Dynamic DNS Client";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
CLOUDFLARE_DOMAINS = toString cfg.domains;
};
serviceConfig = {
Type = "simple";
DynamicUser = true;
StateDirectory = "cloudflare-dyndns";
Environment = [ "XDG_CACHE_HOME=%S/cloudflare-dyndns/.cache" ];
LoadCredential = [
"apiToken:${cfg.apiTokenFile}"
];
};
script =
let
args = [
"--cache-file /var/lib/cloudflare-dyndns/ip.cache"
]
++ (if cfg.ipv4 then [ "-4" ] else [ "-no-4" ])
++ (if cfg.ipv6 then [ "-6" ] else [ "-no-6" ])
++ lib.optional cfg.deleteMissing "--delete-missing"
++ lib.optional cfg.proxied "--proxied";
in
''
export CLOUDFLARE_API_TOKEN_FILE=''${CREDENTIALS_DIRECTORY}/apiToken
# Added 2025-03-10: `cfg.apiTokenFile` used to be passed as an
# `EnvironmentFile` to the service, which required it to be of
# the form "CLOUDFLARE_API_TOKEN=" rather than just the secret.
# If we detect this legacy usage, error out.
token=$(< "''${CLOUDFLARE_API_TOKEN_FILE}")
if [[ $token == CLOUDFLARE_API_TOKEN* ]]; then
echo "Error: your api token starts with 'CLOUDFLARE_API_TOKEN='. Remove that, and instead specify just the token." >&2
exit 1
fi
exec ${lib.getExe cfg.package} ${toString args}
'';
}
// lib.optionalAttrs (cfg.frequency != null) {
startAt = cfg.frequency;
};
};
}

View File

@@ -0,0 +1,99 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.cloudflare-warp;
in
{
options.services.cloudflare-warp = {
enable = lib.mkEnableOption "Cloudflare Zero Trust client daemon";
package = lib.mkPackageOption pkgs "cloudflare-warp" { };
rootDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/cloudflare-warp";
description = ''
Working directory for the warp-svc daemon.
'';
};
udpPort = lib.mkOption {
type = lib.types.port;
default = 2408;
description = ''
The UDP port to open in the firewall. Warp uses port 2408 by default, but fallback ports can be used
if that conflicts with another service. See the [firewall documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/deployment/firewall#warp-udp-ports)
for the pre-configured available fallback ports.
'';
};
openFirewall = lib.mkEnableOption "opening UDP ports in the firewall" // {
default = true;
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
networking.firewall = lib.mkIf cfg.openFirewall {
allowedUDPPorts = [ cfg.udpPort ];
};
systemd.tmpfiles.rules = [
"d ${cfg.rootDir} - root root"
"z ${cfg.rootDir} - root root"
];
systemd.services.cloudflare-warp = {
enable = true;
description = "Cloudflare Zero Trust Client Daemon";
# lsof is used by the service to determine which UDP port to bind to
# in the case that it detects collisions.
path = [ pkgs.lsof ];
requires = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig =
let
caps = [
"CAP_NET_ADMIN"
"CAP_NET_BIND_SERVICE"
"CAP_SYS_PTRACE"
];
in
{
Type = "simple";
ExecStart = "${cfg.package}/bin/warp-svc";
ReadWritePaths = [
"${cfg.rootDir}"
"/etc/resolv.conf"
];
CapabilityBoundingSet = caps;
AmbientCapabilities = caps;
Restart = "always";
RestartSec = 5;
Environment = [ "RUST_BACKTRACE=full" ];
WorkingDirectory = cfg.rootDir;
# See the systemd.exec docs for the canonicalized paths, the service
# makes use of them for logging, and account state info tracking.
# https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#RuntimeDirectory=
StateDirectory = "cloudflare-warp";
RuntimeDirectory = "cloudflare-warp";
LogsDirectory = "cloudflare-warp";
# The service needs to write to /etc/resolv.conf to configure DNS, so that file would have to
# be world read/writable to run as anything other than root.
User = "root";
Group = "root";
};
};
};
meta.maintainers = with lib.maintainers; [ treyfortmuller ];
}

View File

@@ -0,0 +1,389 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.cloudflared;
certificateFile = lib.mkOption {
type = with lib.types; nullOr path;
description = ''
Account certificate file, necessary to create, delete and manage tunnels. It can be obtained by running `cloudflared login`.
Note that this is **necessary** for a fully declarative set up, as routes can not otherwise be created outside of the Cloudflare interface.
See [Cert.pem](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-useful-terms/#certpem) for information about the file, and [Tunnel permissions](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/local-management/tunnel-permissions/) for a comparison between the account certificate and the tunnel credentials file.
'';
default = null;
};
originRequest = {
connectTimeout = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
example = "30s";
description = ''
Timeout for establishing a new TCP connection to your origin server. This excludes the time taken to establish TLS, which is controlled by [tlsTimeout](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/local-management/ingress/#tlstimeout).
'';
};
tlsTimeout = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
example = "10s";
description = ''
Timeout for completing a TLS handshake to your origin server, if you have chosen to connect Tunnel to an HTTPS server.
'';
};
tcpKeepAlive = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
example = "30s";
description = ''
The timeout after which a TCP keepalive packet is sent on a connection between Tunnel and the origin server.
'';
};
noHappyEyeballs = lib.mkOption {
type = with lib.types; nullOr bool;
default = null;
example = false;
description = ''
Disable the happy eyeballs algorithm for IPv4/IPv6 fallback if your local network has misconfigured one of the protocols.
'';
};
keepAliveConnections = lib.mkOption {
type = with lib.types; nullOr int;
default = null;
example = 100;
description = ''
Maximum number of idle keepalive connections between Tunnel and your origin. This does not restrict the total number of concurrent connections.
'';
};
keepAliveTimeout = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
example = "1m30s";
description = ''
Timeout after which an idle keepalive connection can be discarded.
'';
};
httpHostHeader = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
example = "";
description = ''
Sets the HTTP `Host` header on requests sent to the local service.
'';
};
originServerName = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
example = "";
description = ''
Hostname that `cloudflared` should expect from your origin server certificate.
'';
};
caPool = lib.mkOption {
type = with lib.types; nullOr (either str path);
default = null;
example = "";
description = ''
Path to the certificate authority (CA) for the certificate of your origin. This option should be used only if your certificate is not signed by Cloudflare.
'';
};
noTLSVerify = lib.mkOption {
type = with lib.types; nullOr bool;
default = null;
example = false;
description = ''
Disables TLS verification of the certificate presented by your origin. Will allow any certificate from the origin to be accepted.
'';
};
disableChunkedEncoding = lib.mkOption {
type = with lib.types; nullOr bool;
default = null;
example = false;
description = ''
Disables chunked transfer encoding. Useful if you are running a WSGI server.
'';
};
proxyAddress = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
example = "127.0.0.1";
description = ''
`cloudflared` starts a proxy server to translate HTTP traffic into TCP when proxying, for example, SSH or RDP. This configures the listen address for that proxy.
'';
};
proxyPort = lib.mkOption {
type = with lib.types; nullOr int;
default = null;
example = 0;
description = ''
`cloudflared` starts a proxy server to translate HTTP traffic into TCP when proxying, for example, SSH or RDP. This configures the listen port for that proxy. If set to zero, an unused port will randomly be chosen.
'';
};
proxyType = lib.mkOption {
type =
with lib.types;
nullOr (enum [
""
"socks"
]);
default = null;
example = "";
description = ''
`cloudflared` starts a proxy server to translate HTTP traffic into TCP when proxying, for example, SSH or RDP. This configures what type of proxy will be started. Valid options are:
- `""` for the regular proxy
- `"socks"` for a SOCKS5 proxy. Refer to the [tutorial on connecting through Cloudflare Access using kubectl](https://developers.cloudflare.com/cloudflare-one/tutorials/kubectl/) for more information.
'';
};
};
in
{
imports = [
(lib.mkRemovedOptionModule
[
"services"
"cloudflared"
"user"
]
''
Cloudflared now uses a dynamic user, and this option no longer has any effect.
If the user is still necessary, please define it manually using users.users.cloudflared.
''
)
(lib.mkRemovedOptionModule
[
"services"
"cloudflared"
"group"
]
''
Cloudflared now uses a dynamic user, and this option no longer has any effect.
If the group is still necessary, please define it manually using users.groups.cloudflared.
''
)
];
options.services.cloudflared = {
inherit certificateFile;
enable = lib.mkEnableOption "Cloudflare Tunnel client daemon (formerly Argo Tunnel)";
package = lib.mkPackageOption pkgs "cloudflared" { };
tunnels = lib.mkOption {
description = ''
Cloudflare tunnels.
'';
type = lib.types.attrsOf (
lib.types.submodule (
{ name, ... }:
{
options = {
inherit certificateFile originRequest;
credentialsFile = lib.mkOption {
type = lib.types.path;
description = ''
Credential file.
See [Credentials file](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-useful-terms/#credentials-file).
'';
};
warp-routing = {
enabled = lib.mkOption {
type = with lib.types; nullOr bool;
default = null;
description = ''
Enable warp routing.
See [Connect from WARP to a private network on Cloudflare using Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/tutorials/warp-to-tunnel/).
'';
};
};
default = lib.mkOption {
type = lib.types.str;
description = ''
Catch-all service if no ingress matches.
See `service`.
'';
example = "http_status:404";
};
ingress = lib.mkOption {
type =
with lib.types;
attrsOf (
either str (
submodule (
{ hostname, ... }:
{
options = {
inherit originRequest;
service = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = ''
Service to pass the traffic.
See [Supported protocols](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/local-management/ingress/#supported-protocols).
'';
example = "http://localhost:80, tcp://localhost:8000, unix:/home/production/echo.sock, hello_world or http_status:404";
};
path = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = ''
Path filter.
If not specified, all paths will be matched.
'';
example = "/*.(jpg|png|css|js)";
};
};
}
)
)
);
default = { };
description = ''
Ingress rules.
See [Ingress rules](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/local-management/ingress/).
'';
example = {
"*.domain.com" = "http://localhost:80";
"*.anotherone.com" = "http://localhost:80";
};
};
};
}
)
);
default = { };
example = {
"00000000-0000-0000-0000-000000000000" = {
credentialsFile = "/tmp/test";
ingress = {
"*.domain1.com" = {
service = "http://localhost:80";
};
};
default = "http_status:404";
};
};
};
};
config = lib.mkIf cfg.enable {
systemd.targets = lib.mapAttrs' (
name: tunnel:
lib.nameValuePair "cloudflared-tunnel-${name}" {
description = "Cloudflare tunnel '${name}' target";
requires = [ "cloudflared-tunnel-${name}.service" ];
after = [ "cloudflared-tunnel-${name}.service" ];
unitConfig.StopWhenUnneeded = true;
}
) config.services.cloudflared.tunnels;
systemd.services = lib.mapAttrs' (
name: tunnel:
let
filterConfig = lib.attrsets.filterAttrsRecursive (
_: v:
!builtins.elem v [
null
[ ]
{ }
]
);
filterIngressSet = lib.filterAttrs (_: v: builtins.typeOf v == "set");
filterIngressStr = lib.filterAttrs (_: v: builtins.typeOf v == "string");
ingressesSet = filterIngressSet tunnel.ingress;
ingressesStr = filterIngressStr tunnel.ingress;
fullConfig = filterConfig {
tunnel = name;
credentials-file = "/run/credentials/cloudflared-tunnel-${name}.service/credentials.json";
warp-routing = filterConfig tunnel.warp-routing;
originRequest = filterConfig tunnel.originRequest;
ingress =
(map (
key:
{
hostname = key;
}
// lib.getAttr key (filterConfig (filterConfig ingressesSet))
) (lib.attrNames ingressesSet))
++ (map (key: {
hostname = key;
service = lib.getAttr key ingressesStr;
}) (lib.attrNames ingressesStr))
++ [ { service = tunnel.default; } ];
};
mkConfigFile = pkgs.writeText "cloudflared.yml" (builtins.toJSON fullConfig);
certFile = if (tunnel.certificateFile != null) then tunnel.certificateFile else cfg.certificateFile;
in
lib.nameValuePair "cloudflared-tunnel-${name}" {
after = [
"network.target"
"network-online.target"
];
wants = [
"network.target"
"network-online.target"
];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
RuntimeDirectory = "cloudflared-tunnel-${name}";
RuntimeDirectoryMode = "0400";
LoadCredential = [
"credentials.json:${tunnel.credentialsFile}"
]
++ (lib.optional (certFile != null) "cert.pem:${certFile}");
ExecStart = "${cfg.package}/bin/cloudflared tunnel --config=${mkConfigFile} --no-autoupdate run";
Restart = "on-failure";
DynamicUser = true;
};
environment.TUNNEL_ORIGIN_CERT = lib.mkIf (certFile != null) ''%d/cert.pem'';
}
) config.services.cloudflared.tunnels;
};
meta.maintainers = with lib.maintainers; [
bbigras
anpin
];
}

View File

@@ -0,0 +1,132 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.cntlm;
configFile =
if cfg.configText != "" then
pkgs.writeText "cntlm.conf" ''
${cfg.configText}
''
else
pkgs.writeText "lighttpd.conf" ''
# Cntlm Authentication Proxy Configuration
Username ${cfg.username}
Domain ${cfg.domain}
Password ${cfg.password}
${lib.optionalString (cfg.netbios_hostname != "") "Workstation ${cfg.netbios_hostname}"}
${lib.concatMapStrings (entry: "Proxy ${entry}\n") cfg.proxy}
${lib.optionalString (cfg.noproxy != [ ]) "NoProxy ${lib.concatStringsSep ", " cfg.noproxy}"}
${lib.concatMapStrings (port: ''
Listen ${toString port}
'') cfg.port}
${cfg.extraConfig}
'';
in
{
options.services.cntlm = {
enable = lib.mkEnableOption "cntlm, which starts a local proxy";
username = lib.mkOption {
type = lib.types.str;
description = ''
Proxy account name, without the possibility to include domain name ('at' sign is interpreted literally).
'';
};
domain = lib.mkOption {
type = lib.types.str;
description = "Proxy account domain/workgroup name.";
};
password = lib.mkOption {
default = "/etc/cntlm.password";
type = lib.types.str;
description = "Proxy account password. Note: use chmod 0600 on /etc/cntlm.password for security.";
};
netbios_hostname = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
The hostname of your machine.
'';
};
proxy = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = ''
A list of NTLM/NTLMv2 authenticating HTTP proxies.
Parent proxy, which requires authentication. The same as proxy on the command-line, can be used more than once to specify unlimited
number of proxies. Should one proxy fail, cntlm automatically moves on to the next one. The connect request fails only if the whole
list of proxies is scanned and (for each request) and found to be invalid. Command-line takes precedence over the configuration file.
'';
example = [ "proxy.example.com:81" ];
};
noproxy = lib.mkOption {
description = ''
A list of domains where the proxy is skipped.
'';
default = [ ];
type = lib.types.listOf lib.types.str;
example = [
"*.example.com"
"example.com"
];
};
port = lib.mkOption {
default = [ 3128 ];
type = lib.types.listOf lib.types.port;
description = "Specifies on which ports the cntlm daemon listens.";
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Additional config appended to the end of the generated {file}`cntlm.conf`.";
};
configText = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Verbatim contents of {file}`cntlm.conf`.";
};
};
###### implementation
config = lib.mkIf cfg.enable {
systemd.services.cntlm = {
description = "CNTLM is an NTLM / NTLM Session Response / NTLMv2 authenticating HTTP proxy";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "cntlm";
ExecStart = ''
${pkgs.cntlm}/bin/cntlm -U cntlm -c ${configFile} -v -f
'';
};
};
users.users.cntlm = {
name = "cntlm";
description = "cntlm system-wide daemon";
isSystemUser = true;
};
};
}

View File

@@ -0,0 +1,175 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.connman;
configFile = pkgs.writeText "connman.conf" ''
[General]
NetworkInterfaceBlacklist=${lib.concatStringsSep "," cfg.networkInterfaceBlacklist}
${cfg.extraConfig}
'';
enableIwd = cfg.wifi.backend == "iwd";
in
{
meta.maintainers = [ ];
imports = [
(lib.mkRenamedOptionModule [ "networking" "connman" ] [ "services" "connman" ])
];
###### interface
options = {
services.connman = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to use ConnMan for managing your network connections.
'';
};
package = lib.mkOption {
type = lib.types.package;
description = "The connman package / build flavor";
default = pkgs.connman;
defaultText = lib.literalExpression "pkgs.connman";
example = lib.literalExpression "pkgs.connmanFull";
};
enableVPN = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to enable ConnMan VPN service.
'';
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Configuration lines appended to the generated connman configuration file.
'';
};
networkInterfaceBlacklist = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
"vmnet"
"vboxnet"
"virbr"
"ifb"
"ve"
];
description = ''
Default blacklisted interfaces, this includes NixOS containers interfaces (ve).
'';
};
wifi = {
backend = lib.mkOption {
type = lib.types.enum [
"wpa_supplicant"
"iwd"
];
default = "wpa_supplicant";
description = ''
Specify the Wi-Fi backend used.
Currently supported are {option}`wpa_supplicant` or {option}`iwd`.
'';
};
};
extraFlags = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "--nodnsproxy" ];
description = ''
Extra flags to pass to connmand
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = !config.networking.useDHCP;
message = "You can not use services.connman with networking.useDHCP";
}
{
# TODO: connman seemingly can be used along network manager and
# connmanFull supports this - so this should be worked out somehow
assertion = !config.networking.networkmanager.enable;
message = "You can not use services.connman with networking.networkmanager";
}
];
environment.systemPackages = [ cfg.package ];
systemd.services.connman = {
description = "Connection service";
wantedBy = [ "multi-user.target" ];
after = lib.optional enableIwd "iwd.service";
requires = lib.optional enableIwd "iwd.service";
serviceConfig = {
Type = "dbus";
BusName = "net.connman";
Restart = "on-failure";
ExecStart = toString (
[
"${cfg.package}/sbin/connmand"
"--config=${configFile}"
"--nodaemon"
]
++ lib.optional enableIwd "--wifi=iwd_agent"
++ cfg.extraFlags
);
StandardOutput = "null";
};
};
systemd.services.connman-vpn = lib.mkIf cfg.enableVPN {
description = "ConnMan VPN service";
wantedBy = [ "multi-user.target" ];
before = [ "connman.service" ];
serviceConfig = {
Type = "dbus";
BusName = "net.connman.vpn";
ExecStart = "${cfg.package}/sbin/connman-vpnd -n";
StandardOutput = "null";
};
};
systemd.services.net-connman-vpn = lib.mkIf cfg.enableVPN {
description = "D-BUS Service";
serviceConfig = {
Name = "net.connman.vpn";
before = [ "connman.service" ];
ExecStart = "${cfg.package}/sbin/connman-vpnd -n";
User = "root";
SystemdService = "connman-vpn.service";
};
};
networking = {
useDHCP = false;
wireless = {
enable = lib.mkIf (!enableIwd) true;
dbusControlled = true;
iwd = lib.mkIf enableIwd {
enable = true;
};
};
networkmanager.enable = false;
};
};
}

View File

@@ -0,0 +1,303 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
dataDir = "/var/lib/consul";
cfg = config.services.consul;
configOptions = {
data_dir = dataDir;
ui_config = {
enabled = cfg.webUi;
};
}
// cfg.extraConfig;
configFiles = [
"/etc/consul.json"
"/etc/consul-addrs.json"
]
++ cfg.extraConfigFiles;
devices = lib.attrValues (lib.filterAttrs (_: i: i != null) cfg.interface);
systemdDevices = lib.forEach devices (
i: "sys-subsystem-net-devices-${utils.escapeSystemdPath i}.device"
);
in
{
options = {
services.consul = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Enables the consul daemon.
'';
};
package = lib.mkPackageOption pkgs "consul" { };
webUi = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Enables the web interface on the consul http port.
'';
};
leaveOnStop = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
If enabled, causes a leave action to be sent when closing consul.
This allows a clean termination of the node, but permanently removes
it from the cluster. You probably don't want this option unless you
are running a node which going offline in a permanent / semi-permanent
fashion.
'';
};
interface = {
advertise = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
The name of the interface to pull the advertise_addr from.
'';
};
bind = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
The name of the interface to pull the bind_addr from.
'';
};
};
forceAddrFamily = lib.mkOption {
type = lib.types.enum [
"any"
"ipv4"
"ipv6"
];
default = "any";
description = ''
Whether to bind ipv4/ipv6 or both kind of addresses.
'';
};
forceIpv4 = lib.mkOption {
type = lib.types.nullOr lib.types.bool;
default = null;
description = ''
Deprecated: Use consul.forceAddrFamily instead.
Whether we should force the interfaces to only pull ipv4 addresses.
'';
};
dropPrivileges = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether the consul agent should be run as a non-root consul user.
'';
};
extraConfig = lib.mkOption {
default = { };
type = lib.types.attrsOf lib.types.anything;
description = ''
Extra configuration options which are serialized to json and added
to the config.json file.
'';
};
extraConfigFiles = lib.mkOption {
default = [ ];
type = lib.types.listOf lib.types.str;
description = ''
Additional configuration files to pass to consul
NOTE: These will not trigger the service to be restarted when altered.
'';
};
alerts = {
enable = lib.mkEnableOption "consul-alerts";
package = lib.mkPackageOption pkgs "consul-alerts" { };
listenAddr = lib.mkOption {
description = "Api listening address.";
default = "localhost:9000";
type = lib.types.str;
};
consulAddr = lib.mkOption {
description = "Consul api listening address";
default = "localhost:8500";
type = lib.types.str;
};
watchChecks = lib.mkOption {
description = "Whether to enable check watcher.";
default = true;
type = lib.types.bool;
};
watchEvents = lib.mkOption {
description = "Whether to enable event watcher.";
default = true;
type = lib.types.bool;
};
};
};
};
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
users.users.consul = {
description = "Consul agent daemon user";
isSystemUser = true;
group = "consul";
# The shell is needed for health checks
shell = "/run/current-system/sw/bin/bash";
};
users.groups.consul = { };
environment = {
etc."consul.json".text = builtins.toJSON configOptions;
# We need consul.d to exist for consul to start
etc."consul.d/dummy.json".text = "{ }";
systemPackages = [ cfg.package ];
};
warnings = lib.flatten [
(lib.optional (cfg.forceIpv4 != null) ''
The option consul.forceIpv4 is deprecated, please use
consul.forceAddrFamily instead.
'')
];
systemd.services.consul = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ] ++ systemdDevices;
bindsTo = systemdDevices;
restartTriggers = [
config.environment.etc."consul.json".source
]
++ lib.mapAttrsToList (_: d: d.source) (
lib.filterAttrs (n: _: lib.hasPrefix "consul.d/" n) config.environment.etc
);
serviceConfig = {
ExecStart =
"@${lib.getExe cfg.package} consul agent -config-dir /etc/consul.d"
+ lib.concatMapStrings (n: " -config-file ${n}") configFiles;
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
PermissionsStartOnly = true;
User = if cfg.dropPrivileges then "consul" else null;
Restart = "on-failure";
TimeoutStartSec = "infinity";
}
// (lib.optionalAttrs (cfg.leaveOnStop) {
ExecStop = "${lib.getExe cfg.package} leave";
});
path = with pkgs; [
iproute2
gawk
cfg.package
];
preStart =
let
family =
if cfg.forceAddrFamily == "ipv6" then
"-6"
else if cfg.forceAddrFamily == "ipv4" then
"-4"
else
"";
in
''
mkdir -m 0700 -p ${dataDir}
chown -R consul ${dataDir}
# Determine interface addresses
getAddrOnce () {
ip ${family} addr show dev "$1" scope global \
| awk -F '[ /\t]*' '/inet/ {print $3}' | head -n 1
}
getAddr () {
ADDR="$(getAddrOnce $1)"
LEFT=60 # Die after 1 minute
while [ -z "$ADDR" ]; do
sleep 1
LEFT=$(expr $LEFT - 1)
if [ "$LEFT" -eq "0" ]; then
echo "Address lookup timed out"
exit 1
fi
ADDR="$(getAddrOnce $1)"
done
echo "$ADDR"
}
echo "{" > /etc/consul-addrs.json
delim=" "
''
+ lib.concatStrings (
lib.flip lib.mapAttrsToList cfg.interface (
name: i:
lib.optionalString (i != null) ''
echo "$delim \"${name}_addr\": \"$(getAddr "${i}")\"" >> /etc/consul-addrs.json
delim=","
''
)
)
+ ''
echo "}" >> /etc/consul-addrs.json
'';
};
}
# deprecated
(lib.mkIf (cfg.forceIpv4 != null && cfg.forceIpv4) {
services.consul.forceAddrFamily = "ipv4";
})
(lib.mkIf (cfg.alerts.enable) {
systemd.services.consul-alerts = {
wantedBy = [ "multi-user.target" ];
after = [ "consul.service" ];
path = [ cfg.package ];
serviceConfig = {
ExecStart = ''
${lib.getExe cfg.alerts.package} start \
--alert-addr=${cfg.alerts.listenAddr} \
--consul-addr=${cfg.alerts.consulAddr} \
${lib.optionalString cfg.alerts.watchChecks "--watch-checks"} \
${lib.optionalString cfg.alerts.watchEvents "--watch-events"}
'';
User = if cfg.dropPrivileges then "consul" else null;
Restart = "on-failure";
};
};
})
]
);
}

View File

@@ -0,0 +1,58 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.coredns;
configFile = pkgs.writeText "Corefile" cfg.config;
in
{
options.services.coredns = {
enable = lib.mkEnableOption "Coredns dns server";
config = lib.mkOption {
default = "";
example = ''
. {
whoami
}
'';
type = lib.types.lines;
description = ''
Verbatim Corefile to use.
See <https://coredns.io/manual/toc/#configuration> for details.
'';
};
package = lib.mkPackageOption pkgs "coredns" { };
extraArgs = lib.mkOption {
default = [ ];
example = [ "-dns.port=53" ];
type = lib.types.listOf lib.types.str;
description = "Extra arguments to pass to coredns.";
};
};
config = lib.mkIf cfg.enable {
systemd.services.coredns = {
description = "Coredns dns server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
PermissionsStartOnly = true;
LimitNPROC = 512;
LimitNOFILE = 1048576;
CapabilityBoundingSet = "cap_net_bind_service";
AmbientCapabilities = "cap_net_bind_service";
NoNewPrivileges = true;
DynamicUser = true;
ExecStart = "${lib.getBin cfg.package}/bin/coredns -conf=${configFile} ${lib.escapeShellArgs cfg.extraArgs}";
ExecReload = "${pkgs.coreutils}/bin/kill -SIGUSR1 $MAINPID";
Restart = "on-failure";
};
};
};
}

View File

@@ -0,0 +1,80 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.corerad;
settingsFormat = pkgs.formats.toml { };
in
{
meta.maintainers = with lib.maintainers; [ mdlayher ];
options.services.corerad = {
enable = lib.mkEnableOption "CoreRAD IPv6 NDP RA daemon";
settings = lib.mkOption {
type = settingsFormat.type;
example = lib.literalExpression ''
{
interfaces = [
# eth0 is an upstream interface monitoring for IPv6 router advertisements.
{
name = "eth0";
monitor = true;
}
# eth1 is a downstream interface advertising IPv6 prefixes for SLAAC.
{
name = "eth1";
advertise = true;
prefix = [{ prefix = "::/64"; }];
}
];
# Optionally enable Prometheus metrics.
debug = {
address = "localhost:9430";
prometheus = true;
};
}
'';
description = ''
Configuration for CoreRAD, see <https://github.com/mdlayher/corerad/blob/main/internal/config/reference.toml>
for supported values. Ignored if configFile is set.
'';
};
configFile = lib.mkOption {
type = lib.types.path;
example = lib.literalExpression ''"''${pkgs.corerad}/etc/corerad/corerad.toml"'';
description = "Path to CoreRAD TOML configuration file.";
};
package = lib.mkPackageOption pkgs "corerad" { };
};
config = lib.mkIf cfg.enable {
# Prefer the config file over settings if both are set.
services.corerad.configFile = lib.mkDefault (settingsFormat.generate "corerad.toml" cfg.settings);
systemd.services.corerad = {
description = "CoreRAD IPv6 NDP RA daemon";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
LimitNPROC = 512;
LimitNOFILE = 1048576;
CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_RAW";
AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_RAW";
NoNewPrivileges = true;
DynamicUser = true;
Type = "notify";
NotifyAccess = "main";
ExecStart = "${lib.getBin cfg.package}/bin/corerad -c=${cfg.configFile}";
Restart = "on-failure";
RestartKillSignal = "SIGHUP";
};
};
};
}

View File

@@ -0,0 +1,435 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
cfg = config.services.coturn;
pidfile = "/run/turnserver/turnserver.pid";
configFile = pkgs.writeText "turnserver.conf" ''
listening-port=${toString cfg.listening-port}
tls-listening-port=${toString cfg.tls-listening-port}
alt-listening-port=${toString cfg.alt-listening-port}
alt-tls-listening-port=${toString cfg.alt-tls-listening-port}
${lib.concatStringsSep "\n" (map (x: "listening-ip=${x}") cfg.listening-ips)}
${lib.concatStringsSep "\n" (map (x: "relay-ip=${x}") cfg.relay-ips)}
min-port=${toString cfg.min-port}
max-port=${toString cfg.max-port}
${lib.optionalString cfg.lt-cred-mech "lt-cred-mech"}
${lib.optionalString cfg.no-auth "no-auth"}
${lib.optionalString cfg.use-auth-secret "use-auth-secret"}
${lib.optionalString (
cfg.static-auth-secret != null
) "static-auth-secret=${cfg.static-auth-secret}"}
${lib.optionalString (
cfg.static-auth-secret-file != null
) "static-auth-secret=#static-auth-secret#"}
realm=${cfg.realm}
${lib.optionalString cfg.no-udp "no-udp"}
${lib.optionalString cfg.no-tcp "no-tcp"}
${lib.optionalString cfg.no-tls "no-tls"}
${lib.optionalString cfg.no-dtls "no-dtls"}
${lib.optionalString cfg.no-udp-relay "no-udp-relay"}
${lib.optionalString cfg.no-tcp-relay "no-tcp-relay"}
${lib.optionalString (cfg.cert != null) "cert=${cfg.cert}"}
${lib.optionalString (cfg.pkey != null) "pkey=${cfg.pkey}"}
${lib.optionalString (cfg.dh-file != null) "dh-file=${cfg.dh-file}"}
pidfile=${pidfile}
${lib.optionalString cfg.secure-stun "secure-stun"}
${lib.optionalString cfg.no-cli "no-cli"}
cli-ip=${cfg.cli-ip}
cli-port=${toString cfg.cli-port}
${lib.optionalString (cfg.cli-password != null) "cli-password=${cfg.cli-password}"}
${cfg.extraConfig}
'';
in
{
options = {
services.coturn = {
enable = lib.mkEnableOption "coturn TURN server";
listening-port = lib.mkOption {
type = lib.types.port;
default = 3478;
description = ''
TURN listener port for UDP and TCP.
Note: actually, TLS and DTLS sessions can connect to the
"plain" TCP and UDP port(s), too - if allowed by configuration.
'';
};
tls-listening-port = lib.mkOption {
type = lib.types.port;
default = 5349;
description = ''
TURN listener port for TLS.
Note: actually, "plain" TCP and UDP sessions can connect to the TLS and
DTLS port(s), too - if allowed by configuration. The TURN server
"automatically" recognizes the type of traffic. Actually, two listening
endpoints (the "plain" one and the "tls" one) are equivalent in terms of
functionality; but we keep both endpoints to satisfy the RFC 5766 specs.
For secure TCP connections, we currently support SSL version 3 and
TLS version 1.0, 1.1 and 1.2.
For secure UDP connections, we support DTLS version 1.
'';
};
alt-listening-port = lib.mkOption {
type = lib.types.port;
default = cfg.listening-port + 1;
defaultText = lib.literalExpression "listening-port + 1";
description = ''
Alternative listening port for UDP and TCP listeners;
default (or zero) value means "listening port plus one".
This is needed for RFC 5780 support
(STUN extension specs, NAT behavior discovery). The TURN Server
supports RFC 5780 only if it is started with more than one
listening IP address of the same family (IPv4 or IPv6).
RFC 5780 is supported only by UDP protocol, other protocols
are listening to that endpoint only for "symmetry".
'';
};
alt-tls-listening-port = lib.mkOption {
type = lib.types.port;
default = cfg.tls-listening-port + 1;
defaultText = lib.literalExpression "tls-listening-port + 1";
description = ''
Alternative listening port for TLS and DTLS protocols.
'';
};
listening-ips = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"203.0.113.42"
"2001:DB8::42"
];
description = ''
Listener IP addresses of relay server.
If no IP(s) specified in the config file or in the command line options,
then all IPv4 and IPv6 system IPs will be used for listening.
'';
};
relay-ips = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"203.0.113.42"
"2001:DB8::42"
];
description = ''
Relay address (the local IP address that will be used to relay the
packets to the peer).
Multiple relay addresses may be used.
The same IP(s) can be used as both listening IP(s) and relay IP(s).
If no relay IP(s) specified, then the turnserver will apply the default
policy: it will decide itself which relay addresses to be used, and it
will always be using the client socket IP address as the relay IP address
of the TURN session (if the requested relay address family is the same
as the family of the client socket).
'';
};
min-port = lib.mkOption {
type = lib.types.port;
default = 49152;
description = ''
Lower bound of UDP relay endpoints
'';
};
max-port = lib.mkOption {
type = lib.types.port;
default = 65535;
description = ''
Upper bound of UDP relay endpoints
'';
};
lt-cred-mech = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Use long-term credential mechanism.
'';
};
no-auth = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
This option is opposite to lt-cred-mech.
(TURN Server with no-auth option allows anonymous access).
If neither option is defined, and no users are defined,
then no-auth is default. If at least one user is defined,
in this file or in command line or in usersdb file, then
lt-cred-mech is default.
'';
};
use-auth-secret = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
TURN REST API flag.
Flag that sets a special authorization option that is based upon authentication secret.
This feature can be used with the long-term authentication mechanism, only.
This feature purpose is to support "TURN Server REST API", see
"TURN REST API" link in the project's page
<https://github.com/coturn/coturn/>
This option is used with timestamp:
usercombo -> "timestamp:userid"
turn user -> usercombo
turn password -> base64(hmac(secret key, usercombo))
This allows TURN credentials to be accounted for a specific user id.
If you don't have a suitable id, the timestamp alone can be used.
This option is just turning on secret-based authentication.
The actual value of the secret is defined either by option static-auth-secret,
or can be found in the turn_secret table in the database.
'';
};
static-auth-secret = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
'Static' authentication secret value (a string) for TURN REST API only.
If not set, then the turn server
will try to use the 'dynamic' value in turn_secret table
in user database (if present). The database-stored value can be changed on-the-fly
by a separate program, so this is why that other mode is 'dynamic'.
'';
};
static-auth-secret-file = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Path to the file containing the static authentication secret.
'';
};
realm = lib.mkOption {
type = lib.types.str;
default = config.networking.hostName;
defaultText = lib.literalExpression "config.networking.hostName";
example = "example.com";
description = ''
The default realm to be used for the users when no explicit
origin/realm relationship was found in the database, or if the TURN
server is not using any database (just the commands-line settings
and the userdb file). Must be used with long-term credentials
mechanism or with TURN REST API.
'';
};
cert = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/var/lib/acme/example.com/fullchain.pem";
description = ''
Certificate file in PEM format.
'';
};
pkey = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/var/lib/acme/example.com/key.pem";
description = ''
Private key file in PEM format.
'';
};
dh-file = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Use custom DH TLS key, stored in PEM format in the file.
'';
};
secure-stun = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Require authentication of the STUN Binding request.
By default, the clients are allowed anonymous access to the STUN Binding functionality.
'';
};
no-cli = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Turn OFF the CLI support.
'';
};
cli-ip = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = ''
Local system IP address to be used for CLI server endpoint.
'';
};
cli-port = lib.mkOption {
type = lib.types.port;
default = 5766;
description = ''
CLI server port.
'';
};
cli-password = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
CLI access password.
For the security reasons, it is recommended to use the encrypted
for of the password (see the -P command in the turnadmin utility).
'';
};
no-udp = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Disable UDP client listener";
};
no-tcp = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Disable TCP client listener";
};
no-tls = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Disable TLS client listener";
};
no-dtls = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Disable DTLS client listener";
};
no-udp-relay = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Disable UDP relay endpoints";
};
no-tcp-relay = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Disable TCP relay endpoints";
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Additional configuration options";
};
};
};
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
assertions = [
{
assertion = cfg.static-auth-secret != null -> cfg.static-auth-secret-file == null;
message = "static-auth-secret and static-auth-secret-file cannot be set at the same time";
}
];
}
{
users.users.turnserver = {
uid = config.ids.uids.turnserver;
group = "turnserver";
description = "coturn TURN server user";
};
users.groups.turnserver = {
gid = config.ids.gids.turnserver;
members = [ "turnserver" ];
};
systemd.services.coturn =
let
runConfig = "/run/coturn/turnserver.cfg";
in
{
description = "coturn TURN server";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
unitConfig = {
Documentation = "man:coturn(1) man:turnadmin(1) man:turnserver(1)";
};
preStart = ''
cat ${configFile} > ${runConfig}
${lib.optionalString (cfg.static-auth-secret-file != null) ''
${pkgs.replace-secret}/bin/replace-secret \
"#static-auth-secret#" \
${cfg.static-auth-secret-file} \
${runConfig}
''}
chmod 640 ${runConfig}
'';
serviceConfig = rec {
Type = "notify";
ExecStart = utils.escapeSystemdExecArgs [
(lib.getExe' pkgs.coturn "turnserver")
"-c"
runConfig
];
User = "turnserver";
Group = "turnserver";
RuntimeDirectory = [
"coturn"
"turnserver"
];
RuntimeDirectoryMode = "0700";
Restart = "on-abort";
# Hardening
AmbientCapabilities =
if
cfg.listening-port < 1024
|| cfg.alt-listening-port < 1024
|| cfg.tls-listening-port < 1024
|| cfg.alt-tls-listening-port < 1024
|| cfg.min-port < 1024
then
[ "CAP_NET_BIND_SERVICE" ]
else
[ "" ];
CapabilityBoundingSet = AmbientCapabilities;
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
]
++ lib.optionals (cfg.listening-ips == [ ]) [
# only used for interface discovery when no listening ips are configured
"AF_NETLINK"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged @resources"
];
UMask = "0077";
};
};
}
]
);
}

View File

@@ -0,0 +1,213 @@
# 🦀 crab-hole {#module-services-crab-hole}
Crab-hole is a cross platform Pi-hole clone written in Rust using [hickory-dns/trust-dns](https://github.com/hickory-dns/hickory-dns).
It can be used as a network wide ad and spy blocker or run on your local PC.
For a secure and private communication, crab-hole has builtin support for DoH(HTTPS), DoQ(QUIC) and DoT(TLS) for down- and upstreams and DNSSEC for upstreams.
It also comes with privacy friendly default logging settings.
## Configuration {#module-services-crab-hole-configuration}
As an example config file using Cloudflare as DoT upstream, you can use this [crab-hole.toml](https://github.com/LuckyTurtleDev/crab-hole/blob/main/example-config.toml)
The following is a basic nix config using UDP as a downstream and Cloudflare as upstream.
```nix
{
services.crab-hole = {
enable = true;
settings = {
blocklist = {
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"
];
};
downstream = [
{
protocol = "udp";
listen = "127.0.0.1";
port = 53;
}
{
protocol = "udp";
listen = "::1";
port = 53;
}
];
upstream = {
name_servers = [
{
socket_addr = "1.1.1.1:853";
protocol = "tls";
tls_dns_name = "1dot1dot1dot1.cloudflare-dns.com";
trust_nx_responses = false;
}
{
socket_addr = "[2606:4700:4700::1111]:853";
protocol = "tls";
tls_dns_name = "1dot1dot1dot1.cloudflare-dns.com";
trust_nx_responses = false;
}
];
};
};
};
}
```
To test your setup, just query the DNS server with any domain like `example.com`.
To test if a domain gets blocked, just choose one of the domains from the blocklist.
If the server does not return an IP, this worked correctly.
### Downstream options {#module-services-crab-hole-downstream}
There are multiple protocols which are supported for the downstream:
UDP, TLS, HTTPS and QUIC.
Below you can find a brief overview over the various protocol options together with an example for each protocol.
#### UDP {#module-services-crab-hole-udp}
UDP is the simplest downstream, but it is not encrypted.
If you want encryption, you need to use another protocol.
***Note:** This also opens a TCP port*
```nix
{
services.crab-hole.settings.downstream = [
{
protocol = "udp";
listen = "localhost";
port = 53;
}
];
}
```
#### TLS {#module-services-crab-hole-tls}
TLS is a simple encrypted options to serve DNS.
It comes with similar settings to UDP,
but you additionally need a valid TLS certificate and its private key.
The later are specified via a path to the files.
A valid TLS certificate and private key can be obtained using services like ACME.
Make sure the crab-hole service user has access to these files.
Additionally you can set an optional timeout value.
```nix
{
services.crab-hole.settings.downstream = [
{
protocol = "tls";
listen = "[::]";
port = 853;
certificate = ./dns.example.com.crt;
key = "/dns.example.com.key";
# optional (default = 3000)
timeout_ms = 3000;
}
];
}
```
#### HTTPS {#module-services-crab-hole-https}
HTTPS has similar settings to TLS, with the only difference being the additional `dns_hostname` option.
This protocol might need a reverse proxy if other HTTPS services are to share the same port.
Make sure the service has permissions to access the certificate and key.
***Note:** this config is untested*
```nix
{
services.crab-hole.settings.downstream = [
{
protocol = "https";
listen = "[::]";
port = 443;
certificate = ./dns.example.com.crt;
key = "/dns.example.com.key";
# optional
dns_hostname = "dns.example.com";
# optional (default = 3000)
timeout_ms = 3000;
}
];
}
```
#### QUIC {#module-services-crab-hole-quic}
QUIC has identical settings to the HTTPS protocol.
Since by default it doesn't run on the standard HTTPS port, you shouldn't need a reverse proxy.
Make sure the service has permissions to access the certificate and key.
```nix
{
services.crab-hole.settings.downstream = [
{
protocol = "quic";
listen = "127.0.0.1";
port = 853;
certificate = ./dns.example.com.crt;
key = "/dns.example.com.key";
# optional
dns_hostname = "dns.example.com";
# optional (default = 3000)
timeout_ms = 3000;
}
];
}
```
### Upstream options {#module-services-crab-hole-upstream-options}
You can set additional options of the underlying DNS server. A full list of all the options can be found in the [hickory-dns documentation](https://docs.rs/trust-dns-resolver/0.23.0/trust_dns_resolver/config/struct.ResolverOpts.html).
This can look like the following example.
```nix
{
services.crab-hole.settings.upstream.options = {
validate = false;
};
}
```
#### DNSSEC Issues {#module-services-crab-hole-dnssec}
Due to an upstream issue of [hickory-dns](https://github.com/hickory-dns/hickory-dns/issues/2429), sites without DNSSEC will not be resolved if `validate = true`.
Only DNSSEC capable sites will be resolved with this setting.
To prevent this, set `validate = false` or omit the `[upstream.options]`.
### API {#module-services-crab-hole-api}
The API allows a user to fetch statistic and information about the crab-hole instance.
Basic information is available for everyone, while more detailed information is secured by a key, which will be set with the `admin_key` option.
```nix
{
services.crab-hole.settings.api = {
listen = "127.0.0.1";
port = 8080;
# optional (default = false)
show_doc = true; # OpenAPI doc loads content from third party websites
# optional
admin_key = "1234";
};
}
```
The documentation can be enabled separately for the instance with `show_doc`.
This will then create an additional webserver, which hosts the API documentation.
An additional resource is in work in the [crab-hole repository](https://github.com/LuckyTurtleDev/crab-hole).
## Troubleshooting {#module-services-crab-hole-troubleshooting}
You can check for errors using `systemctl status crab-hole` or `journalctl -xeu crab-hole.service`.
### Invalid config {#module-services-crab-hole-invalid-config}
Some options of the service are in freeform and not type checked.
This can lead to a config which is not valid or cannot be parsed by crab-hole.
The error message will tell you what config value could not be parsed.
For more information check the [example config](https://github.com/LuckyTurtleDev/crab-hole/blob/main/example-config.toml).
### Permission Error {#module-services-crab-hole-permission-error}
It can happen that the created certificates for TLS, HTTPS or QUIC are owned by another user or group.
For ACME for example this would be `acme:acme`.
To give the crab-hole service access to these files, the group which owns the certificate can be added as a supplementary group to the service.
For ACME for example:
```nix
{ services.crab-hole.supplementaryGroups = [ "acme" ]; }
```

View File

@@ -0,0 +1,180 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.crab-hole;
settingsFormat = pkgs.formats.toml { };
checkConfig =
file:
pkgs.runCommand "check-config"
{
nativeBuildInputs = [
cfg.package
pkgs.cacert
pkgs.dig
];
}
''
ln -s ${file} $out
ln -s ${file} ./config.toml
export CRAB_HOLE_DIR=$(pwd)
${lib.getExe cfg.package} validate-config
'';
in
{
options = {
services.crab-hole = {
enable = lib.mkEnableOption "Crab-hole Service";
package = lib.mkPackageOption pkgs "crab-hole" { };
supplementaryGroups = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "acme" ];
description = "Adds additional groups to the crab-hole service. Can be useful to prevent permission issues.";
};
settings = lib.mkOption {
description = "Crab-holes config. See big example <https://github.com/LuckyTurtleDev/crab-hole/blob/main/example-config.toml>";
example = {
downstream = [
{
listen = "localhost";
port = 8080;
protocol = "udp";
}
{
certificate = "dns.example.com.crt";
dns_hostname = "dns.example.com";
key = "dns.example.com.key";
listen = "[::]";
port = 8055;
protocol = "https";
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 = "1.1.1.1:853";
tls_dns_name = "1dot1dot1dot1.cloudflare-dns.com";
trust_nx_responses = false;
}
];
options = {
validate = false;
};
};
};
type = lib.types.submodule {
freeformType = settingsFormat.type;
options = {
blocklist =
let
listOption =
name:
lib.mkOption {
type = lib.types.listOf (lib.types.either lib.types.str lib.types.path);
default = [ ];
description = "List of ${name}. If files are added via url, make sure the service has access to them!";
apply = map (v: if builtins.isPath v then "file://${v}" else v);
};
in
{
include_subdomains = lib.mkEnableOption "Include subdomains";
lists = listOption "blocklists";
allow_list = listOption "allowlists";
};
};
};
};
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.
Setting this option will override any configuration applied by the settings option.
'';
};
};
};
config = lib.mkIf cfg.enable {
# Warning due to DNSSec issue in crab-hole
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
'';
services.crab-hole.configFile = lib.mkDefault (
checkConfig (settingsFormat.generate "crab-hole.toml" cfg.settings)
);
environment.etc."crab-hole.toml".source = cfg.configFile;
systemd.services.crab-hole = {
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
description = "Crab-hole dns server";
environment.HOME = "/var/lib/crab-hole";
restartTriggers = [ cfg.configFile ];
serviceConfig = {
Type = "simple";
DynamicUser = true;
SupplementaryGroups = cfg.supplementaryGroups;
StateDirectory = "crab-hole";
WorkingDirectory = "/var/lib/crab-hole";
ExecStart = lib.getExe cfg.package;
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
Restart = "on-failure";
RestartSec = 1;
};
};
};
meta.maintainers = [
lib.maintainers.NiklasVousten
];
# Readme from upstream
meta.doc = ./crab-hole.md;
}

View File

@@ -0,0 +1,59 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.create_ap;
configFile = pkgs.writeText "create_ap.conf" (lib.generators.toKeyValue { } cfg.settings);
in
{
options = {
services.create_ap = {
enable = lib.mkEnableOption "setting up wifi hotspots using create_ap";
settings = lib.mkOption {
type =
with lib.types;
attrsOf (oneOf [
int
bool
str
]);
default = { };
description = ''
Configuration for `create_ap`.
See [upstream example configuration](https://raw.githubusercontent.com/lakinduakash/linux-wifi-hotspot/master/src/scripts/create_ap.conf)
for supported values.
'';
example = {
INTERNET_IFACE = "eth0";
WIFI_IFACE = "wlan0";
SSID = "My Wifi Hotspot";
PASSPHRASE = "12345678";
};
};
};
};
config = lib.mkIf cfg.enable {
systemd = {
services.create_ap = {
wantedBy = [ "multi-user.target" ];
description = "Create AP Service";
after = [ "network.target" ];
restartTriggers = [ configFile ];
serviceConfig = {
ExecStart = "${pkgs.linux-wifi-hotspot}/bin/create_ap --config ${configFile}";
KillSignal = "SIGINT";
Restart = "on-failure";
};
};
};
};
meta.maintainers = with lib.maintainers; [ onny ];
}

View File

@@ -0,0 +1,112 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) types;
cfg = config.services.croc;
rootDir = "/run/croc";
in
{
options.services.croc = {
enable = lib.mkEnableOption "croc relay";
ports = lib.mkOption {
type = with types; listOf port;
default = [
9009
9010
9011
9012
9013
];
description = "Ports of the relay.";
};
pass = lib.mkOption {
type = with types; either path str;
default = "pass123";
description = "Password or passwordfile for the relay.";
};
openFirewall = lib.mkEnableOption "opening of the peer port(s) in the firewall";
debug = lib.mkEnableOption "debug logs";
};
config = lib.mkIf cfg.enable {
systemd.services.croc = {
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${pkgs.croc}/bin/croc --pass '${cfg.pass}' ${lib.optionalString cfg.debug "--debug"} relay --ports ${
lib.concatMapStringsSep "," toString cfg.ports
}";
# The following options are only for optimizing:
# systemd-analyze security croc
AmbientCapabilities = "";
CapabilityBoundingSet = "";
DynamicUser = true;
# ProtectClock= adds DeviceAllow=char-rtc r
DeviceAllow = "";
LockPersonality = true;
MemoryDenyWriteExecute = true;
MountAPIVFS = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateNetwork = lib.mkDefault false;
PrivateTmp = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RootDirectory = rootDir;
# Avoid mounting rootDir in the own rootDir of ExecStart='s mount namespace.
InaccessiblePaths = [ "-+${rootDir}" ];
BindReadOnlyPaths = [
builtins.storeDir
]
++ lib.optional (types.path.check cfg.pass) cfg.pass;
# This is for BindReadOnlyPaths=
# to allow traversal of directories they create in RootDirectory=.
UMask = "0066";
# Create rootDir in the host's mount namespace.
RuntimeDirectory = [ (baseNameOf rootDir) ];
RuntimeDirectoryMode = "700";
SystemCallFilter = [
"@system-service"
"~@aio"
"~@keyring"
"~@memlock"
"~@privileged"
"~@setuid"
"~@sync"
"~@timer"
];
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
};
};
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall cfg.ports;
};
meta.maintainers = with lib.maintainers; [
hax404
julm
];
}

View File

@@ -0,0 +1,190 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.dae;
assets = cfg.assets;
genAssetsDrv =
paths:
pkgs.symlinkJoin {
name = "dae-assets";
inherit paths;
};
in
{
meta.maintainers = with lib.maintainers; [
pokon548
oluceps
];
options = {
services.dae = with lib; {
enable = mkEnableOption "dae, a Linux high-performance transparent proxy solution based on eBPF";
package = mkPackageOption pkgs "dae" { };
assets = mkOption {
type = with types; (listOf path);
default = with pkgs; [
v2ray-geoip
v2ray-domain-list-community
];
defaultText = literalExpression "with pkgs; [ v2ray-geoip v2ray-domain-list-community ]";
description = ''
Assets required to run dae.
'';
};
assetsPath = mkOption {
type = types.str;
default = "${genAssetsDrv assets}/share/v2ray";
defaultText = literalExpression ''
(symlinkJoin {
name = "dae-assets";
paths = assets;
})/share/v2ray
'';
description = ''
The path which contains geolocation database.
This option will override `assets`.
'';
};
openFirewall = mkOption {
type =
with types;
submodule {
options = {
enable = mkEnableOption "opening {option}`port` in the firewall";
port = mkOption {
type = types.port;
description = ''
Port to be opened. Consist with field `tproxy_port` in config file.
'';
};
};
};
default = {
enable = true;
port = 12345;
};
defaultText = literalExpression ''
{
enable = true;
port = 12345;
}
'';
description = ''
Open the firewall port.
'';
};
configFile = mkOption {
type = with types; (nullOr path);
default = null;
example = "/path/to/your/config.dae";
description = ''
The path of dae config file, end with `.dae`.
'';
};
config = mkOption {
type = with types; (nullOr str);
default = null;
description = ''
WARNING: This option will expose store your config unencrypted world-readable in the nix store.
Config text for dae.
See <https://github.com/daeuniverse/dae/blob/main/example.dae>.
'';
};
disableTxChecksumIpGeneric = mkEnableOption "" // {
description = "See <https://github.com/daeuniverse/dae/issues/43>";
};
};
};
config =
lib.mkIf cfg.enable
{
environment.systemPackages = [ cfg.package ];
systemd.packages = [ cfg.package ];
networking = lib.mkIf cfg.openFirewall.enable {
firewall =
let
portToOpen = cfg.openFirewall.port;
in
{
allowedTCPPorts = [ portToOpen ];
allowedUDPPorts = [ portToOpen ];
};
};
systemd.services.dae =
let
daeBin = lib.getExe cfg.package;
configPath =
if cfg.configFile != null then cfg.configFile else pkgs.writeText "config.dae" cfg.config;
TxChecksumIpGenericWorkaround =
with lib;
(getExe pkgs.writeShellApplication {
name = "disable-tx-checksum-ip-generic";
text = with pkgs; ''
iface=$(${iproute2}/bin/ip route | ${lib.getExe gawk} '/default/ {print $5}')
${lib.getExe ethtool} -K "$iface" tx-checksum-ip-generic off
'';
});
in
{
wantedBy = [ "multi-user.target" ];
serviceConfig = {
LoadCredential = [ "config.dae:${configPath}" ];
ExecStartPre = [
""
"${daeBin} validate -c \${CREDENTIALS_DIRECTORY}/config.dae"
]
++ (with lib; optional cfg.disableTxChecksumIpGeneric TxChecksumIpGenericWorkaround);
ExecStart = [
""
"${daeBin} run --disable-timestamp -c \${CREDENTIALS_DIRECTORY}/config.dae"
];
Environment = "DAE_LOCATION_ASSET=${cfg.assetsPath}";
};
};
assertions = [
{
assertion = lib.pathExists (toString (genAssetsDrv cfg.assets) + "/share/v2ray");
message = ''
Packages in `assets` has no preset paths included.
Please set `assetsPath` instead.
'';
}
{
assertion = !((config.services.dae.config != null) && (config.services.dae.configFile != null));
message = ''
Option `config` and `configFile` could not be set
at the same time.
'';
}
{
assertion = !((config.services.dae.config == null) && (config.services.dae.configFile == null));
message = ''
Either `config` or `configFile` should be set.
'';
}
];
};
}

View File

@@ -0,0 +1,67 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.dante;
confFile = pkgs.writeText "dante-sockd.conf" ''
user.privileged: root
user.unprivileged: dante
logoutput: syslog
${cfg.config}
'';
in
{
meta = {
maintainers = with lib.maintainers; [ arobyn ];
};
options = {
services.dante = {
enable = lib.mkEnableOption "Dante SOCKS proxy";
config = lib.mkOption {
type = lib.types.lines;
description = ''
Contents of Dante's configuration file.
NOTE: user.privileged, user.unprivileged and logoutput are set by the service.
'';
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.config != "";
message = "please provide Dante configuration file contents";
}
];
users.users.dante = {
description = "Dante SOCKS proxy daemon user";
isSystemUser = true;
group = "dante";
};
users.groups.dante = { };
systemd.services.dante = {
description = "Dante SOCKS v4 and v5 compatible proxy server";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.dante}/bin/sockd -f ${confFile}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
# Can crash sometimes; see https://github.com/NixOS/nixpkgs/pull/39005#issuecomment-381828708
Restart = "on-failure";
};
};
};
}

View File

@@ -0,0 +1,305 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.ddclient;
boolToStr = bool: if bool then "yes" else "no";
dataDir = "/var/lib/ddclient";
StateDirectory = builtins.baseNameOf dataDir;
RuntimeDirectory = StateDirectory;
configFile' = pkgs.writeText "ddclient.conf" ''
# This file can be used as a template for configFile or is automatically generated by Nix options.
cache=${dataDir}/ddclient.cache
foreground=YES
${lib.optionalString (cfg.use != "") "use=${cfg.use}"}
${lib.optionalString (cfg.use == "" && cfg.usev4 != "") "usev4=${cfg.usev4}"}
${lib.optionalString (cfg.use == "" && cfg.usev6 != "") "usev6=${cfg.usev6}"}
${lib.optionalString (cfg.username != "") "login=${cfg.username}"}
${
if cfg.protocol == "nsupdate" then
"/run/${RuntimeDirectory}/ddclient.key"
else if (cfg.passwordFile != null) then
"password=@password_placeholder@"
else if (cfg.secretsFile != null) then
"@secrets_placeholder@"
else
""
}
protocol=${cfg.protocol}
${lib.optionalString (cfg.script != "") "script=${cfg.script}"}
${lib.optionalString (cfg.server != "") "server=${cfg.server}"}
${lib.optionalString (cfg.zone != "") "zone=${cfg.zone}"}
ssl=${boolToStr cfg.ssl}
wildcard=YES
quiet=${boolToStr cfg.quiet}
verbose=${boolToStr cfg.verbose}
${cfg.extraConfig}
${lib.concatStringsSep "," cfg.domains}
'';
configFile = if (cfg.configFile != null) then cfg.configFile else configFile';
preStart = ''
install --mode=600 --owner=$USER ${configFile} /run/${RuntimeDirectory}/ddclient.conf
${lib.optionalString (cfg.configFile == null) (
if (cfg.protocol == "nsupdate") then
''
install --mode=600 --owner=$USER ${cfg.passwordFile} /run/${RuntimeDirectory}/ddclient.key
''
else if (cfg.passwordFile != null) then
''
"${pkgs.replace-secret}/bin/replace-secret" "@password_placeholder@" "${cfg.passwordFile}" "/run/${RuntimeDirectory}/ddclient.conf"
''
else if (cfg.secretsFile != null) then
''
"${pkgs.replace-secret}/bin/replace-secret" "@secrets_placeholder@" "${cfg.secretsFile}" "/run/${RuntimeDirectory}/ddclient.conf"
''
else
''
sed -i '/^password=@password_placeholder@$/d' /run/${RuntimeDirectory}/ddclient.conf
''
)}
'';
in
{
imports = [
(lib.mkChangedOptionModule [ "services" "ddclient" "domain" ] [ "services" "ddclient" "domains" ] (
config:
let
value = lib.getAttrFromPath [ "services" "ddclient" "domain" ] config;
in
lib.optional (value != "") value
))
(lib.mkRemovedOptionModule [ "services" "ddclient" "homeDir" ] "")
(lib.mkRemovedOptionModule [
"services"
"ddclient"
"password"
] "Use services.ddclient.passwordFile instead.")
(lib.mkRemovedOptionModule [ "services" "ddclient" "ipv6" ] "")
];
###### interface
options = {
services.ddclient = with lib.types; {
enable = lib.mkOption {
default = false;
type = bool;
description = ''
Whether to synchronise your machine's IP address with a dynamic DNS provider (e.g. dyndns.org).
'';
};
package = lib.mkOption {
type = package;
default = pkgs.ddclient;
defaultText = lib.literalExpression "pkgs.ddclient";
description = ''
The ddclient executable package run by the service.
'';
};
domains = lib.mkOption {
default = [ "" ];
type = listOf str;
description = ''
Domain name(s) to synchronize.
'';
};
username = lib.mkOption {
# For `nsupdate` username contains the path to the nsupdate executable
default = lib.optionalString (
config.services.ddclient.protocol == "nsupdate"
) "${pkgs.bind.dnsutils}/bin/nsupdate";
defaultText = "";
type = str;
description = ''
User name.
'';
};
passwordFile = lib.mkOption {
default = null;
type = nullOr str;
description = ''
A file containing the password or a TSIG key in named format when using the nsupdate protocol.
'';
};
secretsFile = lib.mkOption {
default = null;
type = nullOr str;
description = ''
A file containing the secrets for the dynamic DNS provider.
This file should contain lines of valid secrets in the format specified by the ddclient documentation.
If this option is set, it overrides the `passwordFile` option.
'';
};
interval = lib.mkOption {
default = "10min";
type = str;
description = ''
The interval at which to run the check and update.
See {command}`man 7 systemd.time` for the format.
'';
};
configFile = lib.mkOption {
default = null;
type = nullOr path;
description = ''
Path to configuration file.
When set this overrides the generated configuration from module options.
'';
example = "/root/nixos/secrets/ddclient.conf";
};
protocol = lib.mkOption {
default = "dyndns2";
type = str;
description = ''
Protocol to use with dynamic DNS provider (see <https://ddclient.net/protocols.html> ).
'';
};
server = lib.mkOption {
default = "";
type = str;
description = ''
Server address.
'';
};
ssl = lib.mkOption {
default = true;
type = bool;
description = ''
Whether to use SSL/TLS to connect to dynamic DNS provider.
'';
};
quiet = lib.mkOption {
default = false;
type = bool;
description = ''
Print no messages for unnecessary updates.
'';
};
script = lib.mkOption {
default = "";
type = str;
description = ''
script as required by some providers.
'';
};
use = lib.mkOption {
default = "";
type = str;
description = ''
Method to determine the IP address to send to the dynamic DNS provider.
'';
};
usev4 = lib.mkOption {
default = "webv4, webv4=ipify-ipv4";
type = str;
description = ''
Method to determine the IPv4 address to send to the dynamic DNS provider. Only used if `use` is not set.
'';
};
usev6 = lib.mkOption {
default = "webv6, webv6=ipify-ipv6";
type = str;
description = ''
Method to determine the IPv6 address to send to the dynamic DNS provider. Only used if `use` is not set.
'';
};
verbose = lib.mkOption {
default = false;
type = bool;
description = ''
Print verbose information.
'';
};
zone = lib.mkOption {
default = "";
type = str;
description = ''
zone as required by some providers.
'';
};
extraConfig = lib.mkOption {
default = "";
type = lines;
description = ''
Extra configuration. Contents will be added verbatim to the configuration file.
::: {.note}
`daemon` should not be added here because it does not work great with the systemd-timer approach the service uses.
:::
'';
};
};
};
###### implementation
config = lib.mkIf config.services.ddclient.enable {
warnings =
lib.optional (cfg.use != "")
"Setting `use` is deprecated, ddclient now supports `usev4` and `usev6` for separate IPv4/IPv6 configuration.";
assertions = [
{
assertion = !((cfg.passwordFile != null) && (cfg.secretsFile != null));
message = "You cannot use both services.ddclient.passwordFile and services.ddclient.secretsFile at the same time.";
}
{
assertion = (cfg.protocol != "nsupdate") || (cfg.secretsFile == null);
message = "You cannot use services.ddclient.secretsFile when services.ddclient.protocol is \"nsupdate\". Use services.ddclient.passwordFile instead.";
}
];
systemd.services.ddclient = {
description = "Dynamic DNS Client";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
restartTriggers = lib.optional (cfg.configFile != null) cfg.configFile;
path = lib.optional (
lib.hasPrefix "if," cfg.use || lib.hasPrefix "ifv4," cfg.usev4 || lib.hasPrefix "ifv6," cfg.usev6
) pkgs.iproute2;
serviceConfig = {
DynamicUser = true;
RuntimeDirectoryMode = "0700";
inherit RuntimeDirectory;
inherit StateDirectory;
Type = "oneshot";
ExecStartPre = [ "!${pkgs.writeShellScript "ddclient-prestart" preStart}" ];
ExecStart = "${lib.getExe cfg.package} -file /run/${RuntimeDirectory}/ddclient.conf";
};
};
systemd.timers.ddclient = {
description = "Run ddclient";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = cfg.interval;
OnUnitInactiveSec = cfg.interval;
};
};
};
}

View File

@@ -0,0 +1,46 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.ddns-updater;
in
{
options.services.ddns-updater = {
enable = lib.mkEnableOption "Container to update DNS records periodically with WebUI for many DNS providers";
package = lib.mkPackageOption pkgs "ddns-updater" { };
environment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
description = "Environment variables to be set for the ddns-updater service. DATADIR is ignored to enable using systemd DynamicUser. For full list see <https://github.com/qdm12/ddns-updater>";
default = { };
};
};
config = lib.mkIf cfg.enable {
systemd.services.ddns-updater = {
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
environment = cfg.environment // {
DATADIR = "%S/ddns-updater";
};
unitConfig = {
Description = "DDNS-updater service";
};
serviceConfig = {
TimeoutSec = "5min";
ExecStart = lib.getExe cfg.package;
RestartSec = 30;
DynamicUser = true;
StateDirectory = "ddns-updater";
Restart = "on-failure";
};
};
};
}

View File

@@ -0,0 +1,134 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.deconz;
name = "deconz";
stateDir = "/var/lib/${name}";
# ref. upstream deconz.service
capabilities =
lib.optionals (cfg.httpPort < 1024 || cfg.wsPort < 1024) [ "CAP_NET_BIND_SERVICE" ]
++ lib.optionals (cfg.allowRebootSystem) [ "CAP_SYS_BOOT" ]
++ lib.optionals (cfg.allowRestartService) [ "CAP_KILL" ]
++ lib.optionals (cfg.allowSetSystemTime) [ "CAP_SYS_TIME" ];
in
{
options.services.deconz = {
enable = lib.mkEnableOption "deCONZ, a Zigbee gateway for use with ConBee/RaspBee hardware (https://phoscon.de/)";
package = lib.mkPackageOption pkgs "deconz" { };
device = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Force deCONZ to use a specific USB device (e.g. /dev/ttyACM0). By
default it does a search.
'';
};
listenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = ''
Pin deCONZ to the network interface specified through the provided IP
address. This applies for the webserver as well as the websocket
notifications.
'';
};
httpPort = lib.mkOption {
type = lib.types.port;
default = 80;
description = "TCP port for the web server.";
};
wsPort = lib.mkOption {
type = lib.types.port;
default = 443;
description = "TCP port for the WebSocket.";
};
openFirewall = lib.mkEnableOption "opening up the service ports in the firewall";
allowRebootSystem = lib.mkEnableOption "rebooting the system";
allowRestartService = lib.mkEnableOption "killing/restarting processes";
allowSetSystemTime = lib.mkEnableOption "setting the system time";
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"--dbg-info=1"
"--dbg-err=2"
];
description = ''
Extra command line arguments for deCONZ, see
<https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/deCONZ-command-line-parameters>.
'';
};
};
config = lib.mkIf cfg.enable {
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [
cfg.httpPort
cfg.wsPort
];
services.udev.packages = [ cfg.package ];
systemd.services.deconz = {
description = "deCONZ Zigbee gateway";
wantedBy = [ "multi-user.target" ];
preStart = ''
# The service puts a nix store path reference in here, and that path can
# be garbage collected. Ensure the file gets "refreshed" on every start.
rm -f ${stateDir}/.local/share/dresden-elektronik/deCONZ/zcldb.txt
'';
postStart = ''
# Delay signalling service readiness until it's actually up.
while ! "${lib.getExe pkgs.curl}" -sSfL -o /dev/null "http://${cfg.listenAddress}:${toString cfg.httpPort}"; do
echo "Waiting for TCP port ${toString cfg.httpPort} to be open..."
sleep 1
done
'';
environment = {
HOME = stateDir;
XDG_RUNTIME_DIR = "/run/${name}";
};
serviceConfig = {
ExecStart =
"${lib.getExe cfg.package}"
+ " -platform minimal"
+ " --http-listen=${cfg.listenAddress}"
+ " --http-port=${toString cfg.httpPort}"
+ " --ws-port=${toString cfg.wsPort}"
+ " --auto-connect=1"
+ (lib.optionalString (cfg.device != null) " --dev=${cfg.device}")
+ " "
+ (lib.escapeShellArgs cfg.extraArgs);
Restart = "on-failure";
AmbientCapabilities = capabilities;
CapabilityBoundingSet = capabilities;
UMask = "0027";
DynamicUser = true;
RuntimeDirectory = name;
RuntimeDirectoryMode = "0700";
StateDirectory = name;
SuccessExitStatus = [ 143 ];
WorkingDirectory = stateDir;
# For access to /dev/ttyACM0 (ConBee).
SupplementaryGroups = [ "dialout" ];
ProtectHome = true;
};
};
};
}

View File

@@ -0,0 +1,437 @@
{
config,
lib,
pkgs,
...
}:
let
dhcpcd =
if !config.boot.isContainer then
pkgs.dhcpcd
else
pkgs.dhcpcd.override {
withUdev = false;
};
cfg = config.networking.dhcpcd;
interfaces = lib.attrValues config.networking.interfaces;
enableDHCP =
config.networking.dhcpcd.enable
&& (config.networking.useDHCP || lib.any (i: i.useDHCP == true) interfaces);
useResolvConf = config.networking.resolvconf.enable;
# Don't start dhcpcd on explicitly configured interfaces or on
# interfaces that are part of a bridge, bond or sit device.
ignoredInterfaces =
map (i: i.name) (
lib.filter (i: if i.useDHCP != null then !i.useDHCP else i.ipv4.addresses != [ ]) interfaces
)
++ lib.mapAttrsToList (i: _: i) config.networking.sits
++ lib.concatLists (lib.attrValues (lib.mapAttrs (n: v: v.interfaces) config.networking.bridges))
++ lib.flatten (
lib.concatMap (
i: lib.attrNames (lib.filterAttrs (_: config: config.type != "internal") i.interfaces)
) (lib.attrValues config.networking.vswitches)
)
++ lib.concatLists (lib.attrValues (lib.mapAttrs (n: v: v.interfaces) config.networking.bonds))
++ config.networking.dhcpcd.denyInterfaces;
arrayAppendOrNull =
a1: a2:
if a1 == null && a2 == null then
null
else if a1 == null then
a2
else if a2 == null then
a1
else
a1 ++ a2;
# If dhcp is disabled but explicit interfaces are enabled,
# we need to provide dhcp just for those interfaces.
allowInterfaces = arrayAppendOrNull cfg.allowInterfaces (
if !config.networking.useDHCP && enableDHCP then
map (i: i.name) (lib.filter (i: i.useDHCP == true) interfaces)
else
null
);
staticIPv6Addresses = map (i: i.name) (lib.filter (i: i.ipv6.addresses != [ ]) interfaces);
noIPv6rs = lib.concatStringsSep "\n" (
map (name: ''
interface ${name}
noipv6rs
'') staticIPv6Addresses
);
# Config file adapted from the one that ships with dhcpcd.
dhcpcdConf = pkgs.writeText "dhcpcd.conf" ''
# Inform the DHCP server of our hostname for DDNS.
hostname
# A list of options to request from the DHCP server.
option domain_name_servers, domain_name, domain_search
option classless_static_routes, ntp_servers, interface_mtu
# A ServerID is required by RFC2131.
# Commented out because of many non-compliant DHCP servers in the wild :(
#require dhcp_server_identifier
# A hook script is provided to lookup the hostname if not set by
# the DHCP server, but it should not be run by default.
nohook lookup-hostname
# Ignore peth* devices; on Xen, they're renamed physical
# Ethernet cards used for bridging. Likewise for vif* and tap*
# (Xen) and virbr* and vnet* (libvirt).
denyinterfaces ${toString ignoredInterfaces} lo peth* vif* tap* tun* virbr* vnet* vboxnet* sit*
# Use the list of allowed interfaces if specified
${lib.optionalString (allowInterfaces != null) "allowinterfaces ${toString allowInterfaces}"}
# Immediately fork to background if specified, otherwise wait for IP address to be assigned
${
{
background = "background";
any = "waitip";
ipv4 = "waitip 4";
ipv6 = "waitip 6";
both = "waitip 4\nwaitip 6";
if-carrier-up = "";
}
.${cfg.wait}
}
${lib.optionalString (config.networking.enableIPv6 == false) ''
# Don't solicit or accept IPv6 Router Advertisements and DHCPv6 if disabled IPv6
noipv6
''}
${lib.optionalString (
config.networking.enableIPv6 && cfg.IPv6rs == null && staticIPv6Addresses != [ ]
) noIPv6rs}
${lib.optionalString (config.networking.enableIPv6 && cfg.IPv6rs == false) ''
noipv6rs
''}
${lib.optionalString cfg.setHostname "option host_name"}
${cfg.extraConfig}
'';
in
{
###### interface
options = {
networking.dhcpcd.enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to enable dhcpcd for device configuration. This is mainly to
explicitly disable dhcpcd (for example when using networkd).
'';
};
networking.dhcpcd.persistent = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to leave interfaces configured on dhcpcd daemon
shutdown. Set to true if you have your root or store mounted
over the network or this machine accepts SSH connections
through DHCP interfaces and clients should be notified when
it shuts down.
'';
};
networking.dhcpcd.setHostname = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to set the machine hostname based on the information
received from the DHCP server.
::: {.note}
The hostname will be changed only if the current one is
the empty string, `localhost` or `nixos`.
Polkit ([](#opt-security.polkit.enable)) is also required.
:::
'';
};
networking.dhcpcd.denyInterfaces = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Disable the DHCP client for any interface whose name matches
any of the shell glob patterns in this list. The purpose of
this option is to blacklist virtual interfaces such as those
created by Xen, libvirt, LXC, etc.
'';
};
networking.dhcpcd.allowInterfaces = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
description = ''
Enable the DHCP client for any interface whose name matches
any of the shell glob patterns in this list. Any interface not
explicitly matched by this pattern will be denied. This pattern only
applies when non-null.
'';
};
networking.dhcpcd.extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Literal string to append to the config file generated for dhcpcd.
'';
};
networking.dhcpcd.IPv6rs = lib.mkOption {
type = lib.types.nullOr lib.types.bool;
default = null;
description = ''
Force enable or disable solicitation and receipt of IPv6 Router Advertisements.
This is required, for example, when using a static unique local IPv6 address (ULA)
and global IPv6 address auto-configuration with SLAAC.
'';
};
networking.dhcpcd.allowSetuid = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to relax the security sandbox to allow running setuid
binaries (e.g. `sudo`) in the dhcpcd hooks.
'';
};
networking.dhcpcd.runHook = lib.mkOption {
type = lib.types.lines;
default = "";
example = "if [[ $reason =~ BOUND ]]; then echo $interface: Routers are $new_routers - were $old_routers; fi";
description = ''
Shell code that will be run after all other hooks. See
`man dhcpcd-run-hooks` for details on what is possible.
::: {.note}
To use sudo or similar tools in your script you may have to set:
networking.dhcpcd.allowSetuid = true;
In addition, as most of the filesystem is inaccessible to dhcpcd
by default, you may want to define some exceptions, e.g.
systemd.services.dhcpcd.serviceConfig.ReadOnlyPaths = [
"/run/user/1000/bus" # to send desktop notifications
];
:::
'';
};
networking.dhcpcd.wait = lib.mkOption {
type = lib.types.enum [
"background"
"any"
"ipv4"
"ipv6"
"both"
"if-carrier-up"
];
default = "any";
description = ''
This option specifies when the dhcpcd service will fork to background.
If set to "background", dhcpcd will fork to background immediately.
If set to "ipv4" or "ipv6", dhcpcd will wait for the corresponding IP
address to be assigned. If set to "any", dhcpcd will wait for any type
(IPv4 or IPv6) to be assigned. If set to "both", dhcpcd will wait for
both an IPv4 and an IPv6 address before forking.
The option "if-carrier-up" is equivalent to "any" if either ethernet
is plugged or WiFi is powered, and to "background" otherwise.
'';
};
};
###### implementation
config = lib.mkIf enableDHCP {
systemd.services.dhcpcd =
let
cfgN = config.networking;
hasDefaultGatewaySet =
(cfgN.defaultGateway != null && cfgN.defaultGateway.address != "")
&& (!cfgN.enableIPv6 || (cfgN.defaultGateway6 != null && cfgN.defaultGateway6.address != ""));
in
{
description = "DHCP Client";
documentation = [ "man:dhcpcd(8)" ];
wantedBy = [ "multi-user.target" ] ++ lib.optional (!hasDefaultGatewaySet) "network-online.target";
wants = [
"network.target"
"resolvconf.service"
];
after = [ "resolvconf.service" ];
before = [ "network-online.target" ];
restartTriggers = [ cfg.runHook ];
# Stopping dhcpcd during a reconfiguration is undesirable
# because it brings down the network interfaces configured by
# dhcpcd. So do a "systemctl restart" instead.
stopIfChanged = false;
path = [
dhcpcd
config.networking.resolvconf.package
]
++ lib.optional cfg.setHostname (
pkgs.writeShellScriptBin "hostname" ''
${lib.getExe' pkgs.systemd "hostnamectl"} set-hostname --transient $1
''
);
unitConfig.ConditionCapability = "CAP_NET_ADMIN";
serviceConfig = {
Type = "forking";
PIDFile = "/run/dhcpcd/pid";
SupplementaryGroups = lib.optional useResolvConf "resolvconf";
User = "dhcpcd";
Group = "dhcpcd";
StateDirectory = "dhcpcd";
RuntimeDirectory = "dhcpcd";
ExecStartPre = "+${pkgs.writeShellScript "migrate-dhcpcd" ''
# migrate from old database directory
if test -f /var/db/dhcpcd/duid; then
echo 'migrating DHCP leases from /var/db/dhcpcd to /var/lib/dhcpcd ...'
mv /var/db/dhcpcd/* -t /var/lib/dhcpcd
chown dhcpcd:dhcpcd /var/lib/dhcpcd/*
rmdir /var/db/dhcpcd || true
echo done
fi
''}";
ExecStart = "@${dhcpcd}/sbin/dhcpcd dhcpcd --quiet ${lib.optionalString cfg.persistent "--persistent"} --config ${dhcpcdConf}";
ExecReload = "${dhcpcd}/sbin/dhcpcd --rebind";
Restart = "always";
AmbientCapabilities = [
"CAP_NET_ADMIN"
"CAP_NET_RAW"
"CAP_NET_BIND_SERVICE"
];
CapabilityBoundingSet = lib.optionals (!cfg.allowSetuid) [
"CAP_NET_ADMIN"
"CAP_NET_RAW"
"CAP_NET_BIND_SERVICE"
];
ReadWritePaths = [
"/proc/sys/net/ipv4"
]
++ lib.optional cfgN.enableIPv6 "/proc/sys/net/ipv6"
++ lib.optionals useResolvConf (
[ "/run/resolvconf" ] ++ config.networking.resolvconf.subscriberFiles
);
DeviceAllow = "";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = lib.mkDefault (!cfg.allowSetuid); # may be disabled for sudo in runHook
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = false;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = "tmpfs"; # allow exceptions to be added to ReadOnlyPaths, etc.
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
"AF_NETLINK"
"AF_PACKET"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallFilter = [
"@system-service"
"~@aio"
"~@keyring"
"~@memlock"
"~@mount"
]
++ lib.optionals (!cfg.allowSetuid) [
"~@privileged"
"~@resources"
];
SystemCallArchitectures = "native";
UMask = "0027";
};
};
# Note: the service could run with `DynamicUser`, however that makes
# impossible (for no good reason, see systemd issue #20495) to disable
# `NoNewPrivileges` or `ProtectHome`, which users may want to in order
# to run certain scripts in `networking.dhcpcd.runHook`.
users.users.dhcpcd = {
isSystemUser = true;
group = "dhcpcd";
};
users.groups.dhcpcd = { };
environment.systemPackages = [ dhcpcd ];
environment.etc."dhcpcd.exit-hook".text = cfg.runHook;
powerManagement.resumeCommands = lib.mkIf config.systemd.services.dhcpcd.enable ''
# Tell dhcpcd to rebind its interfaces if it's running.
/run/current-system/systemd/bin/systemctl reload dhcpcd.service
'';
security.polkit.extraConfig = lib.mkMerge [
(lib.mkIf config.services.resolved.enable ''
polkit.addRule(function(action, subject) {
if (action.id == 'org.freedesktop.resolve1.revert' ||
action.id == 'org.freedesktop.resolve1.set-dns-servers' ||
action.id == 'org.freedesktop.resolve1.set-domains') {
if (subject.user == 'dhcpcd') {
return polkit.Result.YES;
}
}
});
'')
(lib.mkIf cfg.setHostname ''
polkit.addRule(function(action, subject) {
if (action.id == 'org.freedesktop.hostname1.set-hostname' &&
subject.user == 'dhcpcd') {
return polkit.Result.YES;
}
});
'')
];
};
}

View File

@@ -0,0 +1,124 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.dnscache;
dnscache-root = pkgs.runCommand "dnscache-root" { preferLocalBuild = true; } ''
mkdir -p $out/{servers,ip}
${lib.concatMapStrings (ip: ''
touch "$out/ip/"${lib.escapeShellArg ip}
'') cfg.clientIps}
${lib.concatStrings (
lib.mapAttrsToList (host: ips: ''
${lib.concatMapStrings (ip: ''
echo ${lib.escapeShellArg ip} >> "$out/servers/"${lib.escapeShellArg host}
'') ips}
'') cfg.domainServers
)}
# if a list of root servers was not provided in config, copy it
# over. (this is also done by dnscache-conf, but we 'rm -rf
# /var/lib/dnscache/root' below & replace it wholesale with this,
# so we have to ensure servers/@ exists ourselves.)
if [ ! -e $out/servers/@ ]; then
# symlink does not work here, due chroot
cp ${pkgs.djbdns}/etc/dnsroots.global $out/servers/@;
fi
'';
in
{
###### interface
options = {
services.dnscache = {
enable = lib.mkOption {
default = false;
type = lib.types.bool;
description = "Whether to run the dnscache caching dns server.";
};
ip = lib.mkOption {
default = "0.0.0.0";
type = lib.types.str;
description = "IP address on which to listen for connections.";
};
clientIps = lib.mkOption {
default = [ "127.0.0.1" ];
type = lib.types.listOf lib.types.str;
description = "Client IP addresses (or prefixes) from which to accept connections.";
example = [
"192.168"
"172.23.75.82"
];
};
domainServers = lib.mkOption {
default = { };
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
description = ''
Table of {hostname: server} pairs to use as authoritative servers for hosts (and subhosts).
If entry for @ is not specified predefined list of root servers is used.
'';
example = lib.literalExpression ''
{
"@" = ["8.8.8.8" "8.8.4.4"];
"example.com" = ["192.168.100.100"];
}
'';
};
forwardOnly = lib.mkOption {
default = false;
type = lib.types.bool;
description = ''
Whether to treat root servers (for @) as caching
servers, requesting addresses the same way a client does. This is
needed if you want to use e.g. Google DNS as your upstream DNS.
'';
};
};
};
###### implementation
config = lib.mkIf config.services.dnscache.enable {
environment.systemPackages = [ pkgs.djbdns ];
users.users.dnscache = {
isSystemUser = true;
group = "dnscache";
};
users.groups.dnscache = { };
systemd.services.dnscache = {
description = "djbdns dnscache server";
wantedBy = [ "multi-user.target" ];
path = with pkgs; [
bash
daemontools
djbdns
];
preStart = ''
rm -rf /var/lib/dnscache
dnscache-conf dnscache dnscache /var/lib/dnscache ${config.services.dnscache.ip}
rm -rf /var/lib/dnscache/root
ln -sf ${dnscache-root} /var/lib/dnscache/root
'';
script = ''
cd /var/lib/dnscache/
${lib.optionalString cfg.forwardOnly "export FORWARDONLY=1"}
exec ./run
'';
};
};
}

View File

@@ -0,0 +1,145 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.dnscrypt-proxy;
in
{
imports = [
(lib.mkRenamedOptionModule [ "services" "dnscrypt-proxy2" ] [ "services" "dnscrypt-proxy" ])
];
options.services.dnscrypt-proxy = {
enable = lib.mkEnableOption "dnscrypt-proxy";
package = lib.mkPackageOption pkgs "dnscrypt-proxy" { };
settings = lib.mkOption {
description = ''
Attrset that is converted and passed as TOML config file.
For available params, see: <https://github.com/DNSCrypt/dnscrypt-proxy/blob/${pkgs.dnscrypt-proxy.version}/dnscrypt-proxy/example-dnscrypt-proxy.toml>
'';
example = lib.literalExpression ''
{
sources.public-resolvers = {
urls = [ "https://download.dnscrypt.info/resolvers-list/v2/public-resolvers.md" ];
cache_file = "public-resolvers.md";
minisign_key = "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3";
refresh_delay = 72;
};
}
'';
type = lib.types.attrs;
default = { };
};
upstreamDefaults = lib.mkOption {
description = ''
Whether to base the config declared in {option}`services.dnscrypt-proxy.settings` on the upstream example config (<https://github.com/DNSCrypt/dnscrypt-proxy/blob/master/dnscrypt-proxy/example-dnscrypt-proxy.toml>)
Disable this if you want to declare your dnscrypt config from scratch.
'';
type = lib.types.bool;
default = true;
};
configFile = lib.mkOption {
description = ''
Path to TOML config file. See: <https://github.com/DNSCrypt/dnscrypt-proxy/blob/master/dnscrypt-proxy/example-dnscrypt-proxy.toml>
If this option is set, it will override any configuration done in options.services.dnscrypt-proxy.settings.
'';
example = "/etc/dnscrypt-proxy/dnscrypt-proxy.toml";
type = lib.types.path;
default =
pkgs.runCommand "dnscrypt-proxy.toml"
{
json = builtins.toJSON cfg.settings;
passAsFile = [ "json" ];
}
''
${
if cfg.upstreamDefaults then
''
${pkgs.buildPackages.remarshal}/bin/toml2json ${pkgs.dnscrypt-proxy.src}/dnscrypt-proxy/example-dnscrypt-proxy.toml > example.json
${pkgs.buildPackages.jq}/bin/jq --slurp add example.json $jsonPath > config.json # merges the two
''
else
''
cp $jsonPath config.json
''
}
${pkgs.buildPackages.remarshal}/bin/json2toml < config.json > $out
'';
defaultText = lib.literalMD "TOML file generated from {option}`services.dnscrypt-proxy.settings`";
};
};
config = lib.mkIf cfg.enable {
networking.nameservers = lib.mkDefault [ "127.0.0.1" ];
systemd.services.dnscrypt-proxy = {
description = "DNSCrypt-proxy client";
wants = [
"network-online.target"
"nss-lookup.target"
];
before = [
"nss-lookup.target"
];
wantedBy = [
"multi-user.target"
];
aliases = [ "dnscrypt-proxy2.service" ];
serviceConfig = {
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
CacheDirectory = "dnscrypt-proxy";
DynamicUser = true;
ExecStart = "${lib.getExe cfg.package} -config ${cfg.configFile}";
LockPersonality = true;
LogsDirectory = "dnscrypt-proxy";
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
NonBlocking = true;
PrivateDevices = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
Restart = "always";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RuntimeDirectory = "dnscrypt-proxy";
StateDirectory = "dnscrypt-proxy";
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"@chown"
"~@aio"
"~@keyring"
"~@memlock"
"~@setuid"
"~@timer"
];
};
};
};
# uses attributes of the linked package
meta.buildDocsInSandbox = false;
}

View File

@@ -0,0 +1,195 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.dnsdist;
toLua = lib.generators.toLua { };
mkBind = cfg: toLua "${cfg.listenAddress}:${toString cfg.listenPort}";
configFile = pkgs.writeText "dnsdist.conf" ''
setLocal(${mkBind cfg})
${lib.optionalString cfg.dnscrypt.enable dnscryptSetup}
${cfg.extraConfig}
'';
dnscryptSetup = ''
last_rotation = 0
cert_serial = 0
provider_key = ${toLua cfg.dnscrypt.providerKey}
cert_lifetime = ${toLua cfg.dnscrypt.certLifetime} * 60
function file_exists(name)
local f = io.open(name, "r")
return f ~= nil and io.close(f)
end
function dnscrypt_setup()
-- generate provider keys on first run
if provider_key == nil then
provider_key = "/var/lib/dnsdist/private.key"
if not file_exists(provider_key) then
generateDNSCryptProviderKeys("/var/lib/dnsdist/public.key",
"/var/lib/dnsdist/private.key")
print("DNSCrypt: generated provider keypair")
end
end
-- generate resolver certificate
local now = os.time()
generateDNSCryptCertificate(
provider_key, "/run/dnsdist/resolver.cert", "/run/dnsdist/resolver.key",
cert_serial, now - 60, now + cert_lifetime)
addDNSCryptBind(
${mkBind cfg.dnscrypt}, ${toLua cfg.dnscrypt.providerName},
"/run/dnsdist/resolver.cert", "/run/dnsdist/resolver.key")
end
function maintenance()
-- certificate rotation
local now = os.time()
local dnscrypt = getDNSCryptBind(0)
if ((now - last_rotation) > 0.9 * cert_lifetime) then
-- generate and start using a new certificate
dnscrypt:generateAndLoadInMemoryCertificate(
provider_key, cert_serial + 1,
now - 60, now + cert_lifetime)
-- stop advertising the last certificate
dnscrypt:markInactive(cert_serial)
-- remove the second to last certificate
if (cert_serial > 1) then
dnscrypt:removeInactiveCertificate(cert_serial - 1)
end
print("DNSCrypt: rotated certificate")
-- increment serial number
cert_serial = cert_serial + 1
last_rotation = now
end
end
dnscrypt_setup()
'';
in
{
options = {
services.dnsdist = {
enable = lib.mkEnableOption "dnsdist domain name server";
listenAddress = lib.mkOption {
type = lib.types.str;
description = "Listen IP address";
default = "0.0.0.0";
};
listenPort = lib.mkOption {
type = lib.types.port;
description = "Listen port";
default = 53;
};
dnscrypt = {
enable = lib.mkEnableOption "a DNSCrypt endpoint to dnsdist";
listenAddress = lib.mkOption {
type = lib.types.str;
description = "Listen IP address of the endpoint";
default = "0.0.0.0";
};
listenPort = lib.mkOption {
type = lib.types.port;
description = "Listen port of the endpoint";
default = 443;
};
providerName = lib.mkOption {
type = lib.types.str;
default = "2.dnscrypt-cert.${config.networking.hostName}";
defaultText = lib.literalExpression "2.dnscrypt-cert.\${config.networking.hostName}";
example = "2.dnscrypt-cert.myresolver";
description = ''
The name that will be given to this DNSCrypt resolver.
::: {.note}
The provider name must start with `2.dnscrypt-cert.`.
:::
'';
};
providerKey = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
The filepath to the provider secret key.
If not given a new provider key pair will be generated in
/var/lib/dnsdist on the first run.
::: {.note}
The file must be readable by the dnsdist user/group.
:::
'';
};
certLifetime = lib.mkOption {
type = lib.types.ints.positive;
default = 15;
description = ''
The lifetime (in minutes) of the resolver certificate.
This will be automatically rotated before expiration.
'';
};
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Extra lines to be added verbatim to dnsdist.conf.
'';
};
};
};
config = lib.mkIf cfg.enable {
users.users.dnsdist = {
description = "dnsdist daemons user";
isSystemUser = true;
group = "dnsdist";
};
users.groups.dnsdist = { };
systemd.packages = [ pkgs.dnsdist ];
systemd.services.dnsdist = {
wantedBy = [ "multi-user.target" ];
startLimitIntervalSec = 0;
serviceConfig = {
User = "dnsdist";
Group = "dnsdist";
RuntimeDirectory = "dnsdist";
StateDirectory = "dnsdist";
# upstream overrides for better nixos compatibility
ExecStartPre = [
""
"${pkgs.dnsdist}/bin/dnsdist --check-config --config ${configFile}"
];
ExecStart = [
""
"${pkgs.dnsdist}/bin/dnsdist --supervised --disable-syslog --config ${configFile}"
];
};
};
};
}

View File

@@ -0,0 +1,68 @@
# Dnsmasq {#module-services-networking-dnsmasq}
Dnsmasq is an integrated DNS, DHCP and TFTP server for small networks.
## Configuration {#module-services-networking-dnsmasq-configuration}
### An authoritative DHCP and DNS server on a home network {#module-services-networking-dnsmasq-configuration-home}
On a home network, you can use Dnsmasq as a DHCP and DNS server. New devices on
your network will be configured by Dnsmasq, and instructed to use it as the DNS
server by default. This allows you to rely on your own server to perform DNS
queries and caching, with DNSSEC enabled.
The following example assumes that
- you have disabled your router's integrated DHCP server, if it has one
- your router's address is set in [](#opt-networking.defaultGateway.address)
- your system's Ethernet interface is `eth0`
- you have configured the address(es) to forward DNS queries in [](#opt-networking.nameservers)
```nix
{
services.dnsmasq = {
enable = true;
settings = {
interface = "eth0";
bind-interfaces = true; # Only bind to the specified interface
dhcp-authoritative = true; # Should be set when dnsmasq is definitely the only DHCP server on a network
server = config.networking.nameservers; # Upstream dns servers to which requests should be forwarded
dhcp-host = [
# Give the current system a fixed address of 192.168.0.254
"dc:a6:32:0b:ea:b9,192.168.0.254,${config.networking.hostName},infinite"
];
dhcp-option = [
# Address of the gateway, i.e. your router
"option:router,${config.networking.defaultGateway.address}"
];
dhcp-range = [
# Range of IPv4 addresses to give out
# <range start>,<range end>,<lease time>
"192.168.0.10,192.168.0.253,24h"
# Enable stateless IPv6 allocation
"::f,::ff,constructor:eth0,ra-stateless"
];
dhcp-rapid-commit = true; # Faster DHCP negotiation for IPv6
local-service = true; # Accept DNS queries only from hosts whose address is on a local subnet
log-queries = true; # Log results of all DNS queries
bogus-priv = true; # Don't forward requests for the local address ranges (192.168.x.x etc) to upstream nameservers
domain-needed = true; # Don't forward requests without dots or domain parts to upstream nameservers
dnssec = true; # Enable DNSSEC
# DNSSEC trust anchor. Source: https://data.iana.org/root-anchors/root-anchors.xml
trust-anchor = ".,20326,8,2,E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D";
};
};
}
```
## References {#module-services-networking-dnsmasq-references}
- Upstream website: <https://dnsmasq.org>
- Manpage: <https://dnsmasq.org/docs/dnsmasq-man.html>
- FAQ: <https://dnsmasq.org/docs/FAQ>

View File

@@ -0,0 +1,199 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.dnsmasq;
dnsmasq = cfg.package;
stateDir = "/var/lib/dnsmasq";
# True values are just put as `name` instead of `name=true`, and false values
# are turned to comments (false values are expected to be overrides e.g.
# lib.mkForce)
formatKeyValue =
name: value:
if value == true then
name
else if value == false then
"# setting `${name}` explicitly set to false"
else
lib.generators.mkKeyValueDefault { } "=" name value;
settingsFormat = pkgs.formats.keyValue {
mkKeyValue = formatKeyValue;
listsAsDuplicateKeys = true;
};
dnsmasqConf = settingsFormat.generate "dnsmasq.conf" cfg.settings;
in
{
imports = [
(lib.mkRenamedOptionModule
[ "services" "dnsmasq" "servers" ]
[ "services" "dnsmasq" "settings" "server" ]
)
(lib.mkRemovedOptionModule [
"services"
"dnsmasq"
"extraConfig"
] "This option has been replaced by `services.dnsmasq.settings`")
];
###### interface
options = {
services.dnsmasq = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to run dnsmasq.
'';
};
package = lib.mkPackageOption pkgs "dnsmasq" { };
resolveLocalQueries = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether dnsmasq should resolve local queries (i.e. add 127.0.0.1 to
/etc/resolv.conf).
'';
};
alwaysKeepRunning = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
If enabled, systemd will always respawn dnsmasq even if shut down manually. The default, disabled, will only restart it on error.
'';
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options.server = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"8.8.8.8"
"8.8.4.4"
];
description = ''
The DNS servers which dnsmasq should query.
'';
};
};
default = { };
description = ''
Configuration of dnsmasq. Lists get added one value per line (empty
lists and false values don't get added, though false values get
turned to comments). Gets merged with
{
dhcp-leasefile = "${stateDir}/dnsmasq.leases";
conf-file = optional cfg.resolveLocalQueries "/etc/dnsmasq-conf.conf";
resolv-file = optional cfg.resolveLocalQueries "/etc/dnsmasq-resolv.conf";
}
'';
example = lib.literalExpression ''
{
domain-needed = true;
dhcp-range = [ "192.168.0.2,192.168.0.254" ];
}
'';
};
configFile = lib.mkOption {
type = lib.types.package;
readOnly = true;
description = ''
Path to the configuration file of dnsmasq.
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
services.dnsmasq = {
settings = {
dhcp-leasefile = lib.mkDefault "${stateDir}/dnsmasq.leases";
conf-file = lib.mkDefault (lib.optional cfg.resolveLocalQueries "/etc/dnsmasq-conf.conf");
resolv-file = lib.mkDefault (lib.optional cfg.resolveLocalQueries "/etc/dnsmasq-resolv.conf");
};
configFile = dnsmasqConf;
};
networking.nameservers = lib.optional cfg.resolveLocalQueries "127.0.0.1";
services.dbus.packages = [ dnsmasq ];
users.users.dnsmasq = {
isSystemUser = true;
group = "dnsmasq";
description = "Dnsmasq daemon user";
};
users.groups.dnsmasq = { };
networking.resolvconf = lib.mkIf cfg.resolveLocalQueries {
useLocalResolver = lib.mkDefault true;
extraConfig = ''
dnsmasq_conf=/etc/dnsmasq-conf.conf
dnsmasq_resolv=/etc/dnsmasq-resolv.conf
'';
subscriberFiles = [
"/etc/dnsmasq-conf.conf"
"/etc/dnsmasq-resolv.conf"
];
};
systemd.services.dnsmasq = {
description = "Dnsmasq Daemon";
after = [
"network.target"
"systemd-resolved.service"
];
wantedBy = [ "multi-user.target" ];
path = [ dnsmasq ];
preStart = ''
mkdir -m 755 -p ${stateDir}
touch ${stateDir}/dnsmasq.leases
chown -R dnsmasq ${stateDir}
${lib.optionalString cfg.resolveLocalQueries "touch /etc/dnsmasq-{conf,resolv}.conf"}
dnsmasq --test -C ${cfg.configFile}
'';
serviceConfig = {
Type = "dbus";
BusName = "uk.org.thekelleys.dnsmasq";
ExecStart = "${dnsmasq}/bin/dnsmasq -k --enable-dbus --user=dnsmasq -C ${cfg.configFile}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
PrivateTmp = true;
ProtectSystem = true;
ProtectHome = true;
Restart = if cfg.alwaysKeepRunning then "always" else "on-failure";
};
restartTriggers = [ config.environment.etc.hosts.source ];
};
};
meta.doc = ./dnsmasq.md;
}

View File

@@ -0,0 +1,120 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
escapeShellArgs
getExe
lists
literalExpression
maintainers
mkEnableOption
mkIf
mkOption
mkPackageOption
types
;
cfg = config.services.dnsproxy;
yaml = pkgs.formats.yaml { };
configFile = yaml.generate "config.yaml" cfg.settings;
finalFlags = (lists.optional (cfg.settings != { }) "--config-path=${configFile}") ++ cfg.flags;
in
{
options.services.dnsproxy = {
enable = mkEnableOption "dnsproxy";
package = mkPackageOption pkgs "dnsproxy" { };
settings = mkOption {
type = yaml.type;
default = { };
example = literalExpression ''
{
bootstrap = [
"8.8.8.8:53"
];
listen-addrs = [
"0.0.0.0"
];
listen-ports = [
53
];
upstream = [
"1.1.1.1:53"
];
}
'';
description = ''
Contents of the `config.yaml` config file.
The `--config-path` argument will only be passed if this set is not empty.
See <https://github.com/AdguardTeam/dnsproxy/blob/master/config.yaml.dist>.
'';
};
flags = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--upstream=1.1.1.1:53" ];
description = ''
A list of extra command-line flags to pass to dnsproxy. For details on the
available options, see <https://github.com/AdguardTeam/dnsproxy#usage>.
Keep in mind that options passed through command-line flags override
config options.
'';
};
};
config = mkIf cfg.enable {
systemd.services.dnsproxy = {
description = "Simple DNS proxy with DoH, DoT, DoQ and DNSCrypt support";
after = [
"network.target"
"nss-lookup.target"
];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${getExe cfg.package} ${escapeShellArgs finalFlags}";
Restart = "always";
RestartSec = 10;
DynamicUser = true;
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
ProtectClock = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
SystemCallFilter = [
"@system-service"
"~@privileged @resources"
];
};
};
};
meta.maintainers = with maintainers; [ diogotcorreia ];
}

View File

@@ -0,0 +1,69 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.doh-proxy-rust;
in
{
options.services.doh-proxy-rust = {
enable = lib.mkEnableOption "doh-proxy-rust";
flags = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "--server-address=9.9.9.9:53" ];
description = ''
A list of command-line flags to pass to doh-proxy. For details on the
available options, see <https://github.com/jedisct1/doh-server#usage>.
'';
};
};
config = lib.mkIf cfg.enable {
systemd.services.doh-proxy-rust = {
description = "doh-proxy-rust";
after = [
"network.target"
"nss-lookup.target"
];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${pkgs.doh-proxy-rust}/bin/doh-proxy ${lib.escapeShellArgs cfg.flags}";
Restart = "always";
RestartSec = 10;
DynamicUser = true;
CapabilityBoundingSet = "";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
ProtectClock = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
RemoveIPC = true;
RestrictAddressFamilies = "AF_INET AF_INET6";
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
SystemCallFilter = [
"@system-service"
"~@privileged @resources"
];
};
};
};
meta.maintainers = with lib.maintainers; [ stephank ];
}

View File

@@ -0,0 +1,72 @@
# DNS-over-HTTPS Server {#module-service-doh-server}
[DNS-over-HTTPS](https://github.com/m13253/dns-over-https) is a high performance DNS over HTTPS client & server. This module enables its server part (`doh-server`).
## Quick Start {#module-service-doh-server-quick-start}
Setup with Nginx + ACME (recommended):
```nix
{
services.doh-server = {
enable = true;
settings = {
upstream = [ "udp:1.1.1.1:53" ];
};
};
services.nginx = {
enable = true;
virtualHosts."doh.example.com" = {
enableACME = true;
forceSSL = true;
http2 = true;
locations."/".return = 404;
locations."/dns-query" = {
proxyPass = "http://127.0.0.1:8053/dns-query";
recommendedProxySettings = true;
};
};
# and other virtual hosts ...
};
security.acme = {
acceptTerms = true;
defaults.email = "you@example.com";
};
networking.firewall.allowedTCPPorts = [
80
443
];
}
```
`doh-server` can also work as a standalone HTTPS web server (with SSL cert and key specified), but this is not recommended as `doh-server` does not do OCSP Stabbing.
Setup a standalone instance with ACME:
```nix
let
domain = "doh.example.com";
in
{
security.acme.certs.${domain} = {
dnsProvider = "cloudflare";
credentialFiles."CF_DNS_API_TOKEN_FILE" = "/run/secrets/cf-api-token";
};
services.doh-server = {
enable = true;
settings = {
listen = [ ":443" ];
upstream = [ "udp:1.1.1.1:53" ];
};
useACMEHost = domain;
};
networking.firewall.allowedTCPPorts = [ 443 ];
}
```
See a full configuration in <https://github.com/m13253/dns-over-https/blob/master/doh-server/doh-server.conf>.

View File

@@ -0,0 +1,176 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.doh-server;
toml = pkgs.formats.toml { };
in
{
options.services.doh-server = {
enable = lib.mkEnableOption "DNS-over-HTTPS server";
package = lib.mkPackageOption pkgs "dns-over-https" { };
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = toml.type;
options = {
listen = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
"127.0.0.1:8053"
"[::1]:8053"
];
example = [ ":443" ];
description = "HTTP listen address and port";
};
path = lib.mkOption {
type = lib.types.str;
default = "/dns-query";
example = "/dns-query";
description = "HTTP path for resolve application";
};
upstream = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
"udp:1.1.1.1:53"
"udp:1.0.0.1:53"
"udp:8.8.8.8:53"
"udp:8.8.4.4:53"
];
example = [ "udp:127.0.0.1:53" ];
description = ''
Upstream DNS resolver.
If multiple servers are specified, a random one will be chosen each time.
You can use "udp", "tcp" or "tcp-tls" for the type prefix.
For "udp", UDP will first be used, and switch to TCP when the server asks to or the response is too large.
For "tcp", only TCP will be used.
For "tcp-tls", DNS-over-TLS (RFC 7858) will be used to secure the upstream connection.
'';
};
timeout = lib.mkOption {
type = lib.types.int;
default = 10;
example = 15;
description = "Upstream timeout";
};
tries = lib.mkOption {
type = lib.types.int;
default = 3;
example = 5;
description = "Number of tries if upstream DNS fails";
};
verbose = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = "Enable logging";
};
log_guessed_client_ip = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
Enable log IP from HTTPS-reverse proxy header: X-Forwarded-For or X-Real-IP
Note: http uri/useragent log cannot be controlled by this config
'';
};
ecs_allow_non_global_ip = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
By default, non global IP addresses are never forwarded to upstream servers.
This is to prevent two things from happening:
1. the upstream server knowing your private LAN addresses;
2. the upstream server unable to provide geographically near results,
or even fail to provide any result.
However, if you are deploying a split tunnel corporation network environment, or for any other reason you want to inhibit this behavior and allow local (eg RFC1918) address to be forwarded, change the following option to "true".
'';
};
ecs_use_precise_ip = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
If ECS is added to the request, let the full IP address or cap it to 24 or 128 mask. This option is to be used only on private networks where knowledge of the terminal endpoint may be required for security purposes (eg. DNS Firewalling). Not a good option on the internet where IP address may be used to identify the user and not only the approximate location.
'';
};
};
};
default = { };
example = {
listen = [ ":8153" ];
upstream = [ "udp:127.0.0.1:53" ];
};
description = "Configuration of doh-server in toml. See example in <https://github.com/m13253/dns-over-https/blob/master/doh-server/doh-server.conf>";
};
useACMEHost = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "doh.example.com";
description = ''
A host of an existing Let's Encrypt certificate to use.
*Note that this option does not create any certificates, nor it does add subdomains to existing ones you will need to create them manually using [](#opt-security.acme.certs).*
'';
};
configFile = lib.mkOption {
type = lib.types.path;
example = "/path/to/doh-server.conf";
description = ''
The config file for the doh-server.
Setting this option will override any configuration applied by the `settings` option.
'';
};
};
config = lib.mkIf cfg.enable {
services.doh-server.configFile = lib.mkDefault (toml.generate "doh-server.conf" cfg.settings);
services.doh-server.settings = lib.mkIf (cfg.useACMEHost != null) (
let
sslCertDir = config.security.acme.certs.${cfg.useACMEHost}.directory;
in
{
cert = "${sslCertDir}/cert.pem";
key = "${sslCertDir}/key.pem";
}
);
systemd.services.doh-server = {
description = "DNS-over-HTTPS Server";
documentation = [ "https://github.com/m13253/dns-over-https" ];
after = [
"network.target"
]
++ lib.optional (cfg.useACMEHost != null) "acme-${cfg.useACMEHost}.service";
wants = lib.optional (cfg.useACMEHost != null) "acme-${cfg.useACMEHost}.service";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
ExecStart = "${cfg.package}/bin/doh-server -conf ${cfg.configFile}";
LimitNOFILE = 1048576;
Restart = "always";
RestartSec = 3;
Type = "simple";
DynamicUser = true;
SupplementaryGroups = lib.optional (cfg.useACMEHost != null) "acme";
};
};
};
meta.maintainers = with lib.maintainers; [ DictXiong ];
meta.doc = ./doh-server.md;
}

View File

@@ -0,0 +1,31 @@
# dsnet {#module-services-dsnet}
dsnet is a CLI tool to manage a centralised wireguard server. It allows easy
generation of client configuration, handling key generation, IP allocation etc.
It keeps its own configuration at `/etc/dsnetconfig.json`, which is more of a
database. It contains key material too.
The way this module works is to patch this database with whatever is configured
in the nix service instantiation. This happens automatically when required.
This way it is possible to decide what to let dnset manage and what parts you
want to keep declaratively.
```
services.dsnet = {
enable = true;
settings = {
ExternalHostname = "vpn.example.com";
Network = "10.171.90.0/24";
Network6 = "";
IP = "10.171.90.1";
IP6 = "";
DNS = "10.171.90.1";
Networks = [ "0.0.0.0/0" ];
};
```
See <https://github.com/naggie/dsnet> for more information.

View File

@@ -0,0 +1,184 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.dsnet;
settingsFormat = pkgs.formats.json { };
patchFile = settingsFormat.generate "dsnet-patch.json" cfg.settings;
in
{
options.services.dsnet = {
enable = lib.mkEnableOption "dsnet, a centralised Wireguard VPN manager";
package = lib.mkPackageOption pkgs "dsnet" { };
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options = {
ExternalHostname = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "vpn.example.com";
description = ''
The hostname that clients should use to connect to this server.
This is used to generate the client configuration files.
This is preferred over ExternalIP, as it allows for IPv4 and
IPv6, as well as enabling the ability tp change IP.
'';
};
ExternalIP = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "192.0.2.1";
description = ''
The external IP address of the server. This is used to generate
the client configuration files for when an ExternalHostname is not set.
Leaving this empty will cause dsnet to use the IP address of
what looks like the WAN interface.
'';
};
ExternalIP6 = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "2001:db8::1";
description = ''
The external IPv6 address of the server. This is used to generate
the client configuration files for when an ExternalHostname is
not set. Used in preference to ExternalIP.
Leaving this empty will cause dsnet to use the IP address of
what looks like the WAN interface.
'';
};
Network = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "172.18.0.0/24";
description = ''
The IPv4 network that the server will use to allocate IPs on the network.
Leave this empty to let dsnet choose a network.
'';
};
Network6 = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "2001:db8::1/64";
description = ''
The IPv6 network that the server will use to allocate IPs on the
network.
Leave this empty to let dsnet choose a network.
'';
};
IP = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "172.18.0.1";
description = ''
The IPv4 address that the server will use on the network.
Leave this empty to let dsnet choose an address.
'';
};
IP6 = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "2001:db8::1";
description = ''
The IPv6 address that the server will use on the network
Leave this empty to let dsnet choose an address.
'';
};
Networks = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
example = [
"0.0.0.0/0"
"192.168.0.0/24"
];
description = ''
The CIDR networks that should route through this server. Clients
will be configured to route traffic for these networks through
the server peer.
'';
};
};
};
default = { };
description = ''
The settings to use for dsnet. This will be converted to a JSON
object that will be passed to dsnet as a patch, using the patch
command when the service is started. See the dsnet documentation for
more information on the additional options.
Note that the resulting /etc/dsnetconfg.json is more of a database
than it is a configuration file. It is therefore recommended that
system specific values are configured here, rather than the full
configuration including peers.
Peers may be managed via the dsnet add/remove commands, negating the
need to manage key material and cumbersom configuration with nix. If
you want peer configuration in nix, you may as well use the regular
wireguard module.
'';
example = {
ExternalHostname = "vpn.example.com";
ExternalIP = "127.0.0.1";
ExternalIP6 = "";
ListenPort = 51820;
Network = "10.3.148.0/22";
Network6 = "";
IP = "10.3.148.1";
IP6 = "";
DNS = "8.8.8.8";
Networks = [ "0.0.0.0/0" ];
};
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
systemd.services.dsnet = {
description = "dsnet VPN Management";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
preStart = ''
test ! -f /etc/dsnetconfig.json && ${lib.getExe cfg.package} init
${lib.getExe cfg.package} patch < ${patchFile}
'';
serviceConfig = {
ExecStart = "${lib.getExe cfg.package} up";
ExecStop = "${lib.getExe cfg.package} down";
Type = "oneshot";
# consider the service to be active after process exits, so it can be
# reloaded
RemainAfterExit = true;
};
reload = ''
${lib.getExe cfg.package} patch < ${patchFile}
${lib.getExe cfg.package} sync < ${patchFile}
'';
# reload _instead_ of restarting on change
reloadIfChanged = true;
};
};
}

View File

@@ -0,0 +1,292 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.easytier;
settingsFormat = pkgs.formats.toml { };
genFinalSettings =
inst:
attrsets.filterAttrsRecursive (_: v: v != { }) (
attrsets.filterAttrsRecursive (_: v: v != null) (
{
inherit (inst.settings)
instance_name
hostname
ipv4
dhcp
listeners
;
network_identity = {
inherit (inst.settings) network_name network_secret;
};
peer = map (p: { uri = p; }) inst.settings.peers;
}
// inst.extraSettings
)
);
genConfigFile =
name: inst:
if inst.configFile == null then
settingsFormat.generate "easytier-${name}.toml" (genFinalSettings inst)
else
inst.configFile;
activeInsts = filterAttrs (_: inst: inst.enable) cfg.instances;
settingsModule = name: {
options = {
instance_name = mkOption {
type = types.str;
default = name;
description = "Identify different instances on same host";
};
hostname = mkOption {
type = with types; nullOr str;
default = null;
description = "Hostname shown in peer list and web console.";
};
network_name = mkOption {
type = with types; nullOr str;
default = null;
description = "EasyTier network name.";
};
network_secret = mkOption {
type = with types; nullOr str;
default = null;
description = ''
EasyTier network credential used for verification and
encryption. It can also be set in environmentFile.
'';
};
ipv4 = mkOption {
type = with types; nullOr str;
default = null;
description = ''
IPv4 cidr address of this peer in the virtual network. If
empty, this peer will only forward packets and no TUN device
will be created.
'';
example = "10.144.144.1/24";
};
dhcp = mkOption {
type = types.bool;
default = false;
description = ''
Automatically determine the IPv4 address of this peer based on
existing peers on network.
'';
};
listeners = mkOption {
type = with types; listOf str;
default = [
"tcp://0.0.0.0:11010"
"udp://0.0.0.0:11010"
];
description = ''
Listener addresses to accept connections from other peers.
Valid format is: `<proto>://<addr>:<port>`, where the protocol
can be `tcp`, `udp`, `ring`, `wg`, `ws`, `wss`.
'';
};
peers = mkOption {
type = with types; listOf str;
default = [ ];
description = ''
Peers to connect initially. Valid format is: `<proto>://<addr>:<port>`.
'';
example = [
"tcp://example.com:11010"
];
};
};
};
instanceModule =
{ name, ... }:
{
options = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable the instance.";
};
configServer = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Configure the instance from config server. When this option
set, any other settings for configuring the instance manually
except `hostname` will be ignored. Valid formats are:
- full uri for custom server: `udp://example.com:22020/<token>`
- username only for official server: `<token>`
'';
example = "udp://example.com:22020/myusername";
};
configFile = mkOption {
type = with types; nullOr path;
default = null;
description = ''
Path to easytier config file. Setting this option will
override `settings` and `extraSettings` of this instance.
'';
};
environmentFiles = mkOption {
type = with types; listOf path;
default = [ ];
description = ''
Environment files for this instance. All command-line args
have corresponding environment variables.
'';
example = literalExpression ''
[
/path/to/.env
/path/to/.env.secret
]
'';
};
settings = mkOption {
type = types.submodule (settingsModule name);
default = { };
description = ''
Settings to generate {file}`easytier-${name}.toml`
'';
};
extraSettings = mkOption {
type = settingsFormat.type;
default = { };
description = ''
Extra settings to add to {file}`easytier-${name}.toml`.
'';
};
extraArgs = mkOption {
type = with types; listOf str;
default = [ ];
description = ''
Extra args append to the easytier command-line.
'';
};
};
};
in
{
options.services.easytier = {
enable = mkEnableOption "EasyTier daemon";
package = mkPackageOption pkgs "easytier" { };
allowSystemForward = mkEnableOption ''
Allow the system to forward packets from easytier. Useful when
`proxy_forward_by_system` enabled.
'';
instances = mkOption {
description = ''
EasyTier instances.
'';
type = types.attrsOf (types.submodule instanceModule);
default = { };
example = {
settings = {
network_name = "easytier";
network_secret = "easytier";
ipv4 = "10.144.144.1/24";
peers = [
"tcp://public.easytier.cn:11010"
"wss://example.com:443"
];
};
extraSettings = {
flags.dev_name = "tun1";
};
};
};
};
config = mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
systemd.services = mapAttrs' (
name: inst:
let
configFile = genConfigFile name inst;
in
nameValuePair "easytier-${name}" {
description = "EasyTier Daemon - ${name}";
wants = [
"network-online.target"
"nss-lookup.target"
];
after = [
"network-online.target"
"nss-lookup.target"
];
wantedBy = [ "multi-user.target" ];
path = with pkgs; [
cfg.package
iproute2
bash
];
restartTriggers = inst.environmentFiles ++ (optionals (inst.configServer == null) [ configFile ]);
serviceConfig = {
Type = "simple";
Restart = "on-failure";
EnvironmentFile = inst.environmentFiles;
StateDirectory = "easytier/easytier-${name}";
StateDirectoryMode = "0700";
WorkingDirectory = "/var/lib/easytier/easytier-${name}";
ExecStart = escapeShellArgs (
[
"${cfg.package}/bin/easytier-core"
]
++ optionals (inst.configServer != null) (
[
"-w"
"${inst.configServer}"
]
++ (optionals (inst.settings.hostname != null) [
"--hostname"
"${inst.settings.hostname}"
])
)
++ optionals (inst.configServer == null) [
"-c"
"${configFile}"
]
++ inst.extraArgs
);
};
}
) activeInsts;
boot.kernel.sysctl = mkIf cfg.allowSystemForward {
"net.ipv4.conf.all.forwarding" = mkOverride 97 true;
"net.ipv6.conf.all.forwarding" = mkOverride 97 true;
};
};
meta.maintainers = with maintainers; [
ltrump
];
}

View File

@@ -0,0 +1,166 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.ejabberd;
ctlcfg = pkgs.writeText "ejabberdctl.cfg" ''
ERL_EPMD_ADDRESS=127.0.0.1
${cfg.ctlConfig}
'';
ectl = ''${cfg.package}/bin/ejabberdctl ${
lib.optionalString (cfg.configFile != null) "--config ${cfg.configFile}"
} --ctl-config "${ctlcfg}" --spool "${cfg.spoolDir}" --logs "${cfg.logsDir}"'';
dumps = lib.escapeShellArgs cfg.loadDumps;
in
{
###### interface
options = {
services.ejabberd = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to enable ejabberd server";
};
package = lib.mkPackageOption pkgs "ejabberd" { };
user = lib.mkOption {
type = lib.types.str;
default = "ejabberd";
description = "User under which ejabberd is ran";
};
group = lib.mkOption {
type = lib.types.str;
default = "ejabberd";
description = "Group under which ejabberd is ran";
};
spoolDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/ejabberd";
description = "Location of the spooldir of ejabberd";
};
logsDir = lib.mkOption {
type = lib.types.path;
default = "/var/log/ejabberd";
description = "Location of the logfile directory of ejabberd";
};
configFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
description = "Configuration file for ejabberd in YAML format";
default = null;
};
ctlConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Configuration of ejabberdctl";
};
loadDumps = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ ];
description = "Configuration dumps that should be loaded on the first startup";
example = lib.literalExpression "[ ./myejabberd.dump ]";
};
imagemagick = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Add ImageMagick to server's path; allows for image thumbnailing";
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
users.users = lib.optionalAttrs (cfg.user == "ejabberd") {
ejabberd = {
group = cfg.group;
home = cfg.spoolDir;
createHome = true;
uid = config.ids.uids.ejabberd;
};
};
users.groups = lib.optionalAttrs (cfg.group == "ejabberd") {
ejabberd.gid = config.ids.gids.ejabberd;
};
systemd.services.ejabberd = {
description = "ejabberd server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
path = [
pkgs.findutils
pkgs.coreutils
]
++ lib.optional cfg.imagemagick pkgs.imagemagick;
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStart = "${ectl} foreground";
ExecStop = "${ectl} stop";
ExecReload = "${ectl} reload_config";
};
preStart = ''
if [ -z "$(ls -A '${cfg.spoolDir}')" ]; then
touch "${cfg.spoolDir}/.firstRun"
fi
if ! test -e ${cfg.spoolDir}/.erlang.cookie; then
touch ${cfg.spoolDir}/.erlang.cookie
chmod 600 ${cfg.spoolDir}/.erlang.cookie
dd if=/dev/random bs=16 count=1 | base64 > ${cfg.spoolDir}/.erlang.cookie
fi
'';
postStart = ''
while ! ${ectl} status >/dev/null 2>&1; do
if ! kill -0 "$MAINPID"; then exit 1; fi
sleep 0.1
done
if [ -e "${cfg.spoolDir}/.firstRun" ]; then
rm "${cfg.spoolDir}/.firstRun"
for src in ${dumps}; do
find "$src" -type f | while read dump; do
echo "Loading configuration dump at $dump"
${ectl} load "$dump"
done
done
fi
'';
};
systemd.tmpfiles.rules = [
"d '${cfg.logsDir}' 0750 ${cfg.user} ${cfg.group} -"
"d '${cfg.spoolDir}' 0700 ${cfg.user} ${cfg.group} -"
];
security.pam.services.ejabberd = { };
};
}

View File

@@ -0,0 +1,116 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.envoy;
format = pkgs.formats.json { };
conf = format.generate "envoy.json" cfg.settings;
validateConfig =
required: file:
pkgs.runCommand "validate-envoy-conf" { } ''
${cfg.package}/bin/envoy --log-level error --mode validate -c "${file}" ${
lib.optionalString (!required) "|| true"
}
cp "${file}" "$out"
'';
in
{
options.services.envoy = {
enable = lib.mkEnableOption "Envoy reverse proxy";
package = lib.mkPackageOption pkgs "envoy" { };
requireValidConfig = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether a failure during config validation at build time is fatal.
When the config can't be checked during build time, for example when it includes
other files, disable this option.
'';
};
settings = lib.mkOption {
type = format.type;
default = { };
example = lib.literalExpression ''
{
admin = {
access_log_path = "/dev/null";
address = {
socket_address = {
protocol = "TCP";
address = "127.0.0.1";
port_value = 9901;
};
};
};
static_resources = {
listeners = [];
clusters = [];
};
}
'';
description = ''
Specify the configuration for Envoy in Nix.
'';
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
systemd.services.envoy = {
description = "Envoy reverse proxy";
after = [ "network-online.target" ];
requires = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/envoy -c ${validateConfig cfg.requireValidConfig conf}";
CacheDirectory = [ "envoy" ];
LogsDirectory = [ "envoy" ];
Restart = "no";
# Hardening
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
DeviceAllow = [ "" ];
DevicePolicy = "closed";
DynamicUser = true;
LockPersonality = true;
MemoryDenyWriteExecute = false; # at least wasmr needs WX permission
PrivateDevices = true;
PrivateUsers = false; # breaks CAP_NET_BIND_SERVICE
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "ptraceable";
ProtectSystem = "strict";
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
"AF_NETLINK"
"AF_XDP"
];
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
];
UMask = "0066";
};
};
};
}

View File

@@ -0,0 +1,67 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.epmd;
in
{
###### interface
options.services.epmd = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable socket activation for Erlang Port Mapper Daemon (epmd),
which acts as a name server on all hosts involved in distributed
Erlang computations.
'';
};
package = lib.mkPackageOption pkgs "erlang" { };
listenStream = lib.mkOption {
type = lib.types.str;
default = "[::]:4369";
description = ''
the listenStream used by the systemd socket.
see <https://www.freedesktop.org/software/systemd/man/systemd.socket.html#ListenStream=> for more information.
use this to change the port epmd will run on.
if not defined, epmd will use "[::]:4369"
'';
};
};
###### implementation
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.listenStream == "[::]:4369" -> config.networking.enableIPv6;
message = "epmd listens by default on ipv6, enable ipv6 or change config.services.epmd.listenStream";
}
];
systemd.sockets.epmd = rec {
description = "Erlang Port Mapper Daemon Activation Socket";
wantedBy = [ "sockets.target" ];
before = wantedBy;
socketConfig = {
ListenStream = cfg.listenStream;
Accept = "false";
};
};
systemd.services.epmd = {
description = "Erlang Port Mapper Daemon";
after = [ "network.target" ];
requires = [ "epmd.socket" ];
serviceConfig = {
DynamicUser = true;
ExecStart = "${cfg.package}/bin/epmd -systemd";
Type = "notify";
};
};
};
meta.maintainers = lib.teams.beam.members;
}

View File

@@ -0,0 +1,162 @@
{
config,
lib,
options,
pkgs,
...
}:
let
cfg = config.services.ergo;
opt = options.services.ergo;
inherit (lib)
literalExpression
mkEnableOption
mkIf
mkOption
optionalString
types
;
configFile = pkgs.writeText "ergo.conf" (
''
ergo {
directory = "${cfg.dataDir}"
node {
mining = false
}
wallet.secretStorage.secretDir = "${cfg.dataDir}/wallet/keystore"
}
scorex {
network {
bindAddress = "${cfg.listen.ip}:${toString cfg.listen.port}"
}
''
+ optionalString (cfg.api.keyHash != null) ''
restApi {
apiKeyHash = "${cfg.api.keyHash}"
bindAddress = "${cfg.api.listen.ip}:${toString cfg.api.listen.port}"
}
''
+ ''
}
''
);
in
{
options = {
services.ergo = {
enable = mkEnableOption "Ergo service";
dataDir = mkOption {
type = types.path;
default = "/var/lib/ergo";
description = "The data directory for the Ergo node.";
};
listen = {
ip = mkOption {
type = types.str;
default = "0.0.0.0";
description = "IP address on which the Ergo node should listen.";
};
port = mkOption {
type = types.port;
default = 9006;
description = "Listen port for the Ergo node.";
};
};
api = {
keyHash = mkOption {
type = types.nullOr types.str;
default = null;
example = "324dcf027dd4a30a932c441f365a25e86b173defa4b8e58948253471b81b72cf";
description = "Hex-encoded Blake2b256 hash of an API key as a 64-chars long Base16 string.";
};
listen = {
ip = mkOption {
type = types.str;
default = "0.0.0.0";
description = "IP address that the Ergo node API should listen on if {option}`api.keyHash` is defined.";
};
port = mkOption {
type = types.port;
default = 9052;
description = "Listen port for the API endpoint if {option}`api.keyHash` is defined.";
};
};
};
testnet = mkOption {
type = types.bool;
default = false;
description = "Connect to testnet network instead of the default mainnet.";
};
user = mkOption {
type = types.str;
default = "ergo";
description = "The user as which to run the Ergo node.";
};
group = mkOption {
type = types.str;
default = cfg.user;
defaultText = literalExpression "config.${opt.user}";
description = "The group as which to run the Ergo node.";
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Open ports in the firewall for the Ergo node as well as the API.";
};
};
};
config = mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' 0770 '${cfg.user}' '${cfg.group}' - -"
];
systemd.services.ergo = {
description = "ergo server";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStart = ''
${pkgs.ergo}/bin/ergo \
${optionalString (!cfg.testnet) "--mainnet"} \
-c ${configFile}'';
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.listen.port ] ++ [ cfg.api.listen.port ];
};
users.users.${cfg.user} = {
name = cfg.user;
group = cfg.group;
description = "Ergo daemon user";
home = cfg.dataDir;
isSystemUser = true;
};
users.groups.${cfg.group} = { };
};
}

View File

@@ -0,0 +1,167 @@
{
config,
lib,
options,
pkgs,
...
}:
let
cfg = config.services.ergochat;
in
{
options = {
services.ergochat = {
enable = lib.mkEnableOption "Ergo IRC daemon";
openFilesLimit = lib.mkOption {
type = lib.types.int;
default = 1024;
description = ''
Maximum number of open files. Limits the clients and server connections.
'';
};
configFile = lib.mkOption {
type = lib.types.path;
default = (pkgs.formats.yaml { }).generate "ergo.conf" cfg.settings;
defaultText = lib.literalMD "generated config file from `settings`";
description = ''
Path to configuration file.
Setting this will skip any configuration done via `settings`
'';
};
settings = lib.mkOption {
type = (pkgs.formats.yaml { }).type;
description = ''
Ergo IRC daemon configuration file.
https://raw.githubusercontent.com/ergochat/ergo/master/default.yaml
'';
default = {
network = {
name = "testnetwork";
};
server = {
name = "example.com";
listeners = {
":6667" = { };
};
casemapping = "permissive";
enforce-utf = true;
lookup-hostnames = false;
ip-cloaking = {
enabled = false;
};
forward-confirm-hostnames = false;
check-ident = false;
relaymsg = {
enabled = false;
};
max-sendq = "1M";
ip-limits = {
count = false;
throttle = false;
};
};
datastore = {
autoupgrade = true;
# this points to the StateDirectory of the systemd service
path = "/var/lib/ergo/ircd.db";
};
accounts = {
authentication-enabled = true;
registration = {
enabled = true;
allow-before-connect = true;
throttling = {
enabled = true;
duration = "10m";
max-attempts = 30;
};
bcrypt-cost = 4;
email-verification.enabled = false;
};
multiclient = {
enabled = true;
allowed-by-default = true;
always-on = "opt-out";
auto-away = "opt-out";
};
};
channels = {
default-modes = "+ntC";
registration = {
enabled = true;
};
};
limits = {
nicklen = 32;
identlen = 20;
channellen = 64;
awaylen = 390;
kicklen = 390;
topiclen = 390;
};
history = {
enabled = true;
channel-length = 2048;
client-length = 256;
autoresize-window = "3d";
autoreplay-on-join = 0;
chathistory-maxmessages = 100;
znc-maxmessages = 2048;
restrictions = {
expire-time = "1w";
query-cutoff = "none";
grace-period = "1h";
};
retention = {
allow-individual-delete = false;
enable-account-indexing = false;
};
tagmsg-storage = {
default = false;
whitelist = [
"+draft/react"
"+react"
];
};
};
};
};
};
};
config = lib.mkIf cfg.enable {
environment.etc."ergo.yaml".source = cfg.configFile;
# merge configured values with default values
services.ergochat.settings = lib.mapAttrsRecursive (
_: lib.mkDefault
) options.services.ergochat.settings.default;
systemd.services.ergochat = {
description = "Ergo IRC daemon";
wantedBy = [ "multi-user.target" ];
# reload is not applying the changed config. further investigation is needed
# at some point this should be enabled, since we don't want to restart for
# every config change
# reloadIfChanged = true;
restartTriggers = [ cfg.configFile ];
serviceConfig = {
ExecStart = "${pkgs.ergochat}/bin/ergo run --conf /etc/ergo.yaml";
ExecReload = "${pkgs.util-linux}/bin/kill -HUP $MAINPID";
DynamicUser = true;
StateDirectory = "ergo";
LimitNOFILE = toString cfg.openFilesLimit;
};
};
};
meta.maintainers = with lib.maintainers; [
lassulus
tv
];
}

View File

@@ -0,0 +1,97 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.eternal-terminal;
in
{
###### interface
options = {
services.eternal-terminal = {
enable = lib.mkEnableOption "Eternal Terminal server";
port = lib.mkOption {
default = 2022;
type = lib.types.port;
description = ''
The port the server should listen on. Will use the server's default (2022) if not specified.
Make sure to open this port in the firewall if necessary.
'';
};
verbosity = lib.mkOption {
default = 0;
type = lib.types.enum (lib.range 0 9);
description = ''
The verbosity level (0-9).
'';
};
silent = lib.mkOption {
default = false;
type = lib.types.bool;
description = ''
If enabled, disables all logging.
'';
};
logSize = lib.mkOption {
default = 20971520;
type = lib.types.int;
description = ''
The maximum log size.
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
# We need to ensure the et package is fully installed because
# the (remote) et client runs the `etterminal` binary when it
# connects.
environment.systemPackages = [ pkgs.eternal-terminal ];
systemd.services = {
eternal-terminal = {
description = "Eternal Terminal server.";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
Type = "forking";
ExecStart = "${pkgs.eternal-terminal}/bin/etserver --daemon --cfgfile=${pkgs.writeText "et.cfg" ''
; et.cfg : Config file for Eternal Terminal
;
[Networking]
port = ${toString cfg.port}
[Debug]
verbose = ${toString cfg.verbosity}
silent = ${if cfg.silent then "1" else "0"}
logsize = ${toString cfg.logSize}
''}";
Restart = "on-failure";
KillMode = "process";
};
};
};
};
meta = {
maintainers = [ ];
};
}

View File

@@ -0,0 +1,36 @@
{
config,
lib,
pkgs,
...
}:
{
options.services.expressvpn.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Enable the ExpressVPN daemon.
'';
};
config = lib.mkIf config.services.expressvpn.enable {
boot.kernelModules = [ "tun" ];
systemd.services.expressvpn = {
description = "ExpressVPN Daemon";
serviceConfig = {
ExecStart = "${pkgs.expressvpn}/bin/expressvpnd";
Restart = "on-failure";
RestartSec = 5;
};
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [
"network.target"
"network-online.target"
];
};
};
meta.maintainers = with lib.maintainers; [ yureien ];
}

View File

@@ -0,0 +1,63 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.fakeroute;
routeConf = pkgs.writeText "route.conf" (lib.concatStringsSep "\n" cfg.route);
in
{
###### interface
options = {
services.fakeroute = {
enable = lib.mkEnableOption "the fakeroute service";
route = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
example = [
"216.102.187.130"
"4.0.1.122"
"198.116.142.34"
"63.199.8.242"
];
description = ''
Fake route that will appear after the real
one to any host running a traceroute.
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
systemd.services.fakeroute = {
description = "Fakeroute Daemon";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "forking";
User = "fakeroute";
DynamicUser = true;
AmbientCapabilities = [ "CAP_NET_RAW" ];
ExecStart = "${pkgs.fakeroute}/bin/fakeroute -f ${routeConf}";
};
};
};
meta.maintainers = with lib.maintainers; [ rnhmjoj ];
}

View File

@@ -0,0 +1,249 @@
{
config,
lib,
pkgs,
...
}:
let
# Background information: FastNetMon requires a MongoDB to start. This is because
# it uses MongoDB to store its configuration. That is, in a normal setup there is
# one collection with one document.
# To provide declarative configuration in our NixOS module, this database is
# completely emptied and replaced on each boot by the fastnetmon-setup service
# using the configuration backup functionality.
cfg = config.services.fastnetmon-advanced;
settingsFormat = pkgs.formats.yaml { };
# obtain the default configs by starting up ferretdb and fcli in a derivation
default_configs =
pkgs.runCommand "default-configs"
{
nativeBuildInputs = [
pkgs.ferretdb
pkgs.fastnetmon-advanced # for fcli
pkgs.proot
];
}
''
mkdir ferretdb fastnetmon $out
FERRETDB_TELEMETRY="disable" FERRETDB_HANDLER="sqlite" FERRETDB_STATE_DIR="$PWD/ferretdb" FERRETDB_SQLITE_URL="file:$PWD/ferretdb/" ferretdb &
cat << EOF > fastnetmon/fastnetmon.conf
${builtins.toJSON {
mongodb_username = "";
}}
EOF
proot -b fastnetmon:/etc/fastnetmon -0 fcli create_configuration
proot -b fastnetmon:/etc/fastnetmon -0 fcli set bgp default
proot -b fastnetmon:/etc/fastnetmon -0 fcli export_configuration backup.tar
tar -C $out --no-same-owner -xvf backup.tar
'';
# merge the user configs into the default configs
config_tar =
pkgs.runCommand "fastnetmon-config.tar"
{
nativeBuildInputs = with pkgs; [ jq ];
}
''
jq -s add ${default_configs}/main.json ${pkgs.writeText "main-add.json" (builtins.toJSON cfg.settings)} > main.json
mkdir hostgroup
${lib.concatImapStringsSep "\n" (pos: hostgroup: ''
jq -s add ${default_configs}/hostgroup/0.json ${pkgs.writeText "hostgroup-${toString (pos - 1)}-add.json" (builtins.toJSON hostgroup)} > hostgroup/${toString (pos - 1)}.json
'') hostgroups}
mkdir bgp
${lib.concatImapStringsSep "\n" (pos: bgp: ''
jq -s add ${default_configs}/bgp/0.json ${pkgs.writeText "bgp-${toString (pos - 1)}-add.json" (builtins.toJSON bgp)} > bgp/${toString (pos - 1)}.json
'') bgpPeers}
tar -cf $out main.json ${
lib.concatImapStringsSep " " (pos: _: "hostgroup/${toString (pos - 1)}.json") hostgroups
} ${lib.concatImapStringsSep " " (pos: _: "bgp/${toString (pos - 1)}.json") bgpPeers}
'';
hostgroups = lib.mapAttrsToList (name: hostgroup: { inherit name; } // hostgroup) cfg.hostgroups;
bgpPeers = lib.mapAttrsToList (name: bgpPeer: { inherit name; } // bgpPeer) cfg.bgpPeers;
in
{
options.services.fastnetmon-advanced = with lib; {
enable = mkEnableOption "the fastnetmon-advanced DDoS Protection daemon";
settings = mkOption {
description = ''
Extra configuration options to declaratively load into FastNetMon Advanced.
See the [FastNetMon Advanced Configuration options reference](https://fastnetmon.com/docs-fnm-advanced/fastnetmon-advanced-configuration-options/) for more details.
'';
type = settingsFormat.type;
default = { };
example = literalExpression ''
{
networks_list = [ "192.0.2.0/24" ];
gobgp = true;
gobgp_flow_spec_announces = true;
}
'';
};
hostgroups = mkOption {
description = "Hostgroups to declaratively load into FastNetMon Advanced";
type = types.attrsOf settingsFormat.type;
default = { };
};
bgpPeers = mkOption {
description = "BGP Peers to declaratively load into FastNetMon Advanced";
type = types.attrsOf settingsFormat.type;
default = { };
};
enableAdvancedTrafficPersistence = mkOption {
description = "Store historical flow data in clickhouse";
type = types.bool;
default = false;
};
traffic_db.settings = mkOption {
type = settingsFormat.type;
description = "Additional settings for /etc/fastnetmon/traffic_db.conf";
};
};
config = lib.mkMerge [
(lib.mkIf cfg.enable {
environment.systemPackages = with pkgs; [
fastnetmon-advanced # for fcli
];
environment.etc."fastnetmon/license.lic".source = "/var/lib/fastnetmon/license.lic";
environment.etc."fastnetmon/gobgpd.conf".source = "/run/fastnetmon/gobgpd.conf";
environment.etc."fastnetmon/fastnetmon.conf".source = pkgs.writeText "fastnetmon.conf" (
builtins.toJSON {
mongodb_username = "";
}
);
services.ferretdb.enable = true;
systemd.services.fastnetmon-setup = {
wantedBy = [ "multi-user.target" ];
after = [ "ferretdb.service" ];
path = with pkgs; [
fastnetmon-advanced
config.systemd.package
];
script = ''
fcli create_configuration
fcli delete hostgroup global
fcli import_configuration ${config_tar}
systemctl --no-block try-restart fastnetmon
'';
serviceConfig.Type = "oneshot";
};
systemd.services.fastnetmon = {
wantedBy = [ "multi-user.target" ];
after = [
"ferretdb.service"
"fastnetmon-setup.service"
"polkit.service"
];
path = with pkgs; [ iproute2 ];
unitConfig = {
# Disable logic which shuts service when we do too many restarts
# We do restarts from sudo fcli commit and it's expected that we may have many restarts
# Details: https://github.com/systemd/systemd/issues/2416
StartLimitInterval = 0;
};
serviceConfig = {
ExecStart = "${pkgs.fastnetmon-advanced}/bin/fastnetmon --log_to_console";
LimitNOFILE = 65535;
# Restart service when it fails due to any reasons, we need to keep processing traffic no matter what happened
Restart = "on-failure";
RestartSec = "5s";
DynamicUser = true;
CacheDirectory = "fastnetmon";
RuntimeDirectory = "fastnetmon"; # for gobgpd config
StateDirectory = "fastnetmon"; # for license file
};
};
security.polkit.enable = true;
security.polkit.extraConfig = ''
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.systemd1.manage-units" &&
subject.isInGroup("fastnetmon")) {
if (action.lookup("unit") == "gobgp.service") {
var verb = action.lookup("verb");
if (verb == "start" || verb == "stop" || verb == "restart") {
return polkit.Result.YES;
}
}
}
});
'';
# dbus/polkit with DynamicUser is broken with the default implementation
services.dbus.implementation = "broker";
# We don't use the existing gobgp NixOS module and package, because the gobgp
# version might not be compatible with fastnetmon. Also, the service name
# _must_ be 'gobgp' and not 'gobgpd', so that fastnetmon can reload the config.
systemd.services.gobgp = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
description = "GoBGP Routing Daemon";
unitConfig = {
ConditionPathExists = "/run/fastnetmon/gobgpd.conf";
};
serviceConfig = {
Type = "notify";
ExecStartPre = "${pkgs.fastnetmon-advanced}/bin/fnm-gobgpd -f /run/fastnetmon/gobgpd.conf -d";
SupplementaryGroups = [ "fastnetmon" ];
ExecStart = "${pkgs.fastnetmon-advanced}/bin/fnm-gobgpd -f /run/fastnetmon/gobgpd.conf --sdnotify";
ExecReload = "${pkgs.fastnetmon-advanced}/bin/fnm-gobgpd -r";
DynamicUser = true;
AmbientCapabilities = "cap_net_bind_service";
};
};
})
(lib.mkIf (cfg.enable && cfg.enableAdvancedTrafficPersistence) {
## Advanced Traffic persistence
## https://fastnetmon.com/docs-fnm-advanced/fastnetmon-advanced-traffic-persistency/
services.clickhouse.enable = true;
services.fastnetmon-advanced.settings.traffic_db = true;
services.fastnetmon-advanced.traffic_db.settings = {
clickhouse_batch_size = lib.mkDefault 1000;
clickhouse_batch_delay = lib.mkDefault 1;
traffic_db_host = lib.mkDefault "127.0.0.1";
traffic_db_port = lib.mkDefault 8100;
clickhouse_host = lib.mkDefault "127.0.0.1";
clickhouse_port = lib.mkDefault 9000;
clickhouse_user = lib.mkDefault "default";
clickhouse_password = lib.mkDefault "";
};
environment.etc."fastnetmon/traffic_db.conf".text = builtins.toJSON cfg.traffic_db.settings;
systemd.services.traffic_db = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
ExecStart = "${pkgs.fastnetmon-advanced}/bin/traffic_db";
# Restart service when it fails due to any reasons, we need to keep processing traffic no matter what happened
Restart = "on-failure";
RestartSec = "5s";
DynamicUser = true;
};
};
})
];
meta.maintainers = lib.teams.wdz.members;
}

View File

@@ -0,0 +1,381 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
concatLists
filterAttrs
mapAttrs'
mapAttrsToList
mkEnableOption
mkIf
mkOption
mkOverride
mkPackageOption
nameValuePair
recursiveUpdate
types
;
fedimintdOpts =
{
config,
lib,
name,
...
}:
{
options = {
enable = mkEnableOption "fedimintd";
package = mkPackageOption pkgs "fedimint" { };
environment = mkOption {
type = types.attrsOf types.str;
description = "Extra Environment variables to pass to the fedimintd.";
default = {
RUST_BACKTRACE = "1";
};
example = {
RUST_LOG = "info,fm=debug";
RUST_BACKTRACE = "1";
};
};
p2p = {
openFirewall = mkOption {
type = types.bool;
default = true;
description = "Opens port in firewall for fedimintd's p2p port (both TCP and UDP)";
};
port = mkOption {
type = types.port;
default = 8173;
description = "Port to bind on for p2p connections from peers (both TCP and UDP)";
};
bind = mkOption {
type = types.str;
default = "0.0.0.0";
description = "Address to bind on for p2p connections from peers (both TCP and UDP)";
};
url = mkOption {
type = types.nullOr types.str;
example = "fedimint://p2p.myfedimint.com:8173";
description = ''
Public address for p2p connections from peers (if TCP is used)
'';
};
};
api_ws = {
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Opens TCP port in firewall for fedimintd's Websocket API";
};
port = mkOption {
type = types.port;
default = 8174;
description = "TCP Port to bind on for API connections relayed by the reverse proxy/tls terminator.";
};
bind = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to bind on for API connections relied by the reverse proxy/tls terminator.";
};
url = mkOption {
type = types.nullOr types.str;
description = ''
Public URL of the API address of the reverse proxy/tls terminator. Usually starting with `wss://`.
'';
};
};
api_iroh = {
openFirewall = mkOption {
type = types.bool;
default = true;
description = "Opens UDP port in firewall for fedimintd's API Iroh endpoint";
};
port = mkOption {
type = types.port;
default = 8174;
description = "UDP Port to bind Iroh endpoint for API connections";
};
bind = mkOption {
type = types.str;
default = "0.0.0.0";
description = "Address to bind on for Iroh endpoint for API connections";
};
};
ui = {
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Opens TCP port in firewall for built-in UI";
};
port = mkOption {
type = types.port;
default = 8175;
description = "TCP Port to bind on for UI connections";
};
bind = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to bind on for UI connections";
};
};
bitcoin = {
network = mkOption {
type = types.str;
default = "signet";
example = "bitcoin";
description = "Bitcoin network to participate in.";
};
rpc = {
url = mkOption {
type = types.str;
default = "http://127.0.0.1:38332";
example = "signet";
description = "Bitcoin node (bitcoind/electrum/esplora) address to connect to";
};
kind = mkOption {
type = types.str;
default = "bitcoind";
example = "electrum";
description = "Kind of a bitcoin node.";
};
secretFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
If set the URL specified in `bitcoin.rpc.url` will get the content of this file added
as an URL password, so `http://user@example.com` will turn into `http://user:SOMESECRET@example.com`.
Example:
`/etc/nix-bitcoin-secrets/bitcoin-rpcpassword-public` (for nix-bitcoin default)
'';
};
};
};
consensus.finalityDelay = mkOption {
type = types.ints.unsigned;
default = 10;
description = "Consensus peg-in finality delay.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/fedimintd-${name}/";
readOnly = true;
description = ''
Path to the data dir fedimintd will use to store its data.
Note that due to using the DynamicUser feature of systemd, this value should not be changed
and is set to be read only.
'';
};
nginx = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to configure nginx for fedimintd
'';
};
fqdn = mkOption {
type = types.str;
example = "api.myfedimint.com";
description = "Public domain of the API address of the reverse proxy/tls terminator.";
};
path_ui = mkOption {
type = types.str;
example = "/";
default = "/";
description = "Path to host the built-in UI on and forward to the daemon's api port";
};
path_ws = mkOption {
type = types.str;
example = "/";
default = "/ws/";
description = "Path to host the API on and forward to the daemon's api port";
};
config = mkOption {
type = types.submodule (
recursiveUpdate (import ../web-servers/nginx/vhost-options.nix {
inherit config lib;
}) { }
);
default = { };
description = "Overrides to the nginx vhost section for api";
};
};
};
};
in
{
options = {
services.fedimintd = mkOption {
type = types.attrsOf (types.submodule fedimintdOpts);
default = { };
description = "Specification of one or more fedimintd instances.";
};
};
config =
let
eachFedimintd = filterAttrs (fedimintdName: cfg: cfg.enable) config.services.fedimintd;
eachFedimintdNginx = filterAttrs (fedimintdName: cfg: cfg.nginx.enable) eachFedimintd;
in
mkIf (eachFedimintd != { }) {
networking.firewall.allowedTCPPorts = concatLists (
mapAttrsToList (
fedimintdName: cfg:
(
lib.optional cfg.api_ws.openFirewall cfg.api_ws.port
++ lib.optional cfg.p2p.openFirewall cfg.p2p.port
++ lib.optional cfg.ui.openFirewall cfg.ui.port
)
) eachFedimintd
);
networking.firewall.allowedUDPPorts = concatLists (
mapAttrsToList (
fedimintdName: cfg:
(
lib.optional cfg.api_iroh.openFirewall cfg.api_iroh.port
++ lib.optional cfg.p2p.openFirewall cfg.p2p.port
)
) eachFedimintd
);
systemd.services = mapAttrs' (
fedimintdName: cfg:
(nameValuePair "fedimintd-${fedimintdName}" (
let
startScript = pkgs.writeShellScriptBin "fedimintd" (
(
if cfg.bitcoin.rpc.secretFile != null then
''
>&2 echo "Setting FM_FORCE_BITCOIN_RPC_URL using password from ${cfg.bitcoin.rpc.secretFile}"
secret=$(${pkgs.coreutils}/bin/head -n 1 "${cfg.bitcoin.rpc.secretFile}" || exit 1)
export FM_FORCE_BITCOIN_RPC_URL=$(echo "$FM_BITCOIN_RPC_URL" | sed "s|^\(\w\+://[^@]\+\)\(@.*\)|\1:''${secret}\2|")
''
else
""
)
+ ''
exec ${cfg.package}/bin/fedimintd
''
);
in
{
description = "Fedimint Server";
documentation = [ "https://github.com/fedimint/fedimint/" ];
wantedBy = [ "multi-user.target" ];
environment = lib.mkMerge [
{
FM_BIND_P2P = "${cfg.p2p.bind}:${toString cfg.p2p.port}";
FM_BIND_API_WS = "${cfg.api_ws.bind}:${toString cfg.api_ws.port}";
FM_BIND_API_IROH = "${cfg.api_iroh.bind}:${toString cfg.api_iroh.port}";
FM_BIND_UI = "${cfg.ui.bind}:${toString cfg.ui.port}";
FM_DATA_DIR = cfg.dataDir;
FM_BITCOIN_NETWORK = cfg.bitcoin.network;
FM_BITCOIN_RPC_URL = cfg.bitcoin.rpc.url;
FM_BITCOIN_RPC_KIND = cfg.bitcoin.rpc.kind;
}
(lib.optionalAttrs (cfg.p2p.url != null) {
FM_P2P_URL = cfg.p2p.url;
})
(lib.optionalAttrs (cfg.api_ws.url != null) {
FM_API_URL = cfg.api_ws.url;
})
cfg.environment
];
serviceConfig = {
DynamicUser = true;
StateDirectory = "fedimintd-${fedimintdName}";
StateDirectoryMode = "0700";
ExecStart = "${startScript}/bin/fedimintd";
Restart = "always";
RestartSec = 10;
UMask = "007";
LimitNOFILE = "100000";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "full";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
];
RestrictNamespaces = true;
RestrictRealtime = true;
SocketBindAllow = "udp:${builtins.toString cfg.api_iroh.port}";
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
};
unitConfig = {
StartLimitBurst = 5;
};
}
))
) eachFedimintd;
services.nginx.virtualHosts = mapAttrs' (
fedimintdName: cfg:
(nameValuePair cfg.nginx.fqdn (
lib.mkMerge [
cfg.nginx.config
{
# Note: we want by default to enable OpenSSL, but it seems anything 100 and above is
# overridden by default value from vhost-options.nix
enableACME = mkOverride 99 true;
forceSSL = mkOverride 99 true;
locations.${cfg.nginx.path_ws} = {
proxyPass = "http://127.0.0.1:${builtins.toString cfg.api_ws.port}/";
proxyWebsockets = true;
extraConfig = ''
proxy_pass_header Authorization;
'';
};
locations.${cfg.nginx.path_ui} = {
proxyPass = "http://127.0.0.1:${builtins.toString cfg.ui.port}/";
extraConfig = ''
proxy_pass_header Authorization;
'';
};
}
]
))
) eachFedimintdNginx;
};
meta.maintainers = with lib.maintainers; [ dpc ];
}

View File

@@ -0,0 +1,61 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.ferm;
configFile = pkgs.stdenv.mkDerivation {
name = "ferm.conf";
text = cfg.config;
preferLocalBuild = true;
buildCommand = ''
echo -n "$text" > $out
${cfg.package}/bin/ferm --noexec $out
'';
};
in
{
options = {
services.ferm = {
enable = lib.mkOption {
default = false;
type = lib.types.bool;
description = ''
Whether to enable Ferm Firewall.
*Warning*: Enabling this service WILL disable the existing NixOS
firewall! Default firewall rules provided by packages are not
considered at the moment.
'';
};
config = lib.mkOption {
description = "Verbatim ferm.conf configuration.";
default = "";
defaultText = lib.literalMD "empty firewall, allows any traffic";
type = lib.types.lines;
};
package = lib.mkPackageOption pkgs "ferm" { };
};
};
config = lib.mkIf cfg.enable {
systemd.services.firewall.enable = false;
systemd.services.ferm = {
description = "Ferm Firewall";
after = [ "ipset.target" ];
before = [ "network-pre.target" ];
wants = [ "network-pre.target" ];
wantedBy = [ "multi-user.target" ];
reloadIfChanged = true;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = "yes";
ExecStart = "${cfg.package}/bin/ferm ${configFile}";
ExecReload = "${cfg.package}/bin/ferm ${configFile}";
ExecStop = "${cfg.package}/bin/ferm -F ${configFile}";
};
};
};
}

View File

@@ -0,0 +1,77 @@
# Firefox Sync server {#module-services-firefox-syncserver}
A storage server for Firefox Sync that you can easily host yourself.
## Quickstart {#module-services-firefox-syncserver-quickstart}
The absolute minimal configuration for the sync server looks like this:
```nix
{
services.mysql.package = pkgs.mariadb;
services.firefox-syncserver = {
enable = true;
secrets = builtins.toFile "sync-secrets" ''
SYNC_MASTER_SECRET=this-secret-is-actually-leaked-to-/nix/store
'';
singleNode = {
enable = true;
hostname = "localhost";
url = "http://localhost:5000";
};
};
}
```
This will start a sync server that is only accessible locally on the following url: `http://localhost:5000/1.0/sync/1.5`.
See [the dedicated section](#module-services-firefox-syncserver-clients) to configure your browser to use this sync server.
::: {.warning}
This configuration should never be used in production. It is not encrypted and
stores its secrets in a world-readable location.
:::
## More detailed setup {#module-services-firefox-syncserver-configuration}
The `firefox-syncserver` service provides a number of options to make setting up
small deployment easier. These are grouped under the `singleNode` element of the
option tree and allow simple configuration of the most important parameters.
Single node setup is split into two kinds of options: those that affect the sync
server itself, and those that affect its surroundings. Options that affect the
sync server are `capacity`, which configures how many accounts may be active on
this instance, and `url`, which holds the URL under which the sync server can be
accessed. The `url` can be configured automatically when using nginx.
Options that affect the surroundings of the sync server are `enableNginx`,
`enableTLS` and `hostname`. If `enableNginx` is set the sync server module will
automatically add an nginx virtual host to the system using `hostname` as the
domain and set `url` accordingly. If `enableTLS` is set the module will also
enable ACME certificates on the new virtual host and force all connections to
be made via TLS.
For actual deployment it is also recommended to store the `secrets` file in a
secure location.
## Configuring clients to use this server {#module-services-firefox-syncserver-clients}
### Firefox desktop {#module-services-firefox-syncserver-clients-desktop}
To configure a desktop version of Firefox to use your server, navigate to
`about:config` in your Firefox profile and set
`identity.sync.tokenserver.uri` to `https://myhostname:5000/1.0/sync/1.5`.
### Firefox Android {#module-services-firefox-syncserver-clients-android}
To configure an Android version of Firefox to use your server:
* First ensure that you are disconnected from you Mozilla account.
* Go to App Menu > Settings > About Firefox and click the logo 5 times. You
should see a “debug menu enabled” notification.
* Back to the main menu, a new menu "sync debug" should have appeared.
* In this menu, set "custom sync server" to `https://myhostname:5000/1.0/sync/1.5`.
::: {.warning}
Changes to this configuration value are ignored if you are currently connected to your account.
:::
* Restart the application.
* Log in to your account.

View File

@@ -0,0 +1,331 @@
{
config,
pkgs,
lib,
options,
...
}:
let
cfg = config.services.firefox-syncserver;
opt = options.services.firefox-syncserver;
defaultDatabase = "firefox_syncserver";
defaultUser = "firefox-syncserver";
dbIsLocal = cfg.database.host == "localhost";
dbURL = "mysql://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}";
format = pkgs.formats.toml { };
settings = {
human_logs = true;
syncstorage = {
database_url = dbURL;
};
tokenserver = {
node_type = "mysql";
database_url = dbURL;
fxa_email_domain = "api.accounts.firefox.com";
fxa_oauth_server_url = "https://oauth.accounts.firefox.com/v1";
run_migrations = true;
# if JWK caching is not enabled the token server must verify tokens
# using the fxa api, on a thread pool with a static size.
additional_blocking_threads_for_fxa_requests = 10;
}
// lib.optionalAttrs cfg.singleNode.enable {
# Single-node mode is likely to be used on small instances with little
# capacity. The default value (0.1) can only ever release capacity when
# accounts are removed if the total capacity is 10 or larger to begin
# with.
# https://github.com/mozilla-services/syncstorage-rs/issues/1313#issuecomment-1145293375
node_capacity_release_rate = 1;
};
};
configFile = format.generate "syncstorage.toml" (lib.recursiveUpdate settings cfg.settings);
setupScript = pkgs.writeShellScript "firefox-syncserver-setup" ''
set -euo pipefail
shopt -s inherit_errexit
schema_configured() {
mysql ${cfg.database.name} -Ne 'SHOW TABLES' | grep -q services
}
update_config() {
mysql ${cfg.database.name} <<"EOF"
BEGIN;
INSERT INTO `services` (`id`, `service`, `pattern`)
VALUES (1, 'sync-1.5', '{node}/1.5/{uid}')
ON DUPLICATE KEY UPDATE service='sync-1.5', pattern='{node}/1.5/{uid}';
INSERT INTO `nodes` (`id`, `service`, `node`, `available`, `current_load`,
`capacity`, `downed`, `backoff`)
VALUES (1, 1, '${cfg.singleNode.url}', ${toString cfg.singleNode.capacity},
0, ${toString cfg.singleNode.capacity}, 0, 0)
ON DUPLICATE KEY UPDATE node = '${cfg.singleNode.url}', capacity=${toString cfg.singleNode.capacity};
COMMIT;
EOF
}
for (( try = 0; try < 60; try++ )); do
if ! schema_configured; then
sleep 2
else
update_config
exit 0
fi
done
echo "Single-node setup failed"
exit 1
'';
in
{
options = {
services.firefox-syncserver = {
enable = lib.mkEnableOption ''
the Firefox Sync storage service.
Out of the box this will not be very useful unless you also configure at least
one service and one nodes by inserting them into the mysql database manually, e.g.
by running
```
INSERT INTO `services` (`id`, `service`, `pattern`) VALUES ('1', 'sync-1.5', '{node}/1.5/{uid}');
INSERT INTO `nodes` (`id`, `service`, `node`, `available`, `current_load`,
`capacity`, `downed`, `backoff`)
VALUES ('1', '1', 'https://mydomain.tld', '1', '0', '10', '0', '0');
```
{option}`${opt.singleNode.enable}` does this automatically when enabled
'';
package = lib.mkPackageOption pkgs "syncstorage-rs" { };
database.name = lib.mkOption {
# the mysql module does not allow `-quoting without resorting to shell
# escaping, so we restrict db names for forward compaitiblity should this
# behavior ever change.
type = lib.types.strMatching "[a-z_][a-z0-9_]*";
default = defaultDatabase;
description = ''
Database to use for storage. Will be created automatically if it does not exist
and `config.${opt.database.createLocally}` is set.
'';
};
database.user = lib.mkOption {
type = lib.types.str;
default = defaultUser;
description = ''
Username for database connections.
'';
};
database.host = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = ''
Database host name. `localhost` is treated specially and inserts
systemd dependencies, other hostnames or IP addresses of the local machine do not.
'';
};
database.createLocally = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to create database and user on the local machine if they do not exist.
This includes enabling unix domain socket authentication for the configured user.
'';
};
logLevel = lib.mkOption {
type = lib.types.str;
default = "error";
description = ''
Log level to run with. This can be a simple log level like `error`
or `trace`, or a more complicated logging expression.
'';
};
secrets = lib.mkOption {
type = lib.types.path;
description = ''
A file containing the various secrets. Should be in the format expected by systemd's
`EnvironmentFile` directory. Two secrets are currently available:
`SYNC_MASTER_SECRET` and
`SYNC_TOKENSERVER__FXA_METRICS_HASH_SECRET`.
'';
};
singleNode = {
enable = lib.mkEnableOption "auto-configuration for a simple single-node setup";
enableTLS = lib.mkEnableOption "automatic TLS setup";
enableNginx = lib.mkEnableOption "nginx virtualhost definitions";
hostname = lib.mkOption {
type = lib.types.str;
description = ''
Host name to use for this service.
'';
};
capacity = lib.mkOption {
type = lib.types.ints.unsigned;
default = 10;
description = ''
How many sync accounts are allowed on this server. Setting this value
equal to or less than the number of currently active accounts will
effectively deny service to accounts not yet registered here.
'';
};
url = lib.mkOption {
type = lib.types.str;
default = "${if cfg.singleNode.enableTLS then "https" else "http"}://${cfg.singleNode.hostname}";
defaultText = lib.literalExpression ''
''${if cfg.singleNode.enableTLS then "https" else "http"}://''${config.${opt.singleNode.hostname}}
'';
description = ''
URL of the host. If you are not using the automatic webserver proxy setup you will have
to change this setting or your sync server may not be functional.
'';
};
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = format.type;
options = {
port = lib.mkOption {
type = lib.types.port;
default = 5000;
description = ''
Port to bind to.
'';
};
tokenserver.enabled = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to enable the token service as well.
'';
};
};
};
default = { };
description = ''
Settings for the sync server. These take priority over values computed
from NixOS options.
See the example config in
<https://github.com/mozilla-services/syncstorage-rs/blob/master/config/local.example.toml>
and the doc comments on the `Settings` structs in
<https://github.com/mozilla-services/syncstorage-rs/blob/master/syncstorage-settings/src/lib.rs>
and
<https://github.com/mozilla-services/syncstorage-rs/blob/master/tokenserver-settings/src/lib.rs>
for available options.
'';
};
};
};
config = lib.mkIf cfg.enable {
services.mysql = lib.mkIf cfg.database.createLocally {
enable = true;
ensureDatabases = [ cfg.database.name ];
ensureUsers = [
{
name = cfg.database.user;
ensurePermissions = {
"${cfg.database.name}.*" = "all privileges";
};
}
];
};
systemd.services.firefox-syncserver = {
wantedBy = [ "multi-user.target" ];
requires = lib.mkIf dbIsLocal [ "mysql.service" ];
after = lib.mkIf dbIsLocal [ "mysql.service" ];
restartTriggers = lib.optional cfg.singleNode.enable setupScript;
environment.RUST_LOG = cfg.logLevel;
serviceConfig = {
User = defaultUser;
Group = defaultUser;
ExecStart = "${cfg.package}/bin/syncserver --config ${configFile}";
EnvironmentFile = lib.mkIf (cfg.secrets != null) "${cfg.secrets}";
# hardening
RemoveIPC = true;
CapabilityBoundingSet = [ "" ];
DynamicUser = true;
NoNewPrivileges = true;
PrivateDevices = true;
ProtectClock = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
ProtectKernelModules = true;
SystemCallArchitectures = "native";
# syncstorage-rs uses python-cffi internally, and python-cffi does not
# work with MemoryDenyWriteExecute=true
MemoryDenyWriteExecute = false;
RestrictNamespaces = true;
RestrictSUIDSGID = true;
ProtectHostname = true;
LockPersonality = true;
ProtectKernelTunables = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictRealtime = true;
ProtectSystem = "strict";
ProtectProc = "invisible";
ProcSubset = "pid";
ProtectHome = true;
PrivateUsers = true;
PrivateTmp = true;
SystemCallFilter = [
"@system-service"
"~ @privileged @resources"
];
UMask = "0077";
};
};
systemd.services.firefox-syncserver-setup = lib.mkIf cfg.singleNode.enable {
wantedBy = [ "firefox-syncserver.service" ];
requires = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal "mysql.service";
after = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal "mysql.service";
path = [ config.services.mysql.package ];
serviceConfig.ExecStart = [ "${setupScript}" ];
};
services.nginx.virtualHosts = lib.mkIf cfg.singleNode.enableNginx {
${cfg.singleNode.hostname} = {
enableACME = cfg.singleNode.enableTLS;
forceSSL = cfg.singleNode.enableTLS;
locations."/" = {
proxyPass = "http://127.0.0.1:${toString cfg.settings.port}";
# We need to pass the Host header that matches the original Host header. Otherwise,
# Hawk authentication will fail (because it assumes that the client and server see
# the same value of the Host header).
recommendedProxySettings = true;
};
};
};
};
meta = {
maintainers = [ ];
doc = ./firefox-syncserver.md;
};
}

View File

@@ -0,0 +1,48 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.fireqos;
fireqosConfig = pkgs.writeText "fireqos.conf" cfg.config;
in
{
options.services.fireqos = {
enable = lib.mkEnableOption "FireQOS";
config = lib.mkOption {
type = lib.types.lines;
example = ''
interface wlp3s0 world-in input rate 10mbit ethernet
class web commit 50kbit
match tcp ports 80,443
interface wlp3s0 world-out input rate 10mbit ethernet
class web commit 50kbit
match tcp ports 80,443
'';
description = ''
The FireQOS configuration.
'';
};
};
config = lib.mkIf cfg.enable {
systemd.services.fireqos = {
description = "FireQOS";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.firehol}/bin/fireqos start ${fireqosConfig}";
ExecStop = [
"${pkgs.firehol}/bin/fireqos stop"
"${pkgs.firehol}/bin/fireqos clear_all_qos"
];
};
};
};
}

View File

@@ -0,0 +1,371 @@
/*
This module enables a simple firewall.
The firewall can be customised in arbitrary ways by setting
networking.firewall.extraCommands. For modularity, the firewall
uses several chains:
- nixos-fw is the main chain for input packet processing.
- nixos-fw-accept is called for accepted packets. If you want
additional logging, or want to reject certain packets anyway, you
can insert rules at the start of this chain.
- nixos-fw-log-refuse and nixos-fw-refuse are called for
refused packets. (The former jumps to the latter after logging
the packet.) If you want additional logging, or want to accept
certain packets anyway, you can insert rules at the start of
this chain.
- nixos-fw-rpfilter is used as the main chain in the mangle table,
called from the built-in PREROUTING chain. If the kernel
supports it and `cfg.checkReversePath` is set this chain will
perform a reverse path filter test.
- nixos-drop is used while reloading the firewall in order to drop
all traffic. Since reloading isn't implemented in an atomic way
this'll prevent any traffic from leaking through while reloading
the firewall. However, if the reloading fails, the firewall-stop
script will be called which in return will effectively disable the
complete firewall (in the default configuration).
*/
{
config,
lib,
pkgs,
...
}:
let
cfg = config.networking.firewall;
inherit (config.boot.kernelPackages) kernel;
kernelHasRPFilter =
((kernel.config.isEnabled or (x: false)) "IP_NF_MATCH_RPFILTER")
|| (kernel.features.netfilterRPFilter or false);
helpers = import ./helpers.nix { inherit config lib; };
writeShScript =
name: text:
let
dir = pkgs.writeScriptBin name ''
#! ${pkgs.runtimeShell} -e
${text}
'';
in
"${dir}/bin/${name}";
startScript = writeShScript "firewall-start" ''
${helpers}
# Flush the old firewall rules. !!! Ideally, updating the
# firewall would be atomic. Apparently that's possible
# with iptables-restore.
ip46tables -D INPUT -j nixos-fw 2> /dev/null || true
for chain in nixos-fw nixos-fw-accept nixos-fw-log-refuse nixos-fw-refuse; do
ip46tables -F "$chain" 2> /dev/null || true
ip46tables -X "$chain" 2> /dev/null || true
done
# The "nixos-fw-accept" chain just accepts packets.
ip46tables -N nixos-fw-accept
ip46tables -A nixos-fw-accept -j ACCEPT
# The "nixos-fw-refuse" chain rejects or drops packets.
ip46tables -N nixos-fw-refuse
${
if cfg.rejectPackets then
''
# Send a reset for existing TCP connections that we've
# somehow forgotten about. Send ICMP "port unreachable"
# for everything else.
ip46tables -A nixos-fw-refuse -p tcp ! --syn -j REJECT --reject-with tcp-reset
ip46tables -A nixos-fw-refuse -j REJECT
''
else
''
ip46tables -A nixos-fw-refuse -j DROP
''
}
# The "nixos-fw-log-refuse" chain performs logging, then
# jumps to the "nixos-fw-refuse" chain.
ip46tables -N nixos-fw-log-refuse
${lib.optionalString cfg.logRefusedConnections ''
ip46tables -A nixos-fw-log-refuse -p tcp --syn -j LOG --log-level info --log-prefix "refused connection: "
''}
${lib.optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) ''
ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type broadcast \
-j LOG --log-level info --log-prefix "refused broadcast: "
ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type multicast \
-j LOG --log-level info --log-prefix "refused multicast: "
''}
ip46tables -A nixos-fw-log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse
${lib.optionalString cfg.logRefusedPackets ''
ip46tables -A nixos-fw-log-refuse \
-j LOG --log-level info --log-prefix "refused packet: "
''}
ip46tables -A nixos-fw-log-refuse -j nixos-fw-refuse
# The "nixos-fw" chain does the actual work.
ip46tables -N nixos-fw
# Clean up rpfilter rules
ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2> /dev/null || true
ip46tables -t mangle -F nixos-fw-rpfilter 2> /dev/null || true
ip46tables -t mangle -X nixos-fw-rpfilter 2> /dev/null || true
${lib.optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) ''
# Perform a reverse-path test to refuse spoofers
# For now, we just drop, as the mangle table doesn't have a log-refuse yet
ip46tables -t mangle -N nixos-fw-rpfilter 2> /dev/null || true
ip46tables -t mangle -A nixos-fw-rpfilter -m rpfilter --validmark ${
lib.optionalString (cfg.checkReversePath == "loose") "--loose"
} -j RETURN
# Allows this host to act as a DHCP4 client without first having to use APIPA
iptables -t mangle -A nixos-fw-rpfilter -p udp --sport 67 --dport 68 -j RETURN
# Allows this host to act as a DHCPv4 server
iptables -t mangle -A nixos-fw-rpfilter -s 0.0.0.0 -d 255.255.255.255 -p udp --sport 68 --dport 67 -j RETURN
${lib.optionalString cfg.logReversePathDrops ''
ip46tables -t mangle -A nixos-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: "
''}
ip46tables -t mangle -A nixos-fw-rpfilter -j DROP
ip46tables -t mangle -A PREROUTING -j nixos-fw-rpfilter
''}
# Accept all traffic on the trusted interfaces.
${lib.flip lib.concatMapStrings cfg.trustedInterfaces (iface: ''
ip46tables -A nixos-fw -i ${iface} -j nixos-fw-accept
'')}
# Accept packets from established or related connections.
ip46tables -A nixos-fw -m conntrack --ctstate ESTABLISHED,RELATED -j nixos-fw-accept
# Accept connections to the allowed TCP ports.
${lib.concatStrings (
lib.mapAttrsToList (
iface: cfg:
lib.concatMapStrings (port: ''
ip46tables -A nixos-fw -p tcp --dport ${toString port} -j nixos-fw-accept ${
lib.optionalString (iface != "default") "-i ${iface}"
}
'') cfg.allowedTCPPorts
) cfg.allInterfaces
)}
# Accept connections to the allowed TCP port ranges.
${lib.concatStrings (
lib.mapAttrsToList (
iface: cfg:
lib.concatMapStrings (
rangeAttr:
let
range = toString rangeAttr.from + ":" + toString rangeAttr.to;
in
''
ip46tables -A nixos-fw -p tcp --dport ${range} -j nixos-fw-accept ${
lib.optionalString (iface != "default") "-i ${iface}"
}
''
) cfg.allowedTCPPortRanges
) cfg.allInterfaces
)}
# Accept packets on the allowed UDP ports.
${lib.concatStrings (
lib.mapAttrsToList (
iface: cfg:
lib.concatMapStrings (port: ''
ip46tables -A nixos-fw -p udp --dport ${toString port} -j nixos-fw-accept ${
lib.optionalString (iface != "default") "-i ${iface}"
}
'') cfg.allowedUDPPorts
) cfg.allInterfaces
)}
# Accept packets on the allowed UDP port ranges.
${lib.concatStrings (
lib.mapAttrsToList (
iface: cfg:
lib.concatMapStrings (
rangeAttr:
let
range = toString rangeAttr.from + ":" + toString rangeAttr.to;
in
''
ip46tables -A nixos-fw -p udp --dport ${range} -j nixos-fw-accept ${
lib.optionalString (iface != "default") "-i ${iface}"
}
''
) cfg.allowedUDPPortRanges
) cfg.allInterfaces
)}
# Optionally respond to ICMPv4 pings.
${lib.optionalString cfg.allowPing ''
iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${
lib.optionalString (cfg.pingLimit != null) "-m limit ${cfg.pingLimit} "
}-j nixos-fw-accept
''}
${lib.optionalString config.networking.enableIPv6 ''
# Accept all ICMPv6 messages except redirects and node
# information queries (type 139). See RFC 4890, section
# 4.4.
ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP
ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP
ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept
# Allow this host to act as a DHCPv6 client
ip6tables -A nixos-fw -d fe80::/64 -p udp --dport 546 -j nixos-fw-accept
''}
${cfg.extraCommands}
# Reject/drop everything else.
ip46tables -A nixos-fw -j nixos-fw-log-refuse
# Enable the firewall.
ip46tables -A INPUT -j nixos-fw
'';
stopScript = writeShScript "firewall-stop" ''
${helpers}
# Clean up in case reload fails
ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
# Clean up after added ruleset
ip46tables -D INPUT -j nixos-fw 2>/dev/null || true
${lib.optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) ''
ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2>/dev/null || true
''}
${cfg.extraStopCommands}
'';
reloadScript = writeShScript "firewall-reload" ''
${helpers}
# Create a unique drop rule
ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
ip46tables -F nixos-drop 2>/dev/null || true
ip46tables -X nixos-drop 2>/dev/null || true
ip46tables -N nixos-drop
ip46tables -A nixos-drop -j DROP
# Don't allow traffic to leak out until the script has completed
ip46tables -A INPUT -j nixos-drop
${cfg.extraStopCommands}
if ${startScript}; then
ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
else
echo "Failed to reload firewall... Stopping"
${stopScript}
exit 1
fi
'';
in
{
options = {
networking.firewall = {
extraCommands = lib.mkOption {
type = lib.types.lines;
default = "";
example = "iptables -A INPUT -p icmp -j ACCEPT";
description = ''
Additional shell commands executed as part of the firewall
initialisation script. These are executed just before the
final "reject" firewall rule is added, so they can be used
to allow packets that would otherwise be refused.
This option only works with the iptables based firewall.
'';
};
extraStopCommands = lib.mkOption {
type = lib.types.lines;
default = "";
example = "iptables -P INPUT ACCEPT";
description = ''
Additional shell commands executed as part of the firewall
shutdown script. These are executed just after the removal
of the NixOS input rule, or if the service enters a failed
state.
This option only works with the iptables based firewall.
'';
};
};
};
# FIXME: Maybe if `enable' is false, the firewall should still be
# built but not started by default?
config = lib.mkIf (cfg.enable && config.networking.nftables.enable == false) {
assertions = [
# This is approximately "checkReversePath -> kernelHasRPFilter",
# but the checkReversePath option can include non-boolean
# values.
{
assertion = cfg.checkReversePath == false || kernelHasRPFilter;
message = "This kernel does not support rpfilter";
}
];
networking.firewall.checkReversePath = lib.mkIf (!kernelHasRPFilter) (lib.mkDefault false);
systemd.services.firewall = {
description = "Firewall";
wantedBy = [ "sysinit.target" ];
wants = [ "network-pre.target" ];
after = [ "systemd-modules-load.service" ];
before = [
"network-pre.target"
"shutdown.target"
];
conflicts = [ "shutdown.target" ];
path = [ cfg.package ] ++ cfg.extraPackages;
# FIXME: this module may also try to load kernel modules, but
# containers don't have CAP_SYS_MODULE. So the host system had
# better have all necessary modules already loaded.
unitConfig.ConditionCapability = "CAP_NET_ADMIN";
unitConfig.DefaultDependencies = false;
reloadIfChanged = true;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "@${startScript} firewall-start";
ExecReload = "@${reloadScript} firewall-reload";
ExecStop = "@${stopScript} firewall-stop";
};
};
};
}

View File

@@ -0,0 +1,209 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.networking.firewall;
ifaceSet = lib.concatStringsSep ", " (map (x: ''"${x}"'') cfg.trustedInterfaces);
portsToNftSet =
ports: portRanges:
lib.concatStringsSep ", " (
map (x: toString x) ports ++ map (x: "${toString x.from}-${toString x.to}") portRanges
);
in
{
options = {
networking.firewall = {
extraInputRules = lib.mkOption {
type = lib.types.lines;
default = "";
example = "ip6 saddr { fc00::/7, fe80::/10 } tcp dport 24800 accept";
description = ''
Additional nftables rules to be appended to the input-allow
chain.
This option only works with the nftables based firewall.
'';
};
extraForwardRules = lib.mkOption {
type = lib.types.lines;
default = "";
example = "iifname wg0 accept";
description = ''
Additional nftables rules to be appended to the forward-allow
chain.
This option only works with the nftables based firewall.
'';
};
extraReversePathFilterRules = lib.mkOption {
type = lib.types.lines;
default = "";
example = "fib daddr . mark . iif type local accept";
description = ''
Additional nftables rules to be appended to the rpfilter-allow
chain.
This option only works with the nftables based firewall.
'';
};
};
};
config = lib.mkIf (cfg.enable && config.networking.nftables.enable) {
assertions = [
{
assertion = cfg.extraCommands == "";
message = "extraCommands is incompatible with the nftables based firewall: ${cfg.extraCommands}";
}
{
assertion = cfg.extraStopCommands == "";
message = "extraStopCommands is incompatible with the nftables based firewall: ${cfg.extraStopCommands}";
}
{
assertion = cfg.pingLimit == null || !(lib.hasPrefix "--" cfg.pingLimit);
message = "nftables syntax like \"2/second\" should be used in networking.firewall.pingLimit";
}
{
assertion = config.networking.nftables.rulesetFile == null;
message = "networking.nftables.rulesetFile conflicts with the firewall";
}
];
networking.nftables.tables."nixos-fw".family = "inet";
networking.nftables.tables."nixos-fw".content = ''
set temp-ports {
comment "Temporarily opened ports"
type inet_proto . inet_service
flags interval
auto-merge
}
${lib.optionalString (cfg.checkReversePath != false) ''
chain rpfilter {
type filter hook prerouting priority mangle + 10; policy drop;
meta nfproto ipv4 udp sport . udp dport { 67 . 68, 68 . 67 } accept comment "DHCPv4 client/server"
fib saddr . mark ${lib.optionalString (cfg.checkReversePath != "loose") ". iif"} oif exists accept
jump rpfilter-allow
${lib.optionalString cfg.logReversePathDrops ''
log level info prefix "rpfilter drop: "
''}
}
''}
chain rpfilter-allow {
${cfg.extraReversePathFilterRules}
}
chain input {
type filter hook input priority filter; policy drop;
${lib.optionalString (
ifaceSet != ""
) ''iifname { ${ifaceSet} } accept comment "trusted interfaces"''}
# Some ICMPv6 types like NDP is untracked
ct state vmap {
invalid : drop,
established : accept,
related : accept,
new : jump input-allow,
untracked: jump input-allow,
}
${lib.optionalString cfg.logRefusedConnections ''
tcp flags syn / fin,syn,rst,ack log level info prefix "refused connection: "
''}
${lib.optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) ''
pkttype broadcast log level info prefix "refused broadcast: "
pkttype multicast log level info prefix "refused multicast: "
''}
${lib.optionalString cfg.logRefusedPackets ''
pkttype host log level info prefix "refused packet: "
''}
${lib.optionalString cfg.rejectPackets ''
meta l4proto tcp reject with tcp reset
reject
''}
}
chain input-allow {
${lib.concatStrings (
lib.mapAttrsToList (
iface: cfg:
let
ifaceExpr = lib.optionalString (iface != "default") "iifname ${iface}";
tcpSet = portsToNftSet cfg.allowedTCPPorts cfg.allowedTCPPortRanges;
udpSet = portsToNftSet cfg.allowedUDPPorts cfg.allowedUDPPortRanges;
in
''
${lib.optionalString (tcpSet != "") "${ifaceExpr} tcp dport { ${tcpSet} } accept"}
${lib.optionalString (udpSet != "") "${ifaceExpr} udp dport { ${udpSet} } accept"}
''
) cfg.allInterfaces
)}
meta l4proto . th dport @temp-ports accept
${lib.optionalString cfg.allowPing ''
icmp type echo-request ${
lib.optionalString (cfg.pingLimit != null) "limit rate ${cfg.pingLimit}"
} accept comment "allow ping"
''}
icmpv6 type != { nd-redirect, 139 } accept comment "Accept all ICMPv6 messages except redirects and node information queries (type 139). See RFC 4890, section 4.4."
ip6 daddr fe80::/64 udp dport 546 accept comment "DHCPv6 client"
${cfg.extraInputRules}
}
${lib.optionalString cfg.filterForward ''
chain forward {
type filter hook forward priority filter; policy drop;
ct state vmap {
invalid : drop,
established : accept,
related : accept,
new : jump forward-allow,
untracked : jump forward-allow,
}
}
chain forward-allow {
icmpv6 type != { router-renumbering, 139 } accept comment "Accept all ICMPv6 messages except renumbering and node information queries (type 139). See RFC 4890, section 4.3."
ct status dnat accept comment "allow port forward"
${cfg.extraForwardRules}
}
''}
'';
};
}

View File

@@ -0,0 +1,329 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.networking.firewall;
canonicalizePortList = ports: lib.unique (builtins.sort builtins.lessThan ports);
commonOptions = {
allowedTCPPorts = lib.mkOption {
type = lib.types.listOf lib.types.port;
default = [ ];
apply = canonicalizePortList;
example = [
22
80
];
description = ''
List of TCP ports on which incoming connections are
accepted.
'';
};
allowedTCPPortRanges = lib.mkOption {
type = lib.types.listOf (lib.types.attrsOf lib.types.port);
default = [ ];
example = [
{
from = 8999;
to = 9003;
}
];
description = ''
A range of TCP ports on which incoming connections are
accepted.
'';
};
allowedUDPPorts = lib.mkOption {
type = lib.types.listOf lib.types.port;
default = [ ];
apply = canonicalizePortList;
example = [ 53 ];
description = ''
List of open UDP ports.
'';
};
allowedUDPPortRanges = lib.mkOption {
type = lib.types.listOf (lib.types.attrsOf lib.types.port);
default = [ ];
example = [
{
from = 60000;
to = 61000;
}
];
description = ''
Range of open UDP ports.
'';
};
};
in
{
options = {
networking.firewall = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to enable the firewall. This is a simple stateful
firewall that blocks connection attempts to unauthorised TCP
or UDP ports on this machine.
'';
};
package = lib.mkOption {
type = lib.types.package;
default = if config.networking.nftables.enable then pkgs.nftables else pkgs.iptables;
defaultText = lib.literalExpression ''if config.networking.nftables.enable then "pkgs.nftables" else "pkgs.iptables"'';
example = lib.literalExpression "pkgs.iptables-legacy";
description = ''
The package to use for running the firewall service.
'';
};
logRefusedConnections = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to log rejected or dropped incoming connections.
Note: The logs are found in the kernel logs, i.e. dmesg
or journalctl -k.
'';
};
logRefusedPackets = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to log all rejected or dropped incoming packets.
This tends to give a lot of log messages, so it's mostly
useful for debugging.
Note: The logs are found in the kernel logs, i.e. dmesg
or journalctl -k.
'';
};
logRefusedUnicastsOnly = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
If {option}`networking.firewall.logRefusedPackets`
and this option are enabled, then only log packets
specifically directed at this machine, i.e., not broadcasts
or multicasts.
'';
};
rejectPackets = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
If set, refused packets are rejected rather than dropped
(ignored). This means that an ICMP "port unreachable" error
message is sent back to the client (or a TCP RST packet in
case of an existing connection). Rejecting packets makes
port scanning somewhat easier.
'';
};
trustedInterfaces = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "enp0s2" ];
description = ''
Traffic coming in from these interfaces will be accepted
unconditionally. Traffic from the loopback (lo) interface
will always be accepted.
'';
};
allowPing = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to respond to incoming ICMPv4 echo requests
("pings"). ICMPv6 pings are always allowed because the
larger address space of IPv6 makes network scanning much
less effective.
'';
};
pingLimit = lib.mkOption {
type = lib.types.nullOr (lib.types.separatedString " ");
default = null;
example = "--limit 1/minute --limit-burst 5";
description = ''
If pings are allowed, this allows setting rate limits on them.
For the iptables based firewall, it should be set like
"--limit 1/minute --limit-burst 5".
For the nftables based firewall, it should be set like
"2/second" or "1/minute burst 5 packets".
'';
};
checkReversePath = lib.mkOption {
type = lib.types.either lib.types.bool (
lib.types.enum [
"strict"
"loose"
]
);
default = true;
defaultText = lib.literalMD "`true` except if the iptables based firewall is in use and the kernel lacks rpfilter support";
example = "loose";
description = ''
Performs a reverse path filter test on a packet. If a reply
to the packet would not be sent via the same interface that
the packet arrived on, it is refused.
If using asymmetric routing or other complicated routing, set
this option to loose mode or disable it and setup your own
counter-measures.
This option can be either true (or "strict"), "loose" (only
drop the packet if the source address is not reachable via any
interface) or false.
'';
};
logReversePathDrops = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Logs dropped packets failing the reverse path filter test if
the option networking.firewall.checkReversePath is enabled.
'';
};
filterForward = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Enable filtering in IP forwarding.
This option only works with the nftables based firewall.
'';
};
connectionTrackingModules = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"ftp"
"irc"
"sane"
"sip"
"tftp"
"amanda"
"h323"
"netbios_sn"
"pptp"
"snmp"
];
description = ''
List of connection-tracking helpers that are auto-loaded.
The complete list of possible values is given in the example.
As helpers can pose as a security risk, it is advised to
set this to an empty list and disable the setting
networking.firewall.autoLoadConntrackHelpers unless you
know what you are doing. Connection tracking is disabled
by default.
Loading of helpers is recommended to be done through the
CT target. More info:
<https://home.regit.org/netfilter-en/secure-use-of-helpers/>
'';
};
autoLoadConntrackHelpers = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to auto-load connection-tracking helpers.
See the description at networking.firewall.connectionTrackingModules
(needs kernel 3.5+)
'';
};
extraPackages = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
example = lib.literalExpression "[ pkgs.ipset ]";
description = ''
Additional packages to be included in the environment of the system
as well as the path of networking.firewall.extraCommands.
'';
};
interfaces = lib.mkOption {
default = { };
type = with lib.types; attrsOf (submodule [ { options = commonOptions; } ]);
description = ''
Interface-specific open ports.
'';
};
allInterfaces = lib.mkOption {
internal = true;
visible = false;
default = {
default = lib.mapAttrs (name: value: cfg.${name}) commonOptions;
}
// cfg.interfaces;
type = with lib.types; attrsOf (submodule [ { options = commonOptions; } ]);
description = ''
All open ports.
'';
};
}
// commonOptions;
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.filterForward -> config.networking.nftables.enable;
message = "filterForward only works with the nftables based firewall";
}
{
assertion =
cfg.autoLoadConntrackHelpers -> lib.versionOlder config.boot.kernelPackages.kernel.version "6";
message = "conntrack helper autoloading has been removed from kernel 6.0 and newer";
}
];
networking.firewall.trustedInterfaces = [ "lo" ];
environment.systemPackages = [
cfg.package
pkgs.nixos-firewall-tool
]
++ cfg.extraPackages;
boot.kernelModules =
(lib.optional cfg.autoLoadConntrackHelpers "nf_conntrack")
++ map (x: "nf_conntrack_${x}") cfg.connectionTrackingModules;
boot.extraModprobeConfig = lib.optionalString cfg.autoLoadConntrackHelpers ''
options nf_conntrack nf_conntrack_helper=1
'';
};
}

View File

@@ -0,0 +1,159 @@
{
lib,
pkgs,
config,
...
}:
let
inherit (lib)
boolToString
getExe
mkEnableOption
mkIf
mkOption
mkPackageOption
types
;
cfg = config.services.firezone.gateway;
in
{
options = {
services.firezone.gateway = {
enable = mkOption {
default = false;
example = true;
description = ''
Whether to enable the firezone gateway.
You have to manually masquerade and forward traffic from the
tun-firezone interface to your resource! Refer to the
[upstream setup script](https://github.com/firezone/firezone/blob/8c7c0a9e8e33ae790aeb75fdb5a15432c2870b79/scripts/gateway-systemd-install.sh#L154-L168)
for a list of iptable commands.
See the firezone nixos test in this repository for an nftables based example.
'';
type = lib.types.bool;
};
package = mkPackageOption pkgs "firezone-gateway" { };
name = mkOption {
type = types.str;
description = "The name of this gateway as shown in firezone";
};
apiUrl = mkOption {
type = types.strMatching "^wss://.+/$";
example = "wss://firezone.example.com/api/";
description = ''
The URL of your firezone server's API. This should be the same
as your server's setting for {option}`services.firezone.server.settings.api.externalUrl`,
but with `wss://` instead of `https://`.
'';
};
tokenFile = mkOption {
type = types.path;
example = "/run/secrets/firezone-gateway-token";
description = ''
A file containing the firezone gateway token. Do not use a nix-store path here
as it will make the token publicly readable!
This file will be passed via systemd credentials, it should only be accessible
by the root user.
'';
};
logLevel = mkOption {
type = types.str;
default = "info";
description = ''
The log level for the firezone application. See
[RUST_LOG](https://docs.rs/env_logger/latest/env_logger/#enabling-logging)
for the format.
'';
};
enableTelemetry = mkEnableOption "telemetry";
};
};
config = mkIf cfg.enable {
systemd.services.firezone-gateway = {
description = "Gateway service for the Firezone zero-trust access platform";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.util-linux ];
script = ''
# If FIREZONE_ID is not given by the user, use a persisted (or newly generated) uuid.
if [[ -z "''${FIREZONE_ID:-}" ]]; then
if [[ ! -e gateway_id ]]; then
uuidgen -r > gateway_id
fi
export FIREZONE_ID=$(< gateway_id)
fi
export FIREZONE_TOKEN=$(< "$CREDENTIALS_DIRECTORY/firezone-token")
exec ${getExe cfg.package}
'';
environment = {
FIREZONE_API_URL = cfg.apiUrl;
FIREZONE_NAME = cfg.name;
FIREZONE_NO_TELEMETRY = boolToString (!cfg.enableTelemetry);
RUST_LOG = cfg.logLevel;
};
serviceConfig = {
Type = "exec";
DynamicUser = true;
User = "firezone-gateway";
LoadCredential = [ "firezone-token:${cfg.tokenFile}" ];
DeviceAllow = "/dev/net/tun";
AmbientCapabilities = [ "CAP_NET_ADMIN" ];
CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
StateDirectory = "firezone-gateway";
WorkingDirectory = "/var/lib/firezone-gateway";
Restart = "on-failure";
RestartSec = 10;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = false;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = "@system-service";
UMask = "077";
};
};
};
meta.maintainers = with lib.maintainers; [
oddlama
patrickdag
];
}

View File

@@ -0,0 +1,137 @@
{
lib,
pkgs,
config,
...
}:
let
inherit (lib)
getExe'
mkEnableOption
mkIf
mkOption
mkPackageOption
types
;
cfg = config.services.firezone.gui-client;
in
{
options = {
services.firezone.gui-client = {
enable = mkEnableOption "the firezone gui client";
package = mkPackageOption pkgs "firezone-gui-client" { };
allowedUsers = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
All listed users will become part of the `firezone-client` group so
they can control the tunnel service. This is a convenience option.
'';
};
name = mkOption {
type = types.str;
description = "The name of this client as shown in firezone";
};
logLevel = mkOption {
type = types.str;
default = "info";
description = ''
The log level for the firezone application. See
[RUST_LOG](https://docs.rs/env_logger/latest/env_logger/#enabling-logging)
for the format.
'';
};
};
};
config = mkIf cfg.enable {
users.groups.firezone-client.members = cfg.allowedUsers;
# Required for deep-link mimetype registration
environment.systemPackages = [ cfg.package ];
# Required for the token store in the gui application
services.gnome.gnome-keyring.enable = true;
systemd.services.firezone-tunnel-service = {
description = "GUI tunnel service for the Firezone zero-trust access platform";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.util-linux ];
script = ''
# If FIREZONE_ID is not given by the user, use a persisted (or newly generated) uuid.
if [[ -z "''${FIREZONE_ID:-}" ]]; then
if [[ ! -e client_id ]]; then
uuidgen -r > client_id
fi
export FIREZONE_ID=$(< client_id)
fi
exec ${getExe' cfg.package "firezone-client-tunnel"} run
'';
environment = {
FIREZONE_NAME = cfg.name;
LOG_DIR = "%L/dev.firezone.client";
RUST_LOG = cfg.logLevel;
};
serviceConfig = {
Type = "notify";
DeviceAllow = "/dev/net/tun";
AmbientCapabilities = [ "CAP_NET_ADMIN" ];
CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
# This block contains hardcoded values in the client, we cannot change these :(
Group = "firezone-client";
RuntimeDirectory = "dev.firezone.client";
StateDirectory = "dev.firezone.client";
WorkingDirectory = "/var/lib/dev.firezone.client";
LogsDirectory = "dev.firezone.client";
Restart = "on-failure";
RestartSec = 10;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = false;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = "@system-service";
UMask = "077";
};
};
};
meta.maintainers = with lib.maintainers; [
oddlama
patrickdag
];
}

View File

@@ -0,0 +1,148 @@
{
lib,
pkgs,
config,
...
}:
let
inherit (lib)
boolToString
getExe
mkEnableOption
mkIf
mkOption
mkPackageOption
types
;
cfg = config.services.firezone.headless-client;
in
{
options = {
services.firezone.headless-client = {
enable = mkEnableOption "the firezone headless client";
package = mkPackageOption pkgs "firezone-headless-client" { };
name = mkOption {
type = types.str;
description = "The name of this client as shown in firezone";
};
apiUrl = mkOption {
type = types.strMatching "^wss://.+/$";
example = "wss://firezone.example.com/api/";
description = ''
The URL of your firezone server's API. This should be the same
as your server's setting for {option}`services.firezone.server.settings.api.externalUrl`,
but with `wss://` instead of `https://`.
'';
};
tokenFile = mkOption {
type = types.path;
example = "/run/secrets/firezone-client-token";
description = ''
A file containing the firezone client token. Do not use a nix-store path here
as it will make the token publicly readable!
This file will be passed via systemd credentials, it should only be accessible
by the root user.
'';
};
logLevel = mkOption {
type = types.str;
default = "info";
description = ''
The log level for the firezone application. See
[RUST_LOG](https://docs.rs/env_logger/latest/env_logger/#enabling-logging)
for the format.
'';
};
enableTelemetry = mkEnableOption "telemetry";
};
};
config = mkIf cfg.enable {
systemd.services.firezone-headless-client = {
description = "headless client service for the Firezone zero-trust access platform";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.util-linux ];
script = ''
# If FIREZONE_ID is not given by the user, use a persisted (or newly generated) uuid.
if [[ -z "''${FIREZONE_ID:-}" ]]; then
if [[ ! -e client_id ]]; then
uuidgen -r > client_id
fi
export FIREZONE_ID=$(< client_id)
fi
exec ${getExe cfg.package}
'';
environment = {
FIREZONE_API_URL = cfg.apiUrl;
FIREZONE_NAME = cfg.name;
FIREZONE_NO_TELEMETRY = boolToString (!cfg.enableTelemetry);
FIREZONE_TOKEN_PATH = "%d/firezone-token";
LOG_DIR = "%L/dev.firezone.client";
RUST_LOG = cfg.logLevel;
};
serviceConfig = {
Type = "exec";
LoadCredential = [ "firezone-token:${cfg.tokenFile}" ];
DeviceAllow = "/dev/net/tun";
AmbientCapabilities = [ "CAP_NET_ADMIN" ];
CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
# Hardcoded values in the client :(
RuntimeDirectory = "dev.firezone.client";
StateDirectory = "dev.firezone.client";
WorkingDirectory = "/var/lib/dev.firezone.client";
LogsDirectory = "dev.firezone.client";
Restart = "on-failure";
RestartSec = 10;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = false;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = "@system-service";
UMask = "077";
};
};
};
meta.maintainers = with lib.maintainers; [
oddlama
patrickdag
];
}

View File

@@ -0,0 +1,709 @@
defmodule Provision do
alias Domain.{Repo, Accounts, Auth, Actors, Resources, Tokens, Gateways, Relays, Policies}
require Logger
# UUID Mapping handling
defmodule UuidMapping do
@mapping_file "provision-uuids.json"
# Loads the mapping from file
def load do
mappings = case File.read(@mapping_file) do
{:ok, content} ->
case Jason.decode(content) do
{:ok, mapping} -> mapping
_ -> %{"accounts" => %{}}
end
_ -> %{"accounts" => %{}}
end
Process.put(:uuid_mappings, mappings)
mappings
end
# Saves the current mapping (defaulting to the one in the process dictionary)
def save(mapping \\ Process.get(:uuid_mappings)) do
File.write!(@mapping_file, Jason.encode!(mapping))
end
# Retrieves the account-level mapping from a given mapping (or from Process)
def get_account(mapping \\ Process.get(:uuid_mappings), account_slug) do
get_in(mapping, ["accounts", account_slug]) || %{}
end
# Retrieves the entity mapping for a specific account and type
def get_entities(mapping \\ Process.get(:uuid_mappings), account_slug, type) do
get_in(mapping, ["accounts", account_slug, type]) || %{}
end
# Retrieves an entity mapping for a specific account, type and external_id
def get_entity(mapping \\ Process.get(:uuid_mappings), account_slug, type, external_id) do
get_in(mapping, ["accounts", account_slug, type, external_id])
end
# Updates (or creates) the account UUID mapping and stores it in the process dictionary.
def update_account(account_slug, uuid) do
mapping = Process.get(:uuid_mappings) || load()
mapping = ensure_account_exists(mapping, account_slug)
mapping = put_in(mapping, ["accounts", account_slug, "id"], uuid)
Process.put(:uuid_mappings, mapping)
mapping
end
# Ensures that the given account exists in the mapping.
def ensure_account_exists(mapping, account_slug) do
if not Map.has_key?(mapping["accounts"], account_slug) do
put_in(mapping, ["accounts", account_slug], %{})
else
mapping
end
end
# Updates (or creates) the mapping for entities of a given type for the account.
def update_entities(account_slug, type, new_entries) do
mapping = Process.get(:uuid_mappings) || load()
mapping = ensure_account_exists(mapping, account_slug)
current = get_entities(mapping, account_slug, type)
mapping = put_in(mapping, ["accounts", account_slug, type], Map.merge(current, new_entries))
Process.put(:uuid_mappings, mapping)
mapping
end
# Removes an entire account from the mapping.
def remove_account(account_slug) do
mapping = Process.get(:uuid_mappings) || load()
mapping = update_in(mapping, ["accounts"], fn accounts ->
Map.delete(accounts, account_slug)
end)
Process.put(:uuid_mappings, mapping)
mapping
end
# Removes a specific entity mapping for the account.
def remove_entity(account_slug, type, key) do
mapping = Process.get(:uuid_mappings) || load()
mapping = update_in(mapping, ["accounts", account_slug, type], fn entities ->
Map.delete(entities || %{}, key)
end)
Process.put(:uuid_mappings, mapping)
mapping
end
end
defp resolve_references(value) when is_map(value) do
Enum.into(value, %{}, fn {k, v} -> {k, resolve_references(v)} end)
end
defp resolve_references(value) when is_list(value) do
Enum.map(value, &resolve_references/1)
end
defp resolve_references(value) when is_binary(value) do
Regex.replace(~r/\{env:([^}]+)\}/, value, fn _, var ->
System.get_env(var) || raise "Environment variable #{var} not set"
end)
end
defp resolve_references(value), do: value
defp atomize_keys(map) when is_map(map) do
Enum.into(map, %{}, fn {k, v} ->
{
if(is_binary(k), do: String.to_atom(k), else: k),
if(is_map(v), do: atomize_keys(v), else: v)
}
end)
end
defp cleanup_account(uuid) do
case Accounts.fetch_account_by_id_or_slug(uuid) do
{:ok, value} when value.deleted_at == nil ->
Logger.info("Deleting removed account #{value.slug}")
value |> Ecto.Changeset.change(%{ deleted_at: DateTime.utc_now() }) |> Repo.update!()
_ -> :ok
end
end
defp cleanup_actor(uuid, subject) do
case Actors.fetch_actor_by_id(uuid, subject) do
{:ok, value} ->
Logger.info("Deleting removed actor #{value.name}")
{:ok, _} = Actors.delete_actor(value, subject)
_ -> :ok
end
end
defp cleanup_provider(uuid, subject) do
case Auth.fetch_provider_by_id(uuid, subject) do
{:ok, value} ->
Logger.info("Deleting removed provider #{value.name}")
{:ok, _} = Auth.delete_provider(value, subject)
_ -> :ok
end
end
defp cleanup_gateway_group(uuid, subject) do
case Gateways.fetch_group_by_id(uuid, subject) do
{:ok, value} ->
Logger.info("Deleting removed gateway group #{value.name}")
{:ok, _} = Gateways.delete_group(value, subject)
_ -> :ok
end
end
defp cleanup_relay_group(uuid, subject) do
case Relays.fetch_group_by_id(uuid, subject) do
{:ok, value} ->
Logger.info("Deleting removed relay group #{value.name}")
{:ok, _} = Relays.delete_group(value, subject)
_ -> :ok
end
end
defp cleanup_actor_group(uuid, subject) do
case Actors.fetch_group_by_id(uuid, subject) do
{:ok, value} ->
Logger.info("Deleting removed actor group #{value.name}")
{:ok, _} = Actors.delete_group(value, subject)
_ -> :ok
end
end
# Fetch resource by uuid, but follow the chain of replacements if any
defp fetch_resource(uuid, subject) do
case Resources.fetch_resource_by_id(uuid, subject) do
{:ok, resource} when resource.replaced_by_resource_id != nil -> fetch_resource(resource.replaced_by_resource_id, subject)
v -> v
end
end
defp cleanup_resource(uuid, subject) do
case fetch_resource(uuid, subject) do
{:ok, value} when value.deleted_at == nil ->
Logger.info("Deleting removed resource #{value.name}")
{:ok, _} = Resources.delete_resource(value, subject)
_ -> :ok
end
end
# Fetch policy by uuid, but follow the chain of replacements if any
defp fetch_policy(uuid, subject) do
case Policies.fetch_policy_by_id(uuid, subject) do
{:ok, policy} when policy.replaced_by_policy_id != nil -> fetch_policy(policy.replaced_by_policy_id, subject)
v -> v
end
end
defp cleanup_policy(uuid, subject) do
case fetch_policy(uuid, subject) do
{:ok, value} when value.deleted_at == nil ->
Logger.info("Deleting removed policy #{value.description}")
{:ok, _} = Policies.delete_policy(value, subject)
_ -> :ok
end
end
defp cleanup_entity_type(account_slug, entity_type, cleanup_fn, temp_admin_subject) do
# Get mapping for this entity type
existing_entities = UuidMapping.get_entities(account_slug, entity_type)
# Get current entities from account data
current_entities = Process.get(:current_entities)
# Determine which ones to remove
removed_entity_ids = Map.keys(existing_entities) -- (current_entities[entity_type] || [])
# Process each entity to remove
Enum.each(removed_entity_ids, fn entity_id ->
case existing_entities[entity_id] do
nil -> :ok
uuid ->
cleanup_fn.(uuid, temp_admin_subject)
UuidMapping.remove_entity(account_slug, entity_type, entity_id)
end
end)
end
defp collect_current_entities(account_data) do
%{
"actors" => Map.keys(account_data["actors"] || %{}),
"providers" => Map.keys(account_data["auth"] || %{}),
"gateway_groups" => Map.keys(account_data["gatewayGroups"] || %{}),
"relay_groups" => Map.keys(account_data["relayGroups"] || %{}),
"actor_groups" => Map.keys(account_data["groups"] || %{}) ++ ["everyone"],
"resources" => Map.keys(account_data["resources"] || %{}),
"policies" => Map.keys(account_data["policies"] || %{})
}
end
defp nil_if_deleted_or_not_found(value) do
case value do
nil -> nil
{:error, :not_found} -> nil
{:ok, value} when value.deleted_at != nil -> nil
v -> v
end
end
defp create_temp_admin(account, email_provider) do
temp_admin_actor_email = "firezone-provision@localhost.local"
temp_admin_actor_context = %Auth.Context{
type: :browser,
user_agent: "Unspecified/0.0",
remote_ip: {127, 0, 0, 1},
remote_ip_location_region: "N/A",
remote_ip_location_city: "N/A",
remote_ip_location_lat: 0.0,
remote_ip_location_lon: 0.0
}
{:ok, temp_admin_actor} =
Actors.create_actor(account, %{
type: :account_admin_user,
name: "Provisioning"
})
{:ok, temp_admin_actor_email_identity} =
Auth.create_identity(temp_admin_actor, email_provider, %{
provider_identifier: temp_admin_actor_email,
provider_identifier_confirmation: temp_admin_actor_email
})
{:ok, temp_admin_actor_token} =
Auth.create_token(temp_admin_actor_email_identity, temp_admin_actor_context, "temporarynonce", DateTime.utc_now() |> DateTime.add(1, :hour))
{:ok, temp_admin_subject} =
Auth.build_subject(temp_admin_actor_token, temp_admin_actor_context)
{temp_admin_subject, temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token}
end
defp cleanup_temp_admin(temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token, subject) do
Logger.info("Cleaning up temporary admin actor")
{:ok, _} = Tokens.delete_token(temp_admin_actor_token, subject)
{:ok, _} = Auth.delete_identity(temp_admin_actor_email_identity, subject)
{:ok, _} = Actors.delete_actor(temp_admin_actor, subject)
end
def provision() do
Logger.info("Starting provisioning")
# Load desired state
json_file = "provision-state.json"
{:ok, raw_json} = File.read(json_file)
{:ok, %{"accounts" => accounts}} = Jason.decode(raw_json)
accounts = resolve_references(accounts)
# Load existing UUID mappings into the process dictionary.
UuidMapping.load()
# Clean up removed accounts first
current_account_slugs = Map.keys(accounts)
existing_accounts = Map.keys(Process.get(:uuid_mappings)["accounts"])
removed_accounts = existing_accounts -- current_account_slugs
Enum.each(removed_accounts, fn slug ->
if uuid = get_in(Process.get(:uuid_mappings), ["accounts", slug, "id"]) do
cleanup_account(uuid)
# Remove the account from the UUID mapping.
UuidMapping.remove_account(slug)
end
end)
multi = Enum.reduce(accounts, Ecto.Multi.new(), fn {slug, account_data}, multi ->
account_attrs = atomize_keys(%{
name: account_data["name"],
slug: slug,
features: Map.get(account_data, "features", %{}),
metadata: Map.get(account_data, "metadata", %{}),
limits: Map.get(account_data, "limits", %{})
})
multi = multi
|> Ecto.Multi.run({:account, slug}, fn repo, _changes ->
case Accounts.fetch_account_by_id_or_slug(slug) do
{:ok, acc} ->
Logger.info("Updating existing account #{slug}")
updated_acc = acc |> Ecto.Changeset.change(account_attrs) |> repo.update!()
{:ok, {:existing, updated_acc}}
_ ->
Logger.info("Creating new account #{slug}")
{:ok, account} = Accounts.create_account(account_attrs)
Logger.info("Creating internet gateway group")
{:ok, internet_site} = Gateways.create_internet_group(account)
Logger.info("Creating internet resource")
{:ok, _internet_resource} = Resources.create_internet_resource(account, internet_site)
# Store mapping of slug to UUID
UuidMapping.update_account(slug, account.id)
{:ok, {:new, account}}
end
end)
|> Ecto.Multi.run({:everyone_group, slug}, fn _repo, changes ->
case Map.get(changes, {:account, slug}) do
{:new, account} ->
Logger.info("Creating everyone group for new account")
{:ok, actor_group} = Actors.create_managed_group(account, %{name: "Everyone"})
UuidMapping.update_entities(slug, "actor_groups", %{"everyone" => actor_group.id})
{:ok, actor_group}
{:existing, _account} ->
{:ok, :skipped}
end
end)
|> Ecto.Multi.run({:email_provider, slug}, fn _repo, changes ->
case Map.get(changes, {:account, slug}) do
{:new, account} ->
Logger.info("Creating default email provider for new account")
Auth.create_provider(account, %{name: "Email", adapter: :email, adapter_config: %{}})
{:existing, account} ->
Auth.Provider.Query.not_disabled()
|> Auth.Provider.Query.by_adapter(:email)
|> Auth.Provider.Query.by_account_id(account.id)
|> Repo.fetch(Auth.Provider.Query, [])
end
end)
|> Ecto.Multi.run({:temp_admin, slug}, fn _repo, changes ->
{_, account} = changes[{:account, slug}]
email_provider = changes[{:email_provider, slug}]
{:ok, create_temp_admin(account, email_provider)}
end)
# Clean up removed entities for this account after we have an admin subject
multi = multi
|> Ecto.Multi.run({:cleanup_entities, slug}, fn _repo, changes ->
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
# Store current entities in process dictionary for our helper function
current_entities = collect_current_entities(account_data)
Process.put(:current_entities, current_entities)
# Define entity types and their cleanup functions
entity_types = [
{"actors", &cleanup_actor/2},
{"providers", &cleanup_provider/2},
{"gateway_groups", &cleanup_gateway_group/2},
{"relay_groups", &cleanup_relay_group/2},
{"actor_groups", &cleanup_actor_group/2},
{"resources", &cleanup_resource/2},
{"policies", &cleanup_policy/2}
]
# Clean up each entity type
Enum.each(entity_types, fn {entity_type, cleanup_fn} ->
cleanup_entity_type(slug, entity_type, cleanup_fn, temp_admin_subject)
end)
{:ok, :cleaned}
end)
# Create or update actors
multi = Enum.reduce(account_data["actors"] || %{}, multi, fn {external_id, actor_data}, multi ->
actor_attrs = atomize_keys(%{
name: actor_data["name"],
type: String.to_atom(actor_data["type"])
})
Ecto.Multi.run(multi, {:actor, slug, external_id}, fn _repo, changes ->
{_, account} = changes[{:account, slug}]
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
uuid = UuidMapping.get_entity(slug, "actors", external_id)
case uuid && Actors.fetch_actor_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
nil ->
Logger.info("Creating new actor #{actor_data["name"]}")
{:ok, actor} = Actors.create_actor(account, actor_attrs)
# Update the mapping without manually handling Process.get/put.
UuidMapping.update_entities(slug, "actors", %{external_id => actor.id})
{:ok, {:new, actor}}
{:ok, existing_actor} ->
Logger.info("Updating existing actor #{actor_data["name"]}")
{:ok, updated_act} = Actors.update_actor(existing_actor, actor_attrs, temp_admin_subject)
{:ok, {:existing, updated_act}}
end
end)
|> Ecto.Multi.run({:actor_identity, slug, external_id}, fn repo, changes ->
email_provider = changes[{:email_provider, slug}]
case Map.get(changes, {:actor, slug, external_id}) do
{:new, actor} ->
Logger.info("Creating actor email identity")
Auth.create_identity(actor, email_provider, %{
provider_identifier: actor_data["email"],
provider_identifier_confirmation: actor_data["email"]
})
{:existing, actor} ->
Logger.info("Updating actor email identity")
{:ok, identity} = Auth.Identity.Query.not_deleted()
|> Auth.Identity.Query.by_actor_id(actor.id)
|> Auth.Identity.Query.by_provider_id(email_provider.id)
|> Repo.fetch(Auth.Identity.Query, [])
{:ok, identity |> Ecto.Changeset.change(%{
provider_identifier: actor_data["email"]
}) |> repo.update!()}
end
end)
end)
# Create or update providers
multi = Enum.reduce(account_data["auth"] || %{}, multi, fn {external_id, provider_data}, multi ->
Ecto.Multi.run(multi, {:provider, slug, external_id}, fn repo, changes ->
provider_attrs = %{
name: provider_data["name"],
adapter: String.to_atom(provider_data["adapter"]),
adapter_config: provider_data["adapter_config"]
}
{_, account} = changes[{:account, slug}]
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
uuid = UuidMapping.get_entity(slug, "providers", external_id)
case uuid && Auth.fetch_provider_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
nil ->
Logger.info("Creating new provider #{provider_data["name"]}")
{:ok, provider} = Auth.create_provider(account, provider_attrs)
UuidMapping.update_entities(slug, "providers", %{external_id => provider.id})
{:ok, provider}
{:ok, existing} ->
Logger.info("Updating existing provider #{provider_data["name"]}")
{:ok, existing |> Ecto.Changeset.change(provider_attrs) |> repo.update!()}
end
end)
end)
# Create or update gateway_groups
multi = Enum.reduce(account_data["gatewayGroups"] || %{}, multi, fn {external_id, gateway_group_data}, multi ->
Ecto.Multi.run(multi, {:gateway_group, slug, external_id}, fn _repo, changes ->
gateway_group_attrs = %{
name: gateway_group_data["name"],
tokens: [%{}]
}
{_, account} = changes[{:account, slug}]
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
uuid = UuidMapping.get_entity(slug, "gateway_groups", external_id)
case uuid && Gateways.fetch_group_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
nil ->
Logger.info("Creating new gateway group #{gateway_group_data["name"]}")
gateway_group = account
|> Gateways.Group.Changeset.create(gateway_group_attrs, temp_admin_subject)
|> Repo.insert!()
UuidMapping.update_entities(slug, "gateway_groups", %{external_id => gateway_group.id})
{:ok, gateway_group}
{:ok, existing} ->
# Nothing to update
{:ok, existing}
end
end)
end)
# Create or update relay_groups
multi = Enum.reduce(account_data["relayGroups"] || %{}, multi, fn {external_id, relay_group_data}, multi ->
Ecto.Multi.run(multi, {:relay_group, slug, external_id}, fn _repo, changes ->
relay_group_attrs = %{
name: relay_group_data["name"]
}
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
uuid = UuidMapping.get_entity(slug, "relay_groups", external_id)
existing_relay_group = uuid && Relays.fetch_group_by_id(uuid, temp_admin_subject)
case existing_relay_group do
v when v in [nil, {:error, :not_found}] ->
Logger.info("Creating new relay group #{relay_group_data["name"]}")
{:ok, relay_group} = Relays.create_group(relay_group_attrs, temp_admin_subject)
UuidMapping.update_entities(slug, "relay_groups", %{external_id => relay_group.id})
{:ok, relay_group}
{:ok, existing} ->
# Nothing to update
{:ok, existing}
end
end)
end)
# Create or update actor_groups
multi = Enum.reduce(account_data["groups"] || %{}, multi, fn {external_id, actor_group_data}, multi ->
Ecto.Multi.run(multi, {:actor_group, slug, external_id}, fn _repo, changes ->
actor_group_attrs = %{
name: actor_group_data["name"],
type: :static
}
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
uuid = UuidMapping.get_entity(slug, "actor_groups", external_id)
case uuid && Actors.fetch_group_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
nil ->
Logger.info("Creating new actor group #{actor_group_data["name"]}")
{:ok, actor_group} = Actors.create_group(actor_group_attrs, temp_admin_subject)
UuidMapping.update_entities(slug, "actor_groups", %{external_id => actor_group.id})
{:ok, actor_group}
{:ok, existing} ->
# Nothing to update
{:ok, existing}
end
end)
|> Ecto.Multi.run({:actor_group_members, slug, external_id}, fn repo, changes ->
{_, account} = changes[{:account, slug}]
group_uuid = UuidMapping.get_entity(slug, "actor_groups", external_id)
memberships =
Actors.Membership.Query.all()
|> Actors.Membership.Query.by_group_id(group_uuid)
|> Actors.Membership.Query.returning_all()
|> Repo.all()
existing_members = Enum.map(memberships, fn membership -> membership.actor_id end)
desired_members = Enum.map(actor_group_data["members"] || [], fn member ->
uuid = UuidMapping.get_entity(slug, "actors", member)
if uuid == nil do
raise "Cannot find provisioned actor #{member} to add to group"
end
uuid
end)
missing_members = desired_members -- existing_members
untracked_members = existing_members -- desired_members
Logger.info("Updating members for actor group #{external_id}")
Enum.each(missing_members || [], fn actor_uuid ->
Logger.info("Adding member #{external_id}")
Actors.Membership.Changeset.upsert(account.id, %Actors.Membership{}, %{
group_id: group_uuid,
actor_id: actor_uuid
})
|> repo.insert!()
end)
if actor_group_data["forceMembers"] == true do
# Remove untracked members
to_delete = Enum.map(untracked_members, fn actor_uuid -> {group_uuid, actor_uuid} end)
if to_delete != [] do
Actors.Membership.Query.by_group_id_and_actor_id({:in, to_delete})
|> repo.delete_all()
end
end
{:ok, nil}
end)
end)
# Create or update resources
multi = Enum.reduce(account_data["resources"] || %{}, multi, fn {external_id, resource_data}, multi ->
Ecto.Multi.run(multi, {:resource, slug, external_id}, fn _repo, changes ->
resource_attrs = %{
type: String.to_atom(resource_data["type"]),
name: resource_data["name"],
address: resource_data["address"],
address_description: resource_data["address_description"],
connections: Enum.map(resource_data["gatewayGroups"] || [], fn group ->
%{gateway_group_id: UuidMapping.get_entity(slug, "gateway_groups", group)}
end),
filters: Enum.map(resource_data["filters"] || [], fn filter ->
%{
ports: filter["ports"] || [],
protocol: String.to_atom(filter["protocol"])
}
end)
}
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
uuid = UuidMapping.get_entity(slug, "resources", external_id)
case uuid && fetch_resource(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
nil ->
Logger.info("Creating new resource #{resource_data["name"]}")
{:ok, resource} = Resources.create_resource(resource_attrs, temp_admin_subject)
UuidMapping.update_entities(slug, "resources", %{external_id => resource.id})
{:ok, resource}
{:ok, existing} ->
existing = Repo.preload(existing, :connections)
Logger.info("Updating existing resource #{resource_data["name"]}")
only_updated_attrs = resource_attrs
|> Enum.reject(fn {key, value} ->
case key do
# Compare connections by gateway_group_id only
:connections -> value == Enum.map(existing.connections || [], fn conn -> Map.take(conn, [:gateway_group_id]) end)
# Compare filters by ports and protocol only
:filters -> value == Enum.map(existing.filters || [], fn filter -> Map.take(filter, [:ports, :protocol]) end)
_ -> Map.get(existing, key) == value
end
end)
|> Enum.into(%{})
if only_updated_attrs == %{} do
{:ok, existing}
else
resource = case existing |> Resources.update_resource(resource_attrs, temp_admin_subject) do
{:replaced, _old, new} ->
UuidMapping.update_entities(slug, "resources", %{external_id => new.id})
new
{:updated, value} -> value
x -> x
end
{:ok, resource}
end
end
end)
end)
# Create or update policies
multi = Enum.reduce(account_data["policies"] || %{}, multi, fn {external_id, policy_data}, multi ->
Ecto.Multi.run(multi, {:policy, slug, external_id}, fn _repo, changes ->
policy_attrs = %{
description: policy_data["description"],
actor_group_id: UuidMapping.get_entity(slug, "actor_groups", policy_data["group"]),
resource_id: UuidMapping.get_entity(slug, "resources", policy_data["resource"])
}
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
uuid = UuidMapping.get_entity(slug, "policies", external_id)
case uuid && fetch_policy(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
nil ->
Logger.info("Creating new policy #{policy_data["name"]}")
{:ok, policy} = Policies.create_policy(policy_attrs, temp_admin_subject)
UuidMapping.update_entities(slug, "policies", %{external_id => policy.id})
{:ok, policy}
{:ok, existing} ->
Logger.info("Updating existing policy #{policy_data["name"]}")
only_updated_attrs = policy_attrs
|> Enum.reject(fn {key, value} -> Map.get(existing, key) == value end)
|> Enum.into(%{})
if only_updated_attrs == %{} do
{:ok, existing}
else
policy = case existing |> Policies.update_policy(policy_attrs, temp_admin_subject) do
{:replaced, _old, new} ->
UuidMapping.update_entities(slug, "policies", %{external_id => new.id})
new
{:updated, value} -> value
x -> x
end
{:ok, policy}
end
end
end)
end)
# Clean up temporary admin after all operations
multi |> Ecto.Multi.run({:cleanup_temp_admin, slug}, fn _repo, changes ->
{temp_admin_subject, temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token} =
changes[{:temp_admin, slug}]
cleanup_temp_admin(temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token, temp_admin_subject)
{:ok, :cleaned}
end)
end)
|> Ecto.Multi.run({:save_state}, fn _repo, _changes ->
# Save all UUID mappings to disk.
UuidMapping.save()
{:ok, :saved}
end)
case Repo.transaction(multi) do
{:ok, _result} ->
Logger.info("Provisioning completed successfully")
{:error, step, reason, _changes} ->
Logger.error("Provisioning failed at step #{inspect(step)}, no changes were applied: #{inspect(reason)}")
end
end
end
Provision.provision()

View File

@@ -0,0 +1,202 @@
{
lib,
pkgs,
config,
...
}:
let
inherit (lib)
boolToString
getExe
mkEnableOption
mkIf
mkOption
mkPackageOption
types
;
cfg = config.services.firezone.relay;
in
{
options = {
services.firezone.relay = {
enable = mkEnableOption "the firezone relay server";
package = mkPackageOption pkgs "firezone-relay" { };
name = mkOption {
type = types.str;
example = "My relay";
description = "The name of this gateway as shown in firezone";
};
publicIpv4 = mkOption {
type = types.nullOr types.str;
default = null;
description = "The public ipv4 address of this relay";
};
publicIpv6 = mkOption {
type = types.nullOr types.str;
default = null;
description = "The public ipv6 address of this relay";
};
openFirewall = mkOption {
type = types.bool;
default = true;
description = "Opens up the main STUN port and the TURN allocation range.";
};
port = mkOption {
type = types.port;
default = 3478;
description = "The port to listen on for STUN messages";
};
lowestPort = mkOption {
type = types.port;
default = 49152;
description = "The lowest port to use in TURN allocation";
};
highestPort = mkOption {
type = types.port;
default = 65535;
description = "The highest port to use in TURN allocation";
};
apiUrl = mkOption {
type = types.strMatching "^wss://.+/$";
example = "wss://firezone.example.com/api/";
description = ''
The URL of your firezone server's API. This should be the same
as your server's setting for {option}`services.firezone.server.settings.api.externalUrl`,
but with `wss://` instead of `https://`.
'';
};
tokenFile = mkOption {
type = types.path;
example = "/run/secrets/firezone-relay-token";
description = ''
A file containing the firezone relay token. Do not use a nix-store path here
as it will make the token publicly readable!
This file will be passed via systemd credentials, it should only be accessible
by the root user.
'';
};
logLevel = mkOption {
type = types.str;
default = "info";
description = ''
The log level for the firezone application. See
[RUST_LOG](https://docs.rs/env_logger/latest/env_logger/#enabling-logging)
for the format.
'';
};
enableTelemetry = mkEnableOption "telemetry";
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.publicIpv4 != null || cfg.publicIpv6 != null;
message = "At least one of `services.firezone.relay.publicIpv4` and `services.firezone.relay.publicIpv6` must be set";
}
];
networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall [ cfg.port ];
networking.firewall.allowedUDPPortRanges = mkIf cfg.openFirewall [
{
from = cfg.lowestPort;
to = cfg.highestPort;
}
];
systemd.services.firezone-relay = {
description = "relay service for the Firezone zero-trust access platform";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.util-linux ];
script = ''
# If FIREZONE_ID is not given by the user, use a persisted (or newly generated) uuid.
if [[ -z "''${FIREZONE_ID:-}" ]]; then
if [[ ! -e relay_id ]]; then
uuidgen -r > relay_id
fi
export FIREZONE_ID=$(< relay_id)
fi
export FIREZONE_TOKEN=$(< "$CREDENTIALS_DIRECTORY/firezone-token")
exec ${getExe cfg.package}
'';
environment = {
FIREZONE_API_URL = cfg.apiUrl;
FIREZONE_NAME = cfg.name;
FIREZONE_TELEMETRY = boolToString cfg.enableTelemetry;
PUBLIC_IP4_ADDR = cfg.publicIpv4;
PUBLIC_IP6_ADDR = cfg.publicIpv6;
LISTEN_PORT = toString cfg.port;
LOWEST_PORT = toString cfg.lowestPort;
HIGHEST_PORT = toString cfg.highestPort;
RUST_LOG = cfg.logLevel;
LOG_FORMAT = "human";
};
serviceConfig = {
Type = "exec";
DynamicUser = true;
User = "firezone-relay";
LoadCredential = [ "firezone-token:${cfg.tokenFile}" ];
StateDirectory = "firezone-relay";
WorkingDirectory = "/var/lib/firezone-relay";
Restart = "on-failure";
RestartSec = 10;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = false;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = "@system-service";
UMask = "077";
};
};
};
meta.maintainers = with lib.maintainers; [
oddlama
patrickdag
];
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,209 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.flannel;
networkConfig =
(lib.filterAttrs (n: v: v != null) {
Network = cfg.network;
SubnetLen = cfg.subnetLen;
SubnetMin = cfg.subnetMin;
SubnetMax = cfg.subnetMax;
Backend = cfg.backend;
})
// cfg.extraNetworkConfig;
in
{
options.services.flannel = {
enable = lib.mkEnableOption "flannel";
package = lib.mkPackageOption pkgs "flannel" { };
publicIp = lib.mkOption {
description = ''
IP accessible by other nodes for inter-host communication.
Defaults to the IP of the interface being used for communication.
'';
type = lib.types.nullOr lib.types.str;
default = null;
};
iface = lib.mkOption {
description = ''
Interface to use (IP or name) for inter-host communication.
Defaults to the interface for the default route on the machine.
'';
type = lib.types.nullOr lib.types.str;
default = null;
};
etcd = {
endpoints = lib.mkOption {
description = "Etcd endpoints";
type = lib.types.listOf lib.types.str;
default = [ "http://127.0.0.1:2379" ];
};
prefix = lib.mkOption {
description = "Etcd key prefix";
type = lib.types.str;
default = "/coreos.com/network";
};
caFile = lib.mkOption {
description = "Etcd certificate authority file";
type = lib.types.nullOr lib.types.path;
default = null;
};
certFile = lib.mkOption {
description = "Etcd cert file";
type = lib.types.nullOr lib.types.path;
default = null;
};
keyFile = lib.mkOption {
description = "Etcd key file";
type = lib.types.nullOr lib.types.path;
default = null;
};
};
kubeconfig = lib.mkOption {
description = ''
Path to kubeconfig to use for storing flannel config using the
Kubernetes API
'';
type = lib.types.nullOr lib.types.path;
default = null;
};
network = lib.mkOption {
description = "IPv4 network in CIDR format to use for the entire flannel network";
type = lib.types.str;
};
nodeName = lib.mkOption {
description = ''
Needed when running with Kubernetes as backend as this cannot be auto-detected";
'';
type = lib.types.nullOr lib.types.str;
default = config.networking.fqdnOrHostName;
defaultText = lib.literalExpression "config.networking.fqdnOrHostName";
example = "node1.example.com";
};
storageBackend = lib.mkOption {
description = "Determines where flannel stores its configuration at runtime";
type = lib.types.enum [
"etcd"
"kubernetes"
];
default = "etcd";
};
subnetLen = lib.mkOption {
description = ''
The size of the subnet allocated to each host. Defaults to 24 (i.e. /24)
unless the Network was configured to be smaller than a /24 in which case
it is one less than the network.
'';
type = lib.types.int;
default = 24;
};
subnetMin = lib.mkOption {
description = ''
The beginning of IP range which the subnet allocation should start with.
Defaults to the first subnet of Network.
'';
type = lib.types.nullOr lib.types.str;
default = null;
};
subnetMax = lib.mkOption {
description = ''
The end of IP range which the subnet allocation should start with.
Defaults to the last subnet of Network.
'';
type = lib.types.nullOr lib.types.str;
default = null;
};
backend = lib.mkOption {
description = "Type of backend to use and specific configurations for that backend.";
type = lib.types.attrs;
default = {
Type = "vxlan";
};
};
extraNetworkConfig = lib.mkOption {
description = "Extra configuration to be added to the net-conf.json/etcd-backed network configuration.";
type = (pkgs.formats.json { }).type;
default = { };
example = {
EnableIPv6 = true;
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.flannel = {
description = "Flannel Service";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
environment = {
FLANNELD_PUBLIC_IP = cfg.publicIp;
FLANNELD_IFACE = cfg.iface;
}
// lib.optionalAttrs (cfg.storageBackend == "etcd") {
FLANNELD_ETCD_ENDPOINTS = lib.concatStringsSep "," cfg.etcd.endpoints;
FLANNELD_ETCD_KEYFILE = cfg.etcd.keyFile;
FLANNELD_ETCD_CERTFILE = cfg.etcd.certFile;
FLANNELD_ETCD_CAFILE = cfg.etcd.caFile;
ETCDCTL_CERT = cfg.etcd.certFile;
ETCDCTL_KEY = cfg.etcd.keyFile;
ETCDCTL_CACERT = cfg.etcd.caFile;
ETCDCTL_ENDPOINTS = lib.concatStringsSep "," cfg.etcd.endpoints;
ETCDCTL_API = "3";
}
// lib.optionalAttrs (cfg.storageBackend == "kubernetes") {
FLANNELD_KUBE_SUBNET_MGR = "true";
FLANNELD_KUBECONFIG_FILE = cfg.kubeconfig;
NODE_NAME = cfg.nodeName;
};
path = [ pkgs.iptables ];
preStart = lib.optionalString (cfg.storageBackend == "etcd") ''
echo "setting network configuration"
until ${pkgs.etcd}/bin/etcdctl put /coreos.com/network/config '${builtins.toJSON networkConfig}'
do
echo "setting network configuration, retry"
sleep 1
done
'';
serviceConfig = {
ExecStart = "${cfg.package}/bin/flannel";
Restart = "always";
RestartSec = "10s";
RuntimeDirectory = "flannel";
};
};
boot.kernelModules = [ "br_netfilter" ];
services.etcd.enable = lib.mkDefault (
cfg.storageBackend == "etcd" && cfg.etcd.endpoints == [ "http://127.0.0.1:2379" ]
);
# for some reason, flannel doesn't let you configure this path
# see: https://github.com/coreos/flannel/blob/master/Documentation/configuration.md#configuration
environment.etc."kube-flannel/net-conf.json" = lib.mkIf (cfg.storageBackend == "kubernetes") {
source = pkgs.writeText "net-conf.json" (builtins.toJSON networkConfig);
};
};
}

View File

@@ -0,0 +1,51 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.freenet;
varDir = "/var/lib/freenet";
in
{
options = {
services.freenet = {
enable = lib.mkEnableOption "Freenet daemon";
nice = lib.mkOption {
type = lib.types.ints.between (-20) 19;
default = 10;
description = "Set the nice level for the Freenet daemon";
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.freenet = {
description = "Freenet daemon";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = lib.getExe pkgs.freenet;
User = "freenet";
UMask = "0007";
WorkingDirectory = varDir;
Nice = cfg.nice;
};
};
users.users.freenet = {
group = "freenet";
description = "Freenet daemon user";
home = varDir;
createHome = true;
uid = config.ids.uids.freenet;
};
users.groups.freenet.gid = config.ids.gids.freenet;
};
meta.maintainers = with lib.maintainers; [ nagy ];
}

View File

@@ -0,0 +1,90 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.freeradius;
freeradiusService = cfg: {
description = "FreeRadius server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
wants = [ "network.target" ];
preStart = ''
${cfg.package}/bin/radiusd -C -d ${cfg.configDir} -l stdout
'';
serviceConfig = {
ExecStart =
"${cfg.package}/bin/radiusd -f -d ${cfg.configDir} -l stdout" + lib.optionalString cfg.debug " -xx";
ExecReload = [
"${cfg.package}/bin/radiusd -C -d ${cfg.configDir} -l stdout"
"${pkgs.coreutils}/bin/kill -HUP $MAINPID"
];
User = "radius";
ProtectSystem = "full";
ProtectHome = "on";
Restart = "on-failure";
RestartSec = 2;
LogsDirectory = "radius";
};
};
freeradiusConfig = {
enable = lib.mkEnableOption "the freeradius server";
package = lib.mkPackageOption pkgs "freeradius" { };
configDir = lib.mkOption {
type = lib.types.path;
default = "/etc/raddb";
description = ''
The path of the freeradius server configuration directory.
'';
};
debug = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable debug logging for freeradius (-xx
option). This should not be left on, since it includes
sensitive data such as passwords in the logs.
'';
};
};
in
{
###### interface
options = {
services.freeradius = freeradiusConfig;
};
###### implementation
config = lib.mkIf (cfg.enable) {
users = {
users.radius = {
# uid = config.ids.uids.radius;
description = "Radius daemon user";
isSystemUser = true;
group = "radius";
};
groups.radius = { };
};
systemd.services.freeradius = freeradiusService cfg;
warnings = lib.optional cfg.debug "Freeradius debug logging is enabled. This will log passwords in plaintext to the journal!";
};
}

View File

@@ -0,0 +1,98 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.frp;
settingsFormat = pkgs.formats.toml { };
configFile = settingsFormat.generate "frp.toml" cfg.settings;
isClient = (cfg.role == "client");
isServer = (cfg.role == "server");
in
{
options = {
services.frp = {
enable = lib.mkEnableOption "frp";
package = lib.mkPackageOption pkgs "frp" { };
role = lib.mkOption {
type = lib.types.enum [
"server"
"client"
];
description = ''
The frp consists of `client` and `server`. The server is usually
deployed on the machine with a public IP address, and
the client is usually deployed on the machine
where the Intranet service to be penetrated resides.
'';
};
settings = lib.mkOption {
type = settingsFormat.type;
default = { };
description = ''
Frp configuration, for configuration options
see the example of [client](https://github.com/fatedier/frp/blob/dev/conf/frpc_full_example.toml)
or [server](https://github.com/fatedier/frp/blob/dev/conf/frps_full_example.toml) on github.
'';
example = {
serverAddr = "x.x.x.x";
serverPort = 7000;
};
};
};
};
config =
let
serviceCapability = lib.optionals isServer [ "CAP_NET_BIND_SERVICE" ];
executableFile = if isClient then "frpc" else "frps";
in
lib.mkIf cfg.enable {
systemd.services = {
frp = {
wants = lib.optionals isClient [ "network-online.target" ];
after = if isClient then [ "network-online.target" ] else [ "network.target" ];
wantedBy = [ "multi-user.target" ];
description = "A fast reverse proxy frp ${cfg.role}";
serviceConfig = {
Type = "simple";
Restart = "on-failure";
RestartSec = 15;
ExecStart = "${cfg.package}/bin/${executableFile} --strict_config -c ${configFile}";
StateDirectoryMode = lib.optionalString isServer "0700";
DynamicUser = true;
# Hardening
UMask = lib.optionalString isServer "0007";
CapabilityBoundingSet = serviceCapability;
AmbientCapabilities = serviceCapability;
PrivateDevices = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
]
++ lib.optionals isClient [ "AF_UNIX" ];
LockPersonality = true;
MemoryDenyWriteExecute = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
PrivateMounts = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "@system-service" ];
};
};
};
};
meta.maintainers = with lib.maintainers; [ zaldnoay ];
}

View File

@@ -0,0 +1,317 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.frr;
daemons = [
"bgpd"
"ospfd"
"ospf6d"
"ripd"
"ripngd"
"isisd"
"pimd"
"pim6d"
"ldpd"
"nhrpd"
"eigrpd"
"babeld"
"sharpd"
"pbrd"
"bfdd"
"fabricd"
"vrrpd"
"pathd"
];
daemonDefaultOptions = {
zebra = "-A 127.0.0.1 -s 90000000";
mgmtd = "-A 127.0.0.1";
bgpd = "-A 127.0.0.1";
ospfd = "-A 127.0.0.1";
ospf6d = "-A ::1";
ripd = "-A 127.0.0.1";
ripngd = "-A ::1";
isisd = "-A 127.0.0.1";
pimd = "-A 127.0.0.1";
pim6d = "-A ::1";
ldpd = "-A 127.0.0.1";
nhrpd = "-A 127.0.0.1";
eigrpd = "-A 127.0.0.1";
babeld = "-A 127.0.0.1";
sharpd = "-A 127.0.0.1";
pbrd = "-A 127.0.0.1";
staticd = "-A 127.0.0.1";
bfdd = "-A 127.0.0.1";
fabricd = "-A 127.0.0.1";
vrrpd = "-A 127.0.0.1";
pathd = "-A 127.0.0.1";
};
renamedServices = [
"bgp"
"ospf"
"ospf6"
"rip"
"ripng"
"isis"
"pim"
"ldp"
"nhrp"
"eigrp"
"babel"
"sharp"
"pbr"
"bfd"
"fabric"
];
obsoleteServices = renamedServices ++ [
"static"
"mgmt"
"zebra"
];
allDaemons = builtins.attrNames daemonDefaultOptions;
isEnabled = service: cfg.${service}.enable;
daemonLine = d: "${d}=${if isEnabled d then "yes" else "no"}";
configFile =
if cfg.configFile != null then
cfg.configFile
else
pkgs.writeText "frr.conf" ''
! FRR configuration
!
hostname ${config.networking.hostName}
log syslog
service password-encryption
service integrated-vtysh-config
!
${cfg.config}
!
end
'';
serviceOptions =
service:
{
options = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ daemonDefaultOptions.${service} ];
description = ''
Options for the FRR ${service} daemon.
'';
};
extraOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Extra options to be appended to the FRR ${service} daemon options.
'';
};
}
// (
if (builtins.elem service daemons) then { enable = lib.mkEnableOption "FRR ${service}"; } else { }
);
in
{
###### interface
imports = [
{
options.services.frr = {
configFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/etc/frr/frr.conf";
description = ''
Configuration file to use for FRR.
By default the NixOS generated files are used.
'';
};
config = lib.mkOption {
type = lib.types.lines;
default = "";
example = ''
router rip
network 10.0.0.0/8
router ospf
network 10.0.0.0/8 area 0
router bgp 65001
neighbor 10.0.0.1 remote-as 65001
'';
description = ''
FRR configuration statements.
'';
};
openFilesLimit = lib.mkOption {
type = lib.types.ints.unsigned;
default = 1024;
description = ''
This is the maximum number of FD's that will be available. Use a
reasonable value for your setup if you are expecting a large number
of peers in say BGP.
'';
};
};
}
{ options.services.frr = (lib.genAttrs allDaemons serviceOptions); }
(lib.mkRemovedOptionModule [ "services" "frr" "zebra" "enable" ] "FRR zebra is always enabled")
]
++ (map (
d: lib.mkRenamedOptionModule [ "services" "frr" d "enable" ] [ "services" "frr" "${d}d" "enable" ]
) renamedServices)
++ (map
(
d:
lib.mkRenamedOptionModule
[ "services" "frr" d "extraOptions" ]
[ "services" "frr" "${d}d" "extraOptions" ]
)
(
renamedServices
++ [
"static"
"mgmt"
]
)
)
++ (map (d: lib.mkRemovedOptionModule [ "services" "frr" d "enable" ] "FRR ${d}d is always enabled")
[
"static"
"mgmt"
]
)
++ (map (
d:
lib.mkRemovedOptionModule [
"services"
"frr"
d
"config"
] "FRR switched to integrated-vtysh-config, please use services.frr.config"
) obsoleteServices)
++ (map (
d:
lib.mkRemovedOptionModule [ "services" "frr" d "configFile" ]
"FRR switched to integrated-vtysh-config, please use services.frr.config or services.frr.configFile"
) obsoleteServices)
++ (map (
d:
lib.mkRemovedOptionModule [
"services"
"frr"
d
"vtyListenAddress"
] "Please change -A option in services.frr.${d}.options instead"
) obsoleteServices)
++ (map (
d:
lib.mkRemovedOptionModule [ "services" "frr" d "vtyListenPort" ]
"Please use `-P «vtyListenPort»` option with services.frr.${d}.extraOptions instead, or change services.frr.${d}.options accordingly"
) obsoleteServices);
###### implementation
config =
let
daemonList = lib.concatStringsSep "\n" (map daemonLine daemons);
daemonOptionLine =
d: "${d}_options=\"${lib.concatStringsSep " " (cfg.${d}.options ++ cfg.${d}.extraOptions)}\"";
daemonOptions = lib.concatStringsSep "\n" (map daemonOptionLine allDaemons);
in
lib.mkIf (lib.any isEnabled daemons || cfg.configFile != null || cfg.config != "") {
environment.systemPackages = [
pkgs.frr # for the vtysh tool
];
users.users.frr = {
description = "FRR daemon user";
isSystemUser = true;
group = "frr";
};
users.groups = {
frr = { };
# Members of the frrvty group can use vtysh to inspect the FRR daemons
frrvty = {
members = [ "frr" ];
};
};
environment.etc = {
"frr/frr.conf".source = configFile;
"frr/vtysh.conf".text = ''
service integrated-vtysh-config
'';
"frr/daemons".text = ''
# This file tells the frr package which daemons to start.
#
# The watchfrr, zebra and staticd daemons are always started.
#
# This part is auto-generated from services.frr.<daemon>.enable config
${daemonList}
# If this option is set the /etc/init.d/frr script automatically loads
# the config via "vtysh -b" when the servers are started.
#
vtysh_enable=yes
# This part is auto-generated from services.frr.<daemon>.options or
# services.frr.<daemon>.extraOptions
${daemonOptions}
'';
};
systemd.tmpfiles.rules = [ "d /run/frr 0755 frr frr -" ];
systemd.services.frr = {
description = "FRRouting";
documentation = [ "https://frrouting.readthedocs.io/en/latest/setup.html" ];
wants = [ "network.target" ];
after = [
"network-pre.target"
"systemd-sysctl.service"
];
before = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
startLimitIntervalSec = 180;
reloadIfChanged = true;
restartTriggers = [
configFile
daemonList
];
serviceConfig = {
Nice = -5;
Type = "forking";
NotifyAccess = "all";
TimeoutSec = 120;
WatchdogSec = 60;
RestartSec = 5;
Restart = "always";
LimitNOFILE = cfg.openFilesLimit;
PIDFile = "/run/frr/watchfrr.pid";
ExecStart = "${pkgs.frr}/libexec/frr/frrinit.sh start";
ExecStop = "${pkgs.frr}/libexec/frr/frrinit.sh stop";
ExecReload = "${pkgs.frr}/libexec/frr/frrinit.sh reload";
};
unitConfig = {
StartLimitBurst = "3";
};
};
};
meta.maintainers = with lib.maintainers; [ woffs ];
}

View File

@@ -0,0 +1,92 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.g3proxy;
inherit (lib)
mkPackageOption
mkEnableOption
mkOption
mkIf
literalExpression
;
settingsFormat = pkgs.formats.yaml { };
in
{
options.services.g3proxy = {
enable = mkEnableOption "g3proxy, a generic purpose forward proxy";
package = mkPackageOption pkgs "g3proxy" { };
settings = mkOption {
type = settingsFormat.type;
default = { };
example = literalExpression ''
{
server = [{
name = "test";
escaper = "default";
type = "socks_proxy";
listen = {
address = "[::]:10086";
};
}];
}
'';
description = ''
Settings of g3proxy.
'';
};
};
config = mkIf cfg.enable {
systemd.services.g3proxy = {
description = "g3proxy server";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart =
let
g3proxy-yaml = settingsFormat.generate "g3proxy.yaml" cfg.settings;
in
"${lib.getExe cfg.package} --config-file ${g3proxy-yaml} --systemd --control-dir %t/g3proxy";
WorkingDirectory = "/var/lib/g3proxy";
StateDirectory = "g3proxy";
RuntimeDirectory = "g3proxy";
DynamicUser = true;
RuntimeDirectoryMode = "0755";
PrivateTmp = true;
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = true;
PrivateUsers = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
ProtectSystem = "strict";
ProcSubset = "pid";
RestrictNamespaces = true;
RestrictRealtime = true;
RemoveIPC = true;
SystemCallArchitectures = "native";
UMask = "0077";
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
RestrictSUIDSGID = true;
};
};
};
}

View File

@@ -0,0 +1,31 @@
{
config,
lib,
pkgs,
...
}:
{
#
# interface
#
options = {
services.gdomap = {
enable = lib.mkEnableOption "GNUstep Distributed Objects name server";
};
};
#
# implementation
#
config = lib.mkIf config.services.gdomap.enable {
# NOTE: gdomap runs as root
# TODO: extra user for gdomap?
systemd.services.gdomap = {
description = "gdomap server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
path = [ pkgs.gnustep-base ];
serviceConfig.ExecStart = "${pkgs.gnustep-base}/bin/gdomap -f";
};
};
}

View File

@@ -0,0 +1,247 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
attrValues
concatMap
concatStringsSep
escapeShellArg
literalExpression
mapAttrs'
mkDefault
mkEnableOption
mkPackageOption
mkIf
mkOption
nameValuePair
optional
types
;
mainCfg = config.services.ghostunnel;
module =
{ config, name, ... }:
{
options = {
listen = mkOption {
description = ''
Address and port to listen on (can be HOST:PORT, unix:PATH).
'';
type = types.str;
};
target = mkOption {
description = ''
Address to forward connections to (can be HOST:PORT or unix:PATH).
'';
type = types.str;
};
keystore = mkOption {
description = ''
Path to keystore (combined PEM with cert/key, or PKCS12 keystore).
NB: storepass is not supported because it would expose credentials via `/proc/*/cmdline`.
Specify this or `cert` and `key`.
'';
type = types.nullOr types.str;
default = null;
};
cert = mkOption {
description = ''
Path to certificate (PEM with certificate chain).
Not required if `keystore` is set.
'';
type = types.nullOr types.str;
default = null;
};
key = mkOption {
description = ''
Path to certificate private key (PEM with private key).
Not required if `keystore` is set.
'';
type = types.nullOr types.str;
default = null;
};
cacert = mkOption {
description = ''
Path to CA bundle file (PEM/X509). Uses system trust store if `null`.
'';
type = types.nullOr types.str;
};
disableAuthentication = mkOption {
description = ''
Disable client authentication, no client certificate will be required.
'';
type = types.bool;
default = false;
};
allowAll = mkOption {
description = ''
If true, allow all clients, do not check client cert subject.
'';
type = types.bool;
default = false;
};
allowCN = mkOption {
description = ''
Allow client if common name appears in the list.
'';
type = types.listOf types.str;
default = [ ];
};
allowOU = mkOption {
description = ''
Allow client if organizational unit name appears in the list.
'';
type = types.listOf types.str;
default = [ ];
};
allowDNS = mkOption {
description = ''
Allow client if DNS subject alternative name appears in the list.
'';
type = types.listOf types.str;
default = [ ];
};
allowURI = mkOption {
description = ''
Allow client if URI subject alternative name appears in the list.
'';
type = types.listOf types.str;
default = [ ];
};
extraArguments = mkOption {
description = "Extra arguments to pass to `ghostunnel server`";
type = types.separatedString " ";
default = "";
};
unsafeTarget = mkOption {
description = ''
If set, does not limit target to localhost, 127.0.0.1, [::1], or UNIX sockets.
This is meant to protect against accidental unencrypted traffic on
untrusted networks.
'';
type = types.bool;
default = false;
};
# Definitions to apply at the root of the NixOS configuration.
atRoot = mkOption {
internal = true;
};
};
# Clients should not be authenticated with the public root certificates
# (afaict, it doesn't make sense), so we only provide that default when
# client cert auth is disabled.
config.cacert = mkIf config.disableAuthentication (mkDefault null);
config.atRoot = {
assertions = [
{
message = ''
services.ghostunnel.servers.${name}: At least one access control flag is required.
Set at least one of:
- services.ghostunnel.servers.${name}.disableAuthentication
- services.ghostunnel.servers.${name}.allowAll
- services.ghostunnel.servers.${name}.allowCN
- services.ghostunnel.servers.${name}.allowOU
- services.ghostunnel.servers.${name}.allowDNS
- services.ghostunnel.servers.${name}.allowURI
'';
assertion =
config.disableAuthentication
|| config.allowAll
|| config.allowCN != [ ]
|| config.allowOU != [ ]
|| config.allowDNS != [ ]
|| config.allowURI != [ ];
}
];
systemd.services."ghostunnel-server-${name}" = {
after = [ "network.target" ];
wants = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Restart = "always";
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
DynamicUser = true;
LoadCredential =
optional (config.keystore != null) "keystore:${config.keystore}"
++ optional (config.cert != null) "cert:${config.cert}"
++ optional (config.key != null) "key:${config.key}"
++ optional (config.cacert != null) "cacert:${config.cacert}";
};
script = concatStringsSep " " (
[ "${mainCfg.package}/bin/ghostunnel" ]
++ optional (config.keystore != null) "--keystore=$CREDENTIALS_DIRECTORY/keystore"
++ optional (config.cert != null) "--cert=$CREDENTIALS_DIRECTORY/cert"
++ optional (config.key != null) "--key=$CREDENTIALS_DIRECTORY/key"
++ optional (config.cacert != null) "--cacert=$CREDENTIALS_DIRECTORY/cacert"
++ [
"server"
"--listen ${config.listen}"
"--target ${config.target}"
]
++ optional config.allowAll "--allow-all"
++ map (v: "--allow-cn=${escapeShellArg v}") config.allowCN
++ map (v: "--allow-ou=${escapeShellArg v}") config.allowOU
++ map (v: "--allow-dns=${escapeShellArg v}") config.allowDNS
++ map (v: "--allow-uri=${escapeShellArg v}") config.allowURI
++ optional config.disableAuthentication "--disable-authentication"
++ optional config.unsafeTarget "--unsafe-target"
++ [ config.extraArguments ]
);
};
};
};
in
{
options = {
services.ghostunnel.enable = mkEnableOption "ghostunnel";
services.ghostunnel.package = mkPackageOption pkgs "ghostunnel" { };
services.ghostunnel.servers = mkOption {
description = ''
Server mode ghostunnels (TLS listener -> plain TCP/UNIX target)
'';
type = types.attrsOf (types.submodule module);
default = { };
};
};
config = mkIf mainCfg.enable {
assertions = lib.mkMerge (map (v: v.atRoot.assertions) (attrValues mainCfg.servers));
systemd = lib.mkMerge (map (v: v.atRoot.systemd) (attrValues mainCfg.servers));
};
meta.maintainers = with lib.maintainers; [
roberth
];
}

View File

@@ -0,0 +1,143 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.gitDaemon;
in
{
###### interface
options = {
services.gitDaemon = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Enable Git daemon, which allows public hosting of git repositories
without any access controls. This is mostly intended for read-only access.
You can allow write access by setting daemon.receivepack configuration
item of the repository to true. This is solely meant for a closed LAN setting
where everybody is friendly.
If you need any access controls, use something else.
'';
};
package = lib.mkPackageOption pkgs "git" { };
basePath = lib.mkOption {
type = lib.types.str;
default = "";
example = "/srv/git/";
description = ''
Remap all the path requests as relative to the given path. For example,
if you set base-path to /srv/git, then if you later try to pull
git://example.com/hello.git, Git daemon will interpret the path as /srv/git/hello.git.
'';
};
exportAll = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Publish all directories that look like Git repositories (have the objects
and refs subdirectories), even if they do not have the git-daemon-export-ok file.
If disabled, you need to touch .git/git-daemon-export-ok in each repository
you want the daemon to publish.
Warning: enabling this without a repository whitelist or basePath
publishes every git repository you have.
'';
};
repositories = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"/srv/git"
"/home/user/git/repo2"
];
description = ''
A whitelist of paths of git repositories, or directories containing repositories
all of which would be published. Paths must not end in "/".
Warning: leaving this empty and enabling exportAll publishes all
repositories in your filesystem or basePath if specified.
'';
};
listenAddress = lib.mkOption {
type = lib.types.str;
default = "";
example = "example.com";
description = "Listen on a specific IP address or hostname.";
};
port = lib.mkOption {
type = lib.types.port;
default = 9418;
description = "Port to listen on.";
};
options = lib.mkOption {
type = lib.types.str;
default = "";
description = "Extra configuration options to be passed to Git daemon.";
};
user = lib.mkOption {
type = lib.types.str;
default = "git";
description = "User under which Git daemon would be running.";
};
group = lib.mkOption {
type = lib.types.str;
default = "git";
description = "Group under which Git daemon would be running.";
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
users.users = lib.optionalAttrs (cfg.user == "git") {
git = {
uid = config.ids.uids.git;
group = "git";
description = "Git daemon user";
};
};
users.groups = lib.optionalAttrs (cfg.group == "git") {
git.gid = config.ids.gids.git;
};
systemd.services.git-daemon = {
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
script =
"${lib.getExe cfg.package} daemon --reuseaddr "
+ (lib.optionalString (cfg.basePath != "") "--base-path=${cfg.basePath} ")
+ (lib.optionalString (cfg.listenAddress != "") "--listen=${cfg.listenAddress} ")
+ "--port=${toString cfg.port} --user=${cfg.user} --group=${cfg.group} ${cfg.options} "
+ "--verbose "
+ (lib.optionalString cfg.exportAll "--export-all ")
+ lib.concatStringsSep " " cfg.repositories;
};
};
}

Some files were not shown because too many files have changed in this diff Show More