1-- Copyright (c) 2019 Marcel Kaiser <mk@freeshell.de>
2--
3-- Redistribution and use in source and binary forms, with or without
4-- modification, are permitted provided that the following conditions
5-- are met:
6-- 1. Redistributions of source code must retain the above copyright
7--    notice, this list of conditions and the following disclaimer.
8-- 2. Redistributions in binary form must reproduce the above copyright
9--    notice, this list of conditions and the following disclaimer in the
10--    documentation and/or other materials provided with the distribution.
11--
12-- THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
13-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
14-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
15-- ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
16-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
17-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
18-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
19-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
20-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
21-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
22-- SUCH DAMAGE.
23--
24
25local netif = {}
26
27-- Constants for the match_netif_type() function
28netif.NETIF_TYPE_WLAN  = 1
29netif.NETIF_TYPE_ETHER = 2
30
31netif.path_rc_conf  = '/etc/rc.conf'
32netif.path_zoneinfo = '/var/db/zoneinfo'
33netif.path_zone_tab = '/usr/share/zoneinfo/zone.tab'
34
35-- Returns a pair, (true|false, NETIF_TYPE_WLAN|NETIF_TYPE_ETHER|nil),
36-- if the given driver name matches an ethernet or wireless device driver.
37function netif.match_netif_type(driver)
38	local m
39	local wlan_kmods = {
40		"if_zyd",    "if_ath",      "if_bwi",      "if_bwn",
41		"if_ipw",    "if_iwi",      "if_iwm",      "if_iwn",
42		"if_malo",   "if_mwl",      "if_otus",     "if_ral",
43		"if_rsu",    "if_rtwn_usb", "if_rtwn_pci", "if_rum",
44		"if_run",    "if_uath",     "if_upgt",     "if_ural",
45		"if_urtw",   "if_wi"
46	}
47	local ether_kmods = {
48		"if_ae",     "if_age",      "if_alc",      "if_ale",
49		"if_aue",    "if_axe",      "if_bce",      "if_bfe",
50		"if_bge",    "if_bnxt",     "if_bxe",      "if_cas",
51		"if_cdce",   "if_cue",      "if_cxgb",     "if_dc",
52		"if_de",     "if_ed",       "if_edsc",     "if_em",
53		"if_et",     "if_fwe",      "if_fxp",      "if_gem",
54		"if_hme",    "if_ntb",      "if_ipheth",   "if_ix",
55		"if_ixl",    "if_jme",      "if_kue",      "if_le",
56		"if_lge",    "if_mos",      "if_msk",      "if_mxge",
57		"if_my",     "if_nf10bmac", "if_nfe",      "if_nge",
58		"if_pcn",    "if_ptnet",    "if_qlnxe",    "if_qlxgb",
59		"if_qlxgbe", "if_qlxge",    "if_re",       "if_rl",
60		"if_rue",    "if_sf",       "if_sfxge",    "if_sge",
61		"if_sis",    "if_sk",       "if_smsc",     "if_sn",
62		"if_ste",    "if_stge",     "if_tap",      "if_ti",
63		"if_tl",     "if_tx",       "if_txp",      "if_udav",
64		"if_ure",    "if_urndis",   "if_vge",      "if_vr",
65		"if_vte",    "if_vtnet",    "if_wb",       "if_xe",
66		"if_xl"
67	}
68	for _, m in pairs(wlan_kmods) do
69		if driver == m then
70			return true, netif.NETIF_TYPE_WLAN
71		end
72	end
73	for _, m in pairs(ether_kmods) do
74		if driver == m then
75			return true, netif.NETIF_TYPE_ETHER
76		end
77	end
78	return false, nil
79end
80
81-- Takes the name of a kernel module, and removes the "if_" prefix and
82-- the "_pci" or "_usb" suffix. Returns the new string.
83function netif.kmod_to_dev(kmod)
84	local dev = kmod
85	if string.match(kmod, ".*_pci$") or string.match(kmod, ".*_usb$") then
86		dev = string.sub(kmod, 1, string.len(kmod) - 4)
87	end
88	if string.match(dev, "^if_.*") then
89		dev = string.sub(dev, 4)
90	end
91	return dev
92end
93
94-- Takes a device name without unit number (e.g. ath, rtwn), and a list of
95-- network devices. Returns the name of the interface if found, nil otherwise.
96function netif.find_netif(dev, netifs)
97	local d
98	for _, d in pairs(netifs) do
99		if d == dev or string.match(d, dev .. "[0-9]+") then
100			return d
101		end
102	end
103	return nil
104end
105
106-- Returns the the unit (X) of a "wlanX" device name to a given
107-- parent device, or nil
108function wlan_unit_from_parent(pdev)
109	local l
110	local proc, e = io.popen("sysctl net.wlan")
111	if proc == nil then
112		io.stderr:write(e)
113		return nil
114	end
115	for l in proc:lines() do
116		if string.match(l, "%%parent") and string.match(l, pdev) then
117			return tonumber(string.match(l, "net.wlan.([0-9]+)."))
118		end
119	end
120	return nil
121end
122
123-- We define a wlan device object which has the following fields:
124--	parent ::= parent device name (e.g., "ath0", "rtwn0")
125--	child  ::= child device unit number ("wlan0" -> unit = 0). If there is no
126--  	child device yet, this field is nil.
127--
128-- This function returns a list of available wlan device objects, or an empty
129-- list if there are none.
130function netif.get_wlan_devs()
131	local i, l, parent
132	local pdevs = {}
133	local wlans = {}
134	local proc, e = io.popen("sysctl -n net.wlan.devices")
135	if proc == nil then
136		io.stderr:write(e)
137		return nil
138	end
139	i = 1
140	for l in proc:lines() do
141		for w in string.gmatch(l, "%w+") do
142			pdevs[i] = w
143			i = i + 1
144		end
145	end
146	proc:close()
147	i = 1
148	for _, parent in pairs(pdevs) do
149		child = wlan_unit_from_parent(parent)
150		wlans[i] = {}
151		wlans[i]["parent"] = parent
152		wlans[i]["child"] = child
153		i = i + 1
154	end
155	return wlans
156end
157
158-- Returns the wlan device object from the given list matching the given
159-- parent device pattern, or nil if there was no match.
160function netif.find_wlan(parent, wlans)
161	local w
162	for _, w in pairs(wlans) do
163		if string.match(w.parent, parent .. "[0-9]+") then
164			return w
165		end
166	end
167	return nil
168end
169
170function netif.get_ifconfig_if_info(ifname)
171	local info = {}
172	local proc, e = io.popen("ifconfig " .. ifname)
173	if proc == nil then
174		io.stderr:write(e)
175		return nil
176	end
177	local l
178	for l in proc:lines() do
179		table.insert(info, l)
180	end
181	proc:close()
182	return info
183end
184
185-- Returns the network interface's media type or nil
186function netif.media_type(ifname)
187	local l
188	local info = netif.get_ifconfig_if_info(ifname)
189	if info == nil then
190		return nil
191	end
192	for _, l in pairs(info) do
193		local type = string.match(l, "^%s+media:%s([%g,%s]+)$")
194		if type then
195			if string.match(type, "%s[wW]ireless%s") or
196			   string.match(type, "%s802.11%s") then
197				return netif.NETIF_TYPE_WLAN
198			elseif string.match(type, "^[Ee]thernet") then
199				return netif.NETIF_TYPE_ETHER
200			else
201				return nil
202			end
203		end
204	end
205	return nil
206end
207
208function netif.get_ifconfig_iflist()
209	local l
210	local iflist = {}
211	local proc, e = io.popen("ifconfig -l")
212	if proc == nil then
213		io.stderr:write(e)
214		return nil
215	end
216	for l in proc:lines() do
217		for w in string.gmatch(l, "%w+") do
218			table.insert(iflist, w)
219		end
220	end
221	proc:close()
222	return iflist
223end
224
225-- Returns the list of network interfaces from the output of "ifconfig" as
226-- array.
227function netif.get_netifs()
228	local iflist = {}
229	local all_ifs = netif.get_ifconfig_iflist()
230	if all_ifs == nil then
231		return nil
232	end
233	for _, i in pairs(all_ifs) do
234		type = netif.media_type(i)
235		if type ~= nil then
236			table.insert(iflist, i)
237		end
238	end
239	return iflist
240end
241
242-- Returns the given network interface's status
243function netif.link_status(ifname)
244	local i, status
245	local info = netif.get_ifconfig_if_info(ifname)
246	if info == nil then
247		return nil
248	end
249	for _, i in pairs(info) do
250		if string.match(i, "^[ \t]*status: (%w+)") then
251			status = string.gsub(i, "^[ \t]*status: ([%w, ]+)$", "%1")
252			if status ~= nil then
253				return status
254			end
255		end
256	end
257	return nil
258end
259
260-- Returns "true" if the given network interface was configured
261-- via /etc/rc.conf
262function netif.in_rc_conf(ifname)
263	local l
264	local f, e = io.open(netif.path_rc_conf)
265	if f == nil then
266		io.stderr:write(e)
267		return nil
268	end
269	for l in f:lines() do
270		if string.match(l, "^[ \t]*ifconfig_" .. ifname) then
271			f:close()
272			return true
273		end
274		if string.match(l, "^[ \t]*ifconfig_" .. ifname .. "_ipv6") then
275			f:close()
276			return true
277		end
278	end
279	f:close()
280	return false
281end
282
283-- Returns the inet (v4) address of the given interface from the dhclient
284-- lease file
285function netif.inet_from_lease(ifname)
286	local l, inet, _inet
287	local f, e, errno = io.open("/var/db/dhclient.leases." .. ifname)
288	if f == nil then
289		if errno == 2 then -- ENOENT
290			return nil
291		end
292		io.stderr:write(e)
293		return nil
294	end
295	inet = nil
296	-- Get IP address of the last lease record
297	for l in f:lines() do
298		_inet = string.match(l, "^%s*fixed.address%s*(.+);$")
299		if _inet ~= nil then
300			inet = _inet
301		end
302	end
303	f:close()
304	return inet
305end
306
307-- Get the inet v4 and v6 addresses of the given interface
308function netif.get_inet_addr(ifname)
309	local i, inet4, inet6
310	local info = netif.get_ifconfig_if_info(ifname)
311	if info == nil then
312		return nil, nil
313	end
314	for _, i in pairs(info) do
315		if inet4 == nil then
316			inet4 = string.match(i, "^%s+inet%s+(%g+)")
317		end
318		if inet6 == nil then
319			inet6 = string.match(i, "^%s+inet6%s+([%x:]+)")
320		end
321		if inet4 ~= nil and inet6 ~= nil then
322			break
323		end
324	end
325	return inet4, inet6
326end
327
328-- Returns a list of wlan device objects configured via /etc/rc.conf
329function netif.wlans_from_rc_conf()
330	local i, l
331	local wlans = {}
332	local f, e = io.open(netif.path_rc_conf)
333	if f == nil then
334		io.stderr:write(e)
335		return nil
336	end
337	i = 1
338	for l in f:lines() do
339		local p, c = string.match(l, "^[ \t]*wlans_(%w+)=\"?(%w+)\"?")
340		if p ~= nil and c ~= nil then
341			wlans[i] = {}
342			wlans[i].parent = p
343			wlans[i].child = tonumber(string.match(c, "wlan(%d+)"))
344			i = i + 1
345		end
346	end
347	f:close()
348	return wlans
349end
350
351-- Returns "true" if the given wlan device object was configured via
352-- /etc/rc.conf, else "false".
353function netif.wlan_rc_configured(wlan)
354	local w
355	local wlans = netif.wlans_from_rc_conf()
356	for _, w in pairs(wlans) do
357		if w.parent == wlan.parent and w.child == wlan.child then
358			return true
359		end
360	end
361	return false
362end
363
364-- Returns the country code of the region defined in /var/db/zoneinfo,
365-- or nil if not found
366function netif.get_wlan_region()
367	local l, zone, code
368	local f, e = io.open(netif.path_zoneinfo)
369	if f == nil then
370		io.stderr:write(e)
371		return nil
372	end
373	for l in f:lines() do
374		if string.match(l, "/") then
375			zone = l
376			break
377		end
378	end
379	f:close()
380	if not zone then
381		return nil
382	end
383	f, e = io.open(netif.path_zone_tab)
384	if f == nil then
385		io.stderr:write(e)
386		return nil
387	end
388	for l in f:lines() do
389		code = string.match(l, "^(%u+)%s+[0-9+-]+%s+" .. zone)
390		if code then
391			break
392		end
393	end
394	f:close()
395	return code
396end
397
398-- Sleeps n seconds
399function netif.sleep(n)
400	os.execute("sleep " .. tonumber(n))
401end
402
403-- Takes a driver name (e.g. if_ath) and waits for not more than "timeout"
404-- seconds for the parent device matching the driver to appear in
405-- net.wlan.devices. If found, the corresponding wlan device object is
406-- returned, else nil.
407function netif.wait_for_new_wlan(driver, timeout)
408	local devname = netif.kmod_to_dev(driver)
409	local tries = 1
410	while true do
411		local wlans = netif.get_wlan_devs()
412		local w = netif.find_wlan(devname, wlans)
413		if w == nil then
414			if tries >= timeout then
415				return nil
416			end
417			netif.sleep(1)
418		else
419			return w
420		end
421		tries = tries + 1
422	end
423end
424
425local function get_wlan_regdomain_args()
426	local country = netif.get_wlan_region()
427	if country == nil then
428		return ""
429	end
430	return "down country " .. country
431end
432
433function netif.restart_netif(ifname)
434	return os.execute("service netif restart " .. ifname)
435end
436
437function netif.run_sysrc(var)
438	return os.execute("sysrc " .. var)
439end
440
441function netif.set_rc_conf_var(var, val)
442	local rc_var = string.format('%s="%s"', var, val)
443	return netif.run_sysrc(rc_var)
444end
445
446function netif.create_wlan_child_dev(parent, child_unit)
447	local cmd = string.format("ifconfig wlan%d create wlandev %s",
448	    child_unit, parent)
449	return os.execute(cmd)
450end
451
452function netif.add_wlan_to_rc_conf(wlan)
453	local args
454
455	netif.set_rc_conf_var("wlans_" .. wlan.parent, "wlan" .. wlan.child)
456	if wlan_set_country then
457		args = get_wlan_regdomain_args()
458	end
459	if wlan_create_args ~= nil then
460		if args == nil then
461			args = wlan_create_args
462
463		else
464			args = args .. " " .. wlan_create_args
465		end
466	end
467	if args ~= nil then
468		netif.set_rc_conf_var("create_args_wlan" .. wlan.child, args)
469	end
470	netif.set_rc_conf_var("ifconfig_wlan" .. wlan.child, wlan_ifconfig_args)
471	if enable_ipv6 then
472		netif.set_rc_conf_var("ifconfig_wlan" .. wlan.child .. "_ipv6",
473		  wlan_ifconfig_ipv6_args)
474	end
475end
476
477-- Creates and configures a new wlan child device (wlanX) for each wlan
478-- device object which doesn't have a child device yet.
479function netif.create_wlan_devs()
480	local w, max_unit
481	local wlans = netif.get_wlan_devs()
482
483	if wlan_ifconfig_args == nil then
484		wlan_ifconfig_args = "up scan WPA DHCP"
485	end
486	if wlan_ifconfig_ipv6_args == nil then
487		wlan_ifconfig_ipv6_args = "inet6 accept_rtadv"
488	end
489	-- Calculate the next available unit number for the child device
490	max_unit = -1
491	for _, w in pairs(wlans) do
492		if w.child ~= nil then
493			if w.child > max_unit then
494				max_unit = w.child
495			end
496		end
497	end
498	-- Create a child device for each parent device which doesn't have
499	-- a child ("wlanX"), and wasn't configured via /etc/rc.conf with
500	-- 'wlans_parent="wlan<max_unit>"'
501	for _, w in pairs(wlans) do
502		if ignore_netifs == nil or
503		   netif.find_netif(w.parent, ignore_netifs) == nil then
504			if not netif.wlan_rc_configured(w) or w.child == nil then
505				if w.child == nil then
506					max_unit = max_unit + 1
507					w.child = max_unit
508					netif.create_wlan_child_dev(w.parent, max_unit)
509				end
510				netif.add_wlan_to_rc_conf(w)
511				local child = "wlan" .. w.child
512				local status = netif.link_status(child)
513				if status == nil or status ~= "associated" then
514					netif.restart_netif(child)
515				end
516			end
517		end
518	end
519end
520
521-- Takes a driver name (e.g. if_alc) and waits for not more than "timeout"
522-- seconds for the device matching the driver to appear in the list of
523-- network interfaces. If found, the interface name is returned, else nil.
524function netif.wait_for_new_ether(driver, timeout)
525	local devname = netif.kmod_to_dev(driver)
526
527	local tries = 1
528	while true do
529		local iflist = netif.get_netifs()
530		local ifname = netif.find_netif(devname, iflist)
531		if ifname == nil then
532			if tries >= timeout then
533				return nil
534			end
535			netif.sleep(1)
536		else
537			return ifname
538		end
539		tries = tries + 1
540	end
541end
542
543-- Starts DHCP on each ethernet device.
544function netif.setup_ether_devs()
545	local i
546	local iflist = netif.get_netifs()
547	if ether_ifconfig_args == nil then
548		ether_ifconfig_args = "DHCP"
549	end
550	if ether_ifconfig_ipv6_args == nil then
551		ether_ifconfig_ipv6_args = "inet6 accept_rtadv"
552	end
553	for _, i in pairs(iflist) do
554		if ignore_netifs == nil or
555		  netif.find_netif(i, ignore_netifs) == nil then
556			if not string.match(i, "wlan") then
557				if not netif.in_rc_conf(i) then
558					netif.set_rc_conf_var('ifconfig_' .. i, ether_ifconfig_args)
559					if enable_ipv6 then
560						netif.set_rc_conf_var('ifconfig_' .. i .. "_ipv6",
561						  ether_ifconfig_ipv6_args)
562					end
563				end
564				local inet4, inet6 = netif.get_inet_addr(i)
565				if inet6 == nil and inet4 == nil then
566					netif.restart_netif(i)
567				end
568			end
569		end
570	end
571end
572
573-- Configures and starts all network interfaces based on the given driver/kmod
574-- name.
575function netif.config_netif(kmod)
576	if netif_wait_max == nil then
577		netif_wait_max = 1
578	end
579	local is_netif, iftype = netif.match_netif_type(kmod)
580	if is_netif and iftype == netif.NETIF_TYPE_WLAN then
581		if netif.wait_for_new_wlan(kmod, netif_wait_max) ~= nil then
582			netif.create_wlan_devs()
583		end
584	elseif is_netif and iftype == netif.NETIF_TYPE_ETHER then
585		if netif.wait_for_new_ether(kmod, netif_wait_max) ~= nil then
586			netif.setup_ether_devs()
587		end
588	end
589end
590
591return netif
592