Compare commits
193 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88fa60901e | |||
| f95763ae42 | |||
| 45d2e51062 | |||
| 7c9110ce9c | |||
| 115019846e | |||
| d7d471308a | |||
| 8654505933 | |||
| 482cabce93 | |||
| 13a7bfef1e | |||
| 326e0774a8 | |||
| d61c470b97 | |||
| 52fc7c749c | |||
| 5d60c7d160 | |||
| 8cadfc4657 | |||
| 5d3761c56c | |||
| b04d47a1bf | |||
| 59a8436a45 | |||
| 2fb3109b9f | |||
| 84689b208e | |||
| 4af46dadad | |||
| 5ed8e54f16 | |||
| 1124cd8159 | |||
| c204f75978 | |||
| 8719535f55 | |||
| dd3aaafffb | |||
| 934dc291f1 | |||
| be5178e04f | |||
| 6cb4e53598 | |||
| 68203f2eb3 | |||
| 24e83ee8cd | |||
| 7cce8c39a6 | |||
| ca4107e2bc | |||
| 192b753f5d | |||
| 4eea268ec5 | |||
| fcb1de2cac | |||
| 4df2e0aeeb | |||
| 2426a7b42c | |||
| d6610f82e9 | |||
| 80ba316d4a | |||
| 7a43ec8f2d | |||
| 05d496432a | |||
| d6c53f6f86 | |||
| a485c8df42 | |||
| f98eedbfbe | |||
| ea6fbe83b5 | |||
| 91ed1adcda | |||
| 95ec25b80e | |||
| 2738e3c7d1 | |||
| 80c97bbc22 | |||
| 077384c433 | |||
| fc204bba17 | |||
| e1e35318de | |||
| bd79bb0043 | |||
| 248ad123bb | |||
| 855fb2dbee | |||
| 45f95a957a | |||
| e50c1900ad | |||
| 957cc8d5cb | |||
| da94cc3f6b | |||
| 465b1f7388 | |||
| e9e4db261d | |||
| 6f6b44b8a2 | |||
| 1a1f8a5a8a | |||
| 61a5936224 | |||
| f03686ac3d | |||
| e1cbdb66c8 | |||
| a89fdaf435 | |||
| 633e06f556 | |||
| b447ad9757 | |||
| d7bbbbdefd | |||
| b1458d3a99 | |||
| 2c5a3c7fa5 | |||
| 5a6675c70a | |||
| 4528493647 | |||
| 9f1e37fc57 | |||
| 5e8b72efdc | |||
| 08b74ce45b | |||
| 7c5604d510 | |||
| 26e65006b5 | |||
| c011215deb | |||
| 6f5ffb8b1d | |||
| 934d4a03d7 | |||
| 27c003864e | |||
| 014309c390 | |||
| d5b1f4428a | |||
| 6c5a762407 | |||
| 9c8a3c0751 | |||
| 272d744e12 | |||
| 5304bd11ca | |||
| 359a23c079 | |||
| facd8bd7d5 | |||
| 83722abd32 | |||
| 359bd6f9c4 | |||
| e81c1dd448 | |||
| 9433dc3547 | |||
| 4d58b01641 | |||
| 4e19c371d0 | |||
| aa620dec6a | |||
| 48340aa7a3 | |||
| 68ebfbaad3 | |||
| 46ff7ca737 | |||
| 08b152a370 | |||
| 6f22aad1ad | |||
| d21863ce8f | |||
| eba741cd97 | |||
| fd9da1fd71 | |||
| d46bc11485 | |||
| f1fa0acfa7 | |||
| a5c1ea1c1b | |||
| 4dbc0bd9f3 | |||
| 94059ee9e6 | |||
| 06d8876b04 | |||
| 90b3baa2fb | |||
| 71224721d3 | |||
| 56f44f5b95 | |||
| 08ac26c698 | |||
| ac0755977c | |||
| c1490421ba | |||
| 2d366c4619 | |||
| dad057528c | |||
| 8ae9149620 | |||
| 9d6c2abb9f | |||
| 4c9e54aaa3 | |||
| 290af48007 | |||
| 9a2212d691 | |||
| 6f57540490 | |||
| 8c1b15a78e | |||
| 52fb2bb5d3 | |||
| 2ce9dbd4e8 | |||
| f85db30c2e | |||
| fc9afd1720 | |||
| c23e0829cd | |||
| 7b102c94fb | |||
| 5fb815a3c1 | |||
| d9bf8d5616 | |||
| e45b79c2a8 | |||
| c3177eb4e4 | |||
| 6141c2b556 | |||
| 8f589901f2 | |||
| 10a9ae2eac | |||
| 53efeab1b3 | |||
| cc0f01b6ae | |||
| 00a0bf661c | |||
| 6a27007b53 | |||
| 2944ca0947 | |||
| 2cfc7408a4 | |||
| 4c8fe3c8d6 | |||
| 8dd5834be2 | |||
| 9461d6b385 | |||
| c0f18a41a1 | |||
| 96248d3d31 | |||
| bfb6dcc42f | |||
| 9470435704 | |||
| c37d4a751c | |||
| 072aa48db0 | |||
| 47ca075bc0 | |||
| 8938d842db | |||
| 710cc6537e | |||
| 224a84e856 | |||
| 062e82bd97 | |||
| 43fd852b3e | |||
| 946334ffc7 | |||
| 94ed6a8184 | |||
| 5b5a4aa009 | |||
| 0a22ecd13f | |||
| 56d70ed031 | |||
| 3133671c02 | |||
| 5fa9f414b0 | |||
| 233f14f998 | |||
| c5364cf5ba | |||
| 60d326880a | |||
| 13e8787553 | |||
| 5795677fd1 | |||
| 7cae9dacf0 | |||
| a745e84470 | |||
| 68a6270b8c | |||
| 6b47f9af17 | |||
| bd93d37645 | |||
| 2e7a6aba90 | |||
| 307cc34752 | |||
| 9aaa9ebfb7 | |||
| ad52b39ed7 | |||
| d9d99e7827 | |||
| 736319645e | |||
| 8e04f33fc3 | |||
| 53df67f085 | |||
| ca52c3bde4 | |||
| 78fcce0d28 | |||
| a4a06593b1 | |||
| e7b1f22b44 | |||
| cbacfea7f2 | |||
| 281ac48d28 | |||
| c2f0e53f1d |
@@ -1,16 +0,0 @@
|
|||||||
name: Beta Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*-beta*'
|
|
||||||
- 'v*-test*'
|
|
||||||
- 'dev-*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
if: "contains(gitea.ref_name, 'beta') || contains(gitea.ref_name, 'test') || startsWith(gitea.ref_name, 'dev-')"
|
|
||||||
uses: ./.gitea/workflows/deploy_call.yaml
|
|
||||||
with:
|
|
||||||
prerelease: true
|
|
||||||
secrets: inherit
|
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
name: Build Android Agent
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
build_type:
|
||||||
|
description: 'Build type (debug or release)'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: 'release'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
RUNNER_TOOL_CACHE: /toolcache
|
||||||
|
steps:
|
||||||
|
- name: Configure git auth for submodules
|
||||||
|
run: git config --global url."https://x-access-token:${{ secrets.BUILD_TOKEN }}@git.datadunia.com/".insteadOf "https://git.datadunia.com/"
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.BUILD_TOKEN }}
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'temurin'
|
||||||
|
cache: gradle
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
|
- name: Check for signing key
|
||||||
|
id: check-signing
|
||||||
|
run: |
|
||||||
|
if [ -n "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" ]; then
|
||||||
|
echo "has_key=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Signing: Using production keystore from secrets"
|
||||||
|
else
|
||||||
|
echo "has_key=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Signing: No keystore found, using debug/self-signed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Decode keystore
|
||||||
|
if: steps.check-signing.outputs.has_key == 'true'
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > /tmp/release.keystore
|
||||||
|
|
||||||
|
- name: Create keystore properties
|
||||||
|
if: steps.check-signing.outputs.has_key == 'true'
|
||||||
|
run: |
|
||||||
|
cat > /tmp/keystore.properties << EOF
|
||||||
|
storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
|
keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
|
keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}
|
||||||
|
storeFile=/tmp/release.keystore
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Configure signing (production)
|
||||||
|
if: steps.check-signing.outputs.has_key == 'true'
|
||||||
|
working-directory: apps/android-agent
|
||||||
|
run: |
|
||||||
|
# Inject signing config into build.gradle.kts
|
||||||
|
cat >> app/build.gradle.kts << 'GRADLE_EOF'
|
||||||
|
|
||||||
|
android {
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
storeFile = file("/tmp/release.keystore")
|
||||||
|
storePassword = System.getenv("KEYSTORE_PASSWORD") ?: ""
|
||||||
|
keyAlias = System.getenv("KEY_ALIAS") ?: ""
|
||||||
|
keyPassword = System.getenv("KEY_PASSWORD") ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GRADLE_EOF
|
||||||
|
env:
|
||||||
|
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
|
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||||
|
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
working-directory: apps/android-agent
|
||||||
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Build debug APK (no signing key)
|
||||||
|
if: steps.check-signing.outputs.has_key == 'false'
|
||||||
|
working-directory: apps/android-agent
|
||||||
|
run: ./gradlew assembleDebug
|
||||||
|
|
||||||
|
- name: Build release APK (with signing key)
|
||||||
|
if: steps.check-signing.outputs.has_key == 'true'
|
||||||
|
working-directory: apps/android-agent
|
||||||
|
run: ./gradlew assembleRelease
|
||||||
|
|
||||||
|
- name: Upload debug APK
|
||||||
|
if: steps.check-signing.outputs.has_key == 'false'
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: nexusguard-android-debug
|
||||||
|
path: apps/android-agent/app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
|
||||||
|
- name: Upload release APK
|
||||||
|
if: steps.check-signing.outputs.has_key == 'true'
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: nexusguard-android-release
|
||||||
|
path: apps/android-agent/app/build/outputs/apk/release/app-release.apk
|
||||||
|
|
||||||
|
- name: Upload mapping file (release only)
|
||||||
|
if: steps.check-signing.outputs.has_key == 'true'
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: nexusguard-android-mapping
|
||||||
|
path: apps/android-agent/app/build/outputs/mapping/release/mapping.txt
|
||||||
|
if-no-files-found: warn
|
||||||
|
|
||||||
|
- name: Cleanup keystore
|
||||||
|
if: always()
|
||||||
|
run: rm -f /tmp/release.keystore /tmp/keystore.properties
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
name: Build Dashboard UI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
RUNNER_TOOL_CACHE: /toolcache
|
||||||
|
steps:
|
||||||
|
- name: Configure git auth for submodules
|
||||||
|
run: git config --global url."https://x-access-token:${{ secrets.BUILD_TOKEN }}@git.datadunia.com/".insteadOf "https://git.datadunia.com/"
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.BUILD_TOKEN }}
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '24'
|
||||||
|
|
||||||
|
- name: Hash source files
|
||||||
|
id: hash-src
|
||||||
|
run: echo "hash=$(find apps/dashboard-ui -type f \( -name '*.vue' -o -name '*.ts' -o -name '*.js' -o -name '*.css' -o -name '*.json' -o -name '*.html' \) | sort | xargs sha256sum | sha256sum | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- uses: actions/cache@v3
|
||||||
|
id: cache-build
|
||||||
|
with:
|
||||||
|
path: apps/dashboard-ui/dist
|
||||||
|
key: build-dashboard-${{ steps.hash-src.outputs.hash }}
|
||||||
|
|
||||||
|
- name: Hash package-lock
|
||||||
|
id: hash-npm
|
||||||
|
run: echo "hash=$(sha256sum apps/dashboard-ui/package-lock.json | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- uses: actions/cache@v3
|
||||||
|
id: cache-npm
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: npm-dashboard-${{ steps.hash-npm.outputs.hash }}
|
||||||
|
restore-keys: npm-dashboard-
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
|
working-directory: apps/dashboard-ui
|
||||||
|
run: npm ci || npm install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
|
working-directory: apps/dashboard-ui
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: dashboard-ui-dist
|
||||||
|
path: apps/dashboard-ui/dist/
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
name: Build Device Agent
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cross-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
RUNNER_TOOL_CACHE: /toolcache
|
||||||
|
steps:
|
||||||
|
- name: Configure git auth for submodules
|
||||||
|
run: git config --global url."https://x-access-token:${{ secrets.BUILD_TOKEN }}@git.datadunia.com/".insteadOf "https://git.datadunia.com/"
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.BUILD_TOKEN }}
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.26'
|
||||||
|
cache: true
|
||||||
|
cache-dependency-path: apps/device-agent/go.sum
|
||||||
|
|
||||||
|
- uses: https://gitea.com/actions/go-hashfiles@v0.0.1
|
||||||
|
id: hash-src
|
||||||
|
with:
|
||||||
|
patterns: |
|
||||||
|
apps/device-agent/**/*.go
|
||||||
|
apps/device-agent/go.mod
|
||||||
|
apps/device-agent/go.sum
|
||||||
|
|
||||||
|
- uses: actions/cache@v3
|
||||||
|
id: cache-build
|
||||||
|
with:
|
||||||
|
path: apps/device-agent/bin
|
||||||
|
key: build-device-agent-${{ steps.hash-src.outputs.hash }}
|
||||||
|
|
||||||
|
- name: Generate Windows resources (UAC manifest + icon)
|
||||||
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
|
working-directory: apps/device-agent
|
||||||
|
run: |
|
||||||
|
go install github.com/tc-hib/go-winres@latest
|
||||||
|
go-winres make
|
||||||
|
|
||||||
|
- name: Build all platforms
|
||||||
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
|
working-directory: apps/device-agent
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
mkdir -p bin
|
||||||
|
for PAIR in linux/amd64 linux/arm64 windows/amd64; do
|
||||||
|
GOOS="${PAIR%%/*}"
|
||||||
|
GOARCH="${PAIR##*/}"
|
||||||
|
EXT=""
|
||||||
|
if [ "$GOOS" = "windows" ]; then EXT=".exe"; fi
|
||||||
|
OUT="bin/nexus-device-agent-${GOOS}-${GOARCH}${EXT}"
|
||||||
|
echo "Building $OUT ..."
|
||||||
|
CGO_ENABLED=0 GOOS="$GOOS" GOARCH="$GOARCH" go build -ldflags="-s -w" -o "$OUT" .
|
||||||
|
done
|
||||||
|
echo "=== Build output ==="
|
||||||
|
ls -la bin/
|
||||||
|
|
||||||
|
- name: Upload linux/amd64
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: nexus-device-agent-linux-amd64
|
||||||
|
path: apps/device-agent/bin/nexus-device-agent-linux-amd64
|
||||||
|
|
||||||
|
- name: Upload linux/arm64
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: nexus-device-agent-linux-arm64
|
||||||
|
path: apps/device-agent/bin/nexus-device-agent-linux-arm64
|
||||||
|
|
||||||
|
- name: Upload windows/amd64
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: nexus-device-agent-windows-amd64
|
||||||
|
path: apps/device-agent/bin/nexus-device-agent-windows-amd64.exe
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
name: Build Server Core
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
RUNNER_TOOL_CACHE: /toolcache
|
||||||
|
steps:
|
||||||
|
- name: Configure git auth for submodules
|
||||||
|
run: git config --global url."https://x-access-token:${{ secrets.BUILD_TOKEN }}@git.datadunia.com/".insteadOf "https://git.datadunia.com/"
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.BUILD_TOKEN }}
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.26'
|
||||||
|
cache: true
|
||||||
|
cache-dependency-path: apps/server-core/go.sum
|
||||||
|
|
||||||
|
- uses: https://gitea.com/actions/go-hashfiles@v0.0.1
|
||||||
|
id: hash-src
|
||||||
|
with:
|
||||||
|
patterns: |
|
||||||
|
apps/server-core/**/*.go
|
||||||
|
apps/server-core/go.mod
|
||||||
|
apps/server-core/go.sum
|
||||||
|
|
||||||
|
- uses: actions/cache@v3
|
||||||
|
id: cache-build
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
apps/server-core/bin
|
||||||
|
apps/server-core/docs
|
||||||
|
key: build-server-core-${{ steps.hash-src.outputs.hash }}
|
||||||
|
|
||||||
|
- name: Generate Swagger docs
|
||||||
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
|
working-directory: apps/server-core
|
||||||
|
run: |
|
||||||
|
go install github.com/swaggo/swag/cmd/swag@v1.16.6
|
||||||
|
swag init -g main.go --parseDependency --parseInternal
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
|
working-directory: apps/server-core
|
||||||
|
run: go build -o bin/server-core .
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: server-core-linux-amd64
|
||||||
|
path: apps/server-core/bin/server-core
|
||||||
+127
-110
@@ -1,25 +1,25 @@
|
|||||||
name: NexusGuard CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
tags:
|
||||||
- main
|
- 'dev-*'
|
||||||
|
- 'v*-dev*'
|
||||||
|
- 'v*-beta*'
|
||||||
|
- 'v*-test*'
|
||||||
|
- 'v[0-9]*.[0-9]*.[0-9]'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- develop
|
||||||
env:
|
|
||||||
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ──────────────────────────────────────────────
|
# ====================================================================
|
||||||
# TEST — all 3 components in parallel
|
# TESTS — test tags only
|
||||||
# ──────────────────────────────────────────────
|
# ====================================================================
|
||||||
server-core-test:
|
server-core-test:
|
||||||
|
if: contains(gitea.ref_name, 'test') || github.event_name == 'pull_request'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: apps/server-core
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -27,18 +27,20 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.BUILD_TOKEN }}
|
token: ${{ secrets.BUILD_TOKEN }}
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
github-server-url: 'https://git.datadunia.com'
|
- name: Configure git auth for submodules
|
||||||
|
run: git config --global url."https://x-access-token:${{ secrets.BUILD_TOKEN }}@git.datadunia.com/".insteadOf "https://git.datadunia.com/"
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.26'
|
||||||
- name: Test
|
cache: true
|
||||||
run: go test ./... -tags dev -cover
|
cache-dependency-path: apps/server-core/go.sum
|
||||||
|
- name: Test (skip nftables - needs root)
|
||||||
|
working-directory: apps/server-core
|
||||||
|
run: go test $(go list ./... | grep -v internal/firewall) -tags dev -cover -count=1
|
||||||
|
|
||||||
device-agent-test:
|
device-agent-test:
|
||||||
|
if: contains(gitea.ref_name, 'test') || github.event_name == 'pull_request'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: apps/device-agent
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -46,18 +48,20 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.BUILD_TOKEN }}
|
token: ${{ secrets.BUILD_TOKEN }}
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
github-server-url: 'https://git.datadunia.com'
|
- name: Configure git auth for submodules
|
||||||
|
run: git config --global url."https://x-access-token:${{ secrets.BUILD_TOKEN }}@git.datadunia.com/".insteadOf "https://git.datadunia.com/"
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.26'
|
||||||
|
cache: true
|
||||||
|
cache-dependency-path: apps/device-agent/go.sum
|
||||||
- name: Test
|
- name: Test
|
||||||
|
working-directory: apps/device-agent
|
||||||
run: go test ./... -cover
|
run: go test ./... -cover
|
||||||
|
|
||||||
dashboard-test:
|
dashboard-test:
|
||||||
|
if: contains(gitea.ref_name, 'test') || github.event_name == 'pull_request'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: apps/dashboard-ui
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -65,103 +69,116 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.BUILD_TOKEN }}
|
token: ${{ secrets.BUILD_TOKEN }}
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
github-server-url: 'https://git.datadunia.com'
|
- name: Configure git auth for submodules
|
||||||
|
run: git config --global url."https://x-access-token:${{ secrets.BUILD_TOKEN }}@git.datadunia.com/".insteadOf "https://git.datadunia.com/"
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '24'
|
node-version: '24'
|
||||||
- name: Install
|
- name: Install
|
||||||
run: npm ci || npm install
|
|
||||||
- name: Build
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────
|
|
||||||
# BUILD — binaries + frontend dist
|
|
||||||
# ──────────────────────────────────────────────
|
|
||||||
server-core-build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: server-core-test
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: apps/server-core
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
token: ${{ secrets.BUILD_TOKEN }}
|
|
||||||
persist-credentials: true
|
|
||||||
github-server-url: 'https://git.datadunia.com'
|
|
||||||
- uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: '1.25'
|
|
||||||
- name: Build
|
|
||||||
run: go build -o bin/server-core .
|
|
||||||
- name: Upload binary
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: server-core-linux-amd64
|
|
||||||
path: apps/server-core/bin/server-core
|
|
||||||
|
|
||||||
device-agent-cross-build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: device-agent-test
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
goos: [linux, windows]
|
|
||||||
goarch: [amd64, arm64]
|
|
||||||
exclude:
|
|
||||||
- goos: windows
|
|
||||||
goarch: arm64
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: apps/device-agent
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
token: ${{ secrets.BUILD_TOKEN }}
|
|
||||||
persist-credentials: true
|
|
||||||
github-server-url: 'https://git.datadunia.com'
|
|
||||||
- uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: '1.25'
|
|
||||||
- name: Build
|
|
||||||
env:
|
|
||||||
CGO_ENABLED: 0
|
|
||||||
GOOS: ${{ matrix.goos }}
|
|
||||||
GOARCH: ${{ matrix.goarch }}
|
|
||||||
run: |
|
|
||||||
EXT=""
|
|
||||||
if [ "${{ matrix.goos }}" = "windows" ]; then EXT=".exe"; fi
|
|
||||||
go build -o bin/nexus-device-agent-${{ matrix.goos }}-${{ matrix.goarch }}${EXT} .
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: nexus-device-agent-${{ matrix.goos }}-${{ matrix.goarch }}
|
|
||||||
path: apps/device-agent/bin/nexus-device-agent-*
|
|
||||||
|
|
||||||
dashboard-dist:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: dashboard-test
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: apps/dashboard-ui
|
working-directory: apps/dashboard-ui
|
||||||
|
run: npm ci || npm install
|
||||||
|
- name: Build
|
||||||
|
working-directory: apps/dashboard-ui
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
android-agent-test:
|
||||||
|
if: contains(gitea.ref_name, 'test') || github.event_name == 'pull_request'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
fetch-depth: 0
|
||||||
token: ${{ secrets.BUILD_TOKEN }}
|
token: ${{ secrets.BUILD_TOKEN }}
|
||||||
github-server-url: 'https://git.datadunia.com'
|
persist-credentials: true
|
||||||
- uses: actions/setup-node@v4
|
- name: Configure git auth for submodules
|
||||||
|
run: git config --global url."https://x-access-token:${{ secrets.BUILD_TOKEN }}@git.datadunia.com/".insteadOf "https://git.datadunia.com/"
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
node-version: '24'
|
java-version: '17'
|
||||||
- name: Install
|
distribution: 'temurin'
|
||||||
run: npm ci || npm install
|
cache: gradle
|
||||||
- name: Build
|
- name: Setup Android SDK
|
||||||
run: npm run build
|
uses: android-actions/setup-android@v3
|
||||||
- name: Upload dist
|
- name: Grant execute permission for gradlew
|
||||||
uses: actions/upload-artifact@v4
|
working-directory: apps/android-agent
|
||||||
|
run: chmod +x gradlew
|
||||||
|
- name: Build debug APK
|
||||||
|
working-directory: apps/android-agent
|
||||||
|
run: ./gradlew assembleDebug
|
||||||
|
|
||||||
|
# ====================================================================
|
||||||
|
# BUILD — after tests pass (release) or directly (dev/beta)
|
||||||
|
# ====================================================================
|
||||||
|
build-server-core:
|
||||||
|
if: always() && !contains(gitea.ref_name, 'test') && !failure() && !cancelled()
|
||||||
|
needs: [server-core-test, device-agent-test, dashboard-test, android-agent-test]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
uses: ./.gitea/workflows/build_server_core.yaml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
build-device-agent:
|
||||||
|
if: always() && !contains(gitea.ref_name, 'test') && !failure() && !cancelled()
|
||||||
|
needs: [server-core-test, device-agent-test, dashboard-test, android-agent-test]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
uses: ./.gitea/workflows/build_device_agent.yaml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
build-dashboard:
|
||||||
|
if: always() && !contains(gitea.ref_name, 'test') && !failure() && !cancelled()
|
||||||
|
needs: [server-core-test, device-agent-test, dashboard-test, android-agent-test]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
uses: ./.gitea/workflows/build_dashboard.yaml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
build-android-agent:
|
||||||
|
if: always() && !contains(gitea.ref_name, 'test') && !failure() && !cancelled()
|
||||||
|
needs: [server-core-test, device-agent-test, dashboard-test, android-agent-test]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
uses: ./.gitea/workflows/build_android_agent.yaml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
# ====================================================================
|
||||||
|
# DOCS — after builds pass, release tags only
|
||||||
|
# ====================================================================
|
||||||
|
build-docs:
|
||||||
|
if: always() && !contains(gitea.ref_name, 'dev') && !contains(gitea.ref_name, 'beta') && !contains(gitea.ref_name, 'test') && !failure() && !cancelled()
|
||||||
|
needs: [build-server-core, build-device-agent, build-dashboard, build-android-agent]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
uses: ./.gitea/workflows/docs_call.yaml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
# ====================================================================
|
||||||
|
# RELEASE — after everything passes
|
||||||
|
# ====================================================================
|
||||||
|
release:
|
||||||
|
if: always() && !contains(gitea.ref_name, 'test') && !failure() && !cancelled()
|
||||||
|
needs: [build-server-core, build-device-agent, build-dashboard, build-android-agent, build-docs]
|
||||||
|
uses: ./.gitea/workflows/release_call.yaml
|
||||||
with:
|
with:
|
||||||
name: dashboard-ui-dist
|
prerelease: ${{ contains(gitea.ref_name, 'dev') }}
|
||||||
path: apps/dashboard-ui/dist/
|
draft: ${{ contains(gitea.ref_name, 'beta') }}
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
# ====================================================================
|
||||||
|
# DEPLOY DEV — auto-deploy on dev tags
|
||||||
|
# ====================================================================
|
||||||
|
deploy-dev:
|
||||||
|
if: contains(gitea.ref_name, 'dev') && !failure() && !cancelled()
|
||||||
|
needs: [build-server-core, build-device-agent, build-dashboard, build-android-agent]
|
||||||
|
uses: ./.gitea/workflows/deploy_call.yaml
|
||||||
|
with:
|
||||||
|
environment: dev
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
# ====================================================================
|
||||||
|
# DEPLOY PROD — manual trigger after release
|
||||||
|
# ====================================================================
|
||||||
|
deploy-prod:
|
||||||
|
if: startsWith(gitea.ref_name, 'v') && !contains(gitea.ref_name, 'dev') && !contains(gitea.ref_name, 'beta') && !contains(gitea.ref_name, 'test') && !failure() && !cancelled()
|
||||||
|
needs: [build-server-core, build-device-agent, build-dashboard, build-android-agent, build-docs]
|
||||||
|
uses: ./.gitea/workflows/deploy_call.yaml
|
||||||
|
with:
|
||||||
|
environment: production
|
||||||
|
secrets: inherit
|
||||||
|
|||||||
@@ -3,205 +3,87 @@ name: Deploy
|
|||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
prerelease:
|
environment:
|
||||||
description: 'Mark as prerelease'
|
description: 'Target environment (dev or production)'
|
||||||
required: false
|
required: true
|
||||||
type: boolean
|
type: string
|
||||||
default: false
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-release:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment: ${{ inputs.environment }}
|
||||||
steps:
|
steps:
|
||||||
- name: Clone main repository
|
- name: Configure git auth for submodules
|
||||||
run: |
|
run: git config --global url."https://x-access-token:${{ secrets.BUILD_TOKEN }}@git.datadunia.com/".insteadOf "https://git.datadunia.com/"
|
||||||
git config --global --remove-section http || true
|
|
||||||
git config --global --unset-all core.askPass || true
|
- uses: actions/checkout@v4
|
||||||
TOKEN="${{ secrets.BUILD_TOKEN }}"
|
|
||||||
git clone -c credential.helper="" \
|
|
||||||
https://token:$TOKEN@git.datadunia.com/nexusguard/Nexus-Guard-Suite.git .
|
|
||||||
- name: Clone submodules
|
|
||||||
run: |
|
|
||||||
TOKEN="${{ secrets.BUILD_TOKEN }}"
|
|
||||||
# Server Core
|
|
||||||
git clone -c credential.helper="" \
|
|
||||||
https://token:$TOKEN@git.datadunia.com/nexusguard/nexus-server-core.git apps/server-core
|
|
||||||
# Device Agent
|
|
||||||
git clone -c credential.helper="" \
|
|
||||||
https://token:$TOKEN@git.datadunia.com/nexusguard/nexus-device-agent.git apps/device-agent
|
|
||||||
# Dashboard UI
|
|
||||||
git clone -c credential.helper="" \
|
|
||||||
https://token:$TOKEN@git.datadunia.com/nexusguard/nexus-dashboard-ui.git apps/dashboard-ui
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
fetch-depth: 0
|
||||||
- name: Setup Node.js
|
token: ${{ secrets.BUILD_TOKEN }}
|
||||||
uses: actions/setup-node@v4
|
persist-credentials: true
|
||||||
|
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
node-version: '24'
|
path: ./artifacts
|
||||||
- name: Download Go modules
|
|
||||||
run: |
|
- name: Deploy to ${{ inputs.environment }}
|
||||||
cd apps/server-core
|
|
||||||
go mod download
|
|
||||||
- name: Generate Swagger docs
|
|
||||||
run: |
|
|
||||||
go install github.com/swaggo/swag/cmd/swag@v1.16.6
|
|
||||||
cd apps/server-core
|
|
||||||
swag init -g main.go --parseDependency --parseInternal
|
|
||||||
- name: Build Server Core
|
|
||||||
run: |
|
|
||||||
cd apps/server-core
|
|
||||||
go build -o bin/server-core .
|
|
||||||
- name: Build Device Agent Cross-platform
|
|
||||||
run: |
|
|
||||||
cd apps/device-agent
|
|
||||||
# Linux amd64
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/nexus-device-agent-linux-amd64 .
|
|
||||||
# Linux arm64
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/nexus-device-agent-linux-arm64 .
|
|
||||||
# Windows amd64
|
|
||||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/nexus-device-agent-windows-amd64.exe .
|
|
||||||
- name: Build Dashboard UI
|
|
||||||
run: |
|
|
||||||
cd apps/dashboard-ui
|
|
||||||
npm ci || npm install
|
|
||||||
npm run build
|
|
||||||
- name: Generate latest.json
|
|
||||||
run: |
|
|
||||||
VERSION="${{ gitea.ref_name }}"
|
|
||||||
RELEASE_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
|
||||||
REPO="${{ gitea.repository }}"
|
|
||||||
SERVER="${{ gitea.server_url }}"
|
|
||||||
cat > latest.json << ENDJSON
|
|
||||||
{
|
|
||||||
"version": "$VERSION",
|
|
||||||
"release_date": "$RELEASE_DATE",
|
|
||||||
"download_urls": {
|
|
||||||
"server-core": "${SERVER}/${REPO}/releases/download/${VERSION}/server-core-linux-amd64",
|
|
||||||
"device-agent-linux-amd64": "${SERVER}/${REPO}/releases/download/${VERSION}/nexus-device-agent-linux-amd64",
|
|
||||||
"device-agent-linux-arm64": "${SERVER}/${REPO}/releases/download/${VERSION}/nexus-device-agent-linux-arm64",
|
|
||||||
"device-agent-windows-amd64": "${SERVER}/${REPO}/releases/download/${VERSION}/nexus-device-agent-windows-amd64.exe",
|
|
||||||
"dashboard-ui": "${SERVER}/${REPO}/releases/download/${VERSION}/dashboard-ui-dist.tar.gz"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ENDJSON
|
|
||||||
- name: Create Release and upload assets
|
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ secrets.BUILD_TOKEN }}
|
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||||
|
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
||||||
|
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
|
||||||
run: |
|
run: |
|
||||||
if [ -z "$TOKEN" ]; then
|
echo "Deploying to ${{ inputs.environment }}..."
|
||||||
echo "Value: [EMPTY]"
|
echo "Host: $DEPLOY_HOST"
|
||||||
exit 1
|
echo "Path: $DEPLOY_PATH"
|
||||||
else
|
|
||||||
echo "Length: ${#TOKEN} characters"
|
# Setup SSH
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
|
||||||
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
|
# Upload artifacts
|
||||||
|
echo "Uploading server-core..."
|
||||||
|
scp -i ~/.ssh/deploy_key artifacts/server-core-linux-amd64/server-core \
|
||||||
|
"${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/bin/"
|
||||||
|
|
||||||
|
echo "Uploading device-agent..."
|
||||||
|
for platform in linux-amd64 linux-arm64; do
|
||||||
|
scp -i ~/.ssh/deploy_key "artifacts/nexus-device-agent-${platform}/nexus-device-agent-${platform}" \
|
||||||
|
"${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/bin/"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Uploading dashboard..."
|
||||||
|
if [ -d "artifacts/dashboard-ui-dist/dist" ]; then
|
||||||
|
tar -czf /tmp/dashboard-ui.tar.gz -C artifacts/dashboard-ui-dist dist/
|
||||||
|
scp -i ~/.ssh/deploy_key /tmp/dashboard-ui.tar.gz \
|
||||||
|
"${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
REPO="${{ gitea.repository }}"
|
# Deploy on server
|
||||||
TAG="${{ gitea.ref_name }}"
|
echo "Running deployment..."
|
||||||
API="${{ gitea.server_url }}/api/v1"
|
ssh -i ~/.ssh/deploy_key "${DEPLOY_USER}@${DEPLOY_HOST}" << 'DEPLOY_SCRIPT'
|
||||||
|
cd "${DEPLOY_PATH}" || exit 1
|
||||||
|
|
||||||
# 0. Check & Delete Existing Release
|
# Stop services
|
||||||
echo "=== 0. Check & Delete Existing Release ==="
|
echo "Stopping services..."
|
||||||
EXISTING_RESP=$(curl -s -H "Authorization: token $TOKEN" "$API/repos/$REPO/releases/tags/$TAG")
|
docker compose down || true
|
||||||
EXISTING_ID=$(echo "$EXISTING_RESP" | grep -o '"id":[0-9]*' | head -n 1 | cut -d':' -f2 || true)
|
|
||||||
|
|
||||||
if [ -n "$EXISTING_ID" ] && [ "$EXISTING_ID" != "null" ]; then
|
# Run update script
|
||||||
echo "⚠️ Found existing release for tag $TAG with ID: $EXISTING_ID. Deleting..."
|
echo "Running update..."
|
||||||
DELETE_RESP=$(curl -s -w "\n%{http_code}" -X DELETE -H "Authorization: token $TOKEN" "$API/repos/$REPO/releases/$EXISTING_ID")
|
bash update.sh --force
|
||||||
echo "✅ Delete response: $DELETE_RESP"
|
|
||||||
else
|
|
||||||
echo "No existing release found for $TAG. Proceeding..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 1. Create Release
|
# Verify services
|
||||||
echo "=== 1. Create New Release ==="
|
echo "Verifying services..."
|
||||||
JSON_BODY=$(printf '{"tag_name":"%s","name":"%s","body":"Release %s","draft":false,"prerelease":%s}' "$TAG" "$TAG" "$TAG" "${{ inputs.prerelease }}")
|
docker compose ps
|
||||||
|
|
||||||
RELEASE_RESP=$(curl -s -X POST \
|
echo "Deployment complete!"
|
||||||
-H "Authorization: token $TOKEN" \
|
DEPLOY_SCRIPT
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$JSON_BODY" \
|
|
||||||
"$API/repos/$REPO/releases")
|
|
||||||
|
|
||||||
# Ambil ID dengan lebih teliti
|
echo "Deployed to ${{ inputs.environment }} successfully!"
|
||||||
# Tambahkan || true agar grep tidak membuat script crash (karena set -e) jika id tidak ditemukan
|
|
||||||
RELEASE_ID=$(echo "$RELEASE_RESP" | grep -o '"id":[0-9]*' | head -n 1 | cut -d':' -f2 || true)
|
|
||||||
|
|
||||||
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
|
- name: Cleanup
|
||||||
echo "Gagal membuat release. Response: $RELEASE_RESP"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Release ID: $RELEASE_ID"
|
|
||||||
|
|
||||||
# 2. Upload Assets
|
|
||||||
# Server Core
|
|
||||||
if [ -f "apps/server-core/bin/server-core" ]; then
|
|
||||||
echo "Uploading server-core-linux-amd64..."
|
|
||||||
curl -s -X POST \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-F "attachment=@apps/server-core/bin/server-core" \
|
|
||||||
"$API/repos/$REPO/releases/$RELEASE_ID/assets?name=server-core-linux-amd64"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Device Agent Linux amd64
|
|
||||||
if [ -f "apps/device-agent/bin/nexus-device-agent-linux-amd64" ]; then
|
|
||||||
echo "Uploading nexus-device-agent-linux-amd64..."
|
|
||||||
curl -s -X POST \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-F "attachment=@apps/device-agent/bin/nexus-device-agent-linux-amd64" \
|
|
||||||
"$API/repos/$REPO/releases/$RELEASE_ID/assets?name=nexus-device-agent-linux-amd64"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Device Agent Linux arm64
|
|
||||||
if [ -f "apps/device-agent/bin/nexus-device-agent-linux-arm64" ]; then
|
|
||||||
echo "Uploading nexus-device-agent-linux-arm64..."
|
|
||||||
curl -s -X POST \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-F "attachment=@apps/device-agent/bin/nexus-device-agent-linux-arm64" \
|
|
||||||
"$API/repos/$REPO/releases/$RELEASE_ID/assets?name=nexus-device-agent-linux-arm64"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Device Agent Windows amd64
|
|
||||||
if [ -f "apps/device-agent/bin/nexus-device-agent-windows-amd64.exe" ]; then
|
|
||||||
echo "Uploading nexus-device-agent-windows-amd64.exe..."
|
|
||||||
curl -s -X POST \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-F "attachment=@apps/device-agent/bin/nexus-device-agent-windows-amd64.exe" \
|
|
||||||
"$API/repos/$REPO/releases/$RELEASE_ID/assets?name=nexus-device-agent-windows-amd64.exe"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Dashboard UI dist
|
|
||||||
if [ -d "apps/dashboard-ui/dist" ]; then
|
|
||||||
echo "Creating dashboard-ui-dist.tar.gz..."
|
|
||||||
cd apps/dashboard-ui
|
|
||||||
tar -czf ../dashboard-ui-dist.tar.gz dist/
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
echo "Uploading dashboard-ui-dist.tar.gz..."
|
|
||||||
curl -s -X POST \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-F "attachment=@dashboard-ui-dist.tar.gz" \
|
|
||||||
"$API/repos/$REPO/releases/$RELEASE_ID/assets?name=dashboard-ui-dist.tar.gz"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# latest.json
|
|
||||||
if [ -f "latest.json" ]; then
|
|
||||||
echo "Uploading latest.json..."
|
|
||||||
curl -s -X POST \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-F "attachment=@latest.json" \
|
|
||||||
"$API/repos/$REPO/releases/$RELEASE_ID/assets?name=latest.json"
|
|
||||||
fi
|
|
||||||
- name: Cleanup build artifacts
|
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: rm -rf ~/.ssh/deploy_key /tmp/dashboard-ui.tar.gz
|
||||||
git config --global --remove-section http || true
|
|
||||||
git config --global --unset-all core.askPass || true
|
|
||||||
rm -f latest.json dashboard-ui-dist.tar.gz
|
|
||||||
rm -rf apps/server-core/bin apps/device-agent/bin apps/dashboard-ui/node_modules apps/dashboard-ui/dist
|
|
||||||
echo "Cleanup done"
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
name: Build Docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Configure git auth for submodules
|
||||||
|
run: git config --global url."https://x-access-token:${{ secrets.BUILD_TOKEN }}@git.datadunia.com/".insteadOf "https://git.datadunia.com/"
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.BUILD_TOKEN }}
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '24'
|
||||||
|
|
||||||
|
- name: Hash package-lock
|
||||||
|
id: hash-npm
|
||||||
|
run: echo "hash=$(sha256sum apps/docs/package-lock.json 2>/dev/null || echo 'none') | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.npm
|
||||||
|
key: npm-docs-${{ steps.hash-npm.outputs.hash }}
|
||||||
|
restore-keys: npm-docs-
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
working-directory: apps/docs
|
||||||
|
run: npm ci || npm install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
working-directory: apps/docs
|
||||||
|
run: npm run docs:build
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: docs-dist
|
||||||
|
path: apps/docs/.vitepress/dist/
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v[0-9]*.[0-9]*.[0-9]'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
if: "!contains(gitea.ref_name, 'beta') && !contains(gitea.ref_name, 'test')"
|
|
||||||
uses: ./.gitea/workflows/deploy_call.yaml
|
|
||||||
with:
|
|
||||||
prerelease: false
|
|
||||||
secrets: inherit
|
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
prerelease:
|
||||||
|
description: 'Mark as prerelease'
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
draft:
|
||||||
|
description: 'Mark as draft'
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Configure git auth for submodules
|
||||||
|
run: git config --global url."https://x-access-token:${{ secrets.BUILD_TOKEN }}@git.datadunia.com/".insteadOf "https://git.datadunia.com/"
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.BUILD_TOKEN }}
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
path: ./artifacts
|
||||||
|
|
||||||
|
- name: Generate latest.json
|
||||||
|
run: |
|
||||||
|
VERSION="${{ gitea.ref_name }}"
|
||||||
|
RELEASE_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
||||||
|
REPO="${{ gitea.repository }}"
|
||||||
|
SERVER="${{ gitea.server_url }}"
|
||||||
|
cat > latest.json << ENDJSON
|
||||||
|
{
|
||||||
|
"version": "$VERSION",
|
||||||
|
"release_date": "$RELEASE_DATE",
|
||||||
|
"download_urls": {
|
||||||
|
"server-core": "${SERVER}/${REPO}/releases/download/${VERSION}/server-core-linux-amd64",
|
||||||
|
"device-agent-linux-amd64": "${SERVER}/${REPO}/releases/download/${VERSION}/nexus-device-agent-linux-amd64",
|
||||||
|
"device-agent-linux-arm64": "${SERVER}/${REPO}/releases/download/${VERSION}/nexus-device-agent-linux-arm64",
|
||||||
|
"device-agent-windows-amd64": "${SERVER}/${REPO}/releases/download/${VERSION}/nexus-device-agent-windows-amd64.exe",
|
||||||
|
"dashboard-ui": "${SERVER}/${REPO}/releases/download/${VERSION}/dashboard-ui-dist.tar.gz",
|
||||||
|
"android-agent": "${SERVER}/${REPO}/releases/download/${VERSION}/nexusguard-android.apk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ENDJSON
|
||||||
|
|
||||||
|
- name: Create Release and upload assets
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||||
|
run: |
|
||||||
|
REPO="${{ gitea.repository }}"
|
||||||
|
TAG="${{ gitea.ref_name }}"
|
||||||
|
API="${{ gitea.server_url }}/api/v1"
|
||||||
|
|
||||||
|
# Delete existing release if present
|
||||||
|
EXISTING_ID=$(curl -s -H "Authorization: token $TOKEN" "$API/repos/$REPO/releases/tags/$TAG" | grep -o '"id":[0-9]*' | head -n 1 | cut -d':' -f2 || true)
|
||||||
|
if [ -n "$EXISTING_ID" ] && [ "$EXISTING_ID" != "null" ]; then
|
||||||
|
echo "Deleting existing release $EXISTING_ID..."
|
||||||
|
curl -s -X DELETE -H "Authorization: token $TOKEN" "$API/repos/$REPO/releases/$EXISTING_ID"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create release
|
||||||
|
JSON_BODY=$(printf '{"tag_name":"%s","name":"%s","body":"Release %s","draft":%s,"prerelease":%s}' "$TAG" "$TAG" "$TAG" "${{ inputs.draft }}" "${{ inputs.prerelease }}")
|
||||||
|
RELEASE_RESP=$(curl -s -X POST \
|
||||||
|
-H "Authorization: token $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$JSON_BODY" \
|
||||||
|
"$API/repos/$REPO/releases")
|
||||||
|
|
||||||
|
RELEASE_ID=$(echo "$RELEASE_RESP" | grep -o '"id":[0-9]*' | head -n 1 | cut -d':' -f2 || true)
|
||||||
|
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
|
||||||
|
echo "Failed to create release. Response: $RELEASE_RESP"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Release ID: $RELEASE_ID"
|
||||||
|
|
||||||
|
# Upload assets
|
||||||
|
upload_asset() {
|
||||||
|
local file="$1"
|
||||||
|
local name="$2"
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
echo "Uploading $name..."
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token $TOKEN" \
|
||||||
|
-F "attachment=@$file" \
|
||||||
|
"$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Server Core
|
||||||
|
upload_asset "artifacts/server-core-linux-amd64/server-core" "server-core-linux-amd64"
|
||||||
|
|
||||||
|
# Device Agent (all platforms)
|
||||||
|
for platform in linux-amd64 linux-arm64 windows-amd64; do
|
||||||
|
EXT=""
|
||||||
|
if [ "$platform" = "windows-amd64" ]; then EXT=".exe"; fi
|
||||||
|
upload_asset "artifacts/nexus-device-agent-${platform}/nexus-device-agent-${platform}${EXT}" "nexus-device-agent-${platform}${EXT}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Dashboard UI tar.gz
|
||||||
|
if [ -d "artifacts/dashboard-ui-dist/dist" ]; then
|
||||||
|
cd artifacts/dashboard-ui-dist
|
||||||
|
tar -czf ../../dashboard-ui-dist.tar.gz dist/
|
||||||
|
cd ../..
|
||||||
|
upload_asset "dashboard-ui-dist.tar.gz" "dashboard-ui-dist.tar.gz"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Android Agent APK
|
||||||
|
if [ -f "artifacts/nexusguard-android-debug/nexusguard-android-debug.apk" ]; then
|
||||||
|
upload_asset "artifacts/nexusguard-android-debug/nexusguard-android-debug.apk" "nexusguard-android.apk"
|
||||||
|
elif [ -f "artifacts/nexusguard-android-release/nexusguard-android-release.apk" ]; then
|
||||||
|
upload_asset "artifacts/nexusguard-android-release/nexusguard-android-release.apk" "nexusguard-android.apk"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Docs tar.gz
|
||||||
|
if [ -d "artifacts/docs-dist" ]; then
|
||||||
|
cd artifacts/docs-dist
|
||||||
|
tar -czf ../../docs-dist.tar.gz ./
|
||||||
|
cd ../..
|
||||||
|
upload_asset "docs-dist.tar.gz" "docs-dist.tar.gz"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# latest.json
|
||||||
|
upload_asset "latest.json" "latest.json"
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: always()
|
||||||
|
run: rm -rf artifacts/ latest.json dashboard-ui-dist.tar.gz docs-dist.tar.gz
|
||||||
+27
-2
@@ -30,18 +30,43 @@ nexusguard.conf
|
|||||||
# Temp
|
# Temp
|
||||||
tmp/
|
tmp/
|
||||||
temp_*
|
temp_*
|
||||||
|
*-test
|
||||||
*.tmp
|
*.tmp
|
||||||
|
|
||||||
# Build output
|
# Build output
|
||||||
dist/
|
dist/
|
||||||
/dist/
|
/dist/
|
||||||
|
tests/
|
||||||
|
.tests/
|
||||||
|
|
||||||
|
# Android build artifacts
|
||||||
|
*.aar
|
||||||
|
*.apk
|
||||||
|
*.aab
|
||||||
|
android-agent/app/build/
|
||||||
|
android-agent/build/
|
||||||
|
android-agent/.gradle/
|
||||||
|
android-agent/local.properties
|
||||||
|
|
||||||
|
# Go mobile build output
|
||||||
|
nexusguard-agent.aar
|
||||||
|
|
||||||
# OpenCode
|
# OpenCode
|
||||||
|
connect_remote.txt
|
||||||
|
.update-state
|
||||||
|
|
||||||
|
# Scripts (contains API tokens)
|
||||||
|
scripts/
|
||||||
|
|
||||||
|
# Oh-My-OpenAgent & Sisyphus plugin files
|
||||||
|
.omo/
|
||||||
|
.sisyphus/
|
||||||
.sisyphus/notepads/
|
.sisyphus/notepads/
|
||||||
.sisyphus/boulder.json
|
.sisyphus/boulder.json
|
||||||
connect_remote.txt
|
|
||||||
.sisyphus/drafts/
|
.sisyphus/drafts/
|
||||||
.update-state
|
.omo/notepads/
|
||||||
|
.omo/boulder.json
|
||||||
|
.omo/drafts/
|
||||||
|
|
||||||
# Node
|
# Node
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
+6
-3
@@ -1,9 +1,12 @@
|
|||||||
[submodule "apps/server-core"]
|
[submodule "apps/server-core"]
|
||||||
path = apps/server-core
|
path = apps/server-core
|
||||||
url = http://git.datadunia.com/nexusguard/nexus-server-core.git
|
url = https://git.datadunia.com/nexusguard/nexus-server-core.git
|
||||||
[submodule "apps/dashboard-ui"]
|
[submodule "apps/dashboard-ui"]
|
||||||
path = apps/dashboard-ui
|
path = apps/dashboard-ui
|
||||||
url = http://git.datadunia.com/nexusguard/nexus-dashboard-ui.git
|
url = https://git.datadunia.com/nexusguard/nexus-dashboard-ui.git
|
||||||
[submodule "apps/device-agent"]
|
[submodule "apps/device-agent"]
|
||||||
path = apps/device-agent
|
path = apps/device-agent
|
||||||
url = http://git.datadunia.com/nexusguard/nexus-device-agent.git
|
url = https://git.datadunia.com/nexusguard/nexus-device-agent.git
|
||||||
|
[submodule "apps/android-agent"]
|
||||||
|
path = apps/android-agent
|
||||||
|
url = https://git.datadunia.com/nexusguard/nexus-android-agent.git
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# Bug Fixes and Features Plan
|
||||||
|
|
||||||
|
## Bug Fixes
|
||||||
|
|
||||||
|
### 1. Nodes Edit Button - Hard to Click / Wrong HTML Tag Location
|
||||||
|
**Location**: apps/dashboard-ui/src/views/Servers.vue - Node cards edit button
|
||||||
|
**Issue**: Edit button on node cards is difficult to click or has incorrect HTML structure
|
||||||
|
**Root Cause**: Button z-index, positioning, or overlapping elements
|
||||||
|
**Files**: Servers.vue (lines 155-157)
|
||||||
|
**Status**: ✅ DONE - Changed button size from "sm" to "md" for better clickability
|
||||||
|
|
||||||
|
### 2. Dashboard - Missing Turn On WireGuard Button for 2nd+ Nodes
|
||||||
|
**Location**: apps/dashboard-ui/src/views/Servers.vue and apps/server-core/api/wg.go
|
||||||
|
**Issue**: Only first node (Local Primary Node) has WG Up/Down buttons; additional nodes lack toggle
|
||||||
|
**Root Cause**: wg.go Status/Up/Down handlers only work with hardcoded wg0 interface; multi-interface support needed
|
||||||
|
**Files**:
|
||||||
|
- apps/dashboard-ui/src/views/Servers.vue - Add WG toggle button per node
|
||||||
|
- apps/server-core/api/wg.go - Fix to accept interface name parameter
|
||||||
|
- apps/server-core/internal/wgmanager/wgmanager_linux.go - Ensure multi-interface support
|
||||||
|
**Status**: ✅ BACKEND DONE - wg.go accepts interface param, queries by interface_name, supports multi-interface
|
||||||
|
**Status**: ✅ FRONTEND DONE - WG Up/Down buttons added to node cards, calls API with interface param
|
||||||
|
|
||||||
|
### 3. Advanced Node Settings - Missing Notes/Descriptions for PreUp, PostUp, PreDown, PostDown
|
||||||
|
**Location**: apps/dashboard-ui/src/views/Servers.vue (lines 83-98)
|
||||||
|
**Issue**: Advanced scripts fields (PreUp, PostUp, PreDown, PostDown) lack helper text/descriptions like Firewall section has
|
||||||
|
**Files**: Servers.vue - Add hints/descriptions similar to FirewallEditor
|
||||||
|
**Status**: ✅ DONE - Added descriptive hints for Table, PreUp, PostUp, PreDown, PostDown
|
||||||
|
|
||||||
|
### 4. Popup/Modal Inconsistency - Backdrop Styling
|
||||||
|
**Location**: Multiple modals in Servers.vue, Devices.vue, DeviceDetail.vue, FirewallEditor.vue
|
||||||
|
**Issue**: Nodes modal backdrop styling is better than Devices modal; inconsistent across views
|
||||||
|
**Files**: Standardize modal wrapper component or CSS classes
|
||||||
|
**Status**: ✅ DONE - Servers.vue modals converted to NgModal, consistent backdrop (bg-bg-overlay)
|
||||||
|
|
||||||
|
### 5. Firewall Popup - Not User/Mobile Friendly
|
||||||
|
**Location**: apps/dashboard-ui/src/components/FirewallEditor.vue
|
||||||
|
**Issue**: Form layout not responsive; input fields too small on mobile; buttons not touch-friendly
|
||||||
|
**Files**: FirewallEditor.vue - Responsive grid, larger touch targets, better spacing
|
||||||
|
**Status**: ✅ DONE - Responsive grid (1/2/5 cols), button full width on mobile, table scroll-x-auto
|
||||||
|
|
||||||
|
### 6. Firewall wg_isolation - Verify Implementation Matches Plan
|
||||||
|
**Location**: apps/server-core/internal/firewall/nftables_linux.go - InitNetworkForServer()
|
||||||
|
**Issue**: Verify wg_isolation rules are correctly implemented per-server with smart isolation (allow server IP, drop peer-to-peer)
|
||||||
|
**Files**: nftables_linux.go - InitNetworkForServer() and TeardownNetworkForServer()
|
||||||
|
**Status**: ✅ DONE - Implementation verified: smart isolation (server IP allow, peer-to-peer drop), per-interface chains with jump rules, proper cleanup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 1. Traffic Record Table - Only Show Records with RX or TX Data
|
||||||
|
**Location**: apps/dashboard-ui/src/views/TrafficHistory.vue, apps/server-core/api/traffic.go, apps/server-core/internal/traffic/recorder.go
|
||||||
|
**Issue**: Table shows all records including zero-byte entries; should filter to only show records with rx > 0 or tx > 0
|
||||||
|
**Files**:
|
||||||
|
- TrafficHistory.vue - Filter trafficData before display
|
||||||
|
- traffic.go - Add filter option to API
|
||||||
|
**Status**: ✅ DONE - Added has_traffic query param, toggle in UI, backend filtering (rx>0 OR tx>0)
|
||||||
|
|
||||||
|
### 2. Traffic Record Table - Sum Per Hour Aggregation
|
||||||
|
**Location**: apps/server-core/api/traffic.go, apps/server-core/internal/traffic/recorder.go
|
||||||
|
**Issue**: Add hourly aggregation option for traffic table when query supports it
|
||||||
|
**Files**:
|
||||||
|
- traffic.go - Add aggregation parameter to GetSummary/GetDeviceTraffic
|
||||||
|
- recorder.go - Add GetHourlyTraffic method with SQL GROUP BY hour
|
||||||
|
**Status**: ✅ DONE - Added GetHourlyTraffic endpoint with SQL GROUP BY hour, returns HourlyTraffic[]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Priority
|
||||||
|
|
||||||
|
| Priority | Task | Category |
|
||||||
|
|----------|------|----------|
|
||||||
|
| P1 | Nodes edit button fix | Bug |
|
||||||
|
| P1 | Dashboard WG toggle for all nodes | Bug |
|
||||||
|
| P1 | Verify wg_isolation implementation | Bug |
|
||||||
|
| P2 | Advanced settings descriptions | Bug |
|
||||||
|
| P2 | Modal consistency (backdrop) | Bug |
|
||||||
|
| P2 | Firewall mobile-friendly | Bug |
|
||||||
|
| P2 | Traffic table filter (RX/TX > 0) | Feature |
|
||||||
|
| P3 | Traffic hourly aggregation | Feature |
|
||||||
|
| P3 | Firewall mobile-friendly | Bug |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Structure Reference
|
||||||
|
|
||||||
|
### Frontend (Vue 3 + TypeScript)
|
||||||
|
apps/dashboard-ui/src/
|
||||||
|
|-- views/
|
||||||
|
| |-- Servers.vue # Node management (edit, WG toggle, advanced)
|
||||||
|
| |-- Devices.vue # Device list, firewall
|
||||||
|
| |-- DeviceDetail.vue # Device detail, firewall editor
|
||||||
|
| |-- TrafficHistory.vue # Traffic table, filters
|
||||||
|
| |-- ...
|
||||||
|
|-- components/
|
||||||
|
| |-- FirewallEditor.vue # Firewall rules UI
|
||||||
|
| |-- ui/ # Ng* design system components
|
||||||
|
| |-- ...
|
||||||
|
|-- ...
|
||||||
|
|
||||||
|
### Backend (Go)
|
||||||
|
apps/server-core/
|
||||||
|
|-- api/
|
||||||
|
| |-- servers.go # Node CRUD, WG Up/Down
|
||||||
|
| |-- wg.go # WG interface control
|
||||||
|
| |-- traffic.go # Traffic API
|
||||||
|
| |-- ...
|
||||||
|
|-- internal/
|
||||||
|
| |-- firewall/
|
||||||
|
| | |-- nftables_linux.go # InitNetworkForServer, Teardown
|
||||||
|
| | |-- ...
|
||||||
|
| |-- wgmanager/
|
||||||
|
| | |-- wgmanager_linux.go # Multi-interface WgManager
|
||||||
|
| |-- traffic/
|
||||||
|
| |-- recorder.go # Traffic queries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Create detailed task breakdown for each bug/feature
|
||||||
|
2. Start with P1 bugs (edit button, WG toggle, wg_isolation)
|
||||||
|
3. Implement fixes following existing code patterns
|
||||||
|
5. Archive completed plan when done
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- All plans from .sisyphus have been migrated to .omo/plans/archive/
|
||||||
|
- New plan saved at .omo/plans/bugfixes-and-features.md
|
||||||
|
- Evidence, notepads, references migrated to .omo/
|
||||||
|
- Boulder state copied to .omo/boulder.json
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
# Firewall InitNetwork Fix — INPUT vs FORWARD Chain Bugs
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
> **Quick Summary**: Fix 3 bugs in `InitNetwork()` that prevent WireGuard clients from reaching the server and Docker containers. ICMP echo-reply blocked, Docker DNAT traffic dropped, and missing base INPUT rules.
|
||||||
|
>
|
||||||
|
> **Deliverables**:
|
||||||
|
> - Fixed `nftables_linux.go` InitNetwork() with correct ICMP, Docker bridge, and INPUT rules
|
||||||
|
> - Updated `manager.go` if needed
|
||||||
|
> - Server rebuilt and deployed via `update.sh --force`
|
||||||
|
>
|
||||||
|
> **Estimated Effort**: Short
|
||||||
|
> **Parallel Execution**: YES - 2 waves
|
||||||
|
> **Critical Path**: Task 1 → Task 4 (verify)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
### Original Request
|
||||||
|
User reported firewall rules from dashboard not working. Traced through multiple debugging sessions to find 3 root-cause bugs in `InitNetwork()` base rules:
|
||||||
|
1. `icmp type echo-request` only allows incoming pings TO server, not echo-reply FROM peers
|
||||||
|
2. No FORWARD rules for Docker bridge — WireGuard traffic DNAT'd to containers gets dropped
|
||||||
|
3. Server→peer traffic works (OUTPUT default accept) but replies hit INPUT chain and get dropped
|
||||||
|
|
||||||
|
### Interview Summary
|
||||||
|
- **Key Discussions**: Extensive debugging on live server (172.20.8.191). User tested each fix manually via SSH. Confirmed Docker DNAT intercepts port 80 traffic via iptables PREROUTING, redirecting to container 172.24.0.4.
|
||||||
|
- **Research Findings**: Docker uses iptables DNAT while NexusGuard uses nftables filter — both coexist. Traffic flow: WireGuard → INPUT (nftables) → ACCEPT → Docker PREROUTING (iptables DNAT) → destination changes to container IP → FORWARD chain (nftables) → DROP (no bridge rule).
|
||||||
|
- **User Constraints**: No local binary builds (Docker only). No temp/debug files. Admin-only firewall (JWT protected).
|
||||||
|
|
||||||
|
### Metis Review (if consulted)
|
||||||
|
N/A — bugs are clear from source code analysis, no ambiguity requiring consultation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Work Objectives
|
||||||
|
|
||||||
|
### Core Objective
|
||||||
|
Fix 3 bugs in `InitNetwork()` that prevent WireGuard peer-to-server and peer-to-Docker-container connectivity.
|
||||||
|
|
||||||
|
### Concrete Deliverables
|
||||||
|
- Fixed `apps/server-core/internal/firewall/nftables_linux.go` InitNetwork()
|
||||||
|
- Fixed `apps/server-core/internal/firewall/manager.go` if interface changes needed
|
||||||
|
- Server rebuilt and deployed
|
||||||
|
- nftables verified working on live server
|
||||||
|
|
||||||
|
### Definition of Done
|
||||||
|
- [ ] `nft list chain ip nexusguard input` shows `meta l4proto icmp accept` (not `icmp type echo-request`)
|
||||||
|
- [ ] `nft list chain ip nexusguard forward` shows `fwd_wg_docker` rules for 172.24.0.0/16 and 172.17.0.0/16
|
||||||
|
- [ ] Client (gogo3 10.172.21.3) can ping server (10.172.21.1)
|
||||||
|
- [ ] Server (10.172.21.1) can ping client (10.172.21.3)
|
||||||
|
- [ ] Client can curl http://10.172.21.1:80 and get 200
|
||||||
|
|
||||||
|
### Must Have
|
||||||
|
- `meta l4proto icmp` replaces `icmp type echo-request` in INPUT chain
|
||||||
|
- `fwd_wg_docker` rules added to FORWARD chain in InitNetwork()
|
||||||
|
- Existing peer routing rules (AddForwardRule) still work
|
||||||
|
- Existing DB firewall rules (syncRuleToFirewall) still work
|
||||||
|
|
||||||
|
### Must NOT Have (Guardrails)
|
||||||
|
- Do NOT `nft flush table nexusguard` — destroys all rules
|
||||||
|
- Do NOT change the FirewallRule model or API endpoints
|
||||||
|
- Do NOT modify peer_sync.go or devices.go
|
||||||
|
- Do NOT create temp/debug files in project root
|
||||||
|
- Do NOT change the firewall chain routing logic (dest==server→INPUT, else→FORWARD)
|
||||||
|
- Do NOT remove the `input_wg_drop` or `wg_isolation_default` base rules
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Strategy
|
||||||
|
|
||||||
|
> **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed. No exceptions.
|
||||||
|
|
||||||
|
### Test Decision
|
||||||
|
- **Infrastructure exists**: NO (no nftables unit tests)
|
||||||
|
- **Automated tests**: None (nftables rules tested via live server SSH)
|
||||||
|
- **Framework**: None needed — live server verification
|
||||||
|
|
||||||
|
### QA Policy
|
||||||
|
Every task includes agent-executed QA scenarios.
|
||||||
|
Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`.
|
||||||
|
|
||||||
|
- **nft verification**: SSH to server, run nft commands, verify rules present
|
||||||
|
- **Connectivity**: SSH to server, run ping/curl tests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Strategy
|
||||||
|
|
||||||
|
### Parallel Execution Waves
|
||||||
|
|
||||||
|
```
|
||||||
|
Wave 1 (Start Immediately — 1 agent):
|
||||||
|
├── Task 1: Fix InitNetwork() in nftables_linux.go (quick)
|
||||||
|
|
||||||
|
Wave 2 (After Wave 1 — 1 agent):
|
||||||
|
├── Task 2: Commit + Push + Deploy (quick)
|
||||||
|
├── Task 3: Verify nft rules on live server (quick)
|
||||||
|
|
||||||
|
Wave FINAL (After Wave 2 — reviewer):
|
||||||
|
├── Task F1: Plan compliance audit (oracle)
|
||||||
|
├── Task F2: Code quality review (unspecified-high)
|
||||||
|
├── Task F3: Real manual QA (unspecified-high)
|
||||||
|
├── Task F4: Scope fidelity check (deep)
|
||||||
|
-> F1-F4 can run in parallel
|
||||||
|
|
||||||
|
Critical Path: Task 1 → Task 2 → Task 3 → F1-F4
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependency Matrix
|
||||||
|
|
||||||
|
| Task | Depends On | Blocks |
|
||||||
|
|------|-----------|--------|
|
||||||
|
| Task 1 | None | Task 2 |
|
||||||
|
| Task 2 | Task 1 | Task 3 |
|
||||||
|
| Task 3 | Task 2 | F1-F4 |
|
||||||
|
| F1-F4 | Task 3 | None |
|
||||||
|
|
||||||
|
### Agent Dispatch Summary
|
||||||
|
|
||||||
|
- **Wave 1**: T1 → `quick`
|
||||||
|
- **Wave 2**: T2 → `quick`, T3 → `quick`
|
||||||
|
- **FINAL**: F1 → `oracle`, F2 → `unspecified-high`, F3 → `unspecified-high`, F4 → `deep`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TODOs
|
||||||
|
|
||||||
|
- [x] 1. Fix InitNetwork() in nftables_linux.go
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
1. In `apps/server-core/internal/firewall/nftables_linux.go`, line 48: change `icmp type echo-request` to `meta l4proto icmp`. Also update the comment from `input_icmp` to `input_icmp_all`.
|
||||||
|
2. In the same function, after the `input_wg_drop` rule block (around line 64), add Docker bridge accept rules to FORWARD chain:
|
||||||
|
- `nft insert rule ip nexusguard forward ip saddr <wgSubnet> ip daddr 172.24.0.0/16 accept comment "fwd_wg_docker"`
|
||||||
|
- `nft insert rule ip nexusguard forward ip saddr <wgSubnet> ip daddr 172.17.0.0/16 accept comment "fwd_wg_docker0"`
|
||||||
|
3. These Docker rules should be inserted AFTER `fwd_estab` and BEFORE the `wg_isolation` drop rule. Use `nft insert rule` with position or append after fwd_estab.
|
||||||
|
4. Add dedup checks (same pattern as existing rules): `grep -q 'fwd_wg_docker'` before inserting.
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Do NOT change AddForwardRule, AddFirewallRule, AddInputFirewallRule, or RemoveFirewallRule
|
||||||
|
- Do NOT change the chain routing logic in syncRuleToFirewall
|
||||||
|
- Do NOT flush or recreate any chains
|
||||||
|
- Do NOT change manager.go interface
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- Reason: Single-file change, 3 specific line edits, clear patterns to follow
|
||||||
|
- **Skills**: []
|
||||||
|
- No special skills needed — straightforward Go code edit
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: NO
|
||||||
|
- **Parallel Group**: Wave 1 (solo)
|
||||||
|
- **Blocks**: Task 2 (commit/deploy)
|
||||||
|
- **Blocked By**: None (can start immediately)
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `apps/server-core/internal/firewall/nftables_linux.go:25-68` — InitNetwork() function, all 3 bugs are here
|
||||||
|
- `apps/server-core/internal/firewall/nftables_linux.go:30-38` — existing FORWARD chain setup (fwd_estab, wg_isolation) — Docker rules go between these
|
||||||
|
- `apps/server-core/internal/firewall/nftables_linux.go:40-64` — existing INPUT chain setup — ICMP fix at line 48
|
||||||
|
- `apps/server-core/main.go:210-259` — startup re-apply code that calls AddForwardRule and AddInputFirewallRule — do NOT modify
|
||||||
|
- `apps/server-core/internal/firewall/manager.go:5-18` — NetManager interface — do NOT modify
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] Line 48 reads `meta l4proto icmp` not `icmp type echo-request`
|
||||||
|
- [ ] Comment reads `input_icmp_all` not `input_icmp`
|
||||||
|
- [ ] FORWARD chain has dedup check for `fwd_wg_docker` before inserting
|
||||||
|
- [ ] `go vet ./internal/firewall/...` passes
|
||||||
|
- [ ] No other lines in InitNetwork() changed
|
||||||
|
|
||||||
|
**QA Scenarios**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Scenario: Verify ICMP rule is correct
|
||||||
|
Tool: Bash (grep)
|
||||||
|
Steps:
|
||||||
|
1. grep "meta l4proto icmp" apps/server-core/internal/firewall/nftables_linux.go
|
||||||
|
2. grep "icmp type echo-request" apps/server-core/internal/firewall/nftables_linux.go
|
||||||
|
Expected Result: First grep returns match, second grep returns nothing
|
||||||
|
Evidence: .sisyphus/evidence/task-1-icmp-rule.txt
|
||||||
|
|
||||||
|
Scenario: Verify Docker bridge rules exist
|
||||||
|
Tool: Bash (grep)
|
||||||
|
Steps:
|
||||||
|
1. grep "fwd_wg_docker" apps/server-core/internal/firewall/nftables_linux.go
|
||||||
|
2. grep "172.24.0.0/16" apps/server-core/internal/firewall/nftables_linux.go
|
||||||
|
3. grep "172.17.0.0/16" apps/server-core/internal/firewall/nftables_linux.go
|
||||||
|
Expected Result: All 3 greps return matches
|
||||||
|
Evidence: .sisyphus/evidence/task-1-docker-rules.txt
|
||||||
|
|
||||||
|
Scenario: Verify dedup check pattern
|
||||||
|
Tool: Bash (grep)
|
||||||
|
Steps:
|
||||||
|
1. grep "fwd_wg_docker" apps/server-core/internal/firewall/nftables_linux.go | head -5
|
||||||
|
Expected Result: Shows both the grep check command AND the nft insert command
|
||||||
|
Evidence: .sisyphus/evidence/task-1-dedup-pattern.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit**: YES
|
||||||
|
- Message: `fix(nftables): InitNetwork ICMP all, Docker bridge accept, base INPUT rules`
|
||||||
|
- Files: `apps/server-core/internal/firewall/nftables_linux.go`
|
||||||
|
- Pre-commit: `go vet ./internal/firewall/...`
|
||||||
|
|
||||||
|
- [x] 2. Commit, Push, Deploy to Server
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
1. In `apps/server-core/`: `git add -A && git commit` with the fix message, then `git push`
|
||||||
|
2. In root `Nexus-Guard-Suite/`: `git add apps/server-core && git commit && git push`
|
||||||
|
3. SSH to server: `cd /root/Nexus-Guard-Suite && bash update.sh --force`
|
||||||
|
4. Wait for deployment to complete
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Do NOT build binary locally
|
||||||
|
- Do NOT create temp files on server
|
||||||
|
- Do NOT use `nft flush` on server
|
||||||
|
- Do NOT modify any code files
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- Reason: Simple git + SSH commands, well-documented in AGENTS.md
|
||||||
|
- **Skills**: []
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: NO
|
||||||
|
- **Parallel Group**: Wave 2 (solo)
|
||||||
|
- **Blocks**: Task 3 (verify)
|
||||||
|
- **Blocked By**: Task 1 (code change)
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `D:\www-project\NexusGuard\connect_remote.txt` — SSH credentials (HOST=172.20.8.191, USER=root)
|
||||||
|
- `D:\www-project\NexusGuard\update.sh` — Docker rebuild script
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] Submodule HEAD updated (new commit hash)
|
||||||
|
- [ ] Root repo HEAD updated
|
||||||
|
- [ ] Server container restarted successfully
|
||||||
|
- [ ] `docker ps` shows server-core running
|
||||||
|
|
||||||
|
**QA Scenarios**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Scenario: Verify deployment
|
||||||
|
Tool: SSH (bash)
|
||||||
|
Steps:
|
||||||
|
1. ssh root@172.20.8.191 'docker ps | grep server-core'
|
||||||
|
2. ssh root@172.20.8.191 'docker logs nexus-guard-suite-server-core-1 2>&1 | tail -5'
|
||||||
|
Expected Result: Container running, logs show clean startup
|
||||||
|
Evidence: .sisyphus/evidence/task-2-deployment.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit**: NO (commit done as part of task)
|
||||||
|
|
||||||
|
- [x] 3. Verify nftables Rules and Connectivity on Live Server
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
1. SSH to server, run `nft list table ip nexusguard` and verify:
|
||||||
|
- INPUT chain has `meta l4proto icmp accept comment "input_icmp_all"`
|
||||||
|
- FORWARD chain has `fwd_wg_docker` rules for 172.24.0.0/16 and 172.17.0.0/16
|
||||||
|
- All existing rules intact (server_wg1, input_estab, input_wg_api, etc.)
|
||||||
|
2. Test from server: `ping -c 3 10.172.21.3` — should get replies
|
||||||
|
3. Ask user to test from client: `ping 10.172.21.1` and `curl -v http://10.172.21.1:80`
|
||||||
|
4. Verify nft counters increment when traffic flows
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Do NOT modify any nft rules during verification
|
||||||
|
- Do NOT flush or recreate chains
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- Reason: SSH verification commands only
|
||||||
|
- **Skills**: []
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: NO
|
||||||
|
- **Parallel Group**: Wave 2 (after Task 2)
|
||||||
|
- **Blocks**: F1-F4
|
||||||
|
- **Blocked By**: Task 2 (deployment)
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `D:\www-project\NexusGuard\connect_remote.txt` — SSH credentials
|
||||||
|
- `D:\www-project\NexusGuard\AGENTS.md` — WireGuard AllowedIPs architecture rules
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] INPUT chain has `meta l4proto icmp` (not `icmp type echo-request`)
|
||||||
|
- [ ] FORWARD chain has `fwd_wg_docker` for 172.24.0.0/16
|
||||||
|
- [ ] FORWARD chain has `fwd_wg_docker0` for 172.17.0.0/16
|
||||||
|
- [ ] Server can ping gogo3 (10.172.21.3)
|
||||||
|
- [ ] Client can ping server (10.172.21.1)
|
||||||
|
- [ ] Client can curl http://10.172.21.1:80
|
||||||
|
|
||||||
|
**QA Scenarios**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Scenario: Verify INPUT chain ICMP rule
|
||||||
|
Tool: SSH (bash)
|
||||||
|
Steps:
|
||||||
|
1. ssh root@172.20.8.191 'nft list chain ip nexusguard input | grep icmp'
|
||||||
|
Expected Result: Shows `meta l4proto icmp accept comment "input_icmp_all"`
|
||||||
|
Evidence: .sisyphus/evidence/task-3-input-icmp.txt
|
||||||
|
|
||||||
|
Scenario: Verify FORWARD chain Docker rules
|
||||||
|
Tool: SSH (bash)
|
||||||
|
Steps:
|
||||||
|
1. ssh root@172.20.8.191 'nft list chain ip nexusguard forward | grep docker'
|
||||||
|
Expected Result: Shows both fwd_wg_docker (172.24.0.0/16) and fwd_wg_docker0 (172.17.0.0/16)
|
||||||
|
Evidence: .sisyphus/evidence/task-3-forward-docker.txt
|
||||||
|
|
||||||
|
Scenario: Server ping client
|
||||||
|
Tool: SSH (bash)
|
||||||
|
Steps:
|
||||||
|
1. ssh root@172.20.8.191 'ping -c 3 10.172.21.3'
|
||||||
|
Expected Result: 3 replies, 0% packet loss
|
||||||
|
Evidence: .sisyphus/evidence/task-3-ping-client.txt
|
||||||
|
|
||||||
|
Scenario: Client connectivity (requires user)
|
||||||
|
Tool: User prompt
|
||||||
|
Steps:
|
||||||
|
1. Ask user to run from gogo3 client: `ping 10.172.21.1`
|
||||||
|
2. Ask user to run from gogo3 client: `curl -v http://10.172.21.1:80`
|
||||||
|
Expected Result: Ping replies, curl returns 200
|
||||||
|
Evidence: User provides output
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit**: NO
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final Verification Wave (MANDATORY — after ALL implementation tasks)
|
||||||
|
|
||||||
|
> 4 review agents run in PARALLEL. ALL must APPROVE. Rejection → fix → re-run.
|
||||||
|
|
||||||
|
- [x] F1. **Plan Compliance Audit** — `oracle`
|
||||||
|
Read the plan end-to-end. For each "Must Have": verify implementation exists (read file, curl endpoint, check schema). For each "Must NOT Have": search codebase for forbidden patterns — reject with file:line if found. Check evidence files exist in .sisyphus/evidence/. Compare deliverables against plan.
|
||||||
|
Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT`
|
||||||
|
|
||||||
|
- [x] F2. **Code Quality Review** — `unspecified-high`
|
||||||
|
Run `go vet ./...` on changed packages. Review all changed files for: empty catches, console.logs in prod code, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction, generic variable names.
|
||||||
|
Output: `Build [PASS/FAIL] | Files [N clean/N issues] | VERDICT`
|
||||||
|
|
||||||
|
- [x] F3. **Real Manual QA** — `unspecified-high` (equipment: SSH to 172.20.8.191)
|
||||||
|
SSH to server. Run: `nft list table ip nexusguard` and verify rules. Then test: `ping 10.172.21.3` from server. From client: `ping 10.172.21.1` and `curl -v http://10.172.21.1:80`. Test negative case: verify that WG isolation default drop still blocks unauthorized traffic.
|
||||||
|
Output: `Connectivity [N/N pass] | Firewall [N correct rules] | Negative [PASS/FAIL] | VERDICT`
|
||||||
|
|
||||||
|
- [x] F4. **Scope Fidelity Check** — `deep`
|
||||||
|
For each task: read "What to do", read actual diff (git log/diff). Verify 1:1 — everything in spec was built (no missing), nothing beyond spec was built (no creep). Check "Must NOT do" compliance. Flag unauthorized changes.
|
||||||
|
Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | VERDICT`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commit Strategy
|
||||||
|
|
||||||
|
- **Task 1**: `fix(nftables): InitNetwork ICMP, Docker bridge, INPUT base rules` → `apps/server-core/`
|
||||||
|
- **Task 2**: Submodule push + root push + deploy via `update.sh --force`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
### Verification Commands
|
||||||
|
```bash
|
||||||
|
# From server (SSH root@172.20.8.191):
|
||||||
|
nft list chain ip nexusguard input
|
||||||
|
# Expected: meta l4proto icmp accept comment "input_icmp_all"
|
||||||
|
|
||||||
|
nft list chain ip nexusguard forward
|
||||||
|
# Expected: fwd_wg_docker accept for 172.24.0.0/16 and 172.17.0.0/16
|
||||||
|
|
||||||
|
# From client (gogo3):
|
||||||
|
ping 10.172.21.1
|
||||||
|
# Expected: replies
|
||||||
|
|
||||||
|
# From server:
|
||||||
|
ping 10.172.21.3
|
||||||
|
# Expected: replies
|
||||||
|
|
||||||
|
# From client:
|
||||||
|
curl -s -o /dev/null -w "%{http_code}" http://10.172.21.1:80
|
||||||
|
# Expected: 200
|
||||||
|
```
|
||||||
|
|
||||||
|
### Final Checklist
|
||||||
|
- [x] All "Must Have" present
|
||||||
|
- [x] All "Must NOT Have" absent
|
||||||
|
- [x] Server deployed and running
|
||||||
|
- [ ] Both peers can ping server
|
||||||
|
- [x] Server can ping both peers
|
||||||
|
- [ ] Port 80 accessible from WireGuard client
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# Multi-Interface Refactor
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
> **Objective**: Refactor NexusGuard from single WireGuard interface to multi-interface per WgServer.
|
||||||
|
## Context
|
||||||
|
|
||||||
|
**Original Request**: User wants firewall bug fixed + multi-node isolation like wgdashboard where each node has configurable wg_isolation and NAT interface.
|
||||||
|
**Interview Summary**:
|
||||||
|
- Default deny all for client<->client, allow server->client default
|
||||||
|
- Isolation configurable per node via UI checkbox
|
||||||
|
- NAT interface (eth0/eth1/ens5) configurable per node
|
||||||
|
- Current architecture only supports 1 local interface (wg0)
|
||||||
|
|
||||||
|
**Research Findings**:
|
||||||
|
- WgManager hardcoded to wg0 (wgmanager_linux.go:25)
|
||||||
|
- Firewall InitNetwork() runs once globally for single subnet
|
||||||
|
- NAT uses auto-detected default route interface
|
||||||
|
- WgServer model lacks InterfaceName, IsLocal, PeerIsolation, NatInterface fields
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Work Objectives
|
||||||
|
|
||||||
|
**Core Objective**: Enable multiple local WireGuard interfaces, each with independent subnet, firewall isolation, and NAT egress interface.
|
||||||
|
|
||||||
|
**Concrete Deliverables**:
|
||||||
|
1. Database migration adding 4 fields to wg_servers table
|
||||||
|
2. WgManager supporting multiple interfaces by name
|
||||||
|
3. Firewall manager with per-interface chains (forward_wgX, input_wgX)
|
||||||
|
4. Startup initialization loop for all IsLocal=true servers
|
||||||
|
5. API handlers using server context for all operations
|
||||||
|
6. Cleanup of hardcoded Local Primary Node references
|
||||||
|
|
||||||
|
**Definition of Done**:
|
||||||
|
- [ ] Migration runs: ALTER TABLE wg_servers ADD COLUMN ...
|
||||||
|
- [ ] wg0, wg1, wg2 interfaces can run simultaneously
|
||||||
|
- [ ] Each interface has independent peer isolation (configurable)
|
||||||
|
- [ ] Each interface uses configured NAT interface for masquerade
|
||||||
|
- [ ] Firewall rules scoped to correct interface chain
|
||||||
|
- [ ] Peer sync works per server (WgServerID filter)
|
||||||
|
- [ ] All existing tests pass
|
||||||
|
- [ ] Manual QA: 2+ local nodes with different subnets/NAT interfaces
|
||||||
|
|
||||||
|
**Must Have**:
|
||||||
|
- Backward compatible: existing single-node deployments work unchanged
|
||||||
|
- Default values: InterfaceName=wg0, IsLocal=false, PeerIsolation=true, NatInterface= (auto)
|
||||||
|
|
||||||
|
**Must NOT Have** (Guardrails):
|
||||||
|
- NO breaking changes to external node provisioning
|
||||||
|
- NO nft flush table - only atomic add/remove
|
||||||
|
- NO hardcoded interface names in firewall code
|
||||||
|
- NO cross-interface peer leakage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Strategy
|
||||||
|
|
||||||
|
**Test Decision**:
|
||||||
|
- Infrastructure exists: YES (Go test with -tags dev, GORM AutoMigrate)
|
||||||
|
- Automated tests: Tests-after (add tests for new multi-interface logic)
|
||||||
|
- Framework: Go testing (standard library)
|
||||||
|
|
||||||
|
**QA Policy**: Every task includes agent-executed QA scenarios.
|
||||||
|
|
||||||
|
| Domain | Tool | Evidence Pattern |
|
||||||
|
|--------|------|------------------|
|
||||||
|
| Go unit/integration | go test -tags dev ./... | .sisyphus/evidence/task-{N}-test.log |
|
||||||
|
| nftables rules | bash (nft list) | .sisyphus/evidence/task-{N}-nftables.txt |
|
||||||
|
| WireGuard interfaces | bash (ip link, wg show) | .sisyphus/evidence/task-{N}-wg.txt |
|
||||||
|
| API endpoints | bash (curl) | .sisyphus/evidence/task-{N}-api.json |
|
||||||
|
Wave 2 (Core Logic - 4 parallel):
|
||||||
|
├── T5: LinuxWgManager multi-interface implementation [deep]
|
||||||
|
├── T6: LinuxManager InitNetworkForServer + Teardown [deep]
|
||||||
|
├── T7: NAT per-interface masquerade rules [unspecified-high]
|
||||||
|
├── T8: Peer sync per-server (WgServerID filter) [unspecified-high]
|
||||||
|
Wave 3 (Startup & Recovery - 3 parallel):
|
||||||
|
├── T9: Main.go startup loop for all local servers [deep]
|
||||||
|
├── T10: Firewall rules re-apply per server [unspecified-high]
|
||||||
|
├── T11: Input rule (WG port) per server [quick]
|
||||||
|
Wave 4 (API Handlers - 5 parallel):
|
||||||
|
├── T12: servers.go Create/Update with multi-interface [quick]
|
||||||
|
├── T13: peers.go device creation with server context [quick]
|
||||||
|
├── T14: peer_sync.go SyncLocalPeers per server [quick]
|
||||||
|
├── T15: rules.go syncRuleToFirewall per server [quick]
|
||||||
|
├── T16: provisioning.go server-aware [quick]
|
||||||
|
Wave 5 (Cleanup & Migration - 2 parallel):
|
||||||
|
├── T17: Remove hardcoded Local Primary Node refs [quick]
|
||||||
|
├── T18: Migration script + backfill defaults [quick]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TODOs
|
||||||
|
|
||||||
|
- [x] 1. Database Migration + Model Updates [quick]
|
||||||
|
- [x] 2. WgManager Interface + Multi-Interface Struct [deep]
|
||||||
|
- [x] 3. NetManager Interface + Per-Server Methods [deep]
|
||||||
|
- [x] 4. nftables Chain-Per-Interface Scaffolding [quick]
|
||||||
|
- [x] 5. LinuxWgManager Multi-Interface Implementation [deep]
|
||||||
|
- [x] 6. LinuxManager InitNetworkForServer + Teardown [deep]
|
||||||
|
- [x] 7. NAT Per-Interface Masquerade Rules [unspecified-high]
|
||||||
|
- [x] 8. Peer Sync Per-Server (WgServerID Filter) [unspecified-high]
|
||||||
|
- [x] 9. Main.go Startup Loop for All Local Servers [deep]
|
||||||
|
- [x] 10. Firewall Rules Re-apply Per Server [unspecified-high]
|
||||||
|
- [x] 11. Input Rule (WG Port) Per Server [quick]
|
||||||
|
- [x] 12. servers.go Create/Update Multi-Interface [quick]
|
||||||
|
- [x] 13. peers.go Device Creation with Server Context [quick]
|
||||||
|
- [x] 14. peer_sync.go SyncLocalPeers Per Server [quick]
|
||||||
|
- [x] 15. rules.go syncRuleToFirewall Per Server [quick]
|
||||||
|
- [x] 16. provisioning.go Server-Aware [quick]
|
||||||
|
- [x] 17. Remove Hardcoded Local Primary Node References [quick]
|
||||||
|
- [x] 18. Migration Script + Backfill Defaults [quick]
|
||||||
|
- [x] 19. Unit Tests for Multi-Interface Logic [unspecified-low]
|
||||||
|
- [x] 20. Integration Test: 2 Local Nodes Different Subnets [unspecified-high]
|
||||||
|
- [x] 21. Manual QA Checklist Execution [unspecified-high]
|
||||||
|
- [x] F1. Plan Compliance Audit — oracle
|
||||||
|
- [x] F2. Code Quality Review — unspecified-high
|
||||||
|
- [x] F3. Real Manual QA — unspecified-high + playwright
|
||||||
|
- [x] F4. Scope Fidelity Check — deep
|
||||||
@@ -94,7 +94,7 @@ Wave 2 (Integration + Polish):
|
|||||||
|
|
||||||
## TODOs
|
## TODOs
|
||||||
|
|
||||||
- [ ] 1. Add limit parameter to traffic API
|
- [x] 1. Add limit parameter to traffic API
|
||||||
|
|
||||||
**What to do**:
|
**What to do**:
|
||||||
- In `apps/server-core/api/traffic.go`, modify `parseTimeRange` to also parse `limit` query parameter
|
- In `apps/server-core/api/traffic.go`, modify `parseTimeRange` to also parse `limit` query parameter
|
||||||
@@ -135,7 +135,7 @@ Wave 2 (Integration + Polish):
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- [ ] 2. Silent auto-refresh + pagination fix
|
- [x] 2. Silent auto-refresh + pagination fix
|
||||||
|
|
||||||
**What to do**:
|
**What to do**:
|
||||||
- Modify `fetchTrafficData` to accept optional `silent` parameter (default false)
|
- Modify `fetchTrafficData` to accept optional `silent` parameter (default false)
|
||||||
@@ -179,7 +179,7 @@ Wave 2 (Integration + Polish):
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- [ ] 3. Chart downsampling
|
- [x] 3. Chart downsampling
|
||||||
|
|
||||||
**What to do**:
|
**What to do**:
|
||||||
- In `TrafficHistory.vue`, add a `chartDataLimited` computed that limits chart data to max 200 points
|
- In `TrafficHistory.vue`, add a `chartDataLimited` computed that limits chart data to max 200 points
|
||||||
@@ -215,7 +215,7 @@ Wave 2 (Integration + Polish):
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- [ ] 4. CSV export optimization
|
- [x] 4. CSV export optimization
|
||||||
|
|
||||||
**What to do**:
|
**What to do**:
|
||||||
- Change exportToCSV to export only `paginatedData` (current page) by default
|
- Change exportToCSV to export only `paginatedData` (current page) by default
|
||||||
@@ -244,7 +244,7 @@ Wave 2 (Integration + Polish):
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- [ ] 5. Build verify all changes
|
- [x] 5. Build verify all changes
|
||||||
|
|
||||||
**What to do**:
|
**What to do**:
|
||||||
- Run `cd apps/server-core && go build ./...`
|
- Run `cd apps/server-core && go build ./...`
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Android Agent Build & Deploy
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
- Android SDK installed (compileSdk 34, minSdk 26)
|
||||||
|
- Device connected via USB with USB debugging enabled
|
||||||
|
- `adb devices` shows device
|
||||||
|
|
||||||
|
## Device
|
||||||
|
```
|
||||||
|
9b2adc22 device
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
### Build Debug APK
|
||||||
|
```powershell
|
||||||
|
cd D:\www-project\NexusGuard\apps\android-agent
|
||||||
|
.\gradlew assembleDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
Output: `app/build/outputs/apk/debug/app-debug.apk`
|
||||||
|
|
||||||
|
### Install on Device
|
||||||
|
```powershell
|
||||||
|
adb install -r app\build\outputs\apk\debug\app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
### Launch App
|
||||||
|
```powershell
|
||||||
|
adb shell am start -n com.datadunia.nexusguard/.MainActivity
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
```powershell
|
||||||
|
adb logcat -s AgentService GrpcClient Heartbeat TunnelManager Provisioning LogBuffer
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Device Connection
|
||||||
|
```powershell
|
||||||
|
adb devices
|
||||||
|
```
|
||||||
|
|
||||||
|
## One-Shot Build + Install + Launch
|
||||||
|
```powershell
|
||||||
|
cd D:\www-project\NexusGuard\apps\android-agent
|
||||||
|
.\gradlew assembleDebug && adb install -r app\build\outputs\apk\debug\app-debug.apk && adb shell am start -n com.datadunia.nexusguard/.MainActivity
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture (matches device-agent)
|
||||||
|
- **gRPC signaling**: GrpcClient.kt — bidirectional streaming with fallback (HTTPS domain → WG IP → plaintext)
|
||||||
|
- **HTTP heartbeat**: Heartbeat.kt — POST /api/v1/heartbeat every 30s (fallback when gRPC down)
|
||||||
|
- **Background service**: AgentService.kt — foreground service with notification (START_STICKY)
|
||||||
|
- **Tunnel**: TunnelManager.kt — WireGuard via VPNService + GoBackend
|
||||||
|
- **HWID**: SHA-256(ANDROID_ID + manufacturer + model + device), persisted in EncryptedSharedPreferences
|
||||||
|
- **Provisioning**: Provisioning.kt — HTTP POST /api/v1/provisioning
|
||||||
|
- **Auto-start**: BootReceiver.kt — starts agent on device boot
|
||||||
|
|
||||||
|
## Commands (Agent)
|
||||||
|
- gRPC: HelloMessage → ConfigUpdate (full tunnel config + port forwards)
|
||||||
|
- gRPC: HeartbeatRequest every 30s → HeartbeatResponse (config_hash + forwards_changed)
|
||||||
|
- gRPC: Suspend/Resume/Reconnect/Disconnect from server
|
||||||
|
- HTTP heartbeat: fallback when gRPC disconnected
|
||||||
|
|
||||||
|
## .gitignore (Android artifacts)
|
||||||
|
```
|
||||||
|
*.aar
|
||||||
|
*.apk
|
||||||
|
*.aab
|
||||||
|
android-agent/app/build/
|
||||||
|
android-agent/build/
|
||||||
|
android-agent/.gradle/
|
||||||
|
android-agent/local.properties
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commit Workflow
|
||||||
|
```powershell
|
||||||
|
# 1. Build
|
||||||
|
cd D:\www-project\NexusGuard\apps\android-agent
|
||||||
|
.\gradlew assembleDebug
|
||||||
|
|
||||||
|
# 2. Install + test
|
||||||
|
adb install -r app\build\outputs\apk\debug\app-debug.apk
|
||||||
|
adb shell am start -n com.datadunia.nexusguard/.MainActivity
|
||||||
|
|
||||||
|
# 3. Commit submodule
|
||||||
|
cd D:\www-project\NexusGuard\apps\android-agent
|
||||||
|
git add -A
|
||||||
|
git commit -m "feat: message"
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
# 4. Commit parent repo
|
||||||
|
cd D:\www-project\NexusGuard
|
||||||
|
git add apps/android-agent
|
||||||
|
git commit -m "chore: update android-agent submodule"
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
@@ -1,11 +1,22 @@
|
|||||||
# PROJECT KNOWLEDGE BASE
|
# PROJECT KNOWLEDGE BASE
|
||||||
|
|
||||||
**Generated:** 2026-05-22
|
**Generated:** 2026-06-20
|
||||||
**Commit:** `92051d5`
|
|
||||||
**Branch:** `main`
|
**Branch:** `main`
|
||||||
|
|
||||||
## OVERVIEW
|
## OVERVIEW
|
||||||
NexusGuard SD-WAN Suite — Enterprise Zero-Trust SD-WAN with WireGuard tunneling, centralized IPAM, and real-time nftables network isolation. Monorepo with 3 git submodules: Go backend (Gin), Vue 3 dashboard, Go device agent.
|
NexusGuard SD-WAN Suite: Enterprise Zero-Trust SD-WAN with WireGuard tunneling, centralized IPAM, and real-time nftables network isolation. Monorepo with 3 git submodules: Go backend (Gin), Vue 3 dashboard, Go device agent.
|
||||||
|
|
||||||
|
## TOPOLOGY
|
||||||
|
|
||||||
|
| Host | SSH | Role |
|
||||||
|
|------|-----|------|
|
||||||
|
| Production server | `root@172.20.8.191` | Runs server-core, Postgres, Redis, nginx, nftables, WireGuard |
|
||||||
|
| Gitea server | `root@172.20.8.92` | Private Git hosting (`git.datadunia.com`) |
|
||||||
|
|
||||||
|
- Server project folder: `/root/Nexus-Guard-Suite`
|
||||||
|
- Deploy: `./update.sh` (don't build manually)
|
||||||
|
- Actual WireGuard wg0 IP: `10.172.21.1/24` (on server 172.20.8.191)
|
||||||
|
- Agent WG IPs: dynamic from pool `10.172.21.0/24`
|
||||||
|
|
||||||
## STRUCTURE
|
## STRUCTURE
|
||||||
```
|
```
|
||||||
@@ -13,14 +24,13 @@ NexusGuard SD-WAN Suite — Enterprise Zero-Trust SD-WAN with WireGuard tunnelin
|
|||||||
├── apps/
|
├── apps/
|
||||||
│ ├── server-core/ # Go/Gin API backend (submodule)
|
│ ├── server-core/ # Go/Gin API backend (submodule)
|
||||||
│ ├── dashboard-ui/ # Vue 3 + Vite frontend (submodule)
|
│ ├── dashboard-ui/ # Vue 3 + Vite frontend (submodule)
|
||||||
│ └── device-agent/ # Go stealth daemon (submodule)
|
│ └── device-agent/ # Go stealth daemon + system tray (submodule)
|
||||||
├── docker-compose.yml # Production orchestration
|
├── docker-compose.yml # Production orchestration
|
||||||
├── docker-compose.dev.yml # Dev (air hot-reload)
|
├── docker-compose.dev.yml# Dev (air hot-reload)
|
||||||
├── Makefile # up/down/dev/migrate/reset-db
|
├── Makefile # up/down/dev/migrate/reset-db
|
||||||
├── setup.sh # First-run: generate .env + random keys
|
├── update.sh # Docker update: auto-generate .env + pull/build/migrate
|
||||||
├── update.sh # Docker update: pull/build/migrate
|
|
||||||
├── nexusguard-install.sh # Native install (systemd + nginx)
|
├── nexusguard-install.sh # Native install (systemd + nginx)
|
||||||
├── nexusguard-uninstall.sh # Native uninstall
|
├── nexusguard-uninstall.sh
|
||||||
├── .env.example # DB/JWT/SALT/VITE config template
|
├── .env.example # DB/JWT/SALT/VITE config template
|
||||||
├── .gitmodules # 3 submodules → git.datadunia.com
|
├── .gitmodules # 3 submodules → git.datadunia.com
|
||||||
└── .opencode/ # IDE agent config (tooling, not project code)
|
└── .opencode/ # IDE agent config (tooling, not project code)
|
||||||
@@ -28,6 +38,19 @@ NexusGuard SD-WAN Suite — Enterprise Zero-Trust SD-WAN with WireGuard tunnelin
|
|||||||
|
|
||||||
**CRITICAL**: `apps/*` are **git submodules** — clone with `--recurse-submodules`.
|
**CRITICAL**: `apps/*` are **git submodules** — clone with `--recurse-submodules`.
|
||||||
|
|
||||||
|
## SUBMODULE KNOWLEDGE BASES
|
||||||
|
|
||||||
|
Each submodule has its own `AGENTS.md` with detailed architecture, conventions, and anti-patterns:
|
||||||
|
|
||||||
|
| Submodule | AGENTS.md | Scope |
|
||||||
|
|-----------|-----------|-------|
|
||||||
|
| Server Core | `apps/server-core/AGENTS.md` | API handlers, database models, firewall rules, gRPC signaling, WireGuard management |
|
||||||
|
| Dashboard UI | `apps/dashboard-ui/AGENTS.md` | Vue 3 components, Pinia stores, TailwindCSS styling, API client |
|
||||||
|
| Device Agent | `apps/device-agent/AGENTS.md` | Go daemon, system tray, memory-injected WireGuard, heartbeat, gRPC signaling |
|
||||||
|
| Android Agent | `apps/android-agent/AGENTS.md` | Kotlin VPNService, GoBackend tunnel, HTTP heartbeat, port forwarding, boot auto-start |
|
||||||
|
|
||||||
|
**Rule**: When working on a submodule, ALWAYS read its `AGENTS.md` first for project-specific conventions.
|
||||||
|
|
||||||
## WHERE TO LOOK
|
## WHERE TO LOOK
|
||||||
| Task | Location | Notes |
|
| Task | Location | Notes |
|
||||||
|------|----------|-------|
|
|------|----------|-------|
|
||||||
@@ -35,30 +58,109 @@ NexusGuard SD-WAN Suite — Enterprise Zero-Trust SD-WAN with WireGuard tunnelin
|
|||||||
| Backend core | `apps/server-core/internal/` | auth, config, firewall, heartbeat, ipam, models, wgmanager |
|
| Backend core | `apps/server-core/internal/` | auth, config, firewall, heartbeat, ipam, models, wgmanager |
|
||||||
| Dev migration | `apps/server-core/main_dev.go` | GORM AutoMigrate (build tag `dev`) |
|
| Dev migration | `apps/server-core/main_dev.go` | GORM AutoMigrate (build tag `dev`) |
|
||||||
| Firewall rules | `apps/server-core/internal/firewall/` | nftables Linux rules |
|
| Firewall rules | `apps/server-core/internal/firewall/` | nftables Linux rules |
|
||||||
|
| gRPC signaling | `apps/server-core/signaling/` | Manager + Server: gRPC session tracking, Connect handler, recv loop |
|
||||||
| Dashboard views | `apps/dashboard-ui/src/views/` | Vue SFC pages |
|
| Dashboard views | `apps/dashboard-ui/src/views/` | Vue SFC pages |
|
||||||
| Dashboard API client | `apps/dashboard-ui/src/api/` | Axios API modules |
|
| Dashboard API client | `apps/dashboard-ui/src/api/` | Axios API modules |
|
||||||
| Dashboard stores | `apps/dashboard-ui/src/stores/` | Pinia state stores |
|
| Dashboard stores | `apps/dashboard-ui/src/stores/` | Pinia state stores |
|
||||||
| Agent client | `apps/device-agent/internal/client/` | Provisioning + heartbeat |
|
| Agent client | `apps/device-agent/internal/client/` | Provisioning + heartbeat |
|
||||||
|
| Agent signaling | `apps/device-agent/internal/signaling/` | gRPC connect with fallback + reconnect |
|
||||||
| Agent tunnel | `apps/device-agent/internal/tunnel/` | Memory-injected WireGuard |
|
| Agent tunnel | `apps/device-agent/internal/tunnel/` | Memory-injected WireGuard |
|
||||||
| Shared crypto | `apps/*/shared/crypto/encryptor.go` | AES-256-GCM (duplicated identical) |
|
| Shared crypto | `apps/*/shared/crypto/encryptor.go` | AES-256-GCM (duplicated identical) |
|
||||||
| CI workflows | `apps/*/.gitea/workflows/build.yml` | Gitea Actions per submodule |
|
| CI workflows | `apps/*/.gitea/workflows/build.yml` | Gitea Actions per submodule |
|
||||||
| Build config | `apps/dashboard-ui/vite.config.ts` | Vite 8 + Vue + TailwindCSS v4 |
|
| Build config | `apps/dashboard-ui/vite.config.ts` | Vite 8 + Vue + TailwindCSS v4 |
|
||||||
| Source of truth | `apps/server-core/docs/` | API_SPEC, KEY_ROTATION, PEER_DISCOVERY |
|
| Source of truth | `apps/server-core/docs/` | API_SPEC, KEY_ROTATION, PEER_DISCOVERY |
|
||||||
| Plan guardrails | `.sisyphus/plans/` | Anti-patterns, "Must NOT do" rules |
|
|
||||||
|
## SIGNALING ARCHITECTURE (CRITICAL)
|
||||||
|
|
||||||
|
### Topology
|
||||||
|
```
|
||||||
|
┌──────────────┐ ┌─────────────────┐ ┌──────────────┐
|
||||||
|
│ Dashboard │──HTTP──▶│ Server Core │◀─WG────▶│ Device Agent │
|
||||||
|
│ (Vue 3) │ :8080 │ (Go/Gin) │ :51820 │ (Go) │
|
||||||
|
└──────────────┘ │ │ └──────────────┘
|
||||||
|
│ Port 8080: │ │
|
||||||
|
│ - HTTP API │ ┌────┴────┐
|
||||||
|
│ - gRPC Signal │ │ TUN (wg)│
|
||||||
|
│ (cmux) │ │ Memory │
|
||||||
|
└─────────────────┘ └─────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transport Fallback Chain (Agent → Server)
|
||||||
|
1. gRPC via HTTPS domain (TLS) → `api-nexus.datadunia.com:443`
|
||||||
|
2. gRPC via WireGuard IP (insecure, tunnel-encrypted) → `10.172.21.1:8080`
|
||||||
|
3. HTTP heartbeat (fallback) → `serverURL/api/v1/heartbeat`
|
||||||
|
|
||||||
|
**Smart reconnection:** If gRPC was never connected (HTTPS blocked by Cloudflare), agent skips HTTPS on reconnect and only tries WG IP. If gRPC was connected before, agent tries last successful transport first.
|
||||||
|
|
||||||
|
### Heartbeat = PRIMARY Channel
|
||||||
|
Always runs. Handles:
|
||||||
|
- Health check (30s interval)
|
||||||
|
- Config sync (detects config changes → rebuild tunnel)
|
||||||
|
- Handshake monitoring (rebuilds tunnel if lastHandshake > 120s)
|
||||||
|
- Recovery after failure (wasFailing → OnRecovered → full rebuild)
|
||||||
|
|
||||||
|
### gRPC = REAL-TIME Channel (when connected)
|
||||||
|
Handles with **zero delay**:
|
||||||
|
- **ConfigUpdate** → immediate tunnel rebuild (no heartbeat wait)
|
||||||
|
- Suspend / Resume → immediate tunnel stop/rebuild
|
||||||
|
- Reconnect / Disconnect → immediate action
|
||||||
|
- StatusReport from agent (tunnel_up, lastHandshake, state)
|
||||||
|
- Ping/Pong keepalive (20s)
|
||||||
|
|
||||||
|
When gRPC disconnects → falls back to HTTP heartbeat (30s delay for config sync).
|
||||||
|
|
||||||
|
### gRPC Port Multiplexing
|
||||||
|
HTTP + gRPC share port 8080 via `cmux`:
|
||||||
|
- Server: `cmux.New(lis)` → match gRPC by `content-type` header, match HTTP by `Any()`
|
||||||
|
- Agent connects to same port for both HTTP API and gRPC
|
||||||
|
|
||||||
|
### Key Design Decisions
|
||||||
|
- Agent NEVER destroys tunnel on heartbeat failure — only rebuilds
|
||||||
|
- `OnFailure = log only`, `OnRecovered = full rebuild`
|
||||||
|
- gRPC OnDisconnect/OnGRPCFailed just log — heartbeat continues
|
||||||
|
- Heartbeat reads `last_handshake_time_sec` from WG IPC to detect stale tunnel
|
||||||
|
- gRPC StatusReport sends handshake age to server every 30s
|
||||||
|
- Server WG IP read from actual kernel interface (`net.InterfaceByName`), NOT from stale DB
|
||||||
|
|
||||||
|
### Agent Connection Lifecycle
|
||||||
|
1. Provision → register with server, get WireGuard config
|
||||||
|
2. Start tunnel (memory-injected, no disk files)
|
||||||
|
3. Start heartbeat (always, primary channel)
|
||||||
|
4. Start gRPC (if ServerWGIP available, bonus channel)
|
||||||
|
5. On gRPC ConfigUpdate → immediate tunnel rebuild (no delay)
|
||||||
|
6. On heartbeat config change → rebuild tunnel (30s delay, HTTP fallback)
|
||||||
|
7. On heartbeat stale handshake → rebuild tunnel
|
||||||
|
8. On heartbeat failure+recovery → rebuild tunnel
|
||||||
|
9. On gRPC suspend → send heartbeat(tunnel_up=true) → stop tunnel (DisconnectReason.SUSPENDED) → heartbeat continues
|
||||||
|
10. On gRPC resume → rebuild tunnel from server config → send heartbeat(tunnel_up=true)
|
||||||
|
|
||||||
|
### Protobuf Messages
|
||||||
|
- **Agent → Server**: HelloMessage, HeartbeatAck, StatusReport, PingMessage
|
||||||
|
- **Server → Agent**: ConfigUpdate, SuspendCommand, ResumeCommand, ReconnectCommand, DisconnectCommand, KeepAlive, PongMessage
|
||||||
|
|
||||||
## CODE MAP
|
## CODE MAP
|
||||||
| Symbol | Type | Location | Role |
|
| Symbol | Type | Location | Role |
|
||||||
|--------|------|----------|------|
|
|--------|------|----------|------|
|
||||||
| `main()` (server-core) | func | `apps/server-core/main.go` | Entry: CLI flags + Gin init |
|
| `main()` (server-core) | func | `apps/server-core/main.go` | Entry: CLI flags + Gin init |
|
||||||
| `main()` (device-agent) | func | `apps/device-agent/main.go` | Entry: agent daemon lifecycle |
|
| `main()` (device-agent) | func | `apps/device-agent/main.go` | Entry: systray + agent daemon lifecycle |
|
||||||
|
| `onReady()` / `onExit()` | func | `apps/device-agent/main.go` | System tray setup and cleanup |
|
||||||
|
| `startAgent()` / `stopAgent()` | func | `apps/device-agent/main.go` | Agent connect/disconnect lifecycle |
|
||||||
|
| `generateIcon()` | func | `apps/device-agent/icon.go` | 16x16 shield icon for tray |
|
||||||
| `config.Load()` | func | `apps/server-core/internal/config/` | Env-based config loader |
|
| `config.Load()` | func | `apps/server-core/internal/config/` | Env-based config loader |
|
||||||
| `config.LoadConfFile()` | func | `apps/server-core/internal/config/config_loader.go` | Config file parser (.env / nexusguard.conf) |
|
| `config.LoadConfFile()` | func | `apps/server-core/internal/config/config_loader.go` | Config file parser (.env / nexusguard.conf) |
|
||||||
| `auth.Init()` | func | `apps/server-core/internal/auth/` | JWT sign/verify init |
|
| `auth.Init()` | func | `apps/server-core/internal/auth/` | JWT sign/verify init |
|
||||||
| `firewall.InitNetwork()` | func | `apps/server-core/internal/firewall/` | nftables table/set creation |
|
| `firewall.InitNetwork()` | func | `apps/server-core/internal/firewall/` | nftables table/set creation |
|
||||||
| `ipam.AllocateIP()` | func | `apps/server-core/internal/ipam/` | IP pool allocation from CIDR |
|
| `ipam.AllocateIP()` | func | `apps/server-core/internal/ipam/` | IP pool allocation from CIDR |
|
||||||
| `wgmanager.SetConfig()` | func | `apps/server-core/internal/wgmanager/` | WireGuard config push |
|
| `wgmanager.SetConfig()` | func | `apps/server-core/internal/wgmanager/` | WireGuard config push |
|
||||||
|
| `wgmanager.GetInterfaceAddress()` | func | `apps/server-core/internal/wgmanager/` | Read actual WG interface IP from kernel |
|
||||||
| `models.AutoMigrate()` | func | `apps/server-core/internal/models/` | GORM schema migration |
|
| `models.AutoMigrate()` | func | `apps/server-core/internal/models/` | GORM schema migration |
|
||||||
| `encrypt()` / `decrypt()` | func | `apps/*/shared/crypto/encryptor.go` | AES-256-GCM (identical) |
|
| `encrypt()` / `decrypt()` | func | `apps/*/shared/crypto/encryptor.go` | AES-256-GCM (identical) |
|
||||||
|
| `StartHeartbeat()` | func | `apps/device-agent/internal/client/heartbeat.go` | Heartbeat loop + handshake monitoring |
|
||||||
|
| `checkHandshake()` | func | `apps/device-agent/internal/client/heartbeat.go` | Read WG IPC handshake time |
|
||||||
|
| `ConnectAndRun()` | func | `apps/device-agent/internal/signaling/client.go` | gRPC connect with fallback + reconnect |
|
||||||
|
| `statusLoop()` | func | `apps/device-agent/internal/signaling/client.go` | Sends StatusReport every 30s |
|
||||||
|
| `NewManager()` | func | `apps/server-core/signaling/manager.go` | gRPC session tracking |
|
||||||
|
| `NewServer()` | func | `apps/server-core/signaling/server.go` | gRPC Connect handler + recv loop |
|
||||||
|
|
||||||
## CONVENTIONS
|
## CONVENTIONS
|
||||||
- **Go**: Standard layout (`main.go` in root, `internal/`, `api/`)
|
- **Go**: Standard layout (`main.go` in root, `internal/`, `api/`)
|
||||||
@@ -76,10 +178,12 @@ NexusGuard SD-WAN Suite — Enterprise Zero-Trust SD-WAN with WireGuard tunnelin
|
|||||||
|
|
||||||
## ANTI-PATTERNS (THIS PROJECT)
|
## ANTI-PATTERNS (THIS PROJECT)
|
||||||
- **NEVER** `nft flush table` — only atomic add/remove
|
- **NEVER** `nft flush table` — only atomic add/remove
|
||||||
|
- **NEVER** commit temp/debug/test files (`nft-fix.sh`, `temp_*.txt` etc) in project root; use `.tests/` folder
|
||||||
- **NEVER** log plaintext or encryption keys
|
- **NEVER** log plaintext or encryption keys
|
||||||
- **NEVER** reopen completed phases/commits — fix forward only
|
- **NEVER** reopen completed phases/commits — fix forward only
|
||||||
- **NEVER** rebuild `shared/crypto/encryptor.go` — copy identical file
|
- **NEVER** rebuild `shared/crypto/encryptor.go` — copy identical file
|
||||||
- **NEVER** commit build artifacts (binaries, `dist/`)
|
- **NEVER** commit build artifacts (binaries, `dist/`, APK, `.apk`, `.aab`)
|
||||||
|
- **NEVER** leave temp/debug/test outputs in project root — all must go in `.tests/` folder
|
||||||
- **NEVER** force push
|
- **NEVER** force push
|
||||||
- **NEVER** create cross-phase workarounds
|
- **NEVER** create cross-phase workarounds
|
||||||
|
|
||||||
@@ -115,6 +219,15 @@ if d.AllowInternet {
|
|||||||
// Do NOT override with EndpointAllowedIPs here
|
// Do NOT override with EndpointAllowedIPs here
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Client-side AllowedIPs resolution (`Device.ResolveAllowedIPs()`)
|
||||||
|
Centralized in `internal/models/models.go`. Used by `buildConfigUpdate()`, heartbeat, and provisioning.
|
||||||
|
Priority chain:
|
||||||
|
1. `AllowInternet` → `0.0.0.0/0`
|
||||||
|
2. `EndpointAllowedIPs` (if set) → use as-is
|
||||||
|
3. Fallback → compose `IPPoolCIDR, serverWGIP/32` (e.g. `10.172.21.0/24,10.172.21.1/32`)
|
||||||
|
|
||||||
|
**Never** use `InternalIP/32` as client-side AllowedIPs — that value is server-side WireGuard kernel routing only.
|
||||||
|
|
||||||
### Database field: `endpoint_allowed_ips`
|
### Database field: `endpoint_allowed_ips`
|
||||||
- Used for **client config** and **firewall rules**
|
- Used for **client config** and **firewall rules**
|
||||||
- NOT used for server-side WireGuard kernel config
|
- NOT used for server-side WireGuard kernel config
|
||||||
@@ -196,10 +309,43 @@ go run -tags dev ./apps/server-core -create-admin -user admin -pass "..."
|
|||||||
sudo /usr/local/bin/nexusguard-server -create-admin -user admin -pass "..."
|
sudo /usr/local/bin/nexusguard-server -create-admin -user admin -pass "..."
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## heartbeat server <-> device-agent
|
||||||
|
|
||||||
|
Konsep yang Benar
|
||||||
|
Heartbeat = satu konsep, tiga jalur transport:
|
||||||
|
|
||||||
|
0 Transport Protocol Endpoint Kapan Dipakai
|
||||||
|
1 gRPC via domain (HTTPS proxy) gRPC bidi stream api-nexus.datadunia.com:443 Pertama dicoba
|
||||||
|
2 gRPC via WG IP (direct) gRPC bidi stream 10.172.21.1:8080 Fallback jika proxy swallowed
|
||||||
|
3 HTTP API REST POST /api/v1/heartbeat Fallback terakhir / always running
|
||||||
|
Satu konsep yang sama: kirim config_hash + last_handshake + tunnel_up → server compare → respond dengan config jika berubah.
|
||||||
|
|
||||||
|
Yang Perlu Diperbaiki
|
||||||
|
client.go: Perlu ada gRPC heartbeat loop (kirim HeartbeatRequest via stream periodik) + handle HeartbeatResponse
|
||||||
|
heartbeat.go: HTTP heartbeat tetap ada sebagai fallback
|
||||||
|
Transport switching: Saat gRPC connected → heartbeat via gRPC. Saat gRPC disconnected → heartbeat via HTTP
|
||||||
|
handler.go: Perlu handleHeartbeatResponse untuk process config dari gRPC heartbeat
|
||||||
|
Server manager.go: Perlu sendMu untuk prevent concurrent stream.Send()
|
||||||
|
Alur yang Benar (setelah perbaikan)
|
||||||
|
Agent Start
|
||||||
|
→ Provision (HTTP) → config pertama dari HTTP API → build tunnel
|
||||||
|
→ Start HTTP heartbeat (always running, fallback transport)
|
||||||
|
→ Start gRPC (HTTPS → WG IP)
|
||||||
|
|
||||||
|
gRPC Connected:
|
||||||
|
→ establishStream: kirim HelloMessage → terima ConfigUpdate (verify only, jangan rebuild)
|
||||||
|
→ heartbeatLoop: kirim HeartbeatRequest via gRPC setiap 30s
|
||||||
|
→ Server respond: HeartbeatResponse (config_changed? → rebuild via handler)
|
||||||
|
→ dispatch: handle Suspend/Resume/ConfigUpdate/Reconnect/Disconnect
|
||||||
|
|
||||||
|
gRPC Disconnected:
|
||||||
|
→ HTTP heartbeat continues (unaffected)
|
||||||
|
→ gRPC reconnect loop
|
||||||
|
→ When reconnected → switch heartbeat back to gRPC
|
||||||
|
|
||||||
## NOTES
|
## NOTES
|
||||||
- Submodules → private Gitea (`git.datadunia.com`); CI via Gitea Actions per submodule
|
- Submodules → private Gitea (`git.datadunia.com`); CI via Gitea Actions per submodule
|
||||||
- Go versions diverge: server-core `1.25.7`, device-agent `1.25.1`
|
- Go versions diverge: server-core `1.25.7`, device-agent `1.25.1`
|
||||||
- No root linter configs (`.golangci.yml`, `.eslintrc`, `.editorconfig`)
|
- No root linter configs (`.golangci.yml`, `.eslintrc`, `.editorconfig`)
|
||||||
- Shell scripts use deprecated `docker-compose` v1, Makefile uses `docker compose` v2
|
- Shell scripts use deprecated `docker-compose` v1, Makefile uses `docker compose` v2
|
||||||
- Root has stale artifacts: `connect_remote.txt`, `temp_section*.txt`
|
|
||||||
- `package.json` name is `"temp-ui"` (stale scaffold remnant)
|
- `package.json` name is `"temp-ui"` (stale scaffold remnant)
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
NexusGuard is an Enterprise Zero-Trust SD-WAN solution built with Go, Vue 3, and WireGuard. It enables stealth VPN tunneling, centralized IPAM, and real-time network isolation via `nftables`.
|
NexusGuard is an Enterprise Zero-Trust SD-WAN solution built with Go, Vue 3, and WireGuard. It enables stealth VPN tunneling, centralized IPAM, and real-time network isolation via `nftables`.
|
||||||
|
|
||||||
## 🏗️ System Architecture
|
## 🏗️ System Architecture
|
||||||
This suite contains three main components:
|
This suite contains four main components:
|
||||||
1. **[Server Core](http://git.datadunia.com/nexusguard/nexus-server-core)**: The central API and VPN Hub managing database state, token distribution, and Linux firewall isolation.
|
1. **[Server Core](http://git.datadunia.com/nexusguard/nexus-server-core)**: The central API and VPN Hub managing database state, token distribution, and Linux firewall isolation.
|
||||||
2. **[Dashboard UI](http://git.datadunia.com/nexusguard/nexus-dashboard-ui)**: The Admin Web GUI for managing Nodes, Users, Devices, and Firewall rules. Features a futuristic glassmorphism design system using Vue 3, Vite, TailwindCSS v4, and HeadlessUI.
|
2. **[Dashboard UI](http://git.datadunia.com/nexusguard/nexus-dashboard-ui)**: The Admin Web GUI for managing Nodes, Users, Devices, and Firewall rules. Features a futuristic glassmorphism design system using Vue 3, Vite, TailwindCSS v4, and HeadlessUI.
|
||||||
3. **[Device Agent](http://git.datadunia.com/nexusguard/nexus-device-agent)**: A stealth background daemon for Linux client machines that establishes memory-injected WireGuard tunnels.
|
3. **[Device Agent](http://git.datadunia.com/nexusguard/nexus-device-agent)**: A stealth background daemon for Linux/Windows/macOS client machines that establishes memory-injected WireGuard tunnels.
|
||||||
|
4. **[Android Agent](http://git.datadunia.com/nexusguard/nexus-android-agent)**: Android-based WireGuard client for mobile devices using VPNService.
|
||||||
|
5. **ESP32 Agent**: Architecture reference only (see `reference/esp32-agent/`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -18,16 +20,17 @@ Clone this repository and start all services:
|
|||||||
```bash
|
```bash
|
||||||
git clone https://git.datadunia.com/nexusguard/Nexus-Guard-Suite.git
|
git clone https://git.datadunia.com/nexusguard/Nexus-Guard-Suite.git
|
||||||
cd Nexus-Guard-Suite
|
cd Nexus-Guard-Suite
|
||||||
./setup.sh # Generate .env file automatically
|
bash update.sh # Auto-generates .env + pulls, builds, migrates, starts
|
||||||
# Edit .env with your configuration
|
|
||||||
bash update.sh # Pull, build, migrate, and start
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The first run will automatically:
|
The first run will automatically:
|
||||||
- Pull the latest code and build the Docker containers (via `update.sh`).
|
- Generate `.env` from `.env.example` with auto-generated JWT/SALT keys.
|
||||||
|
- Pull the latest code and build the Docker containers.
|
||||||
- Auto-generate the **Local Primary Node** WireGuard keys on the first boot.
|
- Auto-generate the **Local Primary Node** WireGuard keys on the first boot.
|
||||||
- Create the database schema via automated migration.
|
- Create the database schema via automated migration.
|
||||||
|
|
||||||
|
Edit `.env` afterward to customize `DB_PASSWORD`, `API_PORT`, `VITE_API_BASE_URL`.
|
||||||
|
|
||||||
**Update Options:**
|
**Update Options:**
|
||||||
```bash
|
```bash
|
||||||
bash update.sh # Smart update (only rebuild if changes detected)
|
bash update.sh # Smart update (only rebuild if changes detected)
|
||||||
@@ -221,6 +224,41 @@ For automated client deployment with the Device Agent:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 📚 Documentation
|
||||||
|
|
||||||
|
Documentation is built with VitePress and served at `/docs/`.
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/docs
|
||||||
|
npm install
|
||||||
|
npm run docs:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building Docs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/docs
|
||||||
|
npm run docs:build
|
||||||
|
```
|
||||||
|
|
||||||
|
The output is in `apps/docs/.vitepress/dist/`.
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
- `apps/docs/` — VitePress root (i18n: Indonesian + English)
|
||||||
|
- `apps/server-core/docs/` — Backend API docs & guides
|
||||||
|
- `apps/dashboard-ui/docs/` — Dashboard UI docs
|
||||||
|
- `apps/device-agent/docs/` — Device Agent docs
|
||||||
|
|
||||||
|
### Languages
|
||||||
|
|
||||||
|
- **Bahasa Indonesia** (default): `/docs/`
|
||||||
|
- **English**: `/docs/en/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📁 File Structure
|
## 📁 File Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -228,12 +266,14 @@ NexusGuard/
|
|||||||
├── apps/
|
├── apps/
|
||||||
│ ├── server-core/ # Go/Gin API backend
|
│ ├── server-core/ # Go/Gin API backend
|
||||||
│ ├── dashboard-ui/ # Vue 3 frontend
|
│ ├── dashboard-ui/ # Vue 3 frontend
|
||||||
│ └── device-agent/ # Go client agent
|
│ ├── device-agent/ # Go client agent
|
||||||
|
│ └── android-agent/ # Kotlin Android agent
|
||||||
|
├── reference/
|
||||||
|
│ └── esp32-agent/ # ESP32 architecture docs (not code)
|
||||||
├── bin/ # Built binaries (gitignored)
|
├── bin/ # Built binaries (gitignored)
|
||||||
├── docker-compose.yml # Docker orchestration
|
├── docker-compose.yml # Docker orchestration
|
||||||
├── docker-compose.dev.yml # Docker dev (hot-reload)
|
├── docker-compose.dev.yml # Docker dev (hot-reload)
|
||||||
├── update.sh # Docker update script
|
├── update.sh # Docker update + auto-setup script
|
||||||
├── setup.sh # Docker initial setup
|
|
||||||
├── nexusguard-install.sh # Native install script
|
├── nexusguard-install.sh # Native install script
|
||||||
├── nexusguard-uninstall.sh # Native uninstall script
|
├── nexusguard-uninstall.sh # Native uninstall script
|
||||||
├── .env.example # Environment template
|
├── .env.example # Environment template
|
||||||
|
|||||||
Submodule
+1
Submodule apps/android-agent added at ce70c3e197
+1
-1
Submodule apps/dashboard-ui updated: 458f8c5917...1be8f9f20c
+1
-1
Submodule apps/device-agent updated: 314d8fd7bb...651da0ad33
@@ -1,22 +1,50 @@
|
|||||||
import { defineConfig } from 'vitepress'
|
import { defineConfig } from 'vitepress'
|
||||||
|
|
||||||
|
// Sidebar manifests from submodules
|
||||||
|
import serverCoreSidebar from '../../server-core/docs/sidebar.json'
|
||||||
|
import dashboardSidebar from '../../dashboard-ui/docs/sidebar.json'
|
||||||
|
import agentSidebar from '../../device-agent/docs/sidebar.json'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
title: 'NexusGuard Docs',
|
title: 'NexusGuard',
|
||||||
description: 'Enterprise Zero-Trust SD-WAN Documentation',
|
description: 'Enterprise Zero-Trust SD-WAN Suite',
|
||||||
|
base: '/docs/',
|
||||||
cleanUrls: true,
|
cleanUrls: true,
|
||||||
|
|
||||||
|
locales: {
|
||||||
|
root: {
|
||||||
|
label: 'Bahasa Indonesia',
|
||||||
|
lang: 'id',
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
sidebar: [
|
sidebar: [
|
||||||
{
|
serverCoreSidebar,
|
||||||
text: 'Guides',
|
dashboardSidebar,
|
||||||
items: [
|
agentSidebar,
|
||||||
{ text: 'Sign In', link: '/guides/sign-in' },
|
],
|
||||||
{ text: 'Access Remote Server', link: '/guides/access-remote-server' },
|
nav: [
|
||||||
{ text: 'Add WireGuard Configuration', link: '/guides/add-wireguard-configuration' },
|
{ text: 'Beranda', link: '/' },
|
||||||
{ text: 'Peers', link: '/guides/peers' },
|
{ text: 'Panduan', link: '/guides/' },
|
||||||
{ text: 'Email Service', link: '/guides/email-service' },
|
{ text: 'API', link: '/api/' },
|
||||||
{ text: 'WebHooks', link: '/guides/webhooks' },
|
{ text: 'GitHub', link: 'https://git.datadunia.com/nexusguard/Nexus-Guard-Suite' },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
},
|
||||||
}
|
en: {
|
||||||
|
label: 'English',
|
||||||
|
lang: 'en',
|
||||||
|
themeConfig: {
|
||||||
|
sidebar: [
|
||||||
|
serverCoreSidebar,
|
||||||
|
dashboardSidebar,
|
||||||
|
agentSidebar,
|
||||||
|
],
|
||||||
|
nav: [
|
||||||
|
{ text: 'Home', link: '/en/' },
|
||||||
|
{ text: 'Guides', link: '/en/guides/' },
|
||||||
|
{ text: 'API', link: '/en/api/' },
|
||||||
|
{ text: 'GitHub', link: 'https://git.datadunia.com/nexusguard/Nexus-Guard-Suite' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# NexusGuard API Reference
|
||||||
|
|
||||||
|
This page provides interactive documentation for the NexusGuard Server Core API.
|
||||||
|
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Load Swagger UI from CDN
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.src = 'https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js'
|
||||||
|
script.onload = () => {
|
||||||
|
window.SwaggerUIBundle({
|
||||||
|
url: '/server-core/docs/swagger.json',
|
||||||
|
dom_id: '#swagger-ui',
|
||||||
|
presets: [
|
||||||
|
window.SwaggerUIBundle.presets.apis,
|
||||||
|
window.SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||||
|
],
|
||||||
|
layout: 'BaseLayout'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
document.head.appendChild(script)
|
||||||
|
|
||||||
|
const link = document.createElement('link')
|
||||||
|
link.rel = 'stylesheet'
|
||||||
|
link.href = 'https://unpkg.com/swagger-ui-dist@5/swagger-ui.css'
|
||||||
|
document.head.appendChild(link)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Referensi API NexusGuard
|
||||||
|
|
||||||
|
Halaman ini menyediakan dokumentasi interaktif untuk API Server Core NexusGuard.
|
||||||
|
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.src = 'https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js'
|
||||||
|
script.onload = () => {
|
||||||
|
window.SwaggerUIBundle({
|
||||||
|
url: '/server-core/docs/swagger.json',
|
||||||
|
dom_id: '#swagger-ui',
|
||||||
|
presets: [
|
||||||
|
window.SwaggerUIBundle.presets.apis,
|
||||||
|
window.SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||||
|
],
|
||||||
|
layout: 'BaseLayout'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
document.head.appendChild(script)
|
||||||
|
|
||||||
|
const link = document.createElement('link')
|
||||||
|
link.rel = 'stylesheet'
|
||||||
|
link.href = 'https://unpkg.com/swagger-ui-dist@5/swagger-ui.css'
|
||||||
|
document.head.appendChild(link)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { join, dirname } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
const submodules = [
|
||||||
|
{ name: 'server-core', path: '../server-core/docs/sidebar.json' },
|
||||||
|
{ name: 'dashboard-ui', path: '../dashboard-ui/docs/sidebar.json' },
|
||||||
|
{ name: 'device-agent', path: '../device-agent/docs/sidebar.json' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const sidebar = [];
|
||||||
|
|
||||||
|
for (const mod of submodules) {
|
||||||
|
const filePath = join(__dirname, mod.path);
|
||||||
|
try {
|
||||||
|
const content = JSON.parse(readFileSync(filePath, 'utf-8'));
|
||||||
|
sidebar.push(content);
|
||||||
|
console.error(`\u2713 Loaded ${mod.name}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`\u26A0 Skipping ${mod.name}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(sidebar, null, 2));
|
||||||
Generated
+2552
File diff suppressed because it is too large
Load Diff
+1
-1
Submodule apps/server-core updated: e861996717...b4b4a172c8
@@ -15,11 +15,17 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
networks:
|
networks:
|
||||||
- nexusnet
|
- nexusnet
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
|
command: redis-server --save "" --appendonly no
|
||||||
volumes:
|
volumes:
|
||||||
- redisdata:/data
|
- redisdata:/data
|
||||||
ports:
|
ports:
|
||||||
@@ -30,6 +36,11 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
networks:
|
networks:
|
||||||
- nexusnet
|
- nexusnet
|
||||||
|
|
||||||
@@ -52,6 +63,11 @@ services:
|
|||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
- NET_RAW
|
- NET_RAW
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
dashboard-ui:
|
dashboard-ui:
|
||||||
build:
|
build:
|
||||||
@@ -61,9 +77,16 @@ services:
|
|||||||
VITE_API_BASE_URL: ${VITE_API_BASE_URL}
|
VITE_API_BASE_URL: ${VITE_API_BASE_URL}
|
||||||
ports:
|
ports:
|
||||||
- "${WEB_PORT:-80}:80"
|
- "${WEB_PORT:-80}:80"
|
||||||
|
volumes:
|
||||||
|
- ./apps/docs/.vitepress/dist:/usr/share/nginx/html/docs:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- server-core
|
- server-core
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
networks:
|
networks:
|
||||||
- nexusnet
|
- nexusnet
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Android Agent UI Redesign
|
||||||
|
|
||||||
|
**Date:** 2026-07-04
|
||||||
|
**Scope:** NexusGuard Android Agent main activity UI
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Complete redesign of the Android agent main activity UI with a dark theme, green accent color scheme, copy-tap support, and new port forwarding display.
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `app/src/main/res/values/strings.xml` | Added 13 new string resources for UI labels |
|
||||||
|
| `app/src/main/res/values/colors.xml` | Added 8 new color definitions (dark/green theme) |
|
||||||
|
| `app/src/main/res/values/themes.xml` | Applied custom Material3 theme attributes |
|
||||||
|
| `app/src/main/res/layout/activity_main.xml` | Complete layout redesign |
|
||||||
|
| `app/src/main/java/com/datadunia/nexusguard/MainActivity.kt` | Updated Kotlin bindings and logic |
|
||||||
|
|
||||||
|
## Design Changes
|
||||||
|
|
||||||
|
### Theme
|
||||||
|
- Dark background (`#0D1117`) with green accent (`#00C853`)
|
||||||
|
- Material3 `DayNight.NoActionBar` parent theme with custom color overrides
|
||||||
|
- Card borders use `#21262D` stroke color
|
||||||
|
|
||||||
|
### Header
|
||||||
|
- App name "NexusGuard" displayed in green accent color
|
||||||
|
- MaterialSwitch for VPN toggle with ON/OFF labels
|
||||||
|
- Status text below switch (colored by state)
|
||||||
|
|
||||||
|
### Status Device Agent Card
|
||||||
|
- Renamed from "Informasi Tunnel" to "Status Device Agent"
|
||||||
|
- Six data rows: IP Address, Admin Web, Device ID, Handshake Terakhir, Allowed IPs, Transport
|
||||||
|
- All value TextViews are selectable (`textIsSelectable="true"`) and copy-tappable
|
||||||
|
- Device ID displays full text without truncation (no `ellipsize` or `singleLine`)
|
||||||
|
- Admin Web shows fixed domain `https://api-nexus.datadunia.com`
|
||||||
|
|
||||||
|
### Port Forwarding Card (New)
|
||||||
|
- Displays active port forwards from `PortForwarder.getActiveForwards()`
|
||||||
|
- Shows protocol badge (TCP/UDP) and port mapping info
|
||||||
|
- Empty state message when no forwards are active
|
||||||
|
|
||||||
|
### Log Card
|
||||||
|
- Height reduced from 300dp to 250dp
|
||||||
|
- Monospace font at 10sp
|
||||||
|
- Log level filtering: WARN+ in release, all levels in debug
|
||||||
|
|
||||||
|
## Behavior Changes
|
||||||
|
|
||||||
|
### Copy-Tap
|
||||||
|
All value TextViews in the status card respond to tap with clipboard copy and "Disalin" Toast feedback.
|
||||||
|
|
||||||
|
### Log Level Filtering
|
||||||
|
`LogBuffer.initLevel(BuildConfig.DEBUG)` called in `onCreate()` — release builds only store WARN+ entries (50 max), debug builds store all.
|
||||||
|
|
||||||
|
### Port Forward Updates
|
||||||
|
`updatePortForwardCard()` called after every `updateCardFields()` in all tunnel states (CONNECTING, CONNECTED, FAILED).
|
||||||
|
|
||||||
|
## Layout IDs
|
||||||
|
|
||||||
|
| ID | Type | Purpose |
|
||||||
|
|----|------|---------|
|
||||||
|
| `switchVpn` | MaterialSwitch | VPN toggle |
|
||||||
|
| `tvStatus` | TextView | Connection status text |
|
||||||
|
| `cardInfo` | MaterialCardView | Status Device Agent card |
|
||||||
|
| `tvIpAddress` | TextView | Internal IP value |
|
||||||
|
| `tvAdminWeb` | TextView | Admin web domain (was `tvServerEndpoint`) |
|
||||||
|
| `tvDeviceId` | TextView | Device ID value |
|
||||||
|
| `tvHandshake` | TextView | Last handshake time |
|
||||||
|
| `tvAllowedIps` | TextView | Allowed IPs value |
|
||||||
|
| `tvTransport` | TextView | Transport protocol |
|
||||||
|
| `cardPorts` | MaterialCardView | Port Forwarding card |
|
||||||
|
| `portForwardContainer` | LinearLayout | Dynamic port forward items |
|
||||||
|
| `cardLog` | MaterialCardView | Log card |
|
||||||
|
| `tvLogScrollView` | ScrollView | Log scroll container |
|
||||||
|
| `tvLog` | TextView | Log text |
|
||||||
|
| `btnImportConfig` | MaterialButton | Import config button |
|
||||||
|
| `btnToggleLog` | MaterialButton | Log toggle button |
|
||||||
|
| `btnSettings` | ImageButton | Settings icon |
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
|
||||||
|
- `tvServerEndpoint` renamed to `tvAdminWeb` — any code referencing the old ID must be updated
|
||||||
|
- New string resources added — translations needed for non-Indonesian locales
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# NexusGuard SD-WAN Suite
|
||||||
|
|
||||||
|
> Enterprise Zero-Trust SD-WAN with WireGuard
|
||||||
|
|
||||||
|
NexusGuard is a production-grade, zero-trust SD-WAN solution built with Go, Vue 3, and WireGuard. It enables stealth VPN tunneling, centralized IPAM, and real-time network isolation via Linux `nftables` — all managed through a futuristic glassmorphism dashboard.
|
||||||
|
|
||||||
|
## Key Highlights
|
||||||
|
|
||||||
|
- **Zero-Attack Surface** — No public registration. Admin accounts created via CLI only.
|
||||||
|
- **Stealth Agent** — WireGuard config injected into memory. No files written to `/etc/wireguard/`.
|
||||||
|
- **Real-time Firewall** — Per-peer nftables rules synced to kernel instantly.
|
||||||
|
- **Cross-Platform** — Agent runs on Linux (daemon), Windows (tray + service), and macOS (tray).
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`Go` · `Vue 3` · `WireGuard` · `PostgreSQL` · `Redis` · `nftables` · `gRPC` · `Docker`
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐ ┌─────────────────┐ ┌──────────────┐
|
||||||
|
│ Dashboard │──HTTP──▶│ Server Core │◀─WG────▶│ Device Agent │
|
||||||
|
│ (Vue 3) │ :8080 │ (Go/Gin) │ :51820 │ (Go) │
|
||||||
|
└──────────────┘ │ │ └──────────────┘
|
||||||
|
│ Port 8080: │ │
|
||||||
|
│ - HTTP API │ ┌────┴────┐
|
||||||
|
│ - gRPC Signal │ │ TUN (wg)│
|
||||||
|
│ (cmux) │ │ Memory │
|
||||||
|
└─────────────────┘ └─────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Submodule Knowledge Bases
|
||||||
|
|
||||||
|
Each submodule has its own `AGENTS.md` with detailed architecture, conventions, and anti-patterns:
|
||||||
|
|
||||||
|
| Submodule | AGENTS.md | Scope |
|
||||||
|
|-----------|-----------|-------|
|
||||||
|
| [Server Core](apps/server-core/) | [AGENTS.md](apps/server-core/AGENTS.md) | API handlers, database models, firewall rules, gRPC signaling, WireGuard management |
|
||||||
|
| [Dashboard UI](apps/dashboard-ui/) | [AGENTS.md](apps/dashboard-ui/AGENTS.md) | Vue 3 components, Pinia stores, TailwindCSS styling, API client |
|
||||||
|
| [Device Agent](apps/device-agent/) | [AGENTS.md](apps/device-agent/AGENTS.md) | Go daemon, system tray, memory-injected WireGuard, heartbeat, gRPC signaling |
|
||||||
|
| [Android Agent](apps/android-agent/) | [AGENTS.md](apps/android-agent/AGENTS.md) | Kotlin VPNService, GoBackend tunnel, HTTP heartbeat, port forwarding |
|
||||||
|
|
||||||
|
**Rule**: When working on a submodule, always read its `AGENTS.md` first.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
| Document | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| [Architecture](architecture.md) | System architecture, data flow, security model |
|
||||||
|
| [Tech Stack](tech-stack.md) | Technology breakdown per component |
|
||||||
|
| [Features](features.md) | Feature showcase and capabilities |
|
||||||
|
| [Deployment](deployment.md) | Deployment guide (Docker, native, development) |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.datadunia.com/nexusguard/Nexus-Guard-Suite.git
|
||||||
|
cd Nexus-Guard-Suite
|
||||||
|
bash update.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Private — DataDunia
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
# Architecture Overview
|
||||||
|
|
||||||
|
NexusGuard is a three-component SD-WAN system: a central API server, a web dashboard, and cross-platform device agents. All communication is encrypted. Tunnels are fileless. Access is zero-trust.
|
||||||
|
|
||||||
|
## High-Level Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Dashboard (Vue 3) │
|
||||||
|
│ Glassmorphism Web Interface │
|
||||||
|
│ Manages: Nodes, Devices, Rules │
|
||||||
|
└───────────────────────────────────┬─────────────────────────────────┘
|
||||||
|
│ HTTP (port 80)
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Nginx Reverse Proxy │
|
||||||
|
│ Routes: /api/ → :8080, / → SPA │
|
||||||
|
└───────────────────────────────────┬─────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Server Core (Go/Gin) │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
|
||||||
|
│ │ HTTP API │ │ gRPC │ │ IPAM │ │ nftables │ │
|
||||||
|
│ │ (20+ │ │ Signaling│ │ Manager │ │ Firewall │ │
|
||||||
|
│ │ handlers)│ │ (cmux) │ │ │ │ │ │
|
||||||
|
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ ▼ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ PostgreSQL Database │ │
|
||||||
|
│ │ (wg_servers, devices, rules, users) │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────┐ ┌──────────────────────┐ │
|
||||||
|
│ │ Redis │ │ WireGuard │ │
|
||||||
|
│ │ (Heartbeat TTL) │ │ (wg0 interface) │ │
|
||||||
|
│ └──────────────────────┘ └──────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────────┴───────────────┐
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌───────────────────────────┐ ┌───────────────────────────┐
|
||||||
|
│ Device Agent (Linux) │ │ Device Agent (Win/Mac) │
|
||||||
|
│ Systemd Daemon │ │ System Tray │
|
||||||
|
│ Memory-injected WG │ │ Memory-injected WG │
|
||||||
|
└───────────────────────────┘ └───────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Breakdown
|
||||||
|
|
||||||
|
### Server Core
|
||||||
|
|
||||||
|
The central API and VPN hub. Written in Go with Gin framework.
|
||||||
|
|
||||||
|
| Responsibility | Implementation |
|
||||||
|
|----------------|----------------|
|
||||||
|
| API endpoints | 20+ Gin handlers (`api/` directory) |
|
||||||
|
| gRPC signaling | Bidirectional streaming via cmux (port 8080) |
|
||||||
|
| IPAM | IP pool allocation from CIDR per node |
|
||||||
|
| Firewall | nftables rule management (add/remove per peer) |
|
||||||
|
| WireGuard | Interface control, peer sync, config push |
|
||||||
|
| Auth | JWT middleware, admin-only enforcement |
|
||||||
|
| Heartbeat | Config sync, handshake monitoring |
|
||||||
|
|
||||||
|
### Dashboard UI
|
||||||
|
|
||||||
|
Admin web interface. Built with Vue 3 and glassmorphism design system.
|
||||||
|
|
||||||
|
| Responsibility | Implementation |
|
||||||
|
|----------------|----------------|
|
||||||
|
| Node management | Register/edit WireGuard servers |
|
||||||
|
| Device management | CRUD, provisioning tokens, QR codes |
|
||||||
|
| Firewall rules | Per-peer nftables rule editor |
|
||||||
|
| Live telemetry | 10s polling for device health |
|
||||||
|
| Traffic history | Time-range filtering, export |
|
||||||
|
|
||||||
|
### Device Agent
|
||||||
|
|
||||||
|
Stealth VPN daemon. Cross-platform Go binary.
|
||||||
|
|
||||||
|
| Responsibility | Implementation |
|
||||||
|
|----------------|----------------|
|
||||||
|
| Provisioning | HTTP POST with AES-256-GCM encrypted response |
|
||||||
|
| Tunnel | Memory-injected WireGuard (no disk files) |
|
||||||
|
| Heartbeat | HTTP/gRPC, config sync, handshake monitoring |
|
||||||
|
| gRPC | Bidirectional stream for real-time commands |
|
||||||
|
| Self-healing | Exponential backoff reconnection |
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
### Provisioning Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Admin creates device via Dashboard → API generates registration token
|
||||||
|
2. Agent sends token + HWID to POST /api/v1/provision
|
||||||
|
3. Server validates token, allocates IP from pool
|
||||||
|
4. Server responds with WireGuard config (AES-256-GCM encrypted)
|
||||||
|
5. Agent decrypts config, injects into WireGuard via IpcSet
|
||||||
|
6. Tunnel established — no files written to disk
|
||||||
|
```
|
||||||
|
|
||||||
|
### Heartbeat Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Every 30 seconds:
|
||||||
|
1. Agent reads last_handshake_time from WireGuard IPC
|
||||||
|
2. Agent POSTs {device_id, tunnel_up, last_handshake} to server
|
||||||
|
3. Server loads Device + WgServer fresh from DB (dynamic, not cached)
|
||||||
|
4. Server computes config_hash = SHA256(tunnelFields) + ":" + SHA256(forwards)
|
||||||
|
- Tunnel fields: server_pub, endpoint, internal_ip, private_key, preshared_key, allowed_ips, dns
|
||||||
|
- Forwards: sorted protocol:publicPort->targetIP:targetPort:ID
|
||||||
|
5. Server responds with full config + config_hash
|
||||||
|
6. Agent compares config_hash with previous → if different → rebuild tunnel/reload forwards
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Design**: Peers (devices) store only their own data (keys, IP, settings). Node data (endpoint, public key) is loaded fresh from DB on every heartbeat. This ensures config always reflects the latest node state without requiring agent restart.
|
||||||
|
|
||||||
|
### Suspend/Resume Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Suspend:
|
||||||
|
1. Admin clicks "Suspend" in Dashboard
|
||||||
|
2. Dashboard POSTs /api/v1/devices/:id/suspend
|
||||||
|
3. Server updates DB (is_suspended = true)
|
||||||
|
4. Server sends gRPC SuspendCommand to agent
|
||||||
|
5. Server removes WireGuard peer from kernel
|
||||||
|
6. Agent receives command → stops tunnel
|
||||||
|
|
||||||
|
Resume:
|
||||||
|
1. Admin clicks "Resume" in Dashboard
|
||||||
|
2. Server updates DB (is_suspended = false)
|
||||||
|
3. Server re-adds WireGuard peer to kernel
|
||||||
|
4. Server sends gRPC ResumeCommand with full ConfigUpdate
|
||||||
|
5. Agent receives command → rebuilds tunnel
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Model
|
||||||
|
|
||||||
|
### Zero-Trust Principles
|
||||||
|
|
||||||
|
| Principle | Implementation |
|
||||||
|
|-----------|----------------|
|
||||||
|
| No public registration | `/auth/register` locked; admin via CLI only |
|
||||||
|
| Encrypted provisioning | AES-256-GCM for WireGuard config transfer |
|
||||||
|
| Fileless tunnels | WireGuard config in process memory only |
|
||||||
|
| Hardware binding | HWID (DMI/CPU serial) bound to registration token |
|
||||||
|
| Per-peer isolation | nftables rules per device, default deny |
|
||||||
|
| JWT authentication | All API endpoints require valid token |
|
||||||
|
|
||||||
|
### Trust Boundaries
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Trusted Zone │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │
|
||||||
|
│ │ Server │ │ Database │ │ WireGuard Interface │ │
|
||||||
|
│ │ Core │ │ (PG) │ │ (wg0) │ │
|
||||||
|
│ └──────────┘ └──────────┘ └──────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
Encrypted Channel
|
||||||
|
(AES-256-GCM / WG)
|
||||||
|
│
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Untrusted Zone │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Device Agent │ │
|
||||||
|
│ │ (Memory-only WireGuard config) │ │
|
||||||
|
│ └──────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Port Multiplexing
|
||||||
|
|
||||||
|
Server Core uses cmux to serve both HTTP and gRPC on port 8080:
|
||||||
|
|
||||||
|
```go
|
||||||
|
m := cmux.New(lis)
|
||||||
|
grpcLis := m.MatchWithWriters(cmux.HTTP2MatchHeaderFieldSendSettings(
|
||||||
|
"content-type", "application/grpc",
|
||||||
|
))
|
||||||
|
httpLis := m.Match(cmux.Any())
|
||||||
|
```
|
||||||
|
|
||||||
|
- gRPC matched by `content-type: application/grpc` header
|
||||||
|
- HTTP matched by `Any()` (catch-all)
|
||||||
|
- Single port, single listener, zero extra config
|
||||||
@@ -0,0 +1,476 @@
|
|||||||
|
# Deployment Guide
|
||||||
|
|
||||||
|
NexusGuard supports three deployment modes: Docker (recommended), native install, and development.
|
||||||
|
|
||||||
|
## Docker Deployment (Recommended)
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker 20.10+
|
||||||
|
- Docker Compose v2
|
||||||
|
- Git
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
git clone https://git.datadunia.com/nexusguard/Nexus-Guard-Suite.git
|
||||||
|
cd Nexus-Guard-Suite
|
||||||
|
|
||||||
|
# Start all services (auto-generates .env on first run)
|
||||||
|
bash update.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### First Boot
|
||||||
|
|
||||||
|
On first run, the system automatically:
|
||||||
|
1. Pulls latest code and builds Docker containers
|
||||||
|
2. Generates Local Primary Node WireGuard keys
|
||||||
|
3. Creates database schema via migration
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Edit `.env` in root directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database
|
||||||
|
DB_HOST=postgres
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USER=nexusguard
|
||||||
|
DB_PASSWORD=your_secure_password
|
||||||
|
DB_NAME=nexusguard
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_ADDR=redis:6379
|
||||||
|
|
||||||
|
# Security (auto-generated by update.sh)
|
||||||
|
JWT_SECRET=<hex-64-chars>
|
||||||
|
SERVER_SALT=<hex-64-chars>
|
||||||
|
|
||||||
|
# Network
|
||||||
|
NFTABLES_TABLE=nexusguard
|
||||||
|
IPAM_POOL=10.8.0.0/16
|
||||||
|
|
||||||
|
# Server
|
||||||
|
GIN_MODE=release
|
||||||
|
PORT=8080
|
||||||
|
|
||||||
|
# Dashboard
|
||||||
|
VITE_API_BASE_URL=https://api.yourdomain.com/api/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash update.sh # Smart update (rebuild only if changes)
|
||||||
|
bash update.sh --force # Force rebuild
|
||||||
|
bash update.sh --backup # Backup PostgreSQL before update
|
||||||
|
bash update.sh --no-migrate # Skip database migration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Makefile Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `make up` | Start all services |
|
||||||
|
| `make down` | Stop all services |
|
||||||
|
| `make logs` | Tail all service logs |
|
||||||
|
| `make dev` | Start with hot-reload |
|
||||||
|
| `make migrate` | Run database migration |
|
||||||
|
| `make reset-db` | Reset database to initial state |
|
||||||
|
|
||||||
|
### Create Admin Account
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it nexus-guard-suite-server-core-1 ./server-core \
|
||||||
|
-create-admin -user admin -pass "YourSecurePassword123!"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ Docker Compose │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
|
||||||
|
│ │ nginx │ │ server- │ │ postgres │ │
|
||||||
|
│ │ :80/:443 │→ │ core │→ │ :5432 │ │
|
||||||
|
│ │ │ │ :8080 │ │ │ │
|
||||||
|
│ └──────────┘ └──────────┘ └──────────────┘ │
|
||||||
|
│ ↑ ↑ │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ dashboard│ │ redis │ │
|
||||||
|
│ │ (static) │ │ :6379 │ │
|
||||||
|
│ └──────────┘ └──────────┘ │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port Mapping
|
||||||
|
|
||||||
|
| Service | Container Port | Host Port |
|
||||||
|
|---------|----------------|-----------|
|
||||||
|
| nginx | 80 | 80 |
|
||||||
|
| nginx | 443 | 443 |
|
||||||
|
| server-core | 8080 | 8080 |
|
||||||
|
| postgres | 5432 | 5432 |
|
||||||
|
| redis | 6379 | 6379 |
|
||||||
|
|
||||||
|
### Volumes
|
||||||
|
|
||||||
|
| Volume | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `postgres_data` | PostgreSQL data persistence |
|
||||||
|
| `redis_data` | Redis data persistence |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Native Install
|
||||||
|
|
||||||
|
For production servers without Docker.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
**Debian/Ubuntu:**
|
||||||
|
```bash
|
||||||
|
sudo apt install -y golang nginx postgresql redis-server nftables wireguard-tools
|
||||||
|
```
|
||||||
|
|
||||||
|
**CentOS/Rocky:**
|
||||||
|
```bash
|
||||||
|
sudo dnf install -y golang nginx postgresql-server redis nftables wireguard-tools
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Binaries
|
||||||
|
|
||||||
|
**Server Core:**
|
||||||
|
```bash
|
||||||
|
cd apps/server-core
|
||||||
|
CGO_ENABLED=0 go build -o ../../bin/server-core .
|
||||||
|
cd ../..
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dashboard UI:**
|
||||||
|
```bash
|
||||||
|
cd apps/dashboard-ui
|
||||||
|
npm install
|
||||||
|
VITE_API_BASE_URL=/api/v1 npm run build
|
||||||
|
cd ../..
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Installer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash nexusguard-install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
```bash
|
||||||
|
sudo bash nexusguard-install.sh --server-port 8080 --web-port 80
|
||||||
|
sudo bash nexusguard-install.sh --db-host 127.0.0.1 --db-pass mypassword
|
||||||
|
```
|
||||||
|
|
||||||
|
**What the installer does:**
|
||||||
|
1. Creates PostgreSQL database and user
|
||||||
|
2. Installs binary to `/usr/local/bin/nexusguard-server`
|
||||||
|
3. Installs dashboard to `/usr/share/nexusguard/dashboard/`
|
||||||
|
4. Creates config at `/etc/nexusguard/nexusguard.conf`
|
||||||
|
5. Runs database migration
|
||||||
|
6. Creates systemd service
|
||||||
|
7. Configures nginx
|
||||||
|
|
||||||
|
### Create Admin Account
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo /usr/local/bin/nexusguard-server \
|
||||||
|
-create-admin -user admin -pass "YourSecurePassword123!"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start
|
||||||
|
sudo systemctl start nexusguard-server
|
||||||
|
|
||||||
|
# Stop
|
||||||
|
sudo systemctl stop nexusguard-server
|
||||||
|
|
||||||
|
# Status
|
||||||
|
sudo systemctl status nexusguard-server
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
sudo journalctl -u nexusguard-server -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Edit `/etc/nexusguard/nexusguard.conf`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USER=nexusguard
|
||||||
|
DB_PASSWORD=nexusguard
|
||||||
|
DB_NAME=nexusguard
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_ADDR=127.0.0.1:6379
|
||||||
|
|
||||||
|
# Security (auto-generated)
|
||||||
|
JWT_SECRET=<hex-64-chars>
|
||||||
|
SERVER_SALT=<hex-64-chars>
|
||||||
|
|
||||||
|
# Network
|
||||||
|
NFTABLES_TABLE=nexusguard
|
||||||
|
IPAM_POOL=10.8.0.0/16
|
||||||
|
|
||||||
|
# Server
|
||||||
|
GIN_MODE=release
|
||||||
|
PORT=8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### Uninstall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remove files only
|
||||||
|
sudo bash nexusguard-uninstall.sh
|
||||||
|
|
||||||
|
# Also drop database
|
||||||
|
sudo bash nexusguard-uninstall.sh --remove-db
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development Mode
|
||||||
|
|
||||||
|
For local development with hot-reload.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Go 1.25+
|
||||||
|
- Node.js 24+
|
||||||
|
- PostgreSQL
|
||||||
|
- Redis
|
||||||
|
|
||||||
|
### Setup Database
|
||||||
|
|
||||||
|
1. Create PostgreSQL database:
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE nexusguard;
|
||||||
|
CREATE USER nexusguard WITH PASSWORD 'nexusguard';
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE nexusguard TO nexusguard;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Copy environment template:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Edit `.env` with your database credentials.
|
||||||
|
|
||||||
|
### Start Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/server-core
|
||||||
|
go mod download
|
||||||
|
go run -tags dev .
|
||||||
|
```
|
||||||
|
|
||||||
|
The `-tags dev` flag:
|
||||||
|
- Runs AutoMigrate on startup
|
||||||
|
- Provisions local node
|
||||||
|
- Enables debug logging
|
||||||
|
|
||||||
|
### Start Frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/dashboard-ui
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Dashboard available at `http://localhost:5173`.
|
||||||
|
|
||||||
|
### Create Admin Account
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/server-core
|
||||||
|
go run -tags dev . -create-admin -user admin -pass "YourNewSecurePassword123!"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Development Setup │
|
||||||
|
│ │
|
||||||
|
│ Terminal 1: Backend │
|
||||||
|
│ ┌───────────────────────────────────────────────────┐ │
|
||||||
|
│ │ $ go run -tags dev . │ │
|
||||||
|
│ │ [dev] AutoMigrate complete │ │
|
||||||
|
│ │ [dev] Local node provisioned │ │
|
||||||
|
│ │ [gin] Listening on :8080 │ │
|
||||||
|
│ └───────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Terminal 2: Frontend │
|
||||||
|
│ ┌───────────────────────────────────────────────────┐ │
|
||||||
|
│ │ $ npm run dev │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ VITE v8.0.0 ready in 300 ms │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ➜ Local: http://localhost:5173/ │ │
|
||||||
|
│ └───────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Browser: http://localhost:5173 │
|
||||||
|
│ → Dashboard UI (Vue 3 + Vite) │
|
||||||
|
│ → API calls proxied to :8080 │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Agent Installation
|
||||||
|
|
||||||
|
### Linux (Automated)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Transfer script
|
||||||
|
scp scripts/install_agent.sh user@target-machine:~
|
||||||
|
|
||||||
|
# Run installer
|
||||||
|
sudo ./install_agent.sh \
|
||||||
|
--server-url "https://api.yourdomain.com" \
|
||||||
|
--token "REG_TOKEN_FROM_DASHBOARD"
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
sudo systemctl status sys-bridge.service
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--binary-name "my-agent"` — Override default binary name
|
||||||
|
|
||||||
|
**What the script does:**
|
||||||
|
1. Detects OS (APT/YUM)
|
||||||
|
2. Installs dependencies (iproute2, curl)
|
||||||
|
3. Downloads correct binary for architecture
|
||||||
|
4. Creates config at `~/.config/nexusguard/nexusguard.conf`
|
||||||
|
5. Creates systemd service
|
||||||
|
6. Starts agent
|
||||||
|
|
||||||
|
### Linux (Manual)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download binary
|
||||||
|
sudo cp nexusguard-device-agent-linux-amd64 /usr/local/bin/sys-bridge
|
||||||
|
sudo chmod +x /usr/local/bin/sys-bridge
|
||||||
|
|
||||||
|
# Create config
|
||||||
|
mkdir -p ~/.config/nexusguard
|
||||||
|
cat > ~/.config/nexusguard/nexusguard.conf <<EOF
|
||||||
|
SERVER_URL=https://api.yourdomain.com
|
||||||
|
REG_TOKEN=YOUR_REG_TOKEN
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create systemd service
|
||||||
|
sudo tee /etc/systemd/system/sys-bridge.service > /dev/null <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=NexusGuard Device Agent
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/sys-bridge
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Start
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now sys-bridge.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
1. Build GUI binary:
|
||||||
|
```bash
|
||||||
|
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build \
|
||||||
|
-ldflags="-s -w -H windowsgui" \
|
||||||
|
-o nexusguard-device-agent-gui.exe .
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Place `wintun.dll` next to executable
|
||||||
|
|
||||||
|
3. Run agent — config auto-created at `%ProgramData%\NexusGuard\nexusguard.conf`
|
||||||
|
|
||||||
|
4. Edit config with `SERVER_URL` and `REG_TOKEN`
|
||||||
|
|
||||||
|
5. Right-click tray icon → Connect
|
||||||
|
|
||||||
|
**Service Management:**
|
||||||
|
- Install as Service — Register Windows service
|
||||||
|
- Uninstall Service — Remove Windows service
|
||||||
|
- Start on Boot — Toggle auto-start
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t nexus-device-agent .
|
||||||
|
docker run -d \
|
||||||
|
--name nexus-agent \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--network host \
|
||||||
|
--cap-add NET_ADMIN \
|
||||||
|
--cap-add NET_RAW \
|
||||||
|
-e SERVER_URL=https://api.yourdomain.com \
|
||||||
|
-e REG_TOKEN=YOUR_REG_TOKEN \
|
||||||
|
nexus-device-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
**Required capabilities:**
|
||||||
|
- `NET_ADMIN` — Network interface configuration
|
||||||
|
- `NET_RAW` — Raw socket access (WireGuard)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
| Issue | Cause | Solution |
|
||||||
|
|-------|-------|----------|
|
||||||
|
| Agent fails to start | Missing config | Check config file has `SERVER_URL` + `REG_TOKEN` |
|
||||||
|
| Provisioning fails | Invalid token | Generate new token from Dashboard |
|
||||||
|
| Tunnel won't create | Missing kernel module | `sudo modprobe wireguard` |
|
||||||
|
| `ip addr add` fails | Permissions | Run as root or grant `CAP_NET_ADMIN` |
|
||||||
|
| Heartbeat timeout | Network issue | `curl -I <SERVER_URL>` |
|
||||||
|
|
||||||
|
### Log Locations
|
||||||
|
|
||||||
|
| Platform | Location |
|
||||||
|
|----------|----------|
|
||||||
|
| Docker | `docker logs -f nexus-guard-suite-server-core-1` |
|
||||||
|
| Native | `sudo journalctl -u nexusguard-server -f` |
|
||||||
|
| Agent (Linux) | `~/.local/share/nexusguard/logs/` |
|
||||||
|
| Agent (Windows) | `%ProgramData%\NexusGuard\logs\` |
|
||||||
|
| Agent (macOS) | `~/Library/Logs/NexusGuard/` |
|
||||||
|
|
||||||
|
### Debug Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check WireGuard interface
|
||||||
|
sudo wg show
|
||||||
|
|
||||||
|
# Test server connectivity
|
||||||
|
curl -I https://api.yourdomain.com/api/health
|
||||||
|
|
||||||
|
# Check agent version
|
||||||
|
/usr/local/bin/sys-bridge -version
|
||||||
|
|
||||||
|
# Run agent in foreground
|
||||||
|
sudo /usr/local/bin/sys-bridge -debug
|
||||||
|
```
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
# Features
|
||||||
|
|
||||||
|
NexusGuard provides enterprise-grade SD-WAN capabilities with a focus on security, automation, and ease of use.
|
||||||
|
|
||||||
|
## Zero-Trust Security
|
||||||
|
|
||||||
|
### No Public Registration
|
||||||
|
|
||||||
|
The `/auth/register` endpoint is locked. Admin accounts can only be created via CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker
|
||||||
|
docker exec -it nexus-guard-suite-server-core-1 ./server-core \
|
||||||
|
-create-admin -user admin -pass "SecurePassword123!"
|
||||||
|
|
||||||
|
# Native
|
||||||
|
sudo /usr/local/bin/nexusguard-server \
|
||||||
|
-create-admin -user admin -pass "SecurePassword123!"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why?** Eliminates the attack surface of open registration. No bots, no brute force, no unauthorized accounts.
|
||||||
|
|
||||||
|
### Encrypted Provisioning
|
||||||
|
|
||||||
|
Agent provisioning uses AES-256-GCM encryption:
|
||||||
|
|
||||||
|
1. Agent sends registration token + hardware ID (HWID)
|
||||||
|
2. Server generates WireGuard config
|
||||||
|
3. Config encrypted with AES-256-GCM before transmission
|
||||||
|
4. Agent decrypts in memory, never touches disk
|
||||||
|
|
||||||
|
**Why?** WireGuard keys are sensitive. Encryption in transit prevents interception even on compromised networks.
|
||||||
|
|
||||||
|
### Memory-Injected Tunnels
|
||||||
|
|
||||||
|
WireGuard configuration is injected directly into the kernel via `IpcSet`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Traditional: Config file → /etc/wireguard/wg0.conf → wg-quick up wg0
|
||||||
|
NexusGuard: Config bytes → IpcSet() → Tunnel active (no files)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- No config files to steal
|
||||||
|
- No lingering configs after disconnect
|
||||||
|
- Multiple agents can run without conflicts
|
||||||
|
- Clean uninstall = kill process
|
||||||
|
|
||||||
|
### Hardware ID Binding
|
||||||
|
|
||||||
|
Each agent is bound to its hardware via HWID:
|
||||||
|
|
||||||
|
- **Linux:** `/sys/class/dmi/id/product_uuid` or CPU serial
|
||||||
|
- **Windows:** DMI product UUID
|
||||||
|
- **macOS:** IOPlatformSerialNumber
|
||||||
|
|
||||||
|
HWID is included in provisioning request. Server validates before issuing config.
|
||||||
|
|
||||||
|
## Multi-Node Support
|
||||||
|
|
||||||
|
### Geographic Scaling
|
||||||
|
|
||||||
|
Deploy WireGuard servers across multiple regions:
|
||||||
|
|
||||||
|
```
|
||||||
|
Node 1 (Singapore): 10.172.21.0/24
|
||||||
|
Node 2 (Frankfurt): 10.172.22.0/24
|
||||||
|
Node 3 (Virginia): 10.172.23.0/24
|
||||||
|
```
|
||||||
|
|
||||||
|
Each node has its own:
|
||||||
|
- IP pool (CIDR)
|
||||||
|
- Interface address
|
||||||
|
- Peer defaults (DNS, MTU, Keepalive)
|
||||||
|
- Endpoint (IP/Domain + Port)
|
||||||
|
|
||||||
|
### Centralized IPAM
|
||||||
|
|
||||||
|
IP Address Management is centralized in the database:
|
||||||
|
|
||||||
|
1. Admin defines IP pool per node (e.g., `10.172.21.0/24`)
|
||||||
|
2. When device is created, server allocates next available IP
|
||||||
|
3. IP is reserved in database (no duplicates)
|
||||||
|
4. IP is released when device is deleted
|
||||||
|
|
||||||
|
**Why?** Prevents IP conflicts across nodes. Enables static IP assignment for critical devices.
|
||||||
|
|
||||||
|
### Per-Node Defaults
|
||||||
|
|
||||||
|
Each node can have different peer defaults:
|
||||||
|
|
||||||
|
| Setting | Node 1 (SG) | Node 2 (DE) |
|
||||||
|
|---------|-------------|-------------|
|
||||||
|
| DNS | `1.1.1.1` | `8.8.8.8` |
|
||||||
|
| MTU | 1420 | 1280 |
|
||||||
|
| Keepalive | 25s | 0s |
|
||||||
|
| AllowedIPs | `10.172.21.0/24` | `0.0.0.0/0` |
|
||||||
|
|
||||||
|
Devices inherit from their node, with per-device overrides available.
|
||||||
|
|
||||||
|
## Real-Time Firewall
|
||||||
|
|
||||||
|
### nftables Integration
|
||||||
|
|
||||||
|
NexusGuard manages Linux nftables directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# What NexusGuard creates in the kernel
|
||||||
|
table ip nexusguard {
|
||||||
|
set peers_v4 {
|
||||||
|
type ipv4_addr
|
||||||
|
elements = { 10.172.21.2, 10.172.21.3, ... }
|
||||||
|
}
|
||||||
|
chain forward {
|
||||||
|
type filter hook forward priority 0; policy accept;
|
||||||
|
ip daddr @peers_v4 accept
|
||||||
|
ip saddr @peers_v4 accept
|
||||||
|
drop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Per-Peer Rules
|
||||||
|
|
||||||
|
Each device can have custom firewall rules:
|
||||||
|
|
||||||
|
- **Allow/Block IP ranges** — `192.168.1.0/24`, `10.0.0.1`
|
||||||
|
- **Port filtering** — TCP/UDP port ranges
|
||||||
|
- **Direction control** — Inbound, outbound, or both
|
||||||
|
|
||||||
|
Changes are synced to kernel instantly — no restart required.
|
||||||
|
|
||||||
|
### Default SSH Provisioning
|
||||||
|
|
||||||
|
New peers automatically get SSH access (port 22):
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Automatically added on peer creation
|
||||||
|
AddFirewallRule(peerIP, "0.0.0.0/0", 22, "tcp", "allow")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why?** Ensures remote access isn't accidentally locked out.
|
||||||
|
|
||||||
|
## Cross-Platform Agent
|
||||||
|
|
||||||
|
### Linux — Systemd Daemon
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Automated install
|
||||||
|
sudo ./install_agent.sh \
|
||||||
|
--server-url "https://api.yourdomain.com" \
|
||||||
|
--token "REG_TOKEN"
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
sudo systemctl status sys-bridge.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Runs as root (required for WireGuard)
|
||||||
|
- Auto-restart on failure
|
||||||
|
- Journal logging
|
||||||
|
- Config at `~/.config/nexusguard/nexusguard.conf`
|
||||||
|
|
||||||
|
### Windows — System Tray + Service
|
||||||
|
|
||||||
|
System tray application with service management:
|
||||||
|
|
||||||
|
| Menu Item | Action |
|
||||||
|
|-----------|--------|
|
||||||
|
| Status | Shows Connected/Disconnected |
|
||||||
|
| IP | Shows internal VPN IP |
|
||||||
|
| Connect | Start tunnel |
|
||||||
|
| Disconnect | Stop tunnel |
|
||||||
|
| Install as Service | Register Windows service |
|
||||||
|
| Start on Boot | Toggle auto-start |
|
||||||
|
|
||||||
|
### macOS — System Tray
|
||||||
|
|
||||||
|
System tray application (no service support):
|
||||||
|
|
||||||
|
- Config at `~/Library/Application Support/NexusGuard/nexusguard.conf`
|
||||||
|
- Logs at `~/Library/Logs/NexusGuard/`
|
||||||
|
|
||||||
|
### Self-Healing
|
||||||
|
|
||||||
|
All platforms implement exponential backoff:
|
||||||
|
|
||||||
|
```
|
||||||
|
Failure 1: Wait 30s, retry
|
||||||
|
Failure 2: Wait 60s, retry
|
||||||
|
Failure 3: Wait 120s, retry
|
||||||
|
...
|
||||||
|
Failure N: Wait 300s (max), retry
|
||||||
|
```
|
||||||
|
|
||||||
|
Network drops are handled gracefully — tunnel stays alive, agent reconnects in background.
|
||||||
|
|
||||||
|
## Dashboard
|
||||||
|
|
||||||
|
### Glassmorphism Design
|
||||||
|
|
||||||
|
Futuristic UI with glass-like transparency:
|
||||||
|
|
||||||
|
- Backdrop blur effects
|
||||||
|
- Semi-transparent panels
|
||||||
|
- Gradient accents
|
||||||
|
- Smooth animations
|
||||||
|
|
||||||
|
### Live Telemetry
|
||||||
|
|
||||||
|
Device health polled every 10 seconds:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ Device: server-01 │
|
||||||
|
│ Status: ● Online │
|
||||||
|
│ IP: 10.172.21.2 │
|
||||||
|
│ Last Handshake: 15s ago │
|
||||||
|
│ Uptime: 3d 14h 22m │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server-Side Config Sync
|
||||||
|
|
||||||
|
Config changes (firewall rules, AllowedIPs, DNS, endpoint) are detected via SHA256 hash comparison:
|
||||||
|
|
||||||
|
- Server computes `config_hash = SHA256(tunnelFields) + ":" + SHA256(forwards)`
|
||||||
|
- Agent compares with previous hash → if different → tunnel rebuilds automatically
|
||||||
|
- Works over both HTTP heartbeat (30s) and gRPC (immediate)
|
||||||
|
- Agent never caches config — server loads fresh from DB each heartbeat
|
||||||
|
|
||||||
|
### QR Code Setup
|
||||||
|
|
||||||
|
Generate QR codes for mobile WireGuard clients:
|
||||||
|
|
||||||
|
1. Create device in Dashboard
|
||||||
|
2. Click "Show QR Code"
|
||||||
|
3. Scan with WireGuard app on iOS/Android
|
||||||
|
4. Tunnel ready — no manual config
|
||||||
|
|
||||||
|
### Share Links
|
||||||
|
|
||||||
|
Time-limited config sharing:
|
||||||
|
|
||||||
|
1. Click "Generate Share Link"
|
||||||
|
2. Set expiration (1h, 24h, 7d)
|
||||||
|
3. Share URL with recipient
|
||||||
|
4. Recipient downloads `.conf` file
|
||||||
|
5. Link expires automatically
|
||||||
|
|
||||||
|
## Deployment Flexibility
|
||||||
|
|
||||||
|
### Docker (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.datadunia.com/nexusguard/Nexus-Guard-Suite.git
|
||||||
|
cd Nexus-Guard-Suite
|
||||||
|
bash update.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
One command to start everything. Auto-migration on first boot.
|
||||||
|
|
||||||
|
### Native Install
|
||||||
|
|
||||||
|
For servers without Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash nexusguard-install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates systemd service, nginx config, PostgreSQL database.
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
|
Hot-reload for both backend and frontend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Terminal 1: Backend
|
||||||
|
cd apps/server-core
|
||||||
|
go run -tags dev .
|
||||||
|
|
||||||
|
# Terminal 2: Frontend
|
||||||
|
cd apps/dashboard-ui
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Auto-migration on startup. No Docker required.
|
||||||
|
|
||||||
|
## Comparison
|
||||||
|
|
||||||
|
| Feature | NexusGuard | Traditional VPN | Commercial SD-WAN |
|
||||||
|
|---------|------------|-----------------|-------------------|
|
||||||
|
| Zero-trust | ✅ | ❌ | ✅ |
|
||||||
|
| Fileless tunnel | ✅ | ❌ | ❌ |
|
||||||
|
| Multi-platform agent | ✅ | Partial | ✅ |
|
||||||
|
| Real-time firewall | ✅ | ❌ | ✅ |
|
||||||
|
| Self-hosted | ✅ | ✅ | ❌ |
|
||||||
|
| Open source | ✅ | ✅ | ❌ |
|
||||||
|
| Cost | Free | Free | $$$$ |
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# Technology Stack
|
||||||
|
|
||||||
|
NexusGuard uses a modern, production-grade technology stack. Each component is built with tools optimized for its domain.
|
||||||
|
|
||||||
|
## Backend — Server Core
|
||||||
|
|
||||||
|
| Technology | Version | Purpose |
|
||||||
|
|------------|---------|---------|
|
||||||
|
| Go | 1.25+ | Primary language |
|
||||||
|
| Gin | 1.12 | HTTP framework |
|
||||||
|
| GORM | 1.31 | ORM (PostgreSQL) |
|
||||||
|
| grpc-go | latest | gRPC signaling |
|
||||||
|
| cmux | latest | Port multiplexing (HTTP + gRPC on :8080) |
|
||||||
|
| go-redis | 9.x | Heartbeat TTL cache |
|
||||||
|
| google/nftables | 0.3 | Linux firewall management |
|
||||||
|
| wgctrl | latest | WireGuard interface control |
|
||||||
|
| JWT v5 | latest | Authentication tokens |
|
||||||
|
|
||||||
|
### Why Go?
|
||||||
|
|
||||||
|
- **Static binaries** — No runtime dependencies, easy deployment
|
||||||
|
- **Concurrency** — Goroutines for handling 10000+ concurrent agent connections
|
||||||
|
- **WireGuard ecosystem** — Native Go WireGuard libraries (wgctrl, wireguard-go)
|
||||||
|
- **Performance** — Low memory footprint, fast cold start
|
||||||
|
|
||||||
|
### Why cmux?
|
||||||
|
|
||||||
|
Single port for HTTP and gRPC eliminates:
|
||||||
|
- Firewall rules for multiple ports
|
||||||
|
- Load balancer complexity
|
||||||
|
- Docker port mapping overhead
|
||||||
|
|
||||||
|
## Frontend — Dashboard UI
|
||||||
|
|
||||||
|
| Technology | Version | Purpose |
|
||||||
|
|------------|---------|---------|
|
||||||
|
| Vue | 3.5 | UI framework (Composition API) |
|
||||||
|
| Vite | 8 | Build tool + dev server |
|
||||||
|
| TypeScript | 6.0 | Type safety |
|
||||||
|
| TailwindCSS | 4.3 | Styling (glassmorphism design system) |
|
||||||
|
| Pinia | 2.3 | State management |
|
||||||
|
| Axios | 1.16 | HTTP client |
|
||||||
|
| HeadlessUI | 1.7 | Accessible UI primitives |
|
||||||
|
| Iconify | 5.0 | Icon system |
|
||||||
|
| VueUse | 14.3 | Composition utilities |
|
||||||
|
|
||||||
|
### Why Vue 3?
|
||||||
|
|
||||||
|
- **Composition API** — Better TypeScript support, reusable logic via composables
|
||||||
|
- **`<script setup>`** — Cleaner SFC syntax, less boilerplate
|
||||||
|
- **Ecosystem** — Mature ecosystem with Pinia, Vue Router, VueUse
|
||||||
|
|
||||||
|
### Why TailwindCSS 4?
|
||||||
|
|
||||||
|
- **Design tokens** — `@theme` block for consistent colors, spacing, typography
|
||||||
|
- **Glassmorphism** — Utility classes for backdrop-blur, transparency, gradients
|
||||||
|
- **No PostCSS** — Uses `@tailwindcss/vite` plugin (faster builds)
|
||||||
|
|
||||||
|
## Client — Device Agent
|
||||||
|
|
||||||
|
| Technology | Version | Purpose |
|
||||||
|
|------------|---------|---------|
|
||||||
|
| Go | 1.25+ | Primary language |
|
||||||
|
| wireguard-go | latest | Userspace WireGuard |
|
||||||
|
| fyne.io/systray | latest | Cross-platform system tray |
|
||||||
|
| wintun | latest | Windows WireGuard driver |
|
||||||
|
|
||||||
|
### Cross-Compile Targets
|
||||||
|
|
||||||
|
| Platform | Architecture | Binary |
|
||||||
|
|----------|--------------|--------|
|
||||||
|
| Linux | amd64 | `nexusguard-device-agent-linux-amd64` |
|
||||||
|
| Linux | arm64 | `nexusguard-device-agent-linux-arm64` |
|
||||||
|
| Linux | arm | `nexusguard-device-agent-linux-arm` |
|
||||||
|
| Windows | amd64 | `nexusguard-device-agent-gui.exe` |
|
||||||
|
| macOS | arm64 | `nexusguard-device-agent-darwin-arm64` |
|
||||||
|
| macOS | amd64 | `nexusguard-device-agent-darwin-amd64` |
|
||||||
|
|
||||||
|
### Why Memory-Injected Tunnels?
|
||||||
|
|
||||||
|
Traditional WireGuard setups write config to `/etc/wireguard/`. NexusGuard avoids this:
|
||||||
|
|
||||||
|
1. **Security** — No config files on disk = no file theft risk
|
||||||
|
2. **Stealth** — Tunnel exists only in process memory
|
||||||
|
3. **Cleanup** — Process exit = tunnel gone (no lingering configs)
|
||||||
|
4. **Multi-tenant** — Multiple agents can run without config conflicts
|
||||||
|
|
||||||
|
## Infrastructure
|
||||||
|
|
||||||
|
| Technology | Purpose |
|
||||||
|
|------------|---------|
|
||||||
|
| PostgreSQL | Primary database (wg_servers, devices, rules, users) |
|
||||||
|
| Redis | Heartbeat TTL cache, session tracking |
|
||||||
|
| nginx | Reverse proxy, SPA fallback, API routing |
|
||||||
|
| Docker | Container orchestration |
|
||||||
|
| docker-compose | Multi-service deployment |
|
||||||
|
|
||||||
|
### Why PostgreSQL?
|
||||||
|
|
||||||
|
- **ACID compliance** — Critical for IPAM allocation (no duplicate IPs)
|
||||||
|
- **JSON support** — Flexible config storage
|
||||||
|
- **Maturity** — Battle-tested for production workloads
|
||||||
|
|
||||||
|
### Why Redis?
|
||||||
|
|
||||||
|
- **Heartbeat TTL** — Fast expiry checks for agent health
|
||||||
|
- **Session cache** — gRPC session tracking
|
||||||
|
- **Pub/Sub** — Real-time event distribution (future)
|
||||||
|
|
||||||
|
## Build & CI
|
||||||
|
|
||||||
|
| Tool | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| Go build | Static binary compilation |
|
||||||
|
| Vite build | SPA bundling (vue-tsc + vite) |
|
||||||
|
| Gitea Actions | CI/CD per submodule |
|
||||||
|
| Makefile | Quick commands (up, down, dev, migrate) |
|
||||||
|
|
||||||
|
### Build Flags
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Server Core (production)
|
||||||
|
CGO_ENABLED=0 go build -o server-core .
|
||||||
|
|
||||||
|
# Device Agent (stripped)
|
||||||
|
go build -ldflags="-s -w" -o device-agent .
|
||||||
|
|
||||||
|
# Dashboard UI
|
||||||
|
VITE_API_BASE_URL=/api/v1 npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
- `CGO_ENABLED=0` — Static binary, no CGO dependencies
|
||||||
|
- `-ldflags="-s -w"` — Strip debug symbols (~30% smaller binary)
|
||||||
|
- `VITE_API_BASE_URL` — Build-time API endpoint injection
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
# NexusGuard Portfolio Design Spec
|
||||||
|
|
||||||
|
**Date:** 2026-06-21
|
||||||
|
**Status:** Draft
|
||||||
|
**Author:** Sisyphus
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Create a comprehensive technical portfolio for the NexusGuard SD-WAN Suite project, showcasing architecture, technology stack, features, and deployment options. Target audience: technical recruiters, potential clients, and fellow developers.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
1. Present NexusGuard as a production-grade enterprise SD-WAN solution
|
||||||
|
2. Highlight unique selling points (zero-trust, stealth agent, memory-injected tunnels)
|
||||||
|
3. Provide clear architecture overview for technical audience
|
||||||
|
4. Document technology choices and reasoning
|
||||||
|
5. Showcase deployment flexibility (Docker, native, development)
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In Scope
|
||||||
|
|
||||||
|
- Multi-file Markdown portfolio in `docs/portfolio/`
|
||||||
|
- 5 files: README, architecture, tech-stack, features, deployment
|
||||||
|
- ASCII/text-based diagrams (no external dependencies)
|
||||||
|
- Internal navigation links between files
|
||||||
|
- Focus on technical depth over marketing fluff
|
||||||
|
|
||||||
|
### Out of Scope
|
||||||
|
|
||||||
|
- Interactive VitePress integration (future enhancement)
|
||||||
|
- Screenshots/demo embeds (requires running instances)
|
||||||
|
- Video content
|
||||||
|
- Multi-language support (Indonesian/English)
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/portfolio/
|
||||||
|
├── README.md # Landing page / entry point
|
||||||
|
├── architecture.md # System architecture + diagrams
|
||||||
|
├── tech-stack.md # Technology breakdown per component
|
||||||
|
├── features.md # Feature showcase
|
||||||
|
└── deployment.md # Deployment guide
|
||||||
|
```
|
||||||
|
|
||||||
|
### Diagram Style
|
||||||
|
|
||||||
|
Use ASCII/text-based diagrams for portability and version control friendliness. Examples:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐ ┌─────────────────┐ ┌──────────────┐
|
||||||
|
│ Dashboard │──HTTP──▶│ Server Core │◀─WG────▶│ Device Agent │
|
||||||
|
│ (Vue 3) │ :8080 │ (Go/Gin) │ :51820 │ (Go) │
|
||||||
|
└──────────────┘ └─────────────────┘ └──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Content Tone
|
||||||
|
|
||||||
|
Technical and precise. Avoid marketing buzzwords. Focus on:
|
||||||
|
- What it does (functional description)
|
||||||
|
- How it works (architecture/implementation)
|
||||||
|
- Why these choices (technology rationale)
|
||||||
|
|
||||||
|
## File Specifications
|
||||||
|
|
||||||
|
### 1. README.md (Landing Page)
|
||||||
|
|
||||||
|
**Purpose:** Quick overview, entry point to other sections
|
||||||
|
|
||||||
|
**Content:**
|
||||||
|
- Project name + tagline: "Enterprise Zero-Trust SD-WAN with WireGuard"
|
||||||
|
- 2-3 sentence description
|
||||||
|
- Key highlights (3-4 bullet points):
|
||||||
|
- Zero-attack surface (no public registration)
|
||||||
|
- Stealth agent (memory-injected, no disk files)
|
||||||
|
- Real-time nftables isolation
|
||||||
|
- Cross-platform (Linux, Windows, macOS)
|
||||||
|
- Tech badges: Go, Vue 3, WireGuard, PostgreSQL, Docker
|
||||||
|
- Navigation links to other portfolio files
|
||||||
|
|
||||||
|
**Length:** ~100 lines
|
||||||
|
|
||||||
|
### 2. architecture.md (System Architecture)
|
||||||
|
|
||||||
|
**Purpose:** Technical deep-dive into system design
|
||||||
|
|
||||||
|
**Content:**
|
||||||
|
- High-level architecture diagram (ASCII)
|
||||||
|
- Component breakdown:
|
||||||
|
- Server Core: API, gRPC signaling, IPAM, firewall, WireGuard management
|
||||||
|
- Dashboard UI: Management interface, glassmorphism design
|
||||||
|
- Device Agent: Stealth daemon, multi-platform, self-healing
|
||||||
|
- Data flow:
|
||||||
|
- Provisioning flow (HTTP → AES-256-GCM → memory injection)
|
||||||
|
- Heartbeat flow (HTTP/gRPC, config sync, handshake monitoring)
|
||||||
|
- Suspend/Resume flow (gRPC command → tunnel control)
|
||||||
|
- Security model:
|
||||||
|
- Zero-trust (admin CLI only, no public register)
|
||||||
|
- Encrypted provisioning (AES-256-GCM)
|
||||||
|
- Fileless tunnels (WireGuard config in RAM)
|
||||||
|
- Hardware binding (HWID detection)
|
||||||
|
|
||||||
|
**Length:** ~250 lines
|
||||||
|
|
||||||
|
### 3. tech-stack.md (Technology Breakdown)
|
||||||
|
|
||||||
|
**Purpose:** Detailed technology choices per component
|
||||||
|
|
||||||
|
**Content:**
|
||||||
|
- Backend (Server Core):
|
||||||
|
- Go 1.25+, Gin framework, GORM ORM
|
||||||
|
- PostgreSQL, Redis
|
||||||
|
- grpc-go + cmux (port multiplexing)
|
||||||
|
- nftables (google/nftables), WireGuard (wgctrl)
|
||||||
|
- Frontend (Dashboard UI):
|
||||||
|
- Vue 3.5 (Composition API, `<script setup>`)
|
||||||
|
- Vite 8, TypeScript 6.0
|
||||||
|
- TailwindCSS 4.3 (glassmorphism design system)
|
||||||
|
- Pinia 2.3, Axios 1.16, HeadlessUI 1.7
|
||||||
|
- Client (Device Agent):
|
||||||
|
- Go 1.25+, fyne.io/systray
|
||||||
|
- WireGuard (memory-injected via IpcSet)
|
||||||
|
- Cross-compile targets: linux/amd64, arm64, arm; windows/amd64; darwin/arm64, amd64
|
||||||
|
- Infrastructure:
|
||||||
|
- PostgreSQL, Redis, nginx
|
||||||
|
- Docker + docker-compose
|
||||||
|
- Gitea Actions (CI/CD)
|
||||||
|
|
||||||
|
**Length:** ~200 lines
|
||||||
|
|
||||||
|
### 4. features.md (Feature Showcase)
|
||||||
|
|
||||||
|
**Purpose:** Highlight key capabilities
|
||||||
|
|
||||||
|
**Content:**
|
||||||
|
- Zero-Trust Security:
|
||||||
|
- No public registration (admin CLI only)
|
||||||
|
- AES-256-GCM encrypted provisioning
|
||||||
|
- Memory-injected WireGuard (no /etc/wireguard/ files)
|
||||||
|
- Hardware ID binding (HWID)
|
||||||
|
- Multi-Node Support:
|
||||||
|
- Geographic scaling (multiple WG servers)
|
||||||
|
- Centralized IPAM (IP pool allocation per node)
|
||||||
|
- Per-node peer defaults
|
||||||
|
- Real-time Firewall:
|
||||||
|
- nftables integration (Linux kernel)
|
||||||
|
- Per-peer rules (IP/CIDR + port ranges)
|
||||||
|
- Instant sync to kernel (no restart)
|
||||||
|
- Cross-Platform Agent:
|
||||||
|
- Linux (systemd daemon)
|
||||||
|
- Windows (system tray + service)
|
||||||
|
- macOS (system tray)
|
||||||
|
- Self-healing (exponential backoff reconnection)
|
||||||
|
- Dashboard:
|
||||||
|
- Futuristic glassmorphism UI
|
||||||
|
- Live telemetry (10s polling)
|
||||||
|
- QR code for mobile setup
|
||||||
|
- Time-limited share links
|
||||||
|
- Deployment Flexibility:
|
||||||
|
- Docker (recommended)
|
||||||
|
- Native install (systemd + nginx)
|
||||||
|
- Development mode (hot-reload)
|
||||||
|
|
||||||
|
**Length:** ~200 lines
|
||||||
|
|
||||||
|
### 5. deployment.md (Deployment Guide)
|
||||||
|
|
||||||
|
**Purpose:** Practical deployment instructions
|
||||||
|
|
||||||
|
**Content:**
|
||||||
|
- Docker (Recommended):
|
||||||
|
- Quick start (bash update.sh)
|
||||||
|
- Configuration (.env)
|
||||||
|
- Makefile commands (up, down, logs, dev, migrate, reset-db)
|
||||||
|
- Native Install:
|
||||||
|
- Prerequisites (Go, Node.js, PostgreSQL, Redis, nginx, nftables, wireguard-tools)
|
||||||
|
- Build binaries
|
||||||
|
- Run installer (nexusguard-install.sh)
|
||||||
|
- Create admin account
|
||||||
|
- Development:
|
||||||
|
- Local setup (database + .env)
|
||||||
|
- Hot-reload (go run -tags dev, npm run dev)
|
||||||
|
- Database migration
|
||||||
|
- Agent Installation:
|
||||||
|
- Linux (automated install_agent.sh)
|
||||||
|
- Windows (manual + service setup)
|
||||||
|
- Docker container
|
||||||
|
|
||||||
|
**Length:** ~250 lines
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
1. All 5 files created in `docs/portfolio/`
|
||||||
|
2. Each file is self-contained (can be read independently)
|
||||||
|
3. Internal links work between files
|
||||||
|
4. ASCII diagrams render correctly in Markdown viewers
|
||||||
|
5. Content is accurate and matches actual codebase
|
||||||
|
6. No broken links or references
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
- Use existing README content as base (avoid duplication)
|
||||||
|
- Extract key information from AGENTS.md files
|
||||||
|
- Verify technical details against actual code
|
||||||
|
- Keep diagrams simple and readable
|
||||||
|
- Use consistent Markdown formatting across all files
|
||||||
@@ -49,6 +49,7 @@ Prerequisites:
|
|||||||
- nginx installed and running
|
- nginx installed and running
|
||||||
- PostgreSQL installed and running
|
- PostgreSQL installed and running
|
||||||
- Redis installed and running
|
- Redis installed and running
|
||||||
|
- WireGuard tools (will be auto-installed if missing)
|
||||||
- Pre-built binaries in ./bin/ directory
|
- Pre-built binaries in ./bin/ directory
|
||||||
|
|
||||||
This script will:
|
This script will:
|
||||||
@@ -90,6 +91,29 @@ command -v systemctl >/dev/null 2>&1 || error "systemctl is not installed"
|
|||||||
command -v psql >/dev/null 2>&1 || error "PostgreSQL client (psql) is not installed"
|
command -v psql >/dev/null 2>&1 || error "PostgreSQL client (psql) is not installed"
|
||||||
command -v redis-cli >/dev/null 2>&1 || error "Redis client (redis-cli) is not installed"
|
command -v redis-cli >/dev/null 2>&1 || error "Redis client (redis-cli) is not installed"
|
||||||
|
|
||||||
|
# Check and install WireGuard
|
||||||
|
if ! command -v wg >/dev/null 2>&1; then
|
||||||
|
warn "WireGuard tools not found. Installing..."
|
||||||
|
if command -v apt >/dev/null 2>&1; then
|
||||||
|
apt update -qq && apt install -y -qq wireguard-tools
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then
|
||||||
|
dnf install -y -q wireguard-tools
|
||||||
|
elif command -v yum >/dev/null 2>&1; then
|
||||||
|
yum install -y -q wireguard-tools
|
||||||
|
elif command -v pacman >/dev/null 2>&1; then
|
||||||
|
pacman -S --noconfirm wireguard-tools
|
||||||
|
else
|
||||||
|
error "Cannot auto-install WireGuard. Please install wireguard-tools manually."
|
||||||
|
fi
|
||||||
|
info "WireGuard tools installed: $(wg --version)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check WireGuard kernel module
|
||||||
|
if ! lsmod 2>/dev/null | grep -q wireguard; then
|
||||||
|
warn "WireGuard kernel module not loaded. Loading..."
|
||||||
|
modprobe wireguard 2>/dev/null || warn "Could not load wireguard module (may need manual load or reboot)"
|
||||||
|
fi
|
||||||
|
|
||||||
# Check for pre-built binaries
|
# Check for pre-built binaries
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
SERVER_BINARY="$SCRIPT_DIR/bin/server-core"
|
SERVER_BINARY="$SCRIPT_DIR/bin/server-core"
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# ESP32 Agent — Architecture Reference
|
||||||
|
|
||||||
|
**Status**: ARCHITECTURE ONLY (no code)
|
||||||
|
**Location**: `reference/esp32-agent/` (NOT a submodule)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This directory contains the architecture design and documentation for the ESP32-based WireGuard agent. It is **NOT a git submodule** — it is an architecture reference for future implementation.
|
||||||
|
|
||||||
|
## Why `reference/` not `apps/`?
|
||||||
|
|
||||||
|
If ESP32 agent is implemented as a submodule in the future, it will be added to `apps/device-agent-embedded/`. This `reference/` directory stores the architecture docs separately to avoid conflicts.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
- Document the ESP32 agent architecture
|
||||||
|
- Define the API contract (shared with server-core)
|
||||||
|
- Hardware reference for LW840X module
|
||||||
|
- Track implementation status and TODOs
|
||||||
|
|
||||||
|
## When to Implement
|
||||||
|
|
||||||
|
When ready to build the ESP32 agent:
|
||||||
|
|
||||||
|
1. Create a new repo: `nexus-agent-embedded`
|
||||||
|
2. Add as submodule: `apps/device-agent-embedded/`
|
||||||
|
3. Use ESP-IDF v5.2+ toolchain
|
||||||
|
4. Follow the architecture in `docs/DESIGN.md`
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `docs/DESIGN.md` | Technical design document (full architecture) |
|
||||||
|
| `docs/HARDWARE.md` | ESP32/LW840X hardware reference |
|
||||||
|
| `docs/API_COMPAT.md` | Server API contract (shared protocol) |
|
||||||
|
| `docs/ARCHITECTURE.md` | Module architecture + implementation status |
|
||||||
|
|
||||||
|
## Architecture Summary
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ ESP32 (FreeRTOS) │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Application Layer │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ provision│ │ heartbeat│ │ tunnel │ │ config │ │
|
||||||
|
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │
|
||||||
|
│ │ crypto.c │ │
|
||||||
|
│ │ AES-256-GCM decrypt (mbedtls) │ │
|
||||||
|
│ └──────────────────────────────────────────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Network Stack (lwIP) │
|
||||||
|
│ ┌──────────────────────────────────────────────────┐ │
|
||||||
|
│ │ WireGuard (lwIP socket API) │ │
|
||||||
|
│ └──────────────────────────────────────────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Hardware Layer │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ WiFi │ │ NVS │ │ UART │ │ GPIO │ │
|
||||||
|
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Status
|
||||||
|
|
||||||
|
| Module | Status | Notes |
|
||||||
|
|--------|--------|-------|
|
||||||
|
| `tunnel.c` | ❌ STUB | Needs `wireguard-esp32` component integration |
|
||||||
|
| `heartbeat.c` | ✅ Real | HTTP loop working, config sync TODO |
|
||||||
|
| `provision.c` | ✅ Real | HTTP provisioning working |
|
||||||
|
| `crypto.c` | ✅ Real | mbedtls AES-256-GCM |
|
||||||
|
| `config.c` | ✅ Real | NVS storage |
|
||||||
|
| `wifi.c` | ✅ Real | WiFi STA management |
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **ESP-IDF**: v5.2+
|
||||||
|
- **mbedtls**: AES-256-GCM, SHA-256 (built-in)
|
||||||
|
- **lwIP**: TCP/IP stack (built-in)
|
||||||
|
- **FreeRTOS**: RTOS (built-in)
|
||||||
|
- **wireguard-esp32**: WireGuard tunnel library (NOT YET INTEGRATED)
|
||||||
|
|
||||||
|
## API Contract (Shared with Server)
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/provision → { token, hwid } → { encrypted_config }
|
||||||
|
POST /api/v1/heartbeat → { device_id, tunnel_up, last_handshake } → { config sync }
|
||||||
|
```
|
||||||
|
|
||||||
|
See `docs/API_COMPAT.md` for full specification.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Server Core API](../../server-core/docs/)
|
||||||
|
- [Device Agent (Go)](../../apps/device-agent/)
|
||||||
|
- [Android Agent](../../apps/android-agent/)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Proprietary - NexusGuard
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
# API Compatibility Reference
|
||||||
|
|
||||||
|
**Version**: 1.0.0
|
||||||
|
**Date**: 2026-06-26
|
||||||
|
|
||||||
|
This document defines the HTTP API contract between NexusGuard Server Core and Embedded Agent (ESP32).
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The embedded agent communicates with the server via two HTTP endpoints:
|
||||||
|
|
||||||
|
1. **Provisioning**: `POST /api/v1/provision` — Initial key exchange
|
||||||
|
2. **Heartbeat**: `POST /api/v1/heartbeat` — Periodic status + config sync
|
||||||
|
|
||||||
|
## 1. Provisioning
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/provision
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "string (required)",
|
||||||
|
"hwid": "string (required)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `token` | string | Yes | Registration token (single-use) |
|
||||||
|
| `hwid` | string | Yes | Hardware ID (ESP32 MAC or serial) |
|
||||||
|
|
||||||
|
### Response (Success)
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"encrypted_config": "base64-encoded-bytes"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `encrypted_config` | string | AES-256-GCM encrypted ConfigPayload |
|
||||||
|
|
||||||
|
### Response (Error)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Status | Error | Description |
|
||||||
|
|--------|-------|-------------|
|
||||||
|
| 400 | `"invalid token format"` | Token is not valid UUID |
|
||||||
|
| 400 | `"token already used"` | Token was already consumed |
|
||||||
|
| 400 | `"invalid hardware id"` | HWID is empty or invalid |
|
||||||
|
| 403 | `"token expired"` | Token has expired (24h TTL) |
|
||||||
|
| 500 | `"provisioning failed"` | Server internal error |
|
||||||
|
|
||||||
|
### Decrypted ConfigPayload
|
||||||
|
|
||||||
|
After decrypting `encrypted_config`, the agent receives:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"device_id": "string (UUID)",
|
||||||
|
"private_key": "string (hex, 64 chars)",
|
||||||
|
"preshared_key": "string (hex, 64 chars)",
|
||||||
|
"internal_ip": "string (e.g. 10.172.21.2)",
|
||||||
|
"server_pub": "string (hex, 64 chars)",
|
||||||
|
"endpoint": "string (e.g. italy-twenty.gl.at.ply.gg:59750)",
|
||||||
|
"dns": "string (e.g. 1.1.1.1)",
|
||||||
|
"allowed_ips": "string (e.g. 0.0.0.0/0)",
|
||||||
|
"server_wg_ip": "string (e.g. 10.172.21.1)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `device_id` | string | Device UUID (use in heartbeat) |
|
||||||
|
| `private_key` | string | WireGuard private key (hex) |
|
||||||
|
| `preshared_key` | string | Pre-shared key (hex) |
|
||||||
|
| `internal_ip` | string | Device IP address |
|
||||||
|
| `server_pub` | string | Server WireGuard public key (hex) |
|
||||||
|
| `endpoint` | string | Server WireGuard endpoint |
|
||||||
|
| `dns` | string | DNS server IP |
|
||||||
|
| `allowed_ips` | string | Allowed IPs (e.g. "0.0.0.0/0" for full tunnel) |
|
||||||
|
| `server_wg_ip` | string | Server WireGuard IP |
|
||||||
|
|
||||||
|
## 2. Heartbeat
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/heartbeat
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"device_id": "string (required)",
|
||||||
|
"status": "string (optional)",
|
||||||
|
"state": "string (optional)",
|
||||||
|
"tunnel_up": "boolean (optional)",
|
||||||
|
"last_handshake": "string (optional, ISO 8601)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `device_id` | string | Yes | Device UUID |
|
||||||
|
| `status` | string | No | "connected", "recovering", "stopped" |
|
||||||
|
| `state` | string | No | Free-form state description |
|
||||||
|
| `tunnel_up` | boolean | No | WireGuard tunnel status |
|
||||||
|
| `last_handshake` | string | No | ISO 8601 timestamp of last handshake |
|
||||||
|
|
||||||
|
### Response (Success)
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"device_id": "string",
|
||||||
|
"status": "ok",
|
||||||
|
"internal_ip": "string",
|
||||||
|
"private_key": "string (hex)",
|
||||||
|
"preshared_key": "string (hex)",
|
||||||
|
"server_pub": "string (hex)",
|
||||||
|
"endpoint": "string",
|
||||||
|
"server_wg_ip": "string",
|
||||||
|
"dns": "string",
|
||||||
|
"allowed_ips": "string",
|
||||||
|
"forwards_hash": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `device_id` | string | Device UUID |
|
||||||
|
| `status` | string | "ok" or error |
|
||||||
|
| `internal_ip` | string | Device IP (may change) |
|
||||||
|
| `private_key` | string | WireGuard private key (may rotate) |
|
||||||
|
| `preshared_key` | string | Pre-shared key (may rotate) |
|
||||||
|
| `server_pub` | string | Server public key |
|
||||||
|
| `endpoint` | string | Server endpoint |
|
||||||
|
| `server_wg_ip` | string | Server WireGuard IP |
|
||||||
|
| `dns` | string | DNS server |
|
||||||
|
| `allowed_ips` | string | Allowed IPs |
|
||||||
|
| `forwards_hash` | string | Hash of port forwards (for change detection) |
|
||||||
|
|
||||||
|
### Response (Error)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Status | Error | Description |
|
||||||
|
|--------|-------|-------------|
|
||||||
|
| 400 | `"invalid device id"` | Device ID is not valid UUID |
|
||||||
|
| 403 | `"device is suspended"` | Device is suspended by admin |
|
||||||
|
| 500 | `"heartbeat failed"` | Server internal error |
|
||||||
|
|
||||||
|
## 3. Crypto Protocol
|
||||||
|
|
||||||
|
### Key Derivation
|
||||||
|
|
||||||
|
```
|
||||||
|
key = SHA256(hwid + salt)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `hwid`: Hardware ID (ESP32 MAC or serial)
|
||||||
|
- `salt`: Server's `SERVER_SALT` environment variable
|
||||||
|
- Output: 32-byte AES key
|
||||||
|
|
||||||
|
### Encryption (Server Side)
|
||||||
|
|
||||||
|
```
|
||||||
|
plaintext = ConfigPayload JSON
|
||||||
|
nonce = random 12 bytes
|
||||||
|
ciphertext = AES-256-GCM-Encrypt(key, nonce, plaintext)
|
||||||
|
encrypted_config = nonce + ciphertext
|
||||||
|
```
|
||||||
|
|
||||||
|
### Decryption (Agent Side)
|
||||||
|
|
||||||
|
```
|
||||||
|
encrypted_config = response.encrypted_config (base64 decoded)
|
||||||
|
nonce = encrypted_config[0:12]
|
||||||
|
ciphertext = encrypted_config[12:]
|
||||||
|
plaintext = AES-256-GCM-Decrypt(key, nonce, ciphertext)
|
||||||
|
ConfigPayload = JSON.parse(plaintext)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation Notes
|
||||||
|
|
||||||
|
- Use `mbedtls_sha256()` for key derivation
|
||||||
|
- Use `mbedtls_gcm_*()` for AES-256-GCM
|
||||||
|
- Nonce size: 12 bytes (fixed for GCM)
|
||||||
|
- Tag size: 16 bytes (default for GCM)
|
||||||
|
|
||||||
|
## 4. Config Change Detection
|
||||||
|
|
||||||
|
### Heartbeat Hash Comparison
|
||||||
|
|
||||||
|
The server returns `forwards_hash` in heartbeat response. Agent should:
|
||||||
|
|
||||||
|
1. Store last received `forwards_hash` in NVS
|
||||||
|
2. Compare with current hash
|
||||||
|
3. If different, trigger config reload
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Pseudo-code
|
||||||
|
char last_hash[64];
|
||||||
|
nvs_get_str(nvs_handle, "config_hash", last_hash, sizeof(last_hash));
|
||||||
|
|
||||||
|
if (strcmp(last_hash, response.forwards_hash) != 0) {
|
||||||
|
// Config changed, rebuild tunnel
|
||||||
|
tunnel_rebuild(&config);
|
||||||
|
nvs_set_str(nvs_handle, "config_hash", response.forwards_hash);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Versioning
|
||||||
|
|
||||||
|
### Current Version
|
||||||
|
|
||||||
|
- API Version: v1 (implicit, no version in URL)
|
||||||
|
- Agent Version: 1.0.0
|
||||||
|
|
||||||
|
### Compatibility Policy
|
||||||
|
|
||||||
|
- **Breaking Changes**: Major version bump (v2)
|
||||||
|
- **New Fields**: Added without version bump (agent ignores unknown fields)
|
||||||
|
- **Deprecation**: 6-month notice before removal
|
||||||
|
|
||||||
|
### Agent Behavior
|
||||||
|
|
||||||
|
- Agent ignores unknown fields in server response
|
||||||
|
- Agent sends only required fields in request
|
||||||
|
- Agent handles missing optional fields gracefully
|
||||||
|
|
||||||
|
## 6. Examples
|
||||||
|
|
||||||
|
### Provisioning Flow
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 1. Get hardware ID
|
||||||
|
char hwid[32];
|
||||||
|
get_hardware_id(hwid, sizeof(hwid));
|
||||||
|
|
||||||
|
// 2. Send provisioning request
|
||||||
|
char *response = http_post(server_url, "/api/v1/provision",
|
||||||
|
"{\"token\":\"%s\",\"hwid\":\"%s\"}", token, hwid);
|
||||||
|
|
||||||
|
// 3. Parse encrypted config
|
||||||
|
cJSON *json = cJSON_Parse(response);
|
||||||
|
char *encrypted = cJSON_GetObjectItem(json, "encrypted_config")->valuestring;
|
||||||
|
|
||||||
|
// 4. Decrypt config
|
||||||
|
wg_config_t config;
|
||||||
|
decrypt_config(encrypted, strlen(encrypted), hwid, salt, &config);
|
||||||
|
|
||||||
|
// 5. Store in NVS
|
||||||
|
config_save(&config);
|
||||||
|
|
||||||
|
// 6. Apply to WireGuard
|
||||||
|
tunnel_init(&config);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Heartbeat Flow
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 1. Get tunnel status
|
||||||
|
bool tunnel_up = tunnel_is_up();
|
||||||
|
int64_t last_hs = tunnel_get_last_handshake();
|
||||||
|
|
||||||
|
// 2. Send heartbeat
|
||||||
|
char *response = http_post(server_url, "/api/v1/heartbeat",
|
||||||
|
"{\"device_id\":\"%s\",\"tunnel_up\":%s,\"last_handshake\":\"%s\"}",
|
||||||
|
config.device_id, tunnel_up ? "true" : "false",
|
||||||
|
format_timestamp(last_hs));
|
||||||
|
|
||||||
|
// 3. Check for config changes
|
||||||
|
cJSON *json = cJSON_Parse(response);
|
||||||
|
const char *new_hash = cJSON_GetObjectItem(json, "forwards_hash")->valuestring;
|
||||||
|
|
||||||
|
if (strcmp(config.config_hash, new_hash) != 0) {
|
||||||
|
// Config changed, reload
|
||||||
|
load_config_from_response(json, &config);
|
||||||
|
tunnel_rebuild(&config);
|
||||||
|
config_save(&config);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Error Handling
|
||||||
|
|
||||||
|
### HTTP Retries
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define MAX_RETRIES 3
|
||||||
|
#define RETRY_DELAY_MS 1000
|
||||||
|
|
||||||
|
for (int i = 0; i < MAX_RETRIES; i++) {
|
||||||
|
esp_err_t err = http_post(...);
|
||||||
|
if (err == ESP_OK) break;
|
||||||
|
vTaskDelay(RETRY_DELAY_MS * (i + 1) / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timeout Settings
|
||||||
|
|
||||||
|
- Connect timeout: 10 seconds
|
||||||
|
- Read timeout: 30 seconds
|
||||||
|
- Write timeout: 10 seconds
|
||||||
|
|
||||||
|
## 8. Security Notes
|
||||||
|
|
||||||
|
- All communication over HTTPS (TLS 1.2+)
|
||||||
|
- Registration token is single-use (consumed on first provisioning)
|
||||||
|
- Device can re-provision by obtaining new token
|
||||||
|
- Keys never logged or transmitted in plaintext
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
# ESP32 Agent — Module Architecture
|
||||||
|
|
||||||
|
**Status**: ARCHITECTURE ONLY
|
||||||
|
**Last Updated**: 2026-07-06
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document defines the module architecture for the ESP32-based WireGuard agent. The agent runs on FreeRTOS + ESP-IDF and connects to NexusGuard Server Core via HTTP.
|
||||||
|
|
||||||
|
## Task Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
FreeRTOS Tasks
|
||||||
|
├── main_task # Initialization, event loop
|
||||||
|
├── wifi_task # WiFi STA management (priority: 5)
|
||||||
|
├── http_task # HTTP client (provisioning, heartbeat) (priority: 4)
|
||||||
|
├── wg_task # WireGuard tunnel management (priority: 3)
|
||||||
|
└── led_task # Status LED indication (priority: 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Machine
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐
|
||||||
|
│ BOOT │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────┐
|
||||||
|
│ WIFI_INIT │ ◄──────────────────────────────┐
|
||||||
|
└──────┬──────┘ │
|
||||||
|
│ connected │
|
||||||
|
▼ │
|
||||||
|
┌─────────────┐ fail ┌─────────────┐│
|
||||||
|
│ PROVISION │───────────────►│ WIFI_RETRY ││
|
||||||
|
└──────┬──────┘ └──────┬──────┘│
|
||||||
|
│ success │ │
|
||||||
|
▼ └───────┘
|
||||||
|
┌─────────────┐
|
||||||
|
│ TUNNEL_UP │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────┐
|
||||||
|
│ HEARTBEAT │ ◄─── 30s interval
|
||||||
|
└──────┬──────┘
|
||||||
|
│ config_changed
|
||||||
|
▼
|
||||||
|
┌─────────────┐
|
||||||
|
│ TUNNEL_REBUILD │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
└──► HEARTBEAT
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module Design
|
||||||
|
|
||||||
|
### 1. main.c
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Hardware initialization (NVS, WiFi, GPIO)
|
||||||
|
- Task creation and event loop
|
||||||
|
- Signal handling (Ctrl+C graceful shutdown)
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
```c
|
||||||
|
void app_main(void); // Entry point
|
||||||
|
void shutdown_handler(void); // Graceful shutdown
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. provision.c
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- HTTP POST to `/api/v1/provision`
|
||||||
|
- Parse encrypted config response
|
||||||
|
- Decrypt config via crypto.c
|
||||||
|
- Store config in NVS
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
```c
|
||||||
|
esp_err_t provision_device(const char *server_url, const char *token,
|
||||||
|
const char *hwid, wg_config_t *config);
|
||||||
|
esp_err_t decrypt_config(const uint8_t *encrypted, size_t len,
|
||||||
|
const char *hwid, const char *salt,
|
||||||
|
wg_config_t *config);
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP Request**:
|
||||||
|
```json
|
||||||
|
POST /api/v1/provision
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"token": "registration-token",
|
||||||
|
"hwid": "esp32-hardware-id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"encrypted_config": "base64-encoded-bytes"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. heartbeat.c
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- HTTP POST to `/api/v1/heartbeat` every 30s
|
||||||
|
- Send device status + tunnel state
|
||||||
|
- Receive config sync (detect changes)
|
||||||
|
- Trigger tunnel rebuild if config changed
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
```c
|
||||||
|
esp_err_t heartbeat_send(const wg_config_t *config,
|
||||||
|
const char *status, bool tunnel_up,
|
||||||
|
int64_t last_handshake);
|
||||||
|
bool heartbeat_config_changed(const char *old_hash, const char *new_hash);
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP Request**:
|
||||||
|
```json
|
||||||
|
POST /api/v1/heartbeat
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"device_id": "uuid",
|
||||||
|
"status": "connected",
|
||||||
|
"tunnel_up": true,
|
||||||
|
"last_handshake": "2026-06-26T09:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"device_id": "uuid",
|
||||||
|
"status": "ok",
|
||||||
|
"internal_ip": "10.172.21.2",
|
||||||
|
"private_key": "hex",
|
||||||
|
"preshared_key": "hex",
|
||||||
|
"server_pub": "hex",
|
||||||
|
"endpoint": "italy-twenty.gl.at.ply.gg:59750",
|
||||||
|
"dns": "1.1.1.1",
|
||||||
|
"allowed_ips": "0.0.0.0/0",
|
||||||
|
"server_wg_ip": "10.172.21.1",
|
||||||
|
"forwards_hash": "hash-string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. tunnel.c
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Initialize WireGuard tunnel via lwIP
|
||||||
|
- Apply config (keys, endpoint, allowed IPs)
|
||||||
|
- Monitor tunnel state (handshake time)
|
||||||
|
- Rebuild tunnel on config change
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
```c
|
||||||
|
esp_err_t tunnel_init(const wg_config_t *config);
|
||||||
|
esp_err_t tunnel_apply_config(const wg_config_t *config);
|
||||||
|
esp_err_t tunnel_rebuild(const wg_config_t *config);
|
||||||
|
int64_t tunnel_get_last_handshake(void);
|
||||||
|
bool tunnel_is_up(void);
|
||||||
|
```
|
||||||
|
|
||||||
|
**WireGuard Config Application**:
|
||||||
|
```c
|
||||||
|
// Pseudo-code
|
||||||
|
wg_device wg = {
|
||||||
|
.private_key = config->private_key,
|
||||||
|
.listen_port = 0, // ephemeral
|
||||||
|
};
|
||||||
|
wg_peer peer = {
|
||||||
|
.public_key = config->server_pub,
|
||||||
|
.preshared_key = config->preshared_key,
|
||||||
|
.endpoint = config->endpoint,
|
||||||
|
.allowed_ips = config->allowed_ips,
|
||||||
|
};
|
||||||
|
wg_set_device(&wg);
|
||||||
|
wg_add_peer(&wg, &peer);
|
||||||
|
wg_set_peer_allowed_ips(&wg, &peer, config->allowed_ips);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. crypto.c
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Key derivation: SHA256(hwid + salt)
|
||||||
|
- AES-256-GCM decryption
|
||||||
|
- Secure memory handling
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
```c
|
||||||
|
esp_err_t crypto_derive_key(const char *hwid, const char *salt,
|
||||||
|
uint8_t key[32]);
|
||||||
|
esp_err_t crypto_decrypt(const uint8_t *ciphertext, size_t len,
|
||||||
|
const uint8_t key[32], uint8_t **plaintext,
|
||||||
|
size_t *plaintext_len);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Algorithm**:
|
||||||
|
```
|
||||||
|
Key Derivation:
|
||||||
|
key = SHA256(hwid + salt) // 32 bytes
|
||||||
|
|
||||||
|
Decryption:
|
||||||
|
nonce = ciphertext[0:12] // first 12 bytes
|
||||||
|
encrypted = ciphertext[12:] // rest
|
||||||
|
plaintext = AES-256-GCM-Decrypt(key, nonce, encrypted)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. config.c
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- NVS read/write for config fields
|
||||||
|
- Config validation
|
||||||
|
- Config versioning
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
```c
|
||||||
|
esp_err_t config_save(const wg_config_t *config);
|
||||||
|
esp_err_t config_load(wg_config_t *config);
|
||||||
|
esp_err_t config_clear(void);
|
||||||
|
bool config_is_valid(const wg_config_t *config);
|
||||||
|
```
|
||||||
|
|
||||||
|
**NVS Keys**:
|
||||||
|
| Key | Max Size | Description |
|
||||||
|
|-----|----------|-------------|
|
||||||
|
| `device_id` | 37 | UUID string |
|
||||||
|
| `private_key` | 64 | WG private key hex |
|
||||||
|
| `preshared_key` | 64 | PSK hex |
|
||||||
|
| `internal_ip` | 18 | IP address |
|
||||||
|
| `server_pub` | 64 | Server public key hex |
|
||||||
|
| `endpoint` | 128 | Server endpoint |
|
||||||
|
| `dns` | 64 | DNS server |
|
||||||
|
| `allowed_ips` | 128 | Allowed IPs |
|
||||||
|
| `config_hash` | 64 | Last config hash |
|
||||||
|
|
||||||
|
### 7. wifi.c
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- WiFi STA initialization
|
||||||
|
- Connection management
|
||||||
|
- Reconnection handling
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
```c
|
||||||
|
esp_err_t wifi_init_sta(const char *ssid, const char *password);
|
||||||
|
esp_err_t wifi_connect(void);
|
||||||
|
esp_err_t wifi_disconnect(void);
|
||||||
|
bool wifi_is_connected(void);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Structures
|
||||||
|
|
||||||
|
### wg_config_t
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
char device_id[37]; // UUID
|
||||||
|
char private_key[64]; // WG private key hex
|
||||||
|
char preshared_key[64]; // PSK hex
|
||||||
|
char internal_ip[18]; // e.g. "10.172.21.2"
|
||||||
|
char server_pub[64]; // Server WG public key hex
|
||||||
|
char endpoint[128]; // e.g. "italy-twenty.gl.at.ply.gg:59750"
|
||||||
|
char dns[64]; // DNS server
|
||||||
|
char allowed_ips[128]; // e.g. "0.0.0.0/0"
|
||||||
|
char server_wg_ip[18]; // e.g. "10.172.21.1"
|
||||||
|
char config_hash[64]; // Last config hash
|
||||||
|
} wg_config_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
### device_state_t
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum {
|
||||||
|
STATE_BOOT,
|
||||||
|
STATE_WIFI_INIT,
|
||||||
|
STATE_WIFI_CONNECTED,
|
||||||
|
STATE_PROVISIONING,
|
||||||
|
STATE_PROVISIONED,
|
||||||
|
STATE_TUNNEL_UP,
|
||||||
|
STATE_TUNNEL_DOWN,
|
||||||
|
STATE_HEARTBEAT,
|
||||||
|
STATE_ERROR
|
||||||
|
} device_state_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Error Codes
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum {
|
||||||
|
ERR_OK = 0,
|
||||||
|
ERR_WIFI_CONNECT = -1,
|
||||||
|
ERR_HTTP_PROVISION = -2,
|
||||||
|
ERR_HTTP_HEARTBEAT = -3,
|
||||||
|
ERR_CRYPTO_DECRYPT = -4,
|
||||||
|
ERR_CONFIG_INVALID = -5,
|
||||||
|
ERR_TUNNEL_INIT = -6,
|
||||||
|
ERR_TUNNEL_APPLY = -7,
|
||||||
|
ERR_NVS_READ = -8,
|
||||||
|
ERR_NVS_WRITE = -9,
|
||||||
|
} agent_error_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recovery Strategy
|
||||||
|
|
||||||
|
| Error | Recovery |
|
||||||
|
|-------|----------|
|
||||||
|
| WiFi connect fail | Retry 3x, then reboot |
|
||||||
|
| HTTP provision fail | Retry 3x, then reboot |
|
||||||
|
| HTTP heartbeat fail | Log only, continue |
|
||||||
|
| Crypto decrypt fail | Clear config, re-provision |
|
||||||
|
| Tunnel init fail | Retry 3x, then reboot |
|
||||||
|
| NVS read fail | Use defaults, re-provision |
|
||||||
|
|
||||||
|
## Implementation Status
|
||||||
|
|
||||||
|
### ✅ Implemented (Real Code)
|
||||||
|
|
||||||
|
| Module | File | Status |
|
||||||
|
|--------|------|--------|
|
||||||
|
| Provisioning | `provision.c` | HTTP POST working |
|
||||||
|
| Heartbeat | `heartbeat.c` | HTTP loop working |
|
||||||
|
| Crypto | `crypto.c` | mbedtls AES-256-GCM |
|
||||||
|
| Config | `config.c` | NVS read/write |
|
||||||
|
| WiFi | `wifi.c` | STA connection |
|
||||||
|
|
||||||
|
### ❌ STUB (Needs Implementation)
|
||||||
|
|
||||||
|
| Module | File | What's Missing |
|
||||||
|
|--------|------|----------------|
|
||||||
|
| Tunnel | `tunnel.c` | `wireguard-esp32` component integration |
|
||||||
|
| Heartbeat | `heartbeat.c` | Config sync parsing from response |
|
||||||
|
|
||||||
|
### TODO: tunnel.c Integration
|
||||||
|
|
||||||
|
When integrating `wireguard-esp32` component:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 1. Parse private_key hex to binary
|
||||||
|
// 2. Parse server_pub hex to binary
|
||||||
|
// 3. Parse preshared_key hex to binary
|
||||||
|
// 4. Create WireGuard device with private_key
|
||||||
|
// 5. Add peer with server_pub, preshared_key, endpoint
|
||||||
|
// 6. Set peer allowed_ips
|
||||||
|
// 7. Start WireGuard tunnel
|
||||||
|
```
|
||||||
|
|
||||||
|
### TODO: heartbeat.c Config Sync
|
||||||
|
|
||||||
|
When parsing heartbeat response:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 1. Parse response JSON
|
||||||
|
// 2. Extract config_hash
|
||||||
|
// 3. Compare with stored config_hash
|
||||||
|
// 4. If different → parse new config fields
|
||||||
|
// 5. Save to NVS
|
||||||
|
// 6. Call tunnel_rebuild()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Phase 2
|
||||||
|
- OTA firmware updates
|
||||||
|
- Custom CA certificate support
|
||||||
|
- Power optimization (deep sleep)
|
||||||
|
- Multiple server support
|
||||||
|
|
||||||
|
### Phase 3
|
||||||
|
- Zephyr RTOS port
|
||||||
|
- ESP32-S2 support
|
||||||
|
- Ethernet (SPI) support
|
||||||
|
- Bluetooth provisioning
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ESP-IDF Documentation](https://docs.espressif.com/projects/esp-idf/)
|
||||||
|
- [WireGuard Protocol](https://www.wireguard.com/protocol/)
|
||||||
|
- [lwIP Documentation](https://www.nongnu.org/lwip/)
|
||||||
|
- [mbedtls Documentation](https://mbed-tls.readthedocs.io/)
|
||||||
@@ -0,0 +1,582 @@
|
|||||||
|
# Technical Design Document
|
||||||
|
|
||||||
|
**Version**: 1.0.0
|
||||||
|
**Date**: 2026-06-26
|
||||||
|
**Status**: Draft
|
||||||
|
|
||||||
|
## 1. Executive Summary
|
||||||
|
|
||||||
|
NexusGuard Embedded Agent is an ESP32-based WireGuard client designed for IoT and edge devices. It provides zero-trust VPN tunneling with auto-provisioning and heartbeat monitoring, connecting to the existing NexusGuard Server Core infrastructure.
|
||||||
|
|
||||||
|
## 2. Goals & Non-Goals
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
- ESP32-based WireGuard tunnel (LW840X compatible)
|
||||||
|
- Auto-provisioning via HTTP (same API as Go agent)
|
||||||
|
- Heartbeat monitoring with config sync
|
||||||
|
- NVS-based config persistence (encrypted)
|
||||||
|
- FreeRTOS task-based architecture
|
||||||
|
- Low power consumption (WiFi sleep modes)
|
||||||
|
- **Independent build** — no dependency on main NexusGuard repo
|
||||||
|
- **Server discovery** — configurable server URL + registration token
|
||||||
|
|
||||||
|
### Non-Goals
|
||||||
|
- Port forwarding (userspace agent only)
|
||||||
|
- gRPC support (too heavy for ESP32)
|
||||||
|
- Zephyr RTOS support (ESP-IDF uses FreeRTOS)
|
||||||
|
- Multi-peer support (single tunnel only)
|
||||||
|
- Kernel WireGuard (lwIP userspace only)
|
||||||
|
|
||||||
|
## 3. Architecture
|
||||||
|
|
||||||
|
### 3.1 System Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ ESP32 (FreeRTOS) │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Application Layer │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ provision│ │ heartbeat│ │ tunnel │ │ config │ │
|
||||||
|
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │
|
||||||
|
│ │ crypto.c │ │
|
||||||
|
│ │ AES-256-GCM decrypt (mbedtls) │ │
|
||||||
|
│ └──────────────────────────────────────────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Network Stack (lwIP) │
|
||||||
|
│ ┌──────────────────────────────────────────────────┐ │
|
||||||
|
│ │ WireGuard (lwIP socket API) │ │
|
||||||
|
│ └──────────────────────────────────────────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Hardware Layer │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ WiFi │ │ NVS │ │ UART │ │ GPIO │ │
|
||||||
|
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Task Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
FreeRTOS Tasks
|
||||||
|
├── main_task # Initialization, event loop
|
||||||
|
├── wifi_task # WiFi STA management (priority: 5)
|
||||||
|
├── http_task # HTTP client (provisioning, heartbeat) (priority: 4)
|
||||||
|
├── wg_task # WireGuard tunnel management (priority: 3)
|
||||||
|
└── led_task # Status LED indication (priority: 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 State Machine
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐
|
||||||
|
│ BOOT │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────┐
|
||||||
|
│ WIFI_INIT │ ◄──────────────────────────────┐
|
||||||
|
└──────┬──────┘ │
|
||||||
|
│ connected │
|
||||||
|
▼ │
|
||||||
|
┌─────────────┐ fail ┌─────────────┐│
|
||||||
|
│ PROVISION │───────────────►│ WIFI_RETRY ││
|
||||||
|
└──────┬──────┘ └──────┬──────┘│
|
||||||
|
│ success │ │
|
||||||
|
▼ └───────┘
|
||||||
|
┌─────────────┐
|
||||||
|
│ TUNNEL_UP │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────┐
|
||||||
|
│ HEARTBEAT │ ◄─── 30s interval
|
||||||
|
└──────┬──────┘
|
||||||
|
│ config_changed
|
||||||
|
▼
|
||||||
|
┌─────────────┐
|
||||||
|
│ TUNNEL_REBUILD │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
└──► HEARTBEAT
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Server Discovery & Provisioning
|
||||||
|
|
||||||
|
### 4.1 Overview
|
||||||
|
|
||||||
|
The embedded agent connects to NexusGuard Server Core via HTTP. The agent must be configured with:
|
||||||
|
- **Server URL**: The API endpoint (e.g., `https://api-nexus.datadunia.com`)
|
||||||
|
- **Registration Token**: Single-use token for initial provisioning
|
||||||
|
- **Server Salt**: For AES-256-GCM key derivation (shared secret)
|
||||||
|
|
||||||
|
### 4.2 Provisioning Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ PROVISIONING FLOW │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 1. Device boots │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 2. Check NVS for saved config │
|
||||||
|
│ │ │
|
||||||
|
│ ├─► Config exists ──► Load config ──► Skip to TUNNEL │
|
||||||
|
│ │ │
|
||||||
|
│ └─► No config ──► Continue to provisioning │
|
||||||
|
│ │
|
||||||
|
│ 3. Connect to WiFi │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 4. HTTP POST /api/v1/provision │
|
||||||
|
│ │ Request: { "token": "...", "hwid": "..." } │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 5. Server validates token, generates WireGuard keys │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 6. Server encrypts config with AES-256-GCM │
|
||||||
|
│ │ Key = SHA256(hwid + salt) │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 7. Server responds with encrypted_config │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 8. Agent decrypts config using mbedtls │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 9. Store config in NVS │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 10. Apply WireGuard tunnel │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Server Configuration Sources
|
||||||
|
|
||||||
|
The embedded agent can obtain server configuration from:
|
||||||
|
|
||||||
|
| Source | Priority | Use Case |
|
||||||
|
|--------|----------|----------|
|
||||||
|
| NVS (saved config) | 1 | After initial provisioning |
|
||||||
|
| sdkconfig (menuconfig) | 2 | Build-time configuration |
|
||||||
|
| SmartConfig / BLE | 3 | WiFi provisioning (future) |
|
||||||
|
| QR Code | 4 | Manual provisioning (future) |
|
||||||
|
|
||||||
|
### 4.4 Build-Time Configuration
|
||||||
|
|
||||||
|
Configure server URL and token via `idf.py menuconfig`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Component config → NexusGuard Agent
|
||||||
|
├── Server URL (NEXUS_SERVER_URL)
|
||||||
|
├── Registration Token (NEXUS_REG_TOKEN)
|
||||||
|
├── Server Salt (NEXUS_SERVER_SALT)
|
||||||
|
└── WiFi SSID (NEXUS_WIFI_SSID)
|
||||||
|
└── WiFi Password (NEXUS_WIFI_PASSWORD)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 Runtime Configuration (NVS)
|
||||||
|
|
||||||
|
After first provisioning, config is stored in NVS:
|
||||||
|
|
||||||
|
| Key | Description |
|
||||||
|
|-----|-------------|
|
||||||
|
| `server_url` | Server API URL |
|
||||||
|
| `device_id` | Assigned device UUID |
|
||||||
|
| `private_key` | WireGuard private key |
|
||||||
|
| `preshared_key` | Pre-shared key |
|
||||||
|
| `internal_ip` | Device IP address |
|
||||||
|
| `server_pub` | Server public key |
|
||||||
|
| `endpoint` | Server WireGuard endpoint |
|
||||||
|
| `dns` | DNS server |
|
||||||
|
| `allowed_ips` | Allowed IPs |
|
||||||
|
| `server_wg_ip` | Server WireGuard IP |
|
||||||
|
| `config_hash` | Last config hash |
|
||||||
|
|
||||||
|
### 4.6 Server API Contract
|
||||||
|
|
||||||
|
The embedded agent uses the **same HTTP API** as the Go device-agent:
|
||||||
|
|
||||||
|
| Endpoint | Method | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| `/api/v1/provision` | POST | Initial key exchange |
|
||||||
|
| `/api/v1/heartbeat` | POST | Periodic status + config sync |
|
||||||
|
|
||||||
|
**Key**: The API contract is stable and documented in `docs/API_COMPAT.md`. The server does not need to know whether the client is Go agent or ESP32 agent — it treats them identically.
|
||||||
|
|
||||||
|
### 4.7 Independence from Main Repo
|
||||||
|
|
||||||
|
The embedded agent is **fully independent**:
|
||||||
|
|
||||||
|
```
|
||||||
|
nexus-agent-embedded/ # Standalone repo
|
||||||
|
├── main/ # C source code
|
||||||
|
├── components/ # ESP-IDF components
|
||||||
|
├── docs/ # Documentation
|
||||||
|
├── CMakeLists.txt # Build system
|
||||||
|
└── sdkconfig.defaults # Configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
**No dependency on**:
|
||||||
|
- `server-core` (Go backend)
|
||||||
|
- `device-agent` (Go agent)
|
||||||
|
- `dashboard-ui` (Vue frontend)
|
||||||
|
|
||||||
|
**Build anywhere** with ESP-IDF toolchain — no Go, no Node.js, no Docker.
|
||||||
|
|
||||||
|
## 5. Module Design
|
||||||
|
|
||||||
|
### 4.1 main.c
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Hardware initialization (NVS, WiFi, GPIO)
|
||||||
|
- Task creation and event loop
|
||||||
|
- Signal handling (Ctrl+C graceful shutdown)
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
```c
|
||||||
|
void app_main(void); // Entry point
|
||||||
|
void shutdown_handler(void); // Graceful shutdown
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 provision.c
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- HTTP POST to `/api/v1/provision`
|
||||||
|
- Parse encrypted config response
|
||||||
|
- Decrypt config via crypto.c
|
||||||
|
- Store config in NVS
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
```c
|
||||||
|
esp_err_t provision_device(const char *server_url, const char *token,
|
||||||
|
const char *hwid, wg_config_t *config);
|
||||||
|
esp_err_t decrypt_config(const uint8_t *encrypted, size_t len,
|
||||||
|
const char *hwid, const char *salt,
|
||||||
|
wg_config_t *config);
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP Request**:
|
||||||
|
```json
|
||||||
|
POST /api/v1/provision
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"token": "registration-token",
|
||||||
|
"hwid": "esp32-hardware-id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"encrypted_config": "base64-encoded-bytes"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 heartbeat.c
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- HTTP POST to `/api/v1/heartbeat` every 30s
|
||||||
|
- Send device status + tunnel state
|
||||||
|
- Receive config sync (detect changes)
|
||||||
|
- Trigger tunnel rebuild if config changed
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
```c
|
||||||
|
esp_err_t heartbeat_send(const wg_config_t *config,
|
||||||
|
const char *status, bool tunnel_up,
|
||||||
|
int64_t last_handshake);
|
||||||
|
bool heartbeat_config_changed(const char *old_hash, const char *new_hash);
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP Request**:
|
||||||
|
```json
|
||||||
|
POST /api/v1/heartbeat
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"device_id": "uuid",
|
||||||
|
"status": "connected",
|
||||||
|
"tunnel_up": true,
|
||||||
|
"last_handshake": "2026-06-26T09:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"device_id": "uuid",
|
||||||
|
"status": "ok",
|
||||||
|
"internal_ip": "10.172.21.2",
|
||||||
|
"private_key": "hex",
|
||||||
|
"preshared_key": "hex",
|
||||||
|
"server_pub": "hex",
|
||||||
|
"endpoint": "italy-twenty.gl.at.ply.gg:59750",
|
||||||
|
"dns": "1.1.1.1",
|
||||||
|
"allowed_ips": "0.0.0.0/0",
|
||||||
|
"server_wg_ip": "10.172.21.1",
|
||||||
|
"forwards_hash": "hash-string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 tunnel.c
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Initialize WireGuard tunnel via lwIP
|
||||||
|
- Apply config (keys, endpoint, allowed IPs)
|
||||||
|
- Monitor tunnel state (handshake time)
|
||||||
|
- Rebuild tunnel on config change
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
```c
|
||||||
|
esp_err_t tunnel_init(const wg_config_t *config);
|
||||||
|
esp_err_t tunnel_apply_config(const wg_config_t *config);
|
||||||
|
esp_err_t tunnel_rebuild(const wg_config_t *config);
|
||||||
|
int64_t tunnel_get_last_handshake(void);
|
||||||
|
bool tunnel_is_up(void);
|
||||||
|
```
|
||||||
|
|
||||||
|
**WireGuard Config Application**:
|
||||||
|
```c
|
||||||
|
// Pseudo-code
|
||||||
|
wg_device wg = {
|
||||||
|
.private_key = config->private_key,
|
||||||
|
.listen_port = 0, // ephemeral
|
||||||
|
};
|
||||||
|
wg_peer peer = {
|
||||||
|
.public_key = config->server_pub,
|
||||||
|
.preshared_key = config->preshared_key,
|
||||||
|
.endpoint = config->endpoint,
|
||||||
|
.allowed_ips = config->allowed_ips,
|
||||||
|
};
|
||||||
|
wg_set_device(&wg);
|
||||||
|
wg_add_peer(&wg, &peer);
|
||||||
|
wg_set_peer_allowed_ips(&wg, &peer, config->allowed_ips);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 crypto.c
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Key derivation: SHA256(hwid + salt)
|
||||||
|
- AES-256-GCM decryption
|
||||||
|
- Secure memory handling
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
```c
|
||||||
|
esp_err_t crypto_derive_key(const char *hwid, const char *salt,
|
||||||
|
uint8_t key[32]);
|
||||||
|
esp_err_t crypto_decrypt(const uint8_t *ciphertext, size_t len,
|
||||||
|
const uint8_t key[32], uint8_t **plaintext,
|
||||||
|
size_t *plaintext_len);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Algorithm**:
|
||||||
|
```
|
||||||
|
Key Derivation:
|
||||||
|
key = SHA256(hwid + salt) // 32 bytes
|
||||||
|
|
||||||
|
Decryption:
|
||||||
|
nonce = ciphertext[0:12] // first 12 bytes
|
||||||
|
encrypted = ciphertext[12:] // rest
|
||||||
|
plaintext = AES-256-GCM-Decrypt(key, nonce, encrypted)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.6 config.c
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- NVS read/write for config fields
|
||||||
|
- Config validation
|
||||||
|
- Config versioning
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
```c
|
||||||
|
esp_err_t config_save(const wg_config_t *config);
|
||||||
|
esp_err_t config_load(wg_config_t *config);
|
||||||
|
esp_err_t config_clear(void);
|
||||||
|
bool config_is_valid(const wg_config_t *config);
|
||||||
|
```
|
||||||
|
|
||||||
|
**NVS Keys**:
|
||||||
|
| Key | Max Size | Description |
|
||||||
|
|-----|----------|-------------|
|
||||||
|
| `device_id` | 37 | UUID string |
|
||||||
|
| `private_key` | 64 | WG private key hex |
|
||||||
|
| `preshared_key` | 64 | PSK hex |
|
||||||
|
| `internal_ip` | 18 | IP address |
|
||||||
|
| `server_pub` | 64 | Server public key hex |
|
||||||
|
| `endpoint` | 128 | Server endpoint |
|
||||||
|
| `dns` | 64 | DNS server |
|
||||||
|
| `allowed_ips` | 128 | Allowed IPs |
|
||||||
|
| `config_hash` | 64 | Last config hash |
|
||||||
|
|
||||||
|
### 4.7 wifi.c
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- WiFi STA initialization
|
||||||
|
- Connection management
|
||||||
|
- Reconnection handling
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
```c
|
||||||
|
esp_err_t wifi_init_sta(const char *ssid, const char *password);
|
||||||
|
esp_err_t wifi_connect(void);
|
||||||
|
esp_err_t wifi_disconnect(void);
|
||||||
|
bool wifi_is_connected(void);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Data Structures
|
||||||
|
|
||||||
|
### 5.1 wg_config_t
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
char device_id[37]; // UUID
|
||||||
|
char private_key[64]; // WG private key hex
|
||||||
|
char preshared_key[64]; // PSK hex
|
||||||
|
char internal_ip[18]; // e.g. "10.172.21.2"
|
||||||
|
char server_pub[64]; // Server WG public key hex
|
||||||
|
char endpoint[128]; // e.g. "italy-twenty.gl.at.ply.gg:59750"
|
||||||
|
char dns[64]; // DNS server
|
||||||
|
char allowed_ips[128]; // e.g. "0.0.0.0/0"
|
||||||
|
char server_wg_ip[18]; // e.g. "10.172.21.1"
|
||||||
|
char config_hash[64]; // Last config hash
|
||||||
|
} wg_config_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 device_state_t
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum {
|
||||||
|
STATE_BOOT,
|
||||||
|
STATE_WIFI_INIT,
|
||||||
|
STATE_WIFI_CONNECTED,
|
||||||
|
STATE_PROVISIONING,
|
||||||
|
STATE_PROVISIONED,
|
||||||
|
STATE_TUNNEL_UP,
|
||||||
|
STATE_TUNNEL_DOWN,
|
||||||
|
STATE_HEARTBEAT,
|
||||||
|
STATE_ERROR
|
||||||
|
} device_state_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Error Handling
|
||||||
|
|
||||||
|
### 6.1 Error Codes
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum {
|
||||||
|
ERR_OK = 0,
|
||||||
|
ERR_WIFI_CONNECT = -1,
|
||||||
|
ERR_HTTP_PROVISION = -2,
|
||||||
|
ERR_HTTP_HEARTBEAT = -3,
|
||||||
|
ERR_CRYPTO_DECRYPT = -4,
|
||||||
|
ERR_CONFIG_INVALID = -5,
|
||||||
|
ERR_TUNNEL_INIT = -6,
|
||||||
|
ERR_TUNNEL_APPLY = -7,
|
||||||
|
ERR_NVS_READ = -8,
|
||||||
|
ERR_NVS_WRITE = -9,
|
||||||
|
} agent_error_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Recovery Strategy
|
||||||
|
|
||||||
|
| Error | Recovery |
|
||||||
|
|-------|----------|
|
||||||
|
| WiFi connect fail | Retry 3x, then reboot |
|
||||||
|
| HTTP provision fail | Retry 3x, then reboot |
|
||||||
|
| HTTP heartbeat fail | Log only, continue |
|
||||||
|
| Crypto decrypt fail | Clear config, re-provision |
|
||||||
|
| Tunnel init fail | Retry 3x, then reboot |
|
||||||
|
| NVS read fail | Use defaults, re-provision |
|
||||||
|
|
||||||
|
## 7. Power Management
|
||||||
|
|
||||||
|
### 7.1 WiFi Sleep
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Enable WiFi sleep when tunnel is up
|
||||||
|
esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
|
||||||
|
|
||||||
|
// Disable sleep during provisioning/heartbeat
|
||||||
|
esp_wifi_set_ps(WIFI_PS_NONE);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Light Sleep
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Enter light sleep between heartbeats (30s)
|
||||||
|
esp_light_sleep_start();
|
||||||
|
|
||||||
|
// Wake on: WiFi event, GPIO interrupt, timer
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Security Considerations
|
||||||
|
|
||||||
|
### 8.1 Key Storage
|
||||||
|
|
||||||
|
- WireGuard keys stored in NVS (flash, encrypted by NVS encryption)
|
||||||
|
- Never log keys or tokens
|
||||||
|
- Clear memory after use: `memset_s(key, 0, sizeof(key))`
|
||||||
|
|
||||||
|
### 8.2 TLS
|
||||||
|
|
||||||
|
- Server communication over HTTPS (TLS 1.2+)
|
||||||
|
- Skip certificate verification for internal network (configurable)
|
||||||
|
- Future: Support custom CA certificate
|
||||||
|
|
||||||
|
### 8.3 Hardware ID
|
||||||
|
|
||||||
|
- Derived from ESP32 eFuse MAC or custom serial
|
||||||
|
- Used for key derivation (HWID + salt)
|
||||||
|
- Never transmitted in plaintext
|
||||||
|
|
||||||
|
## 9. Testing Strategy
|
||||||
|
|
||||||
|
### 9.1 Unit Tests
|
||||||
|
|
||||||
|
- Crypto: Decrypt known ciphertext with known key
|
||||||
|
- Config: NVS read/write cycle
|
||||||
|
- Heartbeat: Parse server response
|
||||||
|
|
||||||
|
### 9.2 Integration Tests
|
||||||
|
|
||||||
|
- Provision → Tunnel → Heartbeat cycle
|
||||||
|
- Config change detection
|
||||||
|
- Tunnel rebuild on config change
|
||||||
|
|
||||||
|
### 9.3 Hardware Tests
|
||||||
|
|
||||||
|
- ESP32-S3 DevKit flashing
|
||||||
|
- WiFi connection stability
|
||||||
|
- WireGuard handshake verification
|
||||||
|
- Power consumption measurement
|
||||||
|
|
||||||
|
## 10. Future Enhancements
|
||||||
|
|
||||||
|
### 10.1 Phase 2
|
||||||
|
|
||||||
|
- OTA firmware updates
|
||||||
|
- Custom CA certificate support
|
||||||
|
- Power optimization (deep sleep)
|
||||||
|
- Multiple server support
|
||||||
|
|
||||||
|
### 10.2 Phase 3
|
||||||
|
|
||||||
|
- Zephyr RTOS port
|
||||||
|
- ESP32-S2 support
|
||||||
|
- Ethernet (SPI) support
|
||||||
|
- Bluetooth provisioning
|
||||||
|
|
||||||
|
## 11. References
|
||||||
|
|
||||||
|
- [ESP-IDF Documentation](https://docs.espressif.com/projects/esp-idf/)
|
||||||
|
- [WireGuard Protocol](https://www.wireguard.com/protocol/)
|
||||||
|
- [lwIP Documentation](https://www.nongnu.org/lwip/)
|
||||||
|
- [mbedtls Documentation](https://mbed-tls.readthedocs.io/)
|
||||||
|
- [NexusGuard Server API](../../server-core/docs/)
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
# Hardware Reference
|
||||||
|
|
||||||
|
**Version**: 1.0.0
|
||||||
|
**Date**: 2026-06-26
|
||||||
|
|
||||||
|
## 1. Target Hardware
|
||||||
|
|
||||||
|
### 1.1 LW840X Module
|
||||||
|
|
||||||
|
The LW840X is an ESP32-based WiFi module designed for IoT applications.
|
||||||
|
|
||||||
|
| Specification | Value |
|
||||||
|
|---------------|-------|
|
||||||
|
| MCU | ESP32 (Xtensa LX7 dual-core) |
|
||||||
|
| Clock | 240 MHz |
|
||||||
|
| RAM | 520 KB SRAM |
|
||||||
|
| Flash | 4 MB (external) |
|
||||||
|
| WiFi | 802.11 b/g/n (2.4 GHz) |
|
||||||
|
| Bluetooth | None (LW840X) |
|
||||||
|
| GPIO | 20+ (depends on module) |
|
||||||
|
| ADC | 18 channels (12-bit) |
|
||||||
|
| Operating Temp | -40°C to +85°C |
|
||||||
|
| Voltage | 3.0V to 3.6V |
|
||||||
|
|
||||||
|
### 1.2 Pin Mapping
|
||||||
|
|
||||||
|
```
|
||||||
|
LW840X Module Pinout
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ 3V3 ─┐ │
|
||||||
|
│ GND ─┤ │
|
||||||
|
│ EN ─┤ │
|
||||||
|
│ IO0 ─┤ Boot/Flash mode │
|
||||||
|
│ IO1 ─┤ TX0 (UART0) │
|
||||||
|
│ IO3 ─┤ RX0 (UART0) │
|
||||||
|
│ IO4 ─┤ Status LED │
|
||||||
|
│ IO5 ─┤ (reserved) │
|
||||||
|
│ IO12 ─┤ (reserved) │
|
||||||
|
│ IO13 ─┤ (reserved) │
|
||||||
|
│ IO14 ─┤ (reserved) │
|
||||||
|
│ IO15 ─┤ (reserved) │
|
||||||
|
│ IO16 ─┤ (reserved) │
|
||||||
|
│ IO17 ─┤ (reserved) │
|
||||||
|
│ IO18 ─┤ (reserved) │
|
||||||
|
│ IO19 ─┤ (reserved) │
|
||||||
|
│ IO21 ─┤ (reserved) │
|
||||||
|
│ IO22 ─┤ (reserved) │
|
||||||
|
│ IO23 ─┤ (reserved) │
|
||||||
|
│ IO25 ─┤ (reserved) │
|
||||||
|
│ IO26 ─┤ (reserved) │
|
||||||
|
│ IO27 ─┤ (reserved) │
|
||||||
|
│ IO32 ─┤ (reserved) │
|
||||||
|
│ IO33 ─┤ (reserved) │
|
||||||
|
│ IO34 ─┤ (reserved) │
|
||||||
|
│ IO35 ─┤ (reserved) │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 Minimal Circuit
|
||||||
|
|
||||||
|
```
|
||||||
|
Power Supply:
|
||||||
|
3.3V ──── LW840X 3V3
|
||||||
|
GND ──── LW840X GND
|
||||||
|
|
||||||
|
Decoupling:
|
||||||
|
100nF ceramic capacitor between 3V3 and GND (close to module)
|
||||||
|
|
||||||
|
Boot/Flash:
|
||||||
|
IO0 ──── 10K pull-up to 3V3 (normal boot)
|
||||||
|
IO0 ──── GND (flash mode)
|
||||||
|
|
||||||
|
Status LED:
|
||||||
|
IO4 ──── 220Ω ──── LED ──── GND
|
||||||
|
|
||||||
|
UART (for debugging):
|
||||||
|
IO1 (TX0) ──── USB-UART RX
|
||||||
|
IO3 (RX0) ──── USB-UART TX
|
||||||
|
GND ──── USB-UART GND
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Power Requirements
|
||||||
|
|
||||||
|
### 2.1 Current Consumption
|
||||||
|
|
||||||
|
| Mode | Current | Notes |
|
||||||
|
|------|---------|-------|
|
||||||
|
| Active (WiFi TX) | 130-170 mA | Transmitting data |
|
||||||
|
| Active (WiFi RX) | 80-100 mA | Receiving data |
|
||||||
|
| Modem Sleep | 15-20 mA | WiFi connected, low duty cycle |
|
||||||
|
| Light Sleep | 0.8-1.5 mA | CPU paused, WiFi wake |
|
||||||
|
| Deep Sleep | 5-10 µA | RTC only, wake on GPIO |
|
||||||
|
| Off | 0 µA | No power |
|
||||||
|
|
||||||
|
### 2.2 Power Supply Design
|
||||||
|
|
||||||
|
```
|
||||||
|
Recommended:
|
||||||
|
Input: 5V USB or 12V DC
|
||||||
|
Regulator: AMS1117-3.3 or similar LDO
|
||||||
|
Capacity: 500mA minimum
|
||||||
|
|
||||||
|
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||||
|
│ 5V USB │────▶│ LDO │────▶│ LW840X │
|
||||||
|
│ │ │ 3.3V │ │ │
|
||||||
|
└─────────┘ └─────────┘ └─────────┘
|
||||||
|
│
|
||||||
|
GND
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Battery Operation
|
||||||
|
|
||||||
|
For battery-powered applications:
|
||||||
|
|
||||||
|
| Battery | Capacity | Runtime (Active) | Runtime (Sleep) |
|
||||||
|
|---------|----------|------------------|-----------------|
|
||||||
|
| CR2032 | 225 mAh | ~1.5 hours | ~2 years |
|
||||||
|
| 18650 | 3400 mAh | ~20 hours | ~30 years |
|
||||||
|
| LiPo 1000mAh | 1000 mAh | ~6 hours | ~10 years |
|
||||||
|
|
||||||
|
**Note**: Deep sleep with periodic wake (e.g., every 5 minutes) is recommended for battery operation.
|
||||||
|
|
||||||
|
## 3. Antenna Options
|
||||||
|
|
||||||
|
### 3.1 PCB Trace Antenna
|
||||||
|
|
||||||
|
- **Pros**: Low cost, compact
|
||||||
|
- **Cons**: Lower gain, sensitive to placement
|
||||||
|
- **Range**: 10-30m (indoor)
|
||||||
|
- **Use Case**: Short-range, cost-sensitive
|
||||||
|
|
||||||
|
### 3.2 U.FL Connector
|
||||||
|
|
||||||
|
- **Pros**: Higher gain, flexible placement
|
||||||
|
- **Cons**: Additional cost, larger size
|
||||||
|
- **Range**: 50-100m (indoor)
|
||||||
|
- **Use Case**: Longer range, industrial
|
||||||
|
|
||||||
|
### 3.3 External Antenna
|
||||||
|
|
||||||
|
- **Pros**: Best performance, directional options
|
||||||
|
- **Cons**: Highest cost, largest size
|
||||||
|
- **Range**: 100m+ (outdoor)
|
||||||
|
- **Use Case**: Outdoor, long-range
|
||||||
|
|
||||||
|
## 4. Development Board
|
||||||
|
|
||||||
|
### 4.1 ESP32-DevKitC
|
||||||
|
|
||||||
|
Recommended for development and prototyping.
|
||||||
|
|
||||||
|
| Feature | Specification |
|
||||||
|
|---------|---------------|
|
||||||
|
| MCU | ESP32-WROOM-32 |
|
||||||
|
| Flash | 4 MB |
|
||||||
|
| RAM | 520 KB SRAM |
|
||||||
|
| WiFi | 802.11 b/g/n |
|
||||||
|
| Bluetooth | BT 4.2 + BLE |
|
||||||
|
| USB | Micro-USB (CP2102) |
|
||||||
|
| GPIO | 38 pins |
|
||||||
|
| Price | ~$5-10 |
|
||||||
|
|
||||||
|
### 4.2 Flashing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install ESP-IDF
|
||||||
|
# https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/
|
||||||
|
|
||||||
|
# Set target
|
||||||
|
idf.py set-target esp32
|
||||||
|
|
||||||
|
# Build
|
||||||
|
idf.py build
|
||||||
|
|
||||||
|
# Flash (auto-detect port)
|
||||||
|
idf.py -p /dev/ttyUSB0 flash
|
||||||
|
|
||||||
|
# Monitor
|
||||||
|
idf.py -p /dev/ttyUSB0 monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Pin Connections (DevKit)
|
||||||
|
|
||||||
|
```
|
||||||
|
Status LED:
|
||||||
|
GPIO4 ──── 220Ω ──── LED ──── GND
|
||||||
|
|
||||||
|
Debug UART (optional):
|
||||||
|
GPIO1 (TX0) ──── USB-UART RX
|
||||||
|
GPIO3 (RX0) ──── USB-UART TX
|
||||||
|
GND ──── USB-UART GND
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Production Board
|
||||||
|
|
||||||
|
### 5.1 Design Guidelines
|
||||||
|
|
||||||
|
1. **Power**:
|
||||||
|
- Use 3.3V LDO with 500mA capacity
|
||||||
|
- Add 100nF ceramic capacitor close to 3V3 pin
|
||||||
|
- Add 10µF tantalum capacitor for bulk decoupling
|
||||||
|
|
||||||
|
2. **Antenna**:
|
||||||
|
- Keep antenna area clear of copper pours
|
||||||
|
- Minimum 10mm clearance around antenna
|
||||||
|
- Use U.FL connector for external antenna
|
||||||
|
|
||||||
|
3. **Layout**:
|
||||||
|
- Route UART traces away from antenna
|
||||||
|
- Keep crystal traces short
|
||||||
|
- Ground plane under module
|
||||||
|
|
||||||
|
4. **Enclosure**:
|
||||||
|
- Use non-metallic enclosure (plastic)
|
||||||
|
- Ensure antenna is not shielded
|
||||||
|
- Provide mounting holes
|
||||||
|
|
||||||
|
### 5.2 Bill of Materials (BOM)
|
||||||
|
|
||||||
|
| Component | Quantity | Package | Notes |
|
||||||
|
|-----------|----------|---------|-------|
|
||||||
|
| LW840X | 1 | Module | ESP32-based |
|
||||||
|
| AMS1117-3.3 | 1 | SOT-223 | 3.3V LDO |
|
||||||
|
| 100nF | 2 | 0402 | Decoupling |
|
||||||
|
| 10µF | 1 | 0805 | Bulk cap |
|
||||||
|
| 220Ω | 1 | 0402 | LED resistor |
|
||||||
|
| LED | 1 | 0603 | Status indicator |
|
||||||
|
| U.FL | 1 | SMD | Antenna connector |
|
||||||
|
| Header | 1 | 2.54mm | Debug UART |
|
||||||
|
|
||||||
|
### 5.3 Schematic
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ LW840X │
|
||||||
|
3.3V ────────────┤ 3V3 GND ├──── GND
|
||||||
|
│ │
|
||||||
|
IO4 ──── 220Ω ───┤ IO4 TX0 ├──── UART RX
|
||||||
|
└── LED ──┤ RX0 ├──── UART TX
|
||||||
|
GND │ │
|
||||||
|
│ EN ├──── 10K ──── 3.3V
|
||||||
|
│ │
|
||||||
|
│ IO0 ├──── 10K ──── 3.3V
|
||||||
|
│ │ (flash: GND)
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Testing & Validation
|
||||||
|
|
||||||
|
### 6.1 Hardware Tests
|
||||||
|
|
||||||
|
1. **Power-on Test**:
|
||||||
|
- Verify 3.3V at module pin
|
||||||
|
- Check current consumption (~50mA idle)
|
||||||
|
- Confirm LED blinks on boot
|
||||||
|
|
||||||
|
2. **WiFi Test**:
|
||||||
|
- Scan for WiFi networks
|
||||||
|
- Connect to test AP
|
||||||
|
- Measure RSSI at distance
|
||||||
|
|
||||||
|
3. **UART Test**:
|
||||||
|
- Send AT commands (if firmware supports)
|
||||||
|
- Verify debug output
|
||||||
|
- Check baud rate (115200)
|
||||||
|
|
||||||
|
4. **Flash Test**:
|
||||||
|
- Write/read NVS data
|
||||||
|
- Verify flash size
|
||||||
|
- Test wear leveling
|
||||||
|
|
||||||
|
### 6.2 Production Test
|
||||||
|
|
||||||
|
1. **Functional Test**:
|
||||||
|
- Provision with test server
|
||||||
|
- Verify WireGuard handshake
|
||||||
|
- Check heartbeat response
|
||||||
|
|
||||||
|
2. **Stress Test**:
|
||||||
|
- Run for 24+ hours
|
||||||
|
- Monitor memory leaks
|
||||||
|
- Verify reconnection after WiFi drop
|
||||||
|
|
||||||
|
3. **Environmental Test**:
|
||||||
|
- Operating temperature range
|
||||||
|
- Humidity resistance
|
||||||
|
- Vibration resistance
|
||||||
|
|
||||||
|
## 7. Troubleshooting
|
||||||
|
|
||||||
|
### 7.1 Common Issues
|
||||||
|
|
||||||
|
| Symptom | Cause | Solution |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| No boot | IO0 held low | Remove flash jumper |
|
||||||
|
| No WiFi | Antenna issue | Check antenna connection |
|
||||||
|
| High current | WiFi always on | Use modem sleep |
|
||||||
|
| Crash on boot | Stack overflow | Increase task stack size |
|
||||||
|
| Provision fail | Token expired | Get new token from server |
|
||||||
|
|
||||||
|
### 7.2 Debug Tools
|
||||||
|
|
||||||
|
- **Serial Monitor**: `idf.py monitor`
|
||||||
|
- **JTAG Debugger**: OpenOCD + GDB
|
||||||
|
- **Logic Analyzer**: Saleae or similar
|
||||||
|
- **Power Analyzer**: Monsoon or similar
|
||||||
|
|
||||||
|
## 8. Compliance
|
||||||
|
|
||||||
|
### 8.1 Certifications
|
||||||
|
|
||||||
|
- **FCC**: Required for US market
|
||||||
|
- **CE**: Required for EU market
|
||||||
|
- **IC**: Required for Canada market
|
||||||
|
- **TELEC**: Required for Japan market
|
||||||
|
|
||||||
|
### 8.2 Testing
|
||||||
|
|
||||||
|
- EMC testing (emissions and immunity)
|
||||||
|
- SAR testing (if applicable)
|
||||||
|
- Environmental testing (temperature, humidity)
|
||||||
|
|
||||||
|
## 9. References
|
||||||
|
|
||||||
|
- [ESP32 Hardware Design Guidelines](https://www.espressif.com/sites/default/files/documentation/esp32_hardware_design_guidelines_en.pdf)
|
||||||
|
- [ESP32 Datasheet](https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf)
|
||||||
|
- [LW840X Module Datasheet](link-to-datasheet)
|
||||||
|
- [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/)
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# iOS Agent — Architecture Reference
|
||||||
|
|
||||||
|
**Status**: ARCHITECTURE ONLY (no code)
|
||||||
|
**Location**: `reference/ios-agent/` (NOT a submodule)
|
||||||
|
**Mirror**: Android Agent (`apps/android-agent/`)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This directory contains the architecture design for an iOS WireGuard agent that mirrors the Android agent's architecture, UI design, colors, and heartbeat flow.
|
||||||
|
|
||||||
|
## Why `reference/` not `apps/`?
|
||||||
|
|
||||||
|
If iOS agent is implemented in the future, it will be added as `apps/ios-agent/`. This `reference/` directory stores the architecture docs separately to avoid conflicts.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
- Document the iOS agent architecture (mirrors Android)
|
||||||
|
- Define UI design specs (colors, layout, components)
|
||||||
|
- API contract (shared with server-core)
|
||||||
|
- Track implementation status and TODOs
|
||||||
|
|
||||||
|
## Architecture Mirror: Android → iOS
|
||||||
|
|
||||||
|
| Android Component | iOS Equivalent | Notes |
|
||||||
|
|-------------------|----------------|-------|
|
||||||
|
| `AgentService.kt` | `PacketTunnelProvider` | Foreground service → NetworkExtension |
|
||||||
|
| `TunnelManager.kt` | `WireGuardAdapter` | VPNService → NEPacketTunnelProvider |
|
||||||
|
| `Heartbeat.kt` | `HeartbeatService` | HTTP loop (same) |
|
||||||
|
| `Provisioning.kt` | `ProvisioningService` | HTTP provisioning (same) |
|
||||||
|
| `ConfigStorage.kt` | `KeychainStorage` | SharedPreferences → Keychain |
|
||||||
|
| `Encryptor.kt` | `CryptoManager` | JCE → CryptoKit |
|
||||||
|
| `MainActivity.kt` | `ContentView` | Activity → SwiftUI |
|
||||||
|
| `SettingsActivity.kt` | `SettingsView` | Activity → SwiftUI |
|
||||||
|
| `BootReceiver.kt` | `BGAppRefreshTask` | Boot broadcast → Background Tasks |
|
||||||
|
| `LogBuffer.kt` | `LogStore` | In-memory → CoreData |
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `docs/DESIGN.md` | Technical design + UI specs |
|
||||||
|
| `docs/ARCHITECTURE.md` | Module architecture |
|
||||||
|
| `docs/API_COMPAT.md` | Server API contract |
|
||||||
|
| `docs/UI_DESIGN.md` | UI design specs (colors, layout) |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
When implementing:
|
||||||
|
|
||||||
|
1. Create Xcode project: `ios-agent`
|
||||||
|
2. Add NetworkExtension capability
|
||||||
|
3. Use `wireguard-apple` library for WireGuard
|
||||||
|
4. Follow architecture in `docs/ARCHITECTURE.md`
|
||||||
|
5. Match UI design in `docs/UI_DESIGN.md`
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Proprietary - NexusGuard
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
# iOS Agent — API Compatibility Reference
|
||||||
|
|
||||||
|
**Status**: API CONTRACT
|
||||||
|
**Mirror**: Android Agent (`apps/android-agent/`)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The iOS agent communicates with the server via two HTTP endpoints:
|
||||||
|
|
||||||
|
1. **Provisioning**: `POST /api/v1/provision` — Initial key exchange
|
||||||
|
2. **Heartbeat**: `POST /api/v1/heartbeat` — Periodic status + config sync
|
||||||
|
|
||||||
|
## 1. Provisioning
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/provision
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "string (required)",
|
||||||
|
"hwid": "string (required)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `token` | string | Yes | Registration token (single-use) |
|
||||||
|
| `hwid` | string | Yes | Hardware ID (iOS identifierForVendor) |
|
||||||
|
|
||||||
|
### Response (Success)
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"encrypted_config": "base64-encoded-bytes"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `encrypted_config` | string | AES-256-GCM encrypted ConfigPayload |
|
||||||
|
|
||||||
|
### Response (Error)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Status | Error | Description |
|
||||||
|
|--------|-------|-------------|
|
||||||
|
| 400 | `"invalid token format"` | Token is not valid UUID |
|
||||||
|
| 400 | `"token already used"` | Token was already consumed |
|
||||||
|
| 400 | `"invalid hardware id"` | HWID is empty or invalid |
|
||||||
|
| 403 | `"token expired"` | Token has expired (24h TTL) |
|
||||||
|
| 500 | `"provisioning failed"` | Server internal error |
|
||||||
|
|
||||||
|
### Decrypted ConfigPayload
|
||||||
|
|
||||||
|
After decrypting `encrypted_config`, the agent receives:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"device_id": "string (UUID)",
|
||||||
|
"private_key": "string (hex, 64 chars)",
|
||||||
|
"preshared_key": "string (hex, 64 chars)",
|
||||||
|
"internal_ip": "string (e.g. 10.172.21.2)",
|
||||||
|
"server_pub": "string (hex, 64 chars)",
|
||||||
|
"endpoint": "string (e.g. italy-twenty.gl.at.ply.gg:59750)",
|
||||||
|
"dns": "string (e.g. 1.1.1.1)",
|
||||||
|
"allowed_ips": "string (e.g. 0.0.0.0/0)",
|
||||||
|
"server_wg_ip": "string (e.g. 10.172.21.1)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `device_id` | string | Device UUID (use in heartbeat) |
|
||||||
|
| `private_key` | string | WireGuard private key (hex) |
|
||||||
|
| `preshared_key` | string | Pre-shared key (hex) |
|
||||||
|
| `internal_ip` | string | Device IP address |
|
||||||
|
| `server_pub` | string | Server WireGuard public key (hex) |
|
||||||
|
| `endpoint` | string | Server WireGuard endpoint |
|
||||||
|
| `dns` | string | DNS server IP |
|
||||||
|
| `allowed_ips` | string | Allowed IPs (e.g. "0.0.0.0/0" for full tunnel) |
|
||||||
|
| `server_wg_ip` | string | Server WireGuard IP |
|
||||||
|
|
||||||
|
## 2. Heartbeat
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/heartbeat
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"device_id": "string (required)",
|
||||||
|
"status": "string (optional)",
|
||||||
|
"state": "string (optional)",
|
||||||
|
"tunnel_up": "boolean (optional)",
|
||||||
|
"last_handshake": "string (optional, ISO 8601)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `device_id` | string | Yes | Device UUID |
|
||||||
|
| `status` | string | No | "connected", "recovering", "stopped" |
|
||||||
|
| `state` | string | No | Free-form state description |
|
||||||
|
| `tunnel_up` | boolean | No | WireGuard tunnel status |
|
||||||
|
| `last_handshake` | string | No | ISO 8601 timestamp of last handshake |
|
||||||
|
|
||||||
|
### Response (Success)
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"device_id": "string",
|
||||||
|
"status": "ok",
|
||||||
|
"internal_ip": "string",
|
||||||
|
"private_key": "string (hex)",
|
||||||
|
"preshared_key": "string (hex)",
|
||||||
|
"server_pub": "string (hex)",
|
||||||
|
"endpoint": "string",
|
||||||
|
"server_wg_ip": "string",
|
||||||
|
"dns": "string",
|
||||||
|
"allowed_ips": "string",
|
||||||
|
"forwards_hash": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `device_id` | string | Device UUID |
|
||||||
|
| `status` | string | "ok" or error |
|
||||||
|
| `internal_ip` | string | Device IP (may change) |
|
||||||
|
| `private_key` | string | WireGuard private key (may rotate) |
|
||||||
|
| `preshared_key` | string | Pre-shared key (may rotate) |
|
||||||
|
| `server_pub` | string | Server public key |
|
||||||
|
| `endpoint` | string | Server endpoint |
|
||||||
|
| `server_wg_ip` | string | Server WireGuard IP |
|
||||||
|
| `dns` | string | DNS server |
|
||||||
|
| `allowed_ips` | string | Allowed IPs |
|
||||||
|
| `forwards_hash` | string | Hash of port forwards (for change detection) |
|
||||||
|
|
||||||
|
### Response (Error)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Status | Error | Description |
|
||||||
|
|--------|-------|-------------|
|
||||||
|
| 400 | `"invalid device id"` | Device ID is not valid UUID |
|
||||||
|
| 403 | `"device is suspended"` | Device is suspended by admin |
|
||||||
|
| 500 | `"heartbeat failed"` | Server internal error |
|
||||||
|
|
||||||
|
## 3. Crypto Protocol
|
||||||
|
|
||||||
|
### Key Derivation
|
||||||
|
|
||||||
|
```
|
||||||
|
key = SHA256(hwid + salt)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `hwid`: Hardware ID (iOS `identifierForVendor`)
|
||||||
|
- `salt`: Server's `SERVER_SALT` environment variable
|
||||||
|
- Output: 32-byte AES key
|
||||||
|
|
||||||
|
### Encryption (Server Side)
|
||||||
|
|
||||||
|
```
|
||||||
|
plaintext = ConfigPayload JSON
|
||||||
|
nonce = random 12 bytes
|
||||||
|
ciphertext = AES-256-GCM-Encrypt(key, nonce, plaintext)
|
||||||
|
encrypted_config = nonce + ciphertext
|
||||||
|
```
|
||||||
|
|
||||||
|
### Decryption (Agent Side)
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let combined = Data(base64Encoded: encryptedConfig)!
|
||||||
|
let nonce = combined.prefix(12)
|
||||||
|
let ciphertext = combined.dropFirst(12)
|
||||||
|
|
||||||
|
let sealedBox = try AES.GCM.SealedBox(nonce: nonce, ciphertext: ciphertext)
|
||||||
|
let plaintext = try AES.GCM.open(sealedBox, using: key)
|
||||||
|
let json = try JSONSerialization.jsonObject(with: plaintext)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation Notes
|
||||||
|
|
||||||
|
- Use `CryptoKit` for AES-256-GCM
|
||||||
|
- Use `CryptoKit` for SHA-256 key derivation
|
||||||
|
- Nonce size: 12 bytes (fixed for GCM)
|
||||||
|
- Tag size: 16 bytes (default for GCM)
|
||||||
|
|
||||||
|
## 4. Config Change Detection
|
||||||
|
|
||||||
|
### Heartbeat Hash Comparison
|
||||||
|
|
||||||
|
The server returns `forwards_hash` in heartbeat response. Agent should:
|
||||||
|
|
||||||
|
1. Store last received `forwards_hash` in UserDefaults
|
||||||
|
2. Compare with current hash
|
||||||
|
3. If different, trigger config reload
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let lastHash = UserDefaults.standard.string(forKey: "config_hash") ?? ""
|
||||||
|
|
||||||
|
if lastHash != response.forwardsHash {
|
||||||
|
// Config changed, rebuild tunnel
|
||||||
|
tunnelManager.rebuildTunnel(with: newConfig)
|
||||||
|
UserDefaults.standard.set(response.forwardsHash, forKey: "config_hash")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Hardware ID
|
||||||
|
|
||||||
|
### iOS identifierForVendor
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
let hwid = UIDevice.current.identifierForVendor?.uuidString ?? "unknown"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alternative: Custom Serial
|
||||||
|
|
||||||
|
```swift
|
||||||
|
func getHardwareId() -> String {
|
||||||
|
// Use Keychain to persist custom HWID
|
||||||
|
if let savedHwid = KeychainManager.get(key: "hwid") {
|
||||||
|
return savedHwid
|
||||||
|
}
|
||||||
|
|
||||||
|
let newHwid = UUID().uuidString
|
||||||
|
KeychainManager.set(key: "hwid", value: newHwid)
|
||||||
|
return newHwid
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Versioning
|
||||||
|
|
||||||
|
### Current Version
|
||||||
|
|
||||||
|
- API Version: v1 (implicit, no version in URL)
|
||||||
|
- Agent Version: 1.0.0
|
||||||
|
|
||||||
|
### Compatibility Policy
|
||||||
|
|
||||||
|
- **Breaking Changes**: Major version bump (v2)
|
||||||
|
- **New Fields**: Added without version bump (agent ignores unknown fields)
|
||||||
|
- **Deprecation**: 6-month notice before removal
|
||||||
|
|
||||||
|
### Agent Behavior
|
||||||
|
|
||||||
|
- Agent ignores unknown fields in server response
|
||||||
|
- Agent sends only required fields in request
|
||||||
|
- Agent handles missing optional fields gracefully
|
||||||
|
|
||||||
|
## 7. Examples
|
||||||
|
|
||||||
|
### Provisioning Flow
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// 1. Get hardware ID
|
||||||
|
let hwid = UIDevice.current.identifierForVendor?.uuidString ?? "unknown"
|
||||||
|
|
||||||
|
// 2. Send provisioning request
|
||||||
|
let body = ["token": token, "hwid": hwid]
|
||||||
|
let request = URLRequest(url: URL(string: "\(serverUrl)/api/v1/provision")!)
|
||||||
|
|
||||||
|
// 3. Parse encrypted config
|
||||||
|
let encrypted = try JSONDecoder().decode([String: String].self, from: data)["encrypted_config"]!
|
||||||
|
|
||||||
|
// 4. Decrypt config
|
||||||
|
let config = try CryptoManager.decryptConfig(encrypted: encrypted, hwid: hwid, salt: salt)
|
||||||
|
|
||||||
|
// 5. Store in Keychain
|
||||||
|
KeychainStorage.save(config)
|
||||||
|
|
||||||
|
// 6. Apply to WireGuard
|
||||||
|
try await tunnelManager.startTunnel(config: config)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Heartbeat Flow
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// 1. Get tunnel status
|
||||||
|
let tunnelUp = tunnelManager.isTunnelUp()
|
||||||
|
let lastHandshake = tunnelManager.getLastHandshake()
|
||||||
|
|
||||||
|
// 2. Send heartbeat
|
||||||
|
let body: [String: Any] = [
|
||||||
|
"device_id": config.deviceId,
|
||||||
|
"tunnel_up": tunnelUp,
|
||||||
|
"last_handshake": lastHandshake?.iso8601String ?? "1970-01-01T00:00:00Z"
|
||||||
|
]
|
||||||
|
|
||||||
|
// 3. Check for config changes
|
||||||
|
let response = try JSONDecoder().decode(HeartbeatResponse.self, from: data)
|
||||||
|
|
||||||
|
if config.configHash != response.forwardsHash {
|
||||||
|
// Config changed, reload
|
||||||
|
let newConfig = try parseConfig(from: response)
|
||||||
|
tunnelManager.rebuildTunnel(with: newConfig)
|
||||||
|
KeychainStorage.save(newConfig)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Error Handling
|
||||||
|
|
||||||
|
### HTTP Retries
|
||||||
|
|
||||||
|
```swift
|
||||||
|
func sendRequest(url: URL, body: Data, maxRetries: Int = 3) async throws -> Data {
|
||||||
|
for attempt in 1...maxRetries {
|
||||||
|
do {
|
||||||
|
let (data, response) = try await URLSession.shared.data(for: request)
|
||||||
|
if let httpResponse = response as? HTTPURLResponse,
|
||||||
|
httpResponse.statusCode == 200 {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if attempt == maxRetries {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
try await Task.sleep(nanoseconds: UInt64(attempt * 1_000_000_000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw AgentError.heartbeatFailed("Max retries exceeded")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timeout Settings
|
||||||
|
|
||||||
|
- Connect timeout: 10 seconds
|
||||||
|
- Read timeout: 30 seconds
|
||||||
|
- Write timeout: 10 seconds
|
||||||
|
|
||||||
|
## 9. Security Notes
|
||||||
|
|
||||||
|
- All communication over HTTPS (TLS 1.2+)
|
||||||
|
- Registration token is single-use (consumed on first provisioning)
|
||||||
|
- Device can re-provision by obtaining new token
|
||||||
|
- Keys never logged or transmitted in plaintext
|
||||||
|
- WireGuard keys stored in iOS Keychain (hardware-backed)
|
||||||
@@ -0,0 +1,474 @@
|
|||||||
|
# iOS Agent — Module Architecture
|
||||||
|
|
||||||
|
**Status**: ARCHITECTURE ONLY
|
||||||
|
**Mirror**: Android Agent (`apps/android-agent/`)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document defines the module architecture for the iOS WireGuard agent. The agent uses NetworkExtension (NEPacketTunnelProvider) for tunnel management and mirrors the Android agent's architecture.
|
||||||
|
|
||||||
|
## System Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ iOS (SwiftUI) │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ App Layer │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ContentView│ │SettingsView│ │LogView │ │PortView │ │
|
||||||
|
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │
|
||||||
|
│ │ AgentManager │ │
|
||||||
|
│ │ (orchestrates all components) │ │
|
||||||
|
│ └──────────────────────────────────────────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Service Layer │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │Provisioning│ │Heartbeat│ │TunnelMgr │ │ConfigMgr│ │
|
||||||
|
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │
|
||||||
|
│ │ CryptoManager │ │
|
||||||
|
│ │ AES-256-GCM (CryptoKit) │ │
|
||||||
|
│ └──────────────────────────────────────────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ NetworkExtension Layer │
|
||||||
|
│ ┌──────────────────────────────────────────────────┐ │
|
||||||
|
│ │ PacketTunnelProvider │ │
|
||||||
|
│ │ (WireGuard tunnel via wireguard-apple) │ │
|
||||||
|
│ └──────────────────────────────────────────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Storage Layer │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ Keychain │ │ UserDefaults│ │ CoreData│ │ FileMgr │ │
|
||||||
|
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
iOS Threads/Tasks
|
||||||
|
├── Main Thread # UI updates
|
||||||
|
├── AgentManager # Orchestration (async/await)
|
||||||
|
├── HeartbeatTask # HTTP heartbeat (30s interval)
|
||||||
|
├── TunnelManager # WireGuard tunnel management
|
||||||
|
└── BackgroundTask # BGAppRefreshTask (background refresh)
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Machine
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐
|
||||||
|
│ BOOT │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────┐
|
||||||
|
│ APP_INIT │ ◄──────────────────────────────┐
|
||||||
|
└──────┬──────┘ │
|
||||||
|
│ loaded │
|
||||||
|
▼ │
|
||||||
|
┌─────────────┐ fail ┌─────────────┐│
|
||||||
|
│ PROVISION │───────────────►│ APP_RETRY ││
|
||||||
|
└──────┬──────┘ └──────┬──────┘│
|
||||||
|
│ success │ │
|
||||||
|
▼ └───────┘
|
||||||
|
┌─────────────┐
|
||||||
|
│ TUNNEL_UP │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────┐
|
||||||
|
│ HEARTBEAT │ ◄─── 30s interval
|
||||||
|
└──────┬──────┘
|
||||||
|
│ config_changed
|
||||||
|
▼
|
||||||
|
┌─────────────┐
|
||||||
|
│ TUNNEL_REBUILD │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
└──► HEARTBEAT
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module Design
|
||||||
|
|
||||||
|
### 1. AgentManager
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Orchestrate all agent components
|
||||||
|
- Manage agent lifecycle (start/stop)
|
||||||
|
- Handle state transitions
|
||||||
|
|
||||||
|
**Key Properties**:
|
||||||
|
```swift
|
||||||
|
class AgentManager: ObservableObject {
|
||||||
|
@Published var state: AgentState = .disconnected
|
||||||
|
@Published var tunnelInfo: TunnelInfo?
|
||||||
|
@Published var logs: [LogEntry] = []
|
||||||
|
|
||||||
|
private let provisioning: ProvisioningService
|
||||||
|
private let heartbeat: HeartbeatService
|
||||||
|
private let tunnel: TunnelManager
|
||||||
|
private let config: KeychainStorage
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Methods**:
|
||||||
|
```swift
|
||||||
|
func startAgent() async
|
||||||
|
func stopAgent()
|
||||||
|
func rebuildTunnel(with config: WireGuardConfig) async
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. PacketTunnelProvider
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Handle NetworkExtension tunnel lifecycle
|
||||||
|
- Start/stop WireGuard tunnel
|
||||||
|
- Report tunnel status
|
||||||
|
|
||||||
|
**Key Methods**:
|
||||||
|
```swift
|
||||||
|
override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void)
|
||||||
|
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void)
|
||||||
|
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?)
|
||||||
|
```
|
||||||
|
|
||||||
|
**WireGuard Integration**:
|
||||||
|
```swift
|
||||||
|
// Uses wireguard-apple library
|
||||||
|
let adapter = WireGuardAdapter(with: self) { logLevel, message in
|
||||||
|
Logger.log(level: logLevel, message: message)
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.start(tunnelConfiguration: tunnelConfig) { error in
|
||||||
|
if let error = error {
|
||||||
|
completionHandler(error)
|
||||||
|
} else {
|
||||||
|
completionHandler(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. TunnelManager
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Build WireGuard config from stored keys
|
||||||
|
- Start/stop tunnel via PacketTunnelProvider
|
||||||
|
- Monitor tunnel state
|
||||||
|
- Track last handshake time
|
||||||
|
|
||||||
|
**Key Methods**:
|
||||||
|
```swift
|
||||||
|
func startTunnel(config: WireGuardConfig) async throws
|
||||||
|
func stopTunnel()
|
||||||
|
func isTunnelUp() -> Bool
|
||||||
|
func getLastHandshake() -> Date?
|
||||||
|
```
|
||||||
|
|
||||||
|
**Config Build**:
|
||||||
|
```swift
|
||||||
|
func buildTunnelConfig(from config: WireGuardConfig) -> TunnelConfiguration {
|
||||||
|
let interface = InterfaceConfiguration(
|
||||||
|
privateKey: config.privateKey,
|
||||||
|
addresses: [config.internalIp],
|
||||||
|
dns: [config.dns]
|
||||||
|
)
|
||||||
|
|
||||||
|
let peer = PeerConfiguration(
|
||||||
|
publicKey: config.serverPub,
|
||||||
|
allowedIPs: config.allowedIps,
|
||||||
|
endpoint: config.endpoint,
|
||||||
|
preSharedKey: config.presharedKey,
|
||||||
|
persistentKeepAlive: 25
|
||||||
|
)
|
||||||
|
|
||||||
|
return TunnelConfiguration(interface: interface, peers: [peer])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. HeartbeatService
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- HTTP POST to `/api/v1/heartbeat` every 30s
|
||||||
|
- Send device status + tunnel state
|
||||||
|
- Receive config sync (detect changes)
|
||||||
|
- Trigger tunnel rebuild if config changed
|
||||||
|
|
||||||
|
**Key Methods**:
|
||||||
|
```swift
|
||||||
|
func startHeartbeatLoop()
|
||||||
|
func stopHeartbeat()
|
||||||
|
func sendHeartbeat(config: WireGuardConfig, tunnelUp: Bool, lastHandshake: Date?) async -> WireGuardConfig?
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP Request**:
|
||||||
|
```json
|
||||||
|
POST /api/v1/heartbeat
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"device_id": "uuid",
|
||||||
|
"status": "connected",
|
||||||
|
"tunnel_up": true,
|
||||||
|
"last_handshake": "2026-06-26T09:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"device_id": "uuid",
|
||||||
|
"status": "ok",
|
||||||
|
"internal_ip": "10.172.21.2",
|
||||||
|
"private_key": "hex",
|
||||||
|
"preshared_key": "hex",
|
||||||
|
"server_pub": "hex",
|
||||||
|
"endpoint": "italy-twenty.gl.at.ply.gg:59750",
|
||||||
|
"dns": "1.1.1.1",
|
||||||
|
"allowed_ips": "0.0.0.0/0",
|
||||||
|
"server_wg_ip": "10.172.21.1",
|
||||||
|
"forwards_hash": "hash-string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. ProvisioningService
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- HTTP POST to `/api/v1/provision`
|
||||||
|
- Parse encrypted config response
|
||||||
|
- Decrypt config via CryptoManager
|
||||||
|
- Store config in Keychain
|
||||||
|
|
||||||
|
**Key Methods**:
|
||||||
|
```swift
|
||||||
|
func provision(serverUrl: String, token: String, hwid: String) async -> WireGuardConfig?
|
||||||
|
func decryptConfig(encrypted: String, hwid: String, salt: String) -> WireGuardConfig?
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP Request**:
|
||||||
|
```json
|
||||||
|
POST /api/v1/provision
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"token": "registration-token",
|
||||||
|
"hwid": "ios-hardware-id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"encrypted_config": "base64-encoded-bytes"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. CryptoManager
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Key derivation: SHA-256(hwid + salt)
|
||||||
|
- AES-256-GCM decryption
|
||||||
|
- Secure memory handling
|
||||||
|
|
||||||
|
**Key Methods**:
|
||||||
|
```swift
|
||||||
|
func deriveKey(token: String, hwid: String) -> SymmetricKey
|
||||||
|
func decrypt(encoded: String, key: SymmetricKey) -> Data?
|
||||||
|
```
|
||||||
|
|
||||||
|
**Algorithm**:
|
||||||
|
```swift
|
||||||
|
// Key Derivation
|
||||||
|
let input = "\(token)\(hwid)".data(using: .utf8)!
|
||||||
|
let hash = SHA256.hash(data: input)
|
||||||
|
let key = SymmetricKey(data: hash)
|
||||||
|
|
||||||
|
// Decryption
|
||||||
|
let combined = Data(base64Encoded: encoded)!
|
||||||
|
let nonce = combined.prefix(12)
|
||||||
|
let ciphertext = combined.dropFirst(12)
|
||||||
|
|
||||||
|
let sealedBox = try AES.GCM.SealedBox(nonce: nonce, ciphertext: ciphertext)
|
||||||
|
let plaintext = try AES.GCM.open(sealedBox, using: key)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. KeychainStorage
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Store WireGuard keys in Keychain
|
||||||
|
- Store agent config in UserDefaults
|
||||||
|
- Secure key access
|
||||||
|
|
||||||
|
**Key Methods**:
|
||||||
|
```swift
|
||||||
|
func saveConfig(_ config: AgentConfig)
|
||||||
|
func loadConfig() -> AgentConfig?
|
||||||
|
func saveWireGuardConfig(_ config: WireGuardConfig)
|
||||||
|
func loadWireGuardConfig() -> WireGuardConfig?
|
||||||
|
func clearAll()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Storage Strategy**:
|
||||||
|
- **Keychain**: WireGuard keys (private_key, preshared_key)
|
||||||
|
- **UserDefaults**: Non-sensitive config (server_url, device_id, endpoint)
|
||||||
|
- **File**: Log entries (optional)
|
||||||
|
|
||||||
|
### 8. LogStore
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Buffer log entries in memory
|
||||||
|
- Persist to CoreData (optional)
|
||||||
|
- Provide log stream for UI
|
||||||
|
|
||||||
|
**Key Methods**:
|
||||||
|
```swift
|
||||||
|
func add(level: LogLevel, tag: String, message: String)
|
||||||
|
func clear()
|
||||||
|
func getLogs() -> [LogEntry]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Log Levels**:
|
||||||
|
```swift
|
||||||
|
enum LogLevel: String {
|
||||||
|
case debug = "DEBUG"
|
||||||
|
case info = "INFO"
|
||||||
|
case warn = "WARN"
|
||||||
|
case error = "ERROR"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Structures
|
||||||
|
|
||||||
|
### WireGuardConfig
|
||||||
|
|
||||||
|
```swift
|
||||||
|
struct WireGuardConfig: Codable {
|
||||||
|
let deviceId: String
|
||||||
|
let privateKey: String
|
||||||
|
let presharedKey: String
|
||||||
|
let internalIp: String
|
||||||
|
let serverPub: String
|
||||||
|
let endpoint: String
|
||||||
|
let dns: String
|
||||||
|
let allowedIps: String
|
||||||
|
let serverWgIp: String
|
||||||
|
let configHash: String
|
||||||
|
let forwardsHash: String
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### AgentConfig
|
||||||
|
|
||||||
|
```swift
|
||||||
|
struct AgentConfig: Codable {
|
||||||
|
let serverUrl: String
|
||||||
|
let registrationToken: String
|
||||||
|
let isProvisioned: Bool
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### AgentState
|
||||||
|
|
||||||
|
```swift
|
||||||
|
enum AgentState: String {
|
||||||
|
case disconnected = "disconnected"
|
||||||
|
case connecting = "connecting"
|
||||||
|
case connected = "connected"
|
||||||
|
case failed = "failed"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### LogEntry
|
||||||
|
|
||||||
|
```swift
|
||||||
|
struct LogEntry: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
let timestamp: Date
|
||||||
|
let level: LogLevel
|
||||||
|
let tag: String
|
||||||
|
let message: String
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Error Types
|
||||||
|
|
||||||
|
```swift
|
||||||
|
enum AgentError: Error {
|
||||||
|
case wifiNotConnected
|
||||||
|
case provisioningFailed(String)
|
||||||
|
case heartbeatFailed(String)
|
||||||
|
case cryptoDecryptionFailed
|
||||||
|
case configInvalid
|
||||||
|
case tunnelInitFailed
|
||||||
|
case keychainError(OSStatus)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recovery Strategy
|
||||||
|
|
||||||
|
| Error | Recovery |
|
||||||
|
|-------|----------|
|
||||||
|
| WiFi not connected | Retry 3x, then pause heartbeat |
|
||||||
|
| HTTP provision fail | Retry 3x, then notify user |
|
||||||
|
| HTTP heartbeat fail | Log only, continue |
|
||||||
|
| Crypto decrypt fail | Clear config, re-provision |
|
||||||
|
| Tunnel init fail | Retry 3x, then notify user |
|
||||||
|
| Keychain error | Use UserDefaults fallback |
|
||||||
|
|
||||||
|
## Background Tasks
|
||||||
|
|
||||||
|
### BGAppRefreshTask
|
||||||
|
|
||||||
|
iOS doesn't have boot receiver like Android. Use Background Tasks:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// Register task
|
||||||
|
BGAppRefreshTaskRequest.register(forTaskWithIdentifier: "com.nexusguard.agent.refresh", using: nil) {
|
||||||
|
task in
|
||||||
|
self.handleBackgroundRefresh(task: task as! BGAppRefreshTask)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule task
|
||||||
|
let request = BGAppRefreshTaskRequest(identifier: "com.nexusguard.agent.refresh")
|
||||||
|
request.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60) // 30 minutes
|
||||||
|
try BGTaskScheduler.shared.submit(request)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Background Modes
|
||||||
|
|
||||||
|
Add to `Info.plist`:
|
||||||
|
```xml
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>fetch</string>
|
||||||
|
<string>processing</string>
|
||||||
|
<string>vpn-api</string>
|
||||||
|
</array>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **wireguard-apple**: WireGuard tunnel library
|
||||||
|
- **CryptoKit**: AES-256-GCM encryption
|
||||||
|
- **NetworkExtension**: VPN tunnel management
|
||||||
|
- **BackgroundTasks**: Background refresh
|
||||||
|
- **SwiftUI**: UI framework
|
||||||
|
- **Combine**: Reactive state management
|
||||||
|
|
||||||
|
## Android → iOS Mapping
|
||||||
|
|
||||||
|
| Android | iOS | Notes |
|
||||||
|
|---------|-----|-------|
|
||||||
|
| `LifecycleService` | `PacketTunnelProvider` | Background service |
|
||||||
|
| `startForeground()` | `NEPacketTunnelProvider` | Always-on tunnel |
|
||||||
|
| `SharedPreferences` | `Keychain + UserDefaults` | Config storage |
|
||||||
|
| `BroadcastReceiver` | `BGAppRefreshTask` | Background triggers |
|
||||||
|
| `lifecycleScope` | `Task {}` | Coroutine/async |
|
||||||
|
| `OkHttp` | `URLSession` | HTTP client |
|
||||||
|
| `Base64` | `Data(base64Encoded:)` | Encoding |
|
||||||
|
| `Log.d/i/w/e` | `os_log` or `print` | Logging |
|
||||||
@@ -0,0 +1,425 @@
|
|||||||
|
# iOS Agent — Technical Design Document
|
||||||
|
|
||||||
|
**Version**: 1.0.0
|
||||||
|
**Date**: 2026-07-06
|
||||||
|
**Status**: Draft
|
||||||
|
**Mirror**: Android Agent (`apps/android-agent/`)
|
||||||
|
|
||||||
|
## 1. Executive Summary
|
||||||
|
|
||||||
|
NexusGuard iOS Agent is an iOS-based WireGuard client for mobile devices. It mirrors the Android agent's architecture, UI design, and heartbeat flow, using iOS-native APIs (NetworkExtension, CryptoKit, Keychain).
|
||||||
|
|
||||||
|
## 2. Goals & Non-Goals
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
- iOS-based WireGuard tunnel (NetworkExtension)
|
||||||
|
- Auto-provisioning via HTTP (same API as Android/ESP32)
|
||||||
|
- Heartbeat monitoring with config sync (30s interval)
|
||||||
|
- Keychain-based config storage (hardware-backed encryption)
|
||||||
|
- SwiftUI UI matching Android agent design
|
||||||
|
- Background operation via NetworkExtension
|
||||||
|
|
||||||
|
### Non-Goals
|
||||||
|
- gRPC support (not needed for mobile)
|
||||||
|
- Port forwarding (userspace agent only)
|
||||||
|
- Jailbroken device support
|
||||||
|
- macOS/iPadOS support (iPhone only for v1)
|
||||||
|
|
||||||
|
## 3. Architecture
|
||||||
|
|
||||||
|
### 3.1 System Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ iOS (SwiftUI) │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ App Layer │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ContentView│ │SettingsView│ │LogView │ │PortView │ │
|
||||||
|
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │
|
||||||
|
│ │ AgentManager │ │
|
||||||
|
│ │ (orchestrates all components) │ │
|
||||||
|
│ └──────────────────────────────────────────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Service Layer │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │Provisioning│ │Heartbeat│ │TunnelMgr │ │ConfigMgr│ │
|
||||||
|
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │
|
||||||
|
│ │ CryptoManager │ │
|
||||||
|
│ │ AES-256-GCM (CryptoKit) │ │
|
||||||
|
│ └──────────────────────────────────────────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ NetworkExtension Layer │
|
||||||
|
│ ┌──────────────────────────────────────────────────┐ │
|
||||||
|
│ │ PacketTunnelProvider │ │
|
||||||
|
│ │ (WireGuard tunnel via wireguard-apple) │ │
|
||||||
|
│ └──────────────────────────────────────────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Storage Layer │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ Keychain │ │ UserDefaults│ │ CoreData│ │ FileMgr │ │
|
||||||
|
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Task Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
iOS Threads/Tasks
|
||||||
|
├── Main Thread # UI updates
|
||||||
|
├── AgentManager # Orchestration (async/await)
|
||||||
|
├── HeartbeatTask # HTTP heartbeat (30s interval)
|
||||||
|
├── TunnelManager # WireGuard tunnel management
|
||||||
|
└── BackgroundTask # BGAppRefreshTask (background refresh)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 State Machine
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐
|
||||||
|
│ BOOT │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────┐
|
||||||
|
│ APP_INIT │ ◄──────────────────────────────┐
|
||||||
|
└──────┬──────┘ │
|
||||||
|
│ loaded │
|
||||||
|
▼ │
|
||||||
|
┌─────────────┐ fail ┌─────────────┐│
|
||||||
|
│ PROVISION │───────────────►│ APP_RETRY ││
|
||||||
|
└──────┬──────┘ └──────┬──────┘│
|
||||||
|
│ success │ │
|
||||||
|
▼ └───────┘
|
||||||
|
┌─────────────┐
|
||||||
|
│ TUNNEL_UP │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────┐
|
||||||
|
│ HEARTBEAT │ ◄─── 30s interval
|
||||||
|
└──────┬──────┘
|
||||||
|
│ config_changed
|
||||||
|
▼
|
||||||
|
┌─────────────┐
|
||||||
|
│ TUNNEL_REBUILD │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
└──► HEARTBEAT
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Module Design
|
||||||
|
|
||||||
|
### 4.1 AgentManager
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Orchestrate all agent components
|
||||||
|
- Manage agent lifecycle (start/stop)
|
||||||
|
- Handle state transitions
|
||||||
|
|
||||||
|
**Key Properties**:
|
||||||
|
```swift
|
||||||
|
class AgentManager: ObservableObject {
|
||||||
|
@Published var state: AgentState = .disconnected
|
||||||
|
@Published var tunnelInfo: TunnelInfo?
|
||||||
|
@Published var logs: [LogEntry] = []
|
||||||
|
|
||||||
|
private let provisioning: ProvisioningService
|
||||||
|
private let heartbeat: HeartbeatService
|
||||||
|
private let tunnel: TunnelManager
|
||||||
|
private let config: KeychainStorage
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Methods**:
|
||||||
|
```swift
|
||||||
|
func startAgent() async
|
||||||
|
func stopAgent()
|
||||||
|
func rebuildTunnel(with config: WireGuardConfig) async
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 PacketTunnelProvider
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Handle NetworkExtension tunnel lifecycle
|
||||||
|
- Start/stop WireGuard tunnel
|
||||||
|
- Report tunnel status
|
||||||
|
|
||||||
|
**Key Methods**:
|
||||||
|
```swift
|
||||||
|
override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void)
|
||||||
|
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void)
|
||||||
|
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 TunnelManager
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Build WireGuard config from stored keys
|
||||||
|
- Start/stop tunnel via PacketTunnelProvider
|
||||||
|
- Monitor tunnel state
|
||||||
|
- Track last handshake time
|
||||||
|
|
||||||
|
**Key Methods**:
|
||||||
|
```swift
|
||||||
|
func startTunnel(config: WireGuardConfig) async throws
|
||||||
|
func stopTunnel()
|
||||||
|
func isTunnelUp() -> Bool
|
||||||
|
func getLastHandshake() -> Date?
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 HeartbeatService
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- HTTP POST to `/api/v1/heartbeat` every 30s
|
||||||
|
- Send device status + tunnel state
|
||||||
|
- Receive config sync (detect changes)
|
||||||
|
- Trigger tunnel rebuild if config changed
|
||||||
|
|
||||||
|
**Key Methods**:
|
||||||
|
```swift
|
||||||
|
func startHeartbeatLoop()
|
||||||
|
func stopHeartbeat()
|
||||||
|
func sendHeartbeat(config: WireGuardConfig, tunnelUp: Bool, lastHandshake: Date?) async -> WireGuardConfig?
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 ProvisioningService
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- HTTP POST to `/api/v1/provision`
|
||||||
|
- Parse encrypted config response
|
||||||
|
- Decrypt config via CryptoManager
|
||||||
|
- Store config in Keychain
|
||||||
|
|
||||||
|
**Key Methods**:
|
||||||
|
```swift
|
||||||
|
func provision(serverUrl: String, token: String, hwid: String) async -> WireGuardConfig?
|
||||||
|
func decryptConfig(encrypted: String, hwid: String, salt: String) -> WireGuardConfig?
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.6 CryptoManager
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Key derivation: SHA-256(hwid + salt)
|
||||||
|
- AES-256-GCM decryption
|
||||||
|
- Secure memory handling
|
||||||
|
|
||||||
|
**Key Methods**:
|
||||||
|
```swift
|
||||||
|
func deriveKey(token: String, hwid: String) -> SymmetricKey
|
||||||
|
func decrypt(encoded: String, key: SymmetricKey) -> Data?
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.7 KeychainStorage
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Store WireGuard keys in Keychain
|
||||||
|
- Store agent config in UserDefaults
|
||||||
|
- Secure key access
|
||||||
|
|
||||||
|
**Key Methods**:
|
||||||
|
```swift
|
||||||
|
func saveConfig(_ config: AgentConfig)
|
||||||
|
func loadConfig() -> AgentConfig?
|
||||||
|
func saveWireGuardConfig(_ config: WireGuardConfig)
|
||||||
|
func loadWireGuardConfig() -> WireGuardConfig?
|
||||||
|
func clearAll()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.8 LogStore
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Buffer log entries in memory
|
||||||
|
- Persist to CoreData (optional)
|
||||||
|
- Provide log stream for UI
|
||||||
|
|
||||||
|
**Key Methods**:
|
||||||
|
```swift
|
||||||
|
func add(level: LogLevel, tag: String, message: String)
|
||||||
|
func clear()
|
||||||
|
func getLogs() -> [LogEntry]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Data Structures
|
||||||
|
|
||||||
|
### 5.1 WireGuardConfig
|
||||||
|
|
||||||
|
```swift
|
||||||
|
struct WireGuardConfig: Codable {
|
||||||
|
let deviceId: String
|
||||||
|
let privateKey: String
|
||||||
|
let presharedKey: String
|
||||||
|
let internalIp: String
|
||||||
|
let serverPub: String
|
||||||
|
let endpoint: String
|
||||||
|
let dns: String
|
||||||
|
let allowedIps: String
|
||||||
|
let serverWgIp: String
|
||||||
|
let configHash: String
|
||||||
|
let forwardsHash: String
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 AgentConfig
|
||||||
|
|
||||||
|
```swift
|
||||||
|
struct AgentConfig: Codable {
|
||||||
|
let serverUrl: String
|
||||||
|
let registrationToken: String
|
||||||
|
let isProvisioned: Bool
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 AgentState
|
||||||
|
|
||||||
|
```swift
|
||||||
|
enum AgentState: String {
|
||||||
|
case disconnected = "disconnected"
|
||||||
|
case connecting = "connecting"
|
||||||
|
case connected = "connected"
|
||||||
|
case failed = "failed"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 LogEntry
|
||||||
|
|
||||||
|
```swift
|
||||||
|
struct LogEntry: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
let timestamp: Date
|
||||||
|
let level: LogLevel
|
||||||
|
let tag: String
|
||||||
|
let message: String
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Error Handling
|
||||||
|
|
||||||
|
### 6.1 Error Types
|
||||||
|
|
||||||
|
```swift
|
||||||
|
enum AgentError: Error {
|
||||||
|
case wifiNotConnected
|
||||||
|
case provisioningFailed(String)
|
||||||
|
case heartbeatFailed(String)
|
||||||
|
case cryptoDecryptionFailed
|
||||||
|
case configInvalid
|
||||||
|
case tunnelInitFailed
|
||||||
|
case keychainError(OSStatus)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Recovery Strategy
|
||||||
|
|
||||||
|
| Error | Recovery |
|
||||||
|
|-------|----------|
|
||||||
|
| WiFi not connected | Retry 3x, then pause heartbeat |
|
||||||
|
| HTTP provision fail | Retry 3x, then notify user |
|
||||||
|
| HTTP heartbeat fail | Log only, continue |
|
||||||
|
| Crypto decrypt fail | Clear config, re-provision |
|
||||||
|
| Tunnel init fail | Retry 3x, then notify user |
|
||||||
|
| Keychain error | Use UserDefaults fallback |
|
||||||
|
|
||||||
|
## 7. Background Tasks
|
||||||
|
|
||||||
|
### 7.1 BGAppRefreshTask
|
||||||
|
|
||||||
|
iOS doesn't have boot receiver like Android. Use Background Tasks:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// Register task
|
||||||
|
BGAppRefreshTaskRequest.register(forTaskWithIdentifier: "com.nexusguard.agent.refresh", using: nil) {
|
||||||
|
task in
|
||||||
|
self.handleBackgroundRefresh(task: task as! BGAppRefreshTask)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule task
|
||||||
|
let request = BGAppRefreshTaskRequest(identifier: "com.nexusguard.agent.refresh")
|
||||||
|
request.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60) // 30 minutes
|
||||||
|
try BGTaskScheduler.shared.submit(request)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Background Modes
|
||||||
|
|
||||||
|
Add to `Info.plist`:
|
||||||
|
```xml
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>fetch</string>
|
||||||
|
<string>processing</string>
|
||||||
|
<string>vpn-api</string>
|
||||||
|
</array>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Security Considerations
|
||||||
|
|
||||||
|
### 8.1 Key Storage
|
||||||
|
|
||||||
|
- WireGuard private keys stored in iOS Keychain (hardware-backed)
|
||||||
|
- Non-sensitive config in UserDefaults
|
||||||
|
- Never log keys or tokens
|
||||||
|
|
||||||
|
### 8.2 TLS
|
||||||
|
|
||||||
|
- Server communication over HTTPS (TLS 1.2+)
|
||||||
|
- Use `URLSession` with default TLS settings
|
||||||
|
- Certificate pinning (optional, future)
|
||||||
|
|
||||||
|
### 8.3 Hardware ID
|
||||||
|
|
||||||
|
- Use `identifierForVendor` for device identification
|
||||||
|
- Persists across app reinstalls until device factory reset
|
||||||
|
- Used for key derivation (HWID + salt)
|
||||||
|
|
||||||
|
## 9. Testing Strategy
|
||||||
|
|
||||||
|
### 9.1 Unit Tests
|
||||||
|
|
||||||
|
- Crypto: Decrypt known ciphertext with known key
|
||||||
|
- Config: Keychain read/write cycle
|
||||||
|
- Heartbeat: Parse server response
|
||||||
|
|
||||||
|
### 9.2 Integration Tests
|
||||||
|
|
||||||
|
- Provision → Tunnel → Heartbeat cycle
|
||||||
|
- Config change detection
|
||||||
|
- Tunnel rebuild on config change
|
||||||
|
|
||||||
|
### 9.3 UI Tests
|
||||||
|
|
||||||
|
- VPN toggle interaction
|
||||||
|
- Tab switching
|
||||||
|
- Settings save/load
|
||||||
|
|
||||||
|
### 9.4 Device Tests
|
||||||
|
|
||||||
|
- iPhone 12+ running iOS 16+
|
||||||
|
- NetworkExtension background operation
|
||||||
|
- Keychain access in background
|
||||||
|
|
||||||
|
## 10. Future Enhancements
|
||||||
|
|
||||||
|
### 10.1 Phase 2
|
||||||
|
|
||||||
|
- iPadOS support
|
||||||
|
- Widget for quick status
|
||||||
|
- Shortcuts integration
|
||||||
|
- Siri commands
|
||||||
|
|
||||||
|
### 10.2 Phase 3
|
||||||
|
|
||||||
|
- macOS Catalyst support
|
||||||
|
- Apple Watch companion
|
||||||
|
- CarPlay integration
|
||||||
|
|
||||||
|
## 11. References
|
||||||
|
|
||||||
|
- [Apple NetworkExtension Documentation](https://developer.apple.com/documentation/networkextension)
|
||||||
|
- [WireGuard for iOS](https://github.com/WireGuard/wireguard-apple)
|
||||||
|
- [CryptoKit Documentation](https://developer.apple.com/cryptokit/)
|
||||||
|
- [Background Tasks](https://developer.apple.com/documentation/backgroundtasks)
|
||||||
|
- [NexusGuard Server API](../../server-core/docs/)
|
||||||
@@ -0,0 +1,506 @@
|
|||||||
|
# iOS Agent — UI Design Specs
|
||||||
|
|
||||||
|
**Status**: DESIGN ONLY
|
||||||
|
**Mirror**: Android Agent (`apps/android-agent/`)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document defines the UI design for the iOS WireGuard agent. It mirrors the Android agent's design, colors, layout, and user experience.
|
||||||
|
|
||||||
|
## Color Palette
|
||||||
|
|
||||||
|
### Status Colors
|
||||||
|
|
||||||
|
| Name | Hex | RGB | Usage |
|
||||||
|
|------|-----|-----|-------|
|
||||||
|
| `status_disconnected` | `#FFB0B0B0` | (176, 176, 176) | Disconnected state |
|
||||||
|
| `status_connecting` | `#FFFFD93D` | (255, 217, 61) | Connecting state |
|
||||||
|
| `status_connected` | `#FF4CAF50` | (76, 175, 80) | Connected state |
|
||||||
|
| `status_failed` | `#FFFF6B6B` | (255, 107, 107) | Failed state |
|
||||||
|
|
||||||
|
### UI Colors
|
||||||
|
|
||||||
|
| Name | Hex | RGB | Usage |
|
||||||
|
|------|-----|-----|-------|
|
||||||
|
| `card_bg` | `#FF1A1A2E` | (26, 26, 46) | Card background |
|
||||||
|
| `card_bg_dark` | `#FF0F0F1A` | (15, 15, 26) | Badge background |
|
||||||
|
| `text_primary` | `#FFFFFFFF` | (255, 255, 255) | Primary text |
|
||||||
|
| `text_secondary` | `#FFB0B0B0` | (176, 176, 176) | Secondary text |
|
||||||
|
| `accent_green` | `#FF4CAF50` | (76, 175, 80) | Accent/highlight |
|
||||||
|
| `accent_blue` | `#FF2196F3` | (33, 150, 243) | Links/actions |
|
||||||
|
| `border_color` | `#FF2A2A3E` | (42, 42, 62) | Card borders |
|
||||||
|
|
||||||
|
### Log Colors
|
||||||
|
|
||||||
|
| Level | Hex | RGB |
|
||||||
|
|-------|-----|-----|
|
||||||
|
| `DEBUG` | `#FF6B6B6B` | (107, 107, 107) |
|
||||||
|
| `INFO` | `#FFB0B0B0` | (176, 176, 176) |
|
||||||
|
| `WARN` | `#FFFFD93D` | (255, 217, 61) |
|
||||||
|
| `ERROR` | `#FFFF6B6B` | (255, 107, 107) |
|
||||||
|
|
||||||
|
## Color Definitions (Swift)
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension Color {
|
||||||
|
// Status colors
|
||||||
|
static let statusDisconnected = Color(red: 0.69, green: 0.69, blue: 0.69) // #B0B0B0
|
||||||
|
static let statusConnecting = Color(red: 1.0, green: 0.85, blue: 0.24) // #FFD93D
|
||||||
|
static let statusConnected = Color(red: 0.30, green: 0.69, blue: 0.31) // #4CAF50
|
||||||
|
static let statusFailed = Color(red: 1.0, green: 0.42, blue: 0.42) // #FF6B6B
|
||||||
|
|
||||||
|
// UI colors
|
||||||
|
static let cardBackground = Color(red: 0.10, green: 0.10, blue: 0.18) // #1A1A2E
|
||||||
|
static let cardBackgroundDark = Color(red: 0.06, green: 0.06, blue: 0.10) // #0F0F1A
|
||||||
|
static let textPrimary = Color.white // #FFFFFF
|
||||||
|
static let textSecondary = Color(red: 0.69, green: 0.69, blue: 0.69) // #B0B0B0
|
||||||
|
static let accentGreen = Color(red: 0.30, green: 0.69, blue: 0.31) // #4CAF50
|
||||||
|
static let accentBlue = Color(red: 0.13, green: 0.59, blue: 0.95) // #2196F3
|
||||||
|
static let borderColor = Color(red: 0.16, green: 0.16, blue: 0.24) // #2A2A3E
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout Structure
|
||||||
|
|
||||||
|
### Main Screen (ContentView)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ NexusGuard [⚙️ Settings]│
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ ● STATUS: Connected │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ IP Address 10.172.21.2 │ │
|
||||||
|
│ │ Admin Web https://... │ │
|
||||||
|
│ │ Device ID abc-123-... │ │
|
||||||
|
│ │ Handshake 14:32:05 │ │
|
||||||
|
│ │ Allowed IPs 0.0.0.0/0 │ │
|
||||||
|
│ │ Transport HTTPS │ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ [Status] [Ports] [Log] │ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ Port Forwards: 2 │ │
|
||||||
|
│ │ TCP :8080 → 10.172.21.2:80│ │
|
||||||
|
│ │ UDP :53 → 10.172.21.2:53│ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ [Copy Log] [Clear Log] │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 14:32:05 [INFO] Agent... │ │
|
||||||
|
│ │ 14:32:05 [INFO] Tunnel... │ │
|
||||||
|
│ │ 14:32:05 [WARN] Retry... │ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ ══════════════════ │ │
|
||||||
|
│ │ VPN Toggle │ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Settings Screen (SettingsView)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ ← Settings │
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Server URL │
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ https://api-nexus.datadunia.com│ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Registration Token │
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ •••••••••••••••• │ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Auto-Start on Boot │
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ [Toggle] │ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ Save Settings │ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ Export Config │ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ Import Config │ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ Paste Config │ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Specs
|
||||||
|
|
||||||
|
### Status Card
|
||||||
|
|
||||||
|
```swift
|
||||||
|
struct StatusCard: View {
|
||||||
|
let state: AgentState
|
||||||
|
let config: WireGuardConfig?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
// Status indicator
|
||||||
|
HStack {
|
||||||
|
Circle()
|
||||||
|
.fill(statusColor)
|
||||||
|
.frame(width: 12, height: 12)
|
||||||
|
Text(statusText)
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(statusColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info fields
|
||||||
|
if state == .connected {
|
||||||
|
InfoRow(label: "IP Address", value: config?.internalIp ?? "-")
|
||||||
|
InfoRow(label: "Admin Web", value: "https://api-nexus.datadunia.com")
|
||||||
|
InfoRow(label: "Device ID", value: config?.deviceId ?? "-")
|
||||||
|
InfoRow(label: "Handshake", value: lastHandshake ?? "-")
|
||||||
|
InfoRow(label: "Allowed IPs", value: config?.allowedIps ?? "-")
|
||||||
|
InfoRow(label: "Transport", value: transport)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.cardBackground)
|
||||||
|
.cornerRadius(12)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.stroke(Color.borderColor, lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var statusColor: Color {
|
||||||
|
switch state {
|
||||||
|
case .disconnected: return .statusDisconnected
|
||||||
|
case .connecting: return .statusConnecting
|
||||||
|
case .connected: return .statusConnected
|
||||||
|
case .failed: return .statusFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Info Row
|
||||||
|
|
||||||
|
```swift
|
||||||
|
struct InfoRow: View {
|
||||||
|
let label: String
|
||||||
|
let value: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Text(label)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.textSecondary)
|
||||||
|
Spacer()
|
||||||
|
Text(value)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.textPrimary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port Forward Row
|
||||||
|
|
||||||
|
```swift
|
||||||
|
struct PortForwardRow: View {
|
||||||
|
let forward: PortForward
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Text(forward.protocol.uppercased())
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.foregroundColor(.cardBackgroundDark)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(Color.accentGreen)
|
||||||
|
.cornerRadius(4)
|
||||||
|
|
||||||
|
Text(":\(forward.publicPort)")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.textPrimary)
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("\(forward.targetIp):\(forward.targetPort)")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.textSecondary)
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Entry
|
||||||
|
|
||||||
|
```swift
|
||||||
|
struct LogEntryView: View {
|
||||||
|
let entry: LogEntry
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .top) {
|
||||||
|
Text(entry.timestamp, style: .time)
|
||||||
|
.font(.system(.caption, design: .monospaced))
|
||||||
|
.foregroundColor(entry.level.color)
|
||||||
|
|
||||||
|
Text("[\(entry.level.rawValue)]")
|
||||||
|
.font(.system(.caption, design: .monospaced))
|
||||||
|
.foregroundColor(entry.level.color)
|
||||||
|
|
||||||
|
Text("\(entry.tag): \(entry.message)")
|
||||||
|
.font(.system(.caption, design: .monospaced))
|
||||||
|
.foregroundColor(entry.level.color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LogLevel {
|
||||||
|
var color: Color {
|
||||||
|
switch self {
|
||||||
|
case .debug: return Color(red: 0.42, green: 0.42, blue: 0.42) // #6B6B6B
|
||||||
|
case .info: return Color(red: 0.69, green: 0.69, blue: 0.69) // #B0B0B0
|
||||||
|
case .warn: return Color(red: 1.0, green: 0.85, blue: 0.24) // #FFD93D
|
||||||
|
case .error: return Color(red: 1.0, green: 0.42, blue: 0.42) // #FF6B6B
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### VPN Toggle
|
||||||
|
|
||||||
|
```swift
|
||||||
|
struct VPNToggle: View {
|
||||||
|
@Binding var isOn: Bool
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: action) {
|
||||||
|
RoundedRectangle(cornerRadius: 25)
|
||||||
|
.fill(isOn ? Color.accentGreen : Color.statusDisconnected)
|
||||||
|
.frame(height: 50)
|
||||||
|
.overlay(
|
||||||
|
HStack {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.white)
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
|
.offset(x: isOn ? 40 : -40)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tab Layout
|
||||||
|
|
||||||
|
```swift
|
||||||
|
struct TabBar: View {
|
||||||
|
@Binding var selectedTab: Int
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
TabButton(title: "Status", isSelected: selectedTab == 0) {
|
||||||
|
selectedTab = 0
|
||||||
|
}
|
||||||
|
TabButton(title: "Ports", isSelected: selectedTab == 1) {
|
||||||
|
selectedTab = 1
|
||||||
|
}
|
||||||
|
TabButton(title: "Log", isSelected: selectedTab == 2) {
|
||||||
|
selectedTab = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(Color.cardBackground)
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TabButton: View {
|
||||||
|
let title: String
|
||||||
|
let isSelected: Bool
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: action) {
|
||||||
|
Text(title)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(isSelected ? .textPrimary : .textSecondary)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background(isSelected ? Color.accentGreen : Color.clear)
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Typography
|
||||||
|
|
||||||
|
### Font Sizes
|
||||||
|
|
||||||
|
| Element | Size | Weight | Design |
|
||||||
|
|---------|------|--------|--------|
|
||||||
|
| Status text | 18pt | Semibold | Default |
|
||||||
|
| Info label | 14pt | Regular | Default |
|
||||||
|
| Info value | 14pt | Regular | Monospaced |
|
||||||
|
| Tab text | 14pt | Medium | Default |
|
||||||
|
| Log text | 12pt | Regular | Monospaced |
|
||||||
|
| Badge text | 10pt | Bold | Default |
|
||||||
|
|
||||||
|
### Font Definitions
|
||||||
|
|
||||||
|
```swift
|
||||||
|
extension Font {
|
||||||
|
static let statusText = Font.headline
|
||||||
|
static let infoLabel = Font.subheadline
|
||||||
|
static let infoValue = Font.subheadline.monospaced()
|
||||||
|
static let tabText = Font.subheadline.weight(.medium)
|
||||||
|
static let logText = Font.caption.monospaced()
|
||||||
|
static let badgeText = Font.caption2.bold()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Spacing
|
||||||
|
|
||||||
|
| Element | Value |
|
||||||
|
|---------|-------|
|
||||||
|
| Card padding | 16pt |
|
||||||
|
| Card corner radius | 12pt |
|
||||||
|
| Card border width | 1pt |
|
||||||
|
| Row spacing | 12pt |
|
||||||
|
| Badge padding | 8pt horizontal, 4pt vertical |
|
||||||
|
| Badge corner radius | 4pt |
|
||||||
|
| Button height | 50pt |
|
||||||
|
| Button corner radius | 25pt |
|
||||||
|
|
||||||
|
## Animation
|
||||||
|
|
||||||
|
### Status Change
|
||||||
|
|
||||||
|
```swift
|
||||||
|
withAnimation(.easeInOut(duration: 0.3)) {
|
||||||
|
state = .connected
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tab Switch
|
||||||
|
|
||||||
|
```swift
|
||||||
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
|
selectedTab = 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### VPN Toggle
|
||||||
|
|
||||||
|
```swift
|
||||||
|
withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
|
||||||
|
isVPNOn.toggle()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Haptic Feedback
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// Success
|
||||||
|
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||||
|
|
||||||
|
// Error
|
||||||
|
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||||
|
|
||||||
|
// Selection
|
||||||
|
UISelectionFeedbackGenerator().selectionChanged()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dark Mode
|
||||||
|
|
||||||
|
iOS agent uses dark mode only (matches Android design). Force dark mode:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
@main
|
||||||
|
struct NexusGuardApp: App {
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
ContentView()
|
||||||
|
.preferredColorScheme(.dark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Responsive Design
|
||||||
|
|
||||||
|
### iPhone SE
|
||||||
|
|
||||||
|
- Compact layout
|
||||||
|
- Smaller font sizes
|
||||||
|
- Reduced padding
|
||||||
|
|
||||||
|
### iPhone 14/15
|
||||||
|
|
||||||
|
- Standard layout
|
||||||
|
- Default font sizes
|
||||||
|
- Standard padding
|
||||||
|
|
||||||
|
### iPhone 14/15 Pro Max
|
||||||
|
|
||||||
|
- Expanded layout
|
||||||
|
- Larger font sizes
|
||||||
|
- Increased padding
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
|
||||||
|
### VoiceOver
|
||||||
|
|
||||||
|
```swift
|
||||||
|
Text("Connected")
|
||||||
|
.accessibilityLabel("VPN Status: Connected")
|
||||||
|
.accessibilityHint("Double tap to toggle VPN")
|
||||||
|
|
||||||
|
InfoRow(label: "IP Address", value: "10.172.21.2")
|
||||||
|
.accessibilityElement(children: .combine)
|
||||||
|
.accessibilityLabel("IP Address: 10.172.21.2")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic Type
|
||||||
|
|
||||||
|
```swift
|
||||||
|
Text("Status")
|
||||||
|
.font(.headline)
|
||||||
|
.dynamicTypeSize(...DynamicTypeSize.accessibility2)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reduce Motion
|
||||||
|
|
||||||
|
```swift
|
||||||
|
@Environment(\.accessibilityReduceMotion) var reduceMotion
|
||||||
|
|
||||||
|
withAnimation(reduceMotion ? nil : .easeInOut(duration: 0.3)) {
|
||||||
|
state = .connected
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "========================================="
|
|
||||||
echo " NexusGuard SD-WAN: INITIAL SETUP "
|
|
||||||
echo "========================================="
|
|
||||||
|
|
||||||
# 1. Environment File Generation
|
|
||||||
if [ ! -f .env ]; then
|
|
||||||
echo "[!] .env file not found."
|
|
||||||
if [ -f .env.example ]; then
|
|
||||||
echo "[+] Auto-generating .env from .env.example..."
|
|
||||||
cp .env.example .env
|
|
||||||
|
|
||||||
# Generate secure random hex keys
|
|
||||||
RANDOM_JWT=$(openssl rand -hex 32)
|
|
||||||
RANDOM_SALT=$(openssl rand -hex 32)
|
|
||||||
|
|
||||||
sed -i "s/GantiDenganKunciRahasiaJWTUntukDashboard256Bit/$RANDOM_JWT/g" .env
|
|
||||||
sed -i "s/GantiDenganKunciGaramUntukWireGuard256Bit/$RANDOM_SALT/g" .env
|
|
||||||
|
|
||||||
echo "[+] Secure cryptographic keys have been auto-generated!"
|
|
||||||
else
|
|
||||||
echo "[-] ERROR: .env.example is missing. Cannot generate .env."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "[+] .env file already exists. Skipping environment generation."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Make sure Docker Compose v2 is available
|
|
||||||
if ! docker compose version &> /dev/null; then
|
|
||||||
echo "[-] ERROR: docker compose (v2) is not available. Please install Docker and Docker Compose plugin."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "========================================="
|
|
||||||
echo " Setup complete! Next Steps: "
|
|
||||||
echo " 1. Open and review the '.env' file. "
|
|
||||||
echo " Make sure to change DB_PASSWORD, "
|
|
||||||
echo " API_PORT, and VITE_API_BASE_URL. "
|
|
||||||
echo " "
|
|
||||||
echo " 2. Run 'bash update.sh' to download the "
|
|
||||||
echo " latest code, build, and start the "
|
|
||||||
echo " system. The script will also run "
|
|
||||||
echo " database migrations automatically. "
|
|
||||||
echo " "
|
|
||||||
echo " 3. Create your Admin account: "
|
|
||||||
echo " docker exec -it nexus-guard-suite-server-core-1 ./server-core -create-admin -user \"admin\" -pass \"PasswordKuat123!\""
|
|
||||||
echo "========================================="
|
|
||||||
@@ -32,10 +32,28 @@ while [[ $# -gt 0 ]]; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# 1. Pastikan file env ada
|
# 1. Auto-generate .env jika belum ada (seamless first-run)
|
||||||
if [ ! -f .env ]; then
|
if [ ! -f .env ]; then
|
||||||
echo "[-] ERROR: .env file not found. Please run ./setup.sh first to initialize configuration."
|
echo "[!] .env file not found."
|
||||||
|
if [ -f .env.example ]; then
|
||||||
|
echo "[+] Auto-generating .env from .env.example..."
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Generate secure random hex keys
|
||||||
|
RANDOM_JWT=$(openssl rand -hex 32)
|
||||||
|
RANDOM_SALT=$(openssl rand -hex 32)
|
||||||
|
|
||||||
|
sed -i "s/GantiDenganKunciRahasiaJWTUntukDashboard256Bit/$RANDOM_JWT/g" .env
|
||||||
|
sed -i "s/GantiDenganKunciGaramUntukWireGuard256Bit/$RANDOM_SALT/g" .env
|
||||||
|
|
||||||
|
echo "[+] Secure cryptographic keys have been auto-generated!"
|
||||||
|
echo "[+] Review .env file before continuing. Edit DB_PASSWORD, API_PORT, VITE_API_BASE_URL as needed."
|
||||||
|
else
|
||||||
|
echo "[-] ERROR: .env.example is missing. Cannot generate .env."
|
||||||
exit 1
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[+] .env file already exists. Skipping environment generation."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2. Update Code dari Git (Main & Submodules)
|
# 2. Update Code dari Git (Main & Submodules)
|
||||||
@@ -47,8 +65,18 @@ echo "[+] Syncing and updating submodules..."
|
|||||||
SUBMODULE_FAILED=false
|
SUBMODULE_FAILED=false
|
||||||
git submodule update --init --recursive --remote || { SUBMODULE_FAILED=true; echo "[!] Git submodule update skipped or failed."; }
|
git submodule update --init --recursive --remote || { SUBMODULE_FAILED=true; echo "[!] Git submodule update skipped or failed."; }
|
||||||
|
|
||||||
# 3. Hitung state hash (git + .env) untuk deteksi perubahan
|
# 2b. Build VitePress docs (non-blocking)
|
||||||
CURRENT_HASH=$(echo "$(git rev-parse HEAD 2>/dev/null)$(git submodule status 2>/dev/null)$(sha256sum .env 2>/dev/null)" | sha256sum | cut -d" " -f1)
|
echo "[+] Building VitePress documentation..."
|
||||||
|
DOCS_BUILD_FAILED=false
|
||||||
|
if [ -d "apps/docs" ]; then
|
||||||
|
(cd apps/docs && npm install && npm run docs:build) || { DOCS_BUILD_FAILED=true; echo "[!] Docs build failed. Documentation may be stale. Continuing deployment..."; }
|
||||||
|
else
|
||||||
|
echo "[!] apps/docs directory not found. Skipping docs build."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Hitung state hash (hanya server-core + dashboard-ui untuk rebuild detection)
|
||||||
|
# device-agent dan device-agent-embedded build terpisah, tidak mempengaruhi Docker build
|
||||||
|
DOCKER_HASH=$(echo "$(git rev-parse HEAD 2>/dev/null)$(git -C apps/server-core rev-parse HEAD 2>/dev/null)$(git -C apps/dashboard-ui rev-parse HEAD 2>/dev/null)$(sha256sum .env 2>/dev/null)" | sha256sum | cut -d" " -f1)
|
||||||
|
|
||||||
# 4. Tentukan apakah perlu rebuild
|
# 4. Tentukan apakah perlu rebuild
|
||||||
NEEDS_REBUILD=false
|
NEEDS_REBUILD=false
|
||||||
@@ -63,11 +91,11 @@ elif [ ! -f "$STATE_FILE" ]; then
|
|||||||
NEEDS_REBUILD=true
|
NEEDS_REBUILD=true
|
||||||
else
|
else
|
||||||
PREVIOUS_HASH=$(cat "$STATE_FILE" 2>/dev/null || echo "")
|
PREVIOUS_HASH=$(cat "$STATE_FILE" 2>/dev/null || echo "")
|
||||||
if [ "$CURRENT_HASH" != "$PREVIOUS_HASH" ]; then
|
if [ "$DOCKER_HASH" != "$PREVIOUS_HASH" ]; then
|
||||||
echo "[+] State hash changed. Rebuilding."
|
echo "[+] Docker components changed. Rebuilding."
|
||||||
NEEDS_REBUILD=true
|
NEEDS_REBUILD=true
|
||||||
else
|
else
|
||||||
echo "[-] No changes detected. Skipping build and restart."
|
echo "[-] No Docker changes detected. Skipping build and restart."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -151,7 +179,7 @@ if [ "$NEEDS_REBUILD" = true ]; then
|
|||||||
docker image prune -f
|
docker image prune -f
|
||||||
|
|
||||||
# Simpan state secara atomik (tmp + mv untuk cegah korupsi file)
|
# Simpan state secara atomik (tmp + mv untuk cegah korupsi file)
|
||||||
echo "$CURRENT_HASH" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
|
echo "$DOCKER_HASH" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
|
||||||
|
|
||||||
echo "========================================="
|
echo "========================================="
|
||||||
echo " Update & Deployment complete! "
|
echo " Update & Deployment complete! "
|
||||||
|
|||||||
Reference in New Issue
Block a user