puffy 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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +36 -0
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/.rubocop.yml +39 -0
- data/.simplecov +8 -0
- data/Gemfile +6 -0
- data/README.md +51 -0
- data/Rakefile +20 -0
- data/bin/puffy +17 -0
- data/lib/core_ext.rb +76 -0
- data/lib/puffy/cli.rb +132 -0
- data/lib/puffy/formatters/base.rb +95 -0
- data/lib/puffy/formatters/netfilter.rb +263 -0
- data/lib/puffy/formatters/netfilter4.rb +23 -0
- data/lib/puffy/formatters/netfilter6.rb +23 -0
- data/lib/puffy/formatters/pf.rb +132 -0
- data/lib/puffy/parser.tab.rb +1217 -0
- data/lib/puffy/puppet.rb +73 -0
- data/lib/puffy/resolver.rb +76 -0
- data/lib/puffy/rule.rb +220 -0
- data/lib/puffy/rule_factory.rb +117 -0
- data/lib/puffy/version.rb +5 -0
- data/lib/puffy.rb +62 -0
- data/puffy.gemspec +47 -0
- metadata +254 -0
@@ -0,0 +1,263 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Puffy
|
4
|
+
module Formatters
|
5
|
+
module Netfilter # :nodoc:
|
6
|
+
# Returns the target to jump to
|
7
|
+
#
|
8
|
+
# @return [String]
|
9
|
+
def self.iptables_action(rule_or_action, ret: false)
|
10
|
+
case rule_or_action
|
11
|
+
when :pass then 'ACCEPT'
|
12
|
+
when :log then 'LOG'
|
13
|
+
when :block then ret ? 'RETURN' : 'DROP'
|
14
|
+
when Puffy::Rule then iptables_action(rule_or_action.action, ret: rule_or_action.return)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Netfilter implementation of a Puffy Ruleset formatter.
|
19
|
+
class Ruleset < Puffy::Formatters::Base::Ruleset # :nodoc:
|
20
|
+
def self.known_conntrack_helpers
|
21
|
+
{
|
22
|
+
21 => 'ftp',
|
23
|
+
69 => 'tftp',
|
24
|
+
194 => 'irc',
|
25
|
+
6566 => 'sane',
|
26
|
+
5060 => 'sip',
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns a Netfilter String representation of the provided +rules+ Array of Puffy::Rule with the +policy+ policy.
|
31
|
+
def emit_ruleset(rules, policy = :block)
|
32
|
+
parts = []
|
33
|
+
parts << emit_header
|
34
|
+
parts << raw_ruleset(raw_rules(rules))
|
35
|
+
parts << nat_ruleset(nat_rules(rules))
|
36
|
+
parts << filter_ruleset(filter_rules(rules), policy)
|
37
|
+
ruleset = parts.flatten.compact.join("\n")
|
38
|
+
"#{ruleset}\n"
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def raw_ruleset(rules)
|
44
|
+
return unless rules.any?
|
45
|
+
|
46
|
+
parts = ['*raw']
|
47
|
+
parts << emit_chain_policies(prerouting: :pass, output: :pass)
|
48
|
+
parts << rules.map { |rule| @rule_formatter.emit_ct_rule(rule) }.uniq
|
49
|
+
parts << 'COMMIT'
|
50
|
+
parts
|
51
|
+
end
|
52
|
+
|
53
|
+
def nat_ruleset(rules)
|
54
|
+
return unless rules.any?
|
55
|
+
|
56
|
+
parts = ['*nat']
|
57
|
+
parts << emit_chain_policies(prerouting: :pass, input: :pass, output: :pass, postrouting: :pass)
|
58
|
+
parts << rules.select(&:rdr?).map { |rule| @rule_formatter.emit_rule(rule) }
|
59
|
+
parts << rules.select(&:nat?).map { |rule| @rule_formatter.emit_rule(rule) }
|
60
|
+
parts << 'COMMIT'
|
61
|
+
parts
|
62
|
+
end
|
63
|
+
|
64
|
+
def filter_ruleset(rules, policy)
|
65
|
+
return unless rules.any?
|
66
|
+
|
67
|
+
parts = ['*filter']
|
68
|
+
parts << emit_chain_policies(input: policy, forward: policy, output: policy)
|
69
|
+
parts << input_filter_ruleset(rules)
|
70
|
+
parts << forward_filter_ruleset(rules)
|
71
|
+
parts << output_filter_rulset(rules)
|
72
|
+
parts << 'COMMIT'
|
73
|
+
parts
|
74
|
+
end
|
75
|
+
|
76
|
+
def emit_chain_policies(policies)
|
77
|
+
policies.map { |chain, action| ":#{chain.upcase} #{Puffy::Formatters::Netfilter.iptables_action(action)} [0:0]" }
|
78
|
+
end
|
79
|
+
|
80
|
+
def input_filter_ruleset(rules)
|
81
|
+
parts = ['-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT']
|
82
|
+
parts << input_filter_rules(rules).map { |rule| @rule_formatter.emit_rule(rule) }
|
83
|
+
end
|
84
|
+
|
85
|
+
def forward_filter_ruleset(rules)
|
86
|
+
parts = ['-A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT']
|
87
|
+
parts << rules.select(&:fwd?).map { |rule| @rule_formatter.emit_rule(rule) }
|
88
|
+
parts << rules.select { |r| r.rdr? && !Puffy::Formatters::Base.loopback_addresses.include?(r.rdr_to_host) }.map { |rule| @rule_formatter.emit_rule(Puffy::Rule.fwd_rule(rule)) }
|
89
|
+
end
|
90
|
+
|
91
|
+
def output_filter_rulset(rules)
|
92
|
+
parts = ['-A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT']
|
93
|
+
parts << output_filter_rules(rules).map { |rule| @rule_formatter.emit_rule(rule) }
|
94
|
+
end
|
95
|
+
|
96
|
+
def raw_rules(rules)
|
97
|
+
rules.select { |r| r.action == :pass && Ruleset.known_conntrack_helpers.include?(r.to_port) }
|
98
|
+
end
|
99
|
+
|
100
|
+
def nat_rules(rules)
|
101
|
+
rules.select { |r| r.nat? || r.rdr? }
|
102
|
+
end
|
103
|
+
|
104
|
+
def filter_rules(rules)
|
105
|
+
rules.select { |r| %i[pass block log].include?(r.action) }
|
106
|
+
end
|
107
|
+
|
108
|
+
def input_filter_rules(rules)
|
109
|
+
rules.select { |r| r.filter? && r.in? }
|
110
|
+
end
|
111
|
+
|
112
|
+
def output_filter_rules(rules)
|
113
|
+
rules.select { |r| r.filter? && r.out? }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Netfilter implementation of a Puffy Rule formatter.
|
118
|
+
class Rule < Puffy::Formatters::Base::Rule # :nodoc:
|
119
|
+
# Returns a Netfilter String representation of the provided +rule+ Puffy::Rule.
|
120
|
+
def emit_rule(rule)
|
121
|
+
if rule.nat?
|
122
|
+
emit_postrouting_rule(rule)
|
123
|
+
elsif rule.rdr?
|
124
|
+
emit_prerouting_rule(rule)
|
125
|
+
else
|
126
|
+
emit_filter_rule(rule)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def emit_ct_rule(rule)
|
131
|
+
parts = ['-A PREROUTING']
|
132
|
+
parts << emit_if(rule)
|
133
|
+
parts << emit_proto(rule)
|
134
|
+
parts << emit_src_port(rule)
|
135
|
+
parts << emit_dst_port(rule)
|
136
|
+
parts << '-j CT'
|
137
|
+
parts << "--helper #{Ruleset.known_conntrack_helpers[rule.to_port]}"
|
138
|
+
pp_rule(parts)
|
139
|
+
end
|
140
|
+
|
141
|
+
def emit_postrouting_rule(rule)
|
142
|
+
"-A POSTROUTING -o #{rule.on} -j MASQUERADE"
|
143
|
+
end
|
144
|
+
|
145
|
+
def emit_prerouting_rule(rule)
|
146
|
+
parts = ['-A PREROUTING']
|
147
|
+
parts << emit_on(rule)
|
148
|
+
parts << emit_proto(rule)
|
149
|
+
parts << emit_src(rule)
|
150
|
+
parts << emit_dst(rule)
|
151
|
+
parts << emit_redirect_or_dnat(rule)
|
152
|
+
pp_rule(parts)
|
153
|
+
end
|
154
|
+
|
155
|
+
def emit_filter_rule(rule)
|
156
|
+
iptables_direction = { in: 'INPUT', out: 'OUTPUT', fwd: 'FORWARD' }
|
157
|
+
parts = ["-A #{iptables_direction[rule.dir]}"]
|
158
|
+
parts << '-m conntrack --ctstate NEW' if %i[tcp udp].include?(rule.proto)
|
159
|
+
parts << emit_if(rule)
|
160
|
+
parts << emit_proto(rule)
|
161
|
+
parts << emit_src(rule)
|
162
|
+
parts << emit_dst(rule)
|
163
|
+
parts << emit_jump(rule)
|
164
|
+
pp_rule(parts)
|
165
|
+
end
|
166
|
+
|
167
|
+
def emit_if(rule)
|
168
|
+
if rule.on
|
169
|
+
emit_on(rule)
|
170
|
+
else
|
171
|
+
emit_in_out(rule)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def emit_on(rule)
|
176
|
+
on_direction_flag = { in: '-i', out: '-o' }
|
177
|
+
|
178
|
+
return unless rule.on || rule.dir
|
179
|
+
|
180
|
+
matches = /(!)?(.*)/.match(rule.on)
|
181
|
+
[matches[1], on_direction_flag[rule.dir], matches[2]].compact
|
182
|
+
end
|
183
|
+
|
184
|
+
def emit_in_out(rule)
|
185
|
+
parts = []
|
186
|
+
parts << "-i #{rule.in}" if rule.in
|
187
|
+
parts << "-o #{rule.out}" if rule.out
|
188
|
+
parts
|
189
|
+
end
|
190
|
+
|
191
|
+
def emit_proto(rule)
|
192
|
+
"-p #{rule.proto}" if rule.proto
|
193
|
+
end
|
194
|
+
|
195
|
+
def emit_src(rule)
|
196
|
+
emit_src_host(rule) + emit_src_port(rule)
|
197
|
+
end
|
198
|
+
|
199
|
+
def emit_src_host(rule)
|
200
|
+
if rule.from_host
|
201
|
+
['-s', emit_address(rule.from_host)]
|
202
|
+
else
|
203
|
+
[]
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def emit_src_port(rule)
|
208
|
+
if rule.from_port
|
209
|
+
['--sport', emit_port(rule.from_port)]
|
210
|
+
else
|
211
|
+
[]
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def emit_dst(rule)
|
216
|
+
emit_dst_host(rule) + emit_dst_port(rule)
|
217
|
+
end
|
218
|
+
|
219
|
+
def emit_dst_host(rule)
|
220
|
+
if rule.to_host
|
221
|
+
['-d', emit_address(rule.to_host)]
|
222
|
+
else
|
223
|
+
[]
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def emit_dst_port(rule)
|
228
|
+
if rule.to_port
|
229
|
+
['--dport', emit_port(rule.to_port)]
|
230
|
+
else
|
231
|
+
[]
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def emit_redirect_or_dnat(rule)
|
236
|
+
if Puffy::Formatters::Base.loopback_addresses.include?(rule.rdr_to_host)
|
237
|
+
emit_redirect(rule)
|
238
|
+
else
|
239
|
+
emit_dnat(rule)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def emit_redirect(rule)
|
244
|
+
"-j REDIRECT --to-port #{rule.rdr_to_port}"
|
245
|
+
end
|
246
|
+
|
247
|
+
def emit_dnat(rule)
|
248
|
+
res = "-j DNAT --to-destination #{rule.rdr_to_host}"
|
249
|
+
res += ":#{rule.rdr_to_port}" if rule.rdr_to_port && rule.rdr_to_port != rule.to_port
|
250
|
+
res
|
251
|
+
end
|
252
|
+
|
253
|
+
def emit_jump(rule)
|
254
|
+
"-j #{Puffy::Formatters::Netfilter.iptables_action(rule)}"
|
255
|
+
end
|
256
|
+
|
257
|
+
def pp_rule(parts)
|
258
|
+
parts.flatten.compact.join(' ')
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Puffy
|
4
|
+
module Formatters
|
5
|
+
module Netfilter4 # :nodoc:
|
6
|
+
# IPv4 Netfilter implementation of a Puffy Ruleset formatter.
|
7
|
+
class Ruleset < Puffy::Formatters::Netfilter::Ruleset # :nodoc:
|
8
|
+
# Return an IPv4 Netfilter String representation of the provided +rules+ Puffy::Rule with the +policy+ policy.
|
9
|
+
def emit_ruleset(rules, policy = :block)
|
10
|
+
super(rules.select(&:ipv4?), policy)
|
11
|
+
end
|
12
|
+
|
13
|
+
def filename_fragment
|
14
|
+
['netfilter', 'rules.v4']
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# IPv4 Netfilter implementation of a Puffy Rulet formatter.
|
19
|
+
class Rule < Puffy::Formatters::Netfilter::Rule # :nodoc:
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Puffy
|
4
|
+
module Formatters
|
5
|
+
module Netfilter6 # :nodoc:
|
6
|
+
# IPv6 Netfilter implementation of a Puffy Ruleset formatter.
|
7
|
+
class Ruleset < Puffy::Formatters::Netfilter::Ruleset # :nodoc:
|
8
|
+
# Return an IPv6 Netfilter String representation of the provided +rules+ Puffy::Rule with the +policy+ policy.
|
9
|
+
def emit_ruleset(rules, policy = :block)
|
10
|
+
super(rules.select(&:ipv6?), policy)
|
11
|
+
end
|
12
|
+
|
13
|
+
def filename_fragment
|
14
|
+
['netfilter', 'rules.v6']
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# IPv6 Netfilter implementation of a Puffy Rule formatter.
|
19
|
+
class Rule < Puffy::Formatters::Netfilter::Rule # :nodoc:
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Puffy
|
4
|
+
module Formatters
|
5
|
+
module Pf # :nodoc:
|
6
|
+
# Pf implementation of a Puffy Ruleset formatter.
|
7
|
+
class Ruleset < Puffy::Formatters::Base::Ruleset # :nodoc:
|
8
|
+
# Returns a Pf String representation of the provided +rules+ Array of Puffy::Rule.
|
9
|
+
def emit_ruleset(rules, policy = :block)
|
10
|
+
parts = []
|
11
|
+
|
12
|
+
parts << emit_header(policy)
|
13
|
+
|
14
|
+
parts << super(rules.select(&:nat?))
|
15
|
+
parts << super(rules.select(&:rdr?))
|
16
|
+
parts << super(rules.select(&:filter?))
|
17
|
+
|
18
|
+
ruleset = parts.reject(&:empty?).join("\n")
|
19
|
+
"#{ruleset}\n"
|
20
|
+
end
|
21
|
+
|
22
|
+
def filename_fragment
|
23
|
+
['pf', 'pf.conf']
|
24
|
+
end
|
25
|
+
|
26
|
+
def emit_header(policy)
|
27
|
+
parts = super()
|
28
|
+
parts << 'match in all scrub (no-df)'
|
29
|
+
parts << 'set skip on lo'
|
30
|
+
parts << @rule_formatter.emit_rule(Puffy::Rule.new(action: policy, dir: :in, no_quick: true))
|
31
|
+
parts << @rule_formatter.emit_rule(Puffy::Rule.new(action: policy, dir: :out, no_quick: true))
|
32
|
+
parts
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Pf implementation of a Puffy Rule formatter.
|
37
|
+
class Rule < Puffy::Formatters::Base::Rule # :nodoc:
|
38
|
+
# Returns a Pf String representation of the provided +rule+ Puffy::Rule.
|
39
|
+
def emit_rule(rule)
|
40
|
+
parts = []
|
41
|
+
parts << emit_action(rule)
|
42
|
+
parts << emit_direction(rule)
|
43
|
+
parts << emit_quick(rule)
|
44
|
+
parts << emit_on(rule)
|
45
|
+
parts << emit_what(rule)
|
46
|
+
parts.flatten.compact.join(' ')
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def emit_action(rule)
|
52
|
+
parts = [rule.action]
|
53
|
+
parts << 'return' if rule.action == :block && rule.return
|
54
|
+
parts
|
55
|
+
end
|
56
|
+
|
57
|
+
def emit_direction(rule)
|
58
|
+
rule.dir
|
59
|
+
end
|
60
|
+
|
61
|
+
def emit_quick(rule)
|
62
|
+
'quick' unless rule.no_quick
|
63
|
+
end
|
64
|
+
|
65
|
+
def emit_on(rule)
|
66
|
+
"on #{rule.on.gsub('!', '! ')}" if rule.on
|
67
|
+
end
|
68
|
+
|
69
|
+
def emit_what(rule)
|
70
|
+
parts = [emit_af(rule)]
|
71
|
+
parts << emit_proto(rule)
|
72
|
+
parts << emit_from(rule)
|
73
|
+
parts << emit_to(rule)
|
74
|
+
parts << emit_rdr_to(rule)
|
75
|
+
parts << emit_nat_to(rule)
|
76
|
+
|
77
|
+
parts.flatten.compact.empty? ? 'all' : parts
|
78
|
+
end
|
79
|
+
|
80
|
+
def emit_af(rule)
|
81
|
+
if rule.implicit_ipv4? || rule.implicit_ipv6?
|
82
|
+
nil
|
83
|
+
elsif rule.ipv4? || rule.ipv6?
|
84
|
+
rule.af
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def emit_proto(rule)
|
89
|
+
"proto #{rule.proto}" if rule.proto
|
90
|
+
end
|
91
|
+
|
92
|
+
def emit_from(rule)
|
93
|
+
emit_endpoint_specification('from', rule.from_host, rule.from_port) if rule.from_host || rule.from_port
|
94
|
+
end
|
95
|
+
|
96
|
+
def emit_to(rule)
|
97
|
+
emit_endpoint_specification('to', rule.to_host, rule.to_port) if rule.to_host || rule.to_port
|
98
|
+
end
|
99
|
+
|
100
|
+
def emit_endpoint_specification(keyword, host, port)
|
101
|
+
parts = [keyword]
|
102
|
+
parts << emit_address(host)
|
103
|
+
parts << "port #{emit_port(port)}" if port
|
104
|
+
parts
|
105
|
+
end
|
106
|
+
|
107
|
+
# Return a valid PF representation of +host+.
|
108
|
+
def emit_address(host, if_unspecified = 'any')
|
109
|
+
if host
|
110
|
+
super(host)
|
111
|
+
else
|
112
|
+
if_unspecified
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def emit_rdr_to(rule)
|
117
|
+
return unless rule.rdr?
|
118
|
+
|
119
|
+
keyword = Puffy::Formatters::Base.loopback_addresses.include?(rule.rdr_to_host) ? 'divert-to' : 'rdr-to'
|
120
|
+
destination = rule.rdr_to_host || loopback_address(rule.af)
|
121
|
+
raise 'Unspecified address family' if destination.nil?
|
122
|
+
|
123
|
+
emit_endpoint_specification(keyword, destination, rule.rdr_to_port)
|
124
|
+
end
|
125
|
+
|
126
|
+
def emit_nat_to(rule)
|
127
|
+
"nat-to #{emit_address(rule.nat_to)}" if rule.nat_to
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|