iptables-ruby 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/CHANGELOG +23 -0
- data/Gemfile +6 -0
- data/Generate.md +278 -0
- data/LICENSE +20 -0
- data/README.md +140 -0
- data/Rakefile +44 -0
- data/bin/check_firewall.rb +220 -0
- data/examples/policy/macros.json +92 -0
- data/examples/policy/policy.json +208 -0
- data/examples/policy/policy6.json +8 -0
- data/examples/policy/primitives.json +40 -0
- data/examples/policy/rules.json +30 -0
- data/examples/policy/services.json +81 -0
- data/lib/iptables.rb +5 -0
- data/lib/iptables/configuration.rb +116 -0
- data/lib/iptables/expansions.rb +189 -0
- data/lib/iptables/logger.rb +5 -0
- data/lib/iptables/primitives.rb +51 -0
- data/lib/iptables/tables.rb +851 -0
- data/test/common.rb +22 -0
- data/test/tc_all.rb +7 -0
- data/test/tc_comparison.rb +751 -0
- data/test/tc_configuration.rb +72 -0
- data/test/tc_expansions.rb +116 -0
- data/test/tc_primitives.rb +35 -0
- data/test/tc_tables.rb +652 -0
- metadata +94 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'iptables/logger'
|
2
|
+
|
3
|
+
module IPTables
|
4
|
+
class Primitives
|
5
|
+
attr_reader :children
|
6
|
+
def initialize(primitives_hash)
|
7
|
+
@children = {}
|
8
|
+
raise "expected Hash" unless primitives_hash.is_a? Hash
|
9
|
+
primitives_hash.each{ |name, info|
|
10
|
+
child = nil
|
11
|
+
case info
|
12
|
+
when Array, String
|
13
|
+
child = info
|
14
|
+
when Hash
|
15
|
+
child = Primitives.new(info)
|
16
|
+
else
|
17
|
+
raise "unknown primitive type: #{name}"
|
18
|
+
end
|
19
|
+
|
20
|
+
self.instance_variable_set "@#{name}", child
|
21
|
+
self.class.class_eval do
|
22
|
+
define_method(name) { child }
|
23
|
+
end
|
24
|
+
@children[name] = child
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def substitute(identifier)
|
29
|
+
components = identifier.split(/\./)
|
30
|
+
the_first = components.first
|
31
|
+
the_rest = components[1 .. -1].join('.')
|
32
|
+
raise "failed to substitute unknown primitive: #{the_first}" unless @children.has_key? the_first
|
33
|
+
case @children[the_first]
|
34
|
+
when Primitives
|
35
|
+
raise "failed to substitute partial primitive: #{the_first}" if the_rest.empty?
|
36
|
+
return @children[the_first].substitute(the_rest)
|
37
|
+
else
|
38
|
+
return @children[the_first]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def has_primitive?(identifier)
|
43
|
+
begin
|
44
|
+
self.substitute(identifier)
|
45
|
+
return true
|
46
|
+
rescue
|
47
|
+
return false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,851 @@
|
|
1
|
+
require 'iptables/logger'
|
2
|
+
|
3
|
+
module IPTables
|
4
|
+
class Tables
|
5
|
+
# The main iptables object, encompassing all tables, their chains, their rules, etc
|
6
|
+
attr_reader :config, :tables
|
7
|
+
|
8
|
+
# Example: *filter
|
9
|
+
@@parse_table_regex = /^\*(\S+)$/
|
10
|
+
# Example: # Generated by iptables-save v1.4.4 on Wed Sep 26 18:38:44 2012
|
11
|
+
@@parse_comment_regex = /^#/
|
12
|
+
|
13
|
+
def initialize(input, config=nil)
|
14
|
+
@config = config
|
15
|
+
$log.debug('init IPTables')
|
16
|
+
@tables = Hash.new
|
17
|
+
|
18
|
+
case input
|
19
|
+
when Hash
|
20
|
+
input.keys.sort.each{ |table_name|
|
21
|
+
table_info = input[table_name]
|
22
|
+
case table_info
|
23
|
+
when nil, false
|
24
|
+
@tables[table_name] = table_info
|
25
|
+
next
|
26
|
+
end
|
27
|
+
table = Table.new(table_name, self, table_info)
|
28
|
+
@tables[table_name] = table
|
29
|
+
}
|
30
|
+
|
31
|
+
when String
|
32
|
+
self.parse(input.split(/\n/))
|
33
|
+
|
34
|
+
else
|
35
|
+
raise "don't know how to handle input: #{input.inspect}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def as_array(comments = true)
|
40
|
+
array = []
|
41
|
+
$log.debug('IPTables array')
|
42
|
+
@tables.keys.sort.each{ |name|
|
43
|
+
table = @tables[name]
|
44
|
+
$log.debug("#{name}: #{table}")
|
45
|
+
next if table.nil?
|
46
|
+
array << '*'+name
|
47
|
+
array += table.as_array(comments)
|
48
|
+
array << 'COMMIT'
|
49
|
+
}
|
50
|
+
return array
|
51
|
+
end
|
52
|
+
|
53
|
+
def merge(merged)
|
54
|
+
raise "must merge another IPTables::Tables" unless merged.class == IPTables::Tables
|
55
|
+
merged.tables.each{ |table_name, table_object|
|
56
|
+
$log.debug("merging table #{table_name}")
|
57
|
+
|
58
|
+
case table_object
|
59
|
+
when false
|
60
|
+
$log.debug("deleting table #{table_name}")
|
61
|
+
@tables.delete(table_name)
|
62
|
+
next
|
63
|
+
|
64
|
+
when nil
|
65
|
+
next
|
66
|
+
end
|
67
|
+
|
68
|
+
# only a Table is expected from here onwards
|
69
|
+
|
70
|
+
# merged table
|
71
|
+
if (@tables.has_key? table_name) and not (@tables[table_name].nil?)
|
72
|
+
@tables[table_name].merge(table_object)
|
73
|
+
next
|
74
|
+
end
|
75
|
+
|
76
|
+
# new table
|
77
|
+
@tables[table_name] = table_object
|
78
|
+
}
|
79
|
+
|
80
|
+
# find and apply any node rule addition points
|
81
|
+
@tables.each{ |name, table|
|
82
|
+
next unless table.class == IPTables::Table
|
83
|
+
$log.debug("applying additions to table #{name}")
|
84
|
+
table.apply_additions(merged)
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
def get_node_additions(table_name, chain_name)
|
89
|
+
$log.debug("finding additions for table #{table_name}, chain #{chain_name}")
|
90
|
+
return unless @tables.has_key? table_name
|
91
|
+
return unless @tables[table_name].class == IPTables::Table
|
92
|
+
return @tables[table_name].get_node_additions(chain_name)
|
93
|
+
end
|
94
|
+
|
95
|
+
def parse(lines)
|
96
|
+
position = 0
|
97
|
+
while position < lines.length
|
98
|
+
line = lines[position]
|
99
|
+
#$log.debug(line)
|
100
|
+
position += 1
|
101
|
+
|
102
|
+
case line
|
103
|
+
when @@parse_comment_regex, 'COMMIT'
|
104
|
+
# ignored
|
105
|
+
when @@parse_table_regex
|
106
|
+
@tables[$1] = IPTables::Table.new($1, self)
|
107
|
+
position += @tables[$1].parse(lines[position .. -1])
|
108
|
+
else
|
109
|
+
raise "unhandled line: #{line}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
raise 'no tables found' unless @tables.any?
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class TablesComparison
|
117
|
+
def initialize(tables1, tables2)
|
118
|
+
raise "must provide two tables" unless (tables1.class == IPTables::Tables) and (tables2.class == IPTables::Tables)
|
119
|
+
@tables1 = tables1
|
120
|
+
@tables2 = tables2
|
121
|
+
@table_diffs = []
|
122
|
+
|
123
|
+
@including_comments = true
|
124
|
+
@compared = false
|
125
|
+
end
|
126
|
+
|
127
|
+
def compare
|
128
|
+
return if @compared
|
129
|
+
@equal = true
|
130
|
+
|
131
|
+
tables1_tables = @tables1.tables.keys.sort
|
132
|
+
tables2_tables = @tables2.tables.keys.sort
|
133
|
+
@only_in_current = (tables1_tables - tables2_tables).reject{ |t| @tables1.tables[t].nil? }
|
134
|
+
@only_in_new = (tables2_tables - tables1_tables).reject{ |t| @tables2.tables[t].nil? }
|
135
|
+
@equal = false if @only_in_current.any? or @only_in_new.any?
|
136
|
+
|
137
|
+
@table_diffs = []
|
138
|
+
(tables1_tables - @only_in_current - @only_in_new).each{ |table|
|
139
|
+
table1 = @tables1.tables[table]
|
140
|
+
table2 = @tables2.tables[table]
|
141
|
+
|
142
|
+
# nil tables are only created by policy, never parsed
|
143
|
+
# they mean "use the parsed policy here"
|
144
|
+
# which means "for comparison purposes, they are always equal"
|
145
|
+
next if table1.nil? or table2.nil?
|
146
|
+
|
147
|
+
table_comparison = IPTables::TableComparison.new(table1, table2)
|
148
|
+
if @including_comments
|
149
|
+
table_comparison.include_comments
|
150
|
+
else
|
151
|
+
table_comparison.ignore_comments
|
152
|
+
end
|
153
|
+
next if table_comparison.equal?
|
154
|
+
|
155
|
+
@equal = false
|
156
|
+
@table_diffs << table_comparison
|
157
|
+
}
|
158
|
+
|
159
|
+
return nil
|
160
|
+
end
|
161
|
+
|
162
|
+
def ignore_comments
|
163
|
+
@including_comments = false
|
164
|
+
@compared = false
|
165
|
+
end
|
166
|
+
|
167
|
+
def include_comments
|
168
|
+
@including_comments = true
|
169
|
+
@compared = false
|
170
|
+
end
|
171
|
+
|
172
|
+
def equal?
|
173
|
+
self.compare
|
174
|
+
return @equal
|
175
|
+
end
|
176
|
+
|
177
|
+
def as_array
|
178
|
+
self.compare
|
179
|
+
array = []
|
180
|
+
return array if self.equal?
|
181
|
+
if @only_in_current.any?
|
182
|
+
@only_in_current.each{ |table_name|
|
183
|
+
array << "Missing table: #{table_name}"
|
184
|
+
array.concat @tables1.tables[table_name].as_array
|
185
|
+
}
|
186
|
+
end
|
187
|
+
if @only_in_new.any?
|
188
|
+
@only_in_new.each{ |table_name|
|
189
|
+
array << "New table: #{table_name}"
|
190
|
+
next if @tables2.tables[table_name].nil?
|
191
|
+
array.concat @tables2.tables[table_name].as_array
|
192
|
+
}
|
193
|
+
end
|
194
|
+
if @table_diffs.any?
|
195
|
+
@table_diffs.each{ |table_comparison|
|
196
|
+
array.concat table_comparison.as_array
|
197
|
+
}
|
198
|
+
end
|
199
|
+
return array
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
class Table
|
204
|
+
# standard tables: nat, mangle, raw, filter
|
205
|
+
attr_reader :chains, :name, :node_addition_points, :my_iptables
|
206
|
+
|
207
|
+
# Example: :INPUT DROP [0:0]
|
208
|
+
@@chain_policy_regex = /^:(\S+)\s+(\S+)\s+/
|
209
|
+
# Example: -A INPUT -m comment --comment "BEGIN: in-bound traffic"
|
210
|
+
@@chain_rule_regex = /^-A\s+(\S+)\s+(.+)/
|
211
|
+
|
212
|
+
def initialize(name, my_iptables, table_info_hash={})
|
213
|
+
@name = name
|
214
|
+
@my_iptables = my_iptables
|
215
|
+
$log.debug("init Table #{@name}")
|
216
|
+
|
217
|
+
@node_addition_points = {}
|
218
|
+
@chains = {}
|
219
|
+
|
220
|
+
table_info_hash.keys.sort.each{ |chain_name|
|
221
|
+
chain_info = table_info_hash[chain_name]
|
222
|
+
case chain_info
|
223
|
+
when Hash
|
224
|
+
@chains[chain_name] = IPTables::Chain.new(chain_name, chain_info, self)
|
225
|
+
|
226
|
+
when false, nil
|
227
|
+
@chains[chain_name] = chain_info
|
228
|
+
|
229
|
+
else
|
230
|
+
raise "don't know how to handle #{chain_name}: #{chain_info.inspect}"
|
231
|
+
end
|
232
|
+
}
|
233
|
+
$log.debug("table #{@name} is #{self}")
|
234
|
+
end
|
235
|
+
|
236
|
+
def as_array(comments = true)
|
237
|
+
policies = []
|
238
|
+
chains = []
|
239
|
+
|
240
|
+
# special sorting rule INPUT FORWARD OUTPUT are always first, in this order
|
241
|
+
chain_order = @chains.keys.sort()
|
242
|
+
%w/INPUT FORWARD OUTPUT/.reverse.each{ |chain|
|
243
|
+
next unless chain_order.include? chain
|
244
|
+
chain_order -= [chain]
|
245
|
+
chain_order.unshift(chain)
|
246
|
+
}
|
247
|
+
$log.debug("chain order: #{chain_order.inspect}")
|
248
|
+
|
249
|
+
chain_order.each{ |name|
|
250
|
+
$log.debug("chain #{name}")
|
251
|
+
chain = @chains[name]
|
252
|
+
policies.push ":#{name} #{chain.output_policy}"
|
253
|
+
chains += chain.as_array(comments)
|
254
|
+
}
|
255
|
+
return policies + chains
|
256
|
+
end
|
257
|
+
|
258
|
+
def path()
|
259
|
+
@name
|
260
|
+
end
|
261
|
+
|
262
|
+
def merge(table_object)
|
263
|
+
table_object.chains.each{ |chain_name, chain_object|
|
264
|
+
$log.debug("merging chain #{chain_name}")
|
265
|
+
|
266
|
+
case chain_object
|
267
|
+
when false
|
268
|
+
@chains.delete(chain_name)
|
269
|
+
next
|
270
|
+
|
271
|
+
when nil
|
272
|
+
next
|
273
|
+
end
|
274
|
+
# only a Chain is expected from here onwards
|
275
|
+
|
276
|
+
# merge Chain
|
277
|
+
if @chains.has_key? chain_name
|
278
|
+
@chains[chain_name].merge(chain_object)
|
279
|
+
next
|
280
|
+
end
|
281
|
+
|
282
|
+
# copy Chain
|
283
|
+
@chains[chain_name] = chain_object if chain_object.complete?
|
284
|
+
}
|
285
|
+
end
|
286
|
+
|
287
|
+
def apply_additions(other_firewall)
|
288
|
+
$log.debug("node addition points: #{@node_addition_points.inspect}")
|
289
|
+
@chains.each{ |name, chain_object|
|
290
|
+
$log.debug("looking for additions to chain #{name}")
|
291
|
+
next unless @node_addition_points.has_key? name
|
292
|
+
chain_object.apply_additions(other_firewall)
|
293
|
+
}
|
294
|
+
end
|
295
|
+
|
296
|
+
def register_node_addition_point(addition_name)
|
297
|
+
$log.debug("registering node addition point for #{addition_name}")
|
298
|
+
@node_addition_points[addition_name] = true
|
299
|
+
end
|
300
|
+
|
301
|
+
def get_node_additions(chain_name)
|
302
|
+
return unless @chains.has_key? chain_name
|
303
|
+
return @chains[chain_name].get_node_additions
|
304
|
+
end
|
305
|
+
|
306
|
+
def parse(lines)
|
307
|
+
position = 0
|
308
|
+
while position < lines.length
|
309
|
+
line = lines[position]
|
310
|
+
position += 1
|
311
|
+
|
312
|
+
case line
|
313
|
+
when @@chain_policy_regex
|
314
|
+
@chains[$1] = IPTables::Chain.new($1, {'policy' => $2}, self)
|
315
|
+
when @@chain_rule_regex
|
316
|
+
raise "unrecognized chain: #{$1}" unless @chains.has_key? $1
|
317
|
+
@chains[$1].parse_rule($2)
|
318
|
+
else
|
319
|
+
$log.debug("returning on unrecognized line: #{line}")
|
320
|
+
# back up a line
|
321
|
+
return position - 1
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
class TableComparison
|
328
|
+
def initialize(table1, table2)
|
329
|
+
raise "must provide two tables" unless (table1.class == IPTables::Table) and (table2.class == IPTables::Table)
|
330
|
+
raise "table names should match" unless table1.name == table2.name
|
331
|
+
@table1 = table1
|
332
|
+
@table2 = table2
|
333
|
+
|
334
|
+
@including_comments = true
|
335
|
+
@compared = false
|
336
|
+
end
|
337
|
+
|
338
|
+
def compare
|
339
|
+
return if @compared
|
340
|
+
@equal = true
|
341
|
+
|
342
|
+
table1_chains = @table1.chains.keys.sort
|
343
|
+
table2_chains = @table2.chains.keys.sort
|
344
|
+
@only_in_current = table1_chains - table2_chains
|
345
|
+
@only_in_new = table2_chains - table1_chains
|
346
|
+
@equal = false if @only_in_current.any? or @only_in_new.any?
|
347
|
+
|
348
|
+
@chain_diffs = []
|
349
|
+
(table1_chains - @only_in_current - @only_in_new).each{ |chain|
|
350
|
+
chain_comparison = IPTables::ChainComparison.new(@table1.chains[chain], @table2.chains[chain])
|
351
|
+
if @including_comments
|
352
|
+
chain_comparison.include_comments
|
353
|
+
else
|
354
|
+
chain_comparison.ignore_comments
|
355
|
+
end
|
356
|
+
next if chain_comparison.equal?
|
357
|
+
|
358
|
+
@equal = false
|
359
|
+
@chain_diffs << chain_comparison
|
360
|
+
}
|
361
|
+
|
362
|
+
return nil
|
363
|
+
end
|
364
|
+
|
365
|
+
def ignore_comments
|
366
|
+
@including_comments = false
|
367
|
+
@compared = false
|
368
|
+
end
|
369
|
+
|
370
|
+
def include_comments
|
371
|
+
@including_comments = true
|
372
|
+
@compared = false
|
373
|
+
end
|
374
|
+
|
375
|
+
def missing
|
376
|
+
self.compare
|
377
|
+
return @only_in_current
|
378
|
+
end
|
379
|
+
|
380
|
+
def new
|
381
|
+
self.compare
|
382
|
+
return @only_in_new
|
383
|
+
end
|
384
|
+
|
385
|
+
def changed
|
386
|
+
self.compare
|
387
|
+
return @chain_diffs
|
388
|
+
end
|
389
|
+
|
390
|
+
def as_array
|
391
|
+
self.compare
|
392
|
+
array = []
|
393
|
+
return array if self.equal?
|
394
|
+
array << "Changed table: #{@table1.name}"
|
395
|
+
if self.missing.any?
|
396
|
+
self.missing.each{ |chain_name|
|
397
|
+
array << 'Missing chain:'
|
398
|
+
array.concat @table1.chains[chain_name].all_as_array
|
399
|
+
}
|
400
|
+
end
|
401
|
+
if self.new.any?
|
402
|
+
self.new.each{ |chain_name|
|
403
|
+
array << 'New chain:'
|
404
|
+
array.concat @table2.chains[chain_name].all_as_array
|
405
|
+
}
|
406
|
+
end
|
407
|
+
if self.changed.any?
|
408
|
+
self.changed.each{ |chain_comparison|
|
409
|
+
array.concat chain_comparison.as_array
|
410
|
+
}
|
411
|
+
end
|
412
|
+
return array
|
413
|
+
end
|
414
|
+
|
415
|
+
def equal?
|
416
|
+
self.compare
|
417
|
+
return @equal
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
class Chain
|
422
|
+
# example chain names in filter table: INPUT, FORWARD, OUTPUT
|
423
|
+
attr_reader :additions, :name, :node_addition_points, :my_table, :policy, :rules
|
424
|
+
|
425
|
+
def initialize(name, chain_info_hash, my_table)
|
426
|
+
@name = name
|
427
|
+
@chain_info_hash = chain_info_hash
|
428
|
+
@my_table = my_table
|
429
|
+
|
430
|
+
$log.debug("init Chain #{@name}")
|
431
|
+
@node_addition_points = []
|
432
|
+
|
433
|
+
@policy = @chain_info_hash['policy']
|
434
|
+
@rules = self.find_and_add_type('rules')
|
435
|
+
@additions = self.find_and_add_type('additions')
|
436
|
+
end
|
437
|
+
|
438
|
+
def find_and_add_type(data_type)
|
439
|
+
rules = []
|
440
|
+
return unless @chain_info_hash.has_key? data_type
|
441
|
+
@chain_info_hash[data_type].each_with_index{ |rule, index|
|
442
|
+
rule_object = IPTables::Rule.new(rule, self)
|
443
|
+
rule_object.set_position(index)
|
444
|
+
rules.push(rule_object)
|
445
|
+
}
|
446
|
+
return rules
|
447
|
+
end
|
448
|
+
|
449
|
+
def output_policy()
|
450
|
+
return (@policy == nil) ? '-' : @policy
|
451
|
+
end
|
452
|
+
|
453
|
+
def as_array(comments = true)
|
454
|
+
$log.debug("Chain #{@name} array")
|
455
|
+
return [] if @rules == nil
|
456
|
+
rules = @rules.collect{ |rule| rule.as_array(comments)}.flatten
|
457
|
+
$log.debug(rules)
|
458
|
+
return rules
|
459
|
+
end
|
460
|
+
|
461
|
+
def all_as_array(comments = true)
|
462
|
+
return [
|
463
|
+
":#{@name} #{self.output_policy}",
|
464
|
+
self.as_array
|
465
|
+
].flatten
|
466
|
+
end
|
467
|
+
|
468
|
+
def merge(chain_object)
|
469
|
+
# if found, replace policy
|
470
|
+
@policy = chain_object.policy unless chain_object.policy.nil?
|
471
|
+
|
472
|
+
# if found, replace rules
|
473
|
+
@rules = chain_object.rules unless chain_object.rules.nil?
|
474
|
+
end
|
475
|
+
|
476
|
+
def path()
|
477
|
+
@my_table.path + ".#{@name}"
|
478
|
+
end
|
479
|
+
|
480
|
+
def register_node_addition_point(rule_object, addition_name)
|
481
|
+
@node_addition_points.push(rule_object) unless @node_addition_points.include? rule_object
|
482
|
+
@my_table.register_node_addition_point(addition_name)
|
483
|
+
end
|
484
|
+
|
485
|
+
def get_node_additions()
|
486
|
+
return if @additions.empty?
|
487
|
+
return @additions
|
488
|
+
end
|
489
|
+
|
490
|
+
def apply_additions(other_firewall)
|
491
|
+
@node_addition_points.each{ |rule_object|
|
492
|
+
$log.debug("applying additions for #{rule_object.path}")
|
493
|
+
rule_object.apply_additions(other_firewall)
|
494
|
+
}
|
495
|
+
end
|
496
|
+
|
497
|
+
def parse_rule(args)
|
498
|
+
@rules = [] if @rules.nil?
|
499
|
+
# parsed rules come with trailing whitespace; remove
|
500
|
+
rule_object = IPTables::Rule.new(args.strip, self)
|
501
|
+
rule_object.set_position(@rules.length)
|
502
|
+
@rules.push(rule_object)
|
503
|
+
end
|
504
|
+
|
505
|
+
def complete?
|
506
|
+
if @rules.nil?
|
507
|
+
return true if @additions.nil?
|
508
|
+
return false
|
509
|
+
end
|
510
|
+
return true if @rules.any?
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
class ChainComparison
|
515
|
+
require 'diff/lcs'
|
516
|
+
|
517
|
+
def initialize(chain1, chain2)
|
518
|
+
raise "must provide two chains" unless (chain1.class == IPTables::Chain) and (chain2.class == IPTables::Chain)
|
519
|
+
raise "first and second chain should have same name" unless chain1.name == chain2.name
|
520
|
+
@chain1 = chain1
|
521
|
+
@chain2 = chain2
|
522
|
+
|
523
|
+
@including_comments = true
|
524
|
+
@compared = false
|
525
|
+
end
|
526
|
+
|
527
|
+
def ignore_comments
|
528
|
+
@including_comments = false
|
529
|
+
@compared = false
|
530
|
+
end
|
531
|
+
|
532
|
+
def include_comments
|
533
|
+
@including_comments = true
|
534
|
+
@compared = false
|
535
|
+
end
|
536
|
+
|
537
|
+
def compare
|
538
|
+
return if @compared
|
539
|
+
|
540
|
+
@equal = true
|
541
|
+
|
542
|
+
@missing_rules = {}
|
543
|
+
@new_rules = {}
|
544
|
+
Diff::LCS.diff(
|
545
|
+
@chain1.as_array(@including_comments),
|
546
|
+
@chain2.as_array(@including_comments)
|
547
|
+
).each{ |diffgroup|
|
548
|
+
diffgroup.each{ |diff|
|
549
|
+
if diff.action == '-'
|
550
|
+
@missing_rules[diff.position] = diff.element
|
551
|
+
else
|
552
|
+
@new_rules[diff.position] = diff.element
|
553
|
+
end
|
554
|
+
@equal = false
|
555
|
+
}
|
556
|
+
}
|
557
|
+
|
558
|
+
@new_policy = false
|
559
|
+
unless @chain1.policy == @chain2.policy
|
560
|
+
@new_policy = true
|
561
|
+
@equal = false
|
562
|
+
end
|
563
|
+
|
564
|
+
@compared = true
|
565
|
+
return nil
|
566
|
+
end
|
567
|
+
|
568
|
+
def equal?
|
569
|
+
self.compare
|
570
|
+
return @equal
|
571
|
+
end
|
572
|
+
|
573
|
+
def missing
|
574
|
+
self.compare
|
575
|
+
return @missing_rules
|
576
|
+
end
|
577
|
+
|
578
|
+
def new
|
579
|
+
self.compare
|
580
|
+
return @new_rules
|
581
|
+
end
|
582
|
+
|
583
|
+
def as_array
|
584
|
+
self.compare
|
585
|
+
array = []
|
586
|
+
return array if self.equal?
|
587
|
+
array << "Changed chain: #{@chain1.name}"
|
588
|
+
array << "New policy: #{@chain2.policy}" if self.new_policy?
|
589
|
+
if self.missing.any?
|
590
|
+
self.missing.keys.sort.each{ |rule_num|
|
591
|
+
array << "-#{rule_num}: #{self.missing[rule_num]}"
|
592
|
+
}
|
593
|
+
end
|
594
|
+
if self.new.any?
|
595
|
+
self.new.keys.sort.each{ |rule_num|
|
596
|
+
array << "+#{rule_num}: #{self.new[rule_num]}"
|
597
|
+
}
|
598
|
+
end
|
599
|
+
return array
|
600
|
+
end
|
601
|
+
|
602
|
+
def new_policy?
|
603
|
+
self.compare
|
604
|
+
return @new_policy
|
605
|
+
end
|
606
|
+
end
|
607
|
+
|
608
|
+
class Rule
|
609
|
+
# possible key names for custom named tcp and/or udp services
|
610
|
+
@@valid_custom_service_keys = %w/service_name service_udp service_tcp/
|
611
|
+
attr_reader :position, :rule_hash, :type
|
612
|
+
|
613
|
+
@@parse_comment_regex = /^\-m\s+comment\s+\-\-comment\s+"([^"]+)"/
|
614
|
+
|
615
|
+
def initialize(rule_info, my_chain)
|
616
|
+
$log.debug("received Rule info #{rule_info.inspect}")
|
617
|
+
|
618
|
+
@rule_info = rule_info
|
619
|
+
case rule_info
|
620
|
+
when String
|
621
|
+
self.handle_string(rule_info)
|
622
|
+
when Hash
|
623
|
+
@rule_hash = rule_info
|
624
|
+
else
|
625
|
+
raise "don't know how to handle rule_info: #{rule_info.inspect}"
|
626
|
+
end
|
627
|
+
|
628
|
+
@my_chain = my_chain
|
629
|
+
|
630
|
+
@position = nil
|
631
|
+
|
632
|
+
# expanded rules will use this instead of @args
|
633
|
+
@children = []
|
634
|
+
|
635
|
+
@args = ''
|
636
|
+
|
637
|
+
self.handle_requires_primitive
|
638
|
+
|
639
|
+
case @rule_hash.length
|
640
|
+
when 1
|
641
|
+
@type = @rule_hash.keys.first
|
642
|
+
when 2, 3
|
643
|
+
@type = 'custom_service'
|
644
|
+
else
|
645
|
+
raise 'do not know how to handle this rule'
|
646
|
+
end
|
647
|
+
|
648
|
+
$log.debug("create Rule #{@type}")
|
649
|
+
|
650
|
+
case @type
|
651
|
+
when 'comment'
|
652
|
+
|
653
|
+
when 'custom_service'
|
654
|
+
self.handle_custom_service()
|
655
|
+
|
656
|
+
when 'empty'
|
657
|
+
|
658
|
+
when 'interpolated'
|
659
|
+
self.handle_interpolated()
|
660
|
+
|
661
|
+
when 'macro'
|
662
|
+
self.handle_macro()
|
663
|
+
|
664
|
+
when 'node_addition_points'
|
665
|
+
self.handle_node_addition_points()
|
666
|
+
|
667
|
+
when 'raw'
|
668
|
+
|
669
|
+
when 'service'
|
670
|
+
self.handle_service()
|
671
|
+
|
672
|
+
when 'service_tcp'
|
673
|
+
|
674
|
+
when 'service_udp'
|
675
|
+
|
676
|
+
when 'ulog'
|
677
|
+
|
678
|
+
else
|
679
|
+
raise "unrecognized rule type #{@type}"
|
680
|
+
end
|
681
|
+
end
|
682
|
+
|
683
|
+
def add_child(rule_hash)
|
684
|
+
@children.push(IPTables::Rule.new(rule_hash, @my_chain))
|
685
|
+
end
|
686
|
+
|
687
|
+
def handle_requires_primitive
|
688
|
+
@requires_primitive = nil
|
689
|
+
return unless @rule_hash.has_key? 'requires_primitive'
|
690
|
+
@requires_primitive = @rule_hash['requires_primitive']
|
691
|
+
@rule_hash.delete('requires_primitive')
|
692
|
+
config = @my_chain.my_table.my_iptables.config
|
693
|
+
raise 'missing config' if config.nil?
|
694
|
+
primitives = config.primitives
|
695
|
+
raise 'missing primitives' if primitives.nil?
|
696
|
+
@rule_hash = {'empty' => nil} unless primitives.has_primitive?(@requires_primitive)
|
697
|
+
end
|
698
|
+
|
699
|
+
def handle_custom_service()
|
700
|
+
raise "missing service name: #{@rule_hash.inspect}" unless @rule_hash.has_key? 'service_name'
|
701
|
+
|
702
|
+
custom_service_port = nil
|
703
|
+
custom_services = []
|
704
|
+
@rule_hash.keys.sort.each{ |key|
|
705
|
+
next if key == 'service_name'
|
706
|
+
raise "unknown service key: #{key}" unless @@valid_custom_service_keys.include? key
|
707
|
+
custom_services << {key => @rule_hash[key]}
|
708
|
+
# set the custom service port if exactly one custom service has a port
|
709
|
+
# or both services have the same port
|
710
|
+
if custom_service_port.nil?
|
711
|
+
custom_service_port = @rule_hash[key]
|
712
|
+
else
|
713
|
+
custom_service_port = nil unless @rule_hash[key].to_i == custom_service_port.to_i
|
714
|
+
end
|
715
|
+
}
|
716
|
+
|
717
|
+
if custom_service_port.nil?
|
718
|
+
self.add_child({'comment' => "_ #{@rule_hash['service_name']}"})
|
719
|
+
else
|
720
|
+
self.add_child({'comment' => "_ Port #{custom_service_port} - #{@rule_hash['service_name']}"})
|
721
|
+
end
|
722
|
+
custom_services.each{ |service_hash|
|
723
|
+
self.add_child(service_hash)
|
724
|
+
}
|
725
|
+
end
|
726
|
+
|
727
|
+
def handle_interpolated()
|
728
|
+
config = @my_chain.my_table.my_iptables.config
|
729
|
+
raise 'missing config' if config.nil?
|
730
|
+
interpolations = config.interpolations
|
731
|
+
$log.debug("interpolating: #{@rule_hash['interpolated']}")
|
732
|
+
interpolations.children(@rule_hash['interpolated']).each{ |rule_hash|
|
733
|
+
self.add_child(rule_hash)
|
734
|
+
}
|
735
|
+
end
|
736
|
+
|
737
|
+
def handle_macro()
|
738
|
+
config = @my_chain.my_table.my_iptables.config
|
739
|
+
raise 'missing config' if config.nil?
|
740
|
+
macro = config.macros.named[@rule_hash['macro']]
|
741
|
+
$log.debug("macro: #{macro.name}")
|
742
|
+
macro.children.each{ |rule_hash|
|
743
|
+
self.add_child(rule_hash)
|
744
|
+
}
|
745
|
+
end
|
746
|
+
|
747
|
+
def handle_node_addition_points()
|
748
|
+
self.add_child({'empty' => nil})
|
749
|
+
@rule_hash['node_addition_points'].each{ |addition_name|
|
750
|
+
@my_chain.register_node_addition_point(self, addition_name)
|
751
|
+
}
|
752
|
+
end
|
753
|
+
|
754
|
+
def handle_service()
|
755
|
+
config = @my_chain.my_table.my_iptables.config
|
756
|
+
raise 'missing config' if config.nil?
|
757
|
+
service = config.services.named[@rule_hash['service']]
|
758
|
+
$log.debug("service: #{service.name}")
|
759
|
+
service.children.each{ |rule_hash|
|
760
|
+
self.add_child(rule_hash)
|
761
|
+
}
|
762
|
+
end
|
763
|
+
|
764
|
+
def handle_string(rule_info)
|
765
|
+
# try to parse strings
|
766
|
+
|
767
|
+
if rule_info =~ @@parse_comment_regex
|
768
|
+
# if we're a comment, set as comment
|
769
|
+
@rule_hash = {'comment' => $1}
|
770
|
+
else
|
771
|
+
# otherwise set as raw
|
772
|
+
@rule_hash = {'raw' => rule_info}
|
773
|
+
end
|
774
|
+
end
|
775
|
+
|
776
|
+
def as_array(comments = true)
|
777
|
+
case @type
|
778
|
+
when 'comment'
|
779
|
+
return [] unless comments
|
780
|
+
self.generate_comment()
|
781
|
+
|
782
|
+
when 'empty'
|
783
|
+
return []
|
784
|
+
|
785
|
+
when 'raw'
|
786
|
+
self.generate_raw()
|
787
|
+
|
788
|
+
when 'service_tcp'
|
789
|
+
self.generate_tcp()
|
790
|
+
|
791
|
+
when 'service_udp'
|
792
|
+
self.generate_udp()
|
793
|
+
|
794
|
+
when 'ulog'
|
795
|
+
self.generate_ulog()
|
796
|
+
end
|
797
|
+
|
798
|
+
if @children.empty?
|
799
|
+
raise "@args is empty" unless @args.length > 0
|
800
|
+
return ["-A #{@my_chain.name} #{@args}"]
|
801
|
+
else
|
802
|
+
rules = @children.collect{ |child| child.as_array(comments)}.flatten
|
803
|
+
$log.debug(rules)
|
804
|
+
return rules
|
805
|
+
end
|
806
|
+
end
|
807
|
+
|
808
|
+
def generate_comment()
|
809
|
+
@args = %Q|-m comment --comment "#{@rule_hash['comment']}"|
|
810
|
+
end
|
811
|
+
|
812
|
+
def generate_raw()
|
813
|
+
@args = @rule_hash['raw']
|
814
|
+
end
|
815
|
+
|
816
|
+
def generate_tcp()
|
817
|
+
@args = "-p tcp -m tcp --sport 1024:65535 --dport #{@rule_hash['service_tcp']} -m state --state NEW,ESTABLISHED -j ACCEPT"
|
818
|
+
end
|
819
|
+
|
820
|
+
def generate_udp()
|
821
|
+
@args = "-p udp -m udp --sport 1024:65535 --dport #{@rule_hash['service_udp']} -m state --state NEW,ESTABLISHED -j ACCEPT"
|
822
|
+
end
|
823
|
+
|
824
|
+
def generate_ulog()
|
825
|
+
@args = %Q|-m limit --limit 1/sec --limit-burst 2 -j ULOG --ulog-prefix "#{@my_chain.name}:"|
|
826
|
+
|
827
|
+
if @rule_hash['ulog'] == '-p tcp'
|
828
|
+
@args = "-p tcp #{@args}"
|
829
|
+
end
|
830
|
+
end
|
831
|
+
|
832
|
+
def path()
|
833
|
+
@my_chain.path + ".#{@position}"
|
834
|
+
end
|
835
|
+
|
836
|
+
def set_position(number)
|
837
|
+
@position = number
|
838
|
+
end
|
839
|
+
|
840
|
+
def apply_additions(other_firewall)
|
841
|
+
@rule_hash['node_addition_points'].each{ |addition_name|
|
842
|
+
other_rules = other_firewall.get_node_additions(@my_chain.my_table.name, addition_name)
|
843
|
+
next if other_rules.nil?
|
844
|
+
$log.debug("applying additions at #{addition_name}")
|
845
|
+
other_rules.each{ |other_rule_object|
|
846
|
+
self.add_child(other_rule_object.rule_hash)
|
847
|
+
}
|
848
|
+
}
|
849
|
+
end
|
850
|
+
end
|
851
|
+
end
|