Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixos/iwd: add networks and interfaces option #75800

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions lib/types.nix
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,17 @@ rec {
merge = mergeEqualOption;
};

fixedLengthString = n: lengthCheckedString n n;

lengthCheckedString = min: max: mkOptionType {
name = "str-with-min-${toString min}-and-max-${toString max}-chars";
description = if min == max
then "string with exactly ${toString min} chars."
else "string which must have at least ${toString min} and at most ${toString max} chars.";
inherit (types.str) merge;
check = x: isString x && stringLength x >= min && stringLength x <= max;
};

str = mkOptionType {
name = "str";
description = "string";
Expand Down
147 changes: 144 additions & 3 deletions nixos/modules/services/networking/iwd.nix
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,132 @@ with lib;

let
cfg = config.networking.wireless.iwd;

encodeFileName = pkgs.writeScript "encode" ''
#! ${pkgs.runtimeShell} -e
echo "=$(printf "$1" | ${pkgs.unixtools.xxd}/bin/xxd -pu)"
'';

# If a network consists of alphanumeric chars and `_` and `-`, the network will be stored
# as `/var/lib/iwd/<ssid>.<ext>`. In any other case a hex-encoded version of the SSID will
# be stored with an `=` as prefix.
encodeIfNeeded = file: ext: let
allowedChars = stringToCharacters "01234556789abcdefghijklmnopqrstuvwxyz-_ ";
skipEncoding = all (char: elem (toLower char) allowedChars) (stringToCharacters file);
in
"/var/lib/iwd/${if skipEncoding then file else "$(${encodeFileName} \"${file}\")"}.${ext}";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since nothing actually needs these values at eval time, you can instead do this check during runtime in preStart directly.


# The file extension of the network-config determines the type of the network. Currently,
# we support public networks (`open`) and networks protected with a passphrase (`psk`).
# Anything else, like 802.1x networks are not supported by this module.
determineType = cfg: if cfg.psk != null || cfg.passphrase != null then "psk" else "open";

# Generates a name-value-pair with configurations for a SSID. For further information
# about the configuration options, please refer to the upstream docs:
#
# https://iwd.wiki.kernel.org/networkconfigurationsettings
predefinedNetworks = flip mapAttrs cfg.networks (name: cfg: {
extension = determineType cfg;

# Cannot use the SSID here: the hex-encode is done at build-time, not eval time
# and several special chars in a file name would break `writeText`.
file = pkgs.writeText "ssid" (generators.toINI {} ({
Settings.AutoConnect = boolToString cfg.autoConnect;
Settings.Hidden = boolToString cfg.hidden;
} // (optionalAttrs (cfg.psk != null) {
Security.PreSharedKey = cfg.psk;
}) // (optionalAttrs (cfg.passphrase != null) {
Security.Passprase = cfg.passphrase;
})));
});
in {
options.networking.wireless.iwd.enable = mkEnableOption "iwd";
options.networking.wireless.iwd = {
enable = mkEnableOption "iwd";

interfaces = mkOption {
type = types.nullOr (types.listOf types.str);
example = literalExample ''
[ "wlp2s0" ]
'';
default = null;
Ma27 marked this conversation as resolved.
Show resolved Hide resolved
description = ''
List of interfaces to use.
'';
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the interfaces change be done separately?
It's not needed for the network changes at all AFAICT so it's easily separated.
The reason is iwd and interface handling, especially naming, might need some careful work.

  • Interface strings: interaction and reliance on use of usePredictableInterfaceNames
  • Implications re:iwd's UseDefaultInterface (and the shipped .link file).
  • (Is there some existing interface abstraction we should be tying to?)
  • The service, I think?, if used with particular interfaces maybe should: wants, requires sys-subsystem-net-devices-${ifacename-escaped-for-systemd} (not sure)
  • iwd's help doesn't tell you, but my reading of the code in src/manager.c suggests the interface list is comma-separated not space-separated, FWIW

All said, what's the use for specifying interfaces explicitly anyway? Just curious :).

And the rest is exciting and I think close to merge-ready, hence the suggestion for splitting off the interface-related bits.
Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All said, what's the use for specifying interfaces explicitly anyway? Just curious :).

It might be possible that I'm missing something, but iwd constantly tried to use wlan0 by default which doesn't even exist on my system.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dtzWill any updates here?


networks = mkOption {
default = {};
example = literalExample ''
{ "karlsruhe.freifunk.net" = {};
"secured".passphrase = "12345678";
"network-with-psk".psk = "bf4c086e055e492373a0f59ff614f61fc2d597a56db94e17a165f9ef1d42c066";
};
'';

description = ''
Declarative configuration of wifi networks for
<citerefentry><refentrytitle>iwd</refentrytitle><manvolnum>8</manvolnum></citerefentry>.

All networks will be stored in
<literal>/var/lib/iwd/&lt;name&gt;.&lt;type&gt;</literal>.

Since each network is stored in its own file, declarative networks can be used in an
environment with imperatively added networks via
<citerefentry><refentrytitle>iwctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>.
'';

type = types.attrsOf (types.submodule {
options = {
psk = mkOption {
type = types.nullOr (types.fixedLengthString 64);
default = null;
description = ''
WPA PSK for the currently defined network. This can be generated by using
<citerefentry><refentrytitle>wpa_passphrase</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
'';
};

passphrase = mkOption {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And manually constructing the INI file during runtime has the benefit that you can convert the psk and passphrase options to pskFile and passphraseFile respectively, which then won't force the user into putting their password in the Nix store. When constructing the INI file in preStart you can read these file paths to get the secret values.

Note that if people don't care about this, they can still do psk = pkgs.writeFile "foo-psk" "the-psk", which still puts the secret in the store.

Furthermore, 802.1x support can be added similarly, as requested by @onny.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relevant issue: #24288

type = types.nullOr (types.lengthCheckedString 8 63);
default = null;
description = ''
Raw passphrase for the currently defined network.
'';
};

autoConnect = mkOption {
default = true;
type = types.bool;
description = ''
Disable automatic connection to the network.
'';
};

hidden = mkOption {
default = false;
type = types.bool;
description = ''
Used for hidden networks that don't respond to scans unless
their SSID is explicitly specified.
'';
};
};
});
};
};

config = mkIf cfg.enable {
assertions = [{
assertion = !config.networking.wireless.enable;
message = ''
Only one wireless daemon is allowed at the time: networking.wireless.enable and networking.wireless.iwd.enable are mutually exclusive.
'';
}];
}] ++ (flip mapAttrsToList cfg.networks (name: value: {
assertion = value.psk != null -> value.passphrase == null;
message = ''
Cannot set WPA PSK and raw passphrase at the same time for network ${name}!
'';
}));

# for iwctl
environment.systemPackages = [ pkgs.iwd ];
Expand All @@ -22,8 +138,33 @@ in {

systemd.packages = [ pkgs.iwd ];

systemd.services.iwd.preStart = mkIf (cfg.networks != {}) (let
ssids = flip mapAttrsToList predefinedNetworks (file: content: {
inherit (content) file;
local = encodeIfNeeded file content.extension;
});
in ''
# Remove all network-configs from `/var/lib/iwd` that are symlinks to a store-path,
# but aren't declared in `cfg.networks` (i.e. all networks that were "removed" from
# `cfg.networks`).
find /var/lib/iwd -type l -lname '${builtins.storeDir}/*' ${optionalString (ssids != {}) ''
-not \( ${concatMapStringsSep " -o " ({ local, ... }:
"-name '${baseNameOf local}*'")
ssids} \) \
''} -delete

${concatMapStrings ({ file, local }: ''
ln -sf '${file}' "${local}"
'') ssids}
'');

systemd.services.iwd.serviceConfig.ExecStart = mkIf (cfg.interfaces != null) [
""
"${pkgs.iwd}/libexec/iwd -i ${concatStringsSep " -i " cfg.interfaces}"
];

systemd.services.iwd.wantedBy = [ "multi-user.target" ];
};

meta.maintainers = with lib.maintainers; [ mic92 dtzWill ];
meta.maintainers = with lib.maintainers; [ mic92 dtzWill ma27 ];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
From bd0a222cca9973d7662a92c794ae04b40c29d914 Mon Sep 17 00:00:00 2001
From: Maximilian Bosch <maximilian@mbosch.me>
Date: Thu, 30 Jul 2020 00:43:20 +0200
Subject: [PATCH] agent/network_connect_psk: check if the config-file for the
SSID points to a store path

If the network is declaratively configured by NixOS, it must not be
modified imperatively.
---
src/dbus.c | 6 ++++++
src/dbus.h | 1 +
src/network.c | 14 +++++++++++++-
3 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/src/dbus.c b/src/dbus.c
index ceede5c..f8b8109 100644
--- a/src/dbus.c
+++ b/src/dbus.c
@@ -114,6 +114,12 @@ struct l_dbus_message *dbus_error_not_supported(struct l_dbus_message *msg)
"Operation not supported");
}

