feat: Phase 7 — MeshCentral Remote Desktop Integration

Backend:
- MeshCentralOptions + MeshCentralService: Node-Lookup via Hostname, Remote-Desktop-URL-Generierung
- RemoteDesktopController: GET /api/v1/agents/{id}/remote-session mit 3 Status-Zuständen (nicht konfiguriert / Agent fehlt / bereit)
- Program.cs: HttpClient + MeshCentralService registriert, appsettings.json mit Konfigurationsblock

Go Agent:
- config.go: MeshCentralUrl + MeshEnabled Felder
- internal/meshagent/installer.go: MeshAgent Download + Installation (Windows Service / Linux systemd)
- main.go: Automatische MeshAgent-Installation nach Enrollment wenn aktiviert

Frontend:
- RemoteDesktopButton: Modales Dialog mit 3 Zustandsanzeigen (Setup nötig / Agent installieren / Remote Desktop öffnen)
- AgentDetailPage: RemoteDesktopButton im Header integriert
- api/types.ts + api/client.ts: RemoteSessionInfo Typ + remoteDesktopApi

docker-compose.yml: MeshCentral Service (ghcr.io/ylianst/meshcentral:latest, Ports 4430/4431)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-03-19 14:39:49 +01:00
parent 84629dfbcf
commit 55e016c07d
14 changed files with 579 additions and 7 deletions

View File

@@ -11,6 +11,8 @@ type Config struct {
AgentID string `yaml:"agent_id"`
HeartbeatInterval int `yaml:"heartbeat_interval"`
TLSEnabled bool `yaml:"tls_enabled"`
MeshCentralUrl string `yaml:"mesh_central_url"`
MeshEnabled bool `yaml:"mesh_enabled"`
}
func Load(path string) (*Config, error) {

View File

@@ -0,0 +1,149 @@
package meshagent
import (
"context"
"crypto/tls"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
)
// IsInstalled prüft ob der MeshAgent-Prozess läuft oder das Binary vorhanden ist.
func IsInstalled() bool {
if runtime.GOOS == "windows" {
// Prüfe ob MeshAgent Service läuft
cmd := exec.Command("sc", "query", "Mesh Agent")
return cmd.Run() == nil
}
// Linux: prüfe ob meshagent Prozess läuft
cmd := exec.Command("pgrep", "-x", "meshagent")
return cmd.Run() == nil
}
// Install lädt den MeshAgent von MeshCentral herunter und installiert ihn.
// meshCentralUrl: z.B. "https://192.168.1.100:4430"
func Install(ctx context.Context, meshCentralUrl string) error {
if IsInstalled() {
log.Println("MeshAgent ist bereits installiert")
return nil
}
log.Printf("MeshAgent wird von %s heruntergeladen...", meshCentralUrl)
// Agent-ID je nach OS
agentID := "6" // Linux x64
if runtime.GOOS == "windows" {
agentID = "3" // Windows x64
}
downloadUrl := fmt.Sprintf("%s/meshagents?id=%s", strings.TrimRight(meshCentralUrl, "/"), agentID)
// SSL-Fehler ignorieren (selbstsigniertes Zertifikat in Dev)
httpClient := &http.Client{
Timeout: 5 * time.Minute,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadUrl, nil)
if err != nil {
return fmt.Errorf("HTTP Request konnte nicht erstellt werden: %w", err)
}
resp, err := httpClient.Do(req)
if err != nil {
return fmt.Errorf("Download fehlgeschlagen: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("MeshCentral antwortete mit Status %d — ist MeshCentral gestartet?", resp.StatusCode)
}
// Temp-Datei speichern
var tmpPath string
if runtime.GOOS == "windows" {
tmpPath = filepath.Join(os.TempDir(), "meshagent.exe")
} else {
tmpPath = filepath.Join(os.TempDir(), "meshagent")
}
f, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
if err != nil {
return fmt.Errorf("Temp-Datei konnte nicht erstellt werden: %w", err)
}
if _, err := io.Copy(f, resp.Body); err != nil {
f.Close()
return fmt.Errorf("Fehler beim Schreiben: %w", err)
}
f.Close()
log.Printf("MeshAgent heruntergeladen nach %s", tmpPath)
// Installieren
return installBinary(ctx, tmpPath, meshCentralUrl)
}
func installBinary(ctx context.Context, binaryPath, meshCentralUrl string) error {
if runtime.GOOS == "windows" {
// Windows: als Service installieren
cmd := exec.CommandContext(ctx, binaryPath, "-install", "-url", meshCentralUrl)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("MeshAgent Windows-Installation fehlgeschlagen: %w", err)
}
log.Println("MeshAgent als Windows Service installiert")
} else {
// Linux: in /usr/local/bin installieren und als Service starten
installPath := "/usr/local/bin/meshagent"
if err := os.Rename(binaryPath, installPath); err != nil {
// Falls rename scheitert (cross-device), kopieren
if err2 := copyFile(binaryPath, installPath); err2 != nil {
return fmt.Errorf("MeshAgent konnte nicht nach %s verschoben werden: %w", installPath, err2)
}
}
if err := os.Chmod(installPath, 0755); err != nil {
return fmt.Errorf("chmod fehlgeschlagen: %w", err)
}
// Als Service starten (systemd oder direkt)
cmd := exec.CommandContext(ctx, installPath, "-install", "-url", meshCentralUrl)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
// Direkt starten als Fallback
log.Printf("Service-Installation fehlgeschlagen, starte direkt: %v", err)
go func() {
exec.Command(installPath, "-url", meshCentralUrl).Run()
}()
}
log.Println("MeshAgent auf Linux installiert")
}
return nil
}
func copyFile(src, dst string) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
if err != nil {
return err
}
defer dstFile.Close()
_, err = io.Copy(dstFile, srcFile)
return err
}