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

759 lines
19 KiB
Markdown

# Namespace + VRF Implementation Guide
## Overview
This document provides a step-by-step implementation guide for the Namespace + VRF architecture in NexusGuard.
## Prerequisites
### System Requirements
```bash
# 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
```bash
# 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
```bash
cd apps/server-core
touch internal/models/migration_vrf.go
```
### Step 1.2: Define Models
```go
// 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
```go
// 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
```go
// 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
```bash
cd apps/server-core
go run -tags dev . -migrate
```
## Phase 2: Namespace Manager
### Step 2.1: Create Namespace Manager
```go
// 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
```go
// 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
```go
// 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
```go
// 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
```go
// 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
```go
// 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
```go
// 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
```bash
# 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
```bash
cd apps/server-core
CGO_ENABLED=0 go build -o ../../bin/server-core .
```
### Step 2: Deploy
```bash
# 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
```bash
# 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
```bash
# 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
```bash
ssh root@172.20.8.191 "systemctl restart nexusguard-server"
```
---
**Status:** Implementation guide — ready for development
**Last Updated:** 2026-07-10
**Author:** NexusGuard Team