applications/luci-splash: add optional QoS rate limiting for splash clients
[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.iptparser")
6
7 -- Init state session
8 local uci = luci.model.uci.cursor_state()
9 local ipt = luci.sys.iptparser.IptParser()
10
11 local splash_interfaces = { }
12 local limit_up = 0
13 local limit_down = 0
14 local limit_down_burst = 0
15
16 function main(argv)
17         local cmd = argv[1]
18         local arg = argv[2]
19
20         limit_up = tonumber(uci:get("luci_splash", "general", "limit_up") or 0)
21         limit_down = tonumber(uci:get("luci_splash", "general", "limit_down") or 0)
22         limit_down_burst = tonumber(uci:get("luci_splash", "general", "limit_down_burst") or limit_down * 2)
23         
24         uci:foreach("luci_splash", "iface", function(s)
25                 if s.network then
26                         splash_interfaces[#splash_interfaces+1] = uci:get("network", s.network, "ifname")
27                 end
28         end)
29
30         if cmd == "status" and arg then
31                 if islisted("whitelist", arg) then
32                         print("whitelisted")
33                 elseif islisted("blacklist", arg) then
34                         print("blacklisted")
35                 else            
36                         local lease = haslease(arg)
37                         if lease and lease.kicked then
38                                 print("kicked")
39                         elseif lease then
40                                 print("lease")
41                         else
42                                 print("unknown")
43                         end
44                 end
45                 os.exit(0)
46         elseif cmd == "add" and arg then
47                 if not haslease(arg) then
48                         add_lease(arg)
49                 else
50                         print("already leased!")
51                         os.exit(2)
52                 end
53                 os.exit(0)
54         elseif cmd == "remove" and arg then
55                 remove_lease(arg)
56                 os.exit(0)              
57         elseif cmd == "sync" then
58                 sync()
59                 os.exit(0)
60         else
61                 print("Usage: " .. argv[0] .. " <status|add|remove|sync> [MAC]")
62                 os.exit(1)      
63         end
64 end
65
66 -- Add a lease to state and invoke add_rule
67 function add_lease(mac)
68         uci:section("luci_splash", "lease", nil, {
69                 mac = mac,
70                 start = os.time()
71         })
72         add_rule(mac)
73         
74         uci:save("luci_splash")
75 end
76
77
78 -- Remove a lease from state and invoke remove_rule
79 function remove_lease(mac)
80         mac = mac:lower()
81         remove_rule(mac)
82
83         uci:delete_all("luci_splash", "lease",
84                 function(s) return ( s.mac:lower() == mac ) end)
85                 
86         uci:save("luci_splash")
87 end
88
89
90 -- Add an iptables rule
91 function add_rule(mac)
92         local a, b, c, d, e, f = mac:match("(%w+):(%w+):(%w+):(%w+):(%w+):(%w+)")
93         local mac_pre  = "%s%s" %{ a, b }
94         local mac_post = "%s%s%s%s" %{ c, d, e, f }
95         local handle   = f
96
97         if limit_up > 0 and limit_down > 0 then
98                 os.execute("iptables -t mangle -I luci_splash_mark -m mac --mac-source %q -j MARK --set-mark 79" % mac)
99
100                 for _, i in ipairs(splash_interfaces) do
101                         os.execute("tc filter add dev %q parent 77:0 protocol ip prio 2 " % i .. 
102                                 "handle ::%q u32 " % handle ..
103                                 "match u16 0x0800 0xFFFF at -2 match u32 0x%q 0xFFFFFFFF at -12 " % mac_post ..
104                                 "match u16 0x%q 0xFFFF at -14 flowid 77:10" % mac_pre)
105                 end
106         end
107
108         os.execute("iptables -t filter -I luci_splash_counter -m mac --mac-source %q -j RETURN" % mac)
109         return os.execute("iptables -t nat -I luci_splash_leases -m mac --mac-source %q -j RETURN" % mac)
110 end
111
112
113 -- Remove an iptables rule
114 function remove_rule(mac)
115         local handle = mac:match("%w+:%w+:%w+:%w+:%w+:(%w+)")
116
117         local function ipt_delete_foreach(args)
118                 for _, r in ipairs(ipt:find(args)) do
119                         if r.options and #r.options >= 2 and r.options[1] == "MAC" and
120                            r.options[2]:lower() == mac:lower()
121                         then
122                                 os.execute("iptables -t %q -D %q -m mac --mac-source %q %s 2>/dev/null"
123                                         %{ r.table, r.chain, mac,
124                                                 r.target == "MARK" and "-j MARK --set-mark 79" or
125                                                         r.target and "-j %q" % r.target or "" })
126                         end
127                 end
128         end
129
130         ipt_delete_foreach({table="filter", chain="luci_splash_counter"})
131         ipt_delete_foreach({table="mangle", chain="luci_splash_mark"})
132         ipt_delete_foreach({table="nat", chain="luci_splash_leases"})
133
134         for _, i in ipairs(splash_interfaces) do
135                 os.execute("tc filter del dev %q parent 77:0 protocol ip prio 2 " % i .. 
136                         "handle 800::%q u32 2>/dev/null" % handle)
137         end
138
139         ipt:resync()
140 end
141
142
143 -- Check whether a MAC-Address is listed in the lease state list
144 function haslease(mac)
145         mac = mac:lower()
146         local lease = nil
147
148         uci:foreach("luci_splash", "lease",
149                 function (section)
150                         if section.mac:lower() == mac then
151                                 lease = section
152                         end
153                 end)
154
155         return lease
156 end
157
158
159 -- Check whether a MAC-Address is in given list
160 function islisted(what, mac)
161         mac = mac:lower()
162
163         uci:foreach("luci_splash", what,
164                 function (section)
165                         if section.mac:lower() == mac then
166                                 stat = true
167                                 return
168                         end
169                 end)
170
171         return false
172 end
173
174
175 -- Returns a list of MAC-Addresses for which a rule is existing
176 function listrules()
177         local macs = { }
178         for i, r in ipairs(ipt:find({table="nat", chain="luci_splash_leases"})) do
179                 if r.options and #r.options >= 2 and r.options[1] == "MAC" then
180                         macs[r.options[2]:lower()] = true
181                 end
182         end
183         return luci.util.keys(macs)
184 end
185
186
187 -- Synchronise leases, remove abandoned rules
188 function sync()
189         local written = {}
190         local time = os.time()
191         
192         -- Current leases in state files
193         local leases = uci:get_all("luci_splash")
194         
195         -- Convert leasetime to seconds
196         local leasetime = tonumber(uci:get("luci_splash", "general", "leasetime")) * 3600
197         
198         -- Clean state file
199         uci:load("luci_splash")
200         uci:revert("luci_splash")
201         
202         
203         -- For all leases
204         for k, v in pairs(leases) do
205                 if v[".type"] == "lease" then
206                         if os.difftime(time, tonumber(v.start)) > leasetime then
207                                 -- Remove expired
208                                 remove_rule(v.mac, v.kicked)
209                         else
210                                 -- Rewrite state
211                                 uci:section("luci_splash", "lease", nil, {              
212                                         mac    = v.mac,
213                                         start  = v.start,
214                                         kicked = v.kicked
215                                 })
216                                 written[v.mac:lower()] = 1
217                         end
218                 elseif v[".type"] == "whitelist" or v[".type"] == "blacklist" then
219                         written[v.mac:lower()] = 1
220                 end
221         end
222         
223         
224         -- Delete rules without state
225         for i, r in ipairs(listrules()) do
226                 if #r > 0 and not written[r:lower()] then
227                         remove_rule(r)
228                 end
229         end
230         
231         uci:save("luci_splash")
232 end
233
234 main(arg)