conduit/cmd/link.go
2025-09-19 17:19:22 -04:00

132 lines
2.8 KiB
Go

package cmd
import (
"fmt"
"log"
"net"
"sync"
"github.com/gorilla/websocket"
"github.com/spf13/cobra"
)
type TunnelMessage struct {
Type string `json:"type"`
StreamID string `json:"stream_id"`
Data []byte `json:"data,omitempty"`
}
var serverAddr string
var linkCmd = &cobra.Command{
Use: "link <vhost_location> <host:port>",
Short: "Create a tunnel link",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
vhostLoc := args[0]
hostPort := args[1]
fmt.Printf("Creating TCP tunnel: %s -> %s\n", vhostLoc, hostPort)
if err := startTCPTunnel(vhostLoc, hostPort); err != nil {
log.Fatal("Failed to start tunnel:", err)
}
},
}
func init() {
linkCmd.Flags().StringVarP(&serverAddr, "server", "s", "localhost:8080", "Conduit server address")
}
func startTCPTunnel(vhost, hostPort string) error {
wsURL := fmt.Sprintf("ws://%s/_conduit/tunnel?vhost=%s", serverAddr, vhost)
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
return fmt.Errorf("failed to connect: %v", err)
}
defer conn.Close()
fmt.Printf("TCP Tunnel active! %s.example.com -> %s\n", vhost, hostPort)
// Track active connections
connections := make(map[string]net.Conn)
var connMutex sync.RWMutex
// Handle messages from server
for {
var msg TunnelMessage
err := conn.ReadJSON(&msg)
if err != nil {
log.Printf("Error reading from tunnel: %v", err)
break
}
switch msg.Type {
case "data":
connMutex.RLock()
localConn, exists := connections[msg.StreamID]
connMutex.RUnlock()
if !exists {
// New connection
localConn, err = net.Dial("tcp", hostPort)
if err != nil {
log.Printf("Failed to connect to %s: %v", hostPort, err)
continue
}
connMutex.Lock()
connections[msg.StreamID] = localConn
connMutex.Unlock()
// Start reading from local connection
go func(streamID string, lConn net.Conn) {
defer func() {
connMutex.Lock()
delete(connections, streamID)
connMutex.Unlock()
lConn.Close()
}()
buffer := make([]byte, 4096)
for {
n, err := lConn.Read(buffer)
if err != nil {
break
}
response := TunnelMessage{
Type: "data",
StreamID: streamID,
Data: buffer[:n],
}
if err := conn.WriteJSON(response); err != nil {
break
}
}
}(msg.StreamID, localConn)
}
// Write data to local connection
if _, err := localConn.Write(msg.Data); err != nil {
log.Printf("Error writing to local connection: %v", err)
localConn.Close()
connMutex.Lock()
delete(connections, msg.StreamID)
connMutex.Unlock()
}
case "close":
connMutex.Lock()
if localConn, exists := connections[msg.StreamID]; exists {
localConn.Close()
delete(connections, msg.StreamID)
}
connMutex.Unlock()
}
}
return nil
}