package tunnel import ( "fmt" "io" "net" "strings" "time" "os" "golang.org/x/crypto/ssh" ) func logToFile(msg string) { f, _ := os.OpenFile("client_debug.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) defer f.Close() f.WriteString(time.Now().Format(time.RFC3339) + " " + msg + "\n") } // Client handles the SSH connection and forwarding type Client struct { ServerAddr string LocalPort string AuthToken string SSHClient *ssh.Client Listener net.Listener Events chan string // Channel to send logs/events to TUI Metrics chan int64 // Channel to send bytes transferred PublicURL string // PublicURL is the URL accessible from the internet } func NewClient(serverAddr, localPort, authToken string) *Client { return &Client{ ServerAddr: serverAddr, LocalPort: localPort, AuthToken: authToken, Events: make(chan string, 10), Metrics: make(chan int64, 10), } } func (c *Client) Start() error { config := &ssh.ClientConfig{ User: "grokway", Auth: []ssh.AuthMethod{ ssh.Password(c.AuthToken), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), // Dev only Timeout: 5 * time.Second, } c.Events <- fmt.Sprintf("Connecting to %s...", c.ServerAddr) client, err := ssh.Dial("tcp", c.ServerAddr, config) if err != nil { return err } c.SSHClient = client c.Events <- "SSH Connected!" // Request remote listening (Reverse Forwarding) // Bind to 0.0.0.0 on server, random port (0) listener, err := client.Listen("tcp", "0.0.0.0:0") if err != nil { return fmt.Errorf("failed to request port forwarding: %w", err) } c.Listener = listener // Query server for assigned slug ok, slugBytes, err := client.SendRequest("grokway-whoami", true, nil) slug := "test-slug" // Fallback if err == nil && ok { slug = string(slugBytes) c.Events <- fmt.Sprintf("Server assigned domain: %s", slug) } else { c.Events <- "Failed to query domain from server, using fallback" } hostname := "localhost" // This should match what the server is running on actually // Assuming HTTP proxy is on port 8080 of the same host as SSH server (but different port) // We extract host from c.ServerAddr host, _, _ := net.SplitHostPort(c.ServerAddr) if host == "" { host = hostname } c.PublicURL = fmt.Sprintf("https://%s.%s", slug, host) c.Events <- fmt.Sprintf("Tunnel established! Public URL: %s", c.PublicURL) go c.acceptLoop() return nil } func (c *Client) acceptLoop() { for { remoteConn, err := c.Listener.Accept() if err != nil { c.Events <- fmt.Sprintf("Accept error: %s", err) break } c.Events <- "New Request received" go c.handleConnection(remoteConn) } } func (c *Client) handleConnection(remoteConn net.Conn) { defer remoteConn.Close() // Dial local service localConn, err := net.Dial("tcp", "localhost:"+c.LocalPort) if err != nil { errMsg := fmt.Sprintf("Failed to dial local: %s", err) c.Events <- errMsg logToFile(errMsg) return } defer localConn.Close() logToFile("Dialed local service successfully") // We need to peek at the connection to see if it's HTTP // Wrap the connection to peek without consuming logToFile("Handling new connection") // Check if we can peek // Since net.Conn doesn't support Peek, we read specific bytes and reconstruct // or use a bufio reader if we weren't doing a raw Copy. // But io.Copy needs a reader. // Create a buffer to read the first few bytes buf := make([]byte, 1024) n, err := remoteConn.Read(buf) if err != nil { logToFile(fmt.Sprintf("Error reading from remote: %v", err)) return } payload := string(buf[:n]) // Try to parse rudimentary HTTP // Format: METHOD PATH PROTOCOL lines := strings.Split(payload, "\n") if len(lines) > 0 { firstLine := lines[0] parts := strings.Fields(firstLine) if len(parts) >= 3 { method := parts[0] path := parts[1] // Send structured log c.Events <- fmt.Sprintf("HTTP|%s|%s|%d", method, path, 200) // Status is fake for now, acts as connection open } else { c.Events <- "TCP|||Connection" } } // We need to write the bytes we read to the local connection first localConn.Write(buf[:n]) // Bidirectional copy // Calculate bytes? // We can use a custom copy to track metrics go func() { n, _ := io.Copy(remoteConn, localConn) c.Metrics <- n }() n2, _ := io.Copy(localConn, remoteConn) c.Metrics <- n2 }