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