مراية لـ
https://github.com/postalserver/postal.git
تم المزامنة 2026-06-13 09:45:43 +00:00
The spec relied on the test machine having real IPv6 connectivity, which GitHub Actions runners do not have.
193 أسطر
5.5 KiB
Ruby
193 أسطر
5.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "rails_helper"
|
|
|
|
RSpec.describe Postal::HTTP::AddressGuard do
|
|
describe ".safe_connect_address" do
|
|
subject(:call) { described_class.safe_connect_address(host) }
|
|
|
|
before do
|
|
allow(Postal::Config.postal).to receive(:allowed_request_destinations).and_return(allowlist)
|
|
end
|
|
|
|
let(:allowlist) { [] }
|
|
|
|
context "when given a public IP literal" do
|
|
let(:host) { "93.184.216.34" }
|
|
|
|
it "returns the address to connect to" do
|
|
expect(call).to eq "93.184.216.34"
|
|
end
|
|
end
|
|
|
|
context "when given a public IPv6 literal" do
|
|
let(:host) { "2606:2800:220:1:248:1893:25c8:1946" }
|
|
|
|
before { allow(described_class).to receive(:ipv6_supported?).and_return(true) }
|
|
|
|
it "returns the address to connect to" do
|
|
expect(call).to eq "2606:2800:220:1:248:1893:25c8:1946"
|
|
end
|
|
end
|
|
|
|
[
|
|
"127.0.0.1",
|
|
"10.0.0.1",
|
|
"172.16.5.4",
|
|
"192.168.1.1",
|
|
"169.254.169.254", # cloud metadata
|
|
"100.64.0.1", # carrier-grade NAT
|
|
"0.0.0.0",
|
|
"::1",
|
|
"fd00::1", # unique-local IPv6
|
|
"fe80::1", # link-local IPv6
|
|
"::ffff:127.0.0.1", # IPv4-mapped loopback
|
|
].each do |blocked|
|
|
context "when given the blocked address #{blocked}" do
|
|
let(:host) { blocked }
|
|
|
|
it "raises BlockedDestinationError" do
|
|
expect { call }.to raise_error(Postal::HTTP::BlockedDestinationError)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when given a hostname that resolves to a public address" do
|
|
let(:host) { "example.com" }
|
|
|
|
before do
|
|
allow(Resolv).to receive(:getaddresses).with(host).and_return(["93.184.216.34"])
|
|
end
|
|
|
|
it "returns the resolved address" do
|
|
expect(call).to eq "93.184.216.34"
|
|
end
|
|
end
|
|
|
|
context "when given a hostname that resolves to a private address" do
|
|
let(:host) { "internal.example.com" }
|
|
|
|
before do
|
|
allow(Resolv).to receive(:getaddresses).with(host).and_return(["10.1.2.3"])
|
|
end
|
|
|
|
it "raises BlockedDestinationError" do
|
|
expect { call }.to raise_error(Postal::HTTP::BlockedDestinationError)
|
|
end
|
|
end
|
|
|
|
context "when a hostname resolves to both a public and a private address" do
|
|
let(:host) { "rebind.example.com" }
|
|
|
|
before do
|
|
allow(Resolv).to receive(:getaddresses).with(host).and_return(["93.184.216.34", "127.0.0.1"])
|
|
end
|
|
|
|
it "raises BlockedDestinationError because one address is blocked" do
|
|
expect { call }.to raise_error(Postal::HTTP::BlockedDestinationError)
|
|
end
|
|
end
|
|
|
|
context "when a hostname resolves to both IPv4 and IPv6 addresses" do
|
|
let(:host) { "dualstack.example.com" }
|
|
let(:ipv6) { "2606:2800:220:1:248:1893:25c8:1946" }
|
|
|
|
before do
|
|
allow(Resolv).to receive(:getaddresses).with(host).and_return([ipv6, "93.184.216.34"])
|
|
end
|
|
|
|
context "and the server does not support IPv6" do
|
|
before { allow(described_class).to receive(:ipv6_supported?).and_return(false) }
|
|
|
|
it "connects over IPv4" do
|
|
expect(call).to eq "93.184.216.34"
|
|
end
|
|
end
|
|
|
|
context "and the server supports IPv6" do
|
|
before { allow(described_class).to receive(:ipv6_supported?).and_return(true) }
|
|
|
|
it "still prefers IPv4 for predictability" do
|
|
expect(call).to eq "93.184.216.34"
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when a hostname resolves only to an IPv6 address" do
|
|
let(:host) { "v6only.example.com" }
|
|
let(:ipv6) { "2606:2800:220:1:248:1893:25c8:1946" }
|
|
|
|
before do
|
|
allow(Resolv).to receive(:getaddresses).with(host).and_return([ipv6])
|
|
end
|
|
|
|
context "and the server does not support IPv6" do
|
|
before { allow(described_class).to receive(:ipv6_supported?).and_return(false) }
|
|
|
|
it "raises a SocketError because the address is unreachable" do
|
|
expect { call }.to raise_error(SocketError)
|
|
end
|
|
end
|
|
|
|
context "and the server supports IPv6" do
|
|
before { allow(described_class).to receive(:ipv6_supported?).and_return(true) }
|
|
|
|
it "connects over IPv6" do
|
|
expect(call).to eq ipv6
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when a hostname cannot be resolved" do
|
|
let(:host) { "nope.example.com" }
|
|
|
|
before do
|
|
allow(Resolv).to receive(:getaddresses).with(host).and_return([])
|
|
end
|
|
|
|
it "raises BlockedDestinationError" do
|
|
expect { call }.to raise_error(Postal::HTTP::BlockedDestinationError, /resolve/)
|
|
end
|
|
end
|
|
|
|
context "when the host is blank" do
|
|
let(:host) { "" }
|
|
|
|
it "raises BlockedDestinationError" do
|
|
expect { call }.to raise_error(Postal::HTTP::BlockedDestinationError)
|
|
end
|
|
end
|
|
|
|
context "when a blocked address is allowlisted by CIDR" do
|
|
let(:host) { "10.0.0.5" }
|
|
let(:allowlist) { ["10.0.0.0/8"] }
|
|
|
|
it "returns the address" do
|
|
expect(call).to eq "10.0.0.5"
|
|
end
|
|
end
|
|
|
|
context "when a blocked address is allowlisted by exact IP" do
|
|
let(:host) { "127.0.0.1" }
|
|
let(:allowlist) { ["127.0.0.1"] }
|
|
|
|
it "returns the address" do
|
|
expect(call).to eq "127.0.0.1"
|
|
end
|
|
end
|
|
|
|
context "when a hostname resolving to a private address is allowlisted by name" do
|
|
let(:host) { "internal.example.com" }
|
|
let(:allowlist) { ["internal.example.com"] }
|
|
|
|
before do
|
|
allow(Resolv).to receive(:getaddresses).with(host).and_return(["10.1.2.3"])
|
|
end
|
|
|
|
it "returns the resolved address" do
|
|
expect(call).to eq "10.1.2.3"
|
|
end
|
|
end
|
|
end
|
|
end
|