- Go Agent (FreeBSD/amd64) fuer OPNsense - Go Backend (Linux/amd64) mit REST API + SQLite - Collector: Hardware, CPU, Memory, Disks, Network, Services, WireGuard, DHCP (KEA/ISC/dnsmasq), Routes, Gateways, Certificates, Plugins, Updates - Persistente Agent-ID - TLS + API-Key Auth - WebSocket-Infrastruktur (WIP): bidirektionaler Command-Kanal, TCP-Tunnel fuer Remote-Zugriff auf Firewall-WebUI - Lastenheft und README
277 lines
5.5 KiB
Go
277 lines
5.5 KiB
Go
package ws
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
type Client struct {
|
|
backendURL string
|
|
agentID string
|
|
apiKey string
|
|
insecure bool
|
|
|
|
conn *websocket.Conn
|
|
connMux sync.RWMutex
|
|
|
|
reconnectInterval time.Duration
|
|
pingInterval time.Duration
|
|
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
|
|
messageHandler *Handler
|
|
tunnelManager *TunnelManager
|
|
}
|
|
|
|
type Message struct {
|
|
Type string `json:"type"`
|
|
ID string `json:"id,omitempty"`
|
|
Command string `json:"command,omitempty"`
|
|
Event string `json:"event,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Params map[string]interface{} `json:"params,omitempty"`
|
|
Data interface{} `json:"data,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
func NewClient(backendURL, agentID, apiKey string, insecure bool) *Client {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
client := &Client{
|
|
backendURL: backendURL,
|
|
agentID: agentID,
|
|
apiKey: apiKey,
|
|
insecure: insecure,
|
|
reconnectInterval: 5 * time.Second,
|
|
pingInterval: 30 * time.Second,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
|
|
client.messageHandler = NewHandler(client)
|
|
client.tunnelManager = NewTunnelManager(client)
|
|
|
|
return client
|
|
}
|
|
|
|
func (c *Client) Start() error {
|
|
log.Println("WebSocket Client startet...")
|
|
|
|
go c.reconnectLoop()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) Stop() {
|
|
log.Println("WebSocket Client stoppt...")
|
|
c.cancel()
|
|
|
|
c.connMux.Lock()
|
|
if c.conn != nil {
|
|
c.conn.Close()
|
|
c.conn = nil
|
|
}
|
|
c.connMux.Unlock()
|
|
}
|
|
|
|
func (c *Client) reconnectLoop() {
|
|
backoff := c.reconnectInterval
|
|
maxBackoff := 5 * time.Minute
|
|
|
|
for {
|
|
select {
|
|
case <-c.ctx.Done():
|
|
return
|
|
default:
|
|
}
|
|
|
|
if err := c.connect(); err != nil {
|
|
log.Printf("WebSocket-Verbindung fehlgeschlagen: %v", err)
|
|
|
|
select {
|
|
case <-time.After(backoff):
|
|
// Exponential backoff mit Maximum
|
|
backoff = time.Duration(float64(backoff) * 1.5)
|
|
if backoff > maxBackoff {
|
|
backoff = maxBackoff
|
|
}
|
|
case <-c.ctx.Done():
|
|
return
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Verbindung erfolgreich - backoff zurücksetzen
|
|
backoff = c.reconnectInterval
|
|
log.Println("WebSocket-Verbindung hergestellt")
|
|
|
|
// Ping/Pong und Message-Handling starten
|
|
c.runConnection()
|
|
|
|
log.Println("WebSocket-Verbindung getrennt")
|
|
}
|
|
}
|
|
|
|
func (c *Client) connect() error {
|
|
// WebSocket-URL aufbauen
|
|
u, err := url.Parse(c.backendURL)
|
|
if err != nil {
|
|
return fmt.Errorf("Backend-URL parsen: %w", err)
|
|
}
|
|
|
|
if u.Scheme == "https" {
|
|
u.Scheme = "wss"
|
|
} else {
|
|
u.Scheme = "ws"
|
|
}
|
|
|
|
u.Path = "/api/v1/agent/ws"
|
|
q := u.Query()
|
|
q.Set("agent_id", c.agentID)
|
|
q.Set("api_key", c.apiKey)
|
|
u.RawQuery = q.Encode()
|
|
|
|
// Dialer mit TLS-Config
|
|
dialer := websocket.Dialer{
|
|
HandshakeTimeout: 10 * time.Second,
|
|
TLSClientConfig: &tls.Config{
|
|
InsecureSkipVerify: c.insecure,
|
|
},
|
|
}
|
|
|
|
conn, _, err := dialer.Dial(u.String(), nil)
|
|
if err != nil {
|
|
return fmt.Errorf("WebSocket-Dial: %w", err)
|
|
}
|
|
|
|
c.connMux.Lock()
|
|
c.conn = conn
|
|
c.connMux.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) runConnection() {
|
|
// Ping-Ticker starten
|
|
pingTicker := time.NewTicker(c.pingInterval)
|
|
defer pingTicker.Stop()
|
|
|
|
// Pong-Handler setzen
|
|
c.connMux.RLock()
|
|
conn := c.conn
|
|
c.connMux.RUnlock()
|
|
|
|
if conn == nil {
|
|
return
|
|
}
|
|
|
|
conn.SetPongHandler(func(string) error {
|
|
conn.SetReadDeadline(time.Now().Add(c.pingInterval * 2))
|
|
return nil
|
|
})
|
|
conn.SetReadDeadline(time.Now().Add(c.pingInterval * 2))
|
|
|
|
// Goroutine für eingehende Nachrichten
|
|
msgChan := make(chan Message, 10)
|
|
errChan := make(chan error, 1)
|
|
|
|
go c.readMessages(conn, msgChan, errChan)
|
|
|
|
for {
|
|
select {
|
|
case <-c.ctx.Done():
|
|
return
|
|
|
|
case <-pingTicker.C:
|
|
c.connMux.RLock()
|
|
if c.conn != nil {
|
|
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
|
c.connMux.RUnlock()
|
|
log.Printf("Ping senden fehlgeschlagen: %v", err)
|
|
return
|
|
}
|
|
}
|
|
c.connMux.RUnlock()
|
|
|
|
case msg := <-msgChan:
|
|
go c.handleMessage(msg)
|
|
|
|
case err := <-errChan:
|
|
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
|
log.Printf("WebSocket-Fehler: %v", err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Client) readMessages(conn *websocket.Conn, msgChan chan<- Message, errChan chan<- error) {
|
|
defer close(msgChan)
|
|
|
|
for {
|
|
var msg Message
|
|
if err := conn.ReadJSON(&msg); err != nil {
|
|
errChan <- err
|
|
return
|
|
}
|
|
|
|
select {
|
|
case msgChan <- msg:
|
|
case <-c.ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Client) handleMessage(msg Message) {
|
|
switch msg.Type {
|
|
case "command":
|
|
c.messageHandler.HandleCommand(msg)
|
|
case "event":
|
|
// Events vom Backend verarbeiten (falls nötig)
|
|
log.Printf("Event erhalten: %s", msg.Event)
|
|
default:
|
|
log.Printf("Unbekannter Nachrichtentyp: %s", msg.Type)
|
|
}
|
|
}
|
|
|
|
func (c *Client) SendMessage(msg Message) error {
|
|
c.connMux.RLock()
|
|
conn := c.conn
|
|
c.connMux.RUnlock()
|
|
|
|
if conn == nil {
|
|
return fmt.Errorf("keine WebSocket-Verbindung")
|
|
}
|
|
|
|
return conn.WriteJSON(msg)
|
|
}
|
|
|
|
func (c *Client) SendBinaryData(data []byte) error {
|
|
c.connMux.RLock()
|
|
conn := c.conn
|
|
c.connMux.RUnlock()
|
|
|
|
if conn == nil {
|
|
return fmt.Errorf("keine WebSocket-Verbindung")
|
|
}
|
|
|
|
return conn.WriteMessage(websocket.BinaryMessage, data)
|
|
}
|
|
|
|
func (c *Client) IsConnected() bool {
|
|
c.connMux.RLock()
|
|
connected := c.conn != nil
|
|
c.connMux.RUnlock()
|
|
|
|
return connected
|
|
} |