fix doc with project reality

Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
GnomeZworc 2026-05-18 19:47:02 +02:00
commit 9d4d1b38b3
Signed by: nicolas.boufideline
GPG key ID: 4406BBBF8845D632
5 changed files with 465 additions and 454 deletions

@ -1,269 +1,157 @@
# Le reseaux entre les vms
# Réseau entre les VMs
## Topologie
```
+--------------------------------------------------------------+ +----------------------------------------------------+
| HOST A | | HOST B |
| | | |
| [ netns-1 (VPC 1) ] | | |
| | | |
| br-subnet-1 (10.0.1.1/32) ---- veth1 <----------------+ | | |
| (dans netns) | | |
| | | |
| br-subnet-2 (10.0.2.1/32) ---- veth2 <----------------+ | | |
| (dans netns) | | |
| | | |
| | | |
| [ netns-2 (VPC 2) ] | | [ netns-2 (VPC 2) ] |
| | | |
| br-subnet-3 (10.0.3.1/32) ---- veth3 <------+ | | br-subnet-3 (10.0.3.1/32) ---- veth4 <------+ |
| (dans netns) | | | (dans netns) | |
| | | | | |
+-------------------------------------------------+------------+ +-------------------+--------------------------------+
| |
| |
+----------v-----------+ +-----------v----------+
| br-vx-1 (host) | | br-vx-1 (host) |
| (infra bridge) | | (infra bridge) |
+----------------------+ +----------------------+
| |
+-----------v-----------+ +----------v------------+
| vxlan1001 | | vxlan1001 |
| (Tunnel inter-host) | | (Tunnel inter-host) |
+-----------------------+ +-----------------------+
Root netns
├── vp-<vpcID>-e ← veth externe vers le netns VPC
├── br-<subnetID> ← bridge subnet (côté root)
│ ├── v-<subnetID>-e ← veth externe subnet
│ └── vxlan-<vxlanID> ← tunnel inter-host
netns.<vpcName>
├── br-public ← bridge d'accès public du VPC
│ └── vp-<vpcID>-i ← veth interne VPC
├── br-<subnetID> ← bridge subnet (côté VPC, isolé)
│ ├── v-<subnetID>-i ← veth interne subnet
│ └── tap<ID> ← interface de la VM
└── ...
```
Chaque VPC est un **Linux network namespace**. Les subnets sont des bridges isolés dans ce namespace. Le trafic inter-host passe par des tunnels **VXLAN** sur les bridges root. Les VMs attachent via des interfaces **TAP**.
## Packages
| Package | Rôle |
|---|---|
| `internal/netns` | Cycle de vie des network namespaces |
| `internal/netif` | Création/suppression d'interfaces (bridge, veth, vxlan, tap) |
| `internal/subnet` | Orchestration complète de création/suppression d'un subnet |
| `internal/vpc` | Orchestration complète de création/suppression d'un VPC |
| `internal/dhcp` | Génération de config dnsmasq + stockage des entrées DHCP |
| `internal/ebtables` | Règles L2 (DROP ARP/DHCP) sur les bridges |
| `internal/iptables` | Redirection NAT pour le serveur metadata |
| `pkg/systemd` | Start/Stop de services systemd via D-Bus |
## `internal/netns`
```go
package netutils
import (
"fmt"
"net"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netns"
"golang.org/x/sys/unix"
)
// CreateNetns crée un nouveau namespace nommé
func CreateNetns(name string) error {
path := fmt.Sprintf("/var/run/netns/%s", name)
return unix.Mount("/proc/self/ns/net", path, "none", unix.MS_BIND, "")
}
// CreateBridge crée un bridge dans le namespace donné (ou root si nsPath == "")
func CreateBridge(name, nsPath string) error {
link := &netlink.Bridge{
LinkAttrs: netlink.LinkAttrs{Name: name},
}
if nsPath != "" {
newNs, err := netns.GetFromPath(nsPath)
if err != nil {
return err
}
defer newNs.Close()
return netns.Do(newNs, func(_ netns.NsHandle) error {
return netlink.LinkAdd(link)
})
}
return netlink.LinkAdd(link)
}
// LinkBridgeNetns connecte deux bridges via un veth pair (br1 dans root, br2 dans netns)
func LinkBridgeNetns(vethName, peerName, netnsPath, bridgeRoot, bridgeInNs string) error {
peer := netlink.Veth{
LinkAttrs: netlink.LinkAttrs{Name: vethName},
PeerName: peerName,
}
if err := netlink.LinkAdd(&peer); err != nil {
return err
}
// Set peer into target netns
ns, err := netns.GetFromPath(netnsPath)
if err != nil {
return err
}
defer ns.Close()
linkPeer, err := netlink.LinkByName(peerName)
if err != nil {
return err
}
if err := netlink.LinkSetNsFd(linkPeer, int(ns)); err != nil {
return err
}
// Attach veth to host bridge
br, err := netlink.LinkByName(bridgeRoot)
if err != nil {
return err
}
veth, _ := netlink.LinkByName(vethName)
if err := netlink.LinkSetMaster(veth, br); err != nil {
return err
}
if err := netlink.LinkSetUp(veth); err != nil {
return err
}
// Attach peer to bridge in netns
return netns.Do(ns, func(_ netns.NsHandle) error {
peerLink, err := netlink.LinkByName(peerName)
if err != nil {
return err
}
brInNs, err := netlink.LinkByName(bridgeInNs)
if err != nil {
return err
}
if err := netlink.LinkSetMaster(peerLink, brInNs); err != nil {
return err
}
return netlink.LinkSetUp(peerLink)
})
}
// AddIPToBridge ajoute une IP en /32 à un bridge dans un namespace
func AddIPToBridge(nsPath, bridgeName, ipCidr string) error {
ns, err := netns.GetFromPath(nsPath)
if err != nil {
return err
}
defer ns.Close()
return netns.Do(ns, func(_ netns.NsHandle) error {
br, err := netlink.LinkByName(bridgeName)
if err != nil {
return err
}
addr, err := netlink.ParseAddr(ipCidr)
if err != nil {
return err
}
return netlink.AddrAdd(br, addr)
})
}
// AddLinkLocalRoute ajoute une route link-local dans un netns
func AddLinkLocalRoute(nsPath string) error {
ns, err := netns.GetFromPath(nsPath)
if err != nil {
return err
}
defer ns.Close()
return netns.Do(ns, func(_ netns.NsHandle) error {
_, dst, _ := net.ParseCIDR("fe80::/64")
route := &netlink.Route{
Dst: dst,
Scope: netlink.SCOPE_LINK,
}
return netlink.RouteAdd(route)
})
}
// CleanupNetns démonte et supprime un netns (utilise iproute2 fs)
func CleanupNetns(name string) error {
path := fmt.Sprintf("/var/run/netns/%s", name)
if err := unix.Unmount(path, 0); err != nil {
return err
}
return unix.Unlink(path)
}
// DeleteLink supprime un lien réseau (bridge, veth, etc.)
func DeleteLink(name string) error {
link, err := netlink.LinkByName(name)
if err != nil {
return err
}
return netlink.LinkDel(link)
}
func Create(name string) error // CLONE_NEWNET + bind mount sur /var/run/netns/<name>
func Delete(name string) error // unmount + suppression du fichier
func Call(name string, fn func() error) error // exécute fn dans le netns, restaure le ns d'origine
func Enter(name string) error // entre définitivement dans le netns
func Exist(name string) bool
```
La création utilise `unix.Unshare(CLONE_NEWNET)` + bind mount du thread courant (`/proc/self/task/<tid>/ns/net`) — pas un bind mount du namespace du processus.
## `internal/netif`
```go
CreateNetns("netns-1")
CreateBridge("br-subnet-1", "/var/run/netns/netns-1")
CreateBridge("br-vx-1", "") // root
LinkBridgeNetns("veth1", "veth1-peer", "/var/run/netns/netns-1", "br-vx-1", "br-subnet-1")
AddIPToBridge("/var/run/netns/netns-1", "br-subnet-1", "10.0.1.1/32")
AddLinkLocalRoute("/var/run/netns/netns-1")
func CreateBridge(name string, mtu int) error
func BridgeSetMaster(iface, bridge string) error
func CreateVethToNetns(rootIf, nsIf, netnsPath string, mtu int) error
func CreateVxlan(name string, vxlanID int, localIface string, mtu int) error
func CreateTap(tapID int, bridgeName, vpcName string) error
func DeleteTap(tapID int, vpcName string) error
func DeleteLink(name string) error
func AddrAdd(iface string, ip net.IP) error
func RouteAdd(iface string, subnet *net.IPNet) error
func LinkSetUp(name string) error
func LinkSetDown(name string) error
```
# systemd pour dnsmasq et metadata
**Conventions de nommage :**
Oui, cest **tout à fait possible** avec la bibliothèque `go-systemd`, en particulier via le **module `dbus`**, qui permet de démarrer, arrêter et interroger l'état des unités `systemd`.
| Interface | Nom | Emplacement |
|---|---|---|
| Bridge subnet | `br-<subnetID>` | root + netns VPC |
| Veth subnet externe | `v-<subnetID>-e` | root |
| Veth subnet interne | `v-<subnetID>-i` | netns VPC |
| Veth VPC externe | `vp-<vpcID>-e` | root |
| Veth VPC interne | `vp-<vpcID>-i` | netns VPC |
| VXLAN | `vxlan-<vxlanID>` | root |
| TAP VM | `tap<ID>` | netns VPC |
| Bridge public VPC | `br-public` | netns VPC |
Voici un exemple minimal de code Go pour **démarrer** et **arrêter** une unité comme `dnsmasq@vpc-00003_br-00000.service` :
**Paramètres réseau :** MTU 1500 pour les subnets, MTU 9000 pour les VPCs. VXLAN port 4789, learning désactivé.
---
## Création d'un subnet
### ✅ Exemple de code pour start/stop via `go-systemd`:
`internal/subnet.CreateSubnet(db, subnetName)` :
1. Crée la paire veth `v-<subnetID>-{e,i}` vers le netns VPC
2. Crée `br-<subnetID>` (root) et `br-<subnetID>` (netns VPC)
3. Crée `vxlan-<vxlanID>` dans root
4. Attache les interfaces à leurs bridges (`BridgeSetMaster`)
5. Active toutes les interfaces (`LinkSetUp`)
6. Ajoute l'adresse gateway sur `br-<subnetID>` dans le VPC
7. Ajoute une route `scope link` vers le CIDR du subnet sur `br-<subnetID>` dans le VPC
8. Applique les règles ebtables (ARP + DHCP DROP)
9. Génère la config dnsmasq → `/etc/dnsmasq.d/<vpc>_<bridge>.conf`
10. Démarre `dnsmasq@<vpc>_<bridge>.service` via systemd
Suppression : ordre inverse — arrêt dnsmasq, suppression config, suppression règles ebtables, suppression des interfaces.
## Création d'un VPC
`internal/vpc.CreateVPC(db, vpcName)` :
1. Crée le netns `<vpcName>`
2. Crée la paire veth `vp-<vpcID>-{e,i}`
3. Crée `br-public` dans le netns VPC
4. Attache les veths à `br-public`
5. Active les interfaces
Suppression : supprime `vp-<vpcID>-e` (la paire disparaît) puis supprime le netns.
## DHCP (`internal/dhcp`)
dnsmasq est utilisé en mode static : chaque IP du subnet se voit attribuer une entrée `dhcp-host=<mac>,<ip>`.
```go
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/coreos/go-systemd/v22/dbus"
)
func main() {
conn, err := dbus.NewSystemdConnectionContext(context.Background())
if err != nil {
log.Fatalf("Erreur de connexion à systemd: %v", err)
}
defer conn.Close()
unitName := "dnsmasq@vpc-00003_br-00000.service"
// Démarrer l'unité
jobID, err := conn.StartUnitContext(context.Background(), unitName, "replace", nil)
if err != nil {
log.Fatalf("Erreur au démarrage de l'unité: %v", err)
}
fmt.Printf("Service démarré (job ID: %s)\n", jobID)
time.Sleep(3 * time.Second) // attendre un peu
// Arrêter l'unité
jobID, err = conn.StopUnitContext(context.Background(), unitName, "replace", nil)
if err != nil {
log.Fatalf("Erreur à l'arrêt de l'unité: %v", err)
}
fmt.Printf("Service arrêté (job ID: %s)\n", jobID)
}
func GenerateConfig(c Config) (string, map[string]string, error)
func StoreDHCPEntries(db *badger.DB, subnetName string, entries map[string]string) error
func GetMACForIP(db *badger.DB, subnetName, ip string) (string, error)
```
---
Config générée dans `/etc/dnsmasq.d/<vpc>_<bridge>.conf`. Entrées stockées en DB sous `subnet/<subnetName>/dhcp/<ip>`.
### 🔐 Privilèges requis :
## Sécurité réseau
* Il faut que votre programme Go soit exécuté avec des **droits suffisants** (souvent `sudo`) pour interagir avec systemd **au niveau système**.
### ebtables (`internal/ebtables`)
Exemple d'exécution :
Appliquées sur chaque bridge subnet :
```bash
sudo ./mon-binaire-go
```go
func DropARPToGateway(bridge, gatewayIP string) error // DROP ARP Request vers la gateway
func DropDHCP(bridge string) error // DROP DHCP (ports 67:68)
func DeleteARPToGateway(bridge, gatewayIP string) error
func DeleteDHCP(bridge string) error
```
---
Empêche les VMs de résoudre ou d'usurper la gateway, et d'émettre leur propre DHCP.
### 📦 Dépendance :
### iptables (`internal/iptables`)
Ajoutez ce module dans votre `go.mod` :
Redirection metadata cloud-init :
```bash
go get github.com/coreos/go-systemd/v22
```go
func AddMetadataRedirect(vmIP, gatewayIP, metadataPort string) error
func DeleteMetadataRedirect(vmIP, gatewayIP, metadataPort string) error
```
---
Redirige `<vmIP> → 169.254.169.254:80` vers `<gatewayIP>:<metadataPort>` via DNAT (table nat, PREROUTING).
Souhaitez-vous aussi vérifier létat de lunité (`Active`, `Failed`, etc.) ou juste la gestion start/stop ?
## systemd (`pkg/systemd`)
```go
func New() (*Manager, error)
func (m *Manager) Start(service string) error
func (m *Manager) Stop(service string) error
func (m *Manager) Status(service string) (*ServiceStatus, error)
```
Utilise D-Bus (`coreos/go-systemd/v22`). Timeout job : 30 secondes. Service type : `"replace"`.
Services gérés : `dnsmasq@<vpc>_<bridge>.service` (un par subnet).

