Initial import
[project/ufp.git] / files / usr / share / ufp / plugin_wifi.uc
1 import * as struct from "struct";
2 let ubus, uloop, global, timer;
3 let ap_cache = {};
4
5 const ie_tags = {
6 PWR_CAPABILITY: 33,
7 HT_CAP: 45,
8 EXT_CAPAB: 127,
9 VHT_CAP: 191,
10 __EXT_START: 0x100,
11 HE_CAP: 0x100 | 35,
12 VENDOR_WPS: 0x0050f204,
13 };
14
15 const ie_parser_proto = {
16 reset: function() {
17 this.offset = 0;
18 },
19
20 parseAt: function(offset) {
21 let hdr = substr(this.buffer, offset, 2);
22 if (length(hdr) != 2)
23 return null;
24
25 let data = this.hdr.unpack(hdr);
26 if (length(data != 2))
27 return null;
28
29 let len = data[1];
30 offset += 2;
31 data[1] += 2;
32
33 if (data[0] == 221 && len >= 4) {
34 hdr = substr(this.buffer, offset, 4);
35 if (length(hdr) != 4)
36 return null;
37
38 let val = this.vendor_hdr.unpack(hdr);
39 if (length(val) != 1 || val[0] < 0x200)
40 return null;
41
42 data[0] = val[0];
43 len -= 4;
44 offset += 4;
45 } else if (data[0] == 255 && len >= 1) {
46 hdr = substr(this.buffer, offset, 2);
47 if (length(hdr) != 2)
48 return null;
49 data[0] = 0x100 + this.hdr.unpack(hdr)[0];
50 len -= 1;
51 offset += 1;
52 }
53
54 data[2] = data[1];
55 data[1] = substr(this.buffer, offset, len);
56 if (length(data[1]) != len)
57 return null;
58
59 return data;
60 },
61
62 next: function() {
63 let data = this.parseAt(this.offset);
64 if (!data)
65 return null;
66
67 this.offset += data[2];
68 return data;
69 },
70
71 foreach: function(cb) {
72 let offset = 0;
73 let data;
74
75 while ((data = this.parseAt(offset)) != null) {
76 offset += data[2];
77 let ret = cb(data);
78 if (type(ret) == "boolean" && !ret)
79 break;
80 }
81 },
82 };
83
84 function ie_parser(data) {
85 let parser = {
86 offset: 0,
87 buffer: data,
88 hdr: struct.new("BB"),
89 vendor_hdr: struct.new(">I"),
90 };
91
92 proto(parser, ie_parser_proto);
93
94 return parser;
95 }
96
97 function format_fn(unpack_str, format)
98 {
99 return (data) => {
100 data = struct.unpack(unpack_str, data);
101 if (data && data[0])
102 data = data[0];
103 else
104 data = 0;
105 return sprintf(format, data)
106 };
107 }
108
109 let unpack;
110 unpack = {
111 u8: format_fn("B", "%02x"),
112 le16: format_fn("<H", "%04x"),
113 le32: format_fn("<I", "%08x"),
114 bytes: (data) => join("", map(split(data, ""), unpack.u8)),
115 };
116 let fingerprint_order = [
117 "htcap", "htagg", "htmcs", "vhtcap", "vhtrxmcs", "vhttxmcs",
118 "txpow", "extcap", "wps", "hemac", "hephy"
119 ];
120
121 function format_wps_ie(data) {
122 let offset = 0;
123 let len = length(data);
124 let s = struct.new("<HH");
125
126 while (offset + 4 <= len) {
127 let hdr = s.unpack(substr(data, offset, 4));
128 let val = substr(data, offset + 4, hdr[1]);
129
130 offset += 4 + hdr[1];
131 if (hdr[0] != 0x1023)
132 continue;
133
134 if (length(val) != hdr[1])
135 break;
136
137 return replace(val, /[^A-Za-z0-9]/, "_");
138 }
139
140 return null;
141 }
142
143 function ie_fingerprint_str(id) {
144 if (id >= 0x200)
145 return sprintf("221(%08x)", id);
146 if (id >= 0x100)
147 return sprintf("255(%d)", id - 0x100);
148 return sprintf("%d", id);
149 }
150
151 let vendor_ie_filter = [
152 0x0050f2, // Microsoft WNN
153 0x506f9a, // WBA
154 0x8cfdf0, // Qualcom
155 0x001018, // Broadcom
156 0x000c43, // Ralink
157 ];
158
159 function ie_fingerprint(data, mode) {
160 let caps = {
161 tags: [],
162 vendor_list: {}
163 };
164 let parser = ie_parser(data);
165
166 parser.foreach(function(ie) {
167 let skip = false;
168 let val = ie[1];
169 switch (ie[0]) {
170 case ie_tags.HT_CAP:
171 caps.htcap = unpack.le16(substr(val, 0, 2));
172 caps.htagg = unpack.u8(substr(val, 2, 1));
173 caps.htmcs = unpack.le32(substr(val, 3, 4));
174 break;
175 case ie_tags.VHT_CAP:
176 caps.vhtcap = unpack.le32(substr(val, 0, 4));
177 caps.vhtrxmcs = unpack.le32(substr(val, 4, 4));
178 caps.vhttxmcs = unpack.le32(substr(val, 8, 4));
179 break;
180 case ie_tags.EXT_CAPAB:
181 caps.extcap = unpack.bytes(val);
182 break;
183 case ie_tags.PWR_CAPABILITY:
184 caps.txpow = unpack.le16(val);
185 break;
186 case ie_tags.VENDOR_WPS:
187 caps.wps = format_wps_ie(val);
188 break;
189 case ie_tags.HE_CAP:
190 if (mode != "wifi6") {
191 skip = true;
192 break;
193 }
194 caps.hemac =
195 unpack.le16(substr(val, 4, 2)) +
196 unpack.le32(substr(val, 0, 4));
197 caps.hephy =
198 unpack.le16(substr(val, 15, 2)) +
199 unpack.le32(substr(val, 11, 4)) +
200 unpack.le32(substr(val, 7, 4));
201 break;
202 }
203 if (ie[0] > 0x200) {
204 let vendor = ie[0] >> 8;
205 if (!(vendor in vendor_ie_filter))
206 caps.vendor_list[sprintf("%06x", vendor)] = 1;
207 }
208 if (!skip)
209 push(caps.tags, ie[0]);
210 return null;
211 });
212
213 switch (mode) {
214 case "wifi6":
215 if (mode == "wifi6" && !caps.hemac)
216 return null;
217 break;
218 case "wifi-vendor-oui":
219 return caps.vendor_list;
220 default:
221 break;
222 }
223
224 let tags = map(caps.tags, ie_fingerprint_str);
225 return
226 join(",", tags) + "," +
227 join(",", map(
228 filter(fingerprint_order, (key) => !!caps[key]),
229 (key) => `${key}:${caps[key]}`
230 ));
231 }
232
233 function fingerprint(mac, mode, ies) {
234 switch (mode) {
235 case "wifi4":
236 if (!ies.assoc_ie)
237 break;
238
239 let assoc = ie_fingerprint(ies.assoc_ie, mode);
240 if (!assoc)
241 break;
242
243 global.device_add_data(mac, `${mode}|${assoc}`);
244 break;
245 case "wifi-vendor-oui":
246 let list = ie_fingerprint(ies.assoc_ie, mode);
247 for (let oui in list) {
248 global.device_add_data(mac, `${mode}-${oui}|1`);
249 }
250 break;
251 case "wifi6":
252 default:
253 let val = ie_fingerprint(ies.assoc_ie, mode);
254 if (!val)
255 break;
256
257 global.device_add_data(mac, `${mode}|${val}`);
258 break;
259 }
260 }
261
262 const fingerprint_modes = [ "wifi4", "wifi6", "wifi-vendor-oui" ];
263
264 function client_refresh(ap, mac, prev_cache)
265 {
266 let ies = ubus.call(ap, "get_sta_ies", { address: mac });
267 if (type(ies) != "object" || !ies.assoc_ie)
268 return null;
269
270 ies.assoc_ie = b64dec(ies.assoc_ie);
271 if (ies.probe_ie)
272 ies.probe_ie = b64dec(ies.probe_ie);
273
274 for (let mode in fingerprint_modes)
275 fingerprint(mac, mode, ies);
276
277 return ies;
278 }
279
280 function refresh()
281 {
282 let ap_objs = filter(ubus.list(), (name) => match(name, /^hostapd\./));
283 let prev_cache = ap_cache;
284 ap_cache = {};
285
286 timer.set(30 * 1000);
287 for (let ap in ap_objs) {
288 try {
289 let cur_cache = {};
290 let prev_ap_cache = prev_cache[ap] ?? {};
291
292 ap_cache[ap] = cur_cache;
293
294 let clients = ubus.call(ap, "get_clients").clients;
295 for (let client in clients) {
296 let client_cache = prev_ap_cache[client];
297 if (!client_cache || client_cache.assoc_ie || !client_cache.probe_ie)
298 client_cache = client_refresh(ap, client);
299 global.device_refresh(client);
300 }
301 } catch (e) {
302 }
303 }
304 }
305
306 function init(gl) {
307 global = gl;
308 ubus = gl.ubus;
309 uloop = gl.uloop;
310
311 global.add_weight({
312 wifi4: 2.0,
313 wifi6: 3.0,
314 "wifi-vendor-oui": 2.0
315 });
316
317 timer = uloop.timer(1000, refresh);
318 }
319
320 return { init, refresh };