Compare commits
122 Commits
v1.0.0-dev.17
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 88fa60901e | |||
| f95763ae42 | |||
| 45d2e51062 | |||
| 7c9110ce9c | |||
| 115019846e | |||
| d7d471308a | |||
| 8654505933 | |||
| 482cabce93 | |||
| 13a7bfef1e | |||
| 326e0774a8 | |||
| d61c470b97 | |||
| 52fc7c749c | |||
| 5d60c7d160 | |||
| 8cadfc4657 | |||
| 5d3761c56c | |||
| b04d47a1bf | |||
| 59a8436a45 | |||
| 2fb3109b9f | |||
| 84689b208e | |||
| 4af46dadad | |||
| 5ed8e54f16 | |||
| 1124cd8159 | |||
| c204f75978 | |||
| 8719535f55 | |||
| dd3aaafffb | |||
| 934dc291f1 | |||
| be5178e04f | |||
| 6cb4e53598 | |||
| 68203f2eb3 | |||
| 24e83ee8cd | |||
| 7cce8c39a6 | |||
| ca4107e2bc | |||
| 192b753f5d | |||
| 4eea268ec5 | |||
| fcb1de2cac | |||
| 4df2e0aeeb | |||
| 2426a7b42c | |||
| d6610f82e9 | |||
| 80ba316d4a | |||
| 7a43ec8f2d | |||
| 05d496432a | |||
| d6c53f6f86 | |||
| a485c8df42 | |||
| f98eedbfbe | |||
| ea6fbe83b5 | |||
| 91ed1adcda | |||
| 95ec25b80e | |||
| 2738e3c7d1 | |||
| 80c97bbc22 | |||
| 077384c433 | |||
| fc204bba17 | |||
| e1e35318de | |||
| bd79bb0043 | |||
| 248ad123bb | |||
| 855fb2dbee | |||
| 45f95a957a | |||
| e50c1900ad | |||
| 957cc8d5cb | |||
| da94cc3f6b | |||
| 465b1f7388 | |||
| e9e4db261d | |||
| 6f6b44b8a2 | |||
| 1a1f8a5a8a | |||
| 61a5936224 | |||
| f03686ac3d | |||
| e1cbdb66c8 | |||
| a89fdaf435 | |||
| 633e06f556 | |||
| b447ad9757 | |||
| d7bbbbdefd | |||
| b1458d3a99 | |||
| 2c5a3c7fa5 | |||
| 5a6675c70a | |||
| 4528493647 | |||
| 9f1e37fc57 | |||
| 5e8b72efdc | |||
| 08b74ce45b | |||
| 7c5604d510 | |||
| 26e65006b5 | |||
| c011215deb | |||
| 6f5ffb8b1d | |||
| 934d4a03d7 | |||
| 27c003864e | |||
| 014309c390 | |||
| d5b1f4428a | |||
| 6c5a762407 | |||
| 9c8a3c0751 | |||
| 272d744e12 | |||
| 5304bd11ca | |||
| 359a23c079 | |||
| facd8bd7d5 | |||
| 83722abd32 | |||
| 359bd6f9c4 | |||
| e81c1dd448 | |||
| 9433dc3547 | |||
| 4d58b01641 | |||
| 4e19c371d0 | |||
| aa620dec6a | |||
| 48340aa7a3 | |||
| 68ebfbaad3 | |||
| 46ff7ca737 | |||
| 08b152a370 | |||
| 6f22aad1ad | |||
| d21863ce8f | |||
| eba741cd97 | |||
| fd9da1fd71 | |||
| d46bc11485 | |||
| f1fa0acfa7 | |||
| a5c1ea1c1b | |||
| 4dbc0bd9f3 | |||
| 94059ee9e6 | |||
| 06d8876b04 | |||
| 90b3baa2fb | |||
| 71224721d3 | |||
| 56f44f5b95 | |||
| 08ac26c698 | |||
| ac0755977c | |||
| c1490421ba | |||
| 2d366c4619 | |||
| dad057528c | |||
| 8ae9149620 | |||
| 9d6c2abb9f |
@@ -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
|
||||
|
||||
|
||||
@@ -22,23 +22,32 @@ jobs:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.26'
|
||||
cache: true
|
||||
cache-dependency-path: apps/device-agent/go.sum
|
||||
|
||||
- uses: https://gitea.com/actions/go-hashfiles@v0.0.1
|
||||
id: hash-go
|
||||
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: |
|
||||
/root/go/pkg/mod
|
||||
/root/.cache/go-build
|
||||
key: go-device-agent-${{ steps.hash-go.outputs.hash }}
|
||||
restore-keys: go-device-agent-
|
||||
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: |
|
||||
@@ -51,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/
|
||||
|
||||
@@ -22,33 +22,34 @@ jobs:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.26'
|
||||
cache: true
|
||||
cache-dependency-path: apps/server-core/go.sum
|
||||
|
||||
- uses: https://gitea.com/actions/go-hashfiles@v0.0.1
|
||||
id: hash-go
|
||||
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: |
|
||||
/root/go/pkg/mod
|
||||
/root/.cache/go-build
|
||||
key: go-server-core-${{ steps.hash-go.outputs.hash }}
|
||||
restore-keys: go-server-core-
|
||||
|
||||
- name: Download Go modules
|
||||
working-directory: apps/server-core
|
||||
run: go mod download
|
||||
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
|
||||
```
|
||||
@@ -15,12 +26,11 @@ NexusGuard SD-WAN Suite — Enterprise Zero-Trust SD-WAN with WireGuard tunnelin
|
||||
│ ├── dashboard-ui/ # Vue 3 + Vite frontend (submodule)
|
||||
│ └── device-agent/ # Go stealth daemon + system tray (submodule)
|
||||
├── docker-compose.yml # Production orchestration
|
||||
├── docker-compose.dev.yml # Dev (air hot-reload)
|
||||
├── docker-compose.dev.yml# Dev (air hot-reload)
|
||||
├── Makefile # up/down/dev/migrate/reset-db
|
||||
├── 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 ce70c3e197
+1
-1
Submodule apps/dashboard-ui updated: 97d91fdc0d...1be8f9f20c
+1
-1
Submodule apps/device-agent updated: f0d0c50487...651da0ad33
+1
-1
Submodule apps/server-core updated: 376c9b8456...b4b4a172c8
@@ -15,11 +15,17 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
networks:
|
||||
- nexusnet
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
command: redis-server --save "" --appendonly no
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
ports:
|
||||
@@ -30,6 +36,11 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
networks:
|
||||
- nexusnet
|
||||
|
||||
@@ -52,6 +63,11 @@ services:
|
||||
- NET_ADMIN
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
dashboard-ui:
|
||||
build:
|
||||
@@ -66,6 +82,11 @@ services:
|
||||
depends_on:
|
||||
- server-core
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
networks:
|
||||
- nexusnet
|
||||
extra_hosts:
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# Android Agent UI Redesign
|
||||
|
||||
**Date:** 2026-07-04
|
||||
**Scope:** NexusGuard Android Agent main activity UI
|
||||
|
||||
## Summary
|
||||
|
||||
Complete redesign of the Android agent main activity UI with a dark theme, green accent color scheme, copy-tap support, and new port forwarding display.
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `app/src/main/res/values/strings.xml` | Added 13 new string resources for UI labels |
|
||||
| `app/src/main/res/values/colors.xml` | Added 8 new color definitions (dark/green theme) |
|
||||
| `app/src/main/res/values/themes.xml` | Applied custom Material3 theme attributes |
|
||||
| `app/src/main/res/layout/activity_main.xml` | Complete layout redesign |
|
||||
| `app/src/main/java/com/datadunia/nexusguard/MainActivity.kt` | Updated Kotlin bindings and logic |
|
||||
|
||||
## Design Changes
|
||||
|
||||
### Theme
|
||||
- Dark background (`#0D1117`) with green accent (`#00C853`)
|
||||
- Material3 `DayNight.NoActionBar` parent theme with custom color overrides
|
||||
- Card borders use `#21262D` stroke color
|
||||
|
||||
### Header
|
||||
- App name "NexusGuard" displayed in green accent color
|
||||
- MaterialSwitch for VPN toggle with ON/OFF labels
|
||||
- Status text below switch (colored by state)
|
||||
|
||||
### Status Device Agent Card
|
||||
- Renamed from "Informasi Tunnel" to "Status Device Agent"
|
||||
- Six data rows: IP Address, Admin Web, Device ID, Handshake Terakhir, Allowed IPs, Transport
|
||||
- All value TextViews are selectable (`textIsSelectable="true"`) and copy-tappable
|
||||
- Device ID displays full text without truncation (no `ellipsize` or `singleLine`)
|
||||
- Admin Web shows fixed domain `https://api-nexus.datadunia.com`
|
||||
|
||||
### Port Forwarding Card (New)
|
||||
- Displays active port forwards from `PortForwarder.getActiveForwards()`
|
||||
- Shows protocol badge (TCP/UDP) and port mapping info
|
||||
- Empty state message when no forwards are active
|
||||
|
||||
### Log Card
|
||||
- Height reduced from 300dp to 250dp
|
||||
- Monospace font at 10sp
|
||||
- Log level filtering: WARN+ in release, all levels in debug
|
||||
|
||||
## Behavior Changes
|
||||
|
||||
### Copy-Tap
|
||||
All value TextViews in the status card respond to tap with clipboard copy and "Disalin" Toast feedback.
|
||||
|
||||
### Log Level Filtering
|
||||
`LogBuffer.initLevel(BuildConfig.DEBUG)` called in `onCreate()` — release builds only store WARN+ entries (50 max), debug builds store all.
|
||||
|
||||
### Port Forward Updates
|
||||
`updatePortForwardCard()` called after every `updateCardFields()` in all tunnel states (CONNECTING, CONNECTED, FAILED).
|
||||
|
||||
## Layout IDs
|
||||
|
||||
| ID | Type | Purpose |
|
||||
|----|------|---------|
|
||||
| `switchVpn` | MaterialSwitch | VPN toggle |
|
||||
| `tvStatus` | TextView | Connection status text |
|
||||
| `cardInfo` | MaterialCardView | Status Device Agent card |
|
||||
| `tvIpAddress` | TextView | Internal IP value |
|
||||
| `tvAdminWeb` | TextView | Admin web domain (was `tvServerEndpoint`) |
|
||||
| `tvDeviceId` | TextView | Device ID value |
|
||||
| `tvHandshake` | TextView | Last handshake time |
|
||||
| `tvAllowedIps` | TextView | Allowed IPs value |
|
||||
| `tvTransport` | TextView | Transport protocol |
|
||||
| `cardPorts` | MaterialCardView | Port Forwarding card |
|
||||
| `portForwardContainer` | LinearLayout | Dynamic port forward items |
|
||||
| `cardLog` | MaterialCardView | Log card |
|
||||
| `tvLogScrollView` | ScrollView | Log scroll container |
|
||||
| `tvLog` | TextView | Log text |
|
||||
| `btnImportConfig` | MaterialButton | Import config button |
|
||||
| `btnToggleLog` | MaterialButton | Log toggle button |
|
||||
| `btnSettings` | ImageButton | Settings icon |
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
- `tvServerEndpoint` renamed to `tvAdminWeb` — any code referencing the old ID must be updated
|
||||
- New string resources added — translations needed for non-Indonesian locales
|
||||
@@ -0,0 +1,64 @@
|
||||
# NexusGuard SD-WAN Suite
|
||||
|
||||
> Enterprise Zero-Trust SD-WAN with WireGuard
|
||||
|
||||
NexusGuard is a production-grade, zero-trust SD-WAN solution built with Go, Vue 3, and WireGuard. It enables stealth VPN tunneling, centralized IPAM, and real-time network isolation via Linux `nftables` — all managed through a futuristic glassmorphism dashboard.
|
||||
|
||||
## Key Highlights
|
||||
|
||||
- **Zero-Attack Surface** — No public registration. Admin accounts created via CLI only.
|
||||
- **Stealth Agent** — WireGuard config injected into memory. No files written to `/etc/wireguard/`.
|
||||
- **Real-time Firewall** — Per-peer nftables rules synced to kernel instantly.
|
||||
- **Cross-Platform** — Agent runs on Linux (daemon), Windows (tray + service), and macOS (tray).
|
||||
|
||||
## Tech Stack
|
||||
|
||||
`Go` · `Vue 3` · `WireGuard` · `PostgreSQL` · `Redis` · `nftables` · `gRPC` · `Docker`
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌─────────────────┐ ┌──────────────┐
|
||||
│ Dashboard │──HTTP──▶│ Server Core │◀─WG────▶│ Device Agent │
|
||||
│ (Vue 3) │ :8080 │ (Go/Gin) │ :51820 │ (Go) │
|
||||
└──────────────┘ │ │ └──────────────┘
|
||||
│ Port 8080: │ │
|
||||
│ - HTTP API │ ┌────┴────┐
|
||||
│ - gRPC Signal │ │ TUN (wg)│
|
||||
│ (cmux) │ │ Memory │
|
||||
└─────────────────┘ └─────────┘
|
||||
```
|
||||
|
||||
## Submodule Knowledge Bases
|
||||
|
||||
Each submodule has its own `AGENTS.md` with detailed architecture, conventions, and anti-patterns:
|
||||
|
||||
| Submodule | AGENTS.md | Scope |
|
||||
|-----------|-----------|-------|
|
||||
| [Server Core](apps/server-core/) | [AGENTS.md](apps/server-core/AGENTS.md) | API handlers, database models, firewall rules, gRPC signaling, WireGuard management |
|
||||
| [Dashboard UI](apps/dashboard-ui/) | [AGENTS.md](apps/dashboard-ui/AGENTS.md) | Vue 3 components, Pinia stores, TailwindCSS styling, API client |
|
||||
| [Device Agent](apps/device-agent/) | [AGENTS.md](apps/device-agent/AGENTS.md) | Go daemon, system tray, memory-injected WireGuard, heartbeat, gRPC signaling |
|
||||
| [Android Agent](apps/android-agent/) | [AGENTS.md](apps/android-agent/AGENTS.md) | Kotlin VPNService, GoBackend tunnel, HTTP heartbeat, port forwarding |
|
||||
|
||||
**Rule**: When working on a submodule, always read its `AGENTS.md` first.
|
||||
|
||||
## Documentation
|
||||
|
||||
| Document | Description |
|
||||
|----------|-------------|
|
||||
| [Architecture](architecture.md) | System architecture, data flow, security model |
|
||||
| [Tech Stack](tech-stack.md) | Technology breakdown per component |
|
||||
| [Features](features.md) | Feature showcase and capabilities |
|
||||
| [Deployment](deployment.md) | Deployment guide (Docker, native, development) |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
git clone https://git.datadunia.com/nexusguard/Nexus-Guard-Suite.git
|
||||
cd Nexus-Guard-Suite
|
||||
bash update.sh
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Private — DataDunia
|
||||
@@ -0,0 +1,194 @@
|
||||
# Architecture Overview
|
||||
|
||||
NexusGuard is a three-component SD-WAN system: a central API server, a web dashboard, and cross-platform device agents. All communication is encrypted. Tunnels are fileless. Access is zero-trust.
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Dashboard (Vue 3) │
|
||||
│ Glassmorphism Web Interface │
|
||||
│ Manages: Nodes, Devices, Rules │
|
||||
└───────────────────────────────────┬─────────────────────────────────┘
|
||||
│ HTTP (port 80)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Nginx Reverse Proxy │
|
||||
│ Routes: /api/ → :8080, / → SPA │
|
||||
└───────────────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Server Core (Go/Gin) │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
|
||||
│ │ HTTP API │ │ gRPC │ │ IPAM │ │ nftables │ │
|
||||
│ │ (20+ │ │ Signaling│ │ Manager │ │ Firewall │ │
|
||||
│ │ handlers)│ │ (cmux) │ │ │ │ │ │
|
||||
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ ▼ ▼ ▼ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ PostgreSQL Database │ │
|
||||
│ │ (wg_servers, devices, rules, users) │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────┐ ┌──────────────────────┐ │
|
||||
│ │ Redis │ │ WireGuard │ │
|
||||
│ │ (Heartbeat TTL) │ │ (wg0 interface) │ │
|
||||
│ └──────────────────────┘ └──────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┴───────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌───────────────────────────┐ ┌───────────────────────────┐
|
||||
│ Device Agent (Linux) │ │ Device Agent (Win/Mac) │
|
||||
│ Systemd Daemon │ │ System Tray │
|
||||
│ Memory-injected WG │ │ Memory-injected WG │
|
||||
└───────────────────────────┘ └───────────────────────────┘
|
||||
```
|
||||
|
||||
## Component Breakdown
|
||||
|
||||
### Server Core
|
||||
|
||||
The central API and VPN hub. Written in Go with Gin framework.
|
||||
|
||||
| Responsibility | Implementation |
|
||||
|----------------|----------------|
|
||||
| API endpoints | 20+ Gin handlers (`api/` directory) |
|
||||
| gRPC signaling | Bidirectional streaming via cmux (port 8080) |
|
||||
| IPAM | IP pool allocation from CIDR per node |
|
||||
| Firewall | nftables rule management (add/remove per peer) |
|
||||
| WireGuard | Interface control, peer sync, config push |
|
||||
| Auth | JWT middleware, admin-only enforcement |
|
||||
| Heartbeat | Config sync, handshake monitoring |
|
||||
|
||||
### Dashboard UI
|
||||
|
||||
Admin web interface. Built with Vue 3 and glassmorphism design system.
|
||||
|
||||
| Responsibility | Implementation |
|
||||
|----------------|----------------|
|
||||
| Node management | Register/edit WireGuard servers |
|
||||
| Device management | CRUD, provisioning tokens, QR codes |
|
||||
| Firewall rules | Per-peer nftables rule editor |
|
||||
| Live telemetry | 10s polling for device health |
|
||||
| Traffic history | Time-range filtering, export |
|
||||
|
||||
### Device Agent
|
||||
|
||||
Stealth VPN daemon. Cross-platform Go binary.
|
||||
|
||||
| Responsibility | Implementation |
|
||||
|----------------|----------------|
|
||||
| Provisioning | HTTP POST with AES-256-GCM encrypted response |
|
||||
| Tunnel | Memory-injected WireGuard (no disk files) |
|
||||
| Heartbeat | HTTP/gRPC, config sync, handshake monitoring |
|
||||
| gRPC | Bidirectional stream for real-time commands |
|
||||
| Self-healing | Exponential backoff reconnection |
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Provisioning Flow
|
||||
|
||||
```
|
||||
1. Admin creates device via Dashboard → API generates registration token
|
||||
2. Agent sends token + HWID to POST /api/v1/provision
|
||||
3. Server validates token, allocates IP from pool
|
||||
4. Server responds with WireGuard config (AES-256-GCM encrypted)
|
||||
5. Agent decrypts config, injects into WireGuard via IpcSet
|
||||
6. Tunnel established — no files written to disk
|
||||
```
|
||||
|
||||
### Heartbeat Flow
|
||||
|
||||
```
|
||||
Every 30 seconds:
|
||||
1. Agent reads last_handshake_time from WireGuard IPC
|
||||
2. Agent POSTs {device_id, tunnel_up, last_handshake} to server
|
||||
3. Server loads Device + WgServer fresh from DB (dynamic, not cached)
|
||||
4. Server computes config_hash = SHA256(tunnelFields) + ":" + SHA256(forwards)
|
||||
- Tunnel fields: server_pub, endpoint, internal_ip, private_key, preshared_key, allowed_ips, dns
|
||||
- Forwards: sorted protocol:publicPort->targetIP:targetPort:ID
|
||||
5. Server responds with full config + config_hash
|
||||
6. Agent compares config_hash with previous → if different → rebuild tunnel/reload forwards
|
||||
```
|
||||
|
||||
> **Design**: Peers (devices) store only their own data (keys, IP, settings). Node data (endpoint, public key) is loaded fresh from DB on every heartbeat. This ensures config always reflects the latest node state without requiring agent restart.
|
||||
|
||||
### Suspend/Resume Flow
|
||||
|
||||
```
|
||||
Suspend:
|
||||
1. Admin clicks "Suspend" in Dashboard
|
||||
2. Dashboard POSTs /api/v1/devices/:id/suspend
|
||||
3. Server updates DB (is_suspended = true)
|
||||
4. Server sends gRPC SuspendCommand to agent
|
||||
5. Server removes WireGuard peer from kernel
|
||||
6. Agent receives command → stops tunnel
|
||||
|
||||
Resume:
|
||||
1. Admin clicks "Resume" in Dashboard
|
||||
2. Server updates DB (is_suspended = false)
|
||||
3. Server re-adds WireGuard peer to kernel
|
||||
4. Server sends gRPC ResumeCommand with full ConfigUpdate
|
||||
5. Agent receives command → rebuilds tunnel
|
||||
```
|
||||
|
||||
## Security Model
|
||||
|
||||
### Zero-Trust Principles
|
||||
|
||||
| Principle | Implementation |
|
||||
|-----------|----------------|
|
||||
| No public registration | `/auth/register` locked; admin via CLI only |
|
||||
| Encrypted provisioning | AES-256-GCM for WireGuard config transfer |
|
||||
| Fileless tunnels | WireGuard config in process memory only |
|
||||
| Hardware binding | HWID (DMI/CPU serial) bound to registration token |
|
||||
| Per-peer isolation | nftables rules per device, default deny |
|
||||
| JWT authentication | All API endpoints require valid token |
|
||||
|
||||
### Trust Boundaries
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Trusted Zone │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │
|
||||
│ │ Server │ │ Database │ │ WireGuard Interface │ │
|
||||
│ │ Core │ │ (PG) │ │ (wg0) │ │
|
||||
│ └──────────┘ └──────────┘ └──────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
Encrypted Channel
|
||||
(AES-256-GCM / WG)
|
||||
│
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Untrusted Zone │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ Device Agent │ │
|
||||
│ │ (Memory-only WireGuard config) │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Port Multiplexing
|
||||
|
||||
Server Core uses cmux to serve both HTTP and gRPC on port 8080:
|
||||
|
||||
```go
|
||||
m := cmux.New(lis)
|
||||
grpcLis := m.MatchWithWriters(cmux.HTTP2MatchHeaderFieldSendSettings(
|
||||
"content-type", "application/grpc",
|
||||
))
|
||||
httpLis := m.Match(cmux.Any())
|
||||
```
|
||||
|
||||
- gRPC matched by `content-type: application/grpc` header
|
||||
- HTTP matched by `Any()` (catch-all)
|
||||
- Single port, single listener, zero extra config
|
||||
@@ -0,0 +1,476 @@
|
||||
# Deployment Guide
|
||||
|
||||
NexusGuard supports three deployment modes: Docker (recommended), native install, and development.
|
||||
|
||||
## Docker Deployment (Recommended)
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker 20.10+
|
||||
- Docker Compose v2
|
||||
- Git
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://git.datadunia.com/nexusguard/Nexus-Guard-Suite.git
|
||||
cd Nexus-Guard-Suite
|
||||
|
||||
# Start all services (auto-generates .env on first run)
|
||||
bash update.sh
|
||||
```
|
||||
|
||||
### First Boot
|
||||
|
||||
On first run, the system automatically:
|
||||
1. Pulls latest code and builds Docker containers
|
||||
2. Generates Local Primary Node WireGuard keys
|
||||
3. Creates database schema via migration
|
||||
|
||||
### Configuration
|
||||
|
||||
Edit `.env` in root directory:
|
||||
|
||||
```bash
|
||||
# Database
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
DB_USER=nexusguard
|
||||
DB_PASSWORD=your_secure_password
|
||||
DB_NAME=nexusguard
|
||||
|
||||
# Redis
|
||||
REDIS_ADDR=redis:6379
|
||||
|
||||
# Security (auto-generated by update.sh)
|
||||
JWT_SECRET=<hex-64-chars>
|
||||
SERVER_SALT=<hex-64-chars>
|
||||
|
||||
# Network
|
||||
NFTABLES_TABLE=nexusguard
|
||||
IPAM_POOL=10.8.0.0/16
|
||||
|
||||
# Server
|
||||
GIN_MODE=release
|
||||
PORT=8080
|
||||
|
||||
# Dashboard
|
||||
VITE_API_BASE_URL=https://api.yourdomain.com/api/v1
|
||||
```
|
||||
|
||||
### Update Commands
|
||||
|
||||
```bash
|
||||
bash update.sh # Smart update (rebuild only if changes)
|
||||
bash update.sh --force # Force rebuild
|
||||
bash update.sh --backup # Backup PostgreSQL before update
|
||||
bash update.sh --no-migrate # Skip database migration
|
||||
```
|
||||
|
||||
### Makefile Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `make up` | Start all services |
|
||||
| `make down` | Stop all services |
|
||||
| `make logs` | Tail all service logs |
|
||||
| `make dev` | Start with hot-reload |
|
||||
| `make migrate` | Run database migration |
|
||||
| `make reset-db` | Reset database to initial state |
|
||||
|
||||
### Create Admin Account
|
||||
|
||||
```bash
|
||||
docker exec -it nexus-guard-suite-server-core-1 ./server-core \
|
||||
-create-admin -user admin -pass "YourSecurePassword123!"
|
||||
```
|
||||
|
||||
### Service Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Docker Compose │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
|
||||
│ │ nginx │ │ server- │ │ postgres │ │
|
||||
│ │ :80/:443 │→ │ core │→ │ :5432 │ │
|
||||
│ │ │ │ :8080 │ │ │ │
|
||||
│ └──────────┘ └──────────┘ └──────────────┘ │
|
||||
│ ↑ ↑ │
|
||||
│ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ dashboard│ │ redis │ │
|
||||
│ │ (static) │ │ :6379 │ │
|
||||
│ └──────────┘ └──────────┘ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Port Mapping
|
||||
|
||||
| Service | Container Port | Host Port |
|
||||
|---------|----------------|-----------|
|
||||
| nginx | 80 | 80 |
|
||||
| nginx | 443 | 443 |
|
||||
| server-core | 8080 | 8080 |
|
||||
| postgres | 5432 | 5432 |
|
||||
| redis | 6379 | 6379 |
|
||||
|
||||
### Volumes
|
||||
|
||||
| Volume | Purpose |
|
||||
|--------|---------|
|
||||
| `postgres_data` | PostgreSQL data persistence |
|
||||
| `redis_data` | Redis data persistence |
|
||||
|
||||
---
|
||||
|
||||
## Native Install
|
||||
|
||||
For production servers without Docker.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
**Debian/Ubuntu:**
|
||||
```bash
|
||||
sudo apt install -y golang nginx postgresql redis-server nftables wireguard-tools
|
||||
```
|
||||
|
||||
**CentOS/Rocky:**
|
||||
```bash
|
||||
sudo dnf install -y golang nginx postgresql-server redis nftables wireguard-tools
|
||||
```
|
||||
|
||||
### Build Binaries
|
||||
|
||||
**Server Core:**
|
||||
```bash
|
||||
cd apps/server-core
|
||||
CGO_ENABLED=0 go build -o ../../bin/server-core .
|
||||
cd ../..
|
||||
```
|
||||
|
||||
**Dashboard UI:**
|
||||
```bash
|
||||
cd apps/dashboard-ui
|
||||
npm install
|
||||
VITE_API_BASE_URL=/api/v1 npm run build
|
||||
cd ../..
|
||||
```
|
||||
|
||||
### Run Installer
|
||||
|
||||
```bash
|
||||
sudo bash nexusguard-install.sh
|
||||
```
|
||||
|
||||
**Options:**
|
||||
```bash
|
||||
sudo bash nexusguard-install.sh --server-port 8080 --web-port 80
|
||||
sudo bash nexusguard-install.sh --db-host 127.0.0.1 --db-pass mypassword
|
||||
```
|
||||
|
||||
**What the installer does:**
|
||||
1. Creates PostgreSQL database and user
|
||||
2. Installs binary to `/usr/local/bin/nexusguard-server`
|
||||
3. Installs dashboard to `/usr/share/nexusguard/dashboard/`
|
||||
4. Creates config at `/etc/nexusguard/nexusguard.conf`
|
||||
5. Runs database migration
|
||||
6. Creates systemd service
|
||||
7. Configures nginx
|
||||
|
||||
### Create Admin Account
|
||||
|
||||
```bash
|
||||
sudo /usr/local/bin/nexusguard-server \
|
||||
-create-admin -user admin -pass "YourSecurePassword123!"
|
||||
```
|
||||
|
||||
### Service Management
|
||||
|
||||
```bash
|
||||
# Start
|
||||
sudo systemctl start nexusguard-server
|
||||
|
||||
# Stop
|
||||
sudo systemctl stop nexusguard-server
|
||||
|
||||
# Status
|
||||
sudo systemctl status nexusguard-server
|
||||
|
||||
# Logs
|
||||
sudo journalctl -u nexusguard-server -f
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Edit `/etc/nexusguard/nexusguard.conf`:
|
||||
|
||||
```bash
|
||||
# Database
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
DB_USER=nexusguard
|
||||
DB_PASSWORD=nexusguard
|
||||
DB_NAME=nexusguard
|
||||
|
||||
# Redis
|
||||
REDIS_ADDR=127.0.0.1:6379
|
||||
|
||||
# Security (auto-generated)
|
||||
JWT_SECRET=<hex-64-chars>
|
||||
SERVER_SALT=<hex-64-chars>
|
||||
|
||||
# Network
|
||||
NFTABLES_TABLE=nexusguard
|
||||
IPAM_POOL=10.8.0.0/16
|
||||
|
||||
# Server
|
||||
GIN_MODE=release
|
||||
PORT=8080
|
||||
```
|
||||
|
||||
### Uninstall
|
||||
|
||||
```bash
|
||||
# Remove files only
|
||||
sudo bash nexusguard-uninstall.sh
|
||||
|
||||
# Also drop database
|
||||
sudo bash nexusguard-uninstall.sh --remove-db
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Mode
|
||||
|
||||
For local development with hot-reload.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.25+
|
||||
- Node.js 24+
|
||||
- PostgreSQL
|
||||
- Redis
|
||||
|
||||
### Setup Database
|
||||
|
||||
1. Create PostgreSQL database:
|
||||
```sql
|
||||
CREATE DATABASE nexusguard;
|
||||
CREATE USER nexusguard WITH PASSWORD 'nexusguard';
|
||||
GRANT ALL PRIVILEGES ON DATABASE nexusguard TO nexusguard;
|
||||
```
|
||||
|
||||
2. Copy environment template:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
3. Edit `.env` with your database credentials.
|
||||
|
||||
### Start Backend
|
||||
|
||||
```bash
|
||||
cd apps/server-core
|
||||
go mod download
|
||||
go run -tags dev .
|
||||
```
|
||||
|
||||
The `-tags dev` flag:
|
||||
- Runs AutoMigrate on startup
|
||||
- Provisions local node
|
||||
- Enables debug logging
|
||||
|
||||
### Start Frontend
|
||||
|
||||
```bash
|
||||
cd apps/dashboard-ui
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Dashboard available at `http://localhost:5173`.
|
||||
|
||||
### Create Admin Account
|
||||
|
||||
```bash
|
||||
cd apps/server-core
|
||||
go run -tags dev . -create-admin -user admin -pass "YourNewSecurePassword123!"
|
||||
```
|
||||
|
||||
### Development Workflow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Development Setup │
|
||||
│ │
|
||||
│ Terminal 1: Backend │
|
||||
│ ┌───────────────────────────────────────────────────┐ │
|
||||
│ │ $ go run -tags dev . │ │
|
||||
│ │ [dev] AutoMigrate complete │ │
|
||||
│ │ [dev] Local node provisioned │ │
|
||||
│ │ [gin] Listening on :8080 │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Terminal 2: Frontend │
|
||||
│ ┌───────────────────────────────────────────────────┐ │
|
||||
│ │ $ npm run dev │ │
|
||||
│ │ │ │
|
||||
│ │ VITE v8.0.0 ready in 300 ms │ │
|
||||
│ │ │ │
|
||||
│ │ ➜ Local: http://localhost:5173/ │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Browser: http://localhost:5173 │
|
||||
│ → Dashboard UI (Vue 3 + Vite) │
|
||||
│ → API calls proxied to :8080 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Agent Installation
|
||||
|
||||
### Linux (Automated)
|
||||
|
||||
```bash
|
||||
# Transfer script
|
||||
scp scripts/install_agent.sh user@target-machine:~
|
||||
|
||||
# Run installer
|
||||
sudo ./install_agent.sh \
|
||||
--server-url "https://api.yourdomain.com" \
|
||||
--token "REG_TOKEN_FROM_DASHBOARD"
|
||||
|
||||
# Verify
|
||||
sudo systemctl status sys-bridge.service
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--binary-name "my-agent"` — Override default binary name
|
||||
|
||||
**What the script does:**
|
||||
1. Detects OS (APT/YUM)
|
||||
2. Installs dependencies (iproute2, curl)
|
||||
3. Downloads correct binary for architecture
|
||||
4. Creates config at `~/.config/nexusguard/nexusguard.conf`
|
||||
5. Creates systemd service
|
||||
6. Starts agent
|
||||
|
||||
### Linux (Manual)
|
||||
|
||||
```bash
|
||||
# Download binary
|
||||
sudo cp nexusguard-device-agent-linux-amd64 /usr/local/bin/sys-bridge
|
||||
sudo chmod +x /usr/local/bin/sys-bridge
|
||||
|
||||
# Create config
|
||||
mkdir -p ~/.config/nexusguard
|
||||
cat > ~/.config/nexusguard/nexusguard.conf <<EOF
|
||||
SERVER_URL=https://api.yourdomain.com
|
||||
REG_TOKEN=YOUR_REG_TOKEN
|
||||
EOF
|
||||
|
||||
# Create systemd service
|
||||
sudo tee /etc/systemd/system/sys-bridge.service > /dev/null <<EOF
|
||||
[Unit]
|
||||
Description=NexusGuard Device Agent
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/sys-bridge
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Start
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now sys-bridge.service
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
1. Build GUI binary:
|
||||
```bash
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build \
|
||||
-ldflags="-s -w -H windowsgui" \
|
||||
-o nexusguard-device-agent-gui.exe .
|
||||
```
|
||||
|
||||
2. Place `wintun.dll` next to executable
|
||||
|
||||
3. Run agent — config auto-created at `%ProgramData%\NexusGuard\nexusguard.conf`
|
||||
|
||||
4. Edit config with `SERVER_URL` and `REG_TOKEN`
|
||||
|
||||
5. Right-click tray icon → Connect
|
||||
|
||||
**Service Management:**
|
||||
- Install as Service — Register Windows service
|
||||
- Uninstall Service — Remove Windows service
|
||||
- Start on Boot — Toggle auto-start
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
docker build -t nexus-device-agent .
|
||||
docker run -d \
|
||||
--name nexus-agent \
|
||||
--restart unless-stopped \
|
||||
--network host \
|
||||
--cap-add NET_ADMIN \
|
||||
--cap-add NET_RAW \
|
||||
-e SERVER_URL=https://api.yourdomain.com \
|
||||
-e REG_TOKEN=YOUR_REG_TOKEN \
|
||||
nexus-device-agent
|
||||
```
|
||||
|
||||
**Required capabilities:**
|
||||
- `NET_ADMIN` — Network interface configuration
|
||||
- `NET_RAW` — Raw socket access (WireGuard)
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
| Issue | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| Agent fails to start | Missing config | Check config file has `SERVER_URL` + `REG_TOKEN` |
|
||||
| Provisioning fails | Invalid token | Generate new token from Dashboard |
|
||||
| Tunnel won't create | Missing kernel module | `sudo modprobe wireguard` |
|
||||
| `ip addr add` fails | Permissions | Run as root or grant `CAP_NET_ADMIN` |
|
||||
| Heartbeat timeout | Network issue | `curl -I <SERVER_URL>` |
|
||||
|
||||
### Log Locations
|
||||
|
||||
| Platform | Location |
|
||||
|----------|----------|
|
||||
| Docker | `docker logs -f nexus-guard-suite-server-core-1` |
|
||||
| Native | `sudo journalctl -u nexusguard-server -f` |
|
||||
| Agent (Linux) | `~/.local/share/nexusguard/logs/` |
|
||||
| Agent (Windows) | `%ProgramData%\NexusGuard\logs\` |
|
||||
| Agent (macOS) | `~/Library/Logs/NexusGuard/` |
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Check WireGuard interface
|
||||
sudo wg show
|
||||
|
||||
# Test server connectivity
|
||||
curl -I https://api.yourdomain.com/api/health
|
||||
|
||||
# Check agent version
|
||||
/usr/local/bin/sys-bridge -version
|
||||
|
||||
# Run agent in foreground
|
||||
sudo /usr/local/bin/sys-bridge -debug
|
||||
```
|
||||
@@ -0,0 +1,299 @@
|
||||
# Features
|
||||
|
||||
NexusGuard provides enterprise-grade SD-WAN capabilities with a focus on security, automation, and ease of use.
|
||||
|
||||
## Zero-Trust Security
|
||||
|
||||
### No Public Registration
|
||||
|
||||
The `/auth/register` endpoint is locked. Admin accounts can only be created via CLI:
|
||||
|
||||
```bash
|
||||
# Docker
|
||||
docker exec -it nexus-guard-suite-server-core-1 ./server-core \
|
||||
-create-admin -user admin -pass "SecurePassword123!"
|
||||
|
||||
# Native
|
||||
sudo /usr/local/bin/nexusguard-server \
|
||||
-create-admin -user admin -pass "SecurePassword123!"
|
||||
```
|
||||
|
||||
**Why?** Eliminates the attack surface of open registration. No bots, no brute force, no unauthorized accounts.
|
||||
|
||||
### Encrypted Provisioning
|
||||
|
||||
Agent provisioning uses AES-256-GCM encryption:
|
||||
|
||||
1. Agent sends registration token + hardware ID (HWID)
|
||||
2. Server generates WireGuard config
|
||||
3. Config encrypted with AES-256-GCM before transmission
|
||||
4. Agent decrypts in memory, never touches disk
|
||||
|
||||
**Why?** WireGuard keys are sensitive. Encryption in transit prevents interception even on compromised networks.
|
||||
|
||||
### Memory-Injected Tunnels
|
||||
|
||||
WireGuard configuration is injected directly into the kernel via `IpcSet`:
|
||||
|
||||
```
|
||||
Traditional: Config file → /etc/wireguard/wg0.conf → wg-quick up wg0
|
||||
NexusGuard: Config bytes → IpcSet() → Tunnel active (no files)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- No config files to steal
|
||||
- No lingering configs after disconnect
|
||||
- Multiple agents can run without conflicts
|
||||
- Clean uninstall = kill process
|
||||
|
||||
### Hardware ID Binding
|
||||
|
||||
Each agent is bound to its hardware via HWID:
|
||||
|
||||
- **Linux:** `/sys/class/dmi/id/product_uuid` or CPU serial
|
||||
- **Windows:** DMI product UUID
|
||||
- **macOS:** IOPlatformSerialNumber
|
||||
|
||||
HWID is included in provisioning request. Server validates before issuing config.
|
||||
|
||||
## Multi-Node Support
|
||||
|
||||
### Geographic Scaling
|
||||
|
||||
Deploy WireGuard servers across multiple regions:
|
||||
|
||||
```
|
||||
Node 1 (Singapore): 10.172.21.0/24
|
||||
Node 2 (Frankfurt): 10.172.22.0/24
|
||||
Node 3 (Virginia): 10.172.23.0/24
|
||||
```
|
||||
|
||||
Each node has its own:
|
||||
- IP pool (CIDR)
|
||||
- Interface address
|
||||
- Peer defaults (DNS, MTU, Keepalive)
|
||||
- Endpoint (IP/Domain + Port)
|
||||
|
||||
### Centralized IPAM
|
||||
|
||||
IP Address Management is centralized in the database:
|
||||
|
||||
1. Admin defines IP pool per node (e.g., `10.172.21.0/24`)
|
||||
2. When device is created, server allocates next available IP
|
||||
3. IP is reserved in database (no duplicates)
|
||||
4. IP is released when device is deleted
|
||||
|
||||
**Why?** Prevents IP conflicts across nodes. Enables static IP assignment for critical devices.
|
||||
|
||||
### Per-Node Defaults
|
||||
|
||||
Each node can have different peer defaults:
|
||||
|
||||
| Setting | Node 1 (SG) | Node 2 (DE) |
|
||||
|---------|-------------|-------------|
|
||||
| DNS | `1.1.1.1` | `8.8.8.8` |
|
||||
| MTU | 1420 | 1280 |
|
||||
| Keepalive | 25s | 0s |
|
||||
| AllowedIPs | `10.172.21.0/24` | `0.0.0.0/0` |
|
||||
|
||||
Devices inherit from their node, with per-device overrides available.
|
||||
|
||||
## Real-Time Firewall
|
||||
|
||||
### nftables Integration
|
||||
|
||||
NexusGuard manages Linux nftables directly:
|
||||
|
||||
```bash
|
||||
# What NexusGuard creates in the kernel
|
||||
table ip nexusguard {
|
||||
set peers_v4 {
|
||||
type ipv4_addr
|
||||
elements = { 10.172.21.2, 10.172.21.3, ... }
|
||||
}
|
||||
chain forward {
|
||||
type filter hook forward priority 0; policy accept;
|
||||
ip daddr @peers_v4 accept
|
||||
ip saddr @peers_v4 accept
|
||||
drop
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Per-Peer Rules
|
||||
|
||||
Each device can have custom firewall rules:
|
||||
|
||||
- **Allow/Block IP ranges** — `192.168.1.0/24`, `10.0.0.1`
|
||||
- **Port filtering** — TCP/UDP port ranges
|
||||
- **Direction control** — Inbound, outbound, or both
|
||||
|
||||
Changes are synced to kernel instantly — no restart required.
|
||||
|
||||
### Default SSH Provisioning
|
||||
|
||||
New peers automatically get SSH access (port 22):
|
||||
|
||||
```go
|
||||
// Automatically added on peer creation
|
||||
AddFirewallRule(peerIP, "0.0.0.0/0", 22, "tcp", "allow")
|
||||
```
|
||||
|
||||
**Why?** Ensures remote access isn't accidentally locked out.
|
||||
|
||||
## Cross-Platform Agent
|
||||
|
||||
### Linux — Systemd Daemon
|
||||
|
||||
```bash
|
||||
# Automated install
|
||||
sudo ./install_agent.sh \
|
||||
--server-url "https://api.yourdomain.com" \
|
||||
--token "REG_TOKEN"
|
||||
|
||||
# Verify
|
||||
sudo systemctl status sys-bridge.service
|
||||
```
|
||||
|
||||
Features:
|
||||
- Runs as root (required for WireGuard)
|
||||
- Auto-restart on failure
|
||||
- Journal logging
|
||||
- Config at `~/.config/nexusguard/nexusguard.conf`
|
||||
|
||||
### Windows — System Tray + Service
|
||||
|
||||
System tray application with service management:
|
||||
|
||||
| Menu Item | Action |
|
||||
|-----------|--------|
|
||||
| Status | Shows Connected/Disconnected |
|
||||
| IP | Shows internal VPN IP |
|
||||
| Connect | Start tunnel |
|
||||
| Disconnect | Stop tunnel |
|
||||
| Install as Service | Register Windows service |
|
||||
| Start on Boot | Toggle auto-start |
|
||||
|
||||
### macOS — System Tray
|
||||
|
||||
System tray application (no service support):
|
||||
|
||||
- Config at `~/Library/Application Support/NexusGuard/nexusguard.conf`
|
||||
- Logs at `~/Library/Logs/NexusGuard/`
|
||||
|
||||
### Self-Healing
|
||||
|
||||
All platforms implement exponential backoff:
|
||||
|
||||
```
|
||||
Failure 1: Wait 30s, retry
|
||||
Failure 2: Wait 60s, retry
|
||||
Failure 3: Wait 120s, retry
|
||||
...
|
||||
Failure N: Wait 300s (max), retry
|
||||
```
|
||||
|
||||
Network drops are handled gracefully — tunnel stays alive, agent reconnects in background.
|
||||
|
||||
## Dashboard
|
||||
|
||||
### Glassmorphism Design
|
||||
|
||||
Futuristic UI with glass-like transparency:
|
||||
|
||||
- Backdrop blur effects
|
||||
- Semi-transparent panels
|
||||
- Gradient accents
|
||||
- Smooth animations
|
||||
|
||||
### Live Telemetry
|
||||
|
||||
Device health polled every 10 seconds:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Device: server-01 │
|
||||
│ Status: ● Online │
|
||||
│ IP: 10.172.21.2 │
|
||||
│ Last Handshake: 15s ago │
|
||||
│ Uptime: 3d 14h 22m │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Server-Side Config Sync
|
||||
|
||||
Config changes (firewall rules, AllowedIPs, DNS, endpoint) are detected via SHA256 hash comparison:
|
||||
|
||||
- Server computes `config_hash = SHA256(tunnelFields) + ":" + SHA256(forwards)`
|
||||
- Agent compares with previous hash → if different → tunnel rebuilds automatically
|
||||
- Works over both HTTP heartbeat (30s) and gRPC (immediate)
|
||||
- Agent never caches config — server loads fresh from DB each heartbeat
|
||||
|
||||
### QR Code Setup
|
||||
|
||||
Generate QR codes for mobile WireGuard clients:
|
||||
|
||||
1. Create device in Dashboard
|
||||
2. Click "Show QR Code"
|
||||
3. Scan with WireGuard app on iOS/Android
|
||||
4. Tunnel ready — no manual config
|
||||
|
||||
### Share Links
|
||||
|
||||
Time-limited config sharing:
|
||||
|
||||
1. Click "Generate Share Link"
|
||||
2. Set expiration (1h, 24h, 7d)
|
||||
3. Share URL with recipient
|
||||
4. Recipient downloads `.conf` file
|
||||
5. Link expires automatically
|
||||
|
||||
## Deployment Flexibility
|
||||
|
||||
### Docker (Recommended)
|
||||
|
||||
```bash
|
||||
git clone https://git.datadunia.com/nexusguard/Nexus-Guard-Suite.git
|
||||
cd Nexus-Guard-Suite
|
||||
bash update.sh
|
||||
```
|
||||
|
||||
One command to start everything. Auto-migration on first boot.
|
||||
|
||||
### Native Install
|
||||
|
||||
For servers without Docker:
|
||||
|
||||
```bash
|
||||
sudo bash nexusguard-install.sh
|
||||
```
|
||||
|
||||
Creates systemd service, nginx config, PostgreSQL database.
|
||||
|
||||
### Development Mode
|
||||
|
||||
Hot-reload for both backend and frontend:
|
||||
|
||||
```bash
|
||||
# Terminal 1: Backend
|
||||
cd apps/server-core
|
||||
go run -tags dev .
|
||||
|
||||
# Terminal 2: Frontend
|
||||
cd apps/dashboard-ui
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Auto-migration on startup. No Docker required.
|
||||
|
||||
## Comparison
|
||||
|
||||
| Feature | NexusGuard | Traditional VPN | Commercial SD-WAN |
|
||||
|---------|------------|-----------------|-------------------|
|
||||
| Zero-trust | ✅ | ❌ | ✅ |
|
||||
| Fileless tunnel | ✅ | ❌ | ❌ |
|
||||
| Multi-platform agent | ✅ | Partial | ✅ |
|
||||
| Real-time firewall | ✅ | ❌ | ✅ |
|
||||
| Self-hosted | ✅ | ✅ | ❌ |
|
||||
| Open source | ✅ | ✅ | ❌ |
|
||||
| Cost | Free | Free | $$$$ |
|
||||
@@ -0,0 +1,134 @@
|
||||
# Technology Stack
|
||||
|
||||
NexusGuard uses a modern, production-grade technology stack. Each component is built with tools optimized for its domain.
|
||||
|
||||
## Backend — Server Core
|
||||
|
||||
| Technology | Version | Purpose |
|
||||
|------------|---------|---------|
|
||||
| Go | 1.25+ | Primary language |
|
||||
| Gin | 1.12 | HTTP framework |
|
||||
| GORM | 1.31 | ORM (PostgreSQL) |
|
||||
| grpc-go | latest | gRPC signaling |
|
||||
| cmux | latest | Port multiplexing (HTTP + gRPC on :8080) |
|
||||
| go-redis | 9.x | Heartbeat TTL cache |
|
||||
| google/nftables | 0.3 | Linux firewall management |
|
||||
| wgctrl | latest | WireGuard interface control |
|
||||
| JWT v5 | latest | Authentication tokens |
|
||||
|
||||
### Why Go?
|
||||
|
||||
- **Static binaries** — No runtime dependencies, easy deployment
|
||||
- **Concurrency** — Goroutines for handling 10000+ concurrent agent connections
|
||||
- **WireGuard ecosystem** — Native Go WireGuard libraries (wgctrl, wireguard-go)
|
||||
- **Performance** — Low memory footprint, fast cold start
|
||||
|
||||
### Why cmux?
|
||||
|
||||
Single port for HTTP and gRPC eliminates:
|
||||
- Firewall rules for multiple ports
|
||||
- Load balancer complexity
|
||||
- Docker port mapping overhead
|
||||
|
||||
## Frontend — Dashboard UI
|
||||
|
||||
| Technology | Version | Purpose |
|
||||
|------------|---------|---------|
|
||||
| Vue | 3.5 | UI framework (Composition API) |
|
||||
| Vite | 8 | Build tool + dev server |
|
||||
| TypeScript | 6.0 | Type safety |
|
||||
| TailwindCSS | 4.3 | Styling (glassmorphism design system) |
|
||||
| Pinia | 2.3 | State management |
|
||||
| Axios | 1.16 | HTTP client |
|
||||
| HeadlessUI | 1.7 | Accessible UI primitives |
|
||||
| Iconify | 5.0 | Icon system |
|
||||
| VueUse | 14.3 | Composition utilities |
|
||||
|
||||
### Why Vue 3?
|
||||
|
||||
- **Composition API** — Better TypeScript support, reusable logic via composables
|
||||
- **`<script setup>`** — Cleaner SFC syntax, less boilerplate
|
||||
- **Ecosystem** — Mature ecosystem with Pinia, Vue Router, VueUse
|
||||
|
||||
### Why TailwindCSS 4?
|
||||
|
||||
- **Design tokens** — `@theme` block for consistent colors, spacing, typography
|
||||
- **Glassmorphism** — Utility classes for backdrop-blur, transparency, gradients
|
||||
- **No PostCSS** — Uses `@tailwindcss/vite` plugin (faster builds)
|
||||
|
||||
## Client — Device Agent
|
||||
|
||||
| Technology | Version | Purpose |
|
||||
|------------|---------|---------|
|
||||
| Go | 1.25+ | Primary language |
|
||||
| wireguard-go | latest | Userspace WireGuard |
|
||||
| fyne.io/systray | latest | Cross-platform system tray |
|
||||
| wintun | latest | Windows WireGuard driver |
|
||||
|
||||
### Cross-Compile Targets
|
||||
|
||||
| Platform | Architecture | Binary |
|
||||
|----------|--------------|--------|
|
||||
| Linux | amd64 | `nexusguard-device-agent-linux-amd64` |
|
||||
| Linux | arm64 | `nexusguard-device-agent-linux-arm64` |
|
||||
| Linux | arm | `nexusguard-device-agent-linux-arm` |
|
||||
| Windows | amd64 | `nexusguard-device-agent-gui.exe` |
|
||||
| macOS | arm64 | `nexusguard-device-agent-darwin-arm64` |
|
||||
| macOS | amd64 | `nexusguard-device-agent-darwin-amd64` |
|
||||
|
||||
### Why Memory-Injected Tunnels?
|
||||
|
||||
Traditional WireGuard setups write config to `/etc/wireguard/`. NexusGuard avoids this:
|
||||
|
||||
1. **Security** — No config files on disk = no file theft risk
|
||||
2. **Stealth** — Tunnel exists only in process memory
|
||||
3. **Cleanup** — Process exit = tunnel gone (no lingering configs)
|
||||
4. **Multi-tenant** — Multiple agents can run without config conflicts
|
||||
|
||||
## Infrastructure
|
||||
|
||||
| Technology | Purpose |
|
||||
|------------|---------|
|
||||
| PostgreSQL | Primary database (wg_servers, devices, rules, users) |
|
||||
| Redis | Heartbeat TTL cache, session tracking |
|
||||
| nginx | Reverse proxy, SPA fallback, API routing |
|
||||
| Docker | Container orchestration |
|
||||
| docker-compose | Multi-service deployment |
|
||||
|
||||
### Why PostgreSQL?
|
||||
|
||||
- **ACID compliance** — Critical for IPAM allocation (no duplicate IPs)
|
||||
- **JSON support** — Flexible config storage
|
||||
- **Maturity** — Battle-tested for production workloads
|
||||
|
||||
### Why Redis?
|
||||
|
||||
- **Heartbeat TTL** — Fast expiry checks for agent health
|
||||
- **Session cache** — gRPC session tracking
|
||||
- **Pub/Sub** — Real-time event distribution (future)
|
||||
|
||||
## Build & CI
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| Go build | Static binary compilation |
|
||||
| Vite build | SPA bundling (vue-tsc + vite) |
|
||||
| Gitea Actions | CI/CD per submodule |
|
||||
| Makefile | Quick commands (up, down, dev, migrate) |
|
||||
|
||||
### Build Flags
|
||||
|
||||
```bash
|
||||
# Server Core (production)
|
||||
CGO_ENABLED=0 go build -o server-core .
|
||||
|
||||
# Device Agent (stripped)
|
||||
go build -ldflags="-s -w" -o device-agent .
|
||||
|
||||
# Dashboard UI
|
||||
VITE_API_BASE_URL=/api/v1 npm run build
|
||||
```
|
||||
|
||||
- `CGO_ENABLED=0` — Static binary, no CGO dependencies
|
||||
- `-ldflags="-s -w"` — Strip debug symbols (~30% smaller binary)
|
||||
- `VITE_API_BASE_URL` — Build-time API endpoint injection
|
||||
@@ -0,0 +1,209 @@
|
||||
# NexusGuard Portfolio Design Spec
|
||||
|
||||
**Date:** 2026-06-21
|
||||
**Status:** Draft
|
||||
**Author:** Sisyphus
|
||||
|
||||
## Overview
|
||||
|
||||
Create a comprehensive technical portfolio for the NexusGuard SD-WAN Suite project, showcasing architecture, technology stack, features, and deployment options. Target audience: technical recruiters, potential clients, and fellow developers.
|
||||
|
||||
## Goals
|
||||
|
||||
1. Present NexusGuard as a production-grade enterprise SD-WAN solution
|
||||
2. Highlight unique selling points (zero-trust, stealth agent, memory-injected tunnels)
|
||||
3. Provide clear architecture overview for technical audience
|
||||
4. Document technology choices and reasoning
|
||||
5. Showcase deployment flexibility (Docker, native, development)
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- Multi-file Markdown portfolio in `docs/portfolio/`
|
||||
- 5 files: README, architecture, tech-stack, features, deployment
|
||||
- ASCII/text-based diagrams (no external dependencies)
|
||||
- Internal navigation links between files
|
||||
- Focus on technical depth over marketing fluff
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Interactive VitePress integration (future enhancement)
|
||||
- Screenshots/demo embeds (requires running instances)
|
||||
- Video content
|
||||
- Multi-language support (Indonesian/English)
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
docs/portfolio/
|
||||
├── README.md # Landing page / entry point
|
||||
├── architecture.md # System architecture + diagrams
|
||||
├── tech-stack.md # Technology breakdown per component
|
||||
├── features.md # Feature showcase
|
||||
└── deployment.md # Deployment guide
|
||||
```
|
||||
|
||||
### Diagram Style
|
||||
|
||||
Use ASCII/text-based diagrams for portability and version control friendliness. Examples:
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌─────────────────┐ ┌──────────────┐
|
||||
│ Dashboard │──HTTP──▶│ Server Core │◀─WG────▶│ Device Agent │
|
||||
│ (Vue 3) │ :8080 │ (Go/Gin) │ :51820 │ (Go) │
|
||||
└──────────────┘ └─────────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
### Content Tone
|
||||
|
||||
Technical and precise. Avoid marketing buzzwords. Focus on:
|
||||
- What it does (functional description)
|
||||
- How it works (architecture/implementation)
|
||||
- Why these choices (technology rationale)
|
||||
|
||||
## File Specifications
|
||||
|
||||
### 1. README.md (Landing Page)
|
||||
|
||||
**Purpose:** Quick overview, entry point to other sections
|
||||
|
||||
**Content:**
|
||||
- Project name + tagline: "Enterprise Zero-Trust SD-WAN with WireGuard"
|
||||
- 2-3 sentence description
|
||||
- Key highlights (3-4 bullet points):
|
||||
- Zero-attack surface (no public registration)
|
||||
- Stealth agent (memory-injected, no disk files)
|
||||
- Real-time nftables isolation
|
||||
- Cross-platform (Linux, Windows, macOS)
|
||||
- Tech badges: Go, Vue 3, WireGuard, PostgreSQL, Docker
|
||||
- Navigation links to other portfolio files
|
||||
|
||||
**Length:** ~100 lines
|
||||
|
||||
### 2. architecture.md (System Architecture)
|
||||
|
||||
**Purpose:** Technical deep-dive into system design
|
||||
|
||||
**Content:**
|
||||
- High-level architecture diagram (ASCII)
|
||||
- Component breakdown:
|
||||
- Server Core: API, gRPC signaling, IPAM, firewall, WireGuard management
|
||||
- Dashboard UI: Management interface, glassmorphism design
|
||||
- Device Agent: Stealth daemon, multi-platform, self-healing
|
||||
- Data flow:
|
||||
- Provisioning flow (HTTP → AES-256-GCM → memory injection)
|
||||
- Heartbeat flow (HTTP/gRPC, config sync, handshake monitoring)
|
||||
- Suspend/Resume flow (gRPC command → tunnel control)
|
||||
- Security model:
|
||||
- Zero-trust (admin CLI only, no public register)
|
||||
- Encrypted provisioning (AES-256-GCM)
|
||||
- Fileless tunnels (WireGuard config in RAM)
|
||||
- Hardware binding (HWID detection)
|
||||
|
||||
**Length:** ~250 lines
|
||||
|
||||
### 3. tech-stack.md (Technology Breakdown)
|
||||
|
||||
**Purpose:** Detailed technology choices per component
|
||||
|
||||
**Content:**
|
||||
- Backend (Server Core):
|
||||
- Go 1.25+, Gin framework, GORM ORM
|
||||
- PostgreSQL, Redis
|
||||
- grpc-go + cmux (port multiplexing)
|
||||
- nftables (google/nftables), WireGuard (wgctrl)
|
||||
- Frontend (Dashboard UI):
|
||||
- Vue 3.5 (Composition API, `<script setup>`)
|
||||
- Vite 8, TypeScript 6.0
|
||||
- TailwindCSS 4.3 (glassmorphism design system)
|
||||
- Pinia 2.3, Axios 1.16, HeadlessUI 1.7
|
||||
- Client (Device Agent):
|
||||
- Go 1.25+, fyne.io/systray
|
||||
- WireGuard (memory-injected via IpcSet)
|
||||
- Cross-compile targets: linux/amd64, arm64, arm; windows/amd64; darwin/arm64, amd64
|
||||
- Infrastructure:
|
||||
- PostgreSQL, Redis, nginx
|
||||
- Docker + docker-compose
|
||||
- Gitea Actions (CI/CD)
|
||||
|
||||
**Length:** ~200 lines
|
||||
|
||||
### 4. features.md (Feature Showcase)
|
||||
|
||||
**Purpose:** Highlight key capabilities
|
||||
|
||||
**Content:**
|
||||
- Zero-Trust Security:
|
||||
- No public registration (admin CLI only)
|
||||
- AES-256-GCM encrypted provisioning
|
||||
- Memory-injected WireGuard (no /etc/wireguard/ files)
|
||||
- Hardware ID binding (HWID)
|
||||
- Multi-Node Support:
|
||||
- Geographic scaling (multiple WG servers)
|
||||
- Centralized IPAM (IP pool allocation per node)
|
||||
- Per-node peer defaults
|
||||
- Real-time Firewall:
|
||||
- nftables integration (Linux kernel)
|
||||
- Per-peer rules (IP/CIDR + port ranges)
|
||||
- Instant sync to kernel (no restart)
|
||||
- Cross-Platform Agent:
|
||||
- Linux (systemd daemon)
|
||||
- Windows (system tray + service)
|
||||
- macOS (system tray)
|
||||
- Self-healing (exponential backoff reconnection)
|
||||
- Dashboard:
|
||||
- Futuristic glassmorphism UI
|
||||
- Live telemetry (10s polling)
|
||||
- QR code for mobile setup
|
||||
- Time-limited share links
|
||||
- Deployment Flexibility:
|
||||
- Docker (recommended)
|
||||
- Native install (systemd + nginx)
|
||||
- Development mode (hot-reload)
|
||||
|
||||
**Length:** ~200 lines
|
||||
|
||||
### 5. deployment.md (Deployment Guide)
|
||||
|
||||
**Purpose:** Practical deployment instructions
|
||||
|
||||
**Content:**
|
||||
- Docker (Recommended):
|
||||
- Quick start (bash update.sh)
|
||||
- Configuration (.env)
|
||||
- Makefile commands (up, down, logs, dev, migrate, reset-db)
|
||||
- Native Install:
|
||||
- Prerequisites (Go, Node.js, PostgreSQL, Redis, nginx, nftables, wireguard-tools)
|
||||
- Build binaries
|
||||
- Run installer (nexusguard-install.sh)
|
||||
- Create admin account
|
||||
- Development:
|
||||
- Local setup (database + .env)
|
||||
- Hot-reload (go run -tags dev, npm run dev)
|
||||
- Database migration
|
||||
- Agent Installation:
|
||||
- Linux (automated install_agent.sh)
|
||||
- Windows (manual + service setup)
|
||||
- Docker container
|
||||
|
||||
**Length:** ~250 lines
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. All 5 files created in `docs/portfolio/`
|
||||
2. Each file is self-contained (can be read independently)
|
||||
3. Internal links work between files
|
||||
4. ASCII diagrams render correctly in Markdown viewers
|
||||
5. Content is accurate and matches actual codebase
|
||||
6. No broken links or references
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Use existing README content as base (avoid duplication)
|
||||
- Extract key information from AGENTS.md files
|
||||
- Verify technical details against actual code
|
||||
- Keep diagrams simple and readable
|
||||
- Use consistent Markdown formatting across all files
|
||||
@@ -49,6 +49,7 @@ Prerequisites:
|
||||
- nginx installed and running
|
||||
- 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,10 +32,28 @@ 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)
|
||||
@@ -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