1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
|
--[[
Iptables parser and query library
(c) 2008-2009 Jo-Philipp Wich <jow@openwrt.org>
(c) 2008-2009 Steven Barth <steven@midlink.org>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
$Id$
]]--
local luci = {}
luci.util = require "luci.util"
luci.sys = require "luci.sys"
luci.ip = require "luci.ip"
local tonumber, ipairs, table = tonumber, ipairs, table
--- LuCI iptables parser and query library
-- @cstyle instance
module("luci.sys.iptparser")
--- Create a new iptables parser object.
-- @class function
-- @name IptParser
-- @param family Number specifying the address family. 4 for IPv4, 6 for IPv6
-- @return IptParser instance
IptParser = luci.util.class()
function IptParser.__init__( self, family )
self._family = (tonumber(family) == 6) and 6 or 4
self._rules = { }
self._chains = { }
if self._family == 4 then
self._nulladdr = "0.0.0.0/0"
self._tables = { "filter", "nat", "mangle", "raw" }
self._command = "iptables -t %s --line-numbers -nxvL"
else
self._nulladdr = "::/0"
self._tables = { "filter", "mangle", "raw" }
self._command = "ip6tables -t %s --line-numbers -nxvL"
end
self:_parse_rules()
end
--- Find all firewall rules that match the given criteria. Expects a table with
-- search criteria as only argument. If args is nil or an empty table then all
-- rules will be returned.
--
-- The following keys in the args table are recognized:
-- <ul>
-- <li> table - Match rules that are located within the given table
-- <li> chain - Match rules that are located within the given chain
-- <li> target - Match rules with the given target
-- <li> protocol - Match rules that match the given protocol, rules with
-- protocol "all" are always matched
-- <li> source - Match rules with the given source, rules with source
-- "0.0.0.0/0" (::/0) are always matched
-- <li> destination - Match rules with the given destination, rules with
-- destination "0.0.0.0/0" (::/0) are always matched
-- <li> inputif - Match rules with the given input interface, rules
-- with input interface "*" (=all) are always matched
-- <li> outputif - Match rules with the given output interface, rules
-- with output interface "*" (=all) are always matched
-- <li> flags - Match rules that match the given flags, current
-- supported values are "-f" (--fragment)
-- and "!f" (! --fragment)
-- <li> options - Match rules containing all given options
-- </ul>
-- The return value is a list of tables representing the matched rules.
-- Each rule table contains the following fields:
-- <ul>
-- <li> index - The index number of the rule
-- <li> table - The table where the rule is located, can be one
-- of "filter", "nat" or "mangle"
-- <li> chain - The chain where the rule is located, e.g. "INPUT"
-- or "postrouting_wan"
-- <li> target - The rule target, e.g. "REJECT" or "DROP"
-- <li> protocol The matching protocols, e.g. "all" or "tcp"
-- <li> flags - Special rule options ("--", "-f" or "!f")
-- <li> inputif - Input interface of the rule, e.g. "eth0.0"
-- or "*" for all interfaces
-- <li> outputif - Output interface of the rule,e.g. "eth0.0"
-- or "*" for all interfaces
-- <li> source - The source ip range, e.g. "0.0.0.0/0" (::/0)
-- <li> destination - The destination ip range, e.g. "0.0.0.0/0" (::/0)
-- <li> options - A list of specific options of the rule,
-- e.g. { "reject-with", "tcp-reset" }
-- <li> packets - The number of packets matched by the rule
-- <li> bytes - The number of total bytes matched by the rule
-- </ul>
-- Example:
-- <pre>
-- ip = luci.sys.iptparser.IptParser()
-- result = ip.find( {
-- target="REJECT",
-- protocol="tcp",
-- options={ "reject-with", "tcp-reset" }
-- } )
-- </pre>
-- This will match all rules with target "-j REJECT",
-- protocol "-p tcp" (or "-p all")
-- and the option "--reject-with tcp-reset".
-- @params args Table containing the search arguments (optional)
-- @return Table of matching rule tables
function IptParser.find( self, args )
local args = args or { }
local rv = { }
args.source = args.source and self:_parse_addr(args.source)
args.destination = args.destination and self:_parse_addr(args.destination)
for i, rule in ipairs(self._rules) do
local match = true
-- match table
if not ( not args.table or args.table:lower() == rule.table ) then
match = false
end
-- match chain
if not ( match == true and (
not args.chain or args.chain == rule.chain
) ) then
match = false
end
-- match target
if not ( match == true and (
not args.target or args.target == rule.target
) ) then
match = false
end
-- match protocol
if not ( match == true and (
not args.protocol or rule.protocol == "all" or
args.protocol:lower() == rule.protocol
) ) then
match = false
end
-- match source
if not ( match == true and (
not args.source or rule.source == self._nulladdr or
self:_parse_addr(rule.source):contains(args.source)
) ) then
match = false
end
-- match destination
if not ( match == true and (
not args.destination or rule.destination == self._nulladdr or
self:_parse_addr(rule.destination):contains(args.destination)
) ) then
match = false
end
-- match input interface
if not ( match == true and (
not args.inputif or rule.inputif == "*" or
args.inputif == rule.inputif
) ) then
match = false
end
-- match output interface
if not ( match == true and (
not args.outputif or rule.outputif == "*" or
args.outputif == rule.outputif
) ) then
match = false
end
-- match flags (the "opt" column)
if not ( match == true and (
not args.flags or rule.flags == args.flags
) ) then
match = false
end
-- match specific options
if not ( match == true and (
not args.options or
self:_match_options( rule.options, args.options )
) ) then
match = false
end
-- insert match
if match == true then
rv[#rv+1] = rule
end
end
return rv
end
--- Rebuild the internal lookup table, for example when rules have changed
-- through external commands.
-- @return nothing
function IptParser.resync( self )
self._rules = { }
self._chain = nil
self:_parse_rules()
end
--- Find the names of all tables.
-- @return Table of table names.
function IptParser.tables( self )
return self._tables
end
--- Find the names of all chains within the given table name.
-- @param table String containing the table name
-- @return Table of chain names in the order they occur.
function IptParser.chains( self, table )
local lookup = { }
local chains = { }
for _, r in ipairs(self:find({table=table})) do
if not lookup[r.chain] then
lookup[r.chain] = true
chains[#chains+1] = r.chain
end
end
return chains
end
--- Return the given firewall chain within the given table name.
-- @param table String containing the table name
-- @param chain String containing the chain name
-- @return Table containing the fields "policy", "packets", "bytes"
-- and "rules". The "rules" field is a table of rule tables.
function IptParser.chain( self, table, chain )
return self._chains[table:lower()] and self._chains[table:lower()][chain]
end
--- Test whether the given target points to a custom chain.
-- @param target String containing the target action
-- @return Boolean indicating whether target is a custom chain.
function IptParser.is_custom_target( self, target )
for _, r in ipairs(self._rules) do
if r.chain == target then
return true
end
end
return false
end
-- [internal] Parse address according to family.
function IptParser._parse_addr( self, addr )
if self._family == 4 then
return luci.ip.IPv4(addr)
else
return luci.ip.IPv6(addr)
end
end
-- [internal] Parse iptables output from all tables.
function IptParser._parse_rules( self )
for i, tbl in ipairs(self._tables) do
self._chains[tbl] = { }
for i, rule in ipairs(luci.util.execl(self._command % tbl)) do
if rule:find( "^Chain " ) == 1 then
local crefs
local cname, cpol, cpkt, cbytes = rule:match(
"^Chain ([^%s]*) %(policy (%w+) " ..
"(%d+) packets, (%d+) bytes%)"
)
if not cname then
cname, crefs = rule:match(
"^Chain ([^%s]*) %((%d+) references%)"
)
end
self._chain = cname
self._chains[tbl][cname] = {
policy = cpol,
packets = tonumber(cpkt or 0),
bytes = tonumber(cbytes or 0),
references = tonumber(crefs or 0),
rules = { }
}
else
if rule:find("%d") == 1 then
local rule_parts = luci.util.split( rule, "%s+", nil, true )
local rule_details = { }
-- cope with rules that have no target assigned
if rule:match("^%d+%s+%d+%s+%d+%s%s") then
table.insert(rule_parts, 4, nil)
end
-- ip6tables opt column is usually zero-width
if self._family == 6 then
table.insert(rule_parts, 6, "--")
end
rule_details["table"] = tbl
rule_details["chain"] = self._chain
rule_details["index"] = tonumber(rule_parts[1])
rule_details["packets"] = tonumber(rule_parts[2])
rule_details["bytes"] = tonumber(rule_parts[3])
rule_details["target"] = rule_parts[4]
rule_details["protocol"] = rule_parts[5]
rule_details["flags"] = rule_parts[6]
rule_details["inputif"] = rule_parts[7]
rule_details["outputif"] = rule_parts[8]
rule_details["source"] = rule_parts[9]
rule_details["destination"] = rule_parts[10]
rule_details["options"] = { }
for i = 11, #rule_parts do
if #rule_parts[i] > 0 then
rule_details["options"][i-10] = rule_parts[i]
end
end
self._rules[#self._rules+1] = rule_details
self._chains[tbl][self._chain].rules[
#self._chains[tbl][self._chain].rules + 1
] = rule_details
end
end
end
end
self._chain = nil
end
-- [internal] Return true if optlist1 contains all elements of optlist 2.
-- Return false in all other cases.
function IptParser._match_options( self, o1, o2 )
-- construct a hashtable of first options list to speed up lookups
local oh = { }
for i, opt in ipairs( o1 ) do oh[opt] = true end
-- iterate over second options list
-- each string in o2 must be also present in o1
-- if o2 contains a string which is not found in o1 then return false
for i, opt in ipairs( o2 ) do
if not oh[opt] then
return false
end
end
return true
end
|