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