mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
189 lines
4.5 KiB
Go
189 lines
4.5 KiB
Go
package proxy
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"sync"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestIsConnectionClosedError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
want bool
|
|
}{
|
|
{name: "closed network connection", err: net.ErrClosed, want: true},
|
|
{name: "closed pipe", err: io.ErrClosedPipe, want: true},
|
|
{name: "broken pipe", err: syscall.EPIPE, want: true},
|
|
{name: "connection reset", err: syscall.ECONNRESET, want: true},
|
|
{name: "wrapped connection error", err: fmt.Errorf("copy data: %w", syscall.EPIPE), want: true},
|
|
{name: "other error", err: errors.New("copy failed"), want: false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
require.Equal(t, tt.want, IsConnectionClosedError(tt.err))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunContinuesAfterClosedConnectionError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
listener := newTestListener()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
closedErrCh := make(chan error, 1)
|
|
unexpectedErrCh := make(chan error, 1)
|
|
var dialCount atomic.Int32
|
|
|
|
p := &Proxy{
|
|
Listener: listener,
|
|
RemoteAddr: "remote:80",
|
|
DialContext: func(context.Context, string, string) (net.Conn, error) {
|
|
if dialCount.Add(1) == 1 {
|
|
return readErrorConn{err: syscall.EPIPE}, nil
|
|
}
|
|
|
|
proxyConn, upstreamConn := net.Pipe()
|
|
go func() {
|
|
defer upstreamConn.Close()
|
|
_, _ = upstreamConn.Write([]byte("ok"))
|
|
}()
|
|
return proxyConn, nil
|
|
},
|
|
OnError: func(err error) {
|
|
if IsConnectionClosedError(err) {
|
|
closedErrCh <- err
|
|
return
|
|
}
|
|
unexpectedErrCh <- err
|
|
},
|
|
}
|
|
|
|
runErrCh := make(chan error, 1)
|
|
go func() {
|
|
runErrCh <- p.Run(ctx)
|
|
}()
|
|
|
|
firstConn := listener.connect()
|
|
defer firstConn.Close()
|
|
|
|
select {
|
|
case connErr := <-closedErrCh:
|
|
require.ErrorIs(t, connErr, syscall.EPIPE)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for closed connection error")
|
|
}
|
|
|
|
secondConn := listener.connect()
|
|
defer secondConn.Close()
|
|
require.NoError(t, secondConn.SetReadDeadline(time.Now().Add(time.Second)))
|
|
|
|
got := make([]byte, 2)
|
|
_, err := io.ReadFull(secondConn, got)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "ok", string(got))
|
|
|
|
select {
|
|
case unexpectedErr := <-unexpectedErrCh:
|
|
t.Fatalf("unexpected connection error: %v", unexpectedErr)
|
|
default:
|
|
}
|
|
|
|
cancel()
|
|
select {
|
|
case runErr := <-runErrCh:
|
|
require.NoError(t, runErr)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for proxy to stop")
|
|
}
|
|
}
|
|
|
|
func TestRunReturnsListenerError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
listenerErr := errors.New("listener failed")
|
|
listener := errorListener{err: listenerErr}
|
|
|
|
p := &Proxy{Listener: listener}
|
|
err := p.Run(context.Background())
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "accept local connection")
|
|
require.ErrorIs(t, err, listenerErr)
|
|
}
|
|
|
|
type testListener struct {
|
|
conns chan net.Conn
|
|
closed chan struct{}
|
|
closeOnce sync.Once
|
|
}
|
|
|
|
func newTestListener() *testListener {
|
|
return &testListener{
|
|
conns: make(chan net.Conn),
|
|
closed: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func (l *testListener) connect() net.Conn {
|
|
clientConn, proxyConn := net.Pipe()
|
|
l.conns <- proxyConn
|
|
return clientConn
|
|
}
|
|
|
|
func (l *testListener) Accept() (net.Conn, error) {
|
|
select {
|
|
case conn := <-l.conns:
|
|
return conn, nil
|
|
case <-l.closed:
|
|
return nil, net.ErrClosed
|
|
}
|
|
}
|
|
|
|
func (l *testListener) Close() error {
|
|
l.closeOnce.Do(func() {
|
|
close(l.closed)
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (l *testListener) Addr() net.Addr {
|
|
return &net.TCPAddr{}
|
|
}
|
|
|
|
type errorListener struct {
|
|
err error
|
|
}
|
|
|
|
func (l errorListener) Accept() (net.Conn, error) { return nil, l.err }
|
|
func (errorListener) Close() error { return nil }
|
|
func (errorListener) Addr() net.Addr { return &net.TCPAddr{} }
|
|
|
|
// readErrorConn fails reads immediately so tests can deterministically exercise a proxy copy failure.
|
|
type readErrorConn struct {
|
|
err error
|
|
}
|
|
|
|
func (c readErrorConn) Read([]byte) (int, error) { return 0, c.err }
|
|
func (readErrorConn) Write(p []byte) (int, error) { return len(p), nil }
|
|
func (readErrorConn) Close() error { return nil }
|
|
func (readErrorConn) LocalAddr() net.Addr { return &net.TCPAddr{} }
|
|
func (readErrorConn) RemoteAddr() net.Addr { return &net.TCPAddr{} }
|
|
func (readErrorConn) SetDeadline(time.Time) error { return nil }
|
|
func (readErrorConn) SetReadDeadline(time.Time) error { return nil }
|
|
func (readErrorConn) SetWriteDeadline(time.Time) error { return nil }
|