catflap 0.0.2 → 1.0.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.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.rubocop.yml +21 -0
- data/.rubocop_todo.yml +7 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +49 -0
- data/LICENSE +20 -0
- data/README.md +134 -0
- data/Rakefile +6 -0
- data/bin/catflap +71 -64
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/catflap.gemspec +32 -0
- data/etc/config.yaml +30 -0
- data/etc/init.d/catflap +89 -0
- data/etc/passfile.yaml +7 -0
- data/lib/catflap.rb +108 -64
- data/lib/catflap/command.rb +102 -0
- data/lib/catflap/firewall.rb +56 -0
- data/lib/catflap/http.rb +288 -0
- data/lib/catflap/netfilter/writer.rb +127 -0
- data/lib/catflap/plugins/firewall/iptables.rb +104 -0
- data/lib/catflap/plugins/firewall/netfilter.rb +114 -0
- data/lib/catflap/plugins/firewall/plugin.rb +67 -0
- data/lib/catflap/version.rb +5 -0
- data/lib/netfilter/writer.rb +125 -0
- data/ui/css/catflap.css +44 -0
- data/ui/images/catflap.png +0 -0
- data/ui/index.rhtml +23 -0
- data/ui/js/catflap.js +85 -0
- data/ui/js/sha256.js +166 -0
- metadata +109 -11
- data/lib/catflap-http.rb +0 -111
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'catflap/plugins/firewall/plugin'
|
2
|
+
require 'netfilter/writer'
|
3
|
+
include NetfilterWriter
|
4
|
+
|
5
|
+
##
|
6
|
+
# A firewall plugin driver to implement rules on the NetFilter filter table.
|
7
|
+
#
|
8
|
+
# This driver passes rules to the filter table via the iptables user-space
|
9
|
+
# client.Two new chains are installed in the filter table, named by default:
|
10
|
+
# CATFLAP-DENY and CATFLAP-ALLOW. These are installed in the INPUT chain. You
|
11
|
+
# can configure the driver to install a LOG to log rejected packets, and also
|
12
|
+
# whether to REJECT or DROP denied packets.
|
13
|
+
#
|
14
|
+
# If you want to redirect to the Catflap server login port instead, then you
|
15
|
+
# should use the 'netfilter' driver instead, which is the default.
|
16
|
+
#
|
17
|
+
# @example firewall: plugin: 'iptables'
|
18
|
+
#
|
19
|
+
# @author Nyk Cowham <nykcowham@gmail.com>
|
20
|
+
class IptablesDriver < FirewallPlugin
|
21
|
+
# Initialize the driver class.
|
22
|
+
# @param [Hash<String, Hash>] config built by Catflap::initialize_config()
|
23
|
+
# @param [Boolean] noop set true to not send commands to firewall client.
|
24
|
+
# @param [Boolean] verbose set true to print command output to stdout stream.
|
25
|
+
# @return void
|
26
|
+
def initialize(config, noop, verbose)
|
27
|
+
super
|
28
|
+
@chain = config['firewall']['options']['chain'] || 'CATFLAP'
|
29
|
+
@log_rejected = config['firewall']['options']['log_rejected'] || false
|
30
|
+
@accept_local = config['firewall']['options']['accept_local'] || false
|
31
|
+
@policy = config['firewall']['options']['reject_policy'].to_sym || :drop
|
32
|
+
@allow = @chain + '-ALLOW'
|
33
|
+
@deny = @chain + '-DENY'
|
34
|
+
@r = Rules.new(:filter, @dports)
|
35
|
+
@r.match('multiport')
|
36
|
+
@r.noop = noop
|
37
|
+
@r.verbose = verbose
|
38
|
+
end
|
39
|
+
|
40
|
+
# Method to install the driver rules into iptables.
|
41
|
+
# @return void
|
42
|
+
# @raise StandardError when iptables reports an error.
|
43
|
+
def install_rules
|
44
|
+
target = (@policy == :reject) ? 'REJECT' : 'DROP'
|
45
|
+
log = @log_rejected
|
46
|
+
local = @accept_local
|
47
|
+
@r.chain(:new, @allow)
|
48
|
+
.chain(:new, @deny)
|
49
|
+
.rule(:add, chain: 'INPUT', jump: @allow)
|
50
|
+
.rule(:add, chain: 'INPUT', jump: @deny)
|
51
|
+
.rule(:add, chain: @deny, jump: 'LOG') { log }
|
52
|
+
.rule(:add, chain: @deny, jump: target)
|
53
|
+
.rule(:add, chain: @allow, jump: 'ACCEPT',
|
54
|
+
src: 'localhost') { local }
|
55
|
+
.do
|
56
|
+
end
|
57
|
+
|
58
|
+
# Method to uninstall the driver rules from iptables.
|
59
|
+
# @return void
|
60
|
+
# @raise StandardError when iptables reports an error.
|
61
|
+
def uninstall_rules
|
62
|
+
@r.rule(:delete, chain: 'INPUT', jump: @allow)
|
63
|
+
.rule(:delete, chain: 'INPUT', jump: @deny)
|
64
|
+
.chain(:flush, @allow)
|
65
|
+
.chain(:delete, @allow)
|
66
|
+
.chain(:flush, @deny)
|
67
|
+
.chain(:delete, @deny)
|
68
|
+
.do
|
69
|
+
end
|
70
|
+
|
71
|
+
# Method to purge all rules from CATFLAP-ALLOW chain.
|
72
|
+
# @return void
|
73
|
+
# @raise StandardError when iptables reports an error.
|
74
|
+
def purge_rules
|
75
|
+
@r.chain(:flush, @allow).do
|
76
|
+
end
|
77
|
+
|
78
|
+
# Method to list rules in CATFLAP-ALLOW chain.
|
79
|
+
# @return void
|
80
|
+
# @raise StandardError when iptables reports an error.
|
81
|
+
def list_rules
|
82
|
+
@r.chain(:list, @allow).do
|
83
|
+
end
|
84
|
+
|
85
|
+
# Method to check the CATFLAP-ALLOW chain for an allowed IP.
|
86
|
+
# @return [Boolean] true indicates that IP address already has access.
|
87
|
+
def check_address(ip)
|
88
|
+
@r.rule(:check, src: ip, chain: @allow, jump: 'ACCEPT').do?
|
89
|
+
end
|
90
|
+
|
91
|
+
# Method to add/grant an IP access in the CATFLAP-ALLOW chain.
|
92
|
+
# @return void
|
93
|
+
# @raise StandardError when iptables reports an error.
|
94
|
+
def add_address(ip)
|
95
|
+
@r.rule(:insert, src: ip, chain: @allow, jump: 'ACCEPT').do
|
96
|
+
end
|
97
|
+
|
98
|
+
# Method to delete/revoke access for an IP in the CATFLAP-ALLOW chain.
|
99
|
+
# @return void
|
100
|
+
# @raise StandardError when iptables reports an error.
|
101
|
+
def delete_address(ip)
|
102
|
+
@r.rule(:delete, src: ip, chain: @allow, jump: 'ACCEPT').do
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'catflap/plugins/firewall/plugin'
|
2
|
+
require 'netfilter/writer'
|
3
|
+
include NetfilterWriter
|
4
|
+
|
5
|
+
##
|
6
|
+
# A firewall plugin driver to implement rules on the NetFilter NAT table.
|
7
|
+
#
|
8
|
+
# This driver passes rules to the NAT table via the iptables user-space client.
|
9
|
+
# Two new chains are installed in the NAT table, named by default:
|
10
|
+
# CATFLAP-DENY and CATFLAP-ALLOW. These are installed in the PREROUTING chain.
|
11
|
+
# You can configure the driver to install a LOG to log denied packets.
|
12
|
+
#
|
13
|
+
# This is the default and recommended driver for Linux systems running iptables.
|
14
|
+
#
|
15
|
+
# @example firewall: plugin: 'netfilter'
|
16
|
+
#
|
17
|
+
# @author Nyk Cowham <nykcowham@gmail.com>
|
18
|
+
class NetfilterDriver < FirewallPlugin
|
19
|
+
# Initialize the driver class.
|
20
|
+
# @param [Hash<String, Hash>] config hash: Catflap::initialize_config()
|
21
|
+
# @param [Boolean] noop send the commands to the firewall client.
|
22
|
+
# @param [Boolean] verbose print the command output to stdout stream.
|
23
|
+
# @return void
|
24
|
+
def initialize(config, noop = false, verbose = false)
|
25
|
+
super
|
26
|
+
@chain = config['firewall']['options']['chain'] || 'CATFLAP'
|
27
|
+
@forward = config['firewall']['options']['forward']
|
28
|
+
@log_rejected = config['firewall']['options']['log_rejected'] || false
|
29
|
+
@accept_local = config['firewall']['options']['accept_local'] || false
|
30
|
+
@allow = @chain + '-ALLOW'
|
31
|
+
@deny = @chain + '-DENY'
|
32
|
+
@r = Rules.new(:nat, @dports)
|
33
|
+
@r.match('multiport')
|
34
|
+
@r.noop = noop
|
35
|
+
@r.verbose = verbose
|
36
|
+
end
|
37
|
+
|
38
|
+
# Method to install the driver rules into iptables.
|
39
|
+
# @return void
|
40
|
+
# @raise StandardError when iptables reports an error.
|
41
|
+
def install_rules
|
42
|
+
# We must make these local variables, so they are exposed to the blocks.
|
43
|
+
log = @log_rejected
|
44
|
+
deny_local = !@accept_local
|
45
|
+
|
46
|
+
# Create a new chain on the NAT table for our catflap netfilter allow rules.
|
47
|
+
@r.chain(:new, @allow)
|
48
|
+
.chain(:new, @deny)
|
49
|
+
.rule(:add, chain: 'PREROUTING', jump: @allow)
|
50
|
+
.rule(:add, chain: 'PREROUTING', jump: @deny)
|
51
|
+
.rule(:add, chain: @deny, jump: 'LOG') { log }
|
52
|
+
.rule(:add, chain: 'OUTPUT', out: 'lo', jump: @allow) { deny_local }
|
53
|
+
.rule(:add, chain: 'OUTPUT', out: 'lo', jump: @deny) { deny_local }
|
54
|
+
|
55
|
+
@forward.each do |src, dest|
|
56
|
+
src = src.to_s
|
57
|
+
@r.rule(:add, chain: @deny, jump: 'REDIRECT', dports: src, to_port: dest)
|
58
|
+
end
|
59
|
+
@r.do
|
60
|
+
end
|
61
|
+
|
62
|
+
# Method to uninstall the driver rules from iptables.
|
63
|
+
# @return void
|
64
|
+
# @raise StandardError when iptables reports an error.
|
65
|
+
def uninstall_rules
|
66
|
+
deny_local = !@accept_local
|
67
|
+
|
68
|
+
@r.rule(:delete, chain: 'PREROUTING', jump: @allow)
|
69
|
+
.rule(:delete, chain: 'PREROUTING', jump: @deny)
|
70
|
+
.rule(:delete, chain: 'OUTPUT', out: 'lo',
|
71
|
+
jump: @allow) { deny_local }
|
72
|
+
.rule(:delete, chain: 'OUTPUT', out: 'lo',
|
73
|
+
jump: @deny) { deny_local }
|
74
|
+
.chain(:flush, @allow)
|
75
|
+
.chain(:flush, @deny)
|
76
|
+
.chain(:delete, @allow)
|
77
|
+
.chain(:delete, @deny)
|
78
|
+
.do
|
79
|
+
end
|
80
|
+
|
81
|
+
# Method to purge all rules from CATFLAP-ALLOW chain.
|
82
|
+
# @return void
|
83
|
+
# @raise StandardError when iptables reports an error.
|
84
|
+
def purge_rules
|
85
|
+
@r.chain(:flush, @allow).do
|
86
|
+
end
|
87
|
+
|
88
|
+
# Method to list rules in CATFLAP-ALLOW chain.
|
89
|
+
# @return void
|
90
|
+
# @raise StandardError when iptables reports an error.
|
91
|
+
def list_rules
|
92
|
+
@r.chain(:list, @allow).do
|
93
|
+
end
|
94
|
+
|
95
|
+
# Method to check the CATFLAP-ALLOW chain for an allowed IP.
|
96
|
+
# @return [Boolean] true indicates that IP address already has access.
|
97
|
+
def check_address(ip)
|
98
|
+
@r.rule(:check, src: ip, chain: @allow, jump: 'ACCEPT').do?
|
99
|
+
end
|
100
|
+
|
101
|
+
# Method to add/grant an IP access in the CATFLAP-ALLOW chain.
|
102
|
+
# @return void
|
103
|
+
# @raise StandardError when iptables reports an error.
|
104
|
+
def add_address(ip)
|
105
|
+
@r.rule(:insert, src: ip, chain: @allow, jump: 'ACCEPT').do
|
106
|
+
end
|
107
|
+
|
108
|
+
# Method to delete/revoke access for an IP in the CATFLAP-ALLOW chain.
|
109
|
+
# @return void
|
110
|
+
# @raise StandardError when iptables reports an error.
|
111
|
+
def delete_address(ip)
|
112
|
+
@r.rule(:delete, src: ip, chain: @allow, jump: 'ACCEPT').do
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# Abstract class plugin driver to implement rules on the NetFilter filter table.
|
2
|
+
#
|
3
|
+
# Firewall drivers should inherit from this abstract class and implement each
|
4
|
+
# method. This serves as a contract for firewalls to follow to ensure that the
|
5
|
+
# driver can respond to firewall calls.
|
6
|
+
#
|
7
|
+
# @example firewall: plugin: 'iptables'
|
8
|
+
#
|
9
|
+
# @author Nyk Cowham <nykcowham@gmail.com>
|
10
|
+
class FirewallPlugin
|
11
|
+
attr_reader :dports, :catflap_port
|
12
|
+
|
13
|
+
def initialize(config, noop = false, verbose = false)
|
14
|
+
@noop = noop
|
15
|
+
@verbose = verbose
|
16
|
+
@catflap_port = config['server']['port']
|
17
|
+
@dports = config['firewall']['dports']
|
18
|
+
end
|
19
|
+
|
20
|
+
# Implement method to install the driver rules into iptables.
|
21
|
+
# @return void
|
22
|
+
# @raise StandardError when iptables reports an error.
|
23
|
+
def install_rules
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
# Implement method to uninstall the driver rules from iptables.
|
28
|
+
# @return void
|
29
|
+
# @raise StandardError when iptables reports an error.
|
30
|
+
def uninstall_rules
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
|
34
|
+
# Implement method to purge all rules from CATFLAP-ALLOW chain.
|
35
|
+
# @return void
|
36
|
+
# @raise StandardError when iptables reports an error.
|
37
|
+
def purge_rules
|
38
|
+
raise NotImplementedError
|
39
|
+
end
|
40
|
+
|
41
|
+
# Implement method to list rules in CATFLAP-ALLOW chain.
|
42
|
+
# @return void
|
43
|
+
# @raise StandardError when iptables reports an error.
|
44
|
+
def list_rules
|
45
|
+
raise NotImplementedError
|
46
|
+
end
|
47
|
+
|
48
|
+
# Implement method to check the CATFLAP-ALLOW chain for an allowed IP.
|
49
|
+
# @return [Boolean] true indicates that IP address already has access.
|
50
|
+
def check_address(_)
|
51
|
+
raise NotImplementedError
|
52
|
+
end
|
53
|
+
|
54
|
+
# Implement method to add/grant an IP access in the CATFLAP-ALLOW chain.
|
55
|
+
# @return void
|
56
|
+
# @raise StandardError when iptables reports an error.
|
57
|
+
def add_address(_)
|
58
|
+
raise NotImplementedError
|
59
|
+
end
|
60
|
+
|
61
|
+
# Implement method to delete/revoke access in the CATFLAP-ALLOW chain.
|
62
|
+
# @return void
|
63
|
+
# @raise StandardError when iptables reports an error.
|
64
|
+
def delete_address(_)
|
65
|
+
raise NotImplementedError
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'catflap/firewall'
|
2
|
+
include Firewall
|
3
|
+
|
4
|
+
# Mixin module to add rule handling functions to netfilter-based drivers.
|
5
|
+
#
|
6
|
+
# @author Nyk Cowham <nykcowham@gmail.com>
|
7
|
+
module NetfilterWriter
|
8
|
+
# Class providing a DSL for defining netfilter rules
|
9
|
+
# @author Nyk Cowham <nykcowham@gmail.com>
|
10
|
+
class Rules
|
11
|
+
attr_accessor :noop, :verbose
|
12
|
+
|
13
|
+
def initialize(table, ports = nil)
|
14
|
+
@table = table
|
15
|
+
@ports = ports
|
16
|
+
@buffer = ''
|
17
|
+
end
|
18
|
+
|
19
|
+
def table(table)
|
20
|
+
@table = table
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def ports(ports)
|
25
|
+
@ports = ports
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def match(match)
|
30
|
+
@match = match
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
# Create, flush and delete chains
|
35
|
+
# @param [String] cmd the operation to perform (add, delete, flush)
|
36
|
+
# @param [String] chain name of the chain (e.g. INPUT, CATFLAP-DENY, etc.)
|
37
|
+
# @return self
|
38
|
+
def chain(cmd, chain, a = {})
|
39
|
+
cmds = {
|
40
|
+
new: '-N', rename: '-E', delete: '-X', flush: '-F',
|
41
|
+
list_rules: '-S', list: '-L', zero: '-Z', policy: '-P'
|
42
|
+
}
|
43
|
+
table = build_option('-t', @table)
|
44
|
+
numeric = build_option('-n', a[:numeric])
|
45
|
+
rulenum = build_option(true, a[:rulenum])
|
46
|
+
to = build_option(true, a[:to])
|
47
|
+
@buffer << [
|
48
|
+
'iptables', table, numeric, cmds[cmd], chain, rulenum, to
|
49
|
+
].compact.join(' ') << "\n"
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
# Create, flush and delete chains
|
54
|
+
# @param [String] cmd the operation to perform (add, delete, insert, etc.)
|
55
|
+
# @param [String] chain name of the chain (e.g. INPUT, CATFLAP-DENY, etc.)
|
56
|
+
# @return self
|
57
|
+
def rule(cmd, a, &block)
|
58
|
+
# Evaluate a block expression and return early if it evaluates to false.
|
59
|
+
# If no block is passed it is equivalent to the block: { true }.
|
60
|
+
return self if block_given? && !instance_eval(&block)
|
61
|
+
|
62
|
+
raise ArgumentError, 'chain is a required argument' unless a[:chain]
|
63
|
+
assert_valid_ipaddr(a[:src]) if a[:src]
|
64
|
+
assert_valid_ipaddr(a[:dst]) if a[:dst]
|
65
|
+
|
66
|
+
# Map of commands for rules
|
67
|
+
cmds = {
|
68
|
+
add: '-A', delete: '-D', insert: '-I', replace: '-R',
|
69
|
+
check: '-C'
|
70
|
+
}
|
71
|
+
|
72
|
+
a[:proto] ||= 'tcp'
|
73
|
+
table = build_option('-t', @table)
|
74
|
+
jump = build_option('-j', a[:jump])
|
75
|
+
goto = build_option('-g', a[:goto])
|
76
|
+
proto = build_option('-p', a[:proto])
|
77
|
+
inface = build_option('-i', a[:in])
|
78
|
+
outface = build_option('-o', a[:out])
|
79
|
+
src = build_option('-s', a[:src])
|
80
|
+
dst = build_option('-d', a[:dst])
|
81
|
+
match = build_option('-m', a[:match] || @match)
|
82
|
+
ports = build_option('--dport', @ports)
|
83
|
+
to_port = build_option('--to-port', a[:to_port])
|
84
|
+
@buffer << [
|
85
|
+
'iptables', table, cmds[cmd], a[:chain], src, dst, outface,
|
86
|
+
inface, proto, match, ports, jump || goto, to_port
|
87
|
+
].compact.join(' ') << "\n"
|
88
|
+
self
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def build_option(flag, value)
|
93
|
+
return flag if value.is_a?(TrueClass)
|
94
|
+
return value if flag.is_a?(TrueClass)
|
95
|
+
return flag << ' ' << value.to_s if flag && value
|
96
|
+
end
|
97
|
+
|
98
|
+
# Add a raw text rule, (e.g.: iptables -t nat CATFLAP-ALLOW ...)
|
99
|
+
# @param [String] raw_rule custom raw iptables command.
|
100
|
+
# @return self
|
101
|
+
def raw(raw_rule)
|
102
|
+
@buffer = raw_rule
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
# Flush the rule buffer and output the resulting iptables commands.
|
107
|
+
# @return [String] rule text that can be sent iptables user-space client.
|
108
|
+
def flush
|
109
|
+
out = @buffer
|
110
|
+
@buffer = ''
|
111
|
+
out
|
112
|
+
end
|
113
|
+
|
114
|
+
# Flush the rule and execute in iptables user-space client.
|
115
|
+
# @return void
|
116
|
+
def do
|
117
|
+
execute flush
|
118
|
+
end
|
119
|
+
|
120
|
+
# Flush the rule and execute commands and return success/fail value.
|
121
|
+
# @return [Boolean] true if the execution was successful.
|
122
|
+
def do?
|
123
|
+
execute_true? flush
|
124
|
+
end
|
125
|
+
end
|
data/ui/css/catflap.css
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
|
2
|
+
.centered {
|
3
|
+
margin: 0px auto;
|
4
|
+
display: block;
|
5
|
+
}
|
6
|
+
|
7
|
+
.hidden {
|
8
|
+
display: none;
|
9
|
+
}
|
10
|
+
|
11
|
+
input[type="text"]:focus.failed {
|
12
|
+
background-color: pink;
|
13
|
+
border-color: red;
|
14
|
+
color: black;
|
15
|
+
}
|
16
|
+
|
17
|
+
input[type="text"] {
|
18
|
+
display: block;
|
19
|
+
margin: 0px auto;
|
20
|
+
margin-top: 1em;
|
21
|
+
padding-left: 0.4em;
|
22
|
+
height: 1.7em;
|
23
|
+
width: 50%;
|
24
|
+
color: #999;
|
25
|
+
font-family: sans-serif;
|
26
|
+
font-size: 2.2em;
|
27
|
+
border: solid 2px #dcdcdc;
|
28
|
+
transition: box-shadow 0.3s, border 0.3s;
|
29
|
+
box-shadow: inset 1px 1px 2px 0 #707070;
|
30
|
+
appearance: none;
|
31
|
+
border-radius: 2px;
|
32
|
+
}
|
33
|
+
|
34
|
+
input[type="text"]:focus {
|
35
|
+
border: solid 2px #707070;
|
36
|
+
box-shadow: inset 1px 1px 2px 0 #c9c9c9;
|
37
|
+
}
|
38
|
+
|
39
|
+
.message {
|
40
|
+
width: 50%;
|
41
|
+
font-size: 2em;
|
42
|
+
margin-top: 1.5em;
|
43
|
+
color: #860000;
|
44
|
+
}
|