mirror of
https://github.com/oddlama/nix-config.git
synced 2025-10-11 07:10:39 +02:00
feat: modulize esphome
This commit is contained in:
parent
8545dff4e7
commit
5d8c1c902d
4 changed files with 241 additions and 116 deletions
153
modules/esphome.nix
Normal file
153
modules/esphome.nix
Normal file
|
@ -0,0 +1,153 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}: let
|
||||
inherit
|
||||
(lib)
|
||||
literalExpression
|
||||
mkEnableOption
|
||||
mkIf
|
||||
mkOption
|
||||
mdDoc
|
||||
types
|
||||
;
|
||||
|
||||
cfg = config.services.esphome;
|
||||
|
||||
name = "esphome";
|
||||
|
||||
stateDir = "/var/lib/${name}";
|
||||
in {
|
||||
options.services.esphome = {
|
||||
enable = mkEnableOption (mdDoc "esphome");
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.esphome;
|
||||
defaultText = literalExpression "pkgs.esphome";
|
||||
description = mdDoc "The package to use for the esphome command.";
|
||||
};
|
||||
|
||||
enableUnixSocket = mkEnableOption (lib.mdDoc ''
|
||||
Expose a unix socket under /run/esphome/esphome.sock instead of using a TCP socket.
|
||||
'');
|
||||
|
||||
address = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = mdDoc "esphome address";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 6052;
|
||||
description = mdDoc "esphome port";
|
||||
};
|
||||
|
||||
openFirewall = mkOption {
|
||||
default = false;
|
||||
type = types.bool;
|
||||
description = mdDoc "Whether to open the firewall for the specified port.";
|
||||
};
|
||||
|
||||
allowedDevices = mkOption {
|
||||
default = [];
|
||||
example = [
|
||||
{
|
||||
node = "/dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_0001-if00-port0";
|
||||
modifier = "rw";
|
||||
}
|
||||
];
|
||||
description = lib.mdDoc ''
|
||||
A list of device nodes to which {command}`esphome` has access to.
|
||||
Beware that permissions are not added dynamically when a device
|
||||
is plugged in while the service is already running.
|
||||
'';
|
||||
type = types.listOf (types.submodule {
|
||||
options = {
|
||||
node = mkOption {
|
||||
example = "/dev/ttyUSB*";
|
||||
type = types.str;
|
||||
description = lib.mdDoc "Path to device node";
|
||||
};
|
||||
modifier = mkOption {
|
||||
example = "rw";
|
||||
type = types.str;
|
||||
description = lib.mdDoc ''
|
||||
Device node access modifier. Takes a combination
|
||||
`r` (read), `w` (write), and `m` (mknod). See the
|
||||
`systemd.resource-control(5)` man page for more
|
||||
information.
|
||||
'';
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
networking.firewall.allowedTCPPorts = mkIf (cfg.openFirewall && !cfg.enableUnixSocket) [cfg.port];
|
||||
|
||||
systemd.services.esphome = {
|
||||
description = "ESPHome dashboard";
|
||||
after = ["network.target"];
|
||||
wantedBy = ["multi-user.target"];
|
||||
path = [cfg.package];
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = let
|
||||
extraParams =
|
||||
if cfg.enableUnixSocket
|
||||
then "--socket /run/${name}/esphome.sock"
|
||||
else "--address ${cfg.address} --port ${toString cfg.port}";
|
||||
in "${cfg.package}/bin/esphome dashboard ${extraParams} ${stateDir}";
|
||||
DynamicUser = true;
|
||||
WorkingDirectory = stateDir;
|
||||
StateDirectory = name;
|
||||
StateDirectoryMode = "0750";
|
||||
Restart = "on-failure";
|
||||
RuntimeDirectory = mkIf cfg.enableUnixSocket name;
|
||||
RuntimeDirectoryMode = "0750";
|
||||
|
||||
# Hardening
|
||||
CapabilityBoundingSet = "";
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
DevicePolicy = "closed";
|
||||
DeviceAllow = map (d: "${d.node} ${d.modifier}") cfg.allowedDevices;
|
||||
SupplementaryGroups = ["dialout"];
|
||||
NoNewPrivileges = true;
|
||||
PrivateUsers = true;
|
||||
PrivateTmp = true;
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
ProcSubset = "pid";
|
||||
ProtectSystem = "strict";
|
||||
RemoveIPC = true;
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_NETLINK"
|
||||
"AF_UNIX"
|
||||
];
|
||||
RestrictNamespaces = false; # Required by platformio for chroot
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = [
|
||||
"@system-service"
|
||||
"@mount" # Required by platformio for chroot
|
||||
];
|
||||
UMask = "0077";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
|
@ -7,21 +7,22 @@
|
|||
}: let
|
||||
inherit
|
||||
(lib)
|
||||
any
|
||||
attrNames
|
||||
attrValues
|
||||
concatLists
|
||||
concatMap
|
||||
concatMapStrings
|
||||
concatStringsSep
|
||||
count
|
||||
escapeShellArg
|
||||
filter
|
||||
getAttr
|
||||
literalExpression
|
||||
mapAttrsToList
|
||||
mdDoc
|
||||
mkIf
|
||||
mkOption
|
||||
optional
|
||||
optionals
|
||||
optionalString
|
||||
stringLength
|
||||
toLower
|
||||
|
@ -30,35 +31,6 @@
|
|||
|
||||
cfg = config.services.hostapd;
|
||||
|
||||
# Maps the specified acl mode to values understood by hostapd
|
||||
macaddrAclModes = {
|
||||
"allow" = "0";
|
||||
"deny" = "1";
|
||||
"radius" = "2";
|
||||
};
|
||||
|
||||
# Maps the specified ignore broadcast ssid mode to values understood by hostapd
|
||||
ignoreBroadcastSsidModes = {
|
||||
"disabled" = "0";
|
||||
"empty" = "1";
|
||||
"clear" = "2";
|
||||
};
|
||||
|
||||
# Maps the specified vht and he channel widths to values understood by hostapd
|
||||
operatingChannelWidth = {
|
||||
"20or40" = "0";
|
||||
"80" = "1";
|
||||
"160" = "2";
|
||||
"80+80" = "3";
|
||||
};
|
||||
|
||||
# Maps the specified vht and he channel widths to values understood by hostapd
|
||||
managementFrameProtection = {
|
||||
"disabled" = "0";
|
||||
"optional" = "1";
|
||||
"required" = "2";
|
||||
};
|
||||
|
||||
bool01 = b:
|
||||
if b
|
||||
then "1"
|
||||
|
@ -91,7 +63,7 @@
|
|||
channel=${toString ifcfg.channel}
|
||||
noscan=${bool01 ifcfg.noScan}
|
||||
# Set the MAC-address access control mode
|
||||
macaddr_acl=${macaddrAclModes.${ifcfg.macAcl}}
|
||||
macaddr_acl=${ifcfg.macAcl}
|
||||
${optionalString (ifcfg.macAllow != [] || ifcfg.macAllowFile != null || ifcfg.authentication.saeAddToMacAllow) ''
|
||||
accept_mac_file=/run/hostapd/${interface}.mac.allow
|
||||
''}
|
||||
|
@ -100,7 +72,7 @@
|
|||
''}
|
||||
# Only allow WPA, disable insecure WEP
|
||||
auth_algs=1
|
||||
ignore_broadcast_ssid=${ignoreBroadcastSsidModes.${ifcfg.ignoreBroadcastSsid}}
|
||||
ignore_broadcast_ssid=${ifcfg.ignoreBroadcastSsid}
|
||||
# Always enable QoS, which is required for 802.11n and above
|
||||
wmm_enabled=1
|
||||
ap_isolate=${bool01 ifcfg.apIsolate}
|
||||
|
@ -119,14 +91,14 @@
|
|||
ieee80211ac=1
|
||||
vht_capab=${concatMapStrings (x: "[${x}]") ifcfg.wifi5.capabilities}
|
||||
require_vht=${bool01 ifcfg.wifi5.require}
|
||||
vht_oper_chwidth=${operatingChannelWidth.${ifcfg.wifi5.operatingChannelWidth}}
|
||||
vht_oper_chwidth=${ifcfg.wifi5.operatingChannelWidth}
|
||||
''}
|
||||
${optionalString ifcfg.wifi6.enable ''
|
||||
##### IEEE 802.11ax (WiFi 6) related configuration #####################################
|
||||
|
||||
ieee80211ax=1
|
||||
require_he=${bool01 ifcfg.wifi6.require}
|
||||
he_oper_chwidth=${operatingChannelWidth.${ifcfg.wifi6.operatingChannelWidth}}
|
||||
he_oper_chwidth=${ifcfg.wifi6.operatingChannelWidth}
|
||||
he_su_beamformer=${bool01 ifcfg.wifi6.singleUserBeamformer}
|
||||
he_su_beamformee=${bool01 ifcfg.wifi6.singleUserBeamformee}
|
||||
he_mu_beamformer=${bool01 ifcfg.wifi6.multiUserBeamformer}
|
||||
|
@ -135,7 +107,7 @@
|
|||
##### IEEE 802.11be (WiFi 7) related configuration #####################################
|
||||
|
||||
ieee80211be=1
|
||||
eht_oper_chwidth=${operatingChannelWidth.${ifcfg.wifi7.operatingChannelWidth}}
|
||||
eht_oper_chwidth=${ifcfg.wifi7.operatingChannelWidth}
|
||||
eht_su_beamformer=${bool01 ifcfg.wifi7.singleUserBeamformer}
|
||||
eht_su_beamformee=${bool01 ifcfg.wifi7.singleUserBeamformee}
|
||||
eht_mu_beamformer=${bool01 ifcfg.wifi7.multiUserBeamformer}
|
||||
|
@ -144,7 +116,7 @@
|
|||
##### WPA/IEEE 802.11i configuration ##########################################
|
||||
|
||||
# Encrypt management frames to protect against deauthentication and similar attacks
|
||||
ieee80211w=${managementFrameProtection.${ifcfg.managementFrameProtection}}
|
||||
ieee80211w=${ifcfg.managementFrameProtection}
|
||||
${optionalString (ifcfg.authentication.mode == "none") ''
|
||||
wpa=0
|
||||
''}
|
||||
|
@ -424,6 +396,12 @@ in {
|
|||
macAcl = mkOption {
|
||||
default = "allow";
|
||||
type = types.enum ["allow" "deny" "radius"];
|
||||
apply = x:
|
||||
getAttr x {
|
||||
"allow" = "0";
|
||||
"deny" = "1";
|
||||
"radius" = "2";
|
||||
};
|
||||
description = mdDoc ''
|
||||
Station MAC address -based authentication. The following modes are available:
|
||||
|
||||
|
@ -484,6 +462,12 @@ in {
|
|||
ignoreBroadcastSsid = mkOption {
|
||||
default = "disabled";
|
||||
type = types.enum ["disabled" "empty" "clear"];
|
||||
apply = x:
|
||||
getAttr x {
|
||||
"disabled" = "0";
|
||||
"empty" = "1";
|
||||
"clear" = "2";
|
||||
};
|
||||
description = mdDoc ''
|
||||
Send empty SSID in beacons and ignore probe request frames that do not
|
||||
specify full SSID, i.e., require stations to know SSID. Note that this does
|
||||
|
@ -717,6 +701,12 @@ in {
|
|||
managementFrameProtection = mkOption {
|
||||
default = "required";
|
||||
type = types.enum ["disabled" "optional" "required"];
|
||||
apply = x:
|
||||
getAttr x {
|
||||
"disabled" = "0";
|
||||
"optional" = "1";
|
||||
"required" = "2";
|
||||
};
|
||||
description = mdDoc ''
|
||||
Management frame protection (MFP) authenticates management frames
|
||||
to prevent deauthentication (or related) attacks.
|
||||
|
@ -789,6 +779,13 @@ in {
|
|||
operatingChannelWidth = mkOption {
|
||||
default = "20or40";
|
||||
type = types.enum ["20or40" "80" "160" "80+80"];
|
||||
apply = x:
|
||||
getAttr x {
|
||||
"20or40" = "0";
|
||||
"80" = "1";
|
||||
"160" = "2";
|
||||
"80+80" = "3";
|
||||
};
|
||||
description = mdDoc ''
|
||||
Determines the operating channel width for VHT.
|
||||
|
||||
|
@ -837,6 +834,13 @@ in {
|
|||
operatingChannelWidth = mkOption {
|
||||
default = "20or40";
|
||||
type = types.enum ["20or40" "80" "160" "80+80"];
|
||||
apply = x:
|
||||
getAttr x {
|
||||
"20or40" = "0";
|
||||
"80" = "1";
|
||||
"160" = "2";
|
||||
"80+80" = "3";
|
||||
};
|
||||
description = mdDoc ''
|
||||
Determines the operating channel width for HE.
|
||||
|
||||
|
@ -882,6 +886,13 @@ in {
|
|||
operatingChannelWidth = mkOption {
|
||||
default = "20or40";
|
||||
type = types.enum ["20or40" "80" "160" "80+80"];
|
||||
apply = x:
|
||||
getAttr x {
|
||||
"20or40" = "0";
|
||||
"80" = "1";
|
||||
"160" = "2";
|
||||
"80+80" = "3";
|
||||
};
|
||||
description = mdDoc ''
|
||||
Determines the operating channel width for EHT.
|
||||
|
||||
|
@ -908,14 +919,18 @@ in {
|
|||
]
|
||||
# Interface warnings
|
||||
++ (concatLists (mapAttrsToList (interface: ifcfg: let
|
||||
countWpaPasswordDefinitions = count (x: x != null) [ifcfg.authentication.wpaPassword ifcfg.authentication.wpaPasswordFile ifcfg.authentication.wpaPskFile];
|
||||
countWpaPasswordDefinitions = count (x: x != null) [
|
||||
ifcfg.authentication.wpaPassword
|
||||
ifcfg.authentication.wpaPasswordFile
|
||||
ifcfg.authentication.wpaPskFile
|
||||
];
|
||||
in [
|
||||
{
|
||||
assertion = ifcfg.authentication.mode == "wpa3-sae" -> ifcfg.managementFrameProtection == "required";
|
||||
assertion = ifcfg.authentication.mode == "wpa3-sae" -> ifcfg.managementFrameProtection == "2";
|
||||
message = ''hostapd interface ${interface} uses WPA3-SAE which requires managementFrameProtection="required"'';
|
||||
}
|
||||
{
|
||||
assertion = ifcfg.authentication.mode == "wpa3-sae-transition" -> ifcfg.managementFrameProtection != "disabled";
|
||||
assertion = ifcfg.authentication.mode == "wpa3-sae-transition" -> ifcfg.managementFrameProtection != "0";
|
||||
message = ''hostapd interface ${interface} uses WPA3-SAE in transition mode with WPA2-SHA256, which requires managementFrameProtection="optional" or ="required"'';
|
||||
}
|
||||
{
|
||||
|
@ -947,15 +962,14 @@ in {
|
|||
|
||||
environment.systemPackages = [pkgs.hostapd];
|
||||
|
||||
services.udev.packages = optionals (any (i: i.countryCode != null) (attrValues cfg.interfaces)) [pkgs.crda];
|
||||
services.udev.packages = with pkgs; [crda];
|
||||
|
||||
systemd.services.hostapd = {
|
||||
description = "Hostapd IEEE 802.11 AP Daemon";
|
||||
description = "IEEE 802.11 Host Access-Point Daemon";
|
||||
|
||||
path = [pkgs.hostapd];
|
||||
after = mapAttrsToList (interface: _: "sys-subsystem-net-devices-${utils.escapeSystemdPath interface}.device") cfg.interfaces;
|
||||
bindsTo = mapAttrsToList (interface: _: "sys-subsystem-net-devices-${utils.escapeSystemdPath interface}.device") cfg.interfaces;
|
||||
requiredBy = mapAttrsToList (interface: _: "network-link-${interface}.service") cfg.interfaces;
|
||||
after = map (interface: "sys-subsystem-net-devices-${utils.escapeSystemdPath interface}.device") (attrNames cfg.interfaces);
|
||||
bindsTo = map (interface: "sys-subsystem-net-devices-${utils.escapeSystemdPath interface}.device") (attrNames cfg.interfaces);
|
||||
wantedBy = ["multi-user.target"];
|
||||
|
||||
# Create merged configuration and acl files for each interface prior to starting
|
||||
|
@ -973,7 +987,7 @@ in {
|
|||
DevicePolicy = "closed";
|
||||
DeviceAllow = "/dev/rfkill rw";
|
||||
NoNewPrivileges = true;
|
||||
PrivateUsers = false; # hostapd requires real system root access.
|
||||
PrivateUsers = false; # hostapd requires true root access.
|
||||
PrivateTmp = true;
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
|
@ -985,12 +999,21 @@ in {
|
|||
ProtectProc = "invisible";
|
||||
ProcSubset = "pid";
|
||||
ProtectSystem = "strict";
|
||||
RestrictAddressFamilies = ["AF_UNIX" "AF_NETLINK" "AF_INET" "AF_INET6"];
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_NETLINK"
|
||||
"AF_UNIX"
|
||||
];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = ["@system-service" "~@privileged" "@chown"];
|
||||
SystemCallFilter = [
|
||||
"@system-service"
|
||||
"~@privileged"
|
||||
"@chown"
|
||||
];
|
||||
UMask = "0077";
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue