Meilenstein 2: Session-basierte TCP-Tunnel
- Tunnel unterstuetzen mehrere gleichzeitige TCP-Verbindungen - Jede Proxy-Verbindung bekommt eigene Session-ID - Agent: Lazy-Connect (Ziel-Verbindung erst bei Datentransfer) - Backend: Ready-Handshake (wartet auf Agent-Bestaetigung) - Binary WebSocket Messages mit Session-ID statt Tunnel-ID - Connection.Close() Race-Condition behoben (sync.Once) - SendToAgent/SendBinaryToAgent mit Timeout statt Blocking - Hub verarbeitet Agent-Responses (Session-Confirm) - README komplett aktualisiert
This commit is contained in:
+54
-118
@@ -19,27 +19,31 @@ func NewHandler(client *Client) *Handler {
|
||||
|
||||
func (h *Handler) HandleCommand(msg Message) {
|
||||
log.Printf("Command erhalten: %s (ID: %s)", msg.Command, msg.ID)
|
||||
|
||||
|
||||
var response Message
|
||||
response.Type = "response"
|
||||
response.ID = msg.ID
|
||||
|
||||
|
||||
switch msg.Command {
|
||||
case "exec":
|
||||
response = h.handleExec(msg)
|
||||
case "tunnel_open":
|
||||
response = h.handleTunnelOpen(msg)
|
||||
// Legacy: wird nicht mehr verwendet, aber fuer Kompatibilitaet
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{"message": "tunnel_open deprecated, use tunnel_connect"}
|
||||
case "tunnel_connect":
|
||||
response = h.handleTunnelConnect(msg)
|
||||
case "tunnel_disconnect":
|
||||
h.handleTunnelDisconnect(msg)
|
||||
return // Keine Response noetig
|
||||
case "tunnel_close":
|
||||
response = h.handleTunnelClose(msg)
|
||||
case "tunnel_data":
|
||||
// Tunnel-Daten werden als Binary-Messages gesendet, nicht als JSON
|
||||
h.handleTunnelData(msg)
|
||||
return
|
||||
// Legacy: alle Sessions fuer diesen Tunnel schliessen
|
||||
response.Status = "ok"
|
||||
default:
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Unbekanntes Command: %s", msg.Command)
|
||||
}
|
||||
|
||||
|
||||
if err := h.client.SendMessage(response); err != nil {
|
||||
log.Printf("Response senden fehlgeschlagen: %v", err)
|
||||
}
|
||||
@@ -50,16 +54,15 @@ func (h *Handler) handleExec(msg Message) Message {
|
||||
Type: "response",
|
||||
ID: msg.ID,
|
||||
}
|
||||
|
||||
// Parameter extrahieren
|
||||
|
||||
command, ok := msg.Params["command"].(string)
|
||||
if !ok || command == "" {
|
||||
response.Status = "error"
|
||||
response.Error = "Parameter 'command' erforderlich"
|
||||
return response
|
||||
}
|
||||
|
||||
timeoutSec := 30 // Default
|
||||
|
||||
timeoutSec := 30
|
||||
if t, ok := msg.Params["timeout"]; ok {
|
||||
if ts, ok := t.(float64); ok {
|
||||
timeoutSec = int(ts)
|
||||
@@ -69,15 +72,13 @@ func (h *Handler) handleExec(msg Message) Message {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Command ausführen mit Timeout
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// FreeBSD/OPNsense nutzt /bin/sh
|
||||
|
||||
cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command)
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
|
||||
if err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = err.Error()
|
||||
@@ -90,127 +91,62 @@ func (h *Handler) handleExec(msg Message) Message {
|
||||
"output": string(output),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func (h *Handler) handleTunnelOpen(msg Message) Message {
|
||||
func (h *Handler) handleTunnelConnect(msg Message) Message {
|
||||
response := Message{
|
||||
Type: "response",
|
||||
ID: msg.ID,
|
||||
}
|
||||
|
||||
// Parameter extrahieren
|
||||
targetHost, ok := msg.Params["target_host"].(string)
|
||||
if !ok || targetHost == "" {
|
||||
response.Status = "error"
|
||||
response.Error = "Parameter 'target_host' erforderlich"
|
||||
return response
|
||||
}
|
||||
|
||||
targetPortFloat, ok := msg.Params["target_port"].(float64)
|
||||
if !ok {
|
||||
response.Status = "error"
|
||||
response.Error = "Parameter 'target_port' erforderlich"
|
||||
return response
|
||||
}
|
||||
targetPort := int(targetPortFloat)
|
||||
|
||||
// Tunnel-ID vom Backend uebernehmen (falls vorhanden)
|
||||
backendTunnelID, _ := msg.Params["tunnel_id"].(string)
|
||||
|
||||
// Tunnel oeffnen
|
||||
tunnelID, err := h.client.tunnelManager.OpenTunnel(targetHost, targetPort, backendTunnelID)
|
||||
if err != nil {
|
||||
sessionID, _ := msg.Params["session_id"].(string)
|
||||
tunnelID, _ := msg.Params["tunnel_id"].(string)
|
||||
targetHost, _ := msg.Params["target_host"].(string)
|
||||
targetPortFloat, _ := msg.Params["target_port"].(float64)
|
||||
targetPort := int(targetPortFloat)
|
||||
|
||||
if sessionID == "" || targetHost == "" || targetPort <= 0 {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Tunnel öffnen fehlgeschlagen: %v", err)
|
||||
response.Error = "session_id, target_host und target_port erforderlich"
|
||||
return response
|
||||
}
|
||||
|
||||
|
||||
if err := h.client.tunnelManager.ConnectSession(sessionID, tunnelID, targetHost, targetPort); err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Session-Connect fehlgeschlagen: %v", err)
|
||||
return response
|
||||
}
|
||||
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{
|
||||
"tunnel_id": tunnelID,
|
||||
"session_id": sessionID,
|
||||
}
|
||||
|
||||
log.Printf("Tunnel geöffnet: %s -> %s:%d", tunnelID, targetHost, targetPort)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func (h *Handler) handleTunnelClose(msg Message) Message {
|
||||
response := Message{
|
||||
Type: "response",
|
||||
ID: msg.ID,
|
||||
}
|
||||
|
||||
// Tunnel-ID extrahieren
|
||||
tunnelID, ok := msg.Params["tunnel_id"].(string)
|
||||
if !ok || tunnelID == "" {
|
||||
response.Status = "error"
|
||||
response.Error = "Parameter 'tunnel_id' erforderlich"
|
||||
return response
|
||||
}
|
||||
|
||||
// Tunnel schließen
|
||||
if err := h.client.tunnelManager.CloseTunnel(tunnelID); err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Tunnel schließen fehlgeschlagen: %v", err)
|
||||
return response
|
||||
}
|
||||
|
||||
response.Status = "ok"
|
||||
log.Printf("Tunnel geschlossen: %s", tunnelID)
|
||||
return response
|
||||
}
|
||||
|
||||
func (h *Handler) handleTunnelData(msg Message) {
|
||||
// Diese Funktion wird für JSON-basierte Tunnel-Daten verwendet
|
||||
// In der Praxis verwenden wir Binary WebSocket Messages für bessere Performance
|
||||
tunnelID, ok := msg.Params["tunnel_id"].(string)
|
||||
if !ok {
|
||||
log.Printf("Tunnel-Daten ohne tunnel_id erhalten")
|
||||
func (h *Handler) handleTunnelDisconnect(msg Message) {
|
||||
sessionID, _ := msg.Params["session_id"].(string)
|
||||
if sessionID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
dataStr, ok := msg.Params["data"].(string)
|
||||
if !ok {
|
||||
log.Printf("Tunnel-Daten ohne data erhalten")
|
||||
return
|
||||
}
|
||||
|
||||
// Base64-Dekodierung würde hier stattfinden
|
||||
// Aber wir verwenden Binary Messages, daher ist das nur ein Fallback
|
||||
log.Printf("Tunnel-Daten via JSON erhalten für %s (Länge: %d)", tunnelID, len(dataStr))
|
||||
|
||||
h.client.tunnelManager.DisconnectSession(sessionID)
|
||||
}
|
||||
|
||||
// SendTunnelData sendet Tunnel-Daten als Binary-Message zurück zum Backend
|
||||
func (h *Handler) SendTunnelData(tunnelID string, data []byte) error {
|
||||
// Format: [tunnel_id_length:1][tunnel_id][data]
|
||||
tunnelIDBytes := []byte(tunnelID)
|
||||
if len(tunnelIDBytes) > 255 {
|
||||
return fmt.Errorf("tunnel_id zu lang")
|
||||
// SendSessionData schickt Daten als Binary-Message zurueck zum Backend
|
||||
func (h *Handler) SendSessionData(sessionID string, data []byte) error {
|
||||
idBytes := []byte(sessionID)
|
||||
if len(idBytes) > 255 {
|
||||
return fmt.Errorf("session_id zu lang")
|
||||
}
|
||||
|
||||
message := make([]byte, 1+len(tunnelIDBytes)+len(data))
|
||||
message[0] = byte(len(tunnelIDBytes))
|
||||
copy(message[1:], tunnelIDBytes)
|
||||
copy(message[1+len(tunnelIDBytes):], data)
|
||||
|
||||
|
||||
message := make([]byte, 1+len(idBytes)+len(data))
|
||||
message[0] = byte(len(idBytes))
|
||||
copy(message[1:], idBytes)
|
||||
copy(message[1+len(idBytes):], data)
|
||||
|
||||
return h.client.SendBinaryData(message)
|
||||
}
|
||||
|
||||
// ParseTunnelData parst eingehende Binary-Messages mit Tunnel-Daten
|
||||
func ParseTunnelData(data []byte) (tunnelID string, payload []byte, err error) {
|
||||
if len(data) < 1 {
|
||||
return "", nil, fmt.Errorf("Daten zu kurz")
|
||||
}
|
||||
|
||||
tunnelIDLen := int(data[0])
|
||||
if len(data) < 1+tunnelIDLen {
|
||||
return "", nil, fmt.Errorf("Tunnel-ID zu kurz")
|
||||
}
|
||||
|
||||
tunnelID = string(data[1 : 1+tunnelIDLen])
|
||||
payload = data[1+tunnelIDLen:]
|
||||
|
||||
return tunnelID, payload, nil
|
||||
}
|
||||
+95
-129
@@ -11,202 +11,168 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Tunnel struct {
|
||||
// TunnelSession repraesentiert eine einzelne TCP-Verbindung zu einem Ziel
|
||||
type TunnelSession struct {
|
||||
ID string
|
||||
TunnelID string
|
||||
TargetHost string
|
||||
TargetPort int
|
||||
Conn net.Conn
|
||||
Active bool
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// TunnelManager verwaltet alle Sessions auf Agent-Seite
|
||||
type TunnelManager struct {
|
||||
client *Client
|
||||
tunnels map[string]*Tunnel
|
||||
mutex sync.RWMutex
|
||||
client *Client
|
||||
sessions map[string]*TunnelSession
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewTunnelManager(client *Client) *TunnelManager {
|
||||
return &TunnelManager{
|
||||
client: client,
|
||||
tunnels: make(map[string]*Tunnel),
|
||||
client: client,
|
||||
sessions: make(map[string]*TunnelSession),
|
||||
}
|
||||
}
|
||||
|
||||
func (tm *TunnelManager) OpenTunnel(targetHost string, targetPort int, tunnelID ...string) (string, error) {
|
||||
// Tunnel-ID vom Backend uebernehmen oder selbst generieren
|
||||
id := ""
|
||||
if len(tunnelID) > 0 && tunnelID[0] != "" {
|
||||
id = tunnelID[0]
|
||||
} else {
|
||||
id = tm.generateTunnelID()
|
||||
}
|
||||
|
||||
// Verbindung zum Ziel aufbauen
|
||||
// ConnectSession oeffnet eine neue TCP-Verbindung fuer eine Session
|
||||
func (tm *TunnelManager) ConnectSession(sessionID, tunnelID, targetHost string, targetPort int) error {
|
||||
target := fmt.Sprintf("%s:%d", targetHost, targetPort)
|
||||
conn, err := net.DialTimeout("tcp", target, 10*time.Second)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Verbindung zu %s fehlgeschlagen: %w", target, err)
|
||||
return fmt.Errorf("Verbindung zu %s fehlgeschlagen: %w", target, err)
|
||||
}
|
||||
|
||||
// Tunnel erstellen
|
||||
tunnel := &Tunnel{
|
||||
ID: id,
|
||||
|
||||
session := &TunnelSession{
|
||||
ID: sessionID,
|
||||
TunnelID: tunnelID,
|
||||
TargetHost: targetHost,
|
||||
TargetPort: targetPort,
|
||||
Conn: conn,
|
||||
Active: true,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Tunnel registrieren
|
||||
|
||||
tm.mutex.Lock()
|
||||
tm.tunnels[id] = tunnel
|
||||
tm.sessions[sessionID] = session
|
||||
tm.mutex.Unlock()
|
||||
|
||||
// Data-Forwarding starten
|
||||
go tm.forwardData(tunnel)
|
||||
|
||||
return id, nil
|
||||
|
||||
// Ziel -> Backend Forwarding starten
|
||||
go tm.forwardFromTarget(session)
|
||||
|
||||
log.Printf("Session %s: Verbindung zu %s hergestellt", sessionID, target)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tm *TunnelManager) CloseTunnel(tunnelID string) error {
|
||||
// DisconnectSession schliesst eine Session
|
||||
func (tm *TunnelManager) DisconnectSession(sessionID string) {
|
||||
tm.mutex.Lock()
|
||||
tunnel, exists := tm.tunnels[tunnelID]
|
||||
session, exists := tm.sessions[sessionID]
|
||||
if !exists {
|
||||
tm.mutex.Unlock()
|
||||
return fmt.Errorf("Tunnel %s nicht gefunden", tunnelID)
|
||||
return
|
||||
}
|
||||
|
||||
tunnel.Active = false
|
||||
delete(tm.tunnels, tunnelID)
|
||||
|
||||
session.Active = false
|
||||
delete(tm.sessions, sessionID)
|
||||
tm.mutex.Unlock()
|
||||
|
||||
// Verbindung schließen
|
||||
if tunnel.Conn != nil {
|
||||
tunnel.Conn.Close()
|
||||
|
||||
if session.Conn != nil {
|
||||
session.Conn.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
log.Printf("Session %s: geschlossen", sessionID)
|
||||
}
|
||||
|
||||
func (tm *TunnelManager) GetActiveTunnels() []string {
|
||||
// SendDataToSession schreibt Daten in die Ziel-Verbindung einer Session
|
||||
func (tm *TunnelManager) SendDataToSession(sessionID string, data []byte) error {
|
||||
tm.mutex.RLock()
|
||||
defer tm.mutex.RUnlock()
|
||||
|
||||
tunnels := make([]string, 0, len(tm.tunnels))
|
||||
for id := range tm.tunnels {
|
||||
tunnels = append(tunnels, id)
|
||||
}
|
||||
|
||||
return tunnels
|
||||
}
|
||||
|
||||
func (tm *TunnelManager) SendDataToTunnel(tunnelID string, data []byte) error {
|
||||
tm.mutex.RLock()
|
||||
tunnel, exists := tm.tunnels[tunnelID]
|
||||
session, exists := tm.sessions[sessionID]
|
||||
tm.mutex.RUnlock()
|
||||
|
||||
if !exists || !tunnel.Active {
|
||||
return fmt.Errorf("Tunnel %s nicht aktiv", tunnelID)
|
||||
|
||||
if !exists || !session.Active {
|
||||
return fmt.Errorf("Session %s nicht aktiv", sessionID)
|
||||
}
|
||||
|
||||
if tunnel.Conn == nil {
|
||||
return fmt.Errorf("Tunnel %s hat keine Verbindung", tunnelID)
|
||||
}
|
||||
|
||||
_, err := tunnel.Conn.Write(data)
|
||||
|
||||
_, err := session.Conn.Write(data)
|
||||
if err != nil {
|
||||
// Bei Fehler Tunnel schließen
|
||||
tm.CloseTunnel(tunnelID)
|
||||
return fmt.Errorf("Daten schreiben fehlgeschlagen: %w", err)
|
||||
tm.DisconnectSession(sessionID)
|
||||
return fmt.Errorf("Session %s: Write fehlgeschlagen: %w", sessionID, err)
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tm *TunnelManager) forwardData(tunnel *Tunnel) {
|
||||
// forwardFromTarget liest Daten vom Ziel und schickt sie als Binary-Message ans Backend
|
||||
func (tm *TunnelManager) forwardFromTarget(session *TunnelSession) {
|
||||
defer func() {
|
||||
if tunnel.Active {
|
||||
tm.CloseTunnel(tunnel.ID)
|
||||
if session.Active {
|
||||
tm.DisconnectSession(session.ID)
|
||||
}
|
||||
|
||||
// Event senden: Tunnel geschlossen
|
||||
event := Message{
|
||||
Type: "event",
|
||||
Event: "tunnel_closed",
|
||||
Data: map[string]interface{}{
|
||||
"tunnel_id": tunnel.ID,
|
||||
},
|
||||
}
|
||||
tm.client.SendMessage(event)
|
||||
|
||||
log.Printf("Tunnel %s: Data-Forwarding beendet", tunnel.ID)
|
||||
log.Printf("Session %s: Forwarding beendet", session.ID)
|
||||
}()
|
||||
|
||||
buffer := make([]byte, 32768) // 32KB Buffer
|
||||
|
||||
for tunnel.Active {
|
||||
// Read-Timeout setzen um regelmäßig Active-Status zu prüfen
|
||||
tunnel.Conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
|
||||
n, err := tunnel.Conn.Read(buffer)
|
||||
|
||||
buffer := make([]byte, 32768)
|
||||
|
||||
for session.Active {
|
||||
session.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
|
||||
n, err := session.Conn.Read(buffer)
|
||||
if err != nil {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
// Timeout ist OK, einfach weiter versuchen
|
||||
continue
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
log.Printf("Tunnel %s: Ziel-Verbindung geschlossen", tunnel.ID)
|
||||
} else {
|
||||
log.Printf("Tunnel %s: Read-Fehler: %v", tunnel.ID, err)
|
||||
log.Printf("Session %s: Ziel-Verbindung geschlossen", session.ID)
|
||||
} else if session.Active {
|
||||
log.Printf("Session %s: Read-Fehler: %v", session.ID, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
if n > 0 {
|
||||
// Daten über WebSocket an Backend weiterleiten
|
||||
data := buffer[:n]
|
||||
if err := tm.client.messageHandler.SendTunnelData(tunnel.ID, data); err != nil {
|
||||
log.Printf("Tunnel %s: WebSocket senden fehlgeschlagen: %v", tunnel.ID, err)
|
||||
if err := tm.client.messageHandler.SendSessionData(session.ID, buffer[:n]); err != nil {
|
||||
log.Printf("Session %s: WebSocket senden fehlgeschlagen: %v", session.ID, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tm *TunnelManager) generateTunnelID() string {
|
||||
// ProcessBinaryMessage verarbeitet eingehende Binary-Messages vom Backend (Proxy -> Ziel)
|
||||
func (tm *TunnelManager) ProcessBinaryMessage(data []byte) error {
|
||||
sessionID, payload, err := ParseBinaryMessage(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Binary-Message parsen: %w", err)
|
||||
}
|
||||
|
||||
return tm.SendDataToSession(sessionID, payload)
|
||||
}
|
||||
|
||||
// GetActiveSessions gibt die Anzahl aktiver Sessions zurueck
|
||||
func (tm *TunnelManager) GetActiveSessions() int {
|
||||
tm.mutex.RLock()
|
||||
defer tm.mutex.RUnlock()
|
||||
return len(tm.sessions)
|
||||
}
|
||||
|
||||
func (tm *TunnelManager) generateID() string {
|
||||
bytes := make([]byte, 8)
|
||||
rand.Read(bytes)
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
// ProcessBinaryMessage verarbeitet eingehende Binary-Messages vom Backend
|
||||
func (tm *TunnelManager) ProcessBinaryMessage(data []byte) error {
|
||||
tunnelID, payload, err := ParseTunnelData(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Binary-Message parsen: %w", err)
|
||||
// ParseBinaryMessage parst das Binary-Format: [id_len:1][id][data]
|
||||
func ParseBinaryMessage(data []byte) (id string, payload []byte, err error) {
|
||||
if len(data) < 1 {
|
||||
return "", nil, fmt.Errorf("Daten zu kurz")
|
||||
}
|
||||
|
||||
return tm.SendDataToTunnel(tunnelID, payload)
|
||||
}
|
||||
|
||||
// CleanupInactiveTunnels schließt inaktive Tunnel (Cleanup-Task)
|
||||
func (tm *TunnelManager) CleanupInactiveTunnels() {
|
||||
tm.mutex.Lock()
|
||||
defer tm.mutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for id, tunnel := range tm.tunnels {
|
||||
// Tunnel die länger als 1 Stunde inaktiv sind, schließen
|
||||
if !tunnel.Active || now.Sub(tunnel.CreatedAt) > time.Hour {
|
||||
tunnel.Active = false
|
||||
if tunnel.Conn != nil {
|
||||
tunnel.Conn.Close()
|
||||
}
|
||||
delete(tm.tunnels, id)
|
||||
log.Printf("Inaktiver Tunnel %s bereinigt", id)
|
||||
}
|
||||
idLen := int(data[0])
|
||||
if len(data) < 1+idLen {
|
||||
return "", nil, fmt.Errorf("ID zu kurz")
|
||||
}
|
||||
}
|
||||
|
||||
id = string(data[1 : 1+idLen])
|
||||
payload = data[1+idLen:]
|
||||
return id, payload, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user