From e2b776851c4fe31858d93bebb8bf7a1cd460e2a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romain=20Tarti=C3=A8re?= Date: Sat, 6 Jul 2024 13:08:52 +0200 Subject: [PATCH 1/3] Add support for variables in host/port lists Allow to pass variables as items of host lists and port lists. This allows constructs like this: ``` clients = { 192.168.0.10 192.168.0.20 } ports = { 123 5432 } node 'test' { pass in proto tcp from any to { $clients 10.0.0.10 } port { $ports 3000 } } ``` --- lib/puffy/parser.y | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/puffy/parser.y b/lib/puffy/parser.y index e92d6c2..69d1ac3 100644 --- a/lib/puffy/parser.y +++ b/lib/puffy/parser.y @@ -16,6 +16,7 @@ rule variable_value: ADDRESS { result = val[0][:value] } | STRING { result = val[0][:value] } | VARIABLE { result = @variables.fetch(val[0][:value]) } + | port { result = val[0] } service: SERVICE service_name block { @services[val[1]] = val[2] } @@ -132,9 +133,12 @@ rule | PORT port { result = val[1] } | - port_list: port_list ',' port { result = val[0] + [val[2]] } - | port_list port { result = val[0] + [val[1]] } - | port { result = [val[0]] } + port_list: port_list ',' port_list_item { result = val[0] + val[2] } + | port_list port_list_item { result = val[0] + val[1] } + | port_list_item { result = val[0] } + + port_list_item: port { result = [val[0]] } + | VARIABLE { result = @variables.fetch(val[0][:value]) } port: INTEGER { result = val[0][:value] } | IDENTIFIER { result = val[0][:value] } @@ -143,9 +147,12 @@ rule host: ADDRESS { result = val[0][:value] } | STRING { result = val[0][:value] } - host_list: host_list ',' host { result = val[0] + [val[2]] } - | host_list host { result = val[0] + [val[1]] } - | host { result = [val[0]] } + host_list: host_list ',' host_list_item { result = val[0] + val[2] } + | host_list host_list_item { result = val[0] + val[1] } + | host_list_item { result = val[0] } + + host_list_item: host { result = [val[0]] } + | VARIABLE { result = @variables.fetch(val[0][:value]) } filteropts: filteropts ',' filteropt { result = val[0].merge(val[2]) } | filteropts filteropt { result = val[0].merge(val[1]) } From d9b9b925fb484d637c6a67caec8cf8dccdf2b9d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romain=20Tarti=C3=A8re?= Date: Sat, 6 Jul 2024 13:38:28 +0200 Subject: [PATCH 2/3] Use the same addresses for example.com accross tests Some tests mock the example.com addresses as loopback addresses, other as IP addresses in reserved for documentation, and other do not mock the addresses and fallback to actual resolution with IP address that change from time to time. Always use the same addresses for all tests: - example.com: 2001:db8:fa4e:adde::42, 203.0.113.42 - example.net: 2001:db8:fa4e:adde::27, 203.0.113.27 --- spec/puffy/resolver_spec.rb | 27 +++++++++++++++++++-------- spec/puffy/rule_factory_spec.rb | 28 +++++++++++++++------------- spec/puffy/rule_spec.rb | 14 +++++++------- 3 files changed, 41 insertions(+), 28 deletions(-) diff --git a/spec/puffy/resolver_spec.rb b/spec/puffy/resolver_spec.rb index cb580b7..d3ac14d 100644 --- a/spec/puffy/resolver_spec.rb +++ b/spec/puffy/resolver_spec.rb @@ -7,21 +7,32 @@ # https://github.com/jordansissel/experiments/tree/master/ruby/dns-resolving-bug module Puffy RSpec.describe Resolver do - subject { Puffy::Resolver.instance } + before(:each) do + dns = Resolv::DNS.new + allow(dns).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([Resolv::DNS::Resource::IN::A.new('203.0.113.42')]) + allow(dns).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([Resolv::DNS::Resource::IN::AAAA.new('2001:db8:fa4e:adde::42')]) + allow(dns).to receive(:getresources).with('host.invalid.', Resolv::DNS::Resource::IN::A).and_call_original + allow(dns).to receive(:getresources).with('host.invalid.', Resolv::DNS::Resource::IN::AAAA).and_call_original + + allow(Resolv::DNS).to receive(:open).with(nil).and_return(dns) + end + + subject { Puffy::Resolver.clone.instance } + it 'resolves IPv4 and IPv6' do - expect(subject.resolv('example.com').collect(&:to_s)).to eq(['2606:2800:220:1:248:1893:25c8:1946', '93.184.216.34']) + expect(subject.resolv('example.com').collect(&:to_s)).to eq(['2001:db8:fa4e:adde::42', '203.0.113.42']) end it 'resolves IPv4 only' do - expect(subject.resolv('example.com', :inet).collect(&:to_s)).to eq(['93.184.216.34']) - expect(subject.resolv(IPAddr.new('93.184.216.34'), :inet).collect(&:to_s)).to eq(['93.184.216.34']) - expect(subject.resolv(IPAddr.new('2606:2800:220:1:248:1893:25c8:1946'), :inet).collect(&:to_s)).to eq([]) + expect(subject.resolv('example.com', :inet).collect(&:to_s)).to eq(['203.0.113.42']) + expect(subject.resolv(IPAddr.new('203.0.113.27'), :inet).collect(&:to_s)).to eq(['203.0.113.27']) + expect(subject.resolv(IPAddr.new('2001:db8:c0ff:ee::42'), :inet).collect(&:to_s)).to eq([]) end it 'resolves IPv6 only' do - expect(subject.resolv('example.com', :inet6).collect(&:to_s)).to eq(['2606:2800:220:1:248:1893:25c8:1946']) - expect(subject.resolv(IPAddr.new('93.184.216.34'), :inet6).collect(&:to_s)).to eq([]) - expect(subject.resolv(IPAddr.new('2606:2800:220:1:248:1893:25c8:1946'), :inet6).collect(&:to_s)).to eq(['2606:2800:220:1:248:1893:25c8:1946']) + expect(subject.resolv('example.com', :inet6).collect(&:to_s)).to eq(['2001:db8:fa4e:adde::42']) + expect(subject.resolv(IPAddr.new('203.0.113.27'), :inet6).collect(&:to_s)).to eq([]) + expect(subject.resolv(IPAddr.new('2001:db8:c0ff:ee::42'), :inet6).collect(&:to_s)).to eq(['2001:db8:c0ff:ee::42']) end it 'raises exceptions with unknown hosts' do diff --git a/spec/puffy/rule_factory_spec.rb b/spec/puffy/rule_factory_spec.rb index a317caf..b8d0581 100644 --- a/spec/puffy/rule_factory_spec.rb +++ b/spec/puffy/rule_factory_spec.rb @@ -26,10 +26,10 @@ module Puffy end it 'passes addresses and networks' do - result = subject.build(to: [{ host: IPAddr.new('192.0.2.1') }]) + result = subject.build(to: [{ host: IPAddr.new('203.0.113.42') }]) expect(result.count).to eq(1) - expect(result[0].to[:host]).to eq(IPAddr.new('192.0.2.1')) + expect(result[0].to[:host]).to eq(IPAddr.new('203.0.113.42')) result = subject.build(to: [{ host: IPAddr.new('192.0.2.0/24') }]) @@ -39,13 +39,13 @@ module Puffy it 'resolves hostnames' do expect(Rule).to receive(:new).twice.and_call_original - expect(Puffy::Resolver.instance).to receive(:resolv).with('example.com').and_return([IPAddr.new('2001:DB8::1'), IPAddr.new('192.0.2.1')]) + expect(Puffy::Resolver.instance).to receive(:resolv).with('example.com').and_return([IPAddr.new('2001:db8:fa4e:adde::42'), IPAddr.new('203.0.113.42')]) result = subject.build(to: [{ host: 'example.com' }]) expect(result.count).to eq(2) - expect(result[0].to[:host]).to eq(IPAddr.new('2001:DB8::1')) - expect(result[1].to[:host]).to eq(IPAddr.new('192.0.2.1')) + expect(result[0].to[:host]).to eq(IPAddr.new('2001:db8:fa4e:adde::42')) + expect(result[1].to[:host]).to eq(IPAddr.new('203.0.113.42')) end it 'accepts service names' do @@ -77,18 +77,18 @@ module Puffy end it 'does not mix IPv4 and IPv6' do - expect(Puffy::Resolver.instance).to receive(:resolv).with('example.net').and_return([IPAddr.new('2001:DB8::FFFF:FFFF:FFFF'), IPAddr.new('198.51.100.1')]) - expect(Puffy::Resolver.instance).to receive(:resolv).with('example.com').and_return([IPAddr.new('2001:DB8::1'), IPAddr.new('192.0.2.1')]) + expect(Puffy::Resolver.instance).to receive(:resolv).with('example.net').and_return([IPAddr.new('2001:db8:fa4e:adde::27'), IPAddr.new('203.0.113.27')]) + expect(Puffy::Resolver.instance).to receive(:resolv).with('example.com').and_return([IPAddr.new('2001:db8:fa4e:adde::42'), IPAddr.new('203.0.113.42')]) expect(Rule).to receive(:new).exactly(4).times.and_call_original result = subject.build(from: [{ host: 'example.net' }], to: [{ host: 'example.com' }]) expect(result.count).to eq(2) - expect(result[0].from[:host]).to eq(IPAddr.new('2001:DB8::FFFF:FFFF:FFFF')) - expect(result[0].to[:host]).to eq(IPAddr.new('2001:DB8::1')) - expect(result[1].from[:host]).to eq(IPAddr.new('198.51.100.1')) - expect(result[1].to[:host]).to eq(IPAddr.new('192.0.2.1')) + expect(result[0].from[:host]).to eq(IPAddr.new('2001:db8:fa4e:adde::27')) + expect(result[0].to[:host]).to eq(IPAddr.new('2001:db8:fa4e:adde::42')) + expect(result[1].from[:host]).to eq(IPAddr.new('203.0.113.27')) + expect(result[1].to[:host]).to eq(IPAddr.new('203.0.113.42')) end it 'filters address family' do @@ -100,19 +100,21 @@ module Puffy end it 'limits scope to IP version' do + expect(Puffy::Resolver.instance).to receive(:resolv).twice.with('example.com').and_return([IPAddr.new('2001:db8:fa4e:adde::42'), IPAddr.new('203.0.113.42')]) + result = [] subject.ipv4 do result = subject.build(to: [{ host: 'example.com' }]) end expect(result.count).to eq(1) - expect(result[0].to[:host]).to eq(IPAddr.new('93.184.216.34')) + expect(result[0].to[:host]).to eq(IPAddr.new('203.0.113.42')) subject.ipv6 do result = subject.build(to: [{ host: 'example.com' }]) end expect(result.count).to eq(1) - expect(result[0].to[:host]).to eq(IPAddr.new('2606:2800:220:1:248:1893:25c8:1946')) + expect(result[0].to[:host]).to eq(IPAddr.new('2001:db8:fa4e:adde::42')) end end end diff --git a/spec/puffy/rule_spec.rb b/spec/puffy/rule_spec.rb index 7f21993..c942535 100644 --- a/spec/puffy/rule_spec.rb +++ b/spec/puffy/rule_spec.rb @@ -12,16 +12,16 @@ module Puffy it 'detects IPv4 rules' do expect(Rule.new.ipv4?).to be_truthy expect(Rule.new(action: :block, dir: :out, proto: :tcp, to: { port: 80 }).ipv4?).to be_truthy - expect(Rule.new(action: :block, dir: :out, proto: :tcp, to: { host: IPAddr.new('192.0.2.1'), port: 80 }).ipv4?).to be_truthy - expect(Rule.new(action: :block, dir: :out, proto: :tcp, to: { host: IPAddr.new('2001:DB8::1'), port: 80 }).ipv4?).to be_falsy + expect(Rule.new(action: :block, dir: :out, proto: :tcp, to: { host: IPAddr.new('203.0.113.42'), port: 80 }).ipv4?).to be_truthy + expect(Rule.new(action: :block, dir: :out, proto: :tcp, to: { host: IPAddr.new('2001:db8:fa4e:adde::42'), port: 80 }).ipv4?).to be_falsy expect(Rule.new(action: :pass, dir: :fwd, in: 'eth0', out: 'eth1').ipv4?).to be_truthy end it 'detects IPv6 rules' do expect(Rule.new.ipv6?).to be_truthy expect(Rule.new(action: :block, dir: :out, proto: :tcp, to: { port: 80 }).ipv6?).to be_truthy - expect(Rule.new(action: :block, dir: :out, proto: :tcp, to: { host: IPAddr.new('192.0.2.1'), port: 80 }).ipv6?).to be_falsy - expect(Rule.new(action: :block, dir: :out, proto: :tcp, to: { host: IPAddr.new('2001:DB8::1'), port: 80 }).ipv6?).to be_truthy + expect(Rule.new(action: :block, dir: :out, proto: :tcp, to: { host: IPAddr.new('203.0.113.42'), port: 80 }).ipv6?).to be_falsy + expect(Rule.new(action: :block, dir: :out, proto: :tcp, to: { host: IPAddr.new('2001:db8:fa4e:adde::42'), port: 80 }).ipv6?).to be_truthy expect(Rule.new(action: :pass, dir: :fwd, in: 'eth0', out: 'eth1').ipv6?).to be_truthy end @@ -45,7 +45,7 @@ module Puffy expect(Rule.new.rdr?).to be_falsy expect(Rule.new(action: :pass, dir: :in, proto: :tcp, to: { port: 80 }).rdr?).to be_falsy expect(Rule.new(action: :pass, dir: :out, on: 'eth0', nat_to: IPAddr.new('198.51.100.72')).rdr?).to be_falsy - expect(Rule.new(action: :pass, dir: :in, on: 'eth0', rdr_to: { host: IPAddr.new('192.0.2.1') }).rdr?).to be_truthy + expect(Rule.new(action: :pass, dir: :in, on: 'eth0', rdr_to: { host: IPAddr.new('203.0.113.42') }).rdr?).to be_truthy expect(Rule.new(action: :pass, dir: :fwd, in: 'eth0', out: 'eth1').rdr?).to be_falsy end @@ -53,7 +53,7 @@ module Puffy expect(Rule.new.nat?).to be_falsy expect(Rule.new(action: :pass, dir: :out, proto: :tcp, to: { port: 80 }).nat?).to be_falsy expect(Rule.new(action: :pass, dir: :out, on: 'eth0', nat_to: IPAddr.new('198.51.100.72')).nat?).to be_truthy - expect(Rule.new(action: :pass, dir: :in, on: 'eth0', rdr_to: { host: IPAddr.new('192.0.2.1') }).nat?).to be_falsy + expect(Rule.new(action: :pass, dir: :in, on: 'eth0', rdr_to: { host: IPAddr.new('203.0.113.42') }).nat?).to be_falsy expect(Rule.new(action: :pass, dir: :fwd, in: 'eth0', out: 'eth1').nat?).to be_falsy end @@ -61,7 +61,7 @@ module Puffy expect(Rule.new.fwd?).to be_falsy expect(Rule.new(action: :pass, dir: :out, proto: :tcp, to: { port: 80 }).fwd?).to be_falsy expect(Rule.new(action: :pass, dir: :out, on: 'eth0', nat_to: IPAddr.new('198.51.100.72')).fwd?).to be_falsy - expect(Rule.new(action: :pass, dir: :in, on: 'eth0', rdr_to: { host: IPAddr.new('192.0.2.1') }).fwd?).to be_falsy + expect(Rule.new(action: :pass, dir: :in, on: 'eth0', rdr_to: { host: IPAddr.new('203.0.113.42') }).fwd?).to be_falsy expect(Rule.new(action: :pass, dir: :fwd, in: 'eth0', out: 'eth1').fwd?).to be_truthy end From 92c1f286501f0287296de349c8a57467a77dad40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romain=20Tarti=C3=A8re?= Date: Sat, 6 Jul 2024 14:44:03 +0200 Subject: [PATCH 3/3] Ensure parser is up-to-date before running spec/features --- Rakefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Rakefile b/Rakefile index a2c8e9c..3f3e16b 100644 --- a/Rakefile +++ b/Rakefile @@ -22,7 +22,9 @@ task test: %i[spec features] task default: :test +task feature: :gen_parser task build: :gen_parser +task spec: :gen_parser desc 'Generate the puffy language parser' task gen_parser: 'lib/puffy/parser.tab.rb'