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