applications/luci-splash: Use a seperate config file for leases, #590
[project/luci.git] / applications / luci-splash / root / usr / sbin / luci-splash
1 #!/usr/bin/lua
2
3 require("luci.util")
4 require("luci.model.uci")
5 require("luci.sys")
6 require("luci.sys.iptparser")
7
8 -- Init state session
9 local uci = luci.model.uci.cursor_state()
10 local ipt = luci.sys.iptparser.IptParser()
11 local net = luci.sys.net
12 local fs = require "luci.fs"
13
14 local limit_up = 0
15 local limit_down = 0
16
17 local has_ipv6 = fs.access("/proc/net/ipv6_route") and fs.access("/usr/sbin/ip6tables")
18
19 function lock()
20 os.execute("lock /var/run/luci_splash.lock")
21 end
22
23 function unlock()
24 os.execute("lock -u /var/run/luci_splash.lock")
25 end
26
27 function main(argv)
28 local cmd = table.remove(argv, 1)
29 local arg = argv[1]
30
31 limit_up = tonumber(uci:get("luci_splash", "general", "limit_up")) or 0
32 limit_down = tonumber(uci:get("luci_splash", "general", "limit_down")) or 0
33
34 if ( cmd == "lease" or cmd == "add-rules" or cmd == "remove" or
35 cmd == "whitelist" or cmd == "blacklist" or cmd == "status" ) and #argv > 0
36 then
37 lock()
38
39 local arp_cache = net.arptable()
40 local leased_macs = get_known_macs("lease")
41 local blacklist_macs = get_known_macs("blacklist")
42 local whitelist_macs = get_known_macs("whitelist")
43
44 for i, adr in ipairs(argv) do
45 local mac = nil
46 if adr:find(":") then
47 mac = adr:lower()
48 else
49 for _, e in ipairs(arp_cache) do
50 if e["IP address"] == adr then
51 mac = e["HW address"]:lower()
52 break
53 end
54 end
55 end
56
57 if mac and cmd == "add-rules" then
58 if leased_macs[mac] then
59 add_lease(mac, arp_cache, true)
60 elseif blacklist_macs[mac] then
61 add_blacklist_rule(mac)
62 elseif whitelist_macs[mac] then
63 add_whitelist_rule(mac)
64 end
65 elseif mac and cmd == "status" then
66 print(leased_macs[mac] and "lease"
67 or whitelist_macs[mac] and "whitelist"
68 or blacklist_macs[mac] and "blacklist"
69 or "new")
70 elseif mac and ( cmd == "whitelist" or cmd == "blacklist" or cmd == "lease" ) then
71 if cmd ~= "lease" and leased_macs[mac] then
72 print("Removing %s from leases" % mac)
73 remove_lease(mac)
74 leased_macs[mac] = nil
75 end
76
77 if cmd ~= "whitelist" and whitelist_macs[mac] then
78 print("Removing %s from whitelist" % mac)
79 remove_whitelist(mac)
80 whitelist_macs[mac] = nil
81 end
82
83 if cmd ~= "blacklist" and blacklist_macs[mac] then
84 print("Removing %s from blacklist" % mac)
85 remove_blacklist(mac)
86 blacklist_macs[mac] = nil
87 end
88
89 if cmd == "lease" and not leased_macs[mac] then
90 print("Adding %s to leases" % mac)
91 add_lease(mac)
92 leased_macs[mac] = true
93 elseif cmd == "whitelist" and not whitelist_macs[mac] then
94 print("Adding %s to whitelist" % mac)
95 add_whitelist(mac)
96 whitelist_macs[mac] = true
97 elseif cmd == "blacklist" and not blacklist_macs[mac] then
98 print("Adding %s to blacklist" % mac)
99 add_blacklist(mac)
100 blacklist_macs[mac] = true
101 else
102 print("The mac %s is already %sed" %{ mac, cmd })
103 end
104 elseif mac and cmd == "remove" then
105 if leased_macs[mac] then
106 print("Removing %s from leases" % mac)
107 remove_lease(mac)
108 leased_macs[mac] = nil
109 elseif whitelist_macs[mac] then
110 print("Removing %s from whitelist" % mac)
111 remove_whitelist(mac)
112 whitelist_macs[mac] = nil
113 elseif blacklist_macs[mac] then
114 print("Removing %s from blacklist" % mac)
115 remove_blacklist(mac)
116 blacklist_macs[mac] = nil
117 else
118 print("The mac %s is not known" % mac)
119 end
120 else
121 print("Can not find mac for ip %s" % argv[i])
122 end
123 end
124
125 unlock()
126 os.exit(0)
127 elseif cmd == "sync" then
128 sync()
129 os.exit(0)
130 elseif cmd == "list" then
131 list()
132 os.exit(0)
133 else
134 print("Usage:")
135 print("\n luci-splash list\n List connected, black- and whitelisted clients")
136 print("\n luci-splash sync\n Synchronize firewall rules and clear expired leases")
137 print("\n luci-splash lease <MAC-or-IP>\n Create a lease for the given address")
138 print("\n luci-splash blacklist <MAC-or-IP>\n Add given address to blacklist")
139 print("\n luci-splash whitelist <MAC-or-IP>\n Add given address to whitelist")
140 print("\n luci-splash remove <MAC-or-IP>\n Remove given address from the lease-, black- or whitelist")
141 print("")
142
143 os.exit(1)
144 end
145 end
146
147 -- Get a list of known mac addresses
148 function get_known_macs(list)
149 local leased_macs = { }
150
151 if not list or list == "lease" then
152 uci:foreach("luci_splash_leases", "lease",
153 function(s) leased_macs[s.mac:lower()] = true end)
154 end
155
156 if not list or list == "whitelist" then
157 uci:foreach("luci_splash", "whitelist",
158 function(s) leased_macs[s.mac:lower()] = true end)
159 end
160
161 if not list or list == "blacklist" then
162 uci:foreach("luci_splash", "blacklist",
163 function(s) leased_macs[s.mac:lower()] = true end)
164 end
165
166 return leased_macs
167 end
168
169
170 -- Get a list of known ip addresses
171 function get_known_ips(macs, arp)
172 local leased_ips = { }
173 if not macs then macs = get_known_macs() end
174 for _, e in ipairs(arp or net.arptable()) do
175 if macs[e["HW address"]:lower()] then leased_ips[e["IP address"]] = true end
176 end
177 return leased_ips
178 end
179
180
181 -- Helper to delete iptables rules
182 function ipt_delete_all(args, comp, off)
183 off = off or { }
184 for i, r in ipairs(ipt:find(args)) do
185 if comp == nil or comp(r) then
186 off[r.table] = off[r.table] or { }
187 off[r.table][r.chain] = off[r.table][r.chain] or 0
188
189 os.execute("iptables -t %q -D %q %d 2>/dev/null"
190 %{ r.table, r.chain, r.index - off[r.table][r.chain] })
191
192 off[r.table][r.chain] = off[r.table][r.chain] + 1
193 end
194 end
195 end
196
197 function ipt6_delete_all(args, comp, off)
198 off = off or { }
199 for i, r in ipairs(ipt:find(args)) do
200 if comp == nil or comp(r) then
201 off[r.table] = off[r.table] or { }
202 off[r.table][r.chain] = off[r.table][r.chain] or 0
203
204 os.execute("ip6tables -t %q -D %q %d 2>/dev/null"
205 %{ r.table, r.chain, r.index - off[r.table][r.chain] })
206
207 off[r.table][r.chain] = off[r.table][r.chain] + 1
208 end
209 end
210 end
211
212
213 -- Convert mac to uci-compatible section name
214 function convert_mac_to_secname(mac)
215 return string.gsub(mac, ":", "")
216 end
217
218 -- Add a lease to state and invoke add_rule
219 function add_lease(mac, arp, no_uci)
220 mac = mac:lower()
221
222 -- Get current ip address
223 local ipaddr
224 for _, entry in ipairs(arp or net.arptable()) do
225 if entry["HW address"]:lower() == mac then
226 ipaddr = entry["IP address"]
227 break
228 end
229 end
230
231 -- Add lease if there is an ip addr
232 if ipaddr then
233 if not no_uci then
234 uci:section("luci_splash_leases", "lease", convert_mac_to_secname(mac), {
235 mac = mac,
236 ipaddr = ipaddr,
237 start = os.time()
238 })
239 uci:save("luci_splash_leases")
240 end
241 add_lease_rule(mac, ipaddr)
242 else
243 print("Found no active IP for %s, lease not added" % mac)
244 end
245 end
246
247
248 -- Remove a lease from state and invoke remove_rule
249 function remove_lease(mac)
250 mac = mac:lower()
251
252 uci:delete_all("luci_splash_leases", "lease",
253 function(s)
254 if s.mac:lower() == mac then
255 remove_lease_rule(mac, s.ipaddr)
256 return true
257 end
258 return false
259 end)
260
261 uci:save("luci_splash_leases")
262 end
263
264
265 -- Add a whitelist entry
266 function add_whitelist(mac)
267 uci:section("luci_splash", "whitelist", convert_mac_to_secname(mac), { mac = mac })
268 uci:save("luci_splash")
269 uci:commit("luci_splash")
270 add_whitelist_rule(mac)
271 end
272
273
274 -- Add a blacklist entry
275 function add_blacklist(mac)
276 uci:section("luci_splash", "blacklist", convert_mac_to_secname(mac), { mac = mac })
277 uci:save("luci_splash")
278 uci:commit("luci_splash")
279 add_blacklist_rule(mac)
280 end
281
282
283 -- Remove a whitelist entry
284 function remove_whitelist(mac)
285 mac = mac:lower()
286 uci:delete_all("luci_splash", "whitelist",
287 function(s) return not s.mac or s.mac:lower() == mac end)
288 uci:save("luci_splash")
289 uci:commit("luci_splash")
290 remove_lease_rule(mac)
291 end
292
293
294 -- Remove a blacklist entry
295 function remove_blacklist(mac)
296 mac = mac:lower()
297 uci:delete_all("luci_splash", "blacklist",
298 function(s) return not s.mac or s.mac:lower() == mac end)
299 uci:save("luci_splash")
300 uci:commit("luci_splash")
301 remove_lease_rule(mac)
302 end
303
304
305 -- Add an iptables rule
306 function add_lease_rule(mac, ipaddr)
307 os.execute("iptables -t mangle -I luci_splash_mark_out -m mac --mac-source %q -j MARK --set-mark 79" % mac)
308 os.execute("iptables -t mangle -I luci_splash_mark_in -d %q -j MARK --set-mark 80" % ipaddr)
309 if has_ipv6 then
310 os.execute("ip6tables -t mangle -I luci_splash_mark_out -m mac --mac-source %q -j MARK --set-mark 79" % mac)
311 os.execute("ip6tables -t mangle -I luci_splash_mark_in -d %q -j MARK --set-mark 80" % ipaddr)
312 end
313
314
315 os.execute("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
316 os.execute("iptables -t nat -I luci_splash_leases -m mac --mac-source %q -j RETURN" % mac)
317 if has_ipv6 then
318 os.execute("ip6tables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
319 end
320 end
321
322
323 -- Remove lease, black- or whitelist rules
324 function remove_lease_rule(mac, ipaddr)
325 ipt:resync()
326
327 if ipaddr then
328 ipt_delete_all({table="mangle", chain="luci_splash_mark_in", destination=ipaddr})
329 ipt_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", mac:upper()}})
330 end
331
332 ipt_delete_all({table="filter", chain="luci_splash_filter", options={"MAC", mac:upper()}})
333 ipt_delete_all({table="nat", chain="luci_splash_leases", options={"MAC", mac:upper()}})
334 if has_ipv6 then
335 ipt6_delete_all({table="filter", chain="luci_splash_filter", options={"MAC", mac:upper()}})
336 end
337 end
338
339
340 -- Add whitelist rules
341 function add_whitelist_rule(mac)
342 os.execute("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
343 os.execute("iptables -t nat -I luci_splash_leases -m mac --mac-source %q -j RETURN" % mac)
344 if has_ipv6 then
345 os.execute("ip6tables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
346 end
347 end
348
349
350 -- Add blacklist rules
351 function add_blacklist_rule(mac)
352 os.execute("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j DROP" % mac)
353 if has_ipv6 then
354 os.execute("ip6tables -t filter -I luci_splash_filter -m mac --mac-source %q -j DROP" % mac)
355 end
356 end
357
358
359 -- Synchronise leases, remove abandoned rules
360 function sync()
361 lock()
362
363 local time = os.time()
364
365 -- Current leases in state files
366 local leases = uci:get_all("luci_splash_leases")
367
368 -- Convert leasetime to seconds
369 local leasetime = tonumber(uci:get("luci_splash", "general", "leasetime")) * 3600
370
371 -- Clean state file
372 uci:load("luci_splash_leases")
373 uci:revert("luci_splash_leases")
374
375 -- For all leases
376 for k, v in pairs(leases) do
377 if v[".type"] == "lease" then
378 if os.difftime(time, tonumber(v.start)) > leasetime then
379 -- Remove expired
380 remove_lease_rule(v.mac, v.ipaddr)
381 else
382 -- Rewrite state
383 uci:section("luci_splash_leases", "lease", convert_mac_to_secname(v.mac), {
384 mac = v.mac,
385 ipaddr = v.ipaddr,
386 start = v.start
387 })
388 end
389 end
390 end
391
392 uci:save("luci_splash_leases")
393
394 -- Get current IPs and MAC addresses
395 local macs = get_known_macs()
396 local ips = get_known_ips(macs)
397
398 ipt:resync()
399
400 ipt_delete_all({table="filter", chain="luci_splash_filter", options={"MAC"}},
401 function(r) return not macs[r.options[2]:lower()] end)
402 ipt_delete_all({table="nat", chain="luci_splash_leases", options={"MAC"}},
403 function(r) return not macs[r.options[2]:lower()] end)
404 ipt_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", "MARK", "set"}},
405 function(r) return not macs[r.options[2]:lower()] end)
406 ipt_delete_all({table="mangle", chain="luci_splash_mark_in", options={"MARK", "set"}},
407 function(r) return not ips[r.destination] end)
408
409 if has_ipv6 then
410 ipt6_delete_all({table="filter", chain="luci_splash_filter", options={"MAC"}},
411 function(r) return not macs[r.options[2]:lower()] end)
412 ipt_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", "MARK", "set"}},
413 function(r) return not macs[r.options[2]:lower()] end)
414 ipt_delete_all({table="mangle", chain="luci_splash_mark_in", options={"MARK", "set"}},
415 function(r) return not ips[r.destination] end)
416 end
417
418 unlock()
419 end
420
421 -- Show client info
422 function list()
423 -- Get current arp cache
424 local arpcache = { }
425 for _, entry in ipairs(net.arptable()) do
426 arpcache[entry["HW address"]:lower()] = { entry["Device"]:lower(), entry["IP address"]:lower() }
427 end
428
429 -- Find traffic usage
430 local function traffic(lease)
431 local traffic_in = 0
432 local traffic_out = 0
433
434 local rin = ipt:find({table="mangle", chain="luci_splash_mark_in", destination=lease.ipaddr})
435 local rout = ipt:find({table="mangle", chain="luci_splash_mark_out", options={"MAC", lease.mac:upper()}})
436
437 if rin and #rin > 0 then traffic_in = math.floor( rin[1].bytes / 1024) end
438 if rout and #rout > 0 then traffic_out = math.floor(rout[1].bytes / 1024) end
439
440 return traffic_in, traffic_out
441 end
442
443 -- Print listings
444 local leases = uci:get_all("luci_splash_leases")
445
446 print(string.format(
447 "%-17s %-15s %-9s %-4s %-7s %20s",
448 "MAC", "IP", "State", "Dur.", "Intf.", "Traffic down/up"
449 ))
450
451 -- Leases
452 for _, s in pairs(leases) do
453 if s[".type"] == "lease" and s.mac then
454 local ti, to = traffic(s)
455 local mac = s.mac:lower()
456 local arp = arpcache[mac]
457 print(string.format(
458 "%-17s %-15s %-9s %3dm %-7s %7dKB %7dKB",
459 mac, s.ipaddr, "leased",
460 math.floor(( os.time() - tonumber(s.start) ) / 60),
461 arp and arp[1] or "?", ti, to
462 ))
463 end
464 end
465
466 -- Whitelist, Blacklist
467 for _, s in luci.util.spairs(leases,
468 function(a,b) return leases[a][".type"] > leases[b][".type"] end
469 ) do
470 if (s[".type"] == "whitelist" or s[".type"] == "blacklist") and s.mac then
471 local mac = s.mac:lower()
472 local arp = arpcache[mac]
473 print(string.format(
474 "%-17s %-15s %-9s %4s %-7s %9s %9s",
475 mac, arp and arp[2] or "?", s[".type"],
476 "- ", arp and arp[1] or "?", "-", "-"
477 ))
478 end
479 end
480 end
481
482 main(arg)