@ -1,98 +1,68 @@
# Persistance des data
# Persistance des données
```go
package vm
L'agent utilise **BadgerDB** (KV store embarqué) pour persister l'état de toutes les ressources.
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
)
Chemin par défaut : `/var/lib/two/data/`
const defaultStateDir = "/var/lib/cloud-agent/vms"
type VMInstance struct {
ID string
Pid int
QMPSocket string
HMPSocket string
}
func (vm *VMInstance) SaveState(stateDir string) error {
if stateDir == "" {
stateDir = defaultStateDir
}
if err := os.MkdirAll(stateDir, 0755); err != nil {
return err
}
file := filepath.Join(stateDir, vm.ID+".json")
data, err := json.MarshalIndent(vm, "", " ")
if err != nil {
return err
}
return os.WriteFile(file, data, 0644)
}
func LoadVMState(path string) (*VMInstance, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var vm VMInstance
if err := json.Unmarshal(data, &vm); err != nil {
return nil, err
}
return &vm, nil
}
func LoadAllVMs(stateDir string) ([]*VMInstance, error) {
if stateDir == "" {
stateDir = defaultStateDir
}
files, err := filepath.Glob(filepath.Join(stateDir, "*.json"))
if err != nil {
return nil, err
}
var vms []*VMInstance
for _, file := range files {
vm, err := LoadVMState(file)
if err != nil {
fmt.Printf("⚠️ Erreur chargement VM %s: %v\n", file, err)
continue
}
vms = append(vms, vm)
}
return vms, nil
}
Configurable via YAML :
```yaml
database:
path: "/var/lib/two/data/"
```
```go
vm, _ := StartVM(cfg)
_ = vm.SaveState("") // "" = default dir
vm.StartMonitorLoop()
## Package `pkg/db/kv`
| Fonction | Signature | Comportement |
|---|---|---|
| `InitDB` | `InitDB(conf Config, readonly bool) *badger.DB` | Ouvre la base (supporte mode lecture seule) |
| `AddInDB` | `AddInDB(db *badger.DB, key, value string) error` | Écrit ou écrase une clé |
| `GetFromDB` | `GetFromDB(db *badger.DB, key string) (string, error)` | Lit une clé, retourne `badger.ErrKeyNotFound` si absente |
| `DeleteInDB` | `DeleteInDB(db *badger.DB, key string) error` | Supprime récursivement une clé et tous ses sous-préfixes |
| `ListByPrefix` | `ListByPrefix(db *badger.DB, prefix string) (map[string]string, error)` | Liste toutes les clés commençant par un préfixe |
Un serveur HTTP d'inspection est disponible via `kv.NewAdminServer` (`GET /db?prefix=...`).
## Structure des clés
### VPC
```
vpc/<vpc-name>/state → "creating" | "created" | "deleting" | "deleted"
```
#### Restaurer toutes les VMs au démarrage :
```go
func RestoreVMsOnStartup() {
vms, err := LoadAllVMs("")
if err != nil {
log.Fatalf("Failed to load saved VMs: %v", err)
}
for _, vm := range vms {
if vm.IsAlive() {
vm.StartMonitorLoop()
log.Printf("✅ Reattached to running VM: %s (PID %d)", vm.ID, vm.Pid)
} else {
log.Printf("⚠️ VM %s is dead, needs cleanup or recovery", vm.ID)
}
}
}
### Subnet
```
subnet/<subnet-name>/state → "creating" | "created" | "deleting" | "deleted"
subnet/<subnet-name>/vpc → <vpc-name>
subnet/<subnet-name>/vxlan_id → <int>
subnet/<subnet-name>/local_iface → <bridge-name>
subnet/<subnet-name>/gateway_ip → <IP>
subnet/<subnet-name>/cidr → <CIDR>
```
il va manquer une fonction `RemoveState(vmID)` après un `CleanupVM`
### VM
```
vm/<vm-name>/state → "starting" | "started" | "stopping" | "stopped"
vm/<vm-name>/subnet → <subnet-name>
vm/<vm-name>/ip → <IP>
vm/<vm-name>/metadata_port → <port>
vm/<vm-name>/volume_path → <path>
vm/<vm-name>/memory → <int MB>
vm/<vm-name>/cpus → <int>
vm/<vm-name>/tap_id → <int>
vm/<vm-name>/password → <password> (optionnel)
vm/<vm-name>/sshkey → <pubkey> (optionnel)
```
## Cycle de vie d'une VM
**Démarrage** (`StartVMCommand`) :
1. `Prepare` : vérifie que la VM n'existe pas → écrit toutes les clés avec état `"starting"`
2. `Execute` : attend que le subnet soit `"created"` → lance la VM → passe à `"started"`
**Arrêt** (`StopVMCommand`) :
1. `Prepare` : vérifie que la VM existe → passe à `"stopping"`
2. `Execute` : arrête la VM → passe à `"stopped"``DeleteInDB("vm/<name>")` (supprime toutes les sous-clés)
## États transitoires et pannes
Il n'y a pas de restauration automatique au démarrage. Si l'agent s'arrête pendant une opération, la base conserve l'état intermédiaire (ex: `"starting"`, `"creating"`). Une intervention externe (appel API) est nécessaire pour nettoyer ou relancer.

@ -1,129 +1,99 @@
# A simple event queue
# Dispatcher & Queue de commandes
## 🧱 Design simple en Go
L'agent traite les opérations (création/suppression de VPC, subnet, VM) via un pattern
**Command** + **Dispatcher** + **Queue de workers**.
### 🎛️ Structure `Event`
```go
type EventType string
const (
EventStart EventType = "start"
EventStop EventType = "stop"
EventReboot EventType = "reboot"
EventAttach EventType = "attach-disk"
EventDetach EventType = "detach-disk"
)
type VMEvent struct {
Type EventType
VMID string
Params map[string]string // optionnel selon le type
}
```
---
### 📬 EventQueue (thread-safe FIFO + notify)
```go
type EventQueue struct {
queue []VMEvent
lock sync.Mutex
signal chan struct{}
}
func NewEventQueue() *EventQueue {
return &EventQueue{
queue: make([]VMEvent, 0),
signal: make(chan struct{}, 1),
}
}
func (eq *EventQueue) Push(ev VMEvent) {
eq.lock.Lock()
defer eq.lock.Unlock()
eq.queue = append(eq.queue, ev)
select {
case eq.signal <- struct{}{}:
default:
// si déjà notify, pas besoin de spammer
}
}
func (eq *EventQueue) Pop() (VMEvent, bool) {
eq.lock.Lock()
defer eq.lock.Unlock()
if len(eq.queue) == 0 {
return VMEvent{}, false
}
ev := eq.queue[0]
eq.queue = eq.queue[1:]
return ev, true
}
```
---
### 🔁 Boucle d'exécution FIFO
```go
func StartEventLoop(eq *EventQueue, manager *Manager) {
go func() {
for {
<-eq.signal // bloquant jusqu'à ce quun event arrive
for {
ev, ok := eq.Pop()
if !ok {
break
}
log.Printf("⏳ Processing event %s for VM %s", ev.Type, ev.VMID)
switch ev.Type {
case EventStart:
manager.StartVMByID(ev.VMID)
case EventStop:
manager.StopVMByID(ev.VMID)
case EventAttach:
manager.AttachDisk(ev.VMID, ev.Params["path"])
case EventDetach:
manager.DetachDisk(ev.VMID, ev.Params["path"])
default:
log.Printf("⚠️ Unknown event type: %s", ev.Type)
}
}
}
}()
}
```
---
### 🌐 Exposition via HTTP (exemple minimal)
```go
func handleStartVM(w http.ResponseWriter, r *http.Request, eq *EventQueue) {
vmID := r.URL.Query().Get("id")
if vmID == "" {
http.Error(w, "missing id", 400)
return
}
eq.Push(VMEvent{Type: EventStart, VMID: vmID})
fmt.Fprintf(w, "Start request enqueued")
}
```
---
## 🔔 Résumé de la comm' inter-composants
## Vue d'ensemble
```text
[API HTTP] --(Push VMEvent)--> [EventQueue] --(notify chan)--> [EventLoop]
|
[Pop / Switch exec based on event type]
[Handler HTTP]
├── Prepare(cmd) → validation + écriture état initial en DB (synchrone)
│ retourne 4xx si la validation échoue
├── Dispatch(cmd) → enfile cmd.Execute() dans la queue (asynchrone)
└── 202 Accepted → retourné immédiatement au client
[Worker goroutine]
└── consomme la queue → exécute cmd.Execute()
```
## Interface `Command`
```go
type Command interface {
Prepare(db *badger.DB, cfg *configuration.Config) error
Execute(db *badger.DB, cfg *configuration.Config) error
}
```
- **`Prepare`** : valide les prérequis, écrit l'état initial en DB (`"creating"`, `"starting"`…)
- **`Execute`** : effectue l'opération réelle (appels système, réseau, QEMU…)
## Queue (`pkg/worker`)
```go
type Task func()
type Queue struct { tasks chan Task }
func New(bufferSize int) *Queue
func (q *Queue) Submit(t Task) // enfile une tâche
func (q *Queue) Start(n int) // lance n goroutines workers
```
La taille du buffer et le nombre de workers sont configurables :
```yaml
worker:
buffer_size: 1000
count: 4
```
## Dispatcher (`internal/dispatcher/agent`)
```go
type Dispatcher struct { /* queue, db, cfg, logger */ }
func (d *Dispatcher) Prepare(cmd Command) error // appelle cmd.Prepare()
func (d *Dispatcher) Dispatch(cmd Command) // enfile cmd.Execute() + log durée
```
## Commandes disponibles
| Commande | Fichier | États écrits |
|---|---|---|
| `CreateVPCCommand` | `vpc_commands.go` | `creating``created` |
| `DeleteVPCCommand` | `vpc_commands.go` | `deleting``deleted` |
| `CreateSubnetCommand` | `subnet_commands.go` | `creating``created` |
| `DeleteSubnetCommand` | `subnet_commands.go` | `deleting``deleted` |
| `StartVMCommand` | `vm_commands.go` | `starting``started` |
| `StopVMCommand` | `vm_commands.go` | `stopping``stopped` |
## Dépendances entre commandes
Certaines commandes attendent via polling que leurs dépendances soient prêtes :
- `StartVMCommand.Execute` attend `subnet/<name>/state == "created"`
- `DeleteVPCCommand.Execute` attend que tous ses subnets soient `"deleted"`
Le timeout est configurable :
```yaml
dispatcher:
timeout_seconds: 120
poll_seconds: 2
```
## Exemple de flux : POST /vpcs
```text
1. Handler parse la requête → crée CreateVPCCommand{Name: "my-vpc"}
2. Prepare : vérifie que "vpc/my-vpc/state" n'existe pas
écrit "vpc/my-vpc/state" = "creating"
3. Dispatch : enfile Execute() dans la queue
4. Handler : retourne 202 {"name": "my-vpc", "state": "creating"}
5. Worker : exécute vpc.CreateVPC(db, "my-vpc")
passe l'état à "created"
```

183
vnc.md Normal file

@ -0,0 +1,183 @@
# 📄 1. Résumé architecture (Markdown)
```markdown
# Architecture VNC over WebSocket avec multiplexage Unix socket
## Vue densemble
Cette architecture permet dexposer des consoles VNC de machines virtuelles QEMU via WebSocket (compatible noVNC), en utilisant des sockets Unix pour renforcer la sécurité et simplifier le réseau.
```
[ Browser (noVNC) ]
↓ HTTPS / WebSocket
[ Caddy (Global, piloté par orchestrateur) ]
[ VNC Gateway (par host) ]
[ Unix socket VNC ]
[ VM QEMU ]
````
---
## Composants
### 1. QEMU (serveur VNC)
Chaque VM expose son interface VNC via un socket Unix :
```bash
qemu-system-x86_64 -vnc unix:/run/vnc/<vm-id>.sock
````
Caractéristiques :
* Aucun port TCP exposé
* Isolation forte par VM
* Accès restreint via permissions filesystem
---
### 2. Organisation des sockets
```
/run/vnc/
├── vm-1.sock
├── vm-2.sock
└── vm-3.sock
```
Le nom du fichier correspond directement à lidentifiant de la VM (`vm-id`).
---
### 3. VNC Gateway (multiplexeur)
Service unique par host.
#### Fonction
* Écoute sur un port TCP unique (ex: `:9000`)
* Accepte des connexions WebSocket
* Route dynamiquement vers le bon socket Unix
#### Routing implicite
```
/vnc/<vm-id> → /run/vnc/<vm-id>.sock
```
#### Flux
```
WebSocket ⇄ Gateway ⇄ Unix socket ⇄ VNC (QEMU)
```
---
### 4. Caddy (reverse proxy global)
Configuré dynamiquement par lorchestrateur.
#### Rôle
* Terminaison TLS
* Routage vers le bon host
* Basé sur le `vm-id` dans lURL
#### Exemple
```
https://example.com/vnc/vm-123
```
→ routé vers :
```
http://host-A:9000
```
---
### 5. Orchestrateur
Responsable du mapping :
```
vm-id → host
```
Fonctions :
* allocation des VMs
* suivi des hosts
* configuration dynamique de Caddy
---
## Flux complet
```
1. Client ouvre noVNC
2. Connexion WebSocket :
wss://example.com/vnc/<vm-id>
3. Caddy :
→ route vers le bon host
4. Gateway :
→ ouvre /run/vnc/<vm-id>.sock
5. Communication VNC établie
```
---
## Sécurité
### Niveau système
* sockets Unix avec permissions restreintes
* aucun port VNC exposé
### Gateway
* validation stricte du `vm-id`
* prévention path traversal
### Edge (Caddy)
* TLS obligatoire
* authentification possible (JWT, session)
---
## Avantages
* **Pas de ports dynamiques**
* **Multiplexage efficace**
* **Isolation forte (Unix sockets)**
* **Scalabilité horizontale**
* **Simplicité réseau**
* **Compatibilité native avec noVNC**
---
## Points de vigilance
* nettoyage des sockets après arrêt VM
* gestion des timeouts WebSocket
* limites système (file descriptors)
* validation stricte des identifiants
---
## Extension possibles
* multi-tenant (`/run/vnc/<tenant>/<vm>.sock`)
* authentification intégrée gateway
* métriques (connexions actives)
* autoscaling des hosts