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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b53bffbf2f30ba597482608cf4f3e005cf481bd69cbfa4fdaf7a9de14f1b90ca
|
4
|
+
data.tar.gz: 5c4fee30b67e7c8cd777d219173473d2e2d4f0b437c9df02e7f0d16ca9341515
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7a23559f7ff420013adaeafa437f13450a78b41bf1757e584c8bea54c7cade3fd51da7fc31497a2c70844c011d13d841edb0091eb801c03eee7737d9f0fe7a19
|
7
|
+
data.tar.gz: a3c6db0679f173cd714665e9a884c5eff22df4847562cb188f8d04c18da31ec79d3b9e54f6084d2a9514eb0dfb831337c878b58e92391ed86a7fd125888e181c
|
@@ -0,0 +1,36 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches:
|
6
|
+
- main
|
7
|
+
- master
|
8
|
+
pull_request:
|
9
|
+
branches:
|
10
|
+
- main
|
11
|
+
- master
|
12
|
+
|
13
|
+
jobs:
|
14
|
+
unit:
|
15
|
+
runs-on: ubuntu-latest
|
16
|
+
strategy:
|
17
|
+
matrix:
|
18
|
+
ruby:
|
19
|
+
- "2.6"
|
20
|
+
- "2.7"
|
21
|
+
- "3.0"
|
22
|
+
name: Ruby ${{ matrix.ruby }}
|
23
|
+
steps:
|
24
|
+
- uses: actions/checkout@v2
|
25
|
+
- name: Setup ruby
|
26
|
+
uses: ruby/setup-ruby@v1
|
27
|
+
with:
|
28
|
+
ruby-version: ${{ matrix.ruby }}
|
29
|
+
- name: Install dependencies
|
30
|
+
run: |
|
31
|
+
gem install bundler
|
32
|
+
bundle install --jobs 4 --retry 3
|
33
|
+
- name: Generate the parser
|
34
|
+
run: bundle exec rake gen_parser
|
35
|
+
- name: Run tests
|
36
|
+
run: bundle exec rake
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: '2.5'
|
3
|
+
AllowSymlinksInCacheRootDirectory: true
|
4
|
+
NewCops: enable
|
5
|
+
Exclude:
|
6
|
+
- lib/melt/*.tab.rb
|
7
|
+
- tmp/**/*.rb
|
8
|
+
|
9
|
+
Layout/HashAlignment:
|
10
|
+
EnforcedColonStyle: table
|
11
|
+
EnforcedHashRocketStyle: table
|
12
|
+
|
13
|
+
Layout/LineLength:
|
14
|
+
Enabled: false
|
15
|
+
|
16
|
+
Metrics/BlockLength:
|
17
|
+
Exclude:
|
18
|
+
- spec/**/*.rb
|
19
|
+
|
20
|
+
Metrics/ClassLength:
|
21
|
+
Max: 200
|
22
|
+
|
23
|
+
Metrics/ModuleLength:
|
24
|
+
Exclude:
|
25
|
+
- spec/**/*.rb
|
26
|
+
|
27
|
+
Style/Documentation:
|
28
|
+
Exclude:
|
29
|
+
- features/support/env.rb
|
30
|
+
- spec/**/*.rb
|
31
|
+
|
32
|
+
Style/TrailingCommaInArrayLiteral:
|
33
|
+
EnforcedStyleForMultiline: comma
|
34
|
+
|
35
|
+
Style/TrailingCommaInHashLiteral:
|
36
|
+
EnforcedStyleForMultiline: comma
|
37
|
+
|
38
|
+
Style/TrailingCommaInArguments:
|
39
|
+
EnforcedStyleForMultiline: comma
|
data/.simplecov
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# Puffy
|
2
|
+
|
3
|
+
[](https://travis-ci.com/opus-codium/puffy)
|
4
|
+
[](https://codeclimate.com/github/opus-codium/puffy/maintainability)
|
5
|
+
[](https://codeclimate.com/github/opus-codium/puffy/test_coverage)
|
6
|
+
[](http://inch-ci.org/github/opus-codium/puffy)
|
7
|
+
|
8
|
+
## Features
|
9
|
+
|
10
|
+
* Generate rules for [Netfilter](http://www.netfilter.org/) and [PF](http://www.openbsd.org/faq/pf/) (extensible);
|
11
|
+
* IPv6 and IPv4 support;
|
12
|
+
* Define the configuration of multiple *nodes* in a single file;
|
13
|
+
* Define *services* as group of rules to mix-in in *nodes* rules definitions;
|
14
|
+
* Handle NAT & port redirection;
|
15
|
+
|
16
|
+
## Requirements
|
17
|
+
|
18
|
+
* Accurate DNS information;
|
19
|
+
|
20
|
+
## Syntax
|
21
|
+
|
22
|
+
The Puffy syntax is inspired by the syntax of the [OpenBSD Packet Filter](http://www.openbsd.org/faq/pf/), with the ability to group rules in reusable blocks in order to describe all rules of a network of nodes in a single file.
|
23
|
+
|
24
|
+
Rules must appear in either a *node* or *service* definition, *services* being
|
25
|
+
reusable blocks of related rules:
|
26
|
+
|
27
|
+
~~~
|
28
|
+
service base do
|
29
|
+
service ntp
|
30
|
+
service ssh
|
31
|
+
end
|
32
|
+
|
33
|
+
service ntp do
|
34
|
+
pass out proto udp from any to port ntp
|
35
|
+
end
|
36
|
+
|
37
|
+
service ssh do
|
38
|
+
pass in proto tcp form any to port ssh
|
39
|
+
end
|
40
|
+
|
41
|
+
node 'db.example.com' do
|
42
|
+
service base
|
43
|
+
pass in proto tcp from 'www1.example.com' to port postgresql
|
44
|
+
end
|
45
|
+
|
46
|
+
node /www\d+.example.com/ do
|
47
|
+
service base
|
48
|
+
pass in proto tcp from any to port www
|
49
|
+
pass out proto tcp from any to 'db.example.com' port postgresql
|
50
|
+
end
|
51
|
+
~~~
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'cucumber'
|
6
|
+
require 'cucumber/rake/task'
|
7
|
+
|
8
|
+
RSpec::Core::RakeTask.new(:spec)
|
9
|
+
Cucumber::Rake::Task.new(:features)
|
10
|
+
|
11
|
+
task test: %i[spec features]
|
12
|
+
|
13
|
+
task default: :test
|
14
|
+
|
15
|
+
desc 'Generate the puffy language parser'
|
16
|
+
task gen_parser: 'lib/puffy/parser.tab.rb'
|
17
|
+
|
18
|
+
file 'lib/puffy/parser.tab.rb' => 'lib/puffy/parser.y' do
|
19
|
+
`racc -S lib/puffy/parser.y`
|
20
|
+
end
|
data/bin/puffy
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
$LOAD_PATH << File.expand_path('../lib', __dir__)
|
5
|
+
|
6
|
+
require 'puffy/cli'
|
7
|
+
|
8
|
+
begin
|
9
|
+
cli = Puffy::Cli.new
|
10
|
+
cli.execute(ARGV)
|
11
|
+
rescue Puffy::SyntaxError => e
|
12
|
+
$stderr.puts e.message
|
13
|
+
exit 1
|
14
|
+
rescue Puffy::ParseError => e
|
15
|
+
$stderr.puts e.message
|
16
|
+
exit 1
|
17
|
+
end
|
data/lib/core_ext.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Mixin for combining variations
|
4
|
+
#
|
5
|
+
# { a: :b }.expand #=> [{ a: :b }]
|
6
|
+
# { a: [:b, :c] }.expand #=> [{ a: :b }, { a: :c }]
|
7
|
+
# {
|
8
|
+
# a: [:b, :c],
|
9
|
+
# d: [:e, :f],
|
10
|
+
# }.expand
|
11
|
+
# #=> [{ a: :b, d: :e }, {a: :b, d: :f}, { a: :c, d: :e }, { a: :c, d: :f }]
|
12
|
+
module Expandable
|
13
|
+
# Returns an array composed of all possible variation of the object by
|
14
|
+
# combining all the items of it's array values.
|
15
|
+
#
|
16
|
+
# @return [Array]
|
17
|
+
def expand
|
18
|
+
@expand_res = [{}]
|
19
|
+
each do |key, value|
|
20
|
+
case value
|
21
|
+
when Array then expand_array(key)
|
22
|
+
when Hash then expand_hash(key)
|
23
|
+
else @expand_res.map! { |hash| hash.merge(key => value) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
@expand_res
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def expand_array(key)
|
32
|
+
orig = @expand_res
|
33
|
+
@expand_res = []
|
34
|
+
fetch(key).each do |value|
|
35
|
+
@expand_res += orig.map { |hash| hash.merge(key => value) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def expand_hash(key)
|
40
|
+
orig = @expand_res
|
41
|
+
@expand_res = []
|
42
|
+
fetch(key).expand.each do |value|
|
43
|
+
@expand_res += orig.map { |hash| hash.merge(key => value) }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Array # :nodoc:
|
49
|
+
def deep_dup
|
50
|
+
array = []
|
51
|
+
each do |value|
|
52
|
+
array << if value.respond_to?(:deep_dup)
|
53
|
+
value.deep_dup
|
54
|
+
else
|
55
|
+
value.dup
|
56
|
+
end
|
57
|
+
end
|
58
|
+
array
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class Hash # :nodoc:
|
63
|
+
include Expandable
|
64
|
+
|
65
|
+
def deep_dup
|
66
|
+
hash = dup
|
67
|
+
each_pair do |key, value|
|
68
|
+
hash[key.dup] = if value.respond_to?(:deep_dup)
|
69
|
+
value.deep_dup
|
70
|
+
else
|
71
|
+
value.dup
|
72
|
+
end
|
73
|
+
end
|
74
|
+
hash
|
75
|
+
end
|
76
|
+
end
|
data/lib/puffy/cli.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cri'
|
4
|
+
require 'puffy'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module Puffy
|
8
|
+
# Command-line processing
|
9
|
+
class Cli
|
10
|
+
def initialize
|
11
|
+
cli = self
|
12
|
+
|
13
|
+
@main = Cri::Command.define do
|
14
|
+
name 'puffy'
|
15
|
+
usage 'puffy [options] <command>'
|
16
|
+
summary 'Network firewall rules made easy!'
|
17
|
+
|
18
|
+
description <<~DESCRIPTION
|
19
|
+
Generate firewall rules for multiple nodes from a single network
|
20
|
+
specification file.
|
21
|
+
DESCRIPTION
|
22
|
+
|
23
|
+
flag :h, :help, 'Show help for this command' do |_value, cmd|
|
24
|
+
puts cmd.help
|
25
|
+
exit 0
|
26
|
+
end
|
27
|
+
|
28
|
+
run do |_opts, _args, cmd|
|
29
|
+
puts cmd.help
|
30
|
+
exit 0
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
@main.define_command do
|
35
|
+
name 'generate'
|
36
|
+
usage 'generate [options] <network> <hostname>'
|
37
|
+
summary 'Generate the firewall configuration for a node'
|
38
|
+
|
39
|
+
description <<~DESCRIPTION
|
40
|
+
Generate the firewall configuration for the node "hostname" for which
|
41
|
+
the configuration is described in the "network" specification file.
|
42
|
+
DESCRIPTION
|
43
|
+
|
44
|
+
required :f, :formatter, 'The formatter to use', default: 'Pf'
|
45
|
+
|
46
|
+
param('network')
|
47
|
+
param('hostname')
|
48
|
+
|
49
|
+
run do |opts, args|
|
50
|
+
parser = cli.load_network(args[:network])
|
51
|
+
rules = parser.ruleset_for(args[:hostname])
|
52
|
+
policy = parser.policy_for(args[:hostname])
|
53
|
+
|
54
|
+
formatter = Object.const_get("Puffy::Formatters::#{opts[:formatter]}::Ruleset").new
|
55
|
+
puts formatter.emit_ruleset(rules, policy)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
puppet = @main.define_command do
|
60
|
+
name 'puppet'
|
61
|
+
usage 'puppet [options] <subcommand>'
|
62
|
+
summary 'Run puppet actions'
|
63
|
+
|
64
|
+
description <<~DESCRIPTION
|
65
|
+
Manage a directory of firewall configurations files suitable for Puppet.
|
66
|
+
DESCRIPTION
|
67
|
+
|
68
|
+
required :o, :output, 'Base directory for network firewall rules', default: '.'
|
69
|
+
|
70
|
+
run do |_opts, _args, cmd|
|
71
|
+
puts cmd.help
|
72
|
+
exit 0
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
puppet.define_command do
|
77
|
+
name 'diff'
|
78
|
+
usage 'diff [options] <network>'
|
79
|
+
summary 'Show differences between network specification and firewall rules'
|
80
|
+
|
81
|
+
description <<~DESCRIPTION
|
82
|
+
Show the changes that would be introduced by running `puffy puppet
|
83
|
+
generate` with the current "network" specification file.
|
84
|
+
DESCRIPTION
|
85
|
+
|
86
|
+
param('network')
|
87
|
+
|
88
|
+
run do |opts, args|
|
89
|
+
parser = cli.load_network(args[:network])
|
90
|
+
puppet = Puffy::Puppet.new(opts[:output], parser)
|
91
|
+
puppet.diff
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
puppet.define_command do
|
96
|
+
name 'generate'
|
97
|
+
usage 'generate [options] <network>'
|
98
|
+
summary 'Generate network firewall rules according to network specification'
|
99
|
+
|
100
|
+
description <<~DESCRIPTION
|
101
|
+
Generate a tree of configuration files suitable for all supported
|
102
|
+
firewalls for all the nodes described in the "network" specification
|
103
|
+
file.
|
104
|
+
DESCRIPTION
|
105
|
+
|
106
|
+
param('network')
|
107
|
+
|
108
|
+
run do |opts, args|
|
109
|
+
parser = cli.load_network(args[:network])
|
110
|
+
puppet = Puffy::Puppet.new(opts[:output], parser)
|
111
|
+
puppet.save
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
help_command = Cri::Command.new_basic_help.modify do
|
116
|
+
summary 'Show help for a command'
|
117
|
+
end
|
118
|
+
|
119
|
+
@main.add_command(help_command)
|
120
|
+
end
|
121
|
+
|
122
|
+
def load_network(filename)
|
123
|
+
parser = Puffy::Parser.new
|
124
|
+
parser.parse_file(filename)
|
125
|
+
parser
|
126
|
+
end
|
127
|
+
|
128
|
+
def execute(argv)
|
129
|
+
@main.run(argv)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Puffy
|
4
|
+
module Formatters # :nodoc:
|
5
|
+
module Base # :nodoc:
|
6
|
+
# Returns the loopback IPv4 IPAddr
|
7
|
+
#
|
8
|
+
# @return [IPAddr]
|
9
|
+
def self.loopback_ipv4
|
10
|
+
IPAddr.new('127.0.0.1')
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns the loopback IPv6 IPAddr
|
14
|
+
#
|
15
|
+
# @return [IPAddr]
|
16
|
+
def self.loopback_ipv6
|
17
|
+
IPAddr.new('::1')
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns a list of loopback addresses
|
21
|
+
#
|
22
|
+
# @return [Array<IPAddr>]
|
23
|
+
def self.loopback_addresses
|
24
|
+
[nil, loopback_ipv4, loopback_ipv6]
|
25
|
+
end
|
26
|
+
|
27
|
+
# Base class for Puffy Formatter Rulesets
|
28
|
+
class Ruleset
|
29
|
+
def initialize
|
30
|
+
@rule_formatter = Class.const_get(self.class.name.sub(/set$/, '')).new
|
31
|
+
end
|
32
|
+
|
33
|
+
def emit_header
|
34
|
+
["# Generated by puffy v#{Puffy::VERSION} on #{Time.now.strftime('%c')}"]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns a String representation of the provided +rules+ Array of Puffy::Rule with the +policy+ policy.
|
38
|
+
#
|
39
|
+
# @param rules [Array<Puffy::Rule>] array of Puffy::Rule.
|
40
|
+
# @param _policy [Symbol] ruleset policy.
|
41
|
+
# @return [String]
|
42
|
+
def emit_ruleset(rules, _policy = nil)
|
43
|
+
rules.collect { |rule| @rule_formatter.emit_rule(rule) }.join("\n")
|
44
|
+
end
|
45
|
+
|
46
|
+
# Filename for a firewall configuration fragment emitted by the formatter.
|
47
|
+
#
|
48
|
+
# @return [Array<String>]
|
49
|
+
def filename_fragment
|
50
|
+
raise 'Formatters#filename_fragment MUST be overriden'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Base class for Puffy Formatter Rulesets
|
55
|
+
class Rule
|
56
|
+
protected
|
57
|
+
|
58
|
+
# Returns the loopback IPAddr of the given +address_family+
|
59
|
+
#
|
60
|
+
# @param address_family [Symbol] the address family, +:inet+ or +:inet6+
|
61
|
+
# @return [IPAddr,nil]
|
62
|
+
def loopback_address(address_family)
|
63
|
+
case address_family
|
64
|
+
when nil then nil
|
65
|
+
when :inet then Puffy::Formatters::Base.loopback_ipv4
|
66
|
+
when :inet6 then Puffy::Formatters::Base.loopback_ipv6
|
67
|
+
else raise "Unsupported address family #{address_family.inspect}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Return a string representation of the +host+ IPAddr as a host or network.
|
72
|
+
# @param host [IPAddr]
|
73
|
+
# @return [String] IP address
|
74
|
+
def emit_address(host)
|
75
|
+
if (host.ipv4? && host.prefix.to_i == 32) || (host.ipv6? && host.prefix.to_i == 128)
|
76
|
+
host.to_s
|
77
|
+
else
|
78
|
+
"#{host}/#{host.prefix}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Return a string representation of the +port+ port.
|
83
|
+
# #param port [Integer,Range]
|
84
|
+
# @return [String] Port
|
85
|
+
def emit_port(port)
|
86
|
+
case port
|
87
|
+
when Integer then port.to_s
|
88
|
+
when Range then "#{port.begin}:#{port.end}"
|
89
|
+
else raise "Unexpected #{port.class.name}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|