Files
datadunia e523398fa8 feat: add ESP32 embedded agent + namespace VRF reference docs + update android-agent submodule
- apps/device-agent-embedded/: ESP32 WireGuard agent (C, ESP-IDF, mbedTLS)
- reference/namespace-vrf/: Linux namespace/VRF network isolation docs
- apps/android-agent: VPN permission check, StartResult sealed class
2026-09-13 14:39:24 +07:00

19 KiB

Namespace + VRF Implementation Guide

Overview

This document provides a step-by-step implementation guide for the Namespace + VRF architecture in NexusGuard.

Prerequisites

System Requirements

# Check kernel version (need 4.3+ for VRF)
uname -r

# Check VRF support
modprobe vrf

# Check namespace support
ip netns list

# Install required packages
apt install -y iproute2 nftables wireguard-tools

Development Environment

# Go 1.25+
go version

# PostgreSQL 15+
psql --version

# Node.js 24+ (for dashboard)
node --version

Phase 1: Database Schema

Step 1.1: Create Migration File

cd apps/server-core
touch internal/models/migration_vrf.go

Step 1.2: Define Models

// internal/models/namespace.go

package models

import (
    "time"
    "github.com/google/uuid"
    "gorm.io/gorm"
)

type Namespace struct {
    ID        uuid.UUID      `json:"id" gorm:"type:uuid;primaryKey"`
    Name      string         `json:"name" gorm:"uniqueIndex;not null;size:64"`
    OwnerID   uuid.UUID      `json:"owner_id" gorm:"type:uuid;not null;index"`
    Owner     User           `json:"owner" gorm:"foreignKey:OwnerID"`
    Users     []NamespaceUser `json:"users" gorm:"foreignKey:NamespaceID"`
    VRFs      []VRF          `json:"vrfs" gorm:"foreignKey:NamespaceID"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
}

func (n *Namespace) BeforeCreate(tx *gorm.DB) (err error) {
    if n.ID == uuid.Nil {
        n.ID = uuid.New()
    }
    return
}

type NamespaceUser struct {
    ID            uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
    NamespaceID   uuid.UUID `json:"namespace_id" gorm:"type:uuid;not null;index"`
    Namespace     Namespace `json:"namespace" gorm:"foreignKey:NamespaceID"`
    UserID        uuid.UUID `json:"user_id" gorm:"type:uuid;not null;index"`
    User          User      `json:"user" gorm:"foreignKey:UserID"`
    Role          string    `json:"role" gorm:"not null;size:16;default:'viewer'"` // admin, operator, viewer
    CreatedAt     time.Time `json:"created_at"`
    UpdatedAt     time.Time `json:"updated_at"`
}

func (n *NamespaceUser) BeforeCreate(tx *gorm.DB) (err error) {
    if n.ID == uuid.Nil {
        n.ID = uuid.New()
    }
    return
}

type VRF struct {
    ID            uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
    NamespaceID   uuid.UUID `json:"namespace_id" gorm:"type:uuid;not null;index"`
    Namespace     Namespace `json:"namespace" gorm:"foreignKey:NamespaceID"`
    Name          string    `json:"name" gorm:"not null;size:64"`
    TableID       int       `json:"table_id" gorm:"not null"`
    CreatedAt     time.Time `json:"created_at"`
    UpdatedAt     time.Time `json:"updated_at"`
}

func (v *VRF) BeforeCreate(tx *gorm.DB) (err error) {
    if v.ID == uuid.Nil {
        v.ID = uuid.New()
    }
    return
}

Step 1.3: Update WgServer Model

// internal/models/models.go

type WgServer struct {
    // ... existing fields
    
    // VRF fields
    NamespaceID *uuid.UUID `json:"namespace_id" gorm:"type:uuid;index"`
    Namespace   *Namespace `json:"namespace" gorm:"foreignKey:NamespaceID"`
    VRFID       *uuid.UUID `json:"vrf_id" gorm:"type:uuid;index"`
    VRF         *VRF       `json:"vrf" gorm:"foreignKey:VRFID"`
}

Step 1.4: Create Migration

// internal/models/migration_vrf.go

package models

import "gorm.io/gorm"

func MigrateVRF(db *gorm.DB) error {
    // Create namespaces table
    if err := db.Exec(`
        CREATE TABLE IF NOT EXISTS namespaces (
            id UUID PRIMARY KEY,
            name VARCHAR(64) UNIQUE NOT NULL,
            owner_id UUID NOT NULL REFERENCES users(id),
            created_at TIMESTAMP DEFAULT NOW(),
            updated_at TIMESTAMP DEFAULT NOW()
        )
    `).Error; err != nil {
        return err
    }

    // Create namespace_users table
    if err := db.Exec(`
        CREATE TABLE IF NOT EXISTS namespace_users (
            id UUID PRIMARY KEY,
            namespace_id UUID NOT NULL REFERENCES namespaces(id) ON DELETE CASCADE,
            user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
            role VARCHAR(16) NOT NULL DEFAULT 'viewer',
            created_at TIMESTAMP DEFAULT NOW(),
            updated_at TIMESTAMP DEFAULT NOW(),
            UNIQUE(namespace_id, user_id)
        )
    `).Error; err != nil {
        return err
    }

    // Create vrfs table
    if err := db.Exec(`
        CREATE TABLE IF NOT EXISTS vrfs (
            id UUID PRIMARY KEY,
            namespace_id UUID NOT NULL REFERENCES namespaces(id) ON DELETE CASCADE,
            name VARCHAR(64) NOT NULL,
            table_id INTEGER NOT NULL,
            created_at TIMESTAMP DEFAULT NOW(),
            updated_at TIMESTAMP DEFAULT NOW(),
            UNIQUE(namespace_id, name)
        )
    `).Error; err != nil {
        return err
    }

    // Add VRF fields to wg_servers
    if err := db.Exec(`
        ALTER TABLE wg_servers 
        ADD COLUMN IF NOT EXISTS namespace_id UUID REFERENCES namespaces(id),
        ADD COLUMN IF NOT EXISTS vrf_id UUID REFERENCES vrfs(id)
    `).Error; err != nil {
        return err
    }

    return nil
}

Step 1.5: Run Migration

cd apps/server-core
go run -tags dev . -migrate

Phase 2: Namespace Manager

Step 2.1: Create Namespace Manager

// internal/namespace/manager.go

package namespace

import (
    "fmt"
    "os/exec"
    
    "github.com/google/uuid"
    "git.datadunia.com/nexusguard/nexus-server-core/internal/models"
    "gorm.io/gorm"
)

type Manager struct {
    db *gorm.DB
}

func NewManager(db *gorm.DB) *Manager {
    return &Manager{db: db}
}

func (m *Manager) Create(name string, ownerID uuid.UUID) (*models.Namespace, error) {
    // Create namespace in database
    ns := &models.Namespace{
        Name:    name,
        OwnerID: ownerID,
    }
    if err := m.db.Create(ns).Error; err != nil {
        return nil, err
    }

    // Create Linux network namespace
    cmd := exec.Command("ip", "netns", "add", "ns_"+name)
    if err := cmd.Run(); err != nil {
        return nil, fmt.Errorf("failed to create network namespace: %w", err)
    }

    // Add owner as admin user
    nsUser := &models.NamespaceUser{
        NamespaceID: ns.ID,
        UserID:      ownerID,
        Role:        "admin",
    }
    if err := m.db.Create(nsUser).Error; err != nil {
        return nil, err
    }

    return ns, nil
}

func (m *Manager) Delete(name string) error {
    // Delete from database
    if err := m.db.Where("name = ?", name).Delete(&models.Namespace{}).Error; err != nil {
        return err
    }

    // Delete Linux network namespace
    cmd := exec.Command("ip", "netns", "del", "ns_"+name)
    if err := cmd.Run(); err != nil {
        return fmt.Errorf("failed to delete network namespace: %w", err)
    }

    return nil
}

func (m *Manager) Get(name string) (*models.Namespace, error) {
    var ns models.Namespace
    if err := m.db.Where("name = ?", name).First(&ns).Error; err != nil {
        return nil, err
    }
    return &ns, nil
}

func (m *Manager) List() ([]models.Namespace, error) {
    var namespaces []models.Namespace
    if err := m.db.Find(&namespaces).Error; err != nil {
        return nil, err
    }
    return namespaces, nil
}

func (m *Manager) AddUser(namespaceID, userID uuid.UUID, role string) error {
    nsUser := &models.NamespaceUser{
        NamespaceID: namespaceID,
        UserID:      userID,
        Role:        role,
    }
    return m.db.Create(nsUser).Error
}

func (m *Manager) RemoveUser(namespaceID, userID uuid.UUID) error {
    return m.db.Where("namespace_id = ? AND user_id = ?", namespaceID, userID).
        Delete(&models.NamespaceUser{}).Error
}

func (m *Manager) UpdateUserRole(namespaceID, userID uuid.UUID, role string) error {
    return m.db.Model(&models.NamespaceUser{}).
        Where("namespace_id = ? AND user_id = ?", namespaceID, userID).
        Update("role", role).Error
}

Step 2.2: Create Namespace API Handler

// api/namespaces.go

package api

import (
    "net/http"
    
    "github.com/gin-gonic/gin"
    "github.com/google/uuid"
    "git.datadunia.com/nexusguard/nexus-server-core/internal/namespace"
)

type NamespacesHandler struct {
    mgr *namespace.Manager
}

func NewNamespacesHandler(mgr *namespace.Manager) *NamespacesHandler {
    return &NamespacesHandler{mgr: mgr}
}

type CreateNamespaceRequest struct {
    Name string `json:"name" binding:"required"`
}

func (h *NamespacesHandler) Create(c *gin.Context) {
    var req CreateNamespaceRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    ownerID, _ := c.Get("userID")
    
    ns, err := h.mgr.Create(req.Name, ownerID.(uuid.UUID))
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusCreated, ns)
}

func (h *NamespacesHandler) List(c *gin.Context) {
    namespaces, err := h.mgr.List()
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, namespaces)
}

func (h *NamespacesHandler) Get(c *gin.Context) {
    name := c.Param("name")
    ns, err := h.mgr.Get(name)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "namespace not found"})
        return
    }
    c.JSON(http.StatusOK, ns)
}

func (h *NamespacesHandler) Delete(c *gin.Context) {
    name := c.Param("name")
    if err := h.mgr.Delete(name); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"status": "deleted"})
}

type AddUserRequest struct {
    UserID string `json:"user_id" binding:"required"`
    Role   string `json:"role" binding:"required"`
}

func (h *NamespacesHandler) AddUser(c *gin.Context) {
    name := c.Param("name")
    
    var req AddUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    ns, err := h.mgr.Get(name)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "namespace not found"})
        return
    }

    userID, err := uuid.Parse(req.UserID)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user_id"})
        return
    }

    if err := h.mgr.AddUser(ns.ID, userID, req.Role); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{"status": "added"})
}

func (h *NamespacesHandler) RemoveUser(c *gin.Context) {
    name := c.Param("name")
    userIDStr := c.Param("user_id")

    ns, err := h.mgr.Get(name)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "namespace not found"})
        return
    }

    userID, err := uuid.Parse(userIDStr)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user_id"})
        return
    }

    if err := h.mgr.RemoveUser(ns.ID, userID); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{"status": "removed"})
}

Step 2.3: Register API Routes

// main.go

func main() {
    // ... existing code ...
    
    // Create namespace manager
    nsMgr := namespace.NewManager(db)
    nsHandler := api.NewNamespacesHandler(nsMgr)
    
    // Register namespace API routes
    v1 := router.Group("/api/v1")
    {
        // Namespace management (admin only)
        v1.POST("/namespaces", nsHandler.Create)
        v1.GET("/namespaces", nsHandler.List)
        v1.GET("/namespaces/:name", nsHandler.Get)
        v1.DELETE("/namespaces/:name", nsHandler.Delete)
        
        // Namespace user management
        v1.POST("/namespaces/:name/users", nsHandler.AddUser)
        v1.DELETE("/namespaces/:name/users/:user_id", nsHandler.RemoveUser)
    }
}

Phase 3: VRF Manager

Step 3.1: Create VRF Manager

// internal/vrf/manager.go

package vrf

import (
    "fmt"
    "os/exec"
    
    "github.com/google/uuid"
    "git.datadunia.com/nexusguard/nexus-server-core/internal/models"
    "gorm.io/gorm"
)

type Manager struct {
    db *gorm.DB
}

func NewManager(db *gorm.DB) *Manager {
    return &Manager{db: db}
}

func (m *Manager) Create(namespaceID uuid.UUID, name string) (*models.VRF, error) {
    // Get next available table ID
    var maxTableID int
    m.db.Model(&models.VRF{}).
        Where("namespace_id = ?", namespaceID).
        Select("COALESCE(MAX(table_id), 99)").
        Scan(&maxTableID)
    tableID := maxTableID + 1

    // Create VRF in database
    vrf := &models.VRF{
        NamespaceID: namespaceID,
        Name:        name,
        TableID:     tableID,
    }
    if err := m.db.Create(vrf).Error; err != nil {
        return nil, err
    }

    // Get namespace name for Linux command
    var ns models.Namespace
    if err := m.db.First(&ns, namespaceID).Error; err != nil {
        return nil, err
    }

    // Create VRF in Linux namespace
    vrfName := fmt.Sprintf("vrf:%s", name)
    cmd := exec.Command("ip", "netns", "exec", "ns_"+ns.Name,
        "ip", "link", "add", vrfName, "type", "vrf", "table", fmt.Sprint(tableID))
    if err := cmd.Run(); err != nil {
        return nil, fmt.Errorf("failed to create VRF: %w", err)
    }

    // Up VRF
    cmd = exec.Command("ip", "netns", "exec", "ns_"+ns.Name,
        "ip", "link", "set", vrfName, "up")
    if err := cmd.Run(); err != nil {
        return nil, fmt.Errorf("failed to up VRF: %w", err)
    }

    return vrf, nil
}

func (m *Manager) Delete(namespaceID uuid.UUID, name string) error {
    // Get namespace name
    var ns models.Namespace
    if err := m.db.First(&ns, namespaceID).Error; err != nil {
        return err
    }

    // Delete VRF from Linux namespace
    vrfName := fmt.Sprintf("vrf:%s", name)
    cmd := exec.Command("ip", "netns", "exec", "ns_"+ns.Name,
        "ip", "link", "del", vrfName)
    if err := cmd.Run(); err != nil {
        return fmt.Errorf("failed to delete VRF: %w", err)
    }

    // Delete from database
    return m.db.Where("namespace_id = ? AND name = ?", namespaceID, name).
        Delete(&models.VRF{}).Error
}

func (m *Manager) Get(namespaceID uuid.UUID, name string) (*models.VRF, error) {
    var vrf models.VRF
    if err := m.db.Where("namespace_id = ? AND name = ?", namespaceID, name).
        First(&vrf).Error; err != nil {
        return nil, err
    }
    return &vrf, nil
}

func (m *Manager) List(namespaceID uuid.UUID) ([]models.VRF, error) {
    var vrfs []models.VRF
    if err := m.db.Where("namespace_id = ?", namespaceID).Find(&vrfs).Error; err != nil {
        return nil, err
    }
    return vrfs, nil
}

Phase 4: WireGuard Integration

Step 4.1: Update WireGuard Manager

// internal/wgmanager/manager.go

type UpConfig struct {
    // ... existing fields
    NamespaceName string // e.g., "zerocode"
    VRFName       string // e.g., "main"
}

Step 4.2: Update WireGuard Linux Implementation

// internal/wgmanager/wgmanager_linux.go

func (m *LinuxWgManager) Up(cfg UpConfig) error {
    // 1. Create WG interface in namespace
    if cfg.NamespaceName != "" {
        // Create in namespace
        cmd := exec.Command("ip", "netns", "exec", "ns_"+cfg.NamespaceName,
            "ip", "link", "add", cfg.InterfaceName, "type", "wireguard")
        if err := cmd.Run(); err != nil {
            return fmt.Errorf("failed to create WG interface: %w", err)
        }
    } else {
        // Create in default namespace (existing code)
        // ...
    }

    // 2. Bind to VRF (if specified)
    if cfg.VRFName != "" && cfg.NamespaceName != "" {
        vrfName := fmt.Sprintf("vrf:%s", cfg.VRFName)
        cmd := exec.Command("ip", "netns", "exec", "ns_"+cfg.NamespaceName,
            "ip", "link", "set", cfg.InterfaceName, "vrf", vrfName)
        if err := cmd.Run(); err != nil {
            return fmt.Errorf("failed to bind to VRF: %w", err)
        }
    }

    // 3. Configure WG (existing code, but in namespace)
    // ...

    return nil
}

Phase 5: Testing

Step 5.1: Unit Tests

// internal/namespace/manager_test.go

package namespace

import (
    "testing"
    
    "github.com/google/uuid"
    "git.datadunia.com/nexusguard/nexus-server-core/internal/models"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

func TestCreateNamespace(t *testing.T) {
    // Setup test database
    db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
    if err != nil {
        t.Fatal(err)
    }
    
    // Auto migrate
    db.AutoMigrate(&models.User{}, &models.Namespace{}, &models.NamespaceUser{})
    
    // Create manager
    mgr := NewManager(db)
    
    // Create test user
    user := &models.User{Username: "testuser"}
    db.Create(user)
    
    // Create namespace
    ns, err := mgr.Create("testns", user.ID)
    if err != nil {
        t.Fatalf("failed to create namespace: %v", err)
    }
    
    // Verify
    if ns.Name != "testns" {
        t.Errorf("expected name 'testns', got '%s'", ns.Name)
    }
    
    // Cleanup
    mgr.Delete("testns")
}

Step 5.2: Integration Tests

# Test namespace creation
curl -X POST http://localhost:8080/api/v1/namespaces \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"name": "testns"}'

# Test VRF creation
curl -X POST http://localhost:8080/api/v1/ns/testns/vrfs \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"name": "main"}'

# Test server creation in namespace
curl -X POST http://localhost:8080/api/v1/ns/testns/servers \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"name": "node1", "public_endpoint": "1.2.3.4:51820"}'

Deployment

Step 1: Build

cd apps/server-core
CGO_ENABLED=0 go build -o ../../bin/server-core .

Step 2: Deploy

# Copy binary to server
scp ../../bin/server-core root@172.20.8.191:/usr/local/bin/nexusguard-server

# Restart service
ssh root@172.20.8.191 "systemctl restart nexusguard-server"

Step 3: Verify

# Check namespaces
ip netns list

# Check VRFs in namespace
ip netns exec ns_testns ip link show type vrf

# Check WireGuard in namespace
ip netns exec ns_testns wg show

Rollback

Step 1: Disable VRF

# Update database
psql -U nexusguard -d nexusguard -c "
UPDATE wg_servers SET 
    namespace_id = NULL, 
    vrf_id = NULL;
"

# Delete namespaces
ip netns list | grep ns_ | xargs -I {} ip netns del {}

Step 2: Restart Service

ssh root@172.20.8.191 "systemctl restart nexusguard-server"

Status: Implementation guide — ready for development Last Updated: 2026-07-10 Author: NexusGuard Team