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 (replaceChars [" "] ["-"] 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-${replaceChars [" "] ["-"] 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 = [ "network.target" ];
38 bindsTo = deps;
39 after = deps;
40 before = [ "network.target" ];
41 # Receive restart event after resume
42 partOf = [ "post-resume.target" ];
43
44 path = [ pkgs.coreutils ];
45
46 preStart = ''
47 ${optionalString (suppl.configFile.path!=null) ''
48 touch -a ${suppl.configFile.path}
49 chmod 600 ${suppl.configFile.path}
50 ''}
51 ${optionalString suppl.userControlled.enable ''
52 if ! test -e ${suppl.userControlled.socketDir}; then
53 mkdir -m 0770 -p ${suppl.userControlled.socketDir}
54 chgrp ${suppl.userControlled.group} ${suppl.userControlled.socketDir}
55 fi
56
57 if test "$(stat --printf '%G' ${suppl.userControlled.socketDir})" != "${suppl.userControlled.group}"; then
58 echo "ERROR: bad ownership on ${suppl.userControlled.socketDir}" >&2
59 exit 1
60 fi
61 ''}
62 '';
63
64 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)}";
65
66 };
67
68
69in
70
71{
72
73 ###### interface
74
75 options = {
76
77 networking.supplicant = mkOption {
78 type = types.attrsOf types.optionSet;
79
80 default = { };
81
82 example = {
83 "wlan0 wlan1" = {
84 configFile = "/etc/wpa_supplicant";
85 userControlled.group = "network";
86 extraConf = ''
87 ap_scan=1
88 p2p_disabled=1
89 '';
90 extraCmdArgs = "-u -W";
91 bridge = "br0";
92 };
93 };
94
95 description = ''
96 Interfaces for which to start <command>wpa_supplicant</command>.
97 The supplicant is used to scan for and associate with wireless networks,
98 or to authenticate with 802.1x capable network switches.
99
100 The value of this option is an attribute set. Each attribute configures a
101 <command>wpa_supplicant</command> service, where the attribute name specifies
102 the name of the interface that <command>wpa_supplicant</command> operates on.
103 The attribute name can be a space separated list of interfaces.
104 The attribute names <literal>WLAN</literal>, <literal>LAN</literal> and <literal>DBUS</literal>
105 have a special meaning. <literal>WLAN</literal> and <literal>LAN</literal> are
106 configurations for universal <command>wpa_supplicant</command> service that is
107 started for each WLAN interface or for each LAN interface, respectively.
108 <literal>DBUS</literal> defines a device-unrelated <command>wpa_supplicant</command>
109 service that can be accessed through <literal>D-Bus</literal>.
110 '';
111
112 options = {
113
114 configFile = {
115
116 path = mkOption {
117 type = types.path;
118 example = literalExample "/etc/wpa_supplicant.conf";
119 description = ''
120 External <literal>wpa_supplicant.conf</literal> configuration file.
121 The configuration options defined declaratively within <literal>networking.supplicant</literal> have
122 precedence over options defined in <literal>configFile</literal>.
123 '';
124 };
125
126 writable = mkOption {
127 type = types.bool;
128 default = false;
129 description = ''
130 Whether the configuration file at <literal>configFile.path</literal> should be written to by
131 <literal>wpa_supplicant</literal>.
132 '';
133 };
134
135 };
136
137 extraConf = mkOption {
138 type = types.lines;
139 default = "";
140 example = ''
141 ap_scan=1
142 device_name=My-NixOS-Device
143 device_type=1-0050F204-1
144 driver_param=use_p2p_group_interface=1
145 disable_scan_offload=1
146 p2p_listen_reg_class=81
147 p2p_listen_channel=1
148 p2p_oper_reg_class=81
149 p2p_oper_channel=1
150 manufacturer=NixOS
151 model_name=NixOS_Unstable
152 model_number=2015
153 '';
154 description = ''
155 Configuration options for <literal>wpa_supplicant.conf</literal>.
156 Options defined here have precedence over options in <literal>configFile</literal>.
157 NOTE: Do not write sensitive data into <literal>extraConf</literal> as it will
158 be world-readable in the <literal>nix-store</literal>. For sensitive information
159 use the <literal>configFile</literal> instead.
160 '';
161 };
162
163 extraCmdArgs = mkOption {
164 type = types.str;
165 default = "";
166 example = "-e/var/run/wpa_supplicant/entropy.bin";
167 description =
168 "Command line arguments to add when executing <literal>wpa_supplicant</literal>.";
169 };
170
171 driver = mkOption {
172 type = types.nullOr types.str;
173 default = "nl80211,wext";
174 description = "Force a specific wpa_supplicant driver.";
175 };
176
177 bridge = mkOption {
178 type = types.str;
179 default = "";
180 description = "Name of the bridge interface that wpa_supplicant should listen at.";
181 };
182
183 userControlled = {
184
185 enable = mkOption {
186 type = types.bool;
187 default = false;
188 description = ''
189 Allow normal users to control wpa_supplicant through wpa_gui or wpa_cli.
190 This is useful for laptop users that switch networks a lot and don't want
191 to depend on a large package such as NetworkManager just to pick nearby
192 access points.
193 '';
194 };
195
196 socketDir = mkOption {
197 type = types.str;
198 default = "/var/run/wpa_supplicant";
199 description = "Directory of sockets for controlling wpa_supplicant.";
200 };
201
202 group = mkOption {
203 type = types.str;
204 default = "wheel";
205 example = "network";
206 description = "Members of this group can control wpa_supplicant.";
207 };
208
209 };
210
211 };
212
213 };
214
215 };
216
217
218 ###### implementation
219
220 config = mkIf (cfg != {}) {
221
222 environment.systemPackages = [ pkgs.wpa_supplicant ];
223
224 services.dbus.packages = [ pkgs.wpa_supplicant ];
225
226 systemd.services = mapAttrs' (n: v: nameValuePair (serviceName n) (supplicantService n v)) cfg;
227
228 services.udev.packages = [
229 (pkgs.writeTextFile {
230 name = "99-zzz-60-supplicant.rules";
231 destination = "/etc/udev/rules.d/99-zzz-60-supplicant.rules";
232 text = ''
233 ${flip (concatMapStringsSep "\n") (filter (n: n!="WLAN" && n!="LAN" && n!="DBUS") (attrNames cfg)) (iface:
234 flip (concatMapStringsSep "\n") (splitString " " iface) (i: ''
235 ACTION=="add", SUBSYSTEM=="net", ENV{INTERFACE}=="${i}", TAG+="systemd", ENV{SYSTEMD_WANTS}+="supplicant-${replaceChars [" "] ["-"] iface}.service", TAG+="SUPPLICANT_ASSIGNED"''))}
236
237 ${optionalString (hasAttr "WLAN" cfg) ''
238 ACTION=="add", SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", TAG!="SUPPLICANT_ASSIGNED", TAG+="systemd", PROGRAM="${pkgs.systemd}/bin/systemd-escape -p %E{INTERFACE}", ENV{SYSTEMD_WANTS}+="supplicant-wlan@$result.service"
239 ''}
240 ${optionalString (hasAttr "LAN" cfg) ''
241 ACTION=="add", SUBSYSTEM=="net", ENV{DEVTYPE}=="lan", TAG!="SUPPLICANT_ASSIGNED", TAG+="systemd", PROGRAM="${pkgs.systemd}/bin/systemd-escape -p %E{INTERFACE}", ENV{SYSTEMD_WANTS}+="supplicant-lan@$result.service"
242 ''}
243 '';
244 })];
245
246 };
247
248}
249