fix(dialer): correct inverted setMark on FreeBSD/OpenBSD

Fix setMark early-return conditions that were inverted relative to
Linux: on FreeBSD (SO_USER_COOKIE) and OpenBSD (SO_RTABLE), a
non-zero mark was short-circuited instead of applied, disabling
socket marking entirely.

Also fix GetClientIP to trim leading whitespace from X-Forwarded-For
entries (per RFC 7239), and fix Body.Read to subtract len(b) instead
of n from recordSize when a single read exceeds the remaining quota.

Add unit tests for internal/net/ (addr, dialer, http, ip, net, pipe,
transport, resolve, proxyproto, udp).
This commit is contained in:
ginuerzh
2026-05-21 20:58:48 +08:00
parent d189dc7967
commit ef58909fb7
16 changed files with 2442 additions and 4 deletions
+410
View File
@@ -0,0 +1,410 @@
package net
import (
"context"
"errors"
"io"
"net"
"testing"
"time"
)
// pipeConn is a fully controllable in-memory connection for testing Pipe.
// It supports proper half-close semantics: CloseRead only stops reads,
// CloseWrite only stops writes, and Close stops both.
type pipeConn struct {
readBuf []byte
readPos int
writeBuf []byte
readClosed bool
writeClosed bool
closedCh chan struct{}
readDead time.Time
writeDead time.Time
readErr error // error to return on next Read
eofAfter int // return io.EOF after this many bytes total read (-1 = never)
bytesRead int
}
func newPipeConn(data []byte) *pipeConn {
return &pipeConn{
readBuf: data,
closedCh: make(chan struct{}),
}
}
func (pc *pipeConn) Read(p []byte) (int, error) {
select {
case <-pc.closedCh:
return 0, net.ErrClosed
default:
}
if pc.readClosed {
return 0, net.ErrClosed
}
if pc.readErr != nil {
err := pc.readErr
pc.readErr = nil
return 0, err
}
if pc.readPos >= len(pc.readBuf) {
// eofAfter=-1 means sleep briefly then return (0, nil) to simulate
// a connection that's still open but has no data yet.
// This avoids busy-looping while still preventing EOF.
if pc.eofAfter < 0 {
time.Sleep(10 * time.Millisecond)
return 0, nil // no data, no error, no EOF
}
return 0, io.EOF
}
n := copy(p, pc.readBuf[pc.readPos:])
pc.readPos += n
pc.bytesRead += n
return n, nil
}
func (pc *pipeConn) Write(p []byte) (int, error) {
select {
case <-pc.closedCh:
return 0, net.ErrClosed
default:
}
if pc.writeClosed {
return 0, net.ErrClosed
}
pc.writeBuf = append(pc.writeBuf, p...)
return len(p), nil
}
func (pc *pipeConn) Close() error {
select {
case <-pc.closedCh:
default:
close(pc.closedCh)
}
return nil
}
func (pc *pipeConn) CloseRead() error {
pc.readClosed = true
return nil
}
func (pc *pipeConn) CloseWrite() error {
pc.writeClosed = true
return nil
}
func (pc *pipeConn) SetReadDeadline(t time.Time) error {
pc.readDead = t
return nil
}
func (pc *pipeConn) SetWriteDeadline(t time.Time) error {
pc.writeDead = t
return nil
}
func TestPipe_NormalTransfer(t *testing.T) {
ctx := context.Background()
c1 := newPipeConn([]byte("hello from c1"))
c2 := newPipeConn([]byte("hello from c2"))
err := Pipe(ctx, c1, c2)
if err != nil {
t.Errorf("expected nil error, got %v", err)
}
// c2's writeBuf has data from c1
if string(c2.writeBuf) != "hello from c1" {
t.Errorf("c2.writeBuf = %q, want %q", string(c2.writeBuf), "hello from c1")
}
// c1's writeBuf has data from c2
if string(c1.writeBuf) != "hello from c2" {
t.Errorf("c1.writeBuf = %q, want %q", string(c1.writeBuf), "hello from c2")
}
}
func TestPipe_ImmediateEOF(t *testing.T) {
ctx := context.Background()
// Empty read buffers produce immediate EOF, simulating closed connections
c1 := newPipeConn(nil)
c2 := newPipeConn(nil)
err := Pipe(ctx, c1, c2)
if err != nil {
t.Errorf("expected nil error (EOF is not an error from Pipe), got %v", err)
}
}
func TestPipe_ContextCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
// These never produce EOF and never error, so Pipe will run until canceled
c1 := newPipeConn(nil)
c2 := newPipeConn(nil)
c1.eofAfter = -1 // disable EOF
c2.eofAfter = -1
// Start cancellation in a goroutine
go func() {
time.Sleep(50 * time.Millisecond)
cancel()
}()
err := Pipe(ctx, c1, c2)
if err == nil {
t.Error("expected context error, got nil")
}
}
func TestPipe_ContextAlreadyCanceled(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
c1 := newPipeConn(nil)
c1.eofAfter = -1
c2 := newPipeConn(nil)
c2.eofAfter = -1
err := Pipe(ctx, c1, c2)
if err == nil {
t.Error("expected context error, got nil")
}
}
func TestPipe_ReadError(t *testing.T) {
ctx := context.Background()
readErr := errors.New("read failure")
c1 := newPipeConn(nil)
c1.readErr = readErr
// c2 also has a read error so both goroutines terminate
otherErr := errors.New("other error")
c2 := newPipeConn(nil)
c2.readErr = otherErr
err := Pipe(ctx, c1, c2)
if err == nil {
t.Error("expected error, got nil")
}
}
func TestPipe_WriteError(t *testing.T) {
ctx := context.Background()
c1 := newPipeConn([]byte("data"))
// c2 has its write side pre-closed
c2 := newPipeConn(nil)
c2.writeClosed = true
err := Pipe(ctx, c1, c2)
if err == nil {
t.Error("expected write error, got nil")
}
}
func Test_readDeadliner(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
r := &mockReadWriteCloser{data: []byte("testdata")}
rd := &readDeadliner{Reader: r, ctx: ctx}
// Normal read
buf := make([]byte, 8)
n, err := rd.Read(buf)
if err != nil && err != io.EOF {
t.Fatal(err)
}
if n != 8 || string(buf[:n]) != "testdata" {
t.Errorf("expected 'testdata', got %q", string(buf[:n]))
}
// Context canceled
cancel()
_, err = rd.Read(buf)
if err == nil {
t.Error("expected context error, got nil")
}
}
func Test_halfClose(t *testing.T) {
// pipeConn supports CloseRead and CloseWrite (success).
// CloseWrite returns nil (successful half-close), which sets a read deadline on dst.
src := newPipeConn(nil)
dst := newPipeConn(nil)
halfClose(src, dst)
if !src.readClosed {
t.Error("src should have CloseRead called")
}
if !dst.writeClosed {
t.Error("dst should have CloseWrite called (success path)")
}
if dst.readDead.IsZero() {
t.Error("dst should have read deadline set after successful CloseWrite")
}
}
func Test_halfClose_MockReadWriteCloser(t *testing.T) {
// mockReadWriteCloser has CloseRead, but CloseWrite returns ErrUnsupported -> dst.Close()
src2 := &mockReadWriteCloser{}
dst2 := &mockReadWriteCloser{}
halfClose(src2, dst2)
if !src2.readClosed {
t.Error("src should have CloseRead called")
}
if !dst2.closed {
t.Error("dst should be fully closed when CloseWrite is ErrUnsupported")
}
}
func Test_forceClose(t *testing.T) {
c1 := newPipeConn(nil)
c2 := newPipeConn(nil)
forceClose(c1, c2, nil)
// Check that closedCh is closed
select {
case <-c1.closedCh:
default:
t.Error("c1 should be closed")
}
select {
case <-c2.closedCh:
default:
t.Error("c2 should be closed")
}
}
func Test_pipeHalf(t *testing.T) {
ctx := context.Background()
src := newPipeConn([]byte("hello world"))
dst := newPipeConn(nil)
err := pipeHalf(ctx, src, dst)
if err != nil {
t.Fatal(err)
}
if string(dst.writeBuf) != "hello world" {
t.Errorf("dst.writeBuf = %q, want %q", string(dst.writeBuf), "hello world")
}
}
func Test_pipeHalf_ReadError(t *testing.T) {
ctx := context.Background()
readErr := errors.New("read failed")
src := newPipeConn(nil)
src.readErr = readErr
dst := newPipeConn(nil)
err := pipeHalf(ctx, src, dst)
if err == nil {
t.Error("expected error, got nil")
}
}
func Test_pipeHalf_WriteError(t *testing.T) {
ctx := context.Background()
src := newPipeConn([]byte("data"))
dst := newPipeConn(nil)
dst.writeClosed = true
err := pipeHalf(ctx, src, dst)
if err == nil {
t.Error("expected error, got nil")
}
}
func Test_pipeHalf_ContextCanceled(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
src := newPipeConn([]byte("data"))
src.eofAfter = -1
dst := newPipeConn(nil)
err := pipeHalf(ctx, src, dst)
if err == nil {
t.Error("expected context error, got nil")
}
}
func TestPipe_CtxDoneWithError(t *testing.T) {
// Test the ctx.Done() path where firstErr is non-nil
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
c1 := newPipeConn(nil)
c1.readErr = errors.New("error before cancel")
c1.eofAfter = -1
c2 := newPipeConn(nil)
c2.eofAfter = -1
done := make(chan error, 1)
go func() {
done <- Pipe(ctx, c1, c2)
}()
time.Sleep(20 * time.Millisecond)
cancel()
err := <-done
if err == nil {
t.Error("expected error, got nil")
}
}
func TestPipe_CtxDoneWithoutError(t *testing.T) {
// Standard ctx.Done path with no prior error → returns ctx.Err()
ctx, cancel := context.WithCancel(context.Background())
c1 := newPipeConn(nil)
c1.eofAfter = -1
c2 := newPipeConn(nil)
c2.eofAfter = -1
go func() {
time.Sleep(30 * time.Millisecond)
cancel()
}()
err := Pipe(ctx, c1, c2)
if err == nil {
t.Error("expected context error, got nil")
}
}
func TestTransport_ReturnsError(t *testing.T) {
// Both directions must return errors since Transport reads one from errc
readErr := errors.New("not EOF error")
rw1 := &mockReadWriter{readErr: readErr}
rw2 := &mockReadWriter{readErr: errors.New("other error")}
err := Transport(rw1, rw2)
if err == nil {
t.Error("expected error, got nil")
}
}
func Test_halfClose_NoCloseWriteInterface(t *testing.T) {
// Test the path where dst doesn't implement CloseWrite
type noCloseWrite struct {
*mockReadWriteCloser
}
src := &mockReadWriteCloser{}
// Create a type that doesn't have CloseWrite (use a type without the method)
// Actually, we need a type that implements io.ReadWriteCloser but NOT CloseWrite
dst := &noCloseWriteCloser{}
halfClose(src, dst)
if !dst.closed {
// halfClose checks cw, ok := dst.(xio.CloseWrite) → false
// falls through to dst.Close()
t.Error("dst should be closed when CloseWrite is not available")
}
}
type noCloseWriteCloser struct {
closed bool
}
func (c *noCloseWriteCloser) Read(p []byte) (int, error) { return 0, nil }
func (c *noCloseWriteCloser) Write(p []byte) (int, error) { return len(p), nil }
func (c *noCloseWriteCloser) Close() error { c.closed = true; return nil }