Initial implementation of mc-proxy
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>
This commit is contained in:
105
internal/proxy/proxy.go
Normal file
105
internal/proxy/proxy.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Result holds the outcome of a relay operation.
|
||||
type Result struct {
|
||||
ClientBytes int64 // bytes sent from client to backend
|
||||
BackendBytes int64 // bytes sent from backend to client
|
||||
}
|
||||
|
||||
// Relay performs bidirectional byte copying between client and backend.
|
||||
// The peeked bytes (the TLS ClientHello) are written to the backend first.
|
||||
// Relay blocks until both directions are done or ctx is cancelled.
|
||||
func Relay(ctx context.Context, client, backend net.Conn, peeked []byte, idleTimeout time.Duration) (Result, error) {
|
||||
// Forward the buffered ClientHello to the backend.
|
||||
if len(peeked) > 0 {
|
||||
if _, err := backend.Write(peeked); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel context closes both connections to unblock copy goroutines.
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
client.Close()
|
||||
backend.Close()
|
||||
}()
|
||||
|
||||
var (
|
||||
result Result
|
||||
wg sync.WaitGroup
|
||||
errC2B error
|
||||
errB2C error
|
||||
)
|
||||
|
||||
wg.Add(2)
|
||||
|
||||
// client → backend
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
result.ClientBytes, errC2B = copyWithIdleTimeout(backend, client, idleTimeout)
|
||||
// Half-close backend's write side.
|
||||
if hc, ok := backend.(interface{ CloseWrite() error }); ok {
|
||||
hc.CloseWrite()
|
||||
}
|
||||
}()
|
||||
|
||||
// backend → client
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
result.BackendBytes, errB2C = copyWithIdleTimeout(client, backend, idleTimeout)
|
||||
// Half-close client's write side.
|
||||
if hc, ok := client.(interface{ CloseWrite() error }); ok {
|
||||
hc.CloseWrite()
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// If context was cancelled, that's the primary error.
|
||||
if ctx.Err() != nil {
|
||||
return result, ctx.Err()
|
||||
}
|
||||
|
||||
// Return the first meaningful error, if any.
|
||||
if errC2B != nil {
|
||||
return result, errC2B
|
||||
}
|
||||
return result, errB2C
|
||||
}
|
||||
|
||||
// copyWithIdleTimeout copies from src to dst, resetting the idle deadline
|
||||
// on each successful read.
|
||||
func copyWithIdleTimeout(dst, src net.Conn, idleTimeout time.Duration) (int64, error) {
|
||||
buf := make([]byte, 32*1024)
|
||||
var total int64
|
||||
|
||||
for {
|
||||
src.SetReadDeadline(time.Now().Add(idleTimeout))
|
||||
nr, readErr := src.Read(buf)
|
||||
if nr > 0 {
|
||||
dst.SetWriteDeadline(time.Now().Add(idleTimeout))
|
||||
nw, writeErr := dst.Write(buf[:nr])
|
||||
total += int64(nw)
|
||||
if writeErr != nil {
|
||||
return total, writeErr
|
||||
}
|
||||
}
|
||||
if readErr != nil {
|
||||
if readErr == io.EOF {
|
||||
return total, nil
|
||||
}
|
||||
return total, readErr
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user