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 = ''
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 = ''
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 = ''
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 = "Command line arguments to add when executing `wpa_supplicant`.";
126 };
127
128 driver = mkOption {
129 type = types.nullOr types.str;
130 default = "nl80211,wext";
131 description = "Force a specific wpa_supplicant driver.";
132 };
133
134 bridge = mkOption {
135 type = types.str;
136 default = "";
137 description = "Name of the bridge interface that wpa_supplicant should listen at.";
138 };
139
140 userControlled = {
141
142 enable = mkOption {
143 type = types.bool;
144 default = false;
145 description = ''
146 Allow normal users to control wpa_supplicant through wpa_gui or wpa_cli.
147 This is useful for laptop users that switch networks a lot and don't want
148 to depend on a large package such as NetworkManager just to pick nearby
149 access points.
150 '';
151 };
152
153 socketDir = mkOption {
154 type = types.str;
155 default = "/run/wpa_supplicant";
156 description = "Directory of sockets for controlling wpa_supplicant.";
157 };
158
159 group = mkOption {
160 type = types.str;
161 default = "wheel";
162 example = "network";
163 description = "Members of this group can control wpa_supplicant.";
164 };
165
166 };
167 };
168 });
169
170 default = { };
171
172 example = literalExpression ''
173 { "wlan0 wlan1" = {
174 configFile.path = "/etc/wpa_supplicant.conf";
175 userControlled.group = "network";
176 extraConf = '''
177 ap_scan=1
178 p2p_disabled=1
179 ''';
180 extraCmdArgs = "-u -W";
181 bridge = "br0";
182 };
183 }
184 '';
185
186 description = ''
187 Interfaces for which to start {command}`wpa_supplicant`.
188 The supplicant is used to scan for and associate with wireless networks,
189 or to authenticate with 802.1x capable network switches.
190
191 The value of this option is an attribute set. Each attribute configures a
192 {command}`wpa_supplicant` service, where the attribute name specifies
193 the name of the interface that {command}`wpa_supplicant` operates on.
194 The attribute name can be a space separated list of interfaces.
195 The attribute names `WLAN`, `LAN` and `DBUS`
196 have a special meaning. `WLAN` and `LAN` are
197 configurations for universal {command}`wpa_supplicant` service that is
198 started for each WLAN interface or for each LAN interface, respectively.
199 `DBUS` defines a device-unrelated {command}`wpa_supplicant`
200 service that can be accessed through `D-Bus`.
201 '';
202
203 };
204
205 };
206
207
208 ###### implementation
209
210 config = mkIf (cfg != {}) {
211
212 environment.systemPackages = [ pkgs.wpa_supplicant ];
213
214 services.dbus.packages = [ pkgs.wpa_supplicant ];
215
216 systemd.services = mapAttrs' (n: v: nameValuePair (serviceName n) (supplicantService n v)) cfg;
217
218 services.udev.packages = [
219 (pkgs.writeTextFile {
220 name = "99-zzz-60-supplicant.rules";
221 destination = "/etc/udev/rules.d/99-zzz-60-supplicant.rules";
222 text = ''
223 ${flip (concatMapStringsSep "\n") (filter (n: n!="WLAN" && n!="LAN" && n!="DBUS") (attrNames cfg)) (iface:
224 flip (concatMapStringsSep "\n") (splitString " " iface) (i: ''
225 ACTION=="add", SUBSYSTEM=="net", ENV{INTERFACE}=="${i}", TAG+="systemd", ENV{SYSTEMD_WANTS}+="supplicant-${replaceStrings [" "] ["-"] iface}.service", TAG+="SUPPLICANT_ASSIGNED"''))}
226
227 ${optionalString (hasAttr "WLAN" cfg) ''
228 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"
229 ''}
230 ${optionalString (hasAttr "LAN" cfg) ''
231 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"
232 ''}
233 '';
234 })];
235
236 };
237
238}
239