Compare commits
103 Commits
v1.0.0-dev.28
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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
|
||||
@@ -23,22 +23,34 @@ jobs:
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@@ -23,8 +23,31 @@ jobs:
|
||||
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: |
|
||||
@@ -37,7 +60,7 @@ jobs:
|
||||
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 -o "$OUT" .
|
||||
CGO_ENABLED=0 GOOS="$GOOS" GOARCH="$GOARCH" go build -ldflags="-s -w" -o "$OUT" .
|
||||
done
|
||||
echo "=== Build output ==="
|
||||
ls -la bin/
|
||||
|
||||
@@ -23,14 +23,33 @@ jobs:
|
||||
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 .
|
||||
|
||||
|
||||
+89
-20
@@ -8,11 +8,17 @@ on:
|
||||
- 'v*-beta*'
|
||||
- 'v*-test*'
|
||||
- 'v[0-9]*.[0-9]*.[0-9]'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
# --- TESTS (test tags + stable release tags) ---
|
||||
# ====================================================================
|
||||
# TESTS — test tags only
|
||||
# ====================================================================
|
||||
server-core-test:
|
||||
if: contains(gitea.ref_name, 'test') || (!contains(gitea.ref_name, 'dev') && !contains(gitea.ref_name, 'beta'))
|
||||
if: contains(gitea.ref_name, 'test') || github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -26,17 +32,14 @@ jobs:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.26'
|
||||
- name: Generate swagger docs
|
||||
working-directory: apps/server-core
|
||||
run: |
|
||||
go install github.com/swaggo/swag/cmd/swag@v1.16.6
|
||||
swag init -g main.go --parseDependency --parseInternal
|
||||
cache: true
|
||||
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:
|
||||
if: contains(gitea.ref_name, 'test') || (!contains(gitea.ref_name, 'dev') && !contains(gitea.ref_name, 'beta'))
|
||||
if: contains(gitea.ref_name, 'test') || github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -50,12 +53,14 @@ jobs:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.26'
|
||||
cache: true
|
||||
cache-dependency-path: apps/device-agent/go.sum
|
||||
- name: Test
|
||||
working-directory: apps/device-agent
|
||||
run: go test ./... -cover
|
||||
|
||||
dashboard-test:
|
||||
if: contains(gitea.ref_name, 'test') || (!contains(gitea.ref_name, 'dev') && !contains(gitea.ref_name, 'beta'))
|
||||
if: contains(gitea.ref_name, 'test') || github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -76,40 +81,104 @@ jobs:
|
||||
working-directory: apps/dashboard-ui
|
||||
run: npm run build
|
||||
|
||||
# --- BUILD + RELEASE (dev/beta/release tags) ---
|
||||
android-agent-test:
|
||||
if: contains(gitea.ref_name, 'test') || github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.BUILD_TOKEN }}
|
||||
persist-credentials: true
|
||||
- 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:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: gradle
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
- name: Grant execute permission for gradlew
|
||||
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()
|
||||
needs: [server-core-test, device-agent-test, dashboard-test]
|
||||
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()
|
||||
needs: [server-core-test, device-agent-test, dashboard-test]
|
||||
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()
|
||||
needs: [server-core-test, device-agent-test, dashboard-test]
|
||||
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, 'test') && !failure()
|
||||
needs: [server-core-test, device-agent-test, dashboard-test]
|
||||
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()
|
||||
needs: [build-server-core, build-device-agent, build-dashboard, build-docs]
|
||||
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:
|
||||
prerelease: ${{ contains(gitea.ref_name, 'dev') }}
|
||||
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
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
environment:
|
||||
description: 'Target environment (dev or production)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.environment }}
|
||||
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: Deploy to ${{ inputs.environment }}
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
||||
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
|
||||
run: |
|
||||
echo "Deploying to ${{ inputs.environment }}..."
|
||||
echo "Host: $DEPLOY_HOST"
|
||||
echo "Path: $DEPLOY_PATH"
|
||||
|
||||
# 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
|
||||
|
||||
# Deploy on server
|
||||
echo "Running deployment..."
|
||||
ssh -i ~/.ssh/deploy_key "${DEPLOY_USER}@${DEPLOY_HOST}" << 'DEPLOY_SCRIPT'
|
||||
cd "${DEPLOY_PATH}" || exit 1
|
||||
|
||||
# Stop services
|
||||
echo "Stopping services..."
|
||||
docker compose down || true
|
||||
|
||||
# Run update script
|
||||
echo "Running update..."
|
||||
bash update.sh --force
|
||||
|
||||
# Verify services
|
||||
echo "Verifying services..."
|
||||
docker compose ps
|
||||
|
||||
echo "Deployment complete!"
|
||||
DEPLOY_SCRIPT
|
||||
|
||||
echo "Deployed to ${{ inputs.environment }} successfully!"
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: rm -rf ~/.ssh/deploy_key /tmp/dashboard-ui.tar.gz
|
||||
@@ -21,6 +21,17 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -49,7 +49,8 @@ jobs:
|
||||
"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"
|
||||
"dashboard-ui": "${SERVER}/${REPO}/releases/download/${VERSION}/dashboard-ui-dist.tar.gz",
|
||||
"android-agent": "${SERVER}/${REPO}/releases/download/${VERSION}/nexusguard-android.apk"
|
||||
}
|
||||
}
|
||||
ENDJSON
|
||||
@@ -115,6 +116,13 @@ jobs:
|
||||
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
|
||||
|
||||
+14
@@ -36,6 +36,20 @@ temp_*
|
||||
# Build output
|
||||
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
|
||||
connect_remote.txt
|
||||
|
||||
@@ -7,3 +7,6 @@
|
||||
[submodule "apps/device-agent"]
|
||||
path = apps/device-agent
|
||||
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,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
|
||||
|
||||
**Generated:** 2026-05-22
|
||||
**Commit:** `92051d5`
|
||||
**Generated:** 2026-06-20
|
||||
**Branch:** `main`
|
||||
|
||||
## 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
|
||||
```
|
||||
@@ -17,10 +28,9 @@ NexusGuard SD-WAN Suite — Enterprise Zero-Trust SD-WAN with WireGuard tunnelin
|
||||
├── docker-compose.yml # Production orchestration
|
||||
├── docker-compose.dev.yml# Dev (air hot-reload)
|
||||
├── Makefile # up/down/dev/migrate/reset-db
|
||||
├── setup.sh # First-run: generate .env + random keys
|
||||
├── update.sh # Docker update: pull/build/migrate
|
||||
├── update.sh # Docker update: auto-generate .env + pull/build/migrate
|
||||
├── nexusguard-install.sh # Native install (systemd + nginx)
|
||||
├── nexusguard-uninstall.sh # Native uninstall
|
||||
├── nexusguard-uninstall.sh
|
||||
├── .env.example # DB/JWT/SALT/VITE config template
|
||||
├── .gitmodules # 3 submodules → git.datadunia.com
|
||||
└── .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`.
|
||||
|
||||
## 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
|
||||
| Task | Location | Notes |
|
||||
|------|----------|-------|
|
||||
@@ -35,16 +58,85 @@ 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 |
|
||||
| Dev migration | `apps/server-core/main_dev.go` | GORM AutoMigrate (build tag `dev`) |
|
||||
| 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 API client | `apps/dashboard-ui/src/api/` | Axios API modules |
|
||||
| Dashboard stores | `apps/dashboard-ui/src/stores/` | Pinia state stores |
|
||||
| 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 |
|
||||
| Shared crypto | `apps/*/shared/crypto/encryptor.go` | AES-256-GCM (duplicated identical) |
|
||||
| CI workflows | `apps/*/.gitea/workflows/build.yml` | Gitea Actions per submodule |
|
||||
| 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 |
|
||||
| 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
|
||||
| Symbol | Type | Location | Role |
|
||||
@@ -60,8 +152,15 @@ NexusGuard SD-WAN Suite — Enterprise Zero-Trust SD-WAN with WireGuard tunnelin
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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
|
||||
- **Go**: Standard layout (`main.go` in root, `internal/`, `api/`)
|
||||
@@ -79,11 +178,12 @@ NexusGuard SD-WAN Suite — Enterprise Zero-Trust SD-WAN with WireGuard tunnelin
|
||||
|
||||
## ANTI-PATTERNS (THIS PROJECT)
|
||||
- **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 and dont commit
|
||||
- **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** reopen completed phases/commits — fix forward only
|
||||
- **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** create cross-phase workarounds
|
||||
|
||||
@@ -119,6 +219,15 @@ if d.AllowInternet {
|
||||
// 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`
|
||||
- Used for **client config** and **firewall rules**
|
||||
- NOT used for server-side WireGuard kernel config
|
||||
@@ -200,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 "..."
|
||||
```
|
||||
|
||||
## 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
|
||||
- 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`
|
||||
- No root linter configs (`.golangci.yml`, `.eslintrc`, `.editorconfig`)
|
||||
- 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)
|
||||
|
||||
@@ -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`.
|
||||
|
||||
## 🏗️ 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.
|
||||
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
|
||||
git clone https://git.datadunia.com/nexusguard/Nexus-Guard-Suite.git
|
||||
cd Nexus-Guard-Suite
|
||||
./setup.sh # Generate .env file automatically
|
||||
# Edit .env with your configuration
|
||||
bash update.sh # Pull, build, migrate, and start
|
||||
bash update.sh # Auto-generates .env + pulls, builds, migrates, starts
|
||||
```
|
||||
|
||||
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.
|
||||
- Create the database schema via automated migration.
|
||||
|
||||
Edit `.env` afterward to customize `DB_PASSWORD`, `API_PORT`, `VITE_API_BASE_URL`.
|
||||
|
||||
**Update Options:**
|
||||
```bash
|
||||
bash update.sh # Smart update (only rebuild if changes detected)
|
||||
@@ -263,12 +266,14 @@ NexusGuard/
|
||||
├── apps/
|
||||
│ ├── server-core/ # Go/Gin API backend
|
||||
│ ├── 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)
|
||||
├── docker-compose.yml # Docker orchestration
|
||||
├── docker-compose.dev.yml # Docker dev (hot-reload)
|
||||
├── update.sh # Docker update script
|
||||
├── setup.sh # Docker initial setup
|
||||
├── update.sh # Docker update + auto-setup script
|
||||
├── nexusguard-install.sh # Native install script
|
||||
├── nexusguard-uninstall.sh # Native uninstall script
|
||||
├── .env.example # Environment template
|
||||
|
||||
Submodule
+1
Submodule apps/android-agent added at f4455b6ad8
+1
-1
Submodule apps/dashboard-ui updated: dc20427f97...b201610b33
+1
-1
Submodule apps/device-agent updated: f2dae58e91...85f53ab7e9
Submodule
+1
Submodule apps/device-agent-embedded added at be477c91ae
+1
-1
Submodule apps/server-core updated: 19e804ca45...bc35442af5
@@ -20,6 +20,7 @@ services:
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
command: redis-server --save "" --appendonly no
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
ports:
|
||||
|
||||
@@ -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,51 @@
|
||||
# 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 │
|
||||
└─────────────────┘ └─────────┘
|
||||
```
|
||||
|
||||
## 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,190 @@
|
||||
# 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 computes config_hash = endpoint + internalIP + serverPub
|
||||
3. Agent POSTs {config_hash, last_handshake, tunnel_up} to server
|
||||
4. Server compares with stored config
|
||||
5. If config changed → server responds with new config
|
||||
6. Agent detects change → rebuilds tunnel
|
||||
```
|
||||
|
||||
### 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,290 @@
|
||||
# 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 │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 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
|
||||
- PostgreSQL installed and running
|
||||
- Redis installed and running
|
||||
- WireGuard tools (will be auto-installed if missing)
|
||||
- Pre-built binaries in ./bin/ directory
|
||||
|
||||
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 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
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
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,11 +32,29 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
# 1. Pastikan file env ada
|
||||
# 1. Auto-generate .env jika belum ada (seamless first-run)
|
||||
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
|
||||
fi
|
||||
else
|
||||
echo "[+] .env file already exists. Skipping environment generation."
|
||||
fi
|
||||
|
||||
# 2. Update Code dari Git (Main & Submodules)
|
||||
echo "[+] Pulling latest source code from repository..."
|
||||
@@ -56,8 +74,9 @@ else
|
||||
echo "[!] apps/docs directory not found. Skipping docs build."
|
||||
fi
|
||||
|
||||
# 3. Hitung state hash (git + .env) untuk deteksi perubahan
|
||||
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)
|
||||
# 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
|
||||
NEEDS_REBUILD=false
|
||||
@@ -72,11 +91,11 @@ elif [ ! -f "$STATE_FILE" ]; then
|
||||
NEEDS_REBUILD=true
|
||||
else
|
||||
PREVIOUS_HASH=$(cat "$STATE_FILE" 2>/dev/null || echo "")
|
||||
if [ "$CURRENT_HASH" != "$PREVIOUS_HASH" ]; then
|
||||
echo "[+] State hash changed. Rebuilding."
|
||||
if [ "$DOCKER_HASH" != "$PREVIOUS_HASH" ]; then
|
||||
echo "[+] Docker components changed. Rebuilding."
|
||||
NEEDS_REBUILD=true
|
||||
else
|
||||
echo "[-] No changes detected. Skipping build and restart."
|
||||
echo "[-] No Docker changes detected. Skipping build and restart."
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -160,7 +179,7 @@ if [ "$NEEDS_REBUILD" = true ]; then
|
||||
docker image prune -f
|
||||
|
||||
# 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 " Update & Deployment complete! "
|
||||
|
||||
Reference in New Issue
Block a user