3 utl = require "luci.util"
4 sys = require "luci.sys"
6 require("luci.model.uci")
7 require("luci.sys.iptparser")
10 local uci = luci.model.uci.cursor_state()
11 local ipt = luci.sys.iptparser.IptParser()
13 local fs = require "luci.fs"
14 local ip = require "luci.ip"
18 local has_ipv6 = fs.access("/proc/net/ipv6_route") and fs.access("/usr/sbin/ip6tables")
21 os.execute("lock /var/run/luci_splash.lock")
25 os.execute("lock -u /var/run/luci_splash.lock")
29 local ret = sys.exec(cmd)
32 if ret and ret ~= "" then
39 local o3, o4 = ip:match("[0-9]+%.[0-9]+%.([0-9]+)%.([0-9]+)")
41 return string.format("%02X%s", tonumber(o3), "") .. string.format("%02X%s", tonumber(o4), "")
47 function update_stats(leased, whitelisted, whitelisttotal, blacklisted, blacklisttotal)
48 local leases = uci:get_all("luci_splash_leases", "stats")
49 uci:delete("luci_splash_leases", "stats")
50 uci:section("luci_splash_leases", "stats", "stats", {
51 leases = leased or (leases and leases.leases) or 0,
52 whitelisttotal = whitelisttotal or (leased and leases.whitelisttotal) or 0,
53 whitelistonline = whitelisted or (leases and leases.whitelistonline) or 0,
54 blacklisttotal = blacklisttotal or (leases and leases.blacklisttotal) or 0,
55 blacklistonline = blacklisted or (leases and leases.blacklistonline) or 0,
57 uci:save("luci_splash_leases")
60 function get_device_for_ip(ipaddr)
62 uci:foreach("network", "interface", function(s)
63 if s.ipaddr and s.netmask then
64 local network = ip.IPv4(s.ipaddr, s.netmask)
65 if network:contains(ip.IPv4(ipaddr)) then
66 -- this should be rewritten to luci functions if possible
67 dev = utl.trim(sys.exec(". /lib/functions/network.sh; network_get_device IFNAME '" .. s['.name'] .. "'; echo $IFNAME"))
74 function get_physdev(interface)
76 dev = utl.trim(sys.exec(". /lib/functions/network.sh; network_get_device IFNAME '" .. interface .. "'; echo $IFNAME"))
82 function get_filter_handle(parent, direction, device, mac)
83 local input = utl.split(sys.exec('/usr/sbin/tc filter show dev ' .. device .. ' parent ' .. parent) or {})
86 for k, v in pairs(input) do
87 handle = v:match('filter protocol ip pref %d+ u32 fh (%d*:%d*:%d*) order')
89 local mac, mac1, mac2, mac3, mac4, mac5, mac6
90 if direction == 'src' then
91 mac1, mac2, mac3, mac4 = input[k+1]:match('match ([%a%d][%a%d])([%a%d][%a%d])([%a%d][%a%d])([%a%d][%a%d])/ffffffff')
92 mac5, mac6 = input[k+2]:match('match ([%a%d][%a%d])([%a%d][%a%d])0000/ffff0000')
94 mac1, mac2 = input[k+1]:match('match 0000([%a%d][%a%d])([%a%d][%a%d])/0000ffff')
95 mac3, mac4, mac5, mac6 = input[k+2]:match('match ([%a%d][%a%d])([%a%d][%a%d])([%a%d][%a%d])([%a%d][%a%d])/ffffffff')
97 if mac1 and mac2 and mac3 and mac4 and mac5 and mac6 then
98 mac = "%s:%s:%s:%s:%s:%s" % { mac1, mac2, mac3, mac4, mac5, mac6 }
109 function macvalid(mac)
110 if mac and mac:match(
111 "^[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:" ..
112 "[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:" ..
113 "[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]$"
121 function ipvalid(ipaddr)
123 return ip.IPv4(ipaddr) and true or false
130 local cmd = table.remove(argv, 1)
133 limit_up = (tonumber(uci:get("luci_splash", "general", "limit_up")) or 0) * 8
134 limit_down = (tonumber(uci:get("luci_splash", "general", "limit_down")) or 0) * 8
136 if ( cmd == "lease" or cmd == "add-rules" or cmd == "remove" or
137 cmd == "whitelist" or cmd == "blacklist" or cmd == "status" ) and #argv > 0
139 if not (macvalid(arg) or ipvalid(arg)) then
140 print("Invalid argument. The second argument must " ..
141 "be a valid IPv4 or Mac Address.")
147 local arp_cache = net.arptable()
148 local leased_macs = get_known_macs("lease")
149 local blacklist_macs = get_known_macs("blacklist")
150 local whitelist_macs = get_known_macs("whitelist")
152 for i, adr in ipairs(argv) do
154 if adr:find(":") then
157 for _, e in ipairs(arp_cache) do
158 if e["IP address"] == adr then
159 mac = e["HW address"]:lower()
165 if mac and cmd == "add-rules" then
166 if leased_macs[mac] then
167 add_lease(mac, arp_cache, true)
168 elseif blacklist_macs[mac] then
169 add_blacklist_rule(mac)
170 elseif whitelist_macs[mac] then
171 add_whitelist_rule(mac)
173 elseif mac and cmd == "status" then
174 print(leased_macs[mac] and "lease"
175 or whitelist_macs[mac] and "whitelist"
176 or blacklist_macs[mac] and "blacklist"
178 elseif mac and ( cmd == "whitelist" or cmd == "blacklist" or cmd == "lease" ) then
179 if cmd ~= "lease" and leased_macs[mac] then
180 print("Removing %s from leases" % mac)
182 leased_macs[mac] = nil
185 if cmd ~= "whitelist" and whitelist_macs[mac] then
186 if cmd == "lease" then
187 print('%s is whitelisted. Remove it before you can lease it.' % mac)
189 print("Removing %s from whitelist" % mac)
190 remove_whitelist(mac)
191 whitelist_macs[mac] = nil
195 if cmd == "whitelist" and leased_macs[mac] then
196 print("Removing %s from leases" % mac)
198 leased_macs[mac] = nil
201 if cmd ~= "blacklist" and blacklist_macs[mac] then
202 print("Removing %s from blacklist" % mac)
203 remove_blacklist(mac)
204 blacklist_macs[mac] = nil
207 if cmd == "lease" and not leased_macs[mac] then
208 if not whitelist_macs[mac] then
209 print("Adding %s to leases" % mac)
211 leased_macs[mac] = true
213 elseif cmd == "whitelist" and not whitelist_macs[mac] then
214 print("Adding %s to whitelist" % mac)
216 whitelist_macs[mac] = true
217 elseif cmd == "blacklist" and not blacklist_macs[mac] then
218 print("Adding %s to blacklist" % mac)
220 blacklist_macs[mac] = true
222 print("The mac %s is already %sed" %{ mac, cmd })
224 elseif mac and cmd == "remove" then
225 if leased_macs[mac] then
226 print("Removing %s from leases" % mac)
228 leased_macs[mac] = nil
229 elseif whitelist_macs[mac] then
230 print("Removing %s from whitelist" % mac)
231 remove_whitelist(mac)
232 whitelist_macs[mac] = nil
233 elseif blacklist_macs[mac] then
234 print("Removing %s from blacklist" % mac)
235 remove_blacklist(mac)
236 blacklist_macs[mac] = nil
238 print("The mac %s is not known" % mac)
241 print("Can not find mac for ip %s" % argv[i])
247 elseif cmd == "sync" then
250 elseif cmd == "list" then
255 print("\n luci-splash list\n List connected, black- and whitelisted clients")
256 print("\n luci-splash sync\n Synchronize firewall rules and clear expired leases")
257 print("\n luci-splash lease <MAC-or-IP>\n Create a lease for the given address")
258 print("\n luci-splash blacklist <MAC-or-IP>\n Add given address to blacklist")
259 print("\n luci-splash whitelist <MAC-or-IP>\n Add given address to whitelist")
260 print("\n luci-splash remove <MAC-or-IP>\n Remove given address from the lease-, black- or whitelist")
267 -- Get current arp cache
268 function get_arpcache()
270 for _, entry in ipairs(net.arptable()) do
271 arpcache[entry["HW address"]:lower()] = { entry["Device"]:lower(), entry["IP address"]:lower() }
276 -- Get a list of known mac addresses
277 function get_known_macs(list)
278 local leased_macs = { }
280 if not list or list == "lease" then
281 uci:foreach("luci_splash_leases", "lease",
282 function(s) leased_macs[s.mac:lower()] = true end)
285 if not list or list == "whitelist" then
286 uci:foreach("luci_splash", "whitelist",
287 function(s) leased_macs[s.mac:lower()] = true end)
290 if not list or list == "blacklist" then
291 uci:foreach("luci_splash", "blacklist",
292 function(s) leased_macs[s.mac:lower()] = true end)
299 -- Helper to delete iptables rules
300 function ipt_delete_all(args, comp, off)
302 for i, r in ipairs(ipt:find(args)) do
303 if comp == nil or comp(r) then
304 off[r.table] = off[r.table] or { }
305 off[r.table][r.chain] = off[r.table][r.chain] or 0
307 exec("iptables -t %q -D %q %d 2>/dev/null"
308 %{ r.table, r.chain, r.index - off[r.table][r.chain] })
310 off[r.table][r.chain] = off[r.table][r.chain] + 1
315 function ipt6_delete_all(args, comp, off)
317 for i, r in ipairs(ipt:find(args)) do
318 if comp == nil or comp(r) then
319 off[r.table] = off[r.table] or { }
320 off[r.table][r.chain] = off[r.table][r.chain] or 0
322 exec("ip6tables -t %q -D %q %d 2>/dev/null"
323 %{ r.table, r.chain, r.index - off[r.table][r.chain] })
325 off[r.table][r.chain] = off[r.table][r.chain] + 1
331 -- Convert mac to uci-compatible section name
332 function convert_mac_to_secname(mac)
333 return string.gsub(mac, ":", "")
336 -- Add a lease to state and invoke add_rule
337 function add_lease(mac, arp, no_uci)
340 -- Get current ip address
342 for _, entry in ipairs(arp or net.arptable()) do
343 if entry["HW address"]:lower() == mac then
344 ipaddr = entry["IP address"]
349 -- Add lease if there is an ip addr
351 local device = get_device_for_ip(ipaddr)
353 local leased = uci:get("luci_splash_leases", "stats", "leases")
354 if type(tonumber(leased)) == "number" then
355 update_stats(leased + 1, nil, nil, nil, nil)
358 uci:section("luci_splash_leases", "lease", convert_mac_to_secname(mac), {
363 limit_down = limit_down,
366 uci:save("luci_splash_leases")
368 add_lease_rule(mac, ipaddr, device)
370 print("Found no active IP for %s, lease not added" % mac)
375 -- Remove a lease from state and invoke remove_rule
376 function remove_lease(mac)
379 uci:delete_all("luci_splash_leases", "lease",
381 if s.mac:lower() == mac then
382 remove_lease_rule(mac, s.ipaddr, s.device, tonumber(s.limit_up), tonumber(s.limit_down))
383 local leased = uci:get("luci_splash_leases", "stats", "leases")
384 if type(tonumber(leased)) == "number" and tonumber(leased) > 0 then
385 update_stats(leased - 1, nil, nil, nil, nil)
392 uci:save("luci_splash_leases")
396 -- Add a whitelist entry
397 function add_whitelist(mac)
398 uci:section("luci_splash", "whitelist", convert_mac_to_secname(mac), { mac = mac })
399 uci:save("luci_splash")
400 uci:commit("luci_splash")
401 add_whitelist_rule(mac)
405 -- Add a blacklist entry
406 function add_blacklist(mac)
407 uci:section("luci_splash", "blacklist", convert_mac_to_secname(mac), { mac = mac })
408 uci:save("luci_splash")
409 uci:commit("luci_splash")
410 add_blacklist_rule(mac)
414 -- Remove a whitelist entry
415 function remove_whitelist(mac)
417 uci:delete_all("luci_splash", "whitelist",
418 function(s) return not s.mac or s.mac:lower() == mac end)
419 uci:save("luci_splash")
420 uci:commit("luci_splash")
421 remove_lease_rule(mac)
422 remove_whitelist_tc(mac)
425 function remove_whitelist_tc(mac)
426 uci:foreach("luci_splash", "iface", function(s)
427 local device = get_physdev(s['.name'])
428 if device and device ~= "" then
430 print("Removing whitelist filters for %s interface %s." % {mac, device})
432 local handle = get_filter_handle('ffff:', 'src', device, mac)
434 exec('tc filter del dev "%s" parent ffff: protocol ip prio 1 handle %s u32' % { device, handle })
436 print('Warning! Could not get a handle for %s parent :ffff on interface %s' % { mac, device })
438 local handle = get_filter_handle('1:', 'dest', device, mac)
440 exec('tc filter del dev "%s" parent 1:0 protocol ip prio 1 handle %s u32' % { device, handle })
442 print('Warning! Could not get a handle for %s parent 1:0 on interface %s' % { mac, device })
448 -- Remove a blacklist entry
449 function remove_blacklist(mac)
451 uci:delete_all("luci_splash", "blacklist",
452 function(s) return not s.mac or s.mac:lower() == mac end)
453 uci:save("luci_splash")
454 uci:commit("luci_splash")
455 remove_lease_rule(mac)
459 -- Add an iptables rule
460 function add_lease_rule(mac, ipaddr, device)
466 exec("iptables -t mangle -I luci_splash_mark_out -m mac --mac-source %q -j RETURN" % mac)
468 if id and device then
469 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()})
473 exec("ip6tables -t mangle -I luci_splash_mark_out -m mac --mac-source %q -j MARK --set-mark 79" % mac)
474 -- not working yet, needs the ip6addr
475 --exec("ip6tables -t mangle -I luci_splash_mark_in -d %q -j MARK --set-mark 80 -m comment --comment %s" % {ipaddr, mac:upper()})
479 if device and tonumber(limit_up) > 0 then
480 exec('tc filter add dev "%s" parent ffff: protocol ip prio 2 u32 match ether src %s police rate %skbit mtu 6k burst 6k drop' % {device, mac, limit_up})
483 if id and device and tonumber(limit_down) > 0 then
484 exec("tc class add dev %s parent 1: classid 1:0x%s htb rate %skbit" % { device, id, limit_down })
485 exec("tc qdisc add dev %s parent 1:%s sfq perturb 10" % { device, id })
488 exec("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
489 exec("iptables -t nat -I luci_splash_leases -m mac --mac-source %q -j RETURN" % mac)
491 exec("ip6tables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
496 -- Remove lease, black- or whitelist rules
497 function remove_lease_rule(mac, ipaddr, device, limit_up, limit_down)
504 ipt_delete_all({table="mangle", chain="luci_splash_mark_in", options={"/*", mac:upper()}})
505 ipt_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", mac:upper()}})
506 ipt_delete_all({table="filter", chain="luci_splash_filter", options={"MAC", mac:upper()}})
507 ipt_delete_all({table="nat", chain="luci_splash_leases", options={"MAC", mac:upper()}})
509 ipt6_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", mac:upper()}})
510 ipt6_delete_all({table="filter", chain="luci_splash_filter", options={"MAC", mac:upper()}})
512 if device and tonumber(limit_up) > 0 then
513 local handle = get_filter_handle('ffff:', 'src', device, mac)
515 exec('tc filter del dev "%s" parent ffff: protocol ip prio 2 handle %s u32 police rate %skbit mtu 6k burst 6k drop' % {device, handle, limit_up})
517 print('Warning! Could not get a handle for %s parent :ffff on interface %s' % { mac, device })
521 -- remove clients class
522 if device and id then
523 exec('tc class del dev "%s" classid 1:%s' % {device, id})
524 exec('tc qdisc del dev "%s" parent 1:%s sfq perturb 10' % { device, id })
530 -- Add whitelist rules
531 function add_whitelist_rule(mac)
532 exec("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
533 exec("iptables -t nat -I luci_splash_leases -m mac --mac-source %q -j RETURN" % mac)
535 exec("ip6tables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
537 uci:foreach("luci_splash", "iface", function(s)
538 local device = get_physdev(s['.name'])
539 if device and device ~= "" then
540 exec('tc filter add dev "%s" parent ffff: protocol ip prio 1 u32 match ether src %s police pass' % { device, mac })
541 exec('tc filter add dev "%s" parent 1:0 protocol ip prio 1 u32 match ether dst %s classid 1:1' % { device, mac })
547 -- Add blacklist rules
548 function add_blacklist_rule(mac)
549 exec("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j DROP" % mac)
551 exec("ip6tables -t filter -I luci_splash_filter -m mac --mac-source %q -j DROP" % mac)
556 -- Synchronise leases, remove abandoned rules
560 local time = os.time()
562 -- Current leases in state files
563 local leases = uci:get_all("luci_splash_leases")
565 -- Convert leasetime to seconds
566 local leasetime = tonumber(uci:get("luci_splash", "general", "leasetime")) * 3600
569 uci:load("luci_splash_leases")
570 uci:revert("luci_splash_leases")
574 for k, v in pairs(leases) do
575 if v[".type"] == "lease" then
576 if os.difftime(time, tonumber(v.start)) > leasetime then
578 remove_lease_rule(v.mac, v.ipaddr, v.device, tonumber(v.limit_up), tonumber(v.limit_down))
580 leasecount = leasecount + 1
582 uci:section("luci_splash_leases", "lease", convert_mac_to_secname(v.mac), {
587 limit_down = limit_down,
594 -- Get the mac addresses of current leases
595 local macs = get_known_macs()
596 local arpcache = get_arpcache()
598 local blackwhitelist = uci:get_all("luci_splash")
599 local whitelist_total = 0
600 local whitelist_online = 0
601 local blacklist_total = 0
602 local blacklist_online = 0
604 -- Whitelist, Blacklist
605 for _, s in utl.spairs(blackwhitelist,
606 function(a,b) return blackwhitelist[a][".type"] > blackwhitelist[b][".type"] end
608 if (s[".type"] == "whitelist") then
609 whitelist_total = whitelist_total + 1
611 local mac = s.mac:lower()
612 if arpcache[mac] then
613 whitelist_online = whitelist_online + 1
617 if (s[".type"] == "blacklist") then
618 blacklist_total = blacklist_total + 1
620 local mac = s.mac:lower()
621 if arpcache[mac] then
622 blacklist_online = blacklist_online + 1
628 update_stats(leasecount, whitelist_online, whitelist_total, blacklist_online, blacklist_total)
630 uci:save("luci_splash_leases")
634 ipt_delete_all({table="filter", chain="luci_splash_filter", options={"MAC"}},
635 function(r) return not macs[r.options[2]:lower()] end)
636 ipt_delete_all({table="nat", chain="luci_splash_leases", options={"MAC"}},
637 function(r) return not macs[r.options[2]:lower()] end)
638 ipt_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", "MARK", "set"}},
639 function(r) return not macs[r.options[2]:lower()] end)
640 ipt_delete_all({table="mangle", chain="luci_splash_mark_in", options={"/*", "MARK", "set"}},
641 function(r) return not macs[r.options[2]:lower()] end)
645 ipt6_delete_all({table="filter", chain="luci_splash_filter", options={"MAC"}},
646 function(r) return not macs[r.options[2]:lower()] end)
647 ipt6_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", "MARK", "set"}},
648 function(r) return not macs[r.options[2]:lower()] end)
656 local arpcache = get_arpcache()
657 -- Find traffic usage
658 local function traffic(lease)
660 local traffic_out = 0
662 local rin = ipt:find({table="mangle", chain="luci_splash_mark_in", destination=lease.ipaddr})
663 local rout = ipt:find({table="mangle", chain="luci_splash_mark_out", options={"MAC", lease.mac:upper()}})
665 if rin and #rin > 0 then traffic_in = math.floor( rin[1].bytes / 1024) end
666 if rout and #rout > 0 then traffic_out = math.floor(rout[1].bytes / 1024) end
668 return traffic_in, traffic_out
672 local leases = uci:get_all("luci_splash_leases")
673 local blackwhitelist = uci:get_all("luci_splash")
676 "%-17s %-15s %-9s %-4s %-7s %20s",
677 "MAC", "IP", "State", "Dur.", "Intf.", "Traffic down/up"
681 for _, s in pairs(leases) do
682 if s[".type"] == "lease" and s.mac then
683 local ti, to = traffic(s)
684 local mac = s.mac:lower()
685 local arp = arpcache[mac]
687 "%-17s %-15s %-9s %3dm %-7s %7dKB %7dKB",
688 mac, s.ipaddr, "leased",
689 math.floor(( os.time() - tonumber(s.start) ) / 60),
690 arp and arp[1] or "?", ti, to
695 -- Whitelist, Blacklist
696 for _, s in utl.spairs(blackwhitelist,
697 function(a,b) return blackwhitelist[a][".type"] > blackwhitelist[b][".type"] end
699 if (s[".type"] == "whitelist" or s[".type"] == "blacklist") and s.mac then
700 local mac = s.mac:lower()
701 local arp = arpcache[mac]
703 "%-17s %-15s %-9s %4s %-7s %9s %9s",
704 mac, arp and arp[2] or "?", s[".type"],
705 "- ", arp and arp[1] or "?", "-", "-"