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
This commit is contained in:
@@ -0,0 +1,758 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user