mirror of
https://github.com/fosrl/gerbil.git
synced 2026-03-22 21:10:00 -05:00
Compare commits
4 Commits
proxy-perf
...
proxy-cont
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eacbb7239 | ||
|
|
d21c09c84f | ||
|
|
28c65b950c | ||
|
|
1643d71905 |
128
proxy/proxy.go
128
proxy/proxy.go
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
"github.com/fosrl/gerbil/logger"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// RouteRecord represents a routing configuration
|
||||
@@ -69,16 +70,12 @@ type SNIProxy struct {
|
||||
|
||||
// Trusted upstream proxies that can send PROXY protocol
|
||||
trustedUpstreams map[string]struct{}
|
||||
|
||||
// Reusable HTTP client for API requests
|
||||
httpClient *http.Client
|
||||
|
||||
// Buffer pool for connection piping
|
||||
bufferPool *sync.Pool
|
||||
}
|
||||
|
||||
type activeTunnel struct {
|
||||
conns []net.Conn
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
count int // protected by activeTunnelsLock
|
||||
}
|
||||
|
||||
// readOnlyConn is a wrapper for io.Reader that implements net.Conn
|
||||
@@ -380,20 +377,6 @@ func NewSNIProxy(port int, remoteConfigURL, publicKey, localProxyAddr string, lo
|
||||
localOverrides: overridesMap,
|
||||
activeTunnels: make(map[string]*activeTunnel),
|
||||
trustedUpstreams: trustedMap,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 10,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
},
|
||||
},
|
||||
bufferPool: &sync.Pool{
|
||||
New: func() interface{} {
|
||||
buf := make([]byte, 32*1024)
|
||||
return &buf
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return proxy, nil
|
||||
@@ -608,37 +591,32 @@ func (p *SNIProxy) handleConnection(clientConn net.Conn) {
|
||||
}
|
||||
}
|
||||
|
||||
// Track this tunnel by SNI
|
||||
// Track this tunnel by SNI using context for cancellation
|
||||
p.activeTunnelsLock.Lock()
|
||||
tunnel, ok := p.activeTunnels[hostname]
|
||||
if !ok {
|
||||
tunnel = &activeTunnel{}
|
||||
ctx, cancel := context.WithCancel(p.ctx)
|
||||
tunnel = &activeTunnel{ctx: ctx, cancel: cancel}
|
||||
p.activeTunnels[hostname] = tunnel
|
||||
}
|
||||
tunnel.conns = append(tunnel.conns, actualClientConn)
|
||||
tunnel.count++
|
||||
tunnelCtx := tunnel.ctx
|
||||
p.activeTunnelsLock.Unlock()
|
||||
|
||||
defer func() {
|
||||
// Remove this conn from active tunnels
|
||||
p.activeTunnelsLock.Lock()
|
||||
if tunnel, ok := p.activeTunnels[hostname]; ok {
|
||||
newConns := make([]net.Conn, 0, len(tunnel.conns))
|
||||
for _, c := range tunnel.conns {
|
||||
if c != actualClientConn {
|
||||
newConns = append(newConns, c)
|
||||
}
|
||||
}
|
||||
if len(newConns) == 0 {
|
||||
tunnel.count--
|
||||
if tunnel.count == 0 {
|
||||
tunnel.cancel()
|
||||
if p.activeTunnels[hostname] == tunnel {
|
||||
delete(p.activeTunnels, hostname)
|
||||
} else {
|
||||
tunnel.conns = newConns
|
||||
}
|
||||
}
|
||||
p.activeTunnelsLock.Unlock()
|
||||
}()
|
||||
|
||||
// Start bidirectional data transfer
|
||||
p.pipe(actualClientConn, targetConn, clientReader)
|
||||
// Start bidirectional data transfer with tunnel context
|
||||
p.pipe(tunnelCtx, actualClientConn, targetConn, clientReader)
|
||||
}
|
||||
|
||||
// getRoute retrieves routing information for a hostname
|
||||
@@ -701,8 +679,9 @@ func (p *SNIProxy) getRoute(hostname, clientAddr string) (*RouteRecord, error) {
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Make HTTP request using reusable client
|
||||
resp, err := p.httpClient.Do(req)
|
||||
// Make HTTP request
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("API request failed: %w", err)
|
||||
}
|
||||
@@ -773,59 +752,36 @@ func (p *SNIProxy) selectStickyEndpoint(clientAddr string, endpoints []string) s
|
||||
}
|
||||
|
||||
// pipe handles bidirectional data transfer between connections
|
||||
func (p *SNIProxy) pipe(clientConn, targetConn net.Conn, clientReader io.Reader) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
func (p *SNIProxy) pipe(ctx context.Context, clientConn, targetConn net.Conn, clientReader io.Reader) {
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
// closeOnce ensures we only close connections once
|
||||
var closeOnce sync.Once
|
||||
closeConns := func() {
|
||||
closeOnce.Do(func() {
|
||||
// Close both connections to unblock any pending reads
|
||||
clientConn.Close()
|
||||
targetConn.Close()
|
||||
})
|
||||
}
|
||||
// Close connections when context cancels to unblock io.Copy operations
|
||||
context.AfterFunc(gCtx, func() {
|
||||
clientConn.Close()
|
||||
targetConn.Close()
|
||||
})
|
||||
|
||||
// Copy data from client to target (using the buffered reader)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer closeConns()
|
||||
|
||||
// Get buffer from pool and return when done
|
||||
bufPtr := p.bufferPool.Get().(*[]byte)
|
||||
defer func() {
|
||||
// Clear buffer before returning to pool to prevent data leakage
|
||||
clear(*bufPtr)
|
||||
p.bufferPool.Put(bufPtr)
|
||||
}()
|
||||
|
||||
_, err := io.CopyBuffer(targetConn, clientReader, *bufPtr)
|
||||
// Copy data from client to target
|
||||
g.Go(func() error {
|
||||
buf := make([]byte, 32*1024)
|
||||
_, err := io.CopyBuffer(targetConn, clientReader, buf)
|
||||
if err != nil && err != io.EOF {
|
||||
logger.Debug("Copy client->target error: %v", err)
|
||||
}
|
||||
}()
|
||||
return err
|
||||
})
|
||||
|
||||
// Copy data from target to client
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer closeConns()
|
||||
|
||||
// Get buffer from pool and return when done
|
||||
bufPtr := p.bufferPool.Get().(*[]byte)
|
||||
defer func() {
|
||||
// Clear buffer before returning to pool to prevent data leakage
|
||||
clear(*bufPtr)
|
||||
p.bufferPool.Put(bufPtr)
|
||||
}()
|
||||
|
||||
_, err := io.CopyBuffer(clientConn, targetConn, *bufPtr)
|
||||
g.Go(func() error {
|
||||
buf := make([]byte, 32*1024)
|
||||
_, err := io.CopyBuffer(clientConn, targetConn, buf)
|
||||
if err != nil && err != io.EOF {
|
||||
logger.Debug("Copy target->client error: %v", err)
|
||||
}
|
||||
}()
|
||||
return err
|
||||
})
|
||||
|
||||
wg.Wait()
|
||||
g.Wait()
|
||||
}
|
||||
|
||||
// GetCacheStats returns cache statistics
|
||||
@@ -861,16 +817,14 @@ func (p *SNIProxy) UpdateLocalSNIs(fullDomains []string) {
|
||||
|
||||
logger.Debug("Updated local SNIs, added %d, removed %d", len(newSNIs), len(removed))
|
||||
|
||||
// Terminate tunnels for removed SNIs
|
||||
// Terminate tunnels for removed SNIs via context cancellation
|
||||
if len(removed) > 0 {
|
||||
p.activeTunnelsLock.Lock()
|
||||
for _, sni := range removed {
|
||||
if tunnels, ok := p.activeTunnels[sni]; ok {
|
||||
for _, conn := range tunnels.conns {
|
||||
conn.Close()
|
||||
}
|
||||
if tunnel, ok := p.activeTunnels[sni]; ok {
|
||||
tunnel.cancel()
|
||||
delete(p.activeTunnels, sni)
|
||||
logger.Debug("Closed tunnels for SNI target change: %s", sni)
|
||||
logger.Debug("Cancelled tunnel context for SNI target change: %s", sni)
|
||||
}
|
||||
}
|
||||
p.activeTunnelsLock.Unlock()
|
||||
|
||||
Reference in New Issue
Block a user