+struct l_dbus_message *dbus_error_file_is_a_nix_store_path(struct l_dbus_message *msg, char *ssid)
+{
+ return l_dbus_message_new_error(msg, IWD_SERVICE ".NotSupportNixOS",
+ "Cannot update SSID %s as it's configured in a file that points to a store path! Please update your NixOS config accordingly!", ssid);
+}
Ma27 marked this conversation as resolved.
Show resolved Hide resolved
+
struct l_dbus_message *dbus_error_no_agent(struct l_dbus_message *msg)
{
return l_dbus_message_new_error(msg, IWD_SERVICE ".NoAgent",
diff --git a/src/dbus.h b/src/dbus.h
index ebae85d..adb0241 100644
--- a/src/dbus.h
+++ b/src/dbus.h
@@ -59,6 +59,7 @@ struct l_dbus_message *dbus_error_invalid_format(struct l_dbus_message *msg);
struct l_dbus_message *dbus_error_already_exists(struct l_dbus_message *msg);
struct l_dbus_message *dbus_error_not_found(struct l_dbus_message *msg);
struct l_dbus_message *dbus_error_not_supported(struct l_dbus_message *msg);
+struct l_dbus_message *dbus_error_file_is_a_nix_store_path(struct l_dbus_message *msg, char *ssid);
struct l_dbus_message *dbus_error_no_agent(struct l_dbus_message *msg);
struct l_dbus_message *dbus_error_not_connected(struct l_dbus_message *msg);
struct l_dbus_message *dbus_error_not_configured(struct l_dbus_message *msg);
diff --git a/src/network.c b/src/network.c
index 70541b5..d510f5e 100644
--- a/src/network.c
+++ b/src/network.c
@@ -851,8 +851,20 @@ static struct l_dbus_message *network_connect_psk(struct network *network,

l_debug("ask_passphrase: %s",
network->ask_passphrase ? "true" : "false");
-
if (network->ask_passphrase) {
+ char store_dir[] = "@storeDir@";
+ char full_path[PATH_MAX];
+ char *cfgpath;
+ if (network->info != NULL) {
+ cfgpath = network->info->ops->get_file_path(network->info);
+ int ret = readlink(cfgpath, full_path, sizeof(full_path));
+ l_free(cfgpath);
+
+ if (ret != -1 && strncmp(full_path, store_dir, strlen(store_dir)) == 0) {
+ return dbus_error_file_is_a_nix_store_path(message, network->ssid);
+ }
+ }
+
network->ask_passphrase = false;

network->agent_request =
--
2.25.4

12 changes: 10 additions & 2 deletions pkgs/os-specific/linux/iwd/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
, readline
, openssl
, python3Packages
, substituteAll
}:

stdenv.mkDerivation rec {
Expand All @@ -23,6 +24,13 @@ stdenv.mkDerivation rec {

outputs = [ "out" "man" ];

patches = [
(substituteAll {
src = ./0001-agent-network_connect_psk-check-if-the-config-file-f.patch;
inherit (builtins) storeDir;
})
];

nativeBuildInputs = [
autoreconfHook
docutils
Expand Down Expand Up @@ -54,7 +62,7 @@ stdenv.mkDerivation rec {
"--with-systemd-networkdir=${placeholder "out"}/lib/systemd/network/"
];

postUnpack = ''
postPatch = ''
patchShebangs .
'';

Expand Down Expand Up @@ -85,6 +93,6 @@ stdenv.mkDerivation rec {
description = "Wireless daemon for Linux";
license = licenses.lgpl21;
platforms = platforms.linux;
maintainers = with maintainers; [ dtzWill fpletz ];
maintainers = with maintainers; [ dtzWill fpletz ma27 ];
};
}