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