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 get_device_for_ip(ipaddr)
49 uci:foreach("network", "interface", function(s)
50 if s.ipaddr and s.netmask then
51 local network = ip.IPv4(s.ipaddr, s.netmask)
52 if network:contains(ip.IPv4(ipaddr)) then
53 -- this should be rewritten to luci functions if possible
54 dev = utl.trim(sys.exec(". /lib/functions/network.sh; network_get_device IFNAME '" .. s['.name'] .. "'; echo $IFNAME"))
61 function get_physdev(interface)
63 dev = utl.trim(sys.exec(". /lib/functions/network.sh; network_get_device IFNAME '" .. interface .. "'; echo $IFNAME"))
69 function get_filter_handle(parent, direction, device, mac)
70 local input = utl.split(sys.exec('/usr/sbin/tc filter show dev ' .. device .. ' parent ' .. parent) or {})
73 for k, v in pairs(input) do
74 handle = v:match('filter protocol ip pref %d+ u32 fh (%d*:%d*:%d*) order')
76 local mac, mac1, mac2, mac3, mac4, mac5, mac6
77 if direction == 'src' then
78 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')
79 mac5, mac6 = input[k+2]:match('match ([%a%d][%a%d])([%a%d][%a%d])0000/ffff0000')
81 mac1, mac2 = input[k+1]:match('match 0000([%a%d][%a%d])([%a%d][%a%d])/0000ffff')
82 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')
84 if mac1 and mac2 and mac3 and mac4 and mac5 and mac6 then
85 mac = "%s:%s:%s:%s:%s:%s" % { mac1, mac2, mac3, mac4, mac5, mac6 }
97 local cmd = table.remove(argv, 1)
100 limit_up = (tonumber(uci:get("luci_splash", "general", "limit_up")) or 0) * 8
101 limit_down = (tonumber(uci:get("luci_splash", "general", "limit_down")) or 0) * 8
103 if ( cmd == "lease" or cmd == "add-rules" or cmd == "remove" or
104 cmd == "whitelist" or cmd == "blacklist" or cmd == "status" ) and #argv > 0
108 local arp_cache = net.arptable()
109 local leased_macs = get_known_macs("lease")
110 local blacklist_macs = get_known_macs("blacklist")
111 local whitelist_macs = get_known_macs("whitelist")
113 for i, adr in ipairs(argv) do
115 if adr:find(":") then
118 for _, e in ipairs(arp_cache) do
119 if e["IP address"] == adr then
120 mac = e["HW address"]:lower()
126 if mac and cmd == "add-rules" then
127 if leased_macs[mac] then
128 add_lease(mac, arp_cache, true)
129 elseif blacklist_macs[mac] then
130 add_blacklist_rule(mac)
131 elseif whitelist_macs[mac] then
132 add_whitelist_rule(mac)
134 elseif mac and cmd == "status" then
135 print(leased_macs[mac] and "lease"
136 or whitelist_macs[mac] and "whitelist"
137 or blacklist_macs[mac] and "blacklist"
139 elseif mac and ( cmd == "whitelist" or cmd == "blacklist" or cmd == "lease" ) then
140 if cmd ~= "lease" and leased_macs[mac] then
141 print("Removing %s from leases" % mac)
143 leased_macs[mac] = nil
146 if cmd ~= "whitelist" and whitelist_macs[mac] then
147 print("Removing %s from whitelist" % mac)
148 remove_whitelist(mac)
149 whitelist_macs[mac] = nil
152 if cmd ~= "blacklist" and blacklist_macs[mac] then
153 print("Removing %s from blacklist" % mac)
154 remove_blacklist(mac)
155 blacklist_macs[mac] = nil
158 if cmd == "lease" and not leased_macs[mac] then
159 print("Adding %s to leases" % mac)
161 leased_macs[mac] = true
162 elseif cmd == "whitelist" and not whitelist_macs[mac] then
163 print("Adding %s to whitelist" % mac)
165 whitelist_macs[mac] = true
166 elseif cmd == "blacklist" and not blacklist_macs[mac] then
167 print("Adding %s to blacklist" % mac)
169 blacklist_macs[mac] = true
171 print("The mac %s is already %sed" %{ mac, cmd })
173 elseif mac and cmd == "remove" then
174 if leased_macs[mac] then
175 print("Removing %s from leases" % mac)
177 leased_macs[mac] = nil
178 elseif whitelist_macs[mac] then
179 print("Removing %s from whitelist" % mac)
180 remove_whitelist(mac)
181 whitelist_macs[mac] = nil
182 elseif blacklist_macs[mac] then
183 print("Removing %s from blacklist" % mac)
184 remove_blacklist(mac)
185 blacklist_macs[mac] = nil
187 print("The mac %s is not known" % mac)
190 print("Can not find mac for ip %s" % argv[i])
196 elseif cmd == "sync" then
199 elseif cmd == "list" then
204 print("\n luci-splash list\n List connected, black- and whitelisted clients")
205 print("\n luci-splash sync\n Synchronize firewall rules and clear expired leases")
206 print("\n luci-splash lease <MAC-or-IP>\n Create a lease for the given address")
207 print("\n luci-splash blacklist <MAC-or-IP>\n Add given address to blacklist")
208 print("\n luci-splash whitelist <MAC-or-IP>\n Add given address to whitelist")
209 print("\n luci-splash remove <MAC-or-IP>\n Remove given address from the lease-, black- or whitelist")
216 -- Get current arp cache
217 function get_arpcache()
219 for _, entry in ipairs(net.arptable()) do
220 arpcache[entry["HW address"]:lower()] = { entry["Device"]:lower(), entry["IP address"]:lower() }
225 -- Get a list of known mac addresses
226 function get_known_macs(list)
227 local leased_macs = { }
229 if not list or list == "lease" then
230 uci:foreach("luci_splash_leases", "lease",
231 function(s) leased_macs[s.mac:lower()] = true end)
234 if not list or list == "whitelist" then
235 uci:foreach("luci_splash", "whitelist",
236 function(s) leased_macs[s.mac:lower()] = true end)
239 if not list or list == "blacklist" then
240 uci:foreach("luci_splash", "blacklist",
241 function(s) leased_macs[s.mac:lower()] = true end)
248 -- Helper to delete iptables rules
249 function ipt_delete_all(args, comp, off)
251 for i, r in ipairs(ipt:find(args)) do
252 if comp == nil or comp(r) then
253 off[r.table] = off[r.table] or { }
254 off[r.table][r.chain] = off[r.table][r.chain] or 0
256 exec("iptables -t %q -D %q %d 2>/dev/null"
257 %{ r.table, r.chain, r.index - off[r.table][r.chain] })
259 off[r.table][r.chain] = off[r.table][r.chain] + 1
264 function ipt6_delete_all(args, comp, off)
266 for i, r in ipairs(ipt:find(args)) do
267 if comp == nil or comp(r) then
268 off[r.table] = off[r.table] or { }
269 off[r.table][r.chain] = off[r.table][r.chain] or 0
271 exec("ip6tables -t %q -D %q %d 2>/dev/null"
272 %{ r.table, r.chain, r.index - off[r.table][r.chain] })
274 off[r.table][r.chain] = off[r.table][r.chain] + 1
280 -- Convert mac to uci-compatible section name
281 function convert_mac_to_secname(mac)
282 return string.gsub(mac, ":", "")
285 -- Add a lease to state and invoke add_rule
286 function add_lease(mac, arp, no_uci)
289 -- Get current ip address
291 for _, entry in ipairs(arp or net.arptable()) do
292 if entry["HW address"]:lower() == mac then
293 ipaddr = entry["IP address"]
298 -- Add lease if there is an ip addr
300 local device = get_device_for_ip(ipaddr)
302 uci:section("luci_splash_leases", "lease", convert_mac_to_secname(mac), {
307 limit_down = limit_down,
310 uci:save("luci_splash_leases")
312 add_lease_rule(mac, ipaddr, device)
314 print("Found no active IP for %s, lease not added" % mac)
319 -- Remove a lease from state and invoke remove_rule
320 function remove_lease(mac)
323 uci:delete_all("luci_splash_leases", "lease",
325 if s.mac:lower() == mac then
326 remove_lease_rule(mac, s.ipaddr, s.device, tonumber(s.limit_up), tonumber(s.limit_down))
332 uci:save("luci_splash_leases")
336 -- Add a whitelist entry
337 function add_whitelist(mac)
338 uci:section("luci_splash", "whitelist", convert_mac_to_secname(mac), { mac = mac })
339 uci:save("luci_splash")
340 uci:commit("luci_splash")
341 add_whitelist_rule(mac)
345 -- Add a blacklist entry
346 function add_blacklist(mac)
347 uci:section("luci_splash", "blacklist", convert_mac_to_secname(mac), { mac = mac })
348 uci:save("luci_splash")
349 uci:commit("luci_splash")
350 add_blacklist_rule(mac)
354 -- Remove a whitelist entry
355 function remove_whitelist(mac)
357 uci:delete_all("luci_splash", "whitelist",
358 function(s) return not s.mac or s.mac:lower() == mac end)
359 uci:save("luci_splash")
360 uci:commit("luci_splash")
361 remove_lease_rule(mac)
362 remove_whitelist_tc(mac)
365 function remove_whitelist_tc(mac)
367 print("Removing whitelist filters for " .. mac)
369 uci:foreach("luci_splash", "iface", function(s)
370 local device = get_physdev(s['.name'])
371 local handle = get_filter_handle('ffff:', 'src', device, mac)
372 exec('tc filter del dev "%s" parent ffff: protocol ip prio 1 handle %s u32' % { device, handle })
373 local handle = get_filter_handle('1:', 'dest', device, mac)
374 exec('tc filter del dev "%s" parent 1:0 protocol ip prio 1 handle %s u32' % { device, handle })
378 -- Remove a blacklist entry
379 function remove_blacklist(mac)
381 uci:delete_all("luci_splash", "blacklist",
382 function(s) return not s.mac or s.mac:lower() == mac end)
383 uci:save("luci_splash")
384 uci:commit("luci_splash")
385 remove_lease_rule(mac)
389 -- Add an iptables rule
390 function add_lease_rule(mac, ipaddr, device)
396 exec("iptables -t mangle -I luci_splash_mark_out -m mac --mac-source %q -j RETURN" % mac)
398 if id and device then
399 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()})
403 exec("ip6tables -t mangle -I luci_splash_mark_out -m mac --mac-source %q -j MARK --set-mark 79" % mac)
404 -- not working yet, needs the ip6addr
405 --exec("ip6tables -t mangle -I luci_splash_mark_in -d %q -j MARK --set-mark 80 -m comment --comment %s" % {ipaddr, mac:upper()})
409 if device and tonumber(limit_up) > 0 then
410 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})
413 if id and device and tonumber(limit_down) > 0 then
414 exec("tc class add dev %s parent 1: classid 1:0x%s htb rate %skbit" % { device, id, limit_down })
415 exec("tc qdisc add dev %s parent 1:%s sfq perturb 10" % { device, id })
418 exec("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
419 exec("iptables -t nat -I luci_splash_leases -m mac --mac-source %q -j RETURN" % mac)
421 exec("ip6tables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
426 -- Remove lease, black- or whitelist rules
427 function remove_lease_rule(mac, ipaddr, device, limit_up, limit_down)
434 ipt_delete_all({table="mangle", chain="luci_splash_mark_in", options={"/*", mac:upper()}})
435 ipt_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", mac:upper()}})
436 ipt_delete_all({table="filter", chain="luci_splash_filter", options={"MAC", mac:upper()}})
437 ipt_delete_all({table="nat", chain="luci_splash_leases", options={"MAC", mac:upper()}})
439 ipt6_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", mac:upper()}})
440 ipt6_delete_all({table="filter", chain="luci_splash_filter", options={"MAC", mac:upper()}})
442 if device and tonumber(limit_up) > 0 then
443 local handle = get_filter_handle('ffff:', 'src', device, mac)
445 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})
449 -- remove clients class
450 if device and id then
451 exec('tc class del dev "%s" classid 1:%s' % {device, id})
452 exec('tc qdisc del dev "%s" parent 1:%s sfq perturb 10' % { device, id })
458 -- Add whitelist rules
459 function add_whitelist_rule(mac)
460 exec("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
461 exec("iptables -t nat -I luci_splash_leases -m mac --mac-source %q -j RETURN" % mac)
463 exec("ip6tables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
465 uci:foreach("luci_splash", "iface", function(s)
466 local device = get_physdev(s['.name'])
467 exec('tc filter add dev "%s" parent ffff: protocol ip prio 1 u32 match ether src %s police pass' % { device, mac })
468 exec('tc filter add dev "%s" parent 1:0 protocol ip prio 1 u32 match ether dst %s classid 1:1' % { device, mac })
473 -- Add blacklist rules
474 function add_blacklist_rule(mac)
475 exec("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j DROP" % mac)
477 exec("ip6tables -t filter -I luci_splash_filter -m mac --mac-source %q -j DROP" % mac)
482 -- Synchronise leases, remove abandoned rules
486 local time = os.time()
488 -- Current leases in state files
489 local leases = uci:get_all("luci_splash_leases")
491 -- Convert leasetime to seconds
492 local leasetime = tonumber(uci:get("luci_splash", "general", "leasetime")) * 3600
495 uci:load("luci_splash_leases")
496 uci:revert("luci_splash_leases")
500 for k, v in pairs(leases) do
501 if v[".type"] == "lease" then
502 if os.difftime(time, tonumber(v.start)) > leasetime then
504 remove_lease_rule(v.mac, v.ipaddr, v.device, tonumber(v.limit_up), tonumber(v.limit_down))
506 leasecount = count + 1
508 uci:section("luci_splash_leases", "lease", convert_mac_to_secname(v.mac), {
513 limit_down = limit_down,
520 -- Get the mac addresses of current leases
521 local macs = get_known_macs()
522 local arpcache = get_arpcache()
524 local blackwhitelist = uci:get_all("luci_splash")
525 local whitelist_total = 0
526 local whitelist_online = 0
527 local blacklist_total = 0
528 local blacklist_online = 0
530 -- Whitelist, Blacklist
531 for _, s in utl.spairs(blackwhitelist,
532 function(a,b) return blackwhitelist[a][".type"] > blackwhitelist[b][".type"] end
534 if (s[".type"] == "whitelist") then
535 whitelist_total = whitelist_total + 1
537 local mac = s.mac:lower()
538 if arpcache[mac] then
539 whitelist_online = whitelist_online + 1
543 if (s[".type"] == "blacklist") then
544 blacklist_total = blacklist_total + 1
546 local mac = s.mac:lower()
547 if arpcache[mac] then
548 blacklist_online = blacklist_online + 1
554 uci:section("luci_splash_leases", "stats", "stats", {
556 whitelisttotal = whitelist_total,
557 whitelistonline = whitelist_online,
558 blacklisttotal = blacklist_total,
559 blacklistonline = blacklist_online,
562 uci:save("luci_splash_leases")
566 ipt_delete_all({table="filter", chain="luci_splash_filter", options={"MAC"}},
567 function(r) return not macs[r.options[2]:lower()] end)
568 ipt_delete_all({table="nat", chain="luci_splash_leases", options={"MAC"}},
569 function(r) return not macs[r.options[2]:lower()] end)
570 ipt_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", "MARK", "set"}},
571 function(r) return not macs[r.options[2]:lower()] end)
572 ipt_delete_all({table="mangle", chain="luci_splash_mark_in", options={"/*", "MARK", "set"}},
573 function(r) return not macs[r.options[2]:lower()] end)
577 ipt6_delete_all({table="filter", chain="luci_splash_filter", options={"MAC"}},
578 function(r) return not macs[r.options[2]:lower()] end)
579 ipt6_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", "MARK", "set"}},
580 function(r) return not macs[r.options[2]:lower()] end)
588 local arpcache = get_arpcache()
589 -- Find traffic usage
590 local function traffic(lease)
592 local traffic_out = 0
594 local rin = ipt:find({table="mangle", chain="luci_splash_mark_in", destination=lease.ipaddr})
595 local rout = ipt:find({table="mangle", chain="luci_splash_mark_out", options={"MAC", lease.mac:upper()}})
597 if rin and #rin > 0 then traffic_in = math.floor( rin[1].bytes / 1024) end
598 if rout and #rout > 0 then traffic_out = math.floor(rout[1].bytes / 1024) end
600 return traffic_in, traffic_out
604 local leases = uci:get_all("luci_splash_leases")
605 local blackwhitelist = uci:get_all("luci_splash")
608 "%-17s %-15s %-9s %-4s %-7s %20s",
609 "MAC", "IP", "State", "Dur.", "Intf.", "Traffic down/up"
613 for _, s in pairs(leases) do
614 if s[".type"] == "lease" and s.mac then
615 local ti, to = traffic(s)
616 local mac = s.mac:lower()
617 local arp = arpcache[mac]
619 "%-17s %-15s %-9s %3dm %-7s %7dKB %7dKB",
620 mac, s.ipaddr, "leased",
621 math.floor(( os.time() - tonumber(s.start) ) / 60),
622 arp and arp[1] or "?", ti, to
627 -- Whitelist, Blacklist
628 for _, s in utl.spairs(blackwhitelist,
629 function(a,b) return blackwhitelist[a][".type"] > blackwhitelist[b][".type"] end
631 if (s[".type"] == "whitelist" or s[".type"] == "blacklist") and s.mac then
632 local mac = s.mac:lower()
633 local arp = arpcache[mac]
635 "%-17s %-15s %-9s %4s %-7s %9s %9s",
636 mac, arp and arp[2] or "?", s[".type"],
637 "- ", arp and arp[1] or "?", "-", "-"