Skip to content
Open
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
68 changes: 60 additions & 8 deletions cmd/gateway/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"syscall"

"github.com/yourorg/kakuremichi/gateway/internal/config"
"github.com/yourorg/kakuremichi/gateway/internal/exitnode"
"github.com/yourorg/kakuremichi/gateway/internal/proxy"
"github.com/yourorg/kakuremichi/gateway/internal/wireguard"
"github.com/yourorg/kakuremichi/gateway/internal/ws"
Expand Down Expand Up @@ -99,6 +100,10 @@ func main() {
}
}()

// Initialize Exit Node proxies (will be started when config is received)
var exitHTTPProxy *exitnode.HTTPProxy
var exitSOCKS5Proxy *exitnode.SOCKS5Proxy

// Initialize WebSocket client (Control connection) with public key
// Note: publicKey is from loadOrCreateWireguardKeys, so it works even if WireGuard interface fails (e.g., on Windows)
wsClient := ws.NewClient(cfg, publicKey)
Expand Down Expand Up @@ -155,6 +160,45 @@ func main() {
if wg != nil {
ensureGatewayIPs(cfg.WireguardInterface, config.Tunnels)
}

// Update Exit Node proxies
// Collect gateway IPs for tunnels with proxy enabled
var httpProxyIPs, socksProxyIPs []string
for _, tunnel := range config.Tunnels {
if tunnel.GatewayIP == "" {
continue
}
if tunnel.HTTPProxyEnabled {
httpProxyIPs = append(httpProxyIPs, tunnel.GatewayIP)
}
if tunnel.SOCKSProxyEnabled {
socksProxyIPs = append(socksProxyIPs, tunnel.GatewayIP)
}
}

// Start/update HTTP proxy
if len(httpProxyIPs) > 0 {
if exitHTTPProxy == nil {
exitHTTPProxy = exitnode.NewHTTPProxy(config.ProxyConfig.HTTPProxyPort)
}
if err := exitHTTPProxy.UpdateListeners(httpProxyIPs); err != nil {
slog.Error("Failed to update HTTP proxy listeners", "error", err)
}
} else if exitHTTPProxy != nil {
exitHTTPProxy.Stop()
}

// Start/update SOCKS5 proxy
if len(socksProxyIPs) > 0 {
if exitSOCKS5Proxy == nil {
exitSOCKS5Proxy = exitnode.NewSOCKS5Proxy(config.ProxyConfig.SOCKSProxyPort)
}
if err := exitSOCKS5Proxy.UpdateListeners(socksProxyIPs); err != nil {
slog.Error("Failed to update SOCKS5 proxy listeners", "error", err)
}
} else if exitSOCKS5Proxy != nil {
exitSOCKS5Proxy.Stop()
}
})

// Connect to Control server
Expand All @@ -175,6 +219,12 @@ func main() {

// Graceful shutdown
httpProxy.Shutdown()
if exitHTTPProxy != nil {
exitHTTPProxy.Stop()
}
if exitSOCKS5Proxy != nil {
exitSOCKS5Proxy.Stop()
}
if wg != nil {
wg.Close()
}
Expand All @@ -185,14 +235,16 @@ func main() {
// ensureGatewayIPs adds gateway IPs for each tunnel's subnet to the WireGuard interface.
// This lets the kernel select a proper source address when proxying to agent IPs.
func ensureGatewayIPs(iface string, tunnels []struct {
ID string `json:"id"`
Domain string `json:"domain"`
AgentID string `json:"agentId"`
Target string `json:"target"`
Enabled bool `json:"enabled"`
Subnet string `json:"subnet"`
GatewayIP string `json:"gatewayIp"`
AgentIP string `json:"agentIp"`
ID string `json:"id"`
Domain string `json:"domain"`
AgentID string `json:"agentId"`
Target string `json:"target"`
Enabled bool `json:"enabled"`
Subnet string `json:"subnet"`
GatewayIP string `json:"gatewayIp"`
AgentIP string `json:"agentIp"`
HTTPProxyEnabled bool `json:"httpProxyEnabled"`
SOCKSProxyEnabled bool `json:"socksProxyEnabled"`
}) {
// Bring interface up (ignore errors)
_ = exec.Command("ip", "link", "set", iface, "up").Run()
Expand Down
175 changes: 175 additions & 0 deletions internal/exitnode/http_proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package exitnode

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

// HTTPProxy is an HTTP CONNECT proxy server for Exit Node functionality.
// It listens on Gateway's WireGuard IPs and forwards traffic to the internet.
type HTTPProxy struct {
port int
listeners []net.Listener
mu sync.Mutex
running bool
}

// NewHTTPProxy creates a new HTTP proxy server
func NewHTTPProxy(port int) *HTTPProxy {
return &HTTPProxy{
port: port,
}
}

// Start starts HTTP proxy listeners on the given gateway IPs
func (p *HTTPProxy) Start(gatewayIPs []string) error {
p.mu.Lock()
defer p.mu.Unlock()

if p.running {
return nil
}

for _, ip := range gatewayIPs {
addr := fmt.Sprintf("%s:%d", ip, p.port)
listener, err := net.Listen("tcp", addr)
if err != nil {
slog.Warn("Failed to start HTTP proxy listener", "addr", addr, "error", err)
continue
}

p.listeners = append(p.listeners, listener)
slog.Info("HTTP proxy listening", "addr", addr)

go p.serve(listener)
}

p.running = true
return nil
}

// UpdateListeners updates the listener addresses based on new gateway IPs
func (p *HTTPProxy) UpdateListeners(gatewayIPs []string) error {
p.Stop()
return p.Start(gatewayIPs)
}

// serve accepts connections and handles them
func (p *HTTPProxy) serve(listener net.Listener) {
for {
conn, err := listener.Accept()
if err != nil {
// Check if listener was closed
select {
default:
slog.Debug("HTTP proxy accept error", "error", err)
}
return
}

go p.handleConnection(conn)
}
}

// handleConnection handles a single proxy connection
func (p *HTTPProxy) 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)
}
}

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

// Connect to destination
destConn, err := net.Dial("tcp", req.Host)
if err != nil {
slog.Debug("Failed to connect to destination", "host", req.Host, "error", err)
clientConn.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n"))
return
}
defer destConn.Close()

// Send 200 Connection Established
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(destConn, clientConn)
}()

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

wg.Wait()
}

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

// Determine target address
host := req.Host
if host == "" {
host = req.URL.Host
}
if host == "" {
clientConn.Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n"))
return
}

// Add port if missing
if _, _, err := net.SplitHostPort(host); err != nil {
host = net.JoinHostPort(host, "80")
}

// Connect to destination
destConn, err := net.Dial("tcp", host)
if err != nil {
slog.Debug("Failed to connect to destination", "host", host, "error", err)
clientConn.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n"))
return
}
defer destConn.Close()

// Forward the request
req.Write(destConn)

// Copy response back
io.Copy(clientConn, destConn)
}

// Stop stops all HTTP proxy listeners
func (p *HTTPProxy) Stop() {
p.mu.Lock()
defer p.mu.Unlock()

for _, listener := range p.listeners {
listener.Close()
}
p.listeners = nil
p.running = false
}
Loading