tablomat 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/tablomat.rb +9 -0
- data/lib/tablomat/exec.rb +19 -0
- data/lib/tablomat/ipset.rb +71 -0
- data/lib/tablomat/ipset/entry.rb +38 -0
- data/lib/tablomat/ipset/set.rb +71 -0
- data/lib/tablomat/iptables.rb +202 -0
- data/lib/tablomat/iptables/chain.rb +170 -0
- data/lib/tablomat/iptables/rule.rb +63 -0
- data/lib/tablomat/iptables/table.rb +85 -0
- data/lib/tablomat/version.rb +5 -0
- data/spec/ipset_spec.rb +144 -0
- data/spec/ipsetiptables_spec.rb +75 -0
- data/spec/iptables_spec.rb +148 -0
- data/spec/spec_helper.rb +21 -0
- metadata +133 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3acd84433060bbc3bf7e1502c05f0e9889694d3a32037e22e15f743ca01b5fc3
|
4
|
+
data.tar.gz: 9dd4eaab8169d4a12bc8d84de57913f35d8ffc0d90d9f716e12a801f2936e4aa
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 25075f75a86af90044a997409afec220443610589c68bba8d22144586724288ae860b5dcfb191ba64afaf14b420c8cfc838bc6e7f3b887f21a07a2ac99862c7e
|
7
|
+
data.tar.gz: c8be92592b3fe8e3c67439db72aa7f1717da3c13ed32b412009a65ebeb4d9bdbbb291c3391a2dda28e684cefb758b9be79960f64d0bc7183e1279deebfbf62bc
|
data/lib/tablomat.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tempfile'
|
4
|
+
require 'English'
|
5
|
+
|
6
|
+
# Module defined in exec.rb File to execute Commands
|
7
|
+
module Exec
|
8
|
+
def self.exec(cmd)
|
9
|
+
errfile = Tempfile.new('tablomat')
|
10
|
+
stdout = `#{cmd} 2> #{errfile.path}`
|
11
|
+
unless $CHILD_STATUS.success?
|
12
|
+
err = errfile.read
|
13
|
+
errfile.close
|
14
|
+
errfile.unlink
|
15
|
+
raise err.to_s
|
16
|
+
end
|
17
|
+
stdout
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tablomat/ipset/set'
|
4
|
+
require 'tablomat/exec'
|
5
|
+
|
6
|
+
module Tablomat
|
7
|
+
# Errorclass for IPSet-Errors
|
8
|
+
class IPSetError < StandardError; end
|
9
|
+
|
10
|
+
# The IPSet interface
|
11
|
+
class IPSet
|
12
|
+
extend Exec
|
13
|
+
attr_accessor :ipset_bin
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@ipset_bin = 'ipset'
|
17
|
+
@ipset_bin = "sudo #{@ipset_bin}" if Etc.getlogin != 'root'
|
18
|
+
@sets = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def matchset(flags:, option: '', negate: false, negate_option: false, set_name:)
|
22
|
+
set_set = "--match-set #{set_name} #{flags}"
|
23
|
+
set_set = "! #{set_set}" if negate
|
24
|
+
option = "! #{option}" if negate_option
|
25
|
+
"set #{set_set} #{option}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def set(name, &block)
|
29
|
+
name = name.to_s.downcase
|
30
|
+
(@sets[name] || Set.new(self, name)).tap do |set|
|
31
|
+
@sets[name] = set
|
32
|
+
block&.call(set)
|
33
|
+
end
|
34
|
+
@sets[name]
|
35
|
+
end
|
36
|
+
|
37
|
+
def exec(cmd)
|
38
|
+
Exec.exec(cmd)
|
39
|
+
rescue StandardError => e
|
40
|
+
raise IPSetError.new, e.message
|
41
|
+
end
|
42
|
+
|
43
|
+
def destroy_all(really_all = false)
|
44
|
+
# destroys all sets created in this Instance unless really_all = true
|
45
|
+
if really_all
|
46
|
+
command = "#{@ipset_bin} destroy"
|
47
|
+
exec(command)
|
48
|
+
else
|
49
|
+
@sets.each do |_name, set|
|
50
|
+
set.destroy if set.active
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def add(set_name:, entry_data:, add_options: '', exist: false)
|
56
|
+
set(set_name).add(entry_data: entry_data, add_options: add_options, exist: exist)
|
57
|
+
end
|
58
|
+
|
59
|
+
def create(set_name:, type:, create_options: '', rangefrom: '', rangeto: '')
|
60
|
+
set(set_name).create(type: type, create_options: create_options, rangefrom: rangefrom, rangeto: rangeto)
|
61
|
+
end
|
62
|
+
|
63
|
+
def del(set_name:, entry_data:, exist: false)
|
64
|
+
set(set_name).del(entry_data: entry_data, exist: exist)
|
65
|
+
end
|
66
|
+
|
67
|
+
def destroy(set_name:)
|
68
|
+
set(set_name).destroy
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tablomat
|
4
|
+
class IPSet
|
5
|
+
# interface to manage the entrys inside the sets
|
6
|
+
class Entry
|
7
|
+
def initialize(set, description)
|
8
|
+
@system = set.system
|
9
|
+
@set = set
|
10
|
+
@description = description
|
11
|
+
end
|
12
|
+
|
13
|
+
def exists?
|
14
|
+
command = "#{@system.ipset_bin} del #{@set.name} #{@description}"
|
15
|
+
@system.exec command
|
16
|
+
command = "#{@system.ipset_bin} add #{@set.name} #{@description}"
|
17
|
+
@system.exec command
|
18
|
+
true
|
19
|
+
rescue IPSetError => e
|
20
|
+
raise unless e.message.include?("Element cannot be deleted from the set: it's not added")
|
21
|
+
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
def add(add_options, exist = false)
|
26
|
+
command = "#{@system.ipset_bin} add #{@set.name} #{@description} #{add_options}"
|
27
|
+
command = "#{command} -!" if exist
|
28
|
+
@system.exec command
|
29
|
+
end
|
30
|
+
|
31
|
+
def del(exist = false)
|
32
|
+
command = "#{@system.ipset_bin} del #{@set.name} #{@description}"
|
33
|
+
command = "#{command} -!" if exist
|
34
|
+
@system.exec command
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tablomat/ipset/entry'
|
4
|
+
|
5
|
+
module Tablomat
|
6
|
+
class IPSet
|
7
|
+
# Interface to manage ipsets
|
8
|
+
class Set
|
9
|
+
attr_reader :name, :system, :active
|
10
|
+
|
11
|
+
def initialize(system, name)
|
12
|
+
@system = system
|
13
|
+
@name = name
|
14
|
+
@entrys = {}
|
15
|
+
@active = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def exists?
|
19
|
+
command = "#{@system.ipset_bin} list #{@name}"
|
20
|
+
@system.exec command
|
21
|
+
true
|
22
|
+
rescue IPSetError => e
|
23
|
+
raise unless e.message.include?('The set with the given name does not exist')
|
24
|
+
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
def type
|
29
|
+
command = "#{system.ipset_bin} list #{@name}"
|
30
|
+
stdout = `#{command} 2>&1`.strip << "\n"
|
31
|
+
if $CHILD_STATUS != 0
|
32
|
+
# throw error
|
33
|
+
puts "Invalid return value when calling #{command}"
|
34
|
+
end
|
35
|
+
stdout = stdout.split("\n").select { |s| s.include?('Type:') }[0]
|
36
|
+
stdout.split(/Type: /)[1]
|
37
|
+
rescue StandardError => e
|
38
|
+
puts "[error] #{e}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def entry(data, &block)
|
42
|
+
data = data.to_s.downcase
|
43
|
+
(@entrys[data] || Entry.new(self, data)).tap do |entry|
|
44
|
+
@entrys[data] = entry
|
45
|
+
block&.call(entry)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def add(entry_data:, add_options: '', exist: false)
|
50
|
+
entry(entry_data).add(add_options, exist)
|
51
|
+
end
|
52
|
+
|
53
|
+
def del(entry_data:, exist: false)
|
54
|
+
entry(entry_data).del(exist)
|
55
|
+
end
|
56
|
+
|
57
|
+
def create(type:, create_options: '', rangefrom: '', rangeto: '')
|
58
|
+
create_options = "range #{rangefrom}-#{rangeto} #{create_options}" if type.include?('bitmap')
|
59
|
+
command = "#{@system.ipset_bin} create #{@name} #{type} #{create_options}"
|
60
|
+
@system.exec command
|
61
|
+
@active = true
|
62
|
+
end
|
63
|
+
|
64
|
+
def destroy
|
65
|
+
command = "#{@system.ipset_bin} destroy #{@name}"
|
66
|
+
@system.exec command
|
67
|
+
@active = false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'English'
|
4
|
+
require 'tablomat/iptables/table'
|
5
|
+
require 'digest'
|
6
|
+
require 'tempfile'
|
7
|
+
require 'tablomat/exec'
|
8
|
+
|
9
|
+
module Tablomat
|
10
|
+
# The IPTables interface
|
11
|
+
class IPTables
|
12
|
+
extend Exec
|
13
|
+
attr_accessor :iptables_bin
|
14
|
+
attr_reader :active, :builtin_chains, :tmp_chain
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
# iptables must be in PATH
|
18
|
+
@iptables_bin = 'iptables'
|
19
|
+
@iptables_bin = "sudo #{@iptables_bin}" if Etc.getlogin != 'root'
|
20
|
+
|
21
|
+
# get random value for rule creation hack, iptables limits name to 28 characters
|
22
|
+
@tmp_chain_prefix = 'tablomat'
|
23
|
+
|
24
|
+
@builtin_chains = { filter: %w[INPUT FORWARD OUTPUT], nat: %w[PREROUTING INPUT OUTPUT POSTROUTING], mangle: %w[PREROUTING INPUT FORWARD OUTPUT POSTROUTING] }
|
25
|
+
@tables = {}
|
26
|
+
|
27
|
+
@active = true
|
28
|
+
synchronize
|
29
|
+
end
|
30
|
+
|
31
|
+
def exec(cmd)
|
32
|
+
Exec.exec(cmd)
|
33
|
+
end
|
34
|
+
|
35
|
+
def synchronize
|
36
|
+
# called regularly by normalize
|
37
|
+
command = "#{@iptables_bin}-save"
|
38
|
+
stdout = `#{command} 2>&1`.strip << "\n"
|
39
|
+
if $CHILD_STATUS != 0
|
40
|
+
# throw error
|
41
|
+
puts "Invalid return value when calling #{command}"
|
42
|
+
end
|
43
|
+
parse_output stdout
|
44
|
+
rescue StandardError => e
|
45
|
+
puts "[error] #{e}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# rubocop:disable Metrics/AbcSize
|
49
|
+
def parse_output(stdout)
|
50
|
+
stdout = stdout.split("\n").reject { |s| s[0] == '#' }.join("\n")
|
51
|
+
data = {}
|
52
|
+
stdout.split(/^\*/).reject { |s| s == '' }.each do |ruleset|
|
53
|
+
table, rule = ruleset.match(/(.+?)\n(.*)/m).to_a[1..-1]
|
54
|
+
data[table] = {}
|
55
|
+
|
56
|
+
raise "Empty ruleset in table #{table}" if table.nil?
|
57
|
+
|
58
|
+
rule.scan(/^:(.+?)\s+/).each do |match|
|
59
|
+
data[table][match[0]] = []
|
60
|
+
end
|
61
|
+
|
62
|
+
rule.scan(/^-A (.+?) (.+?)\n/).each do |match|
|
63
|
+
data[table][match[0]] << match[1]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
parse_data(data)
|
67
|
+
end
|
68
|
+
# rubocop:enable Metrics/AbcSize
|
69
|
+
|
70
|
+
def parse_data(data)
|
71
|
+
data.each do |table, chains|
|
72
|
+
t = self.table(table, false)
|
73
|
+
t.activate true
|
74
|
+
parse_table(t, chains)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def parse_table(table, chains)
|
79
|
+
chains.each do |chain, rules|
|
80
|
+
c = table.chain(chain, false)
|
81
|
+
c.activate true
|
82
|
+
parse_chain(c, rules)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def parse_chain(chain, rules)
|
87
|
+
rules.each do |rule|
|
88
|
+
r = chain.rule(rule, false)
|
89
|
+
r.activate true
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def table(name, owned = true, &block)
|
94
|
+
name = name.to_s.downcase
|
95
|
+
(@tables[name] || Table.new(self, name, owned)).tap do |table|
|
96
|
+
@tables[name] = table
|
97
|
+
block&.call(table)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# converts any given rule to the iptables internal description format by using a temporary table
|
102
|
+
# rubocop:disable Metrics/AbcSize
|
103
|
+
def normalize(data, table = 'filter')
|
104
|
+
synchronize
|
105
|
+
tmp_chain = "#{@tmp_chain_prefix}#{Digest::SHA256.hexdigest Random.rand(1024).to_s}"[0, 28]
|
106
|
+
self.table(table).chain(tmp_chain).activate
|
107
|
+
self.table(table).append(tmp_chain, data)
|
108
|
+
synchronize
|
109
|
+
normalized = data
|
110
|
+
self.table(table).chain(tmp_chain).rules.select { |_k, r| r.owned == false }.each do |_k, r|
|
111
|
+
normalized = r.description
|
112
|
+
end
|
113
|
+
self.table(table).delete(tmp_chain, data)
|
114
|
+
self.table(table).chain(tmp_chain).deactivate
|
115
|
+
self.table(table).chain(tmp_chain).apply_delete
|
116
|
+
normalized
|
117
|
+
end
|
118
|
+
# rubocop:enable Metrics/AbcSize
|
119
|
+
|
120
|
+
# rubocop:disable Metrics/AbcSize
|
121
|
+
def exists(table_name, chain_name, data = nil)
|
122
|
+
if data.nil?
|
123
|
+
table(table_name).chain_exists(chain_name) && table(table_name).chain(chain_name).active
|
124
|
+
else
|
125
|
+
data = normalize data, table_name
|
126
|
+
table(table_name).chain(chain_name).rules.count { |_k, v| normalize(v.description, table_name) == data && v.active } >= 1
|
127
|
+
end
|
128
|
+
end
|
129
|
+
# rubocop:enable Metrics/AbcSize
|
130
|
+
|
131
|
+
def insert(table_name, chain_name, data, pos)
|
132
|
+
data = normalize data, table_name
|
133
|
+
table(table_name).insert(chain_name, data, pos)
|
134
|
+
end
|
135
|
+
|
136
|
+
def append(table_name, chain_name, data)
|
137
|
+
data = normalize data, table_name
|
138
|
+
table(table_name).append(chain_name, data)
|
139
|
+
end
|
140
|
+
|
141
|
+
def delete(table_name, chain_name, data)
|
142
|
+
data = normalize data, table_name
|
143
|
+
table(table_name).delete(chain_name, data)
|
144
|
+
end
|
145
|
+
|
146
|
+
def activate
|
147
|
+
@active = true
|
148
|
+
@tables.each do |_name, table|
|
149
|
+
table.activate
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def deactivate
|
154
|
+
@active = false
|
155
|
+
tables.each do |_name, table|
|
156
|
+
table.deactivate
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# rubocop:disable Metrics/AbcSize
|
161
|
+
def get_active_rules(table = 'nat', chain = 'PREROUTING')
|
162
|
+
rrgx = /(?<key>--?[[:alpha:]-]+) (?<value>[[:alnum:]:\.]+)/.freeze
|
163
|
+
switch_map = { '-s' => :source, '--source' => :source,
|
164
|
+
'-d' => :destination, '--destination' => :destination,
|
165
|
+
'--dport' => :dport, '--to-destination' => :to_dest,
|
166
|
+
'-p' => :protocol, '--protocol' => :protocol,
|
167
|
+
'-j' => :jump, '--jump' => :jump,
|
168
|
+
'--match-set' => :match }
|
169
|
+
|
170
|
+
table(table).chain(chain).rules.filter { |_, r| r.active }.map do |_, rule|
|
171
|
+
rule.description.to_enum(:scan, rrgx)
|
172
|
+
.map { Regexp.last_match }
|
173
|
+
.map { |m| [switch_map[m[:key]], m[:value]] }
|
174
|
+
.filter { |m| m[0] }.to_h
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# used to easily switch rules from one src ip to another
|
179
|
+
def switch_sources(old_src, new_src)
|
180
|
+
@tables.to_a.each do |_kt, tbl|
|
181
|
+
tbl.chains.to_a.each do |_kc, chn|
|
182
|
+
next if chn.name.include? 'tablomat'
|
183
|
+
|
184
|
+
chn.rules.to_a.each do |_key, rule|
|
185
|
+
next unless rule.description.include? "-s #{old_src}"
|
186
|
+
|
187
|
+
new_data = normalize(rule.description, tbl.name).sub "-s #{old_src}", "-s #{new_src}"
|
188
|
+
pos = rule.position + 1
|
189
|
+
delete(tbl.name, chn.name, rule.description)
|
190
|
+
insert(tbl.name, chn.name, new_data, pos)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
# rubocop:enable Metrics/AbcSize
|
196
|
+
|
197
|
+
def print
|
198
|
+
require 'pp'
|
199
|
+
pp self
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tablomat/iptables/rule'
|
4
|
+
|
5
|
+
module Tablomat
|
6
|
+
class IPTables
|
7
|
+
# The IPTables class is the interface to the iptables command
|
8
|
+
class Chain
|
9
|
+
attr_accessor :owned
|
10
|
+
attr_reader :table, :name, :active, :rules
|
11
|
+
|
12
|
+
def initialize(table, name, owned = true)
|
13
|
+
@system = table.system
|
14
|
+
@table = table
|
15
|
+
@name = name
|
16
|
+
@policy = 'ACCEPT'
|
17
|
+
@rules = {}
|
18
|
+
@rules_sorted = []
|
19
|
+
@owned = owned
|
20
|
+
@active = false
|
21
|
+
activate if @table.active
|
22
|
+
end
|
23
|
+
|
24
|
+
def policy(action)
|
25
|
+
# set policy as the last rule of the chain
|
26
|
+
raise 'Unable to assign policy to non builtin chains, TODO: implement handling' unless builtin?
|
27
|
+
|
28
|
+
@policy = action
|
29
|
+
return unless @active
|
30
|
+
|
31
|
+
command = "#{@table.system.iptables_bin} -t #{@table.name} -P #{@name} #{@policy}"
|
32
|
+
@system.exec command
|
33
|
+
end
|
34
|
+
|
35
|
+
def rule(name, owned = true, &block)
|
36
|
+
if name.is_a? Hash
|
37
|
+
name = sethandling(name) if name.key?(:set)
|
38
|
+
name = name.map { |k, v| "--#{k} #{v}" }.join(' ')
|
39
|
+
end
|
40
|
+
key = name.to_s.downcase
|
41
|
+
(@rules[key] || Rule.new(self, name, owned)).tap do |rule|
|
42
|
+
@rules[key] = rule
|
43
|
+
block&.call(rule)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def sethandling(name)
|
48
|
+
trash = {}
|
49
|
+
name.each do |k, v|
|
50
|
+
trash[k] = v
|
51
|
+
trash[:match] = trash.delete :set if trash.key?(:set)
|
52
|
+
end
|
53
|
+
trash
|
54
|
+
end
|
55
|
+
|
56
|
+
def insert(data, pos)
|
57
|
+
rule(data) do |rule|
|
58
|
+
rule.method = 'INSERT'
|
59
|
+
rule.position = pos
|
60
|
+
@rules_sorted.insert(pos - 1, rule)
|
61
|
+
update_rules_position
|
62
|
+
rule.activate if @active
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def append(data)
|
67
|
+
rule(data) do |rule|
|
68
|
+
@rules_sorted << rule
|
69
|
+
rule.activate if @active
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def update_rules_position
|
74
|
+
@rules_sorted = @rules_sorted.compact
|
75
|
+
@rules_sorted.select(&:active).each_with_index do |rule, index|
|
76
|
+
rule.position = index + 1 if (rule.position != 0) && (rule.position != (index + 1))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def delete(data)
|
81
|
+
rule = if data.is_a? Rule
|
82
|
+
data
|
83
|
+
else
|
84
|
+
self.rule(data)
|
85
|
+
end
|
86
|
+
rule.deactivate if rule.active
|
87
|
+
|
88
|
+
@rules_sorted.delete(rule)
|
89
|
+
@rules.delete_if { |_k, v| v.description == rule.description }
|
90
|
+
end
|
91
|
+
|
92
|
+
def activate(override = false)
|
93
|
+
return unless @owned || override
|
94
|
+
return if @active
|
95
|
+
|
96
|
+
@active = true
|
97
|
+
return if override
|
98
|
+
|
99
|
+
apply_create
|
100
|
+
activate_all_rules
|
101
|
+
end
|
102
|
+
|
103
|
+
def deactivate(override = false)
|
104
|
+
return unless @owned || override
|
105
|
+
return unless @active
|
106
|
+
|
107
|
+
@active = false
|
108
|
+
return if override
|
109
|
+
|
110
|
+
deactivate_all_rules
|
111
|
+
@active = false
|
112
|
+
end
|
113
|
+
|
114
|
+
def apply_create
|
115
|
+
unless exists?
|
116
|
+
begin
|
117
|
+
command = "#{@system.iptables_bin} -t #{@table.name} -N #{@name}"
|
118
|
+
@system.exec command
|
119
|
+
rescue StandardError
|
120
|
+
puts "Error: #{$ERROR_INFO}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
# apply policy if builtin chain
|
124
|
+
return unless builtin?
|
125
|
+
|
126
|
+
command = "#{@system.iptables_bin} -t #{@table.name} -P #{@name} #{@policy}"
|
127
|
+
@system.exec command
|
128
|
+
end
|
129
|
+
|
130
|
+
def apply_delete
|
131
|
+
return unless exists? && !builtin?
|
132
|
+
|
133
|
+
begin
|
134
|
+
command = "#{@system.iptables_bin} -t #{@table.name} -F #{@name}"
|
135
|
+
@system.exec command
|
136
|
+
command = "#{@system.iptables_bin} -t #{@table.name} -X #{@name}"
|
137
|
+
@system.exec command
|
138
|
+
rescue StandardError
|
139
|
+
puts "Error removing chain #{command}, message: #{$ERROR_INFO}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def exists?
|
144
|
+
command = "#{@system.iptables_bin} -t #{@table.name} -nL #{@name}"
|
145
|
+
@system.exec command
|
146
|
+
true
|
147
|
+
rescue StandardError
|
148
|
+
false
|
149
|
+
end
|
150
|
+
|
151
|
+
def builtin?
|
152
|
+
@table.system.builtin_chains.key?(@table.name.to_sym) && @table.system.builtin_chains[@table.name.to_sym].include?(@name)
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
def activate_all_rules
|
158
|
+
@rules_sorted.each do |rule|
|
159
|
+
rule.activate if !rule.nil? && !rule.active
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def deactivate_all_rules
|
164
|
+
@rules_sorted.each do |rule|
|
165
|
+
rule.deactivate if !rule.nil? && rule.active
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tablomat
|
4
|
+
class IPTables
|
5
|
+
# IPTables are made of Rules
|
6
|
+
class Rule
|
7
|
+
attr_accessor :owned, :active, :method, :position
|
8
|
+
attr_reader :chain, :description
|
9
|
+
|
10
|
+
def initialize(chain, description, owned = true)
|
11
|
+
@system = chain.table.system
|
12
|
+
@chain = chain
|
13
|
+
@description = description
|
14
|
+
@items = {}
|
15
|
+
@owned = owned
|
16
|
+
@active = false
|
17
|
+
@method = 'APPEND'
|
18
|
+
@position = 0
|
19
|
+
activate if @chain.active
|
20
|
+
end
|
21
|
+
|
22
|
+
def activate(override = false)
|
23
|
+
return unless @owned || override
|
24
|
+
return if @active
|
25
|
+
|
26
|
+
@active = true
|
27
|
+
return if override
|
28
|
+
|
29
|
+
@chain.activate unless @chain.active
|
30
|
+
apply_create
|
31
|
+
end
|
32
|
+
|
33
|
+
def deactivate(override = false)
|
34
|
+
return unless @owned || override
|
35
|
+
return unless @active
|
36
|
+
|
37
|
+
self.active = false
|
38
|
+
return if override
|
39
|
+
|
40
|
+
apply_delete
|
41
|
+
end
|
42
|
+
|
43
|
+
def apply_create
|
44
|
+
return unless @owned
|
45
|
+
|
46
|
+
method = if @method == 'APPEND'
|
47
|
+
"-A #{@chain.name}"
|
48
|
+
else
|
49
|
+
"-I #{@chain.name} #{@position}"
|
50
|
+
end
|
51
|
+
command = "#{@system.iptables_bin} -t #{@chain.table.name} #{method} #{@description}"
|
52
|
+
@system.exec command
|
53
|
+
end
|
54
|
+
|
55
|
+
def apply_delete
|
56
|
+
return unless @owned
|
57
|
+
|
58
|
+
command = "#{@system.iptables_bin} -t #{@chain.table.name} -D #{@chain.name} #{@description}"
|
59
|
+
@system.exec command
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tablomat/iptables/chain'
|
4
|
+
|
5
|
+
module Tablomat
|
6
|
+
class IPTables
|
7
|
+
# Puts the Table in IPTables
|
8
|
+
class Table
|
9
|
+
attr_accessor :owned
|
10
|
+
attr_reader :name, :system, :active, :chains
|
11
|
+
|
12
|
+
def initialize(system, name, owned = true)
|
13
|
+
@system = system
|
14
|
+
@name = name
|
15
|
+
@chains = {}
|
16
|
+
# whether this table is owned/created by us or external
|
17
|
+
@owned = owned
|
18
|
+
@active = false
|
19
|
+
activate if @system.active
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_key(name)
|
23
|
+
name.to_s.downcase[0, 28]
|
24
|
+
end
|
25
|
+
|
26
|
+
def chain(name, owned = true, &block)
|
27
|
+
key = get_key name
|
28
|
+
(@chains[key] || Chain.new(self, name, owned)).tap do |chain|
|
29
|
+
@chains[key] = chain
|
30
|
+
block&.call(chain)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def chain_exists(name)
|
35
|
+
key = get_key name
|
36
|
+
return true if @chains.key? key
|
37
|
+
|
38
|
+
false
|
39
|
+
end
|
40
|
+
|
41
|
+
def insert(chain_name, data, pos)
|
42
|
+
chain chain_name do |chain|
|
43
|
+
chain.insert(data, pos)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def append(chain_name, data)
|
48
|
+
chain(chain_name).append(data)
|
49
|
+
end
|
50
|
+
|
51
|
+
def delete(chain_name, data)
|
52
|
+
chain chain_name do |chain|
|
53
|
+
chain.delete(data)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def activate(override = false)
|
58
|
+
return unless @owned || override
|
59
|
+
|
60
|
+
@active = true
|
61
|
+
return if override
|
62
|
+
|
63
|
+
@chains.each do |_name, chain|
|
64
|
+
chain.activate
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def deactivate(override = false)
|
69
|
+
return unless @owned || override
|
70
|
+
|
71
|
+
@active = false
|
72
|
+
return if override
|
73
|
+
|
74
|
+
@chains.each do |_name, chain|
|
75
|
+
chain.deactivate
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def print
|
80
|
+
require 'pp'
|
81
|
+
pp self
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/spec/ipset_spec.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'etc'
|
5
|
+
|
6
|
+
describe Tablomat::IPSet do
|
7
|
+
describe 'Initialize module' do
|
8
|
+
@ipset = Tablomat::IPSet.new
|
9
|
+
end
|
10
|
+
|
11
|
+
before(:all) do
|
12
|
+
@command = 'ipset'
|
13
|
+
@command = "sudo #{@command}" if Etc.getlogin != 'root'
|
14
|
+
@ipset = Tablomat::IPSet.new
|
15
|
+
end
|
16
|
+
|
17
|
+
before(:each) do
|
18
|
+
@ipset.destroy_all(true)
|
19
|
+
end
|
20
|
+
|
21
|
+
after(:all) do
|
22
|
+
# destroy all sets
|
23
|
+
@ipset.destroy_all(true)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'creates new sets of type hash:ip' do
|
27
|
+
@ipset.create(set_name: 'test1', type: 'hash:ip')
|
28
|
+
expect(@ipset.set('test1').exists?).to be_truthy
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'can destroy existing sets' do
|
32
|
+
@ipset.create(set_name: 'test2', type: 'hash:ip')
|
33
|
+
expect(@ipset.set('test2').exists?).to be_truthy
|
34
|
+
@ipset.destroy(set_name: 'test2')
|
35
|
+
expect(@ipset.set('test2').exists?).to be_falsey
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'set() returns instance of Set' do
|
39
|
+
expect(@ipset.set('test2').exists?).to be_falsey
|
40
|
+
set_set = @ipset.set('test2')
|
41
|
+
set_set.create(type: 'hash:ip')
|
42
|
+
expect(@ipset.set('test2').exists?).to be_truthy
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'checks if set exists' do
|
46
|
+
# check precondition
|
47
|
+
expect(@ipset.set('test3').exists?).to be_falsy
|
48
|
+
|
49
|
+
# check existing
|
50
|
+
@ipset.create(set_name: 'test3', type: 'hash:ip')
|
51
|
+
expect(@ipset.set('test3').exists?).to be_truthy
|
52
|
+
|
53
|
+
# check missing
|
54
|
+
@ipset.destroy(set_name: 'test3')
|
55
|
+
expect(@ipset.set('test3').exists?).to be_falsy
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'can not create set if set with same name already exists' do
|
59
|
+
@ipset.create(set_name: 'test4', type: 'hash:ip')
|
60
|
+
expect(@ipset.set('test4').exists?).to be_truthy
|
61
|
+
expect { @ipset.create(set_name: 'test4', type: 'hash:port') }.to raise_error(Tablomat::IPSetError)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'can not delete set if the set does not exist' do
|
65
|
+
expect(@ipset.set('test6').exists?).to be_falsey
|
66
|
+
expect { @ipset.destroy(set_name: 'test6') }.to raise_error(Tablomat::IPSetError)
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'appends entrys to set in ipset' do
|
70
|
+
@ipset.create(set_name: 'test6', type: 'hash:ip')
|
71
|
+
expect(@ipset.set('test6').entry('192.0.2.42').exists?).to be_falsy
|
72
|
+
@ipset.add(set_name: 'test6', entry_data: '192.0.2.42')
|
73
|
+
expect(@ipset.set('test6').entry('192.0.2.42').exists?).to be_truthy
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'appends entrys to set in set' do
|
77
|
+
@ipset.create(set_name: 'test6', type: 'hash:ip')
|
78
|
+
expect(@ipset.set('test6').entry('192.0.2.52').exists?).to be_falsy
|
79
|
+
@ipset.set('test6').add(entry_data: '192.0.2.52')
|
80
|
+
expect(@ipset.set('test6').entry('192.0.2.52').exists?).to be_truthy
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'can delete entrys in sets in ipset' do
|
84
|
+
@ipset.create(set_name: 'test7', type: 'hash:ip')
|
85
|
+
@ipset.add(set_name: 'test7', entry_data: '192.0.2.42')
|
86
|
+
expect(@ipset.set('test7').entry('192.0.2.42').exists?).to be_truthy
|
87
|
+
@ipset.del(set_name: 'test7', entry_data: '192.0.2.42')
|
88
|
+
expect(@ipset.set('test7').entry('192.0.2.42').exists?).to be_falsy
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'can delete entrys in sets in set' do
|
92
|
+
@ipset.create(set_name: 'test7', type: 'hash:ip')
|
93
|
+
@ipset.set('test7').add(entry_data: '192.0.2.42')
|
94
|
+
expect(@ipset.set('test7').entry('192.0.2.42').exists?).to be_truthy
|
95
|
+
@ipset.set('test7').del(entry_data: '192.0.2.42')
|
96
|
+
expect(@ipset.set('test7').entry('192.0.2.42').exists?).to be_falsy
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'creates set with type bitmap:port' do
|
100
|
+
@ipset.create(set_name: 'test8', type: 'bitmap:port', rangefrom: 500_00, rangeto: 600_00)
|
101
|
+
expect(@ipset.set('test8').exists?).to be_truthy
|
102
|
+
expect(@ipset.set('test8').type == 'bitmap:port').to be_truthy
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'creates set with type hash:ip,port' do
|
106
|
+
@ipset.create(set_name: 'test9', type: 'hash:ip,port')
|
107
|
+
expect(@ipset.set('test9').exists?).to be_truthy
|
108
|
+
expect(@ipset.set('test9').type == 'hash:ip,port').to be_truthy
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'can destroy created in one instance' do
|
112
|
+
# creating sets in an instance
|
113
|
+
@ipset.create(set_name: 'test10', type: 'bitmap:port', rangefrom: 500_00, rangeto: 600_00)
|
114
|
+
expect(@ipset.set('test10').exists?).to be_truthy
|
115
|
+
@ipset.create(set_name: 'test11', type: 'hash:mac')
|
116
|
+
expect(@ipset.set('test11').exists?).to be_truthy
|
117
|
+
# creating sets in another instance
|
118
|
+
ipsetnext = Tablomat::IPSet.new
|
119
|
+
ipsetnext.create(set_name: 'test12', type: 'bitmap:port', rangefrom: 500_00, rangeto: 600_00)
|
120
|
+
expect(@ipset.set('test12').exists?).to be_truthy
|
121
|
+
ipsetnext.create(set_name: 'test13', type: 'hash:ip,port')
|
122
|
+
expect(@ipset.set('test13').exists?).to be_truthy
|
123
|
+
ipsetnext.create(set_name: 'test14', type: 'hash:net')
|
124
|
+
expect(@ipset.set('test14').exists?).to be_truthy
|
125
|
+
# destroy sets created with ipsetnext
|
126
|
+
ipsetnext.destroy_all
|
127
|
+
expect(@ipset.set('test10').exists?).to be_truthy
|
128
|
+
expect(@ipset.set('test11').exists?).to be_truthy
|
129
|
+
expect(@ipset.set('test12').exists?).to be_falsey
|
130
|
+
expect(@ipset.set('test13').exists?).to be_falsey
|
131
|
+
expect(@ipset.set('test14').exists?).to be_falsey
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'can destroy all sets' do
|
135
|
+
# creating sets in another instance
|
136
|
+
ipsetnext = Tablomat::IPSet.new
|
137
|
+
ipsetnext.create(set_name: 'test14', type: 'hash:net')
|
138
|
+
expect(@ipset.set('test14').exists?).to be_truthy
|
139
|
+
ipsetnext.destroy_all(true)
|
140
|
+
expect(@ipset.set('test10').exists?).to be_falsey
|
141
|
+
expect(@ipset.set('test11').exists?).to be_falsey
|
142
|
+
expect(@ipset.set('test14').exists?).to be_falsey
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'etc'
|
5
|
+
|
6
|
+
describe Tablomat::IPTables, Tablomat::IPSet do
|
7
|
+
describe 'Initialize module' do
|
8
|
+
@ipset = Tablomat::IPSet.new
|
9
|
+
@iptables = Tablomat::IPTables.new
|
10
|
+
end
|
11
|
+
|
12
|
+
before(:all) do
|
13
|
+
@command = 'ipset'
|
14
|
+
@command = "sudo #{@command}" if Etc.getlogin != 'root'
|
15
|
+
@ipset = Tablomat::IPSet.new
|
16
|
+
@iptables = Tablomat::IPTables.new
|
17
|
+
@iptables.activate
|
18
|
+
end
|
19
|
+
|
20
|
+
after(:all) do
|
21
|
+
# cleanup table and ipsets
|
22
|
+
@iptables.table('mangle').chain('custom', &:apply_delete)
|
23
|
+
@ipset.destroy_all
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'can create and delete rules using ipsets if set exists' do
|
27
|
+
# initialize ipset and create set
|
28
|
+
@ipset.create(set_name: 'testset1', type: 'hash:ip')
|
29
|
+
# create rule
|
30
|
+
@iptables.append('mangle', 'custom', set: @ipset.matchset(set_name: 'testset1', flags: 'src'), protocol: :tcp, sport: 123)
|
31
|
+
expect(@iptables.exists('mangle', 'custom', set: @ipset.matchset(set_name: 'testset1', flags: 'src'), protocol: :tcp, sport: 123)).to be_truthy
|
32
|
+
@iptables.delete('mangle', 'custom', set: @ipset.matchset(set_name: 'testset1', flags: 'src'), protocol: :tcp, sport: 123)
|
33
|
+
expect(@iptables.exists('mangle', 'custom', set: @ipset.matchset(set_name: 'testset1', flags: 'src'), protocol: :tcp, sport: 123)).to be_falsey
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'can create and delete rules using ipsets with Options' do
|
37
|
+
@iptables.append('mangle', 'custom', match: @ipset.matchset(set_name: 'testset1', flags: 'src', option: '--update-subcounters', negate_option: true, negate: true), protocol: :tcp, sport: 123)
|
38
|
+
expect(@iptables.exists('mangle', 'custom', match: @ipset.matchset(set_name: 'testset1', flags: 'src', option: '--update-subcounters', negate_option: true, negate: true), protocol: :tcp, sport: 123)).to be_truthy
|
39
|
+
@iptables.delete('mangle', 'custom', match: @ipset.matchset(set_name: 'testset1', flags: 'src', option: '--update-subcounters', negate_option: true, negate: true), protocol: :tcp, sport: 123)
|
40
|
+
expect(@iptables.exists('mangle', 'custom', match: @ipset.matchset(set_name: 'testset1', flags: 'src', option: '--update-subcounters', negate_option: true, negate: true), protocol: :tcp, sport: 123)).to be_falsey
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'can not create rules using a set if the set does not exist' do
|
44
|
+
expect(@ipset.set('testset2').exists?).to be_falsey
|
45
|
+
expect { @iptables.append('mangle', 'custom', match: @ipset.matchset(set_name: 'testset', flags: 'src'), protocol: :tcp, sport: 123) }.to raise_error(RuntimeError)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'can not destroy a set if a rule is using it' do
|
49
|
+
@ipset.create(set_name: 'testset3', type: 'hash:ip')
|
50
|
+
@iptables.append('mangle', 'custom', match: @ipset.matchset(set_name: 'testset3', flags: 'src'), protocol: :tcp, sport: 123)
|
51
|
+
expect(@iptables.exists('mangle', 'custom', match: @ipset.matchset(set_name: 'testset3', flags: 'src'), protocol: :tcp, sport: 123)).to be_truthy
|
52
|
+
expect { @ipset.destroy(set_name: 'testset3') }.to raise_error(Tablomat::IPSetError)
|
53
|
+
@iptables.delete('mangle', 'custom', match: @ipset.matchset(set_name: 'testset3', flags: 'src'), protocol: :tcp, sport: 123)
|
54
|
+
expect(@iptables.exists('mangle', 'custom', match: @ipset.matchset(set_name: 'testset3', flags: 'src'), protocol: :tcp, sport: 123)).to be_falsey
|
55
|
+
@ipset.destroy(set_name: 'testset3')
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'lists the set in active rules, if a set is given' do
|
59
|
+
@ipset.create(set_name: 'testset4', type: 'hash:ip')
|
60
|
+
@iptables.append('mangle', 'custom', match: @ipset.matchset(set_name: 'testset4', flags: 'src'), protocol: :tcp, sport: 123)
|
61
|
+
active_rules = @iptables.get_active_rules('mangle', 'custom')
|
62
|
+
expect(active_rules[-1][:match]).to eq('testset4')
|
63
|
+
@iptables.delete('mangle', 'custom', match: @ipset.matchset(set_name: 'testset4', flags: 'src'), protocol: :tcp, sport: 123)
|
64
|
+
@ipset.destroy(set_name: 'testset4')
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'does not list a set in active rules, if no set is given' do
|
68
|
+
@ipset.create(set_name: 'testset5', type: 'hash:ip')
|
69
|
+
@iptables.append('mangle', 'custom', source: '10.0.0.1', protocol: :tcp, sport: 123)
|
70
|
+
active_rules = @iptables.get_active_rules('mangle', 'custom')
|
71
|
+
expect(active_rules[-1].key?(:match)).to be_falsey
|
72
|
+
@iptables.delete('mangle', 'custom', source: '10.0.0.1', protocol: :tcp, sport: 123)
|
73
|
+
@ipset.destroy(set_name: 'testset5')
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'etc'
|
5
|
+
|
6
|
+
describe Tablomat::IPTables do
|
7
|
+
describe 'Initialize module' do
|
8
|
+
@iptables = Tablomat::IPTables.new
|
9
|
+
end
|
10
|
+
|
11
|
+
before(:all) do
|
12
|
+
@command = 'iptables'
|
13
|
+
@command = "sudo #{@command}" if Etc.getlogin != 'root'
|
14
|
+
|
15
|
+
@iptables = Tablomat::IPTables.new
|
16
|
+
@iptables.activate
|
17
|
+
end
|
18
|
+
|
19
|
+
after(:all) do
|
20
|
+
# cleanup tables
|
21
|
+
@iptables.table('mangle').chain('test', &:apply_delete)
|
22
|
+
@iptables.table('mangle').chain('custom', &:apply_delete)
|
23
|
+
@iptables.table('filter').chain('rspec', &:apply_delete)
|
24
|
+
@iptables.table('nat').chain('rspec', &:apply_delete)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'does raise an error when getting an invalid ruleset' do
|
28
|
+
output = <<~IPTABLE
|
29
|
+
*nat
|
30
|
+
*mangle
|
31
|
+
:PREROUTING ACCEPT [57:17363]
|
32
|
+
:INPUT ACCEPT [57:17363]
|
33
|
+
:FORWARD ACCEPT [0:0]
|
34
|
+
:OUTPUT ACCEPT [60:16575]
|
35
|
+
:POSTROUTING ACCEPT [60:16575]
|
36
|
+
COMMIT
|
37
|
+
*filter
|
38
|
+
IPTABLE
|
39
|
+
expect { @iptables.parse_output(output) }.to raise_error(StandardError)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'converts rules to iptables-save format via tmp chain' do
|
43
|
+
expect(@iptables.normalize(protocol: :tcp, sport: 123, source: '1.2.3.4')).to eq('-s 1.2.3.4/32 -p tcp -m tcp --sport 123')
|
44
|
+
expect(@iptables.normalize('-s 1.2.3.4/32 -p tcp -m tcp --sport 123')).to eq('-s 1.2.3.4/32 -p tcp -m tcp --sport 123')
|
45
|
+
expect(@iptables.normalize('-s 1.2.3.4/32 -m tcp --sport 123 -p tcp')).to eq('-s 1.2.3.4/32 -p tcp -m tcp --sport 123')
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'gets current ruleset' do
|
49
|
+
# remove all existing rules in mangle FORWARD (hurts running host rules during test)
|
50
|
+
`#{@command}-save > /tmp/iptables.save`
|
51
|
+
`#{@command} -t mangle -F FORWARD`
|
52
|
+
`#{@command} -t mangle -A FORWARD -p tcp -j ACCEPT`
|
53
|
+
@iptables.synchronize
|
54
|
+
|
55
|
+
`#{@command}-restore < /tmp/iptables.save`
|
56
|
+
# @iptables.print
|
57
|
+
|
58
|
+
# check that filter:INPUT is not owned!
|
59
|
+
expect(@iptables.table('filter').chain('INPUT').owned).to be_falsey
|
60
|
+
|
61
|
+
expect(@iptables.table('mangle').chain('FORWARD').rule(@iptables.normalize(protocol: :tcp, jump: :ACCEPT)).owned).to be_falsey
|
62
|
+
expect(@iptables.table('mangle').chain('FORWARD').rule(@iptables.normalize(protocol: :tcp, jump: :ACCEPT)).active).to be_truthy
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'creates new chain in table mangle' do
|
66
|
+
@iptables.table('mangle').chain('custom').activate
|
67
|
+
expect(@iptables.table('mangle').chain('custom').active).to be_truthy
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'allows to retrieve active rules' do
|
71
|
+
@iptables.append('nat', 'PREROUTING', source: '127.0.0.2',
|
72
|
+
destination: '127.0.0.3',
|
73
|
+
protocol: :tcp,
|
74
|
+
dport: 8080,
|
75
|
+
jump: 'DNAT', to: '127.0.0.4:9090')
|
76
|
+
target_hash = { source: '127.0.0.2', destination: '127.0.0.3', dport: '8080', jump: 'DNAT', protocol: 'tcp', to_dest: '127.0.0.4:9090' }
|
77
|
+
rules = @iptables.get_active_rules
|
78
|
+
expect(rules.length).to eq(2)
|
79
|
+
expect(rules.pop).to eq(target_hash)
|
80
|
+
@iptables.delete('nat', 'PREROUTING', source: '127.0.0.2',
|
81
|
+
destination: '127.0.0.3',
|
82
|
+
protocol: :tcp,
|
83
|
+
dport: 8080,
|
84
|
+
jump: 'DNAT', to: '127.0.0.4:9090')
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'is not possible to set policy for non builtin chains' do
|
88
|
+
expect do
|
89
|
+
@iptables.table('mangle').chain('custom') do |chain|
|
90
|
+
chain.policy 'RETURN'
|
91
|
+
chain.apply
|
92
|
+
end
|
93
|
+
end.to raise_error('Unable to assign policy to non builtin chains, TODO: implement handling')
|
94
|
+
end
|
95
|
+
|
96
|
+
# it "can set policy for builtin chains" do
|
97
|
+
# @iptables.table("mangle").chain("INPUT") do | chain |
|
98
|
+
# chain.set_policy "ACCEPT"
|
99
|
+
# end
|
100
|
+
# @iptables.activate
|
101
|
+
# end
|
102
|
+
|
103
|
+
it 'can remove chains' do
|
104
|
+
# create chain
|
105
|
+
@iptables.table('mangle').chain('test').activate
|
106
|
+
expect(@iptables.exists('mangle', 'test')).to be_truthy
|
107
|
+
@iptables.table('mangle').chain('test').deactivate
|
108
|
+
expect(@iptables.exists('mangle', 'test')).to be_falsey
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'checks if rule already exists' do
|
112
|
+
# check precondition
|
113
|
+
expect(@iptables.exists('mangle', 'custom', protocol: :tcp, sport: 123)).to be_falsy
|
114
|
+
|
115
|
+
# check existing
|
116
|
+
@iptables.append('mangle', 'custom', protocol: :tcp, sport: 123)
|
117
|
+
expect(@iptables.exists('mangle', 'custom', protocol: :tcp, sport: 123)).to be_truthy
|
118
|
+
expect(@iptables.table('mangle').chain('FORWARD').rule(@iptables.normalize(protocol: :tcp, jump: :ACCEPT)).active).to be_truthy
|
119
|
+
|
120
|
+
# check missing
|
121
|
+
@iptables.delete('mangle', 'custom', protocol: :tcp, sport: 123)
|
122
|
+
expect(@iptables.exists('mangle', 'custom', protocol: :tcp, sport: 123)).to be_falsy
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'appends new rule via system' do
|
126
|
+
expect(@iptables.exists('mangle', 'custom', protocol: :tcp, sport: 123)).to be_falsy
|
127
|
+
@iptables.append('mangle', 'custom', protocol: :tcp, sport: 123)
|
128
|
+
expect(@iptables.table('mangle').chain('FORWARD').rule(@iptables.normalize(protocol: :tcp, jump: :ACCEPT)).owned).to be_falsey
|
129
|
+
expect(@iptables.table('mangle').chain('FORWARD').rule(@iptables.normalize(protocol: :tcp, jump: :ACCEPT)).active).to be_truthy
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'can remove rules via system' do
|
133
|
+
expect(@iptables.exists('mangle', 'custom', protocol: :tcp, sport: 123)).to be_truthy
|
134
|
+
@iptables.delete('mangle', 'custom', protocol: :tcp, sport: 123)
|
135
|
+
expect(@iptables.exists('mangle', 'custom', protocol: :tcp, sport: 123)).to be_falsey
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'changes all rules for a specific source address to another source address' do
|
139
|
+
@iptables.append('filter', 'rspec', protocol: :tcp, sport: 123, source: '1.2.3.5')
|
140
|
+
@iptables.switch_sources('1.2.3.5', '5.3.2.1')
|
141
|
+
expect(@iptables.exists('filter', 'rspec', source: '5.3.2.1', protocol: :tcp, sport: 123)).to be_truthy
|
142
|
+
expect(@iptables.exists('filter', 'rspec', source: '1.2.3.5', protocol: :tcp, sport: 123)).to be_falsey
|
143
|
+
@iptables.append('nat', 'rspec', protocol: :tcp, sport: 123, source: '1.2.3.5', jump: :DNAT, 'to-destination': '127.0.0.1:123')
|
144
|
+
@iptables.switch_sources('1.2.3.5', '5.3.2.1')
|
145
|
+
expect(@iptables.exists('nat', 'rspec', source: '5.3.2.1', protocol: :tcp, sport: 123, jump: :DNAT, 'to-destination': '127.0.0.1:123')).to be_truthy
|
146
|
+
expect(@iptables.exists('nat', 'rspec', source: '1.2.3.5', protocol: :tcp, sport: 123, jump: :DNAT, 'to-destination': '127.0.0.1:123')).to be_falsey
|
147
|
+
end
|
148
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'simplecov'
|
4
|
+
SimpleCov.start do
|
5
|
+
enable_coverage :branch
|
6
|
+
add_filter '/spec/'
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rubygems'
|
10
|
+
require 'bundler/setup'
|
11
|
+
require 'tablomat'
|
12
|
+
|
13
|
+
RSpec.configure do |config|
|
14
|
+
# RSpec::Core::Configuration#treat_symbols_as_metadata_keys_with_true_values= is deprecated, it is now set to true as default and setting it to false has no effect.
|
15
|
+
# config.treat_symbols_as_metadata_keys_with_true_values = true
|
16
|
+
config.raise_errors_for_deprecations!
|
17
|
+
config.run_all_when_everything_filtered = true
|
18
|
+
config.filter_run :focus
|
19
|
+
|
20
|
+
config.order = 'random'
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tablomat
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.2.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matthias Wübbeling
|
8
|
+
- Arnold Sykosch
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2021-03-12 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 13.0.1
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 13.0.1
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rspec
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 3.9.0
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 3.9.0
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: rubocop
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 0.88.0
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 0.88.0
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rubocop-performance
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 1.7.1
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 1.7.1
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: simplecov
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 0.18.5
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: 0.18.5
|
84
|
+
description: A simple wrapper for iptables
|
85
|
+
email:
|
86
|
+
- matthias.wuebbeling@cs.uni-bonn.de
|
87
|
+
- sykosch@cs.uni-bonn.de
|
88
|
+
executables: []
|
89
|
+
extensions: []
|
90
|
+
extra_rdoc_files: []
|
91
|
+
files:
|
92
|
+
- lib/tablomat.rb
|
93
|
+
- lib/tablomat/exec.rb
|
94
|
+
- lib/tablomat/ipset.rb
|
95
|
+
- lib/tablomat/ipset/entry.rb
|
96
|
+
- lib/tablomat/ipset/set.rb
|
97
|
+
- lib/tablomat/iptables.rb
|
98
|
+
- lib/tablomat/iptables/chain.rb
|
99
|
+
- lib/tablomat/iptables/rule.rb
|
100
|
+
- lib/tablomat/iptables/table.rb
|
101
|
+
- lib/tablomat/version.rb
|
102
|
+
- spec/ipset_spec.rb
|
103
|
+
- spec/ipsetiptables_spec.rb
|
104
|
+
- spec/iptables_spec.rb
|
105
|
+
- spec/spec_helper.rb
|
106
|
+
homepage: https://gitlab.com/itsape/tablomat
|
107
|
+
licenses:
|
108
|
+
- MIT
|
109
|
+
metadata: {}
|
110
|
+
post_install_message:
|
111
|
+
rdoc_options: []
|
112
|
+
require_paths:
|
113
|
+
- lib
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
requirements: []
|
125
|
+
rubygems_version: 3.0.3
|
126
|
+
signing_key:
|
127
|
+
specification_version: 4
|
128
|
+
summary: This wrapper provides an interface to iptables.
|
129
|
+
test_files:
|
130
|
+
- spec/ipset_spec.rb
|
131
|
+
- spec/ipsetiptables_spec.rb
|
132
|
+
- spec/iptables_spec.rb
|
133
|
+
- spec/spec_helper.rb
|