- Fix 314 errcheck violations (blank identifier for unrecoverable errors) - Fix errorlint violation (errors.Is for io.EOF) - Remove unused serveL7Route test helper - Simplify Duration.Seconds() selectors in tests - Remove unnecessary fmt.Sprintf in test - Migrate exclusion rules from issues.exclusions to linters.exclusions (v2 schema) - Add gosec test exclusions (G115, G304, G402, G705) - Disable fieldalignment govet analyzer (optimization, not correctness) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
390 lines
9.7 KiB
Go
390 lines
9.7 KiB
Go
package proxyproto
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"net"
|
|
"net/netip"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// pipeWithDeadline returns a net.Conn pair where the reader side supports deadlines.
|
|
func pipeWithDeadline(t *testing.T) (reader net.Conn, writer net.Conn) {
|
|
t.Helper()
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = ln.Close() })
|
|
|
|
ch := make(chan net.Conn, 1)
|
|
go func() {
|
|
c, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
ch <- c
|
|
}()
|
|
|
|
w, err := net.Dial("tcp", ln.Addr().String())
|
|
if err != nil {
|
|
t.Fatalf("dial: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = w.Close() })
|
|
|
|
r := <-ch
|
|
t.Cleanup(func() { _ = r.Close() })
|
|
return r, w
|
|
}
|
|
|
|
func TestParseV1TCP4(t *testing.T) {
|
|
reader, writer := pipeWithDeadline(t)
|
|
|
|
go func() {
|
|
_, _ = writer.Write([]byte("PROXY TCP4 192.168.1.1 10.0.0.1 56324 443\r\n"))
|
|
}()
|
|
|
|
hdr, err := Parse(reader, time.Now().Add(5*time.Second))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
|
|
if hdr.Version != 1 {
|
|
t.Fatalf("version = %d, want 1", hdr.Version)
|
|
}
|
|
if hdr.Command != CommandProxy {
|
|
t.Fatalf("command = %d, want CommandProxy", hdr.Command)
|
|
}
|
|
if got := hdr.SrcAddr.String(); got != "192.168.1.1:56324" {
|
|
t.Fatalf("src = %s, want 192.168.1.1:56324", got)
|
|
}
|
|
if got := hdr.DstAddr.String(); got != "10.0.0.1:443" {
|
|
t.Fatalf("dst = %s, want 10.0.0.1:443", got)
|
|
}
|
|
}
|
|
|
|
func TestParseV1TCP6(t *testing.T) {
|
|
reader, writer := pipeWithDeadline(t)
|
|
|
|
go func() {
|
|
_, _ = writer.Write([]byte("PROXY TCP6 2001:db8::1 2001:db8::2 56324 8443\r\n"))
|
|
}()
|
|
|
|
hdr, err := Parse(reader, time.Now().Add(5*time.Second))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
|
|
if hdr.Version != 1 {
|
|
t.Fatalf("version = %d, want 1", hdr.Version)
|
|
}
|
|
wantSrc := "[2001:db8::1]:56324"
|
|
if got := hdr.SrcAddr.String(); got != wantSrc {
|
|
t.Fatalf("src = %s, want %s", got, wantSrc)
|
|
}
|
|
}
|
|
|
|
func TestParseV2TCP4(t *testing.T) {
|
|
reader, writer := pipeWithDeadline(t)
|
|
|
|
go func() {
|
|
var buf []byte
|
|
buf = append(buf, v2Signature[:]...)
|
|
buf = append(buf, 0x21) // version 2, PROXY command
|
|
buf = append(buf, 0x11) // AF_INET, STREAM
|
|
buf = binary.BigEndian.AppendUint16(buf, 12)
|
|
buf = append(buf, 192, 168, 1, 100) // src IP
|
|
buf = append(buf, 10, 0, 0, 1) // dst IP
|
|
buf = binary.BigEndian.AppendUint16(buf, 12345)
|
|
buf = binary.BigEndian.AppendUint16(buf, 443)
|
|
_, _ = writer.Write(buf)
|
|
}()
|
|
|
|
hdr, err := Parse(reader, time.Now().Add(5*time.Second))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
|
|
if hdr.Version != 2 {
|
|
t.Fatalf("version = %d, want 2", hdr.Version)
|
|
}
|
|
if hdr.Command != CommandProxy {
|
|
t.Fatalf("command = %d, want CommandProxy", hdr.Command)
|
|
}
|
|
if got := hdr.SrcAddr.String(); got != "192.168.1.100:12345" {
|
|
t.Fatalf("src = %s, want 192.168.1.100:12345", got)
|
|
}
|
|
if got := hdr.DstAddr.String(); got != "10.0.0.1:443" {
|
|
t.Fatalf("dst = %s, want 10.0.0.1:443", got)
|
|
}
|
|
}
|
|
|
|
func TestParseV2TCP6(t *testing.T) {
|
|
reader, writer := pipeWithDeadline(t)
|
|
|
|
go func() {
|
|
var buf []byte
|
|
buf = append(buf, v2Signature[:]...)
|
|
buf = append(buf, 0x21) // version 2, PROXY command
|
|
buf = append(buf, 0x21) // AF_INET6, STREAM
|
|
buf = binary.BigEndian.AppendUint16(buf, 36)
|
|
// src: 2001:db8::1
|
|
src := netip.MustParseAddr("2001:db8::1").As16()
|
|
buf = append(buf, src[:]...)
|
|
// dst: 2001:db8::2
|
|
dst := netip.MustParseAddr("2001:db8::2").As16()
|
|
buf = append(buf, dst[:]...)
|
|
buf = binary.BigEndian.AppendUint16(buf, 56324)
|
|
buf = binary.BigEndian.AppendUint16(buf, 8443)
|
|
_, _ = writer.Write(buf)
|
|
}()
|
|
|
|
hdr, err := Parse(reader, time.Now().Add(5*time.Second))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
|
|
if hdr.Version != 2 {
|
|
t.Fatalf("version = %d, want 2", hdr.Version)
|
|
}
|
|
wantSrc := "[2001:db8::1]:56324"
|
|
if got := hdr.SrcAddr.String(); got != wantSrc {
|
|
t.Fatalf("src = %s, want %s", got, wantSrc)
|
|
}
|
|
}
|
|
|
|
func TestParseV2Local(t *testing.T) {
|
|
reader, writer := pipeWithDeadline(t)
|
|
|
|
go func() {
|
|
var buf []byte
|
|
buf = append(buf, v2Signature[:]...)
|
|
buf = append(buf, 0x20) // version 2, LOCAL command
|
|
buf = append(buf, 0x00) // unspec family, unspec protocol
|
|
buf = binary.BigEndian.AppendUint16(buf, 0)
|
|
_, _ = writer.Write(buf)
|
|
}()
|
|
|
|
hdr, err := Parse(reader, time.Now().Add(5*time.Second))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
|
|
if hdr.Version != 2 {
|
|
t.Fatalf("version = %d, want 2", hdr.Version)
|
|
}
|
|
if hdr.Command != CommandLocal {
|
|
t.Fatalf("command = %d, want CommandLocal", hdr.Command)
|
|
}
|
|
}
|
|
|
|
func TestParseV1Malformed(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
data string
|
|
}{
|
|
{"wrong prefix", "XROXY TCP4 1.2.3.4 5.6.7.8 1 2\r\n"},
|
|
{"missing fields", "PROXY TCP4 1.2.3.4\r\n"},
|
|
{"bad protocol", "PROXY UDP4 1.2.3.4 5.6.7.8 1 2\r\n"},
|
|
{"bad src IP", "PROXY TCP4 not-an-ip 5.6.7.8 1 2\r\n"},
|
|
{"bad src port", "PROXY TCP4 1.2.3.4 5.6.7.8 notport 2\r\n"},
|
|
{"missing CRLF", "PROXY TCP4 1.2.3.4 5.6.7.8 1 2\n"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
reader, writer := pipeWithDeadline(t)
|
|
go func() {
|
|
_, _ = writer.Write([]byte(tt.data))
|
|
}()
|
|
_, err := Parse(reader, time.Now().Add(2*time.Second))
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseV2Malformed(t *testing.T) {
|
|
t.Run("bad signature", func(t *testing.T) {
|
|
reader, writer := pipeWithDeadline(t)
|
|
go func() {
|
|
bad := make([]byte, v2HeaderLen)
|
|
bad[0] = v2Signature[0] // first byte matches but rest doesn't
|
|
_, _ = writer.Write(bad)
|
|
}()
|
|
_, err := Parse(reader, time.Now().Add(2*time.Second))
|
|
if err == nil {
|
|
t.Fatal("expected error for bad signature")
|
|
}
|
|
})
|
|
|
|
t.Run("bad version", func(t *testing.T) {
|
|
reader, writer := pipeWithDeadline(t)
|
|
go func() {
|
|
var buf []byte
|
|
buf = append(buf, v2Signature[:]...)
|
|
buf = append(buf, 0x31) // version 3, PROXY command
|
|
buf = append(buf, 0x11)
|
|
buf = binary.BigEndian.AppendUint16(buf, 0)
|
|
_, _ = writer.Write(buf)
|
|
}()
|
|
_, err := Parse(reader, time.Now().Add(2*time.Second))
|
|
if err == nil {
|
|
t.Fatal("expected error for bad version")
|
|
}
|
|
})
|
|
|
|
t.Run("truncated address", func(t *testing.T) {
|
|
reader, writer := pipeWithDeadline(t)
|
|
go func() {
|
|
var buf []byte
|
|
buf = append(buf, v2Signature[:]...)
|
|
buf = append(buf, 0x21) // version 2, PROXY
|
|
buf = append(buf, 0x11) // AF_INET, STREAM
|
|
buf = binary.BigEndian.AppendUint16(buf, 12)
|
|
buf = append(buf, 1, 2, 3) // only 3 bytes, need 12
|
|
_, _ = writer.Write(buf)
|
|
_ = writer.Close()
|
|
}()
|
|
_, err := Parse(reader, time.Now().Add(2*time.Second))
|
|
if err == nil {
|
|
t.Fatal("expected error for truncated address")
|
|
}
|
|
})
|
|
|
|
t.Run("unsupported family", func(t *testing.T) {
|
|
reader, writer := pipeWithDeadline(t)
|
|
go func() {
|
|
var buf []byte
|
|
buf = append(buf, v2Signature[:]...)
|
|
buf = append(buf, 0x21) // version 2, PROXY
|
|
buf = append(buf, 0x31) // AF_UNIX (3), STREAM
|
|
buf = binary.BigEndian.AppendUint16(buf, 0)
|
|
_, _ = writer.Write(buf)
|
|
}()
|
|
_, err := Parse(reader, time.Now().Add(2*time.Second))
|
|
if err == nil {
|
|
t.Fatal("expected error for unsupported family")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestParseTimeout(t *testing.T) {
|
|
reader, _ := pipeWithDeadline(t)
|
|
|
|
// No data sent — should hit deadline.
|
|
_, err := Parse(reader, time.Now().Add(100*time.Millisecond))
|
|
if err == nil {
|
|
t.Fatal("expected timeout error")
|
|
}
|
|
}
|
|
|
|
func TestWriteV2IPv4(t *testing.T) {
|
|
src := netip.MustParseAddrPort("192.168.1.1:56324")
|
|
dst := netip.MustParseAddrPort("10.0.0.1:443")
|
|
|
|
var buf bytes.Buffer
|
|
if err := WriteV2(&buf, src, dst); err != nil {
|
|
t.Fatalf("WriteV2: %v", err)
|
|
}
|
|
|
|
b := buf.Bytes()
|
|
// Signature (12) + ver/cmd (1) + fam (1) + len (2) + 4+4+2+2 = 28
|
|
if len(b) != 28 {
|
|
t.Fatalf("wrote %d bytes, want 28", len(b))
|
|
}
|
|
// Verify signature.
|
|
if [12]byte(b[:12]) != v2Signature {
|
|
t.Fatal("bad signature")
|
|
}
|
|
if b[12] != 0x21 {
|
|
t.Fatalf("ver/cmd = 0x%02x, want 0x21", b[12])
|
|
}
|
|
if b[13] != 0x11 {
|
|
t.Fatalf("fam/proto = 0x%02x, want 0x11", b[13])
|
|
}
|
|
}
|
|
|
|
func TestWriteV2IPv6(t *testing.T) {
|
|
src := netip.MustParseAddrPort("[2001:db8::1]:56324")
|
|
dst := netip.MustParseAddrPort("[2001:db8::2]:443")
|
|
|
|
var buf bytes.Buffer
|
|
if err := WriteV2(&buf, src, dst); err != nil {
|
|
t.Fatalf("WriteV2: %v", err)
|
|
}
|
|
|
|
b := buf.Bytes()
|
|
// Signature (12) + ver/cmd (1) + fam (1) + len (2) + 16+16+2+2 = 52
|
|
if len(b) != 52 {
|
|
t.Fatalf("wrote %d bytes, want 52", len(b))
|
|
}
|
|
if b[13] != 0x21 {
|
|
t.Fatalf("fam/proto = 0x%02x, want 0x21", b[13])
|
|
}
|
|
}
|
|
|
|
func TestRoundTripV2IPv4(t *testing.T) {
|
|
src := netip.MustParseAddrPort("203.0.113.50:12345")
|
|
dst := netip.MustParseAddrPort("198.51.100.1:443")
|
|
|
|
reader, writer := pipeWithDeadline(t)
|
|
|
|
go func() {
|
|
_ = WriteV2(writer, src, dst)
|
|
}()
|
|
|
|
hdr, err := Parse(reader, time.Now().Add(5*time.Second))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
|
|
if hdr.Version != 2 {
|
|
t.Fatalf("version = %d, want 2", hdr.Version)
|
|
}
|
|
if hdr.Command != CommandProxy {
|
|
t.Fatalf("command = %d, want CommandProxy", hdr.Command)
|
|
}
|
|
if hdr.SrcAddr != src {
|
|
t.Fatalf("src = %s, want %s", hdr.SrcAddr, src)
|
|
}
|
|
if hdr.DstAddr != dst {
|
|
t.Fatalf("dst = %s, want %s", hdr.DstAddr, dst)
|
|
}
|
|
}
|
|
|
|
func TestRoundTripV2IPv6(t *testing.T) {
|
|
src := netip.MustParseAddrPort("[2001:db8:cafe::1]:40000")
|
|
dst := netip.MustParseAddrPort("[2001:db8:beef::2]:8443")
|
|
|
|
reader, writer := pipeWithDeadline(t)
|
|
|
|
go func() {
|
|
_ = WriteV2(writer, src, dst)
|
|
}()
|
|
|
|
hdr, err := Parse(reader, time.Now().Add(5*time.Second))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
|
|
if hdr.SrcAddr != src {
|
|
t.Fatalf("src = %s, want %s", hdr.SrcAddr, src)
|
|
}
|
|
if hdr.DstAddr != dst {
|
|
t.Fatalf("dst = %s, want %s", hdr.DstAddr, dst)
|
|
}
|
|
}
|
|
|
|
func TestParseGarbageFirstByte(t *testing.T) {
|
|
reader, writer := pipeWithDeadline(t)
|
|
go func() {
|
|
_, _ = writer.Write([]byte{0xFF, 0x00, 0x01})
|
|
}()
|
|
_, err := Parse(reader, time.Now().Add(2*time.Second))
|
|
if err == nil {
|
|
t.Fatal("expected error for garbage first byte")
|
|
}
|
|
}
|