e1f8d4162edb39e1d1ddddb821f3d6e0d7cf738c
[project/luci.git] / applications / luci-splash / root / usr / sbin / luci-splash
1 #!/usr/bin/lua
2
3 utl = require "luci.util"
4 sys = require "luci.sys"
5
6 require("luci.model.uci")
7 require("luci.sys.iptparser")
8
9 -- Init state session
10 local uci = luci.model.uci.cursor_state()
11 local ipt = luci.sys.iptparser.IptParser()
12 local net = sys.net
13 local fs = require "luci.fs"
14 local ip = require "luci.ip"
15
16 local debug = false
17
18 local has_ipv6 = fs.access("/proc/net/ipv6_route") and fs.access("/usr/sbin/ip6tables")
19
20 function lock()
21         os.execute("lock /var/run/luci_splash.lock")
22 end
23
24 function unlock()
25         os.execute("lock -u /var/run/luci_splash.lock")
26 end
27
28 function exec(cmd)
29         local ret = sys.exec(cmd)
30         if debug then
31                 print('+ ' .. cmd)
32                 if ret and ret ~= "" then
33                         print(ret)
34                 end
35         end
36 end
37
38 function get_id(ip)
39         local o3, o4 = ip:match("[0-9]+%.[0-9]+%.([0-9]+)%.([0-9]+)")
40         if o3 and 04 then
41                 return string.format("%02X%s", tonumber(o3), "") .. string.format("%02X%s", tonumber(o4), "")
42         else
43                 return false
44         end
45 end
46
47 function get_device_for_ip(ipaddr)
48         local dev
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"))
55                         end
56                 end
57         end)
58         return dev
59 end
60
61 function get_physdev(interface)
62         local dev
63         dev = utl.trim(sys.exec(". /lib/functions/network.sh; network_get_device IFNAME '" ..  interface .. "'; echo $IFNAME"))
64         return dev
65 end
66
67
68
69 function get_filter_handle(parent, direction, device)
70         local input = utl.split(sys.exec('/usr/sbin/tc filter show dev ' .. device .. ' parent ' .. parent) or {})
71         local tbl = {}
72         local handle
73         for k, v in pairs(input) do
74                 handle = v:match('filter protocol ip pref %d+ u32 fh (%d*:%d*:%d*) order')
75                 if handle then
76                         local 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')
80                         else
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')
83                         end
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 }
86                                 tbl[mac] = handle
87                         end
88                 end
89         end
90         if tbl[mac:lower()] then
91                 handle = tbl[mac:lower()]
92         end
93         return handle
94 end
95
96 function main(argv)
97         local cmd = table.remove(argv, 1)
98         local arg = argv[1]
99
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
102
103         if ( cmd == "lease" or cmd == "add-rules" or cmd == "remove" or
104              cmd == "whitelist" or cmd == "blacklist" or cmd == "status" ) and #argv > 0
105         then
106                 lock()
107
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")
112
113                 for i, adr in ipairs(argv) do
114                         local mac = nil
115                         if adr:find(":") then
116                                 mac = adr:lower()
117                         else
118                                 for _, e in ipairs(arp_cache) do
119                                         if e["IP address"] == adr then
120                                                 mac = e["HW address"]:lower()
121                                                 break
122                                         end
123                                 end
124                         end
125
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)
133                                 end
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"
138                                         or "new")
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)
142                                         remove_lease(mac)
143                                         leased_macs[mac] = nil
144                                 end
145
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                                       
150                                 end
151
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
156                                 end
157
158                                 if cmd == "lease" and not leased_macs[mac] then
159                                         print("Adding %s to leases" % mac)
160                                         add_lease(mac)
161                                         leased_macs[mac] = true
162                                 elseif cmd == "whitelist" and not whitelist_macs[mac] then
163                                         print("Adding %s to whitelist" % mac)
164                                         add_whitelist(mac)
165                                         whitelist_macs[mac] = true
166                                 elseif cmd == "blacklist" and not blacklist_macs[mac] then
167                                         print("Adding %s to blacklist" % mac)
168                                         add_blacklist(mac)
169                                         blacklist_macs[mac] = true
170                                 else
171                                         print("The mac %s is already %sed" %{ mac, cmd })
172                                 end
173                         elseif mac and cmd == "remove" then
174                                 if leased_macs[mac] then
175                                         print("Removing %s from leases" % mac)
176                                         remove_lease(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
186                                 else
187                                         print("The mac %s is not known" % mac)
188                                 end
189                         else
190                                 print("Can not find mac for ip %s" % argv[i])
191                         end
192                 end
193
194                 unlock()
195                 os.exit(0)      
196         elseif cmd == "sync" then
197                 sync()
198                 os.exit(0)
199         elseif cmd == "list" then
200                 list()
201                 os.exit(0)
202         else
203                 print("Usage:")
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")
210                 print("")
211
212                 os.exit(1)      
213         end
214 end
215
216 -- Get a list of known mac addresses
217 function get_known_macs(list)
218         local leased_macs = { }
219
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)
223         end
224
225         if not list or list == "whitelist" then
226                 uci:foreach("luci_splash", "whitelist",
227                         function(s) leased_macs[s.mac:lower()] = true end)
228         end
229
230         if not list or list == "blacklist" then
231                 uci:foreach("luci_splash", "blacklist",
232                         function(s) leased_macs[s.mac:lower()] = true end)
233         end
234
235         return leased_macs
236 end
237
238
239 -- Helper to delete iptables rules
240 function ipt_delete_all(args, comp, off)
241         off = off or { }
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
246
247                         exec("iptables -t %q -D %q %d 2>/dev/null"
248                                 %{ r.table, r.chain, r.index - off[r.table][r.chain] })
249
250                         off[r.table][r.chain] = off[r.table][r.chain] + 1
251                 end
252         end
253 end
254
255 function ipt6_delete_all(args, comp, off)
256         off = off or { }
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
261
262                         exec("ip6tables -t %q -D %q %d 2>/dev/null"
263                                 %{ r.table, r.chain, r.index - off[r.table][r.chain] })
264
265                         off[r.table][r.chain] = off[r.table][r.chain] + 1
266                 end
267         end
268 end
269
270
271 -- Convert mac to uci-compatible section name
272 function convert_mac_to_secname(mac)
273         return string.gsub(mac, ":", "")
274 end
275
276 -- Add a lease to state and invoke add_rule
277 function add_lease(mac, arp, no_uci)
278         mac = mac:lower()
279
280         -- Get current ip address
281         local ipaddr
282         for _, entry in ipairs(arp or net.arptable()) do
283                 if entry["HW address"]:lower() == mac then
284                         ipaddr = entry["IP address"]
285                         break
286                 end
287         end
288
289         -- Add lease if there is an ip addr
290         if ipaddr then
291                 local device = get_device_for_ip(ipaddr)
292                 if not no_uci then
293                         uci:section("luci_splash_leases", "lease", convert_mac_to_secname(mac), {
294                                 mac    = mac,
295                                 ipaddr = ipaddr,
296                                 device = device,
297                                 limit_up = limit_up,
298                                 limit_down = limit_down,
299                                 start  = os.time()
300                         })
301                         uci:save("luci_splash_leases")
302                 end
303                 add_lease_rule(mac, ipaddr, device)
304         else
305                 print("Found no active IP for %s, lease not added" % mac)
306         end
307 end
308
309
310 -- Remove a lease from state and invoke remove_rule
311 function remove_lease(mac)
312         mac = mac:lower()
313
314         uci:delete_all("luci_splash_leases", "lease",
315                 function(s)
316                         if s.mac:lower() == mac then
317                                 remove_lease_rule(mac, s.ipaddr, s.device, tonumber(s.limit_up), tonumber(s.limit_down))
318                                 return true
319                         end
320                         return false
321                 end)
322                 
323         uci:save("luci_splash_leases")
324 end
325
326
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)
333 end
334
335
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)
342 end
343
344
345 -- Remove a whitelist entry
346 function remove_whitelist(mac)
347         mac = mac:lower()
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)
354 end
355
356 function remove_whitelist_tc(mac)
357         if debug then
358                 print("Removing whitelist filters for " .. mac)
359         end
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 })
366         end)
367 end
368
369 -- Remove a blacklist entry
370 function remove_blacklist(mac)
371         mac = mac:lower()
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)
377 end
378
379
380 -- Add an iptables rule
381 function add_lease_rule(mac, ipaddr, device)
382         local id
383         if ipaddr then
384                 id = get_id(ipaddr)
385         end
386
387         exec("iptables -t mangle -I luci_splash_mark_out -m mac --mac-source %q -j RETURN" % mac)
388
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()})
391         end
392
393         if has_ipv6 then
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()})
397         end
398
399
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})
402         end
403
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 })
407         end
408
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)
411         if has_ipv6 then
412                 exec("ip6tables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
413         end
414 end
415
416
417 -- Remove lease, black- or whitelist rules
418 function remove_lease_rule(mac, ipaddr, device, limit_up, limit_down)
419         local id
420         if ipaddr then
421                 id = get_id(ipaddr)
422         end
423
424         ipt:resync()
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()}})
429         if has_ipv6 then
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()}})
432         end
433         if device and tonumber(limit_up) > 0 then
434                 local handle = get_filter_handle('ffff:', 'src', device, mac)
435                 if handle then
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})
437                 end
438         end
439
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 })
444         end
445
446 end
447
448
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)
453         if has_ipv6 then
454                 exec("ip6tables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
455         end
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 })
460         end)
461 end
462
463
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)
467         if has_ipv6 then
468                 exec("ip6tables -t filter -I luci_splash_filter -m mac --mac-source %q -j DROP" % mac)
469         end
470 end
471
472
473 -- Synchronise leases, remove abandoned rules
474 function sync()
475         lock()
476
477         local time = os.time()
478
479         -- Current leases in state files
480         local leases = uci:get_all("luci_splash_leases")
481         
482         -- Convert leasetime to seconds
483         local leasetime = tonumber(uci:get("luci_splash", "general", "leasetime")) * 3600
484         
485         -- Clean state file
486         uci:load("luci_splash_leases")
487         uci:revert("luci_splash_leases")
488         
489         -- For all 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
493                                 -- Remove expired
494                                 remove_lease_rule(v.mac, v.ipaddr, v.device, tonumber(v.limit_up), tonumber(v.limit_down))
495                         else
496                                 -- Rewrite state
497                                 uci:section("luci_splash_leases", "lease", convert_mac_to_secname(v.mac), {             
498                                         mac    = v.mac,
499                                         ipaddr = v.ipaddr,
500                                         device = v.device,
501                                         limit_up = limit_up,
502                                         limit_down = limit_down,
503                                         start  = v.start
504                                 })
505                         end
506                 end
507         end
508
509         uci:save("luci_splash_leases")
510
511         -- Get the mac addresses of current leases
512         local macs = get_known_macs()
513
514         ipt:resync()
515
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)
524
525
526         if has_ipv6 then
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)
531         end
532
533         unlock()
534 end
535
536 -- Show client info
537 function list()
538         -- Get current arp cache
539         local arpcache = { }
540         for _, entry in ipairs(net.arptable()) do
541                 arpcache[entry["HW address"]:lower()] = { entry["Device"]:lower(), entry["IP address"]:lower() }
542         end
543
544         -- Find traffic usage
545         local function traffic(lease)
546                 local traffic_in  = 0
547                 local traffic_out = 0
548
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()}})
551
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
554
555                 return traffic_in, traffic_out
556         end
557
558         -- Print listings
559         local leases = uci:get_all("luci_splash_leases")
560
561         print(string.format(
562                 "%-17s  %-15s  %-9s  %-4s  %-7s  %20s",
563                 "MAC", "IP", "State", "Dur.", "Intf.", "Traffic down/up"
564         ))
565
566         -- Leases
567         for _, s in pairs(leases) do
568                 if s[".type"] == "lease" and s.mac then
569                         local ti, to = traffic(s)
570                         local mac = s.mac:lower()
571                         local arp = arpcache[mac]
572                         print(string.format(
573                                 "%-17s  %-15s  %-9s  %3dm  %-7s  %7dKB  %7dKB",
574                                 mac, s.ipaddr, "leased",
575                                 math.floor(( os.time() - tonumber(s.start) ) / 60),
576                                 arp and arp[1] or "?", ti, to
577                         ))
578                 end
579         end
580
581         -- Whitelist, Blacklist
582         for _, s in utl.spairs(leases,
583                 function(a,b) return leases[a][".type"] > leases[b][".type"] end
584         ) do
585                 if (s[".type"] == "whitelist" or s[".type"] == "blacklist") and s.mac then
586                         local mac = s.mac:lower()
587                         local arp = arpcache[mac]
588                         print(string.format(
589                                 "%-17s  %-15s  %-9s  %4s  %-7s  %9s  %9s",
590                                 mac, arp and arp[2] or "?", s[".type"],
591                                 "- ", arp and arp[1] or "?", "-", "-"
592                         ))
593                 end
594         end
595 end
596
597 main(arg)