1{ config, lib, utils, pkgs, ... }:
2
3with lib;
4
5let
6
7 cfg = config.networking.supplicant;
8
9 # We must escape interfaces due to the systemd interpretation
10 subsystemDevice = interface:
11 "sys-subsystem-net-devices-${utils.escapeSystemdPath interface}.device";
12
13 serviceName = iface: "supplicant-${if (iface=="WLAN") then "wlan@" else (
14 if (iface=="LAN") then "lan@" else (
15 if (iface=="DBUS") then "dbus"
16 else (replaceStrings [" "] ["-"] iface)))}";
17
18 # TODO: Use proper privilege separation for wpa_supplicant
19 supplicantService = iface: suppl:
20 let
21 deps = (if (iface=="WLAN"||iface=="LAN") then ["sys-subsystem-net-devices-%i.device"] else (
22 if (iface=="DBUS") then ["dbus.service"]
23 else (map subsystemDevice (splitString " " iface))))
24 ++ optional (suppl.bridge!="") (subsystemDevice suppl.bridge);
25
26 ifaceArg = concatStringsSep " -N " (map (i: "-i${i}") (splitString " " iface));
27 driverArg = optionalString (suppl.driver != null) "-D${suppl.driver}";
28 bridgeArg = optionalString (suppl.bridge!="") "-b${suppl.bridge}";
29 confFileArg = optionalString (suppl.configFile.path!=null) "-c${suppl.configFile.path}";
30 extraConfFile = pkgs.writeText "supplicant-extra-conf-${replaceStrings [" "] ["-"] iface}" ''
31 ${optionalString suppl.userControlled.enable "ctrl_interface=DIR=${suppl.userControlled.socketDir} GROUP=${suppl.userControlled.group}"}
32 ${optionalString suppl.configFile.writable "update_config=1"}
33 ${suppl.extraConf}
34 '';
35 in
36 { description = "Supplicant ${iface}${optionalString (iface=="WLAN"||iface=="LAN") " %I"}";
37 wantedBy = [ "multi-user.target" ] ++ deps;
38 wants = [ "network.target" ];
39 bindsTo = deps;
40 after = deps;
41 before = [ "network.target" ];
42
43 path = [ pkgs.coreutils ];
44
45 preStart = ''
46 ${optionalString (suppl.configFile.path!=null && suppl.configFile.writable) ''
47 (umask 077 && touch -a "${suppl.configFile.path}")
48 ''}
49 ${optionalString suppl.userControlled.enable ''
50 install -dm770 -g "${suppl.userControlled.group}" "${suppl.userControlled.socketDir}"
51 ''}
52 '';
53
54 serviceConfig.ExecStart = "${pkgs.wpa_supplicant}/bin/wpa_supplicant -s ${driverArg} ${confFileArg} -I${extraConfFile} ${bridgeArg} ${suppl.extraCmdArgs} ${if (iface=="WLAN"||iface=="LAN") then "-i%I" else (if (iface=="DBUS") then "-u" else ifaceArg)}";
55
56 };
57
58
59in
60
61{
62
63 ###### interface
64
65 options = {
66
67 networking.supplicant = mkOption {
68 type = with types; attrsOf (submodule {
69 options = {
70
71 configFile = {
72
73 path = mkOption {
74 type = types.nullOr types.path;
75 default = null;
76 example = literalExpression "/etc/wpa_supplicant.conf";
77 description = lib.mdDoc ''
78 External `wpa_supplicant.conf` configuration file.
79 The configuration options defined declaratively within `networking.supplicant` have
80 precedence over options defined in `configFile`.
81 '';
82 };
83
84 writable = mkOption {
85 type = types.bool;
86 default = false;
87 description = lib.mdDoc ''
88 Whether the configuration file at `configFile.path` should be written to by
89 `wpa_supplicant`.
90 '';
91 };
92
93 };
94
95 extraConf = mkOption {
96 type = types.lines;
97 default = "";
98 example = ''
99 ap_scan=1
100 device_name=My-NixOS-Device
101 device_type=1-0050F204-1
102 driver_param=use_p2p_group_interface=1
103 disable_scan_offload=1
104 p2p_listen_reg_class=81
105 p2p_listen_channel=1
106 p2p_oper_reg_class=81
107 p2p_oper_channel=1
108 manufacturer=NixOS
109 model_name=NixOS_Unstable
110 model_number=2015
111 '';
112 description = lib.mdDoc ''
113 Configuration options for `wpa_supplicant.conf`.
114 Options defined here have precedence over options in `configFile`.
115 NOTE: Do not write sensitive data into `extraConf` as it will
116 be world-readable in the `nix-store`. For sensitive information
117 use the `configFile` instead.
118 '';
119 };
120
121 extraCmdArgs = mkOption {
122 type = types.str;
123 default = "";
124 example = "-e/run/wpa_supplicant/entropy.bin";
125 description =
126 lib.mdDoc "Command line arguments to add when executing `wpa_supplicant`.";
127 };
128
129 driver = mkOption {
130 type = types.nullOr types.str;
131 default = "nl80211,wext";
132 description = lib.mdDoc "Force a specific wpa_supplicant driver.";
133 };
134
135 bridge = mkOption {
136 type = types.str;
137 default = "";
138 description = lib.mdDoc "Name of the bridge interface that wpa_supplicant should listen at.";
139 };
140
141 userControlled = {
142
143 enable = mkOption {
144 type = types.bool;
145 default = false;
146 description = lib.mdDoc ''
147 Allow normal users to control wpa_supplicant through wpa_gui or wpa_cli.
148 This is useful for laptop users that switch networks a lot and don't want
149 to depend on a large package such as NetworkManager just to pick nearby
150 access points.
151 '';
152 };
153
154 socketDir = mkOption {
155 type = types.str;
156 default = "/run/wpa_supplicant";
157 description = lib.mdDoc "Directory of sockets for controlling wpa_supplicant.";
158 };
159
160 group = mkOption {
161 type = types.str;
162 default = "wheel";
163 example = "network";
164 description = lib.mdDoc "Members of this group can control wpa_supplicant.";
165 };
166
167 };
168 };
169 });
170
171 default = { };
172
173 example = literalExpression ''
174 { "wlan0 wlan1" = {
175 configFile.path = "/etc/wpa_supplicant.conf";
176 userControlled.group = "network";
177 extraConf = '''
178 ap_scan=1
179 p2p_disabled=1
180 ''';
181 extraCmdArgs = "-u -W";
182 bridge = "br0";
183 };
184 }
185 '';
186
187 description = lib.mdDoc ''
188 Interfaces for which to start {command}`wpa_supplicant`.
189 The supplicant is used to scan for and associate with wireless networks,
190 or to authenticate with 802.1x capable network switches.
191
192 The value of this option is an attribute set. Each attribute configures a
193 {command}`wpa_supplicant` service, where the attribute name specifies
194 the name of the interface that {command}`wpa_supplicant` operates on.
195 The attribute name can be a space separated list of interfaces.
196 The attribute names `WLAN`, `LAN` and `DBUS`
197 have a special meaning. `WLAN` and `LAN` are
198 configurations for universal {command}`wpa_supplicant` service that is
199 started for each WLAN interface or for each LAN interface, respectively.
200 `DBUS` defines a device-unrelated {command}`wpa_supplicant`
201 service that can be accessed through `D-Bus`.
202 '';
203
204 };
205
206 };
207
208
209 ###### implementation
210
211 config = mkIf (cfg != {}) {
212
213 environment.systemPackages = [ pkgs.wpa_supplicant ];
214
215 services.dbus.packages = [ pkgs.wpa_supplicant ];
216
217 systemd.services = mapAttrs' (n: v: nameValuePair (serviceName n) (supplicantService n v)) cfg;
218
219 services.udev.packages = [
220 (pkgs.writeTextFile {
221 name = "99-zzz-60-supplicant.rules";
222 destination = "/etc/udev/rules.d/99-zzz-60-supplicant.rules";
223 text = ''
224 ${flip (concatMapStringsSep "\n") (filter (n: n!="WLAN" && n!="LAN" && n!="DBUS") (attrNames cfg)) (iface:
225 flip (concatMapStringsSep "\n") (splitString " " iface) (i: ''
226 ACTION=="add", SUBSYSTEM=="net", ENV{INTERFACE}=="${i}", TAG+="systemd", ENV{SYSTEMD_WANTS}+="supplicant-${replaceStrings [" "] ["-"] iface}.service", TAG+="SUPPLICANT_ASSIGNED"''))}
227
228 ${optionalString (hasAttr "WLAN" cfg) ''
229 ACTION=="add", SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", TAG!="SUPPLICANT_ASSIGNED", TAG+="systemd", PROGRAM="/run/current-system/systemd/bin/systemd-escape -p %E{INTERFACE}", ENV{SYSTEMD_WANTS}+="supplicant-wlan@$result.service"
230 ''}
231 ${optionalString (hasAttr "LAN" cfg) ''
232 ACTION=="add", SUBSYSTEM=="net", ENV{DEVTYPE}=="lan", TAG!="SUPPLICANT_ASSIGNED", TAG+="systemd", PROGRAM="/run/current-system/systemd/bin/systemd-escape -p %E{INTERFACE}", ENV{SYSTEMD_WANTS}+="supplicant-lan@$result.service"
233 ''}
234 '';
235 })];
236
237 };
238
239}
240