mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-07-18 08:49:54 +00:00
487 lines
14 KiB
Go
487 lines
14 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"io"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/pires/go-proxyproto"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNewProxyProtocolListenerDisabled(t *testing.T) {
|
|
listener, _ := newProxyProtocolTestConnection(t, "192.0.2.10")
|
|
|
|
wrapped, err := newProxyProtocolListener(listener, nil)
|
|
|
|
require.NoError(t, err)
|
|
assert.Same(t, listener, wrapped)
|
|
}
|
|
|
|
func TestNewProxyProtocolListenerRequiresTCP(t *testing.T) {
|
|
serverConn, clientConn := net.Pipe()
|
|
t.Cleanup(func() {
|
|
_ = serverConn.Close()
|
|
_ = clientConn.Close()
|
|
})
|
|
listener := &singleConnListener{
|
|
conn: serverConn,
|
|
addr: &net.UnixAddr{Name: "/tmp/pocket-id.sock", Net: "unix"},
|
|
closed: make(chan struct{}),
|
|
}
|
|
|
|
_, err := newProxyProtocolListener(listener, []string{"127.0.0.1"})
|
|
|
|
require.Error(t, err)
|
|
assert.ErrorContains(t, err, "PROXY protocol requires a TCP listener")
|
|
}
|
|
|
|
func TestNewProxyProtocolListenerUsesStrictTrustedProxyPolicy(t *testing.T) {
|
|
listener, _ := newProxyProtocolTestConnection(t, "192.0.2.10")
|
|
|
|
wrapped, err := newProxyProtocolListener(listener, []string{"192.0.2.0/24"})
|
|
require.NoError(t, err)
|
|
proxyListener := requireProxyProtocolListener(t, wrapped)
|
|
assert.Equal(t, proxyProtocolReadHeaderTimeout, proxyListener.ReadHeaderTimeout)
|
|
|
|
policy, err := proxyListener.ConnPolicy(proxyproto.ConnPolicyOptions{
|
|
Upstream: &net.TCPAddr{IP: net.ParseIP("192.0.2.10"), Port: 12345},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, proxyproto.REQUIRE, policy)
|
|
|
|
_, err = proxyListener.ConnPolicy(proxyproto.ConnPolicyOptions{
|
|
Upstream: &net.TCPAddr{IP: net.ParseIP("198.51.100.10"), Port: 12345},
|
|
})
|
|
require.ErrorIs(t, err, proxyproto.ErrInvalidUpstream)
|
|
|
|
policy, err = proxyListener.ConnPolicy(proxyproto.ConnPolicyOptions{
|
|
Upstream: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12345},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, proxyproto.SKIP, policy)
|
|
}
|
|
|
|
func TestProxyProtocolListenerAllowsLoopbackWithoutHeader(t *testing.T) {
|
|
listener, clientConn := newProxyProtocolTestConnection(t, "127.0.0.1")
|
|
wrapped, err := newProxyProtocolListener(listener, []string{"192.0.2.0/24"})
|
|
require.NoError(t, err)
|
|
|
|
serverConn, err := wrapped.Accept()
|
|
require.NoError(t, err)
|
|
|
|
writeErr := make(chan error, 1)
|
|
go func() {
|
|
_, err := clientConn.Write([]byte("healthcheck"))
|
|
writeErr <- err
|
|
}()
|
|
|
|
payload := make([]byte, len("healthcheck"))
|
|
_, err = io.ReadFull(serverConn, payload)
|
|
require.NoError(t, err)
|
|
require.NoError(t, <-writeErr)
|
|
assert.Equal(t, "healthcheck", string(payload))
|
|
assert.Equal(t, "127.0.0.1:12345", serverConn.RemoteAddr().String())
|
|
}
|
|
|
|
func TestProxyProtocolListenerAcceptsHeaderFromTrustedLoopback(t *testing.T) {
|
|
listener, clientConn := newProxyProtocolTestConnection(t, "::1")
|
|
wrapped, err := newProxyProtocolListener(listener, []string{"::1"})
|
|
require.NoError(t, err)
|
|
|
|
serverConn, err := wrapped.Accept()
|
|
require.NoError(t, err)
|
|
|
|
source := &net.TCPAddr{IP: net.ParseIP("2001:db8::15"), Port: 42300}
|
|
dest := &net.TCPAddr{IP: net.ParseIP("::1"), Port: 1411}
|
|
writeErr := make(chan error, 1)
|
|
go func() {
|
|
header := proxyproto.HeaderProxyFromAddrs(2, source, dest)
|
|
if _, err := header.WriteTo(clientConn); err != nil {
|
|
writeErr <- err
|
|
return
|
|
}
|
|
_, err := clientConn.Write([]byte("payload"))
|
|
writeErr <- err
|
|
}()
|
|
|
|
payload := make([]byte, len("payload"))
|
|
_, err = io.ReadFull(serverConn, payload)
|
|
require.NoError(t, err)
|
|
require.NoError(t, <-writeErr)
|
|
assert.Equal(t, "payload", string(payload))
|
|
assert.Equal(t, source.String(), serverConn.RemoteAddr().String())
|
|
}
|
|
|
|
func TestProxyProtocolListenerAllowsTrustedLoopbackWithoutHeader(t *testing.T) {
|
|
listener, clientConn := newProxyProtocolTestConnection(t, "::1")
|
|
wrapped, err := newProxyProtocolListener(listener, []string{"::1"})
|
|
require.NoError(t, err)
|
|
|
|
serverConn, err := wrapped.Accept()
|
|
require.NoError(t, err)
|
|
|
|
writeErr := make(chan error, 1)
|
|
go func() {
|
|
_, err := clientConn.Write([]byte("healthcheck"))
|
|
writeErr <- err
|
|
}()
|
|
|
|
payload := make([]byte, len("healthcheck"))
|
|
_, err = io.ReadFull(serverConn, payload)
|
|
require.NoError(t, err)
|
|
require.NoError(t, <-writeErr)
|
|
assert.Equal(t, "healthcheck", string(payload))
|
|
assert.Equal(t, "[::1]:12345", serverConn.RemoteAddr().String())
|
|
}
|
|
|
|
func TestProxyProtocolListenerDropsUntrustedPeerAndContinues(t *testing.T) {
|
|
untrustedServerConn, untrustedClientConn := newAddressedPipe("198.51.100.10")
|
|
trustedServerConn, trustedClientConn := newAddressedPipe("192.0.2.10")
|
|
listener := &queuedConnListener{
|
|
conns: []net.Conn{untrustedServerConn, trustedServerConn},
|
|
addr: trustedServerConn.LocalAddr(),
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = listener.Close()
|
|
_ = untrustedServerConn.Close()
|
|
_ = trustedServerConn.Close()
|
|
_ = untrustedClientConn.Close()
|
|
_ = trustedClientConn.Close()
|
|
})
|
|
|
|
wrapped, err := newProxyProtocolListener(listener, []string{"192.0.2.0/24"})
|
|
require.NoError(t, err)
|
|
serverConn, err := wrapped.Accept()
|
|
require.NoError(t, err)
|
|
|
|
source := &net.TCPAddr{IP: net.ParseIP("203.0.113.15"), Port: 42300}
|
|
dest := &net.TCPAddr{IP: net.ParseIP("192.0.2.20"), Port: 443}
|
|
writeErr := make(chan error, 1)
|
|
go func() {
|
|
header := proxyproto.HeaderProxyFromAddrs(1, source, dest)
|
|
if _, err := header.WriteTo(trustedClientConn); err != nil {
|
|
writeErr <- err
|
|
return
|
|
}
|
|
_, err := trustedClientConn.Write([]byte("payload"))
|
|
writeErr <- err
|
|
}()
|
|
|
|
payload := make([]byte, len("payload"))
|
|
_, err = io.ReadFull(serverConn, payload)
|
|
require.NoError(t, err)
|
|
require.NoError(t, <-writeErr)
|
|
assert.Equal(t, source.String(), serverConn.RemoteAddr().String())
|
|
|
|
_, err = untrustedClientConn.Write([]byte("payload"))
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestProxyProtocolListenerExtractsClientAddress(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
version byte
|
|
source *net.TCPAddr
|
|
dest *net.TCPAddr
|
|
}{
|
|
{
|
|
name: "version 1 IPv4",
|
|
version: 1,
|
|
source: &net.TCPAddr{IP: net.ParseIP("203.0.113.15"), Port: 42300},
|
|
dest: &net.TCPAddr{IP: net.ParseIP("192.0.2.20"), Port: 443},
|
|
},
|
|
{
|
|
name: "version 2 IPv6",
|
|
version: 2,
|
|
source: &net.TCPAddr{IP: net.ParseIP("2001:db8::15"), Port: 42300},
|
|
dest: &net.TCPAddr{IP: net.ParseIP("2001:db8::20"), Port: 443},
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
listener, clientConn := newProxyProtocolTestConnection(t, "192.0.2.10")
|
|
wrapped, err := newProxyProtocolListener(listener, []string{"192.0.2.0/24"})
|
|
require.NoError(t, err)
|
|
|
|
serverConn, err := wrapped.Accept()
|
|
require.NoError(t, err)
|
|
|
|
writeErr := make(chan error, 1)
|
|
go func() {
|
|
header := proxyproto.HeaderProxyFromAddrs(testCase.version, testCase.source, testCase.dest)
|
|
if _, err := header.WriteTo(clientConn); err != nil {
|
|
writeErr <- err
|
|
return
|
|
}
|
|
_, err := clientConn.Write([]byte("payload"))
|
|
writeErr <- err
|
|
}()
|
|
|
|
payload := make([]byte, len("payload"))
|
|
_, err = io.ReadFull(serverConn, payload)
|
|
require.NoError(t, err)
|
|
require.NoError(t, <-writeErr)
|
|
assert.Equal(t, "payload", string(payload))
|
|
assert.Equal(t, testCase.source.String(), serverConn.RemoteAddr().String())
|
|
assert.Equal(t, testCase.dest.String(), serverConn.LocalAddr().String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProxyProtocolListenerRejectsMissingHeader(t *testing.T) {
|
|
listener, clientConn := newProxyProtocolTestConnection(t, "192.0.2.10")
|
|
wrapped, err := newProxyProtocolListener(listener, []string{"192.0.2.0/24"})
|
|
require.NoError(t, err)
|
|
|
|
serverConn, err := wrapped.Accept()
|
|
require.NoError(t, err)
|
|
|
|
go func() {
|
|
_, _ = clientConn.Write([]byte("GET /healthz HTTP/1.1\r\n"))
|
|
}()
|
|
|
|
_, err = serverConn.Read(make([]byte, 1))
|
|
require.ErrorIs(t, err, proxyproto.ErrNoProxyProtocol)
|
|
}
|
|
|
|
func TestProxyProtocolListenerSetsGinClientIP(t *testing.T) {
|
|
listener, clientConn := newProxyProtocolTestConnection(t, "192.0.2.10")
|
|
wrapped, err := newProxyProtocolListener(listener, []string{"192.0.2.0/24"})
|
|
require.NoError(t, err)
|
|
|
|
engine := gin.New()
|
|
require.NoError(t, engine.SetTrustedProxies(nil))
|
|
clientIP := make(chan string, 1)
|
|
engine.GET("/client-ip", func(c *gin.Context) {
|
|
clientIP <- c.ClientIP()
|
|
c.Status(http.StatusNoContent)
|
|
})
|
|
server := &http.Server{
|
|
Handler: engine,
|
|
ReadHeaderTimeout: time.Second,
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = server.Close()
|
|
})
|
|
go func() {
|
|
_ = server.Serve(wrapped)
|
|
}()
|
|
|
|
source := &net.TCPAddr{IP: net.ParseIP("203.0.113.15"), Port: 42300}
|
|
dest := &net.TCPAddr{IP: net.ParseIP("192.0.2.20"), Port: 443}
|
|
writeErr := make(chan error, 1)
|
|
go func() {
|
|
header := proxyproto.HeaderProxyFromAddrs(1, source, dest)
|
|
if _, err := header.WriteTo(clientConn); err != nil {
|
|
writeErr <- err
|
|
return
|
|
}
|
|
_, err := clientConn.Write([]byte("GET /client-ip HTTP/1.1\r\nHost: pocket-id.example\r\nConnection: close\r\n\r\n"))
|
|
writeErr <- err
|
|
}()
|
|
|
|
select {
|
|
case actualClientIP := <-clientIP:
|
|
assert.Equal(t, "203.0.113.15", actualClientIP)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for Gin to handle the proxied request")
|
|
}
|
|
require.NoError(t, <-writeErr)
|
|
}
|
|
|
|
func TestProxyProtocolHeaderIsReadBeforeTLS(t *testing.T) {
|
|
listener, clientConn := newProxyProtocolTestConnection(t, "192.0.2.10")
|
|
proxyListener, err := newProxyProtocolListener(listener, []string{"192.0.2.0/24"})
|
|
require.NoError(t, err)
|
|
|
|
serverTLSConfig, clientTLSConfig := newProxyProtocolTestTLSConfigs(t)
|
|
tlsListener := tls.NewListener(proxyListener, serverTLSConfig)
|
|
serverConn, err := tlsListener.Accept()
|
|
require.NoError(t, err)
|
|
|
|
source := &net.TCPAddr{IP: net.ParseIP("203.0.113.15"), Port: 42300}
|
|
dest := &net.TCPAddr{IP: net.ParseIP("192.0.2.20"), Port: 443}
|
|
ctx := t.Context()
|
|
clientErr := make(chan error, 1)
|
|
go func() {
|
|
header := proxyproto.HeaderProxyFromAddrs(2, source, dest)
|
|
if _, err := header.WriteTo(clientConn); err != nil {
|
|
clientErr <- err
|
|
return
|
|
}
|
|
|
|
tlsClient := tls.Client(clientConn, clientTLSConfig)
|
|
if err := tlsClient.HandshakeContext(ctx); err != nil {
|
|
clientErr <- err
|
|
return
|
|
}
|
|
_, err := tlsClient.Write([]byte("payload"))
|
|
clientErr <- err
|
|
}()
|
|
|
|
tlsServerConn, ok := serverConn.(*tls.Conn)
|
|
require.True(t, ok)
|
|
require.NoError(t, tlsServerConn.HandshakeContext(ctx))
|
|
payload := make([]byte, len("payload"))
|
|
_, err = io.ReadFull(serverConn, payload)
|
|
require.NoError(t, err)
|
|
require.NoError(t, <-clientErr)
|
|
assert.Equal(t, "payload", string(payload))
|
|
assert.Equal(t, source.String(), serverConn.RemoteAddr().String())
|
|
}
|
|
|
|
func requireProxyProtocolListener(t *testing.T, listener net.Listener) *proxyproto.Listener {
|
|
t.Helper()
|
|
|
|
proxyListener, ok := listener.(*proxyproto.Listener)
|
|
require.True(t, ok)
|
|
return proxyListener
|
|
}
|
|
|
|
func newProxyProtocolTestConnection(t *testing.T, upstreamIP string) (*singleConnListener, net.Conn) {
|
|
t.Helper()
|
|
|
|
serverConn, clientConn := newAddressedPipe(upstreamIP)
|
|
listener := &singleConnListener{
|
|
conn: serverConn,
|
|
addr: serverConn.LocalAddr(),
|
|
closed: make(chan struct{}),
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
_ = listener.Close()
|
|
_ = clientConn.Close()
|
|
})
|
|
return listener, clientConn
|
|
}
|
|
|
|
func newAddressedPipe(upstreamIP string) (net.Conn, net.Conn) {
|
|
serverConn, clientConn := net.Pipe()
|
|
return &addressedConn{
|
|
Conn: serverConn,
|
|
localAddr: &net.TCPAddr{IP: net.ParseIP("192.0.2.20"), Port: 1411},
|
|
remoteAddr: &net.TCPAddr{IP: net.ParseIP(upstreamIP), Port: 12345},
|
|
}, clientConn
|
|
}
|
|
|
|
func newProxyProtocolTestTLSConfigs(t *testing.T) (*tls.Config, *tls.Config) {
|
|
t.Helper()
|
|
|
|
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
now := time.Now()
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
DNSNames: []string{"localhost"},
|
|
NotBefore: now.Add(-time.Minute),
|
|
NotAfter: now.Add(time.Hour),
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
}
|
|
certificateDER, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
|
|
require.NoError(t, err)
|
|
certificate, err := x509.ParseCertificate(certificateDER)
|
|
require.NoError(t, err)
|
|
|
|
roots := x509.NewCertPool()
|
|
roots.AddCert(certificate)
|
|
|
|
serverConfig := &tls.Config{
|
|
Certificates: []tls.Certificate{{
|
|
Certificate: [][]byte{certificateDER},
|
|
PrivateKey: privateKey,
|
|
}},
|
|
MinVersion: tls.VersionTLS13,
|
|
}
|
|
clientConfig := &tls.Config{
|
|
RootCAs: roots,
|
|
ServerName: "localhost",
|
|
MinVersion: tls.VersionTLS13,
|
|
}
|
|
return serverConfig, clientConfig
|
|
}
|
|
|
|
type singleConnListener struct {
|
|
conn net.Conn
|
|
addr net.Addr
|
|
accepted bool
|
|
closed chan struct{}
|
|
closeOnce sync.Once
|
|
}
|
|
|
|
func (l *singleConnListener) Accept() (net.Conn, error) {
|
|
if l.accepted {
|
|
<-l.closed
|
|
return nil, net.ErrClosed
|
|
}
|
|
l.accepted = true
|
|
return l.conn, nil
|
|
}
|
|
|
|
func (l *singleConnListener) Close() error {
|
|
var closeErr error
|
|
l.closeOnce.Do(func() {
|
|
close(l.closed)
|
|
closeErr = l.conn.Close()
|
|
})
|
|
return closeErr
|
|
}
|
|
|
|
func (l *singleConnListener) Addr() net.Addr {
|
|
return l.addr
|
|
}
|
|
|
|
type addressedConn struct {
|
|
net.Conn
|
|
localAddr net.Addr
|
|
remoteAddr net.Addr
|
|
}
|
|
|
|
func (c *addressedConn) LocalAddr() net.Addr {
|
|
return c.localAddr
|
|
}
|
|
|
|
func (c *addressedConn) RemoteAddr() net.Addr {
|
|
return c.remoteAddr
|
|
}
|
|
|
|
type queuedConnListener struct {
|
|
conns []net.Conn
|
|
addr net.Addr
|
|
}
|
|
|
|
func (l *queuedConnListener) Accept() (net.Conn, error) {
|
|
if len(l.conns) == 0 {
|
|
return nil, net.ErrClosed
|
|
}
|
|
|
|
conn := l.conns[0]
|
|
l.conns = l.conns[1:]
|
|
return conn, nil
|
|
}
|
|
|
|
func (l *queuedConnListener) Close() error {
|
|
for _, conn := range l.conns {
|
|
_ = conn.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (l *queuedConnListener) Addr() net.Addr {
|
|
return l.addr
|
|
}
|