package proxy import ( "context" "fmt" "io" "net" "os" "sync" "time" ) // Proxy proxies local connections to a remote TCP address optionally using a custom dialer. type Proxy struct { Listener net.Listener RemoteAddr string DialContext func(ctx context.Context, network, address string) (net.Conn, error) OnError func(error) activeConns sync.WaitGroup } // deadliner is an interface for listeners that support setting deadlines. type deadliner interface { SetDeadline(t time.Time) error } // halfCloser is an interface for connections that support half-close. type halfCloser interface { CloseWrite() error } // Run starts the proxy and runs until the context is canceled. func (p *Proxy) Run(ctx context.Context) { if p.DialContext == nil { p.DialContext = (&net.Dialer{}).DialContext } defer p.Listener.Close() // Handle incoming connections until context is canceled. Loop: for { select { case <-ctx.Done(): break Loop default: } // Set a deadline on the listener if supported to check context periodically. if dl, ok := p.Listener.(deadliner); ok { dl.SetDeadline(time.Now().Add(1 * time.Second)) } conn, err := p.Listener.Accept() if err != nil { if os.IsTimeout(err) { // Just a timeout, continue to check context and accept again. continue } select { case <-ctx.Done(): break Loop default: if p.OnError != nil { p.OnError(fmt.Errorf("accept local connection: %w", err)) } continue } } p.activeConns.Add(1) go p.handleConnection(ctx, conn) } // Wait for all connections to finish. p.activeConns.Wait() } func (p *Proxy) handleConnection(ctx context.Context, localConn net.Conn) { defer p.activeConns.Done() defer localConn.Close() // Use a separate context with timeout for dialing the remote address. dialCtx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() remoteConn, err := p.DialContext(dialCtx, "tcp", p.RemoteAddr) if err != nil { if p.OnError != nil { p.OnError(fmt.Errorf("connect remote address '%s': %w", p.RemoteAddr, err)) } return } defer remoteConn.Close() // Bidirectional copy with proper half-close handling. done := make(chan error, 2) go func() { _, err := io.Copy(remoteConn, localConn) // Close write half of remote connection if supported. if hc, ok := remoteConn.(halfCloser); ok { hc.CloseWrite() } done <- err }() go func() { _, err := io.Copy(localConn, remoteConn) // Close write half of local connection if supported. if hc, ok := localConn.(halfCloser); ok { hc.CloseWrite() } done <- err }() // Wait for both copies to complete or context cancel. for range 2 { select { case <-ctx.Done(): // Close connections to abort ongoing copies. localConn.Close() remoteConn.Close() return case err = <-done: if err != nil && p.OnError != nil { p.OnError(fmt.Errorf("data copy: %w", err)) } } } }