shutter 0.0.7 → 0.1.0
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.
- data/README.md +26 -56
- data/bin/shutter +10 -4
- data/lib/shutter.rb +3 -9
- data/lib/shutter/command_line.rb +86 -104
- data/lib/shutter/content.rb +49 -2
- data/lib/shutter/files.rb +31 -0
- data/lib/shutter/iptables.rb +215 -10
- data/lib/shutter/os.rb +40 -1
- data/lib/shutter/version.rb +1 -1
- data/shutter.gemspec +4 -3
- data/spec/command_line_spec.rb +75 -9
- data/spec/content_spec.rb +2 -2
- data/spec/files/base.ipt +160 -0
- data/spec/files/iface.dmz +4 -0
- data/spec/files/iface.forward +3 -0
- data/spec/files/ip.allow +5 -0
- data/spec/files/ip.deny +5 -0
- data/spec/files/iptables_save.out +86 -0
- data/spec/files/ports.private +2 -0
- data/spec/files/ports.public +3 -0
- data/spec/files_spec.rb +76 -0
- data/spec/iptables_spec.rb +157 -0
- data/spec/os_spec.rb +54 -0
- data/spec/spec_helper.rb +10 -4
- metadata +45 -14
- data/lib/shutter/iptables/base.rb +0 -59
- data/lib/shutter/iptables/eyepee.rb +0 -34
- data/lib/shutter/iptables/forward.rb +0 -47
- data/lib/shutter/iptables/iface.rb +0 -30
- data/lib/shutter/iptables/jail.rb +0 -26
- data/lib/shutter/iptables/port.rb +0 -35
- data/spec/env_spec.rb +0 -17
@@ -0,0 +1,31 @@
|
|
1
|
+
module Shutter
|
2
|
+
class Files
|
3
|
+
include Shutter::Content
|
4
|
+
|
5
|
+
class << self
|
6
|
+
include Shutter::Content
|
7
|
+
def create(dir, overwrite=false, except=[])
|
8
|
+
CONFIG_FILES.each do |name|
|
9
|
+
file = "#{dir}/#{name}"
|
10
|
+
if !File.exists?(file) || overwrite || except.include?(name)
|
11
|
+
File.open(file, 'w') do |f|
|
12
|
+
f.write(const_get(name.upcase.gsub(/\./, "_")))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_config_dir(config_path)
|
19
|
+
# Check to see if the path to the config files exist
|
20
|
+
unless File.directory?(config_path)
|
21
|
+
begin
|
22
|
+
Dir.mkdir(config_path)
|
23
|
+
rescue Errno::ENOENT
|
24
|
+
raise "Could not create the configuration directory. Check to see if the parent directory exists."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
data/lib/shutter/iptables.rb
CHANGED
@@ -1,13 +1,218 @@
|
|
1
|
-
require 'shutter/iptables/base'
|
2
|
-
require 'shutter/iptables/eyepee'
|
3
|
-
require 'shutter/iptables/iface'
|
4
|
-
require 'shutter/iptables/jail'
|
5
|
-
require 'shutter/iptables/port'
|
6
|
-
require 'shutter/iptables/forward'
|
7
|
-
require 'shutter/os'
|
8
|
-
|
9
1
|
module Shutter
|
10
|
-
module
|
11
|
-
|
2
|
+
module Firewall
|
3
|
+
class IPTables
|
4
|
+
RULES_DMZ_BLOCK = "# [RULES:DMZ]"
|
5
|
+
RULES_FORWARD_BLOCK = "# [RULES:FORWARD]"
|
6
|
+
RULES_POSTROUTING_BLOCK = "# [RULES:POSTROUTING]"
|
7
|
+
RULES_BASTARDS_BLOCK = "# [RULES:BASTARDS]"
|
8
|
+
RULES_PUBLIC_BLOCK = "# [RULES:PUBLIC]"
|
9
|
+
RULES_ALLOWIP_BLOCK = "# [RULES:ALLOWIP]"
|
10
|
+
RULES_PRIVATE_BLOCK = "# [RULES:PRIVATE]"
|
11
|
+
RULES_FAIL2BAN_BLOCK = "# [RULES:FAIL2BAN]"
|
12
|
+
RULES_JAIL_BLOCK = "# [RULES:JAIL]"
|
13
|
+
CHAIN_FAIL2BAN_BLOCK = "# [CHAIN:FAIL2BAN]"
|
14
|
+
|
15
|
+
def initialize(path)
|
16
|
+
@path = path
|
17
|
+
@base = read("base.ipt",false).join("\n")
|
18
|
+
@iface_forward = read("iface.forward")
|
19
|
+
@ports_private = read("ports.private")
|
20
|
+
@ports_public = read("ports.public")
|
21
|
+
@ip_allow = read("ip.allow")
|
22
|
+
@ip_deny = read("ip.deny")
|
23
|
+
@dmz_device = read("iface.dmz")
|
24
|
+
@os = Shutter::OS.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def base_sub(block,content)
|
28
|
+
@base = @base.gsub(/#{Regexp.quote(block)}/, content)
|
29
|
+
end
|
30
|
+
|
31
|
+
def generate
|
32
|
+
base_sub(RULES_DMZ_BLOCK, dmz_device_block)
|
33
|
+
base_sub(RULES_FORWARD_BLOCK, forward_block)
|
34
|
+
base_sub(RULES_POSTROUTING_BLOCK, postrouting_block)
|
35
|
+
base_sub(RULES_BASTARDS_BLOCK, deny_ip_block)
|
36
|
+
base_sub(RULES_PUBLIC_BLOCK, allow_public_port_block)
|
37
|
+
base_sub(RULES_ALLOWIP_BLOCK, allow_ip_block)
|
38
|
+
base_sub(RULES_PRIVATE_BLOCK, allow_private_port_block)
|
39
|
+
base_sub(RULES_FAIL2BAN_BLOCK, fail2ban_rules_block)
|
40
|
+
base_sub(RULES_JAIL_BLOCK, jail_rules_block)
|
41
|
+
base_sub(CHAIN_FAIL2BAN_BLOCK, fail2ban_chains_block)
|
42
|
+
clean
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
@base
|
47
|
+
end
|
48
|
+
|
49
|
+
def clean
|
50
|
+
@base = @base.gsub(/^#.*$/, "")
|
51
|
+
@base = @base.gsub(/^$\n/, "")
|
52
|
+
end
|
53
|
+
|
54
|
+
def read(file, filter=true)
|
55
|
+
#puts "Reading: #{@path}/#{file}"
|
56
|
+
lines = File.read("#{@path}/#{file}").split("\n")
|
57
|
+
# Doesn't work with 1.8.x
|
58
|
+
# lines.keep_if{ |line| line =~ /^[a-z0-9].+$/ } if filter
|
59
|
+
# so since we are iterating through this, well handle the stripping as well
|
60
|
+
# lines.map { |line| line.strip }
|
61
|
+
newlines = []
|
62
|
+
lines.each do |line|
|
63
|
+
if filter
|
64
|
+
newlines << line.strip if line =~ /^[a-z0-9].+$/
|
65
|
+
else
|
66
|
+
newlines << line.strip
|
67
|
+
end
|
68
|
+
end
|
69
|
+
newlines
|
70
|
+
end
|
71
|
+
|
72
|
+
def save
|
73
|
+
puts self.generate
|
74
|
+
end
|
75
|
+
|
76
|
+
def restore
|
77
|
+
IO.popen("#{iptables_restore}", "r+") do |iptr|
|
78
|
+
iptr.puts self.generate ; iptr.close_write
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def persist(pfile)
|
83
|
+
File.open(pfile, "w") do |f|
|
84
|
+
f.write(@base)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
###
|
89
|
+
### IPTables Commands
|
90
|
+
###
|
91
|
+
def iptables_save
|
92
|
+
@iptable_save ||= `"#{@os.iptables_save}"`
|
93
|
+
end
|
94
|
+
|
95
|
+
def iptables_restore
|
96
|
+
"#{@os.iptables_restore}"
|
97
|
+
end
|
98
|
+
|
99
|
+
###
|
100
|
+
### Block Generation
|
101
|
+
###
|
102
|
+
def forward_block
|
103
|
+
content = ""
|
104
|
+
@iface_forward.each do |line|
|
105
|
+
src, dst = line.split(' ')
|
106
|
+
content += self.forward_content(src,dst)
|
107
|
+
end
|
108
|
+
content
|
109
|
+
end
|
110
|
+
|
111
|
+
def postrouting_block
|
112
|
+
masq_ifaces = []
|
113
|
+
content = ""
|
114
|
+
@iface_forward.each do |line|
|
115
|
+
src, dst = line.split(' ')
|
116
|
+
content += self.postrouting_content(dst) unless masq_ifaces.include?(dst)
|
117
|
+
masq_ifaces << dst
|
118
|
+
end
|
119
|
+
content
|
120
|
+
end
|
121
|
+
|
122
|
+
def allow_private_port_block
|
123
|
+
content = ""
|
124
|
+
@ports_private.each do |line|
|
125
|
+
port,proto = line.split
|
126
|
+
content += self.allow_private_port_content(port, proto)
|
127
|
+
end
|
128
|
+
content
|
129
|
+
end
|
130
|
+
|
131
|
+
def allow_public_port_block
|
132
|
+
content = ""
|
133
|
+
@ports_public.each do |line|
|
134
|
+
port,proto = line.split
|
135
|
+
raise "Invalid port in port.allow" unless port =~ /^[0-9].*$/
|
136
|
+
raise "Invalid protocol in port.allow" unless proto =~ /^(tcp|udp)$/
|
137
|
+
content += self.allow_public_port_content(port, proto)
|
138
|
+
end
|
139
|
+
content
|
140
|
+
end
|
141
|
+
|
142
|
+
def allow_ip_block
|
143
|
+
content = ""
|
144
|
+
@ip_allow.each do |line|
|
145
|
+
raise "Invalid IP address in ip.allow" unless line =~ /^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}(\/[0-9]{0,2})*$/
|
146
|
+
content += self.allow_ip_content(line)
|
147
|
+
end
|
148
|
+
content
|
149
|
+
end
|
150
|
+
|
151
|
+
def deny_ip_block
|
152
|
+
content = ""
|
153
|
+
@ip_deny.each do |line|
|
154
|
+
raise "Invalid IP address in ip.deny" unless line =~ /^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}(\/[0-9]{0,2})*$/
|
155
|
+
content += self.deny_ip_content(line)
|
156
|
+
end
|
157
|
+
content
|
158
|
+
end
|
159
|
+
|
160
|
+
def dmz_device_block
|
161
|
+
content = ""
|
162
|
+
@dmz_device.each do |line|
|
163
|
+
raise "Invalid device in iface.dmz" unless line =~ /^[a-z][a-z0-9].*$/
|
164
|
+
content += self.dmz_device_content(line)
|
165
|
+
end
|
166
|
+
content
|
167
|
+
end
|
168
|
+
|
169
|
+
def fail2ban_chains_block
|
170
|
+
iptables_save.scan(/^:fail2ban.*$/).join("\n")
|
171
|
+
end
|
172
|
+
|
173
|
+
def fail2ban_rules_block
|
174
|
+
iptables_save.scan(/^-A fail2ban.*$/).join("\n")
|
175
|
+
end
|
176
|
+
|
177
|
+
def jail_rules_block
|
178
|
+
lines = iptables_save.scan(/^-A Jail.*$/)
|
179
|
+
lines << "-A Jail -j RETURN\n" unless lines.last =~ /-A Jail -j RETURN/
|
180
|
+
lines.join("\n")
|
181
|
+
end
|
182
|
+
|
183
|
+
###
|
184
|
+
### Block Content
|
185
|
+
###
|
186
|
+
def forward_content(src,dst)
|
187
|
+
rule = "-A FORWARD -i #{src} -o #{dst} -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT\n"
|
188
|
+
rule += "-A FORWARD -i #{dst} -o #{src} -m state --state RELATED,ESTABLISHED -j ACCEPT\n"
|
189
|
+
rule
|
190
|
+
end
|
191
|
+
|
192
|
+
def postrouting_content(iface)
|
193
|
+
"-A POSTROUTING -o #{iface} -j MASQUERADE\n"
|
194
|
+
end
|
195
|
+
|
196
|
+
def allow_private_port_content(port, proto)
|
197
|
+
"-A Private -m state --state NEW -p #{proto} -m #{proto} --dport #{port} -j RETURN\n"
|
198
|
+
end
|
199
|
+
|
200
|
+
def allow_public_port_content(port, proto)
|
201
|
+
"-A Public -m state --state NEW -p #{proto} -m #{proto} --dport #{port} -j ACCEPT\n"
|
202
|
+
end
|
203
|
+
|
204
|
+
def allow_ip_content(ip)
|
205
|
+
"-A AllowIP -m state --state NEW -s #{ip} -j Allowed\n"
|
206
|
+
end
|
207
|
+
|
208
|
+
def deny_ip_content(ip)
|
209
|
+
"-A Bastards -s #{ip} -j DropBastards\n"
|
210
|
+
end
|
211
|
+
|
212
|
+
def dmz_device_content(iface)
|
213
|
+
"-A Dmz -i #{iface} -j ACCEPT\n"
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|
12
217
|
end
|
13
218
|
end
|
data/lib/shutter/os.rb
CHANGED
@@ -6,6 +6,12 @@ module Shutter
|
|
6
6
|
end
|
7
7
|
end
|
8
8
|
|
9
|
+
def validate!
|
10
|
+
if unknown?
|
11
|
+
raise "ERROR: Unsupported operating system"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
9
15
|
def family
|
10
16
|
@family ||= ENV['OS'] ? ENV['OS'] : RUBY_PLATFORM.split('-').last
|
11
17
|
end
|
@@ -15,7 +21,28 @@ module Shutter
|
|
15
21
|
end
|
16
22
|
|
17
23
|
def linux?
|
18
|
-
|
24
|
+
family == "linux"
|
25
|
+
end
|
26
|
+
|
27
|
+
def iptables_save
|
28
|
+
"/sbin/iptables-save"
|
29
|
+
end
|
30
|
+
|
31
|
+
def iptables_restore
|
32
|
+
"/sbin/iptables-restore"
|
33
|
+
end
|
34
|
+
|
35
|
+
def persist_file
|
36
|
+
case version
|
37
|
+
when /Red Hat/
|
38
|
+
"/etc/sysconfig/iptables"
|
39
|
+
when /Debian/
|
40
|
+
"/etc/iptables/rules"
|
41
|
+
when /Ubuntu/
|
42
|
+
"/etc/iptables/rules"
|
43
|
+
else
|
44
|
+
"/tmp/iptables.rules"
|
45
|
+
end
|
19
46
|
end
|
20
47
|
|
21
48
|
def dist
|
@@ -35,6 +62,18 @@ module Shutter
|
|
35
62
|
dist == "RedHat"
|
36
63
|
end
|
37
64
|
|
65
|
+
def ubuntu?
|
66
|
+
dist == "Ubuntu"
|
67
|
+
end
|
68
|
+
|
69
|
+
def debian?
|
70
|
+
dist == "Debian"
|
71
|
+
end
|
72
|
+
|
73
|
+
def unknown?
|
74
|
+
dist == "Unknown"
|
75
|
+
end
|
76
|
+
|
38
77
|
alias :centos? :redhat?
|
39
78
|
alias :fedora? :redhat?
|
40
79
|
end
|
data/lib/shutter/version.rb
CHANGED
data/shutter.gemspec
CHANGED
@@ -6,9 +6,9 @@ Gem::Specification.new do |gem|
|
|
6
6
|
gem.email = ["nosignsoflifehere@gmail.com"]
|
7
7
|
gem.description = %q{Shutter is a tool that gives system administrators the ability
|
8
8
|
to manage iptables firewall settings through simple lists instead
|
9
|
-
of complex iptables rules. Please note:
|
10
|
-
only
|
11
|
-
|
9
|
+
of complex iptables rules. Please note: This application is currently
|
10
|
+
only tested with Red Hat based distributions. Ubuntu and Debian should
|
11
|
+
work but are not supported..
|
12
12
|
}
|
13
13
|
gem.summary = %q{Shutter helps manage iptables firewalls}
|
14
14
|
gem.homepage = ""
|
@@ -21,4 +21,5 @@ Gem::Specification.new do |gem|
|
|
21
21
|
gem.version = Shutter::VERSION
|
22
22
|
gem.add_development_dependency('rspec')
|
23
23
|
gem.add_development_dependency('mocha')
|
24
|
+
gem.add_development_dependency('simplecov')
|
24
25
|
end
|
data/spec/command_line_spec.rb
CHANGED
@@ -1,16 +1,82 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
-
require 'fileutils'
|
3
2
|
|
4
3
|
describe "Shutter::CommandLine" do
|
5
|
-
|
6
|
-
cmd = Shutter::CommandLine.new(
|
7
|
-
cmd.init
|
8
|
-
File.directory?('./tmp/configs').should == true
|
9
|
-
FileUtils.rm_rf('./tmp/configs')
|
4
|
+
before(:each) do
|
5
|
+
@cmd = Shutter::CommandLine.new("./tmp")
|
10
6
|
end
|
11
7
|
|
12
|
-
it "should not
|
13
|
-
cmd
|
14
|
-
expect { cmd.init }.to raise_error
|
8
|
+
it "should not raise exception when firewall is called" do
|
9
|
+
expect { @cmd.firewall }.to_not raise_error
|
15
10
|
end
|
11
|
+
|
12
|
+
it "should set default value of persist to false" do
|
13
|
+
@cmd.persist.should == false
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should set default value of debug to false" do
|
17
|
+
@cmd.debug.should == false
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should have set config_path to ./tmp" do
|
21
|
+
@cmd.config_path.should == "./tmp"
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should set the command to :save" do
|
25
|
+
@cmd.execute(["--save"],true)
|
26
|
+
@cmd.command.should == :save
|
27
|
+
@cmd.execute(["-s"],true)
|
28
|
+
@cmd.command.should == :save
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should set the command to :restore" do
|
32
|
+
@cmd.execute(["--restore"],true)
|
33
|
+
@cmd.command.should == :restore
|
34
|
+
@cmd.execute(["--restore", "--persist"],true)
|
35
|
+
@cmd.command.should == :restore
|
36
|
+
@cmd.persist.should == true
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should set the command to :init" do
|
40
|
+
@cmd.execute(["--init"],true)
|
41
|
+
@cmd.command.should == :init
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should set the command to :reinit" do
|
45
|
+
@cmd.execute(["--reinit"],true)
|
46
|
+
@cmd.command.should == :reinit
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should set the command to :upgrade" do
|
50
|
+
@cmd.execute(["--upgrade"],true)
|
51
|
+
@cmd.command.should == :upgrade
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should set the config path and persist" do
|
55
|
+
Shutter::OS.stubs(:version).returns("Unknown")
|
56
|
+
@cmd.execute(["--dir", "/tmp", "--restore", "--persist"],true)
|
57
|
+
@cmd.command.should == :restore
|
58
|
+
@cmd.persist.should == true
|
59
|
+
@cmd.persist_file.should == "/tmp/iptables.rules"
|
60
|
+
@cmd.config_path.should == "/tmp"
|
61
|
+
@cmd.execute(["-d", "/tmp", "--restore", "--persist"],true)
|
62
|
+
@cmd.command.should == :restore
|
63
|
+
@cmd.persist.should == true
|
64
|
+
@cmd.persist_file.should == "/tmp/iptables.rules"
|
65
|
+
@cmd.config_path.should == "/tmp"
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should set the config path and persist with file" do
|
69
|
+
Shutter::OS.stubs(:version).returns("Unknown")
|
70
|
+
@cmd.execute(["--dir", "/tmp", "--restore", "--persist", "/tmp/persistance.file"],true)
|
71
|
+
@cmd.command.should == :restore
|
72
|
+
@cmd.persist.should == true
|
73
|
+
@cmd.persist_file.should == "/tmp/persistance.file"
|
74
|
+
@cmd.config_path.should == "/tmp"
|
75
|
+
@cmd.execute(["-d", "/tmp", "--restore", "--persist", "/tmp/persistance.file"],true)
|
76
|
+
@cmd.command.should == :restore
|
77
|
+
@cmd.persist.should == true
|
78
|
+
@cmd.persist_file.should == "/tmp/persistance.file"
|
79
|
+
@cmd.config_path.should == "/tmp"
|
80
|
+
end
|
81
|
+
|
16
82
|
end
|
data/spec/content_spec.rb
CHANGED
@@ -2,8 +2,8 @@ require File.dirname(__FILE__) + '/spec_helper'
|
|
2
2
|
|
3
3
|
describe "Shutter" do
|
4
4
|
it "should have templates for all files" do
|
5
|
-
Shutter::CONFIG_FILES.each do |name|
|
6
|
-
Shutter.constants.include?(:"#{name.upcase.gsub(/\./, "_")}").should == true
|
5
|
+
Shutter::Content::CONFIG_FILES.each do |name|
|
6
|
+
Shutter::Content.constants.include?(:"#{name.upcase.gsub(/\./, "_")}").should == true
|
7
7
|
end
|
8
8
|
end
|
9
9
|
end
|