Layer 4 TLS SNI proxy with global firewall (IP/CIDR/GeoIP blocking), per-listener route tables, bidirectional TCP relay with half-close propagation, and a gRPC admin API (routes, firewall, status) with TLS/mTLS support. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
142 lines
2.9 KiB
Go
142 lines
2.9 KiB
Go
package firewall
|
|
|
|
import (
|
|
"net/netip"
|
|
"testing"
|
|
|
|
"git.wntrmute.dev/kyle/mc-proxy/internal/config"
|
|
)
|
|
|
|
func TestEmptyFirewall(t *testing.T) {
|
|
fw, err := New(config.Firewall{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
defer fw.Close()
|
|
|
|
addrs := []string{"192.168.1.1", "10.0.0.1", "::1", "2001:db8::1"}
|
|
for _, a := range addrs {
|
|
addr := netip.MustParseAddr(a)
|
|
if fw.Blocked(addr) {
|
|
t.Fatalf("empty firewall blocked %s", addr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIPBlocking(t *testing.T) {
|
|
fw, err := New(config.Firewall{
|
|
BlockedIPs: []string{"192.0.2.1", "2001:db8::dead"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
defer fw.Close()
|
|
|
|
tests := []struct {
|
|
addr string
|
|
blocked bool
|
|
}{
|
|
{"192.0.2.1", true},
|
|
{"192.0.2.2", false},
|
|
{"2001:db8::dead", true},
|
|
{"2001:db8::beef", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
addr := netip.MustParseAddr(tt.addr)
|
|
if got := fw.Blocked(addr); got != tt.blocked {
|
|
t.Fatalf("Blocked(%s) = %v, want %v", tt.addr, got, tt.blocked)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCIDRBlocking(t *testing.T) {
|
|
fw, err := New(config.Firewall{
|
|
BlockedCIDRs: []string{"198.51.100.0/24", "2001:db8::/32"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
defer fw.Close()
|
|
|
|
tests := []struct {
|
|
addr string
|
|
blocked bool
|
|
}{
|
|
{"198.51.100.1", true},
|
|
{"198.51.100.254", true},
|
|
{"198.51.101.1", false},
|
|
{"2001:db8::1", true},
|
|
{"2001:db9::1", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
addr := netip.MustParseAddr(tt.addr)
|
|
if got := fw.Blocked(addr); got != tt.blocked {
|
|
t.Fatalf("Blocked(%s) = %v, want %v", tt.addr, got, tt.blocked)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIPv4MappedIPv6(t *testing.T) {
|
|
fw, err := New(config.Firewall{
|
|
BlockedIPs: []string{"192.0.2.1"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
defer fw.Close()
|
|
|
|
// IPv4-mapped IPv6 representation of 192.0.2.1.
|
|
addr := netip.MustParseAddr("::ffff:192.0.2.1")
|
|
if !fw.Blocked(addr) {
|
|
t.Fatal("expected IPv4-mapped IPv6 address to be blocked")
|
|
}
|
|
}
|
|
|
|
func TestInvalidIP(t *testing.T) {
|
|
_, err := New(config.Firewall{
|
|
BlockedIPs: []string{"not-an-ip"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid IP")
|
|
}
|
|
}
|
|
|
|
func TestInvalidCIDR(t *testing.T) {
|
|
_, err := New(config.Firewall{
|
|
BlockedCIDRs: []string{"not-a-cidr"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid CIDR")
|
|
}
|
|
}
|
|
|
|
func TestCombinedRules(t *testing.T) {
|
|
fw, err := New(config.Firewall{
|
|
BlockedIPs: []string{"10.0.0.1"},
|
|
BlockedCIDRs: []string{"192.168.0.0/16"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
defer fw.Close()
|
|
|
|
tests := []struct {
|
|
addr string
|
|
blocked bool
|
|
}{
|
|
{"10.0.0.1", true}, // IP match
|
|
{"10.0.0.2", false}, // no match
|
|
{"192.168.1.1", true}, // CIDR match
|
|
{"172.16.0.1", false}, // no match
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
addr := netip.MustParseAddr(tt.addr)
|
|
if got := fw.Blocked(addr); got != tt.blocked {
|
|
t.Fatalf("Blocked(%s) = %v, want %v", tt.addr, got, tt.blocked)
|
|
}
|
|
}
|
|
}
|