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