summaryrefslogtreecommitdiffhomepage
path: root/modules/luci-base/luasrc/sys/iptparser.lua
blob: 6715937c69fb0e0c5f49b5b8423e048fce742cde (plain)
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