puffy 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.com/opus-codium/puffy.svg?branch=master)](https://travis-ci.com/opus-codium/puffy)
|
4
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/1d46ac8511718fd284fd/maintainability)](https://codeclimate.com/github/opus-codium/puffy/maintainability)
|
5
|
+
[![Test Coverage](https://api.codeclimate.com/v1/badges/1d46ac8511718fd284fd/test_coverage)](https://codeclimate.com/github/opus-codium/puffy/test_coverage)
|
6
|
+
[![Inline docs](http://inch-ci.org/github/opus-codium/puffy.svg?branch=master)](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
|