tablomat 1.2.1
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/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
|