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