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 a list of known mac addresses
217 function get_known_macs(list)
218 local leased_macs = { }
220 if not list or list == "lease" then
221 uci:foreach("luci_splash_leases", "lease",
222 function(s) leased_macs[s.mac:lower()] = true end)
225 if not list or list == "whitelist" then
226 uci:foreach("luci_splash", "whitelist",
227 function(s) leased_macs[s.mac:lower()] = true end)
230 if not list or list == "blacklist" then
231 uci:foreach("luci_splash", "blacklist",
232 function(s) leased_macs[s.mac:lower()] = true end)
239 -- Helper to delete iptables rules
240 function ipt_delete_all(args, comp, off)
242 for i, r in ipairs(ipt:find(args)) do
243 if comp == nil or comp(r) then
244 off[r.table] = off[r.table] or { }
245 off[r.table][r.chain] = off[r.table][r.chain] or 0
247 exec("iptables -t %q -D %q %d 2>/dev/null"
248 %{ r.table, r.chain, r.index - off[r.table][r.chain] })
250 off[r.table][r.chain] = off[r.table][r.chain] + 1
255 function ipt6_delete_all(args, comp, off)
257 for i, r in ipairs(ipt:find(args)) do
258 if comp == nil or comp(r) then
259 off[r.table] = off[r.table] or { }
260 off[r.table][r.chain] = off[r.table][r.chain] or 0
262 exec("ip6tables -t %q -D %q %d 2>/dev/null"
263 %{ r.table, r.chain, r.index - off[r.table][r.chain] })
265 off[r.table][r.chain] = off[r.table][r.chain] + 1
271 -- Convert mac to uci-compatible section name
272 function convert_mac_to_secname(mac)
273 return string.gsub(mac, ":", "")
276 -- Add a lease to state and invoke add_rule
277 function add_lease(mac, arp, no_uci)
280 -- Get current ip address
282 for _, entry in ipairs(arp or net.arptable()) do
283 if entry["HW address"]:lower() == mac then
284 ipaddr = entry["IP address"]
289 -- Add lease if there is an ip addr
291 local device = get_device_for_ip(ipaddr)
293 uci:section("luci_splash_leases", "lease", convert_mac_to_secname(mac), {
298 limit_down = limit_down,
301 uci:save("luci_splash_leases")
303 add_lease_rule(mac, ipaddr, device)
305 print("Found no active IP for %s, lease not added" % mac)
310 -- Remove a lease from state and invoke remove_rule
311 function remove_lease(mac)
314 uci:delete_all("luci_splash_leases", "lease",
316 if s.mac:lower() == mac then
317 remove_lease_rule(mac, s.ipaddr, s.device, tonumber(s.limit_up), tonumber(s.limit_down))
323 uci:save("luci_splash_leases")
327 -- Add a whitelist entry
328 function add_whitelist(mac)
329 uci:section("luci_splash", "whitelist", convert_mac_to_secname(mac), { mac = mac })
330 uci:save("luci_splash")
331 uci:commit("luci_splash")
332 add_whitelist_rule(mac)
336 -- Add a blacklist entry
337 function add_blacklist(mac)
338 uci:section("luci_splash", "blacklist", convert_mac_to_secname(mac), { mac = mac })
339 uci:save("luci_splash")
340 uci:commit("luci_splash")
341 add_blacklist_rule(mac)
345 -- Remove a whitelist entry
346 function remove_whitelist(mac)
348 uci:delete_all("luci_splash", "whitelist",
349 function(s) return not s.mac or s.mac:lower() == mac end)
350 uci:save("luci_splash")
351 uci:commit("luci_splash")
352 remove_lease_rule(mac)
353 remove_whitelist_tc(mac)
356 function remove_whitelist_tc(mac)
358 print("Removing whitelist filters for " .. mac)
360 uci:foreach("luci_splash", "iface", function(s)
361 local device = get_physdev(s['.name'])
362 local handle = get_filter_handle('ffff:', 'src', device, mac)
363 exec('tc filter del dev "%s" parent ffff: protocol ip prio 1 handle %s u32' % { device, handle })
364 local handle = get_filter_handle('1:', 'dest', device, mac)
365 exec('tc filter del dev "%s" parent 1:0 protocol ip prio 1 handle %s u32' % { device, handle })
369 -- Remove a blacklist entry
370 function remove_blacklist(mac)
372 uci:delete_all("luci_splash", "blacklist",
373 function(s) return not s.mac or s.mac:lower() == mac end)
374 uci:save("luci_splash")
375 uci:commit("luci_splash")
376 remove_lease_rule(mac)
380 -- Add an iptables rule
381 function add_lease_rule(mac, ipaddr, device)
387 exec("iptables -t mangle -I luci_splash_mark_out -m mac --mac-source %q -j RETURN" % mac)
389 if id and device then
390 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()})
394 exec("ip6tables -t mangle -I luci_splash_mark_out -m mac --mac-source %q -j MARK --set-mark 79" % mac)
395 -- not working yet, needs the ip6addr
396 --exec("ip6tables -t mangle -I luci_splash_mark_in -d %q -j MARK --set-mark 80 -m comment --comment %s" % {ipaddr, mac:upper()})
400 if device and tonumber(limit_up) > 0 then
401 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})
404 if id and device and tonumber(limit_down) > 0 then
405 exec("tc class add dev %s parent 1: classid 1:0x%s htb rate %skbit" % { device, id, limit_down })
406 exec("tc qdisc add dev %s parent 1:%s sfq perturb 10" % { device, id })
409 exec("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
410 exec("iptables -t nat -I luci_splash_leases -m mac --mac-source %q -j RETURN" % mac)
412 exec("ip6tables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
417 -- Remove lease, black- or whitelist rules
418 function remove_lease_rule(mac, ipaddr, device, limit_up, limit_down)
425 ipt_delete_all({table="mangle", chain="luci_splash_mark_in", options={"/*", mac:upper()}})
426 ipt_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", mac:upper()}})
427 ipt_delete_all({table="filter", chain="luci_splash_filter", options={"MAC", mac:upper()}})
428 ipt_delete_all({table="nat", chain="luci_splash_leases", options={"MAC", mac:upper()}})
430 ipt6_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", mac:upper()}})
431 ipt6_delete_all({table="filter", chain="luci_splash_filter", options={"MAC", mac:upper()}})
433 if device and tonumber(limit_up) > 0 then
434 local handle = get_filter_handle('ffff:', 'src', device, mac)
436 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})
440 -- remove clients class
441 if device and id then
442 exec('tc class del dev "%s" classid 1:%s' % {device, id})
443 exec('tc qdisc del dev "%s" parent 1:%s sfq perturb 10' % { device, id })
449 -- Add whitelist rules
450 function add_whitelist_rule(mac)
451 exec("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
452 exec("iptables -t nat -I luci_splash_leases -m mac --mac-source %q -j RETURN" % mac)
454 exec("ip6tables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
456 uci:foreach("luci_splash", "iface", function(s)
457 local device = get_physdev(s['.name'])
458 exec('tc filter add dev "%s" parent ffff: protocol ip prio 1 u32 match ether src %s police pass' % { device, mac })
459 exec('tc filter add dev "%s" parent 1:0 protocol ip prio 1 u32 match ether dst %s classid 1:1' % { device, mac })
464 -- Add blacklist rules
465 function add_blacklist_rule(mac)
466 exec("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j DROP" % mac)
468 exec("ip6tables -t filter -I luci_splash_filter -m mac --mac-source %q -j DROP" % mac)
473 -- Synchronise leases, remove abandoned rules
477 local time = os.time()
479 -- Current leases in state files
480 local leases = uci:get_all("luci_splash_leases")
482 -- Convert leasetime to seconds
483 local leasetime = tonumber(uci:get("luci_splash", "general", "leasetime")) * 3600
486 uci:load("luci_splash_leases")
487 uci:revert("luci_splash_leases")
490 for k, v in pairs(leases) do
491 if v[".type"] == "lease" then
492 if os.difftime(time, tonumber(v.start)) > leasetime then
494 remove_lease_rule(v.mac, v.ipaddr, v.device, tonumber(v.limit_up), tonumber(v.limit_down))
497 uci:section("luci_splash_leases", "lease", convert_mac_to_secname(v.mac), {
502 limit_down = limit_down,
509 uci:save("luci_splash_leases")
511 -- Get the mac addresses of current leases
512 local macs = get_known_macs()
516 ipt_delete_all({table="filter", chain="luci_splash_filter", options={"MAC"}},
517 function(r) return not macs[r.options[2]:lower()] end)
518 ipt_delete_all({table="nat", chain="luci_splash_leases", options={"MAC"}},
519 function(r) return not macs[r.options[2]:lower()] end)
520 ipt_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", "MARK", "set"}},
521 function(r) return not macs[r.options[2]:lower()] end)
522 ipt_delete_all({table="mangle", chain="luci_splash_mark_in", options={"/*", "MARK", "set"}},
523 function(r) return not macs[r.options[2]:lower()] end)
527 ipt6_delete_all({table="filter", chain="luci_splash_filter", options={"MAC"}},
528 function(r) return not macs[r.options[2]:lower()] end)
529 ipt6_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", "MARK", "set"}},
530 function(r) return not macs[r.options[2]:lower()] end)
538 -- Get current arp cache
540 for _, entry in ipairs(net.arptable()) do
541 arpcache[entry["HW address"]:lower()] = { entry["Device"]:lower(), entry["IP address"]:lower() }
544 -- Find traffic usage
545 local function traffic(lease)
547 local traffic_out = 0
549 local rin = ipt:find({table="mangle", chain="luci_splash_mark_in", destination=lease.ipaddr})
550 local rout = ipt:find({table="mangle", chain="luci_splash_mark_out", options={"MAC", lease.mac:upper()}})
552 if rin and #rin > 0 then traffic_in = math.floor( rin[1].bytes / 1024) end
553 if rout and #rout > 0 then traffic_out = math.floor(rout[1].bytes / 1024) end
555 return traffic_in, traffic_out
559 local leases = uci:get_all("luci_splash_leases")
560 local blackwhitelist = uci:get_all("luci_splash")
563 "%-17s %-15s %-9s %-4s %-7s %20s",
564 "MAC", "IP", "State", "Dur.", "Intf.", "Traffic down/up"
568 for _, s in pairs(leases) do
569 if s[".type"] == "lease" and s.mac then
570 local ti, to = traffic(s)
571 local mac = s.mac:lower()
572 local arp = arpcache[mac]
574 "%-17s %-15s %-9s %3dm %-7s %7dKB %7dKB",
575 mac, s.ipaddr, "leased",
576 math.floor(( os.time() - tonumber(s.start) ) / 60),
577 arp and arp[1] or "?", ti, to
582 -- Whitelist, Blacklist
583 for _, s in utl.spairs(blackwhitelist,
584 function(a,b) return blackwhitelist[a][".type"] > blackwhitelist[b][".type"] end
586 if (s[".type"] == "whitelist" or s[".type"] == "blacklist") and s.mac then
587 local mac = s.mac:lower()
588 local arp = arpcache[mac]
590 "%-17s %-15s %-9s %4s %-7s %9s %9s",
591 mac, arp and arp[2] or "?", s[".type"],
592 "- ", arp and arp[1] or "?", "-", "-"