Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 67 additions & 2 deletions cmd/agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import (
"log"
"log/slog"
"os"
"os/signal"
"path/filepath"
"strings"
"os/signal"
"syscall"

"github.com/yourorg/kakuremichi/agent/internal/config"
"github.com/yourorg/kakuremichi/agent/internal/exitnode"
"github.com/yourorg/kakuremichi/agent/internal/proxy"
"github.com/yourorg/kakuremichi/agent/internal/wireguard"
"github.com/yourorg/kakuremichi/agent/internal/ws"
Expand Down Expand Up @@ -55,6 +56,10 @@ func main() {
// Local proxy will be initialized after receiving virtual IP from Control
var localProxy *proxy.LocalProxy

// Exit Node proxies (will be initialized after receiving config)
var exitHTTPProxy *exitnode.LocalHTTPProxy
var exitSOCKS5Proxy *exitnode.LocalSOCKS5Proxy

// Initialize WebSocket client (Control connection)
// Pass public key and private key to the client
wsClient := ws.NewClient(cfg, publicKey, privateKey)
Expand Down Expand Up @@ -144,6 +149,60 @@ func main() {
}
localProxy.UpdateTunnels(tunnels)
}

// Initialize Exit Node proxies (only if WireGuard device is available)
if wgDevice != nil {
// Find first tunnel with proxy enabled and get its gateway IP
var httpGatewayAddr, socksGatewayAddr string
for _, t := range config.Tunnels {
if len(t.GatewayIPs) == 0 {
continue
}
gatewayIP := t.GatewayIPs[0].IP // Use first available gateway
if t.HTTPProxyEnabled && httpGatewayAddr == "" {
httpGatewayAddr = fmt.Sprintf("%s:%d", gatewayIP, config.ProxyConfig.HTTPProxyPort)
}
if t.SOCKSProxyEnabled && socksGatewayAddr == "" {
socksGatewayAddr = fmt.Sprintf("%s:%d", gatewayIP, config.ProxyConfig.SOCKSProxyPort)
}
}

// Start/update HTTP proxy
if httpGatewayAddr != "" {
listenAddr := fmt.Sprintf("%s:%d", config.ProxyConfig.LocalListenAddr, config.ProxyConfig.HTTPProxyPort)
if exitHTTPProxy == nil {
exitHTTPProxy = exitnode.NewLocalHTTPProxy(listenAddr, wgDevice.Net(), httpGatewayAddr)
go func() {
if err := exitHTTPProxy.Start(ctx); err != nil {
slog.Error("Exit HTTP proxy stopped", "error", err)
}
}()
} else {
exitHTTPProxy.UpdateGateway(httpGatewayAddr)
}
} else if exitHTTPProxy != nil {
exitHTTPProxy.Stop()
exitHTTPProxy = nil
}

// Start/update SOCKS5 proxy
if socksGatewayAddr != "" {
listenAddr := fmt.Sprintf("%s:%d", config.ProxyConfig.LocalListenAddr, config.ProxyConfig.SOCKSProxyPort)
if exitSOCKS5Proxy == nil {
exitSOCKS5Proxy = exitnode.NewLocalSOCKS5Proxy(listenAddr, wgDevice.Net(), socksGatewayAddr)
go func() {
if err := exitSOCKS5Proxy.Start(ctx); err != nil {
slog.Error("Exit SOCKS5 proxy stopped", "error", err)
}
}()
} else {
exitSOCKS5Proxy.UpdateGateway(socksGatewayAddr)
}
} else if exitSOCKS5Proxy != nil {
exitSOCKS5Proxy.Stop()
exitSOCKS5Proxy = nil
}
}
})

// Connect to Control server
Expand Down Expand Up @@ -186,14 +245,20 @@ func main() {
cancel()

// Graceful shutdown
if exitHTTPProxy != nil {
exitHTTPProxy.Stop()
}
if exitSOCKS5Proxy != nil {
exitSOCKS5Proxy.Stop()
}
if wgDevice != nil {
wgDevice.Close()
}
if localProxy != nil {
localProxy.Shutdown()
}

fmt.Println("Agent stopped")
fmt.Println("Agent stopped")
}

// loadOrCreateKeys returns (private, public) WireGuard keys.
Expand Down
198 changes: 198 additions & 0 deletions internal/exitnode/http_proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package exitnode

import (
"bufio"
"context"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"sync"

"golang.zx2c4.com/wireguard/tun/netstack"
)

// LocalHTTPProxy is a local HTTP CONNECT proxy that forwards to Gateway via netstack.
// It listens on localhost and routes traffic through WireGuard tunnel.
type LocalHTTPProxy struct {
listenAddr string // e.g., "localhost:8080"
tnet *netstack.Net // WireGuard netstack for outbound connections
gatewayAddr string // Gateway proxy address e.g., "10.1.0.254:8080"
listener net.Listener
mu sync.Mutex
running bool
cancel context.CancelFunc
}

// NewLocalHTTPProxy creates a new local HTTP proxy
func NewLocalHTTPProxy(listenAddr string, tnet *netstack.Net, gatewayAddr string) *LocalHTTPProxy {
return &LocalHTTPProxy{
listenAddr: listenAddr,
tnet: tnet,
gatewayAddr: gatewayAddr,
}
}

// Start starts the local HTTP proxy
func (p *LocalHTTPProxy) Start(ctx context.Context) error {
p.mu.Lock()
if p.running {
p.mu.Unlock()
return nil
}

listener, err := net.Listen("tcp", p.listenAddr)
if err != nil {
p.mu.Unlock()
return fmt.Errorf("failed to listen on %s: %w", p.listenAddr, err)
}

p.listener = listener
p.running = true

childCtx, cancel := context.WithCancel(ctx)
p.cancel = cancel
p.mu.Unlock()

slog.Info("Local HTTP proxy listening", "addr", p.listenAddr, "gateway", p.gatewayAddr)

go func() {
<-childCtx.Done()
listener.Close()
}()

for {
conn, err := listener.Accept()
if err != nil {
select {
case <-childCtx.Done():
return nil
default:
slog.Debug("HTTP proxy accept error", "error", err)
return err
}
}

go p.handleConnection(conn)
}
}

// handleConnection handles a single proxy connection
func (p *LocalHTTPProxy) handleConnection(clientConn net.Conn) {
defer clientConn.Close()

reader := bufio.NewReader(clientConn)
req, err := http.ReadRequest(reader)
if err != nil {
slog.Debug("Failed to read HTTP request", "error", err)
return
}

if req.Method == http.MethodConnect {
p.handleConnect(clientConn, req)
} else {
p.handleHTTP(clientConn, req, reader)
}
}

// handleConnect handles HTTPS tunneling via CONNECT method
func (p *LocalHTTPProxy) handleConnect(clientConn net.Conn, req *http.Request) {
slog.Debug("Local HTTP CONNECT request", "host", req.Host, "via_gateway", p.gatewayAddr)

// Connect to Gateway's proxy via netstack
gatewayConn, err := p.tnet.Dial("tcp", p.gatewayAddr)
if err != nil {
slog.Debug("Failed to connect to gateway proxy", "gateway", p.gatewayAddr, "error", err)
clientConn.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n"))
return
}
defer gatewayConn.Close()

// Forward CONNECT request to Gateway
connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", req.Host, req.Host)
if _, err := gatewayConn.Write([]byte(connectReq)); err != nil {
slog.Debug("Failed to send CONNECT to gateway", "error", err)
clientConn.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n"))
return
}

// Read Gateway's response
gatewayReader := bufio.NewReader(gatewayConn)
resp, err := http.ReadResponse(gatewayReader, req)
if err != nil {
slog.Debug("Failed to read gateway response", "error", err)
clientConn.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n"))
return
}

if resp.StatusCode != http.StatusOK {
slog.Debug("Gateway rejected CONNECT", "status", resp.StatusCode)
clientConn.Write([]byte(fmt.Sprintf("HTTP/1.1 %d %s\r\n\r\n", resp.StatusCode, resp.Status)))
return
}

// Send 200 Connection Established to client
clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))

// Bidirectional relay
var wg sync.WaitGroup
wg.Add(2)

go func() {
defer wg.Done()
io.Copy(gatewayConn, clientConn)
}()

go func() {
defer wg.Done()
io.Copy(clientConn, gatewayConn)
}()

wg.Wait()
}

// handleHTTP handles plain HTTP requests (non-CONNECT)
func (p *LocalHTTPProxy) handleHTTP(clientConn net.Conn, req *http.Request, reader *bufio.Reader) {
slog.Debug("Local HTTP request", "method", req.Method, "url", req.URL.String(), "via_gateway", p.gatewayAddr)

// Connect to Gateway's proxy via netstack
gatewayConn, err := p.tnet.Dial("tcp", p.gatewayAddr)
if err != nil {
slog.Debug("Failed to connect to gateway proxy", "gateway", p.gatewayAddr, "error", err)
clientConn.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n"))
return
}
defer gatewayConn.Close()

// Forward the request to Gateway
if err := req.Write(gatewayConn); err != nil {
slog.Debug("Failed to forward request to gateway", "error", err)
clientConn.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n"))
return
}

// Copy response back to client
io.Copy(clientConn, gatewayConn)
}

// UpdateGateway updates the gateway proxy address
func (p *LocalHTTPProxy) UpdateGateway(gatewayAddr string) {
p.mu.Lock()
defer p.mu.Unlock()
p.gatewayAddr = gatewayAddr
}

// Stop stops the local HTTP proxy
func (p *LocalHTTPProxy) Stop() {
p.mu.Lock()
defer p.mu.Unlock()

if p.cancel != nil {
p.cancel()
}
if p.listener != nil {
p.listener.Close()
}
p.running = false
}
Loading