applications/luci-splash: Always insert the mangling rules, not just when bandwidth...
[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", "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", "lease", convert_mac_to_secname(mac), {
235                                 mac    = mac,
236                                 ipaddr = ipaddr,
237                                 start  = os.time()
238                         })
239                         uci:save("luci_splash")
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", "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")
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")
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")
373         uci:revert("luci_splash")
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", "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")
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")
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)