2f4c1dd79189b61b116d19af62c73de125b66db9
[project/luci.git] / applications / luci-splash / root / usr / sbin / luci-splash
1 #!/usr/bin/lua
2
3 utl = require "luci.util"
4 sys = require "luci.sys"
5
6 require("luci.model.uci")
7 require("luci.sys.iptparser")
8
9 -- Init state session
10 local uci = luci.model.uci.cursor_state()
11 local ipt = luci.sys.iptparser.IptParser()
12 local net = sys.net
13 local fs = require "luci.fs"
14 local ip = require "luci.ip"
15
16 local debug = false
17
18 local has_ipv6 = fs.access("/proc/net/ipv6_route") and fs.access("/usr/sbin/ip6tables")
19
20 function lock()
21         os.execute("lock /var/run/luci_splash.lock")
22 end
23
24 function unlock()
25         os.execute("lock -u /var/run/luci_splash.lock")
26 end
27
28 function exec(cmd)
29         local ret = sys.exec(cmd)
30         if debug then
31                 print('+ ' .. cmd)
32                 if ret and ret ~= "" then
33                         print(ret)
34                 end
35         end
36 end
37
38 function get_id(ip)
39         local o3, o4 = ip:match("[0-9]+%.[0-9]+%.([0-9]+)%.([0-9]+)")
40         if o3 and 04 then
41                 return string.format("%02X%s", tonumber(o3), "") .. string.format("%02X%s", tonumber(o4), "")
42         else
43                 return false
44         end
45 end
46
47 function get_device_for_ip(ipaddr)
48         local dev
49         uci:foreach("network", "interface", function(s)
50                 if s.ipaddr and s.netmask then
51                         local network = ip.IPv4(s.ipaddr, s.netmask)
52                         if network:contains(ip.IPv4(ipaddr)) then
53                                 -- this should be rewritten to luci functions if possible
54                                 dev = utl.trim(sys.exec(". /lib/functions/network.sh; network_get_device IFNAME '" ..  s['.name'] .. "'; echo $IFNAME"))
55                         end
56                 end
57         end)
58         return dev
59 end
60
61 function get_physdev(interface)
62         local dev
63         dev = utl.trim(sys.exec(". /lib/functions/network.sh; network_get_device IFNAME '" ..  interface .. "'; echo $IFNAME"))
64         return dev
65 end
66
67
68
69 function get_filter_handle(parent, direction, device, mac)
70         local input = utl.split(sys.exec('/usr/sbin/tc filter show dev ' .. device .. ' parent ' .. parent) or {})
71         local tbl = {}
72         local handle
73         for k, v in pairs(input) do
74                 handle = v:match('filter protocol ip pref %d+ u32 fh (%d*:%d*:%d*) order')
75                 if handle then
76                         local mac, mac1, mac2, mac3, mac4, mac5, mac6
77                         if direction == 'src' then
78                                 mac1, mac2, mac3, mac4 = input[k+1]:match('match ([%a%d][%a%d])([%a%d][%a%d])([%a%d][%a%d])([%a%d][%a%d])/ffffffff')
79                                 mac5, mac6 = input[k+2]:match('match ([%a%d][%a%d])([%a%d][%a%d])0000/ffff0000')
80                         else
81                                 mac1, mac2 = input[k+1]:match('match 0000([%a%d][%a%d])([%a%d][%a%d])/0000ffff')
82                                 mac3, mac4, mac5, mac6 = input[k+2]:match('match ([%a%d][%a%d])([%a%d][%a%d])([%a%d][%a%d])([%a%d][%a%d])/ffffffff')
83                         end
84                         if mac1 and mac2 and mac3 and mac4 and mac5 and mac6 then
85                                 mac = "%s:%s:%s:%s:%s:%s" % { mac1, mac2, mac3, mac4, mac5, mac6 }
86                                 tbl[mac] = handle
87                         end
88                 end
89         end
90         if tbl[mac] then
91                 handle = tbl[mac]
92         end
93         return handle
94 end
95
96 function main(argv)
97         local cmd = table.remove(argv, 1)
98         local arg = argv[1]
99
100         limit_up = (tonumber(uci:get("luci_splash", "general", "limit_up")) or 0) * 8
101         limit_down = (tonumber(uci:get("luci_splash", "general", "limit_down")) or 0) * 8
102
103         if ( cmd == "lease" or cmd == "add-rules" or cmd == "remove" or
104              cmd == "whitelist" or cmd == "blacklist" or cmd == "status" ) and #argv > 0
105         then
106                 lock()
107
108                 local arp_cache      = net.arptable()
109                 local leased_macs    = get_known_macs("lease")
110                 local blacklist_macs = get_known_macs("blacklist")
111                 local whitelist_macs = get_known_macs("whitelist")
112
113                 for i, adr in ipairs(argv) do
114                         local mac = nil
115                         if adr:find(":") then
116                                 mac = adr:lower()
117                         else
118                                 for _, e in ipairs(arp_cache) do
119                                         if e["IP address"] == adr then
120                                                 mac = e["HW address"]:lower()
121                                                 break
122                                         end
123                                 end
124                         end
125
126                         if mac and cmd == "add-rules" then
127                                 if leased_macs[mac] then
128                                         add_lease(mac, arp_cache, true)
129                                 elseif blacklist_macs[mac] then
130                                         add_blacklist_rule(mac)
131                                 elseif whitelist_macs[mac] then
132                                         add_whitelist_rule(mac)
133                                 end
134                         elseif mac and cmd == "status" then
135                                 print(leased_macs[mac] and "lease"
136                                         or whitelist_macs[mac] and "whitelist"
137                                         or blacklist_macs[mac] and "blacklist"
138                                         or "new")
139                         elseif mac and ( cmd == "whitelist" or cmd == "blacklist" or cmd == "lease" ) then
140                                 if cmd ~= "lease" and leased_macs[mac] then
141                                         print("Removing %s from leases" % mac)
142                                         remove_lease(mac)
143                                         leased_macs[mac] = nil
144                                 end
145
146                                 if cmd ~= "whitelist" and whitelist_macs[mac] then
147                                         print("Removing %s from whitelist" % mac)
148                                         remove_whitelist(mac)
149                                         whitelist_macs[mac] = nil                                       
150                                 end
151
152                                 if cmd ~= "blacklist" and blacklist_macs[mac] then
153                                         print("Removing %s from blacklist" % mac)
154                                         remove_blacklist(mac)
155                                         blacklist_macs[mac] = nil
156                                 end
157
158                                 if cmd == "lease" and not leased_macs[mac] then
159                                         print("Adding %s to leases" % mac)
160                                         add_lease(mac)
161                                         leased_macs[mac] = true
162                                 elseif cmd == "whitelist" and not whitelist_macs[mac] then
163                                         print("Adding %s to whitelist" % mac)
164                                         add_whitelist(mac)
165                                         whitelist_macs[mac] = true
166                                 elseif cmd == "blacklist" and not blacklist_macs[mac] then
167                                         print("Adding %s to blacklist" % mac)
168                                         add_blacklist(mac)
169                                         blacklist_macs[mac] = true
170                                 else
171                                         print("The mac %s is already %sed" %{ mac, cmd })
172                                 end
173                         elseif mac and cmd == "remove" then
174                                 if leased_macs[mac] then
175                                         print("Removing %s from leases" % mac)
176                                         remove_lease(mac)
177                                         leased_macs[mac] = nil
178                                 elseif whitelist_macs[mac] then
179                                         print("Removing %s from whitelist" % mac)
180                                         remove_whitelist(mac)
181                                         whitelist_macs[mac] = nil                                       
182                                 elseif blacklist_macs[mac] then
183                                         print("Removing %s from blacklist" % mac)
184                                         remove_blacklist(mac)
185                                         blacklist_macs[mac] = nil
186                                 else
187                                         print("The mac %s is not known" % mac)
188                                 end
189                         else
190                                 print("Can not find mac for ip %s" % argv[i])
191                         end
192                 end
193
194                 unlock()
195                 os.exit(0)      
196         elseif cmd == "sync" then
197                 sync()
198                 os.exit(0)
199         elseif cmd == "list" then
200                 list()
201                 os.exit(0)
202         else
203                 print("Usage:")
204                 print("\n  luci-splash list\n    List connected, black- and whitelisted clients")
205                 print("\n  luci-splash sync\n    Synchronize firewall rules and clear expired leases")
206                 print("\n  luci-splash lease <MAC-or-IP>\n    Create a lease for the given address")
207                 print("\n  luci-splash blacklist <MAC-or-IP>\n    Add given address to blacklist")
208                 print("\n  luci-splash whitelist <MAC-or-IP>\n    Add given address to whitelist")
209                 print("\n  luci-splash remove <MAC-or-IP>\n    Remove given address from the lease-, black- or whitelist")
210                 print("")
211
212                 os.exit(1)      
213         end
214 end
215
216 -- Get current arp cache
217 function get_arpcache()
218         local arpcache = { }
219         for _, entry in ipairs(net.arptable()) do
220                 arpcache[entry["HW address"]:lower()] = { entry["Device"]:lower(), entry["IP address"]:lower() }
221         end
222         return arpcache
223 end
224
225 -- Get a list of known mac addresses
226 function get_known_macs(list)
227         local leased_macs = { }
228
229         if not list or list == "lease" then
230                 uci:foreach("luci_splash_leases", "lease",
231                         function(s) leased_macs[s.mac:lower()] = true end)
232         end
233
234         if not list or list == "whitelist" then
235                 uci:foreach("luci_splash", "whitelist",
236                         function(s) leased_macs[s.mac:lower()] = true end)
237         end
238
239         if not list or list == "blacklist" then
240                 uci:foreach("luci_splash", "blacklist",
241                         function(s) leased_macs[s.mac:lower()] = true end)
242         end
243
244         return leased_macs
245 end
246
247
248 -- Helper to delete iptables rules
249 function ipt_delete_all(args, comp, off)
250         off = off or { }
251         for i, r in ipairs(ipt:find(args)) do
252                 if comp == nil or comp(r) then
253                         off[r.table] = off[r.table] or { }
254                         off[r.table][r.chain] = off[r.table][r.chain] or 0
255
256                         exec("iptables -t %q -D %q %d 2>/dev/null"
257                                 %{ r.table, r.chain, r.index - off[r.table][r.chain] })
258
259                         off[r.table][r.chain] = off[r.table][r.chain] + 1
260                 end
261         end
262 end
263
264 function ipt6_delete_all(args, comp, off)
265         off = off or { }
266         for i, r in ipairs(ipt:find(args)) do
267                 if comp == nil or comp(r) then
268                         off[r.table] = off[r.table] or { }
269                         off[r.table][r.chain] = off[r.table][r.chain] or 0
270
271                         exec("ip6tables -t %q -D %q %d 2>/dev/null"
272                                 %{ r.table, r.chain, r.index - off[r.table][r.chain] })
273
274                         off[r.table][r.chain] = off[r.table][r.chain] + 1
275                 end
276         end
277 end
278
279
280 -- Convert mac to uci-compatible section name
281 function convert_mac_to_secname(mac)
282         return string.gsub(mac, ":", "")
283 end
284
285 -- Add a lease to state and invoke add_rule
286 function add_lease(mac, arp, no_uci)
287         mac = mac:lower()
288
289         -- Get current ip address
290         local ipaddr
291         for _, entry in ipairs(arp or net.arptable()) do
292                 if entry["HW address"]:lower() == mac then
293                         ipaddr = entry["IP address"]
294                         break
295                 end
296         end
297
298         -- Add lease if there is an ip addr
299         if ipaddr then
300                 local device = get_device_for_ip(ipaddr)
301                 if not no_uci then
302                         uci:section("luci_splash_leases", "lease", convert_mac_to_secname(mac), {
303                                 mac    = mac,
304                                 ipaddr = ipaddr,
305                                 device = device,
306                                 limit_up = limit_up,
307                                 limit_down = limit_down,
308                                 start  = os.time()
309                         })
310                         uci:save("luci_splash_leases")
311                 end
312                 add_lease_rule(mac, ipaddr, device)
313         else
314                 print("Found no active IP for %s, lease not added" % mac)
315         end
316 end
317
318
319 -- Remove a lease from state and invoke remove_rule
320 function remove_lease(mac)
321         mac = mac:lower()
322
323         uci:delete_all("luci_splash_leases", "lease",
324                 function(s)
325                         if s.mac:lower() == mac then
326                                 remove_lease_rule(mac, s.ipaddr, s.device, tonumber(s.limit_up), tonumber(s.limit_down))
327                                 return true
328                         end
329                         return false
330                 end)
331                 
332         uci:save("luci_splash_leases")
333 end
334
335
336 -- Add a whitelist entry
337 function add_whitelist(mac)
338         uci:section("luci_splash", "whitelist", convert_mac_to_secname(mac), { mac = mac })
339         uci:save("luci_splash")
340         uci:commit("luci_splash")
341         add_whitelist_rule(mac)
342 end
343
344
345 -- Add a blacklist entry
346 function add_blacklist(mac)
347         uci:section("luci_splash", "blacklist", convert_mac_to_secname(mac), { mac = mac })
348         uci:save("luci_splash")
349         uci:commit("luci_splash")
350         add_blacklist_rule(mac)
351 end
352
353
354 -- Remove a whitelist entry
355 function remove_whitelist(mac)
356         mac = mac:lower()
357         uci:delete_all("luci_splash", "whitelist",
358                 function(s) return not s.mac or s.mac:lower() == mac end)
359         uci:save("luci_splash")
360         uci:commit("luci_splash")
361         remove_lease_rule(mac)
362         remove_whitelist_tc(mac)
363 end
364
365 function remove_whitelist_tc(mac)
366         if debug then
367                 print("Removing whitelist filters for " .. mac)
368         end
369         uci:foreach("luci_splash", "iface", function(s)
370                 local device = get_physdev(s['.name'])
371                 local handle = get_filter_handle('ffff:', 'src', device, mac)
372                 exec('tc filter del dev "%s" parent ffff: protocol ip prio 1 handle %s u32' % { device, handle })
373                 local handle = get_filter_handle('1:', 'dest', device, mac)
374                 exec('tc filter del dev "%s" parent 1:0 protocol ip prio 1 handle %s u32' % { device, handle })
375         end)
376 end
377
378 -- Remove a blacklist entry
379 function remove_blacklist(mac)
380         mac = mac:lower()
381         uci:delete_all("luci_splash", "blacklist",
382                 function(s) return not s.mac or s.mac:lower() == mac end)
383         uci:save("luci_splash")
384         uci:commit("luci_splash")
385         remove_lease_rule(mac)
386 end
387
388
389 -- Add an iptables rule
390 function add_lease_rule(mac, ipaddr, device)
391         local id
392         if ipaddr then
393                 id = get_id(ipaddr)
394         end
395
396         exec("iptables -t mangle -I luci_splash_mark_out -m mac --mac-source %q -j RETURN" % mac)
397
398         if id and device then
399                 exec("iptables -t mangle -I luci_splash_mark_in -d %q -j MARK --set-mark 0x1%s -m comment --comment %s" % {ipaddr, id, mac:upper()})
400         end
401
402         if has_ipv6 then
403                 exec("ip6tables -t mangle -I luci_splash_mark_out -m mac --mac-source %q -j MARK --set-mark 79" % mac)
404                 -- not working yet, needs the ip6addr
405                 --exec("ip6tables -t mangle -I luci_splash_mark_in -d %q -j MARK --set-mark 80 -m comment --comment %s" % {ipaddr, mac:upper()})
406         end
407
408
409         if device and tonumber(limit_up) > 0 then
410                 exec('tc filter add dev "%s" parent ffff: protocol ip prio 2 u32 match ether src %s police rate %skbit mtu 6k burst 6k drop' % {device, mac, limit_up})
411         end
412
413         if id and device and tonumber(limit_down) > 0 then
414                 exec("tc class add dev %s parent 1: classid 1:0x%s htb rate %skbit" % { device, id, limit_down })
415                 exec("tc qdisc add dev %s parent 1:%s sfq perturb 10" % { device, id })
416         end
417
418         exec("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
419         exec("iptables -t nat    -I luci_splash_leases -m mac --mac-source %q -j RETURN" % mac)
420         if has_ipv6 then
421                 exec("ip6tables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
422         end
423 end
424
425
426 -- Remove lease, black- or whitelist rules
427 function remove_lease_rule(mac, ipaddr, device, limit_up, limit_down)
428         local id
429         if ipaddr then
430                 id = get_id(ipaddr)
431         end
432
433         ipt:resync()
434         ipt_delete_all({table="mangle", chain="luci_splash_mark_in", options={"/*", mac:upper()}})
435         ipt_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", mac:upper()}})
436         ipt_delete_all({table="filter", chain="luci_splash_filter",   options={"MAC", mac:upper()}})
437         ipt_delete_all({table="nat",    chain="luci_splash_leases",   options={"MAC", mac:upper()}})
438         if has_ipv6 then
439                 ipt6_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", mac:upper()}})
440                 ipt6_delete_all({table="filter", chain="luci_splash_filter",   options={"MAC", mac:upper()}})
441         end
442         if device and tonumber(limit_up) > 0 then
443                 local handle = get_filter_handle('ffff:', 'src', device, mac)
444                 if handle then
445                         exec('tc filter del dev "%s" parent ffff: protocol ip prio 2 handle %s u32 police rate %skbit mtu 6k burst 6k drop' % {device, handle, limit_up})
446                 end
447         end
448
449         -- remove clients class
450         if device and id then
451                 exec('tc class del dev "%s" classid 1:%s' % {device, id})
452                 exec('tc qdisc del dev "%s" parent 1:%s sfq perturb 10' % { device, id })
453         end
454
455 end
456
457
458 -- Add whitelist rules
459 function add_whitelist_rule(mac)
460         exec("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
461         exec("iptables -t nat    -I luci_splash_leases -m mac --mac-source %q -j RETURN" % mac)
462         if has_ipv6 then
463                 exec("ip6tables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
464         end
465         uci:foreach("luci_splash", "iface", function(s)
466                 local device = get_physdev(s['.name'])
467                 exec('tc filter add dev "%s" parent ffff: protocol ip prio 1 u32 match ether src %s police pass' % { device, mac })
468                 exec('tc filter add dev "%s" parent 1:0 protocol ip prio 1 u32 match ether dst %s classid 1:1' % { device, mac })
469         end)
470 end
471
472
473 -- Add blacklist rules
474 function add_blacklist_rule(mac)
475         exec("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j DROP" % mac)
476         if has_ipv6 then
477                 exec("ip6tables -t filter -I luci_splash_filter -m mac --mac-source %q -j DROP" % mac)
478         end
479 end
480
481
482 -- Synchronise leases, remove abandoned rules
483 function sync()
484         lock()
485
486         local time = os.time()
487
488         -- Current leases in state files
489         local leases = uci:get_all("luci_splash_leases")
490         
491         -- Convert leasetime to seconds
492         local leasetime = tonumber(uci:get("luci_splash", "general", "leasetime")) * 3600
493         
494         -- Clean state file
495         uci:load("luci_splash_leases")
496         uci:revert("luci_splash_leases")
497         
498         -- For all leases
499         local leasecount = 0
500         for k, v in pairs(leases) do
501                 if v[".type"] == "lease" then
502                         if os.difftime(time, tonumber(v.start)) > leasetime then
503                                 -- Remove expired
504                                 remove_lease_rule(v.mac, v.ipaddr, v.device, tonumber(v.limit_up), tonumber(v.limit_down))
505                         else
506                                 leasecount = count + 1
507                                 -- Rewrite state
508                                 uci:section("luci_splash_leases", "lease", convert_mac_to_secname(v.mac), {             
509                                         mac    = v.mac,
510                                         ipaddr = v.ipaddr,
511                                         device = v.device,
512                                         limit_up = limit_up,
513                                         limit_down = limit_down,
514                                         start  = v.start
515                                 })
516                         end
517                 end
518         end
519         
520         -- Get the mac addresses of current leases
521         local macs = get_known_macs()
522         local arpcache = get_arpcache()
523
524         local blackwhitelist = uci:get_all("luci_splash")
525         local whitelist_total = 0
526         local whitelist_online = 0
527         local blacklist_total = 0
528         local blacklist_online = 0
529
530         -- Whitelist, Blacklist
531         for _, s in utl.spairs(blackwhitelist,
532                 function(a,b) return blackwhitelist[a][".type"] > blackwhitelist[b][".type"] end
533         ) do
534                 if (s[".type"] == "whitelist") then
535                         whitelist_total = whitelist_total + 1
536                         if s.mac then
537                                 local mac = s.mac:lower()
538                                 if arpcache[mac] then
539                                         whitelist_online = whitelist_online + 1
540                                 end
541                         end
542                 end
543                 if (s[".type"] == "blacklist") then
544                         blacklist_total = blacklist_total + 1
545                         if s.mac then
546                                 local mac = s.mac:lower()
547                                 if arpcache[mac] then
548                                         blacklist_online = blacklist_online + 1
549                                 end
550                         end
551                 end
552         end
553
554         uci:section("luci_splash_leases", "stats", "stats", {
555                 leases    = leasecount,
556                 whitelisttotal = whitelist_total,
557                 whitelistonline = whitelist_online,
558                 blacklisttotal = blacklist_total,
559                 blacklistonline = blacklist_online,
560         })
561
562         uci:save("luci_splash_leases")
563
564         ipt:resync()
565
566         ipt_delete_all({table="filter", chain="luci_splash_filter", options={"MAC"}},
567                 function(r) return not macs[r.options[2]:lower()] end)
568         ipt_delete_all({table="nat", chain="luci_splash_leases", options={"MAC"}},
569                 function(r) return not macs[r.options[2]:lower()] end)
570         ipt_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", "MARK", "set"}},
571                 function(r) return not macs[r.options[2]:lower()] end)
572         ipt_delete_all({table="mangle", chain="luci_splash_mark_in", options={"/*", "MARK", "set"}},
573                 function(r) return not macs[r.options[2]:lower()] end)
574
575
576         if has_ipv6 then
577                 ipt6_delete_all({table="filter", chain="luci_splash_filter", options={"MAC"}},
578                         function(r) return not macs[r.options[2]:lower()] end)
579                 ipt6_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", "MARK", "set"}},
580                         function(r) return not macs[r.options[2]:lower()] end)
581         end
582
583         unlock()
584 end
585
586 -- Show client info
587 function list()
588         local arpcache = get_arpcache()
589         -- Find traffic usage
590         local function traffic(lease)
591                 local traffic_in  = 0
592                 local traffic_out = 0
593
594                 local rin  = ipt:find({table="mangle", chain="luci_splash_mark_in", destination=lease.ipaddr})
595                 local rout = ipt:find({table="mangle", chain="luci_splash_mark_out", options={"MAC", lease.mac:upper()}})
596
597                 if rin  and #rin  > 0 then traffic_in  = math.floor( rin[1].bytes / 1024) end
598                 if rout and #rout > 0 then traffic_out = math.floor(rout[1].bytes / 1024) end
599
600                 return traffic_in, traffic_out
601         end
602
603         -- Print listings
604         local leases = uci:get_all("luci_splash_leases")
605         local blackwhitelist = uci:get_all("luci_splash")
606
607         print(string.format(
608                 "%-17s  %-15s  %-9s  %-4s  %-7s  %20s",
609                 "MAC", "IP", "State", "Dur.", "Intf.", "Traffic down/up"
610         ))
611
612         -- Leases
613         for _, s in pairs(leases) do
614                 if s[".type"] == "lease" and s.mac then
615                         local ti, to = traffic(s)
616                         local mac = s.mac:lower()
617                         local arp = arpcache[mac]
618                         print(string.format(
619                                 "%-17s  %-15s  %-9s  %3dm  %-7s  %7dKB  %7dKB",
620                                 mac, s.ipaddr, "leased",
621                                 math.floor(( os.time() - tonumber(s.start) ) / 60),
622                                 arp and arp[1] or "?", ti, to
623                         ))
624                 end
625         end
626
627         -- Whitelist, Blacklist
628         for _, s in utl.spairs(blackwhitelist,
629                 function(a,b) return blackwhitelist[a][".type"] > blackwhitelist[b][".type"] end
630         ) do
631                 if (s[".type"] == "whitelist" or s[".type"] == "blacklist") and s.mac then
632                         local mac = s.mac:lower()
633                         local arp = arpcache[mac]
634                         print(string.format(
635                                 "%-17s  %-15s  %-9s  %4s  %-7s  %9s  %9s",
636                                 mac, arp and arp[2] or "?", s[".type"],
637                                 "- ", arp and arp[1] or "?", "-", "-"
638                         ))
639                 end
640         end
641 end
642
643 main(arg)