mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
178 lines
4.1 KiB
Go
178 lines
4.1 KiB
Go
package tlsutil
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os/exec"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// subprocessConn wraps a subprocess (nc) as a net.Conn so that Go's HTTP/TLS
|
|
// stack can run transparently over a connection that bypasses VPN/NECP routing
|
|
// captures affecting the host process.
|
|
type subprocessConn struct {
|
|
cmd *exec.Cmd
|
|
stdin io.WriteCloser
|
|
stdout io.ReadCloser
|
|
addr net.Addr
|
|
closeMu sync.Once
|
|
|
|
deadlineMu sync.Mutex
|
|
readTimer *time.Timer
|
|
writeTimer *time.Timer
|
|
}
|
|
|
|
func (c *subprocessConn) Read(p []byte) (int, error) { return c.stdout.Read(p) }
|
|
func (c *subprocessConn) Write(p []byte) (int, error) { return c.stdin.Write(p) }
|
|
|
|
func (c *subprocessConn) Close() error {
|
|
c.closeMu.Do(func() {
|
|
c.stdin.Close()
|
|
c.stdout.Close()
|
|
if c.cmd.Process != nil {
|
|
_ = c.cmd.Process.Kill()
|
|
}
|
|
c.cancelTimers()
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (c *subprocessConn) LocalAddr() net.Addr { return &net.TCPAddr{} }
|
|
func (c *subprocessConn) RemoteAddr() net.Addr { return c.addr }
|
|
|
|
func (c *subprocessConn) SetDeadline(t time.Time) error {
|
|
c.setReadDeadline(t)
|
|
c.setWriteDeadline(t)
|
|
return nil
|
|
}
|
|
|
|
func (c *subprocessConn) SetReadDeadline(t time.Time) error {
|
|
c.setReadDeadline(t)
|
|
return nil
|
|
}
|
|
|
|
func (c *subprocessConn) SetWriteDeadline(t time.Time) error {
|
|
c.setWriteDeadline(t)
|
|
return nil
|
|
}
|
|
|
|
func (c *subprocessConn) setReadDeadline(t time.Time) {
|
|
c.deadlineMu.Lock()
|
|
defer c.deadlineMu.Unlock()
|
|
if c.readTimer != nil {
|
|
c.readTimer.Stop()
|
|
c.readTimer = nil
|
|
}
|
|
if t.IsZero() {
|
|
return
|
|
}
|
|
d := time.Until(t)
|
|
if d <= 0 {
|
|
go c.Close()
|
|
return
|
|
}
|
|
c.readTimer = time.AfterFunc(d, func() { c.Close() })
|
|
}
|
|
|
|
func (c *subprocessConn) setWriteDeadline(t time.Time) {
|
|
c.deadlineMu.Lock()
|
|
defer c.deadlineMu.Unlock()
|
|
if c.writeTimer != nil {
|
|
c.writeTimer.Stop()
|
|
c.writeTimer = nil
|
|
}
|
|
if t.IsZero() {
|
|
return
|
|
}
|
|
d := time.Until(t)
|
|
if d <= 0 {
|
|
go c.Close()
|
|
return
|
|
}
|
|
c.writeTimer = time.AfterFunc(d, func() { c.Close() })
|
|
}
|
|
|
|
func (c *subprocessConn) cancelTimers() {
|
|
c.deadlineMu.Lock()
|
|
defer c.deadlineMu.Unlock()
|
|
if c.readTimer != nil {
|
|
c.readTimer.Stop()
|
|
c.readTimer = nil
|
|
}
|
|
if c.writeTimer != nil {
|
|
c.writeTimer.Stop()
|
|
c.writeTimer = nil
|
|
}
|
|
}
|
|
|
|
// dialViaSubprocess spawns nc to establish a TCP connection to address, then
|
|
// wraps its stdin/stdout as a net.Conn. The subprocess is not subject to the
|
|
// NECP routing policies that capture the host process's direct sockets, so it
|
|
// can reach RFC 1918 addresses on macOS even when a VPN system extension is
|
|
// active.
|
|
func dialViaSubprocess(ctx context.Context, network, address string) (net.Conn, error) {
|
|
host, port, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, "nc", "-w", "30", host, port)
|
|
cmd.Stderr = nil
|
|
|
|
stdin, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("nc stdin pipe: %w", err)
|
|
}
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
stdin.Close()
|
|
return nil, fmt.Errorf("nc stdout pipe: %w", err)
|
|
}
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return nil, fmt.Errorf("nc start for %s: %w", address, err)
|
|
}
|
|
|
|
conn := &subprocessConn{
|
|
cmd: cmd,
|
|
stdin: stdin,
|
|
stdout: stdout,
|
|
addr: &net.TCPAddr{IP: net.ParseIP(host)},
|
|
}
|
|
|
|
// Detect immediate exit (connection refused, no route, etc.).
|
|
// If nc is still running after a brief grace period, the connection
|
|
// succeeded and data can flow through the pipes.
|
|
waitCh := make(chan error, 1)
|
|
go func() { waitCh <- cmd.Wait() }()
|
|
|
|
select {
|
|
case err := <-waitCh:
|
|
stdin.Close()
|
|
stdout.Close()
|
|
return nil, &net.OpError{
|
|
Op: "dial",
|
|
Net: network,
|
|
Addr: &net.TCPAddr{IP: net.ParseIP(host)},
|
|
Err: fmt.Errorf("nc exited immediately for %s: %w", address, err),
|
|
}
|
|
case <-time.After(50 * time.Millisecond):
|
|
log.Debug().Str("addr", address).Msg("subprocess TCP relay established")
|
|
go func() {
|
|
if err := <-waitCh; err != nil && !strings.Contains(err.Error(), "signal: killed") {
|
|
log.Debug().Str("addr", address).Err(err).Msg("subprocess TCP relay exited")
|
|
}
|
|
}()
|
|
return conn, nil
|
|
case <-ctx.Done():
|
|
conn.Close()
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|