Compare commits
80 Commits
v0.6.0
...
transport-
| Author | SHA1 | Date | |
|---|---|---|---|
|
4e3e1b9104
|
|||
|
41bcb65e16
|
|||
|
0ba311b25d
|
|||
|
c22aa0cb45
|
|||
|
25e04b1b80
|
|||
|
e508f63b83
|
|||
|
f28ba4d69e
|
|||
|
62b5d6a4d2
|
|||
|
8325666301
|
|||
|
f80d50c27b
|
|||
|
f6b5f3ee82
|
|||
|
14d62efd17
|
|||
|
c9f7f12a03
|
|||
|
548ec55248
|
|||
|
03753bf9bc
|
|||
|
012c0eec62
|
|||
|
6fe193d75a
|
|||
|
6b011144cf
|
|||
|
c26c50cc3a
|
|||
|
b972d87e91
|
|||
|
82bfa43240
|
|||
|
43aa622846
|
|||
|
97353d430b
|
|||
|
1d3a969742
|
|||
|
bad92193a3
|
|||
|
6560949ec4
|
|||
|
d6152ccd85
|
|||
|
e6fd7188d2
|
|||
|
bc44ed2aaa
|
|||
|
e5ac206e5c
|
|||
|
b74012099c
|
|||
|
ae1d290fa7
|
|||
|
562f850b8f
|
|||
|
ff0088644e
|
|||
|
aee52bf56c
|
|||
|
fd951a10f8
|
|||
|
11d4c6407e
|
|||
|
1be94dc0ba
|
|||
|
b30a1ba3eb
|
|||
|
9f755aec21
|
|||
|
1a579bc716
|
|||
|
8124d95192
|
|||
|
a59dca45a7
|
|||
|
1106215241
|
|||
|
ee61747e20
|
|||
|
4c1c819e42
|
|||
|
078fa0f17d
|
|||
|
876476cff5
|
|||
|
899b08e92e
|
|||
|
eec73d2d93
|
|||
|
6888eccc62
|
|||
|
6fa0187ae1
|
|||
|
2ba3f059a1
|
|||
|
63454b3bbb
|
|||
|
9f36e37f94
|
|||
|
73b982c6e0
|
|||
|
22c54f2252
|
|||
|
d68a6cfb9c
|
|||
|
8267123fb5
|
|||
|
a540b64331
|
|||
|
b0d2d4778f
|
|||
|
6cb90d3c4b
|
|||
|
009755c981
|
|||
|
595430c808
|
|||
|
e9b647d5a7
|
|||
|
7d57888696
|
|||
|
cbe2df02ad
|
|||
|
ff893945e9
|
|||
|
b705427bc9
|
|||
|
88083be84e
|
|||
|
4ea1fd1f28
|
|||
|
1ad1f3cfd2
|
|||
|
1281731a81
|
|||
|
8d97c29b19
|
|||
|
f8712b35b8
|
|||
|
0051405033
|
|||
|
3a14394640
|
|||
|
2faf1fb5a2
|
|||
|
e51baa8673
|
|||
|
21a0dafae6
|
@@ -6,7 +6,6 @@ bin/
|
|||||||
*.dylib
|
*.dylib
|
||||||
|
|
||||||
# Go modules' cache
|
# Go modules' cache
|
||||||
/pkg/
|
|
||||||
vendor/
|
vendor/
|
||||||
|
|
||||||
# Local test/coverage/log artifacts
|
# Local test/coverage/log artifacts
|
||||||
|
|||||||
29
.gitea/workflows/benchmark.yml
Normal file
29
.gitea/workflows/benchmark.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: Go Benchmarks
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main", "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main", "master" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
benchmark:
|
||||||
|
name: Run Benchmarks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Source
|
||||||
|
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
|
||||||
|
with:
|
||||||
|
go-version: '1.25'
|
||||||
|
|
||||||
|
- name: Setup Task
|
||||||
|
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2
|
||||||
|
with:
|
||||||
|
version: '3.46.3'
|
||||||
|
|
||||||
|
- name: Run Benchmarks
|
||||||
|
run: task bench
|
||||||
|
|
||||||
55
.gitea/workflows/build-test.yml
Normal file
55
.gitea/workflows/build-test.yml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
name: Go Build Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build (${{ matrix.goos }}, ${{ matrix.goarch }})
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
goos: [linux, windows, darwin, freebsd]
|
||||||
|
goarch: [amd64, arm64, arm]
|
||||||
|
include:
|
||||||
|
- goos: js
|
||||||
|
goarch: wasm
|
||||||
|
exclude:
|
||||||
|
- goos: darwin
|
||||||
|
goarch: arm
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Source
|
||||||
|
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
|
||||||
|
with:
|
||||||
|
go-version: '1.25'
|
||||||
|
|
||||||
|
- name: Setup Task
|
||||||
|
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2
|
||||||
|
with:
|
||||||
|
version: '3.46.3'
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
GOOS: ${{ matrix.goos }}
|
||||||
|
GOARCH: ${{ matrix.goarch }}
|
||||||
|
GOARM: ${{ matrix.goarch == 'arm' && '6' || '' }}
|
||||||
|
CGO_ENABLED: '0'
|
||||||
|
run: |
|
||||||
|
if [ "${{ matrix.goos }}" = "js" ] && [ "${{ matrix.goarch }}" = "wasm" ]; then
|
||||||
|
task build-wasm
|
||||||
|
else
|
||||||
|
task build
|
||||||
|
fi
|
||||||
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
name: Go Build Multi-Platform
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "main", "master" ]
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main", "master" ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
goos: [linux, windows, darwin, freebsd]
|
|
||||||
goarch: [amd64, arm64, arm]
|
|
||||||
include:
|
|
||||||
- goos: js
|
|
||||||
goarch: wasm
|
|
||||||
exclude:
|
|
||||||
- goos: darwin
|
|
||||||
goarch: arm
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
outputs:
|
|
||||||
build_complete: ${{ steps.build_step.outcome == 'success' }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
|
||||||
with:
|
|
||||||
go-version: '1.25'
|
|
||||||
|
|
||||||
- name: Setup Task
|
|
||||||
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
|
|
||||||
with:
|
|
||||||
version: '3.46.3'
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
id: build_step
|
|
||||||
env:
|
|
||||||
GOOS: ${{ matrix.goos }}
|
|
||||||
GOARCH: ${{ matrix.goarch }}
|
|
||||||
GOARM: ${{ matrix.goarch == 'arm' && '6' || '' }}
|
|
||||||
CGO_ENABLED: '0'
|
|
||||||
run: |
|
|
||||||
output_name="reticulum-go-${GOOS}-${GOARCH}"
|
|
||||||
if [ "$GOOS" = "js" ] && [ "$GOARCH" = "wasm" ]; then
|
|
||||||
task build-wasm
|
|
||||||
output_name+=".wasm"
|
|
||||||
mv bin/reticulum-go.wasm "${output_name}"
|
|
||||||
else
|
|
||||||
task build
|
|
||||||
if [ "$GOOS" = "windows" ]; then
|
|
||||||
output_name+=".exe"
|
|
||||||
fi
|
|
||||||
mv bin/reticulum-go "${output_name}"
|
|
||||||
fi
|
|
||||||
echo "Built: ${output_name}"
|
|
||||||
|
|
||||||
- name: Calculate SHA256 Checksum
|
|
||||||
run: |
|
|
||||||
output_name="reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}"
|
|
||||||
if [ "${{ matrix.goos }}" = "windows" ]; then
|
|
||||||
output_name+=".exe"
|
|
||||||
elif [ "${{ matrix.goos }}" = "js" ] && [ "${{ matrix.goarch }}" = "wasm" ]; then
|
|
||||||
output_name+=".wasm"
|
|
||||||
fi
|
|
||||||
BINARY_PATH="${output_name}" task checksum
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
|
||||||
uses: https://git.quad4.io/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3.2.1
|
|
||||||
with:
|
|
||||||
name: reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}
|
|
||||||
path: |
|
|
||||||
reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}*
|
|
||||||
|
|
||||||
release:
|
|
||||||
name: Create Release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Download All Build Artifacts
|
|
||||||
uses: https://git.quad4.io/actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a
|
|
||||||
with:
|
|
||||||
path: ./release-assets
|
|
||||||
|
|
||||||
- name: List downloaded files (for debugging)
|
|
||||||
run: ls -R ./release-assets
|
|
||||||
|
|
||||||
- name: Create Gitea Release
|
|
||||||
uses: https://git.quad4.io/actions/gitea-release-action@4875285c0950474efb7ca2df55233c51333eeb74
|
|
||||||
with:
|
|
||||||
files: ./release-assets/*/*
|
|
||||||
@@ -52,6 +52,12 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-${{ matrix.goarch }}-
|
${{ runner.os }}-go-${{ matrix.goarch }}-
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
||||||
|
uses: https://git.quad4.io/actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: task test
|
run: task test
|
||||||
|
|
||||||
@@ -59,31 +65,20 @@ jobs:
|
|||||||
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
||||||
run: task test-race
|
run: task test-race
|
||||||
|
|
||||||
- name: Test build (ensure compilation works)
|
- name: Run Resource Leak tests (Linux AMD64 only)
|
||||||
run: |
|
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
||||||
echo "Testing build for current platform (${{ matrix.os }}, ${{ matrix.goarch }})..."
|
run: task test-leaks
|
||||||
task build
|
|
||||||
|
|
||||||
- name: Test binary execution
|
- name: Run Network Simulation tests (Linux AMD64 only)
|
||||||
run: |
|
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
||||||
echo "Testing binary execution on (${{ matrix.os }}, ${{ matrix.goarch }})..."
|
run: task test-network
|
||||||
timeout 5s ./bin/reticulum-go || echo "Binary started successfully (timeout expected)"
|
|
||||||
|
|
||||||
- name: Test cross-compilation (AMD64 runners only)
|
- name: Run Fuzz tests (Linux AMD64 only)
|
||||||
if: matrix.goarch == 'amd64'
|
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
||||||
run: |
|
run: task test-fuzz
|
||||||
echo "Testing ARM64 cross-compilation from AMD64..."
|
|
||||||
GOOS=linux GOARCH=arm64 task build
|
|
||||||
env:
|
|
||||||
GOOS: linux
|
|
||||||
GOARCH: arm64
|
|
||||||
|
|
||||||
- name: Test ARMv6 cross-compilation (AMD64 runners only)
|
- name: Run WebAssembly tests (Linux AMD64 only)
|
||||||
if: matrix.goarch == 'amd64'
|
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
||||||
run: |
|
run: |
|
||||||
echo "Testing ARMv6 cross-compilation from AMD64..."
|
chmod +x misc/wasm/go_js_wasm_exec
|
||||||
GOOS=linux GOARCH=arm GOARM=6 task build
|
task test-wasm
|
||||||
env:
|
|
||||||
GOOS: linux
|
|
||||||
GOARCH: arm
|
|
||||||
GOARM: 6
|
|
||||||
|
|||||||
97
.gitea/workflows/publish.yml
Normal file
97
.gitea/workflows/publish.yml
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
name: Go Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build (${{ matrix.goos }}, ${{ matrix.goarch }})
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
goos: [linux, windows, darwin, freebsd]
|
||||||
|
goarch: [amd64, arm64, arm]
|
||||||
|
include:
|
||||||
|
- goos: js
|
||||||
|
goarch: wasm
|
||||||
|
exclude:
|
||||||
|
- goos: darwin
|
||||||
|
goarch: arm
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Source
|
||||||
|
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
|
||||||
|
with:
|
||||||
|
go-version: '1.25'
|
||||||
|
|
||||||
|
- name: Setup Task
|
||||||
|
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2
|
||||||
|
with:
|
||||||
|
version: '3.46.3'
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
GOOS: ${{ matrix.goos }}
|
||||||
|
GOARCH: ${{ matrix.goarch }}
|
||||||
|
GOARM: ${{ matrix.goarch == 'arm' && '6' || '' }}
|
||||||
|
CGO_ENABLED: '0'
|
||||||
|
run: |
|
||||||
|
output_name="reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}"
|
||||||
|
if [ "${{ matrix.goos }}" = "js" ] && [ "${{ matrix.goarch }}" = "wasm" ]; then
|
||||||
|
task build-wasm
|
||||||
|
output_name+=".wasm"
|
||||||
|
mv bin/reticulum-go.wasm "${output_name}"
|
||||||
|
else
|
||||||
|
task build
|
||||||
|
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||||||
|
output_name+=".exe"
|
||||||
|
fi
|
||||||
|
mv bin/reticulum-go "${output_name}"
|
||||||
|
fi
|
||||||
|
echo "Built: ${output_name}"
|
||||||
|
|
||||||
|
- name: Calculate SHA256 Checksum
|
||||||
|
run: |
|
||||||
|
output_name="reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}"
|
||||||
|
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||||||
|
output_name+=".exe"
|
||||||
|
elif [ "${{ matrix.goos }}" = "js" ] && [ "${{ matrix.goarch }}" = "wasm" ]; then
|
||||||
|
output_name+=".wasm"
|
||||||
|
fi
|
||||||
|
BINARY_PATH="${output_name}" task checksum
|
||||||
|
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: https://git.quad4.io/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5
|
||||||
|
with:
|
||||||
|
name: reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
|
path: |
|
||||||
|
reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}*
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Create Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
steps:
|
||||||
|
- name: Download All Build Artifacts
|
||||||
|
uses: https://git.quad4.io/actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a
|
||||||
|
with:
|
||||||
|
path: ./release-assets
|
||||||
|
|
||||||
|
- name: Create Gitea Release
|
||||||
|
uses: https://git.quad4.io/actions/gitea-release-action@4875285c0950474efb7ca2df55233c51333eeb74
|
||||||
|
with:
|
||||||
|
files: ./release-assets/*/*
|
||||||
|
|
||||||
@@ -8,13 +8,14 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
generate-sbom:
|
generate-sbom:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: ${{ github.ref }}
|
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||||
@@ -29,26 +30,24 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: task deps
|
run: task deps
|
||||||
|
|
||||||
- name: Download Trivy
|
- name: Install Trivy
|
||||||
run: |
|
run: task trivy:install
|
||||||
curl -L -o /tmp/trivy.deb https://git.quad4.io/Quad4-Extra/assets/raw/commit/90fdcea1bb71d91df2de6ff2e3897f278413f300/bin/trivy_0.68.2_Linux-64bit.deb
|
|
||||||
sudo dpkg -i /tmp/trivy.deb || sudo apt-get install -f -y
|
|
||||||
|
|
||||||
- name: Generate SBOM
|
- name: Generate SBOM
|
||||||
run: |
|
run: task sbom
|
||||||
mkdir -p sbom
|
|
||||||
trivy fs --format spdx-json --include-dev-deps --output sbom/sbom.spdx.json .
|
|
||||||
trivy fs --format cyclonedx --include-dev-deps --output sbom/sbom.cyclonedx.json .
|
|
||||||
|
|
||||||
- name: Commit and Push Changes
|
- name: Commit and Push Changes
|
||||||
run: |
|
run: |
|
||||||
git config --global user.name "Gitea Action"
|
git config --global user.name "Gitea Action"
|
||||||
git config --global user.email "actions@noreply.quad4.io"
|
git config --global user.email "actions@noreply.quad4.io"
|
||||||
git remote set-url origin https://${{ secrets.GITEA_TOKEN }}@git.quad4.io/${{ github.repository }}.git
|
git remote set-url origin https://${{ secrets.GITEA_TOKEN }}@git.quad4.io/${{ github.repository }}.git
|
||||||
git fetch origin main
|
git fetch origin main || git fetch origin master
|
||||||
git checkout main
|
git checkout main || git checkout master
|
||||||
git add sbom/
|
git add sbom/
|
||||||
git diff --quiet && git diff --staged --quiet || (git commit -m "Auto-update SBOM [skip ci]" && git push origin main)
|
if ! git diff --quiet || ! git diff --staged --quiet; then
|
||||||
|
git commit -m "Auto-update SBOM [skip ci]"
|
||||||
|
git push origin main || git push origin master
|
||||||
|
fi
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -15,7 +15,8 @@ logs/
|
|||||||
*.json
|
*.json
|
||||||
|
|
||||||
# Example files, not adding them just yet.
|
# Example files, not adding them just yet.
|
||||||
examples/
|
/examples/*
|
||||||
|
!/examples/wasm/
|
||||||
|
|
||||||
# OS / Editor files
|
# OS / Editor files
|
||||||
.DS_Store # macOS Finder metadata
|
.DS_Store # macOS Finder metadata
|
||||||
@@ -27,4 +28,5 @@ Thumbs.db # Windows Explorer thumbnail cache
|
|||||||
|
|
||||||
# Swap and test binaries
|
# Swap and test binaries
|
||||||
*.swp # Swap files (e.g. vim)
|
*.swp # Swap files (e.g. vim)
|
||||||
*.test # Go test binaries
|
*.test # Go test binaries
|
||||||
|
test/compat/
|
||||||
14
CONTRIBUTORS
Normal file
14
CONTRIBUTORS
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
CONTRIBUTORS
|
||||||
|
|
||||||
|
This file lists all contributors to the Reticulum-Go project.
|
||||||
|
|
||||||
|
Sudo-Ivan
|
||||||
|
Total commits: 442
|
||||||
|
First contribution: 2024-12-30
|
||||||
|
Last contribution: 2026-01-01
|
||||||
|
|
||||||
|
|
||||||
|
Mike Coles
|
||||||
|
Total commits: 1
|
||||||
|
First contribution: 2025-08-07
|
||||||
|
Last contribution: 2025-08-07
|
||||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2024-2025 Sudo-Ivan / Quad4.io
|
Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
purpose with or without fee is hereby granted.
|
purpose with or without fee is hereby granted.
|
||||||
|
|||||||
156
Makefile
156
Makefile
@@ -1,156 +0,0 @@
|
|||||||
GOCMD=go
|
|
||||||
GOBUILD=$(GOCMD) build
|
|
||||||
GOBUILD_DEBUG=$(GOCMD) build
|
|
||||||
GOBUILD_RELEASE=CGO_ENABLED=0 $(GOCMD) build -ldflags="-s -w"
|
|
||||||
GOBUILD_EXPERIMENTAL=GOEXPERIMENT=greenteagc $(GOCMD) build
|
|
||||||
GOCLEAN=$(GOCMD) clean
|
|
||||||
GOTEST=$(GOCMD) test
|
|
||||||
GOGET=$(GOCMD) get
|
|
||||||
GOMOD=$(GOCMD) mod
|
|
||||||
BINARY_NAME=reticulum-go
|
|
||||||
BINARY_UNIX=$(BINARY_NAME)_unix
|
|
||||||
|
|
||||||
BUILD_DIR=bin
|
|
||||||
|
|
||||||
MAIN_PACKAGE=./cmd/reticulum-go
|
|
||||||
|
|
||||||
ALL_PACKAGES=$$(go list ./... | grep -v /vendor/)
|
|
||||||
|
|
||||||
.PHONY: all build build-experimental experimental release debug lint bench bench-experimental bench-compare clean test coverage deps help tinygo-build tinygo-wasm run
|
|
||||||
|
|
||||||
all: clean deps build test
|
|
||||||
|
|
||||||
build:
|
|
||||||
@mkdir -p $(BUILD_DIR)
|
|
||||||
$(GOBUILD_RELEASE) -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_PACKAGE)
|
|
||||||
|
|
||||||
debug:
|
|
||||||
@mkdir -p $(BUILD_DIR)
|
|
||||||
$(GOBUILD_DEBUG) -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_PACKAGE)
|
|
||||||
|
|
||||||
build-experimental:
|
|
||||||
@mkdir -p $(BUILD_DIR)
|
|
||||||
$(GOBUILD_EXPERIMENTAL) -o $(BUILD_DIR)/$(BINARY_NAME)-experimental $(MAIN_PACKAGE)
|
|
||||||
|
|
||||||
experimental: build-experimental
|
|
||||||
|
|
||||||
release:
|
|
||||||
@mkdir -p $(BUILD_DIR)
|
|
||||||
$(GOBUILD_RELEASE) -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_PACKAGE)
|
|
||||||
|
|
||||||
lint:
|
|
||||||
revive -config revive.toml -formatter friendly ./pkg/* ./cmd/* ./internal/*
|
|
||||||
|
|
||||||
bench:
|
|
||||||
$(GOTEST) -bench=. -benchmem ./...
|
|
||||||
|
|
||||||
bench-experimental:
|
|
||||||
GOEXPERIMENT=greenteagc $(GOTEST) -bench=. -benchmem ./...
|
|
||||||
|
|
||||||
bench-compare: bench bench-experimental
|
|
||||||
|
|
||||||
clean:
|
|
||||||
@rm -rf $(BUILD_DIR)
|
|
||||||
$(GOCLEAN)
|
|
||||||
|
|
||||||
test:
|
|
||||||
$(GOTEST) -v $(ALL_PACKAGES)
|
|
||||||
|
|
||||||
coverage:
|
|
||||||
$(GOTEST) -coverprofile=coverage.out $(ALL_PACKAGES)
|
|
||||||
$(GOCMD) tool cover -html=coverage.out
|
|
||||||
|
|
||||||
deps:
|
|
||||||
@GOPROXY=$${GOPROXY:-https://proxy.golang.org,direct}; \
|
|
||||||
export GOPROXY; \
|
|
||||||
$(GOMOD) download
|
|
||||||
@GOPROXY=$${GOPROXY:-https://proxy.golang.org,direct}; \
|
|
||||||
export GOPROXY; \
|
|
||||||
$(GOMOD) verify
|
|
||||||
|
|
||||||
build-linux:
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 $(MAIN_PACKAGE)
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 $(MAIN_PACKAGE)
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm $(MAIN_PACKAGE)
|
|
||||||
|
|
||||||
build-windows:
|
|
||||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe $(MAIN_PACKAGE)
|
|
||||||
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-arm64.exe $(MAIN_PACKAGE)
|
|
||||||
|
|
||||||
build-darwin:
|
|
||||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 $(MAIN_PACKAGE)
|
|
||||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 $(MAIN_PACKAGE)
|
|
||||||
|
|
||||||
build-freebsd:
|
|
||||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-amd64 $(MAIN_PACKAGE)
|
|
||||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=386 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-386 $(MAIN_PACKAGE)
|
|
||||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-arm64 $(MAIN_PACKAGE)
|
|
||||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-arm $(MAIN_PACKAGE)
|
|
||||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=riscv64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-riscv64 $(MAIN_PACKAGE)
|
|
||||||
|
|
||||||
build-openbsd:
|
|
||||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-amd64 $(MAIN_PACKAGE)
|
|
||||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=386 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-386 $(MAIN_PACKAGE)
|
|
||||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-arm64 $(MAIN_PACKAGE)
|
|
||||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-arm $(MAIN_PACKAGE)
|
|
||||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=ppc64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-ppc64 $(MAIN_PACKAGE)
|
|
||||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=riscv64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-riscv64 $(MAIN_PACKAGE)
|
|
||||||
|
|
||||||
build-netbsd:
|
|
||||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-amd64 $(MAIN_PACKAGE)
|
|
||||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=386 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-386 $(MAIN_PACKAGE)
|
|
||||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm64 $(MAIN_PACKAGE)
|
|
||||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm $(MAIN_PACKAGE)
|
|
||||||
|
|
||||||
build-arm:
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-arm $(MAIN_PACKAGE)
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-arm64 $(MAIN_PACKAGE)
|
|
||||||
|
|
||||||
build-riscv:
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-riscv64 $(MAIN_PACKAGE)
|
|
||||||
|
|
||||||
build-all: build-linux build-windows build-darwin build-freebsd build-openbsd build-netbsd build-arm build-riscv
|
|
||||||
|
|
||||||
run:
|
|
||||||
$(GOCMD) run $(MAIN_PACKAGE)
|
|
||||||
|
|
||||||
tinygo-build:
|
|
||||||
@mkdir -p $(BUILD_DIR)
|
|
||||||
tinygo build -o $(BUILD_DIR)/$(BINARY_NAME)-tinygo -size short $(MAIN_PACKAGE)
|
|
||||||
|
|
||||||
tinygo-wasm:
|
|
||||||
@mkdir -p $(BUILD_DIR)
|
|
||||||
tinygo build -target wasm -o $(BUILD_DIR)/$(BINARY_NAME).wasm $(MAIN_PACKAGE)
|
|
||||||
|
|
||||||
install:
|
|
||||||
$(GOMOD) download
|
|
||||||
|
|
||||||
help:
|
|
||||||
@echo "Available targets:"
|
|
||||||
@echo " all - Clean, download dependencies, build and test"
|
|
||||||
@echo " build - Build release binary (no debug symbols, static)"
|
|
||||||
@echo " debug - Build debug binary"
|
|
||||||
@echo " build-experimental - Build binary with experimental features (GOEXPERIMENT=greenteagc)"
|
|
||||||
@echo " experimental - Alias for build-experimental"
|
|
||||||
@echo " release - Build stripped static binary for release (alias for build)"
|
|
||||||
@echo " lint - Run revive linter"
|
|
||||||
@echo " bench - Run benchmarks with standard GC"
|
|
||||||
@echo " bench-experimental - Run benchmarks with experimental GC"
|
|
||||||
@echo " bench-compare - Run benchmarks with both GC settings"
|
|
||||||
@echo " clean - Remove build artifacts"
|
|
||||||
@echo " test - Run tests"
|
|
||||||
@echo " coverage - Generate test coverage report"
|
|
||||||
@echo " deps - Download dependencies"
|
|
||||||
@echo " build-linux - Build for Linux (amd64, arm64, arm)"
|
|
||||||
@echo " build-windows- Build for Windows (amd64, arm64)"
|
|
||||||
@echo " build-darwin - Build for MacOS (amd64, arm64)"
|
|
||||||
@echo " build-freebsd- Build for FreeBSD (amd64, 386, arm64, arm, riscv64)"
|
|
||||||
@echo " build-openbsd- Build for OpenBSD (amd64, 386, arm64, arm, ppc64, riscv64)"
|
|
||||||
@echo " build-netbsd - Build for NetBSD (amd64, 386, arm64, arm)"
|
|
||||||
@echo " build-arm - Build for ARM architectures (arm, arm64)"
|
|
||||||
@echo " build-riscv - Build for RISC-V architecture (riscv64)"
|
|
||||||
@echo " build-all - Build for all platforms and architectures"
|
|
||||||
@echo " run - Run with go run"
|
|
||||||
@echo " tinygo-build - Build binary with TinyGo compiler"
|
|
||||||
@echo " tinygo-wasm - Build WebAssembly binary with TinyGo compiler"
|
|
||||||
@echo " install - Install dependencies"
|
|
||||||
@@ -145,6 +145,7 @@ The project uses [Task](https://taskfile.dev/) for all development and build ope
|
|||||||
| build-linux | Build for Linux (amd64, arm64, arm, riscv64) |
|
| build-linux | Build for Linux (amd64, arm64, arm, riscv64) |
|
||||||
| build-all | Build for all Linux architectures |
|
| build-all | Build for all Linux architectures |
|
||||||
| build-wasm | Build WebAssembly binary with standard Go compiler |
|
| build-wasm | Build WebAssembly binary with standard Go compiler |
|
||||||
|
| test-wasm | Run WebAssembly tests using Node.js |
|
||||||
| run | Run with go run |
|
| run | Run with go run |
|
||||||
| tinygo-build | Build binary with TinyGo compiler |
|
| tinygo-build | Build binary with TinyGo compiler |
|
||||||
| tinygo-wasm | Build WebAssembly binary with TinyGo |
|
| tinygo-wasm | Build WebAssembly binary with TinyGo |
|
||||||
@@ -179,6 +180,12 @@ Build WebAssembly binary with standard Go compiler:
|
|||||||
task build-wasm
|
task build-wasm
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Run WebAssembly unit tests (requires Node.js):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
task test-wasm
|
||||||
|
```
|
||||||
|
|
||||||
Build with TinyGo:
|
Build with TinyGo:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
425
Taskfile.yml
425
Taskfile.yml
@@ -1,4 +1,6 @@
|
|||||||
version: '3'
|
version: '3'
|
||||||
|
env:
|
||||||
|
GOPRIVATE: git.quad4.io
|
||||||
|
|
||||||
vars:
|
vars:
|
||||||
GOCMD: go
|
GOCMD: go
|
||||||
@@ -75,8 +77,32 @@ tasks:
|
|||||||
- gosec ./...
|
- gosec ./...
|
||||||
|
|
||||||
check:
|
check:
|
||||||
desc: Run fmt-check, vet, and lint
|
desc: Run fmt-check, vet, lint, test-short, and scan with summary
|
||||||
deps: [fmt-check, vet, lint]
|
cmds:
|
||||||
|
- |
|
||||||
|
FAILED_TASKS=""
|
||||||
|
FAIL_COUNT=0
|
||||||
|
TOTAL_TASKS=5
|
||||||
|
|
||||||
|
echo "--- Running all checks ---"
|
||||||
|
task fmt-check || { FAILED_TASKS="$FAILED_TASKS fmt-check"; FAIL_COUNT=$((FAIL_COUNT + 1)); }
|
||||||
|
task vet || { FAILED_TASKS="$FAILED_TASKS vet"; FAIL_COUNT=$((FAIL_COUNT + 1)); }
|
||||||
|
task lint || { FAILED_TASKS="$FAILED_TASKS lint"; FAIL_COUNT=$((FAIL_COUNT + 1)); }
|
||||||
|
task test-short || { FAILED_TASKS="$FAILED_TASKS test-short"; FAIL_COUNT=$((FAIL_COUNT + 1)); }
|
||||||
|
task scan || { FAILED_TASKS="$FAILED_TASKS scan"; FAIL_COUNT=$((FAIL_COUNT + 1)); }
|
||||||
|
|
||||||
|
echo "------------------------------------------"
|
||||||
|
if [ $FAIL_COUNT -eq 0 ]; then
|
||||||
|
echo "OK: All checks passed!"
|
||||||
|
elif [ $FAIL_COUNT -eq $TOTAL_TASKS ]; then
|
||||||
|
echo "ERROR: All tasks failed!"
|
||||||
|
echo "Failed tasks:$FAILED_TASKS"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "ERROR: $FAIL_COUNT task(s) failed out of $TOTAL_TASKS!"
|
||||||
|
echo "Failed tasks:$FAILED_TASKS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
bench:
|
bench:
|
||||||
desc: Run benchmarks with standard GC
|
desc: Run benchmarks with standard GC
|
||||||
@@ -115,6 +141,21 @@ tasks:
|
|||||||
cmds:
|
cmds:
|
||||||
- '{{.GOCMD}} test -race -v ./...'
|
- '{{.GOCMD}} test -race -v ./...'
|
||||||
|
|
||||||
|
test-fuzz:
|
||||||
|
desc: Run fuzz tests for a short duration
|
||||||
|
cmds:
|
||||||
|
- '{{.GOCMD}} test -fuzz=FuzzPacketUnpack -fuzztime=30s ./pkg/packet'
|
||||||
|
|
||||||
|
test-leaks:
|
||||||
|
desc: Run resource leak tests
|
||||||
|
cmds:
|
||||||
|
- '{{.GOCMD}} test -v ./pkg/transport -run TestTransportLeak'
|
||||||
|
|
||||||
|
test-network:
|
||||||
|
desc: Run network simulation tests
|
||||||
|
cmds:
|
||||||
|
- '{{.GOCMD}} test -v ./pkg/transport -run TestTransportNetworkSimulation'
|
||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
desc: Generate test coverage report
|
desc: Generate test coverage report
|
||||||
cmds:
|
cmds:
|
||||||
@@ -150,9 +191,63 @@ tasks:
|
|||||||
- 'GOOS=linux GOARCH=arm {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-linux-arm {{.MAIN_PACKAGE}}'
|
- 'GOOS=linux GOARCH=arm {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-linux-arm {{.MAIN_PACKAGE}}'
|
||||||
- 'GOOS=linux GOARCH=riscv64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-linux-riscv64 {{.MAIN_PACKAGE}}'
|
- 'GOOS=linux GOARCH=riscv64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-linux-riscv64 {{.MAIN_PACKAGE}}'
|
||||||
|
|
||||||
|
build-windows:
|
||||||
|
desc: Build for Windows (amd64, arm64)
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: '0'
|
||||||
|
cmds:
|
||||||
|
- mkdir -p {{.BUILD_DIR}}
|
||||||
|
- 'GOOS=windows GOARCH=amd64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-windows-amd64.exe {{.MAIN_PACKAGE}}'
|
||||||
|
- 'GOOS=windows GOARCH=arm64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-windows-arm64.exe {{.MAIN_PACKAGE}}'
|
||||||
|
|
||||||
|
build-darwin:
|
||||||
|
desc: Build for MacOS (amd64, arm64)
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: '0'
|
||||||
|
cmds:
|
||||||
|
- mkdir -p {{.BUILD_DIR}}
|
||||||
|
- 'GOOS=darwin GOARCH=amd64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-darwin-amd64 {{.MAIN_PACKAGE}}'
|
||||||
|
- 'GOOS=darwin GOARCH=arm64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-darwin-arm64 {{.MAIN_PACKAGE}}'
|
||||||
|
|
||||||
|
build-freebsd:
|
||||||
|
desc: Build for FreeBSD (amd64, 386, arm64, arm, riscv64)
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: '0'
|
||||||
|
cmds:
|
||||||
|
- mkdir -p {{.BUILD_DIR}}
|
||||||
|
- 'GOOS=freebsd GOARCH=amd64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-freebsd-amd64 {{.MAIN_PACKAGE}}'
|
||||||
|
- 'GOOS=freebsd GOARCH=386 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-freebsd-386 {{.MAIN_PACKAGE}}'
|
||||||
|
- 'GOOS=freebsd GOARCH=arm64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-freebsd-arm64 {{.MAIN_PACKAGE}}'
|
||||||
|
- 'GOOS=freebsd GOARCH=arm {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-freebsd-arm {{.MAIN_PACKAGE}}'
|
||||||
|
- 'GOOS=freebsd GOARCH=riscv64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-freebsd-riscv64 {{.MAIN_PACKAGE}}'
|
||||||
|
|
||||||
|
build-openbsd:
|
||||||
|
desc: Build for OpenBSD (amd64, 386, arm64, arm, ppc64, riscv64)
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: '0'
|
||||||
|
cmds:
|
||||||
|
- mkdir -p {{.BUILD_DIR}}
|
||||||
|
- 'GOOS=openbsd GOARCH=amd64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-openbsd-amd64 {{.MAIN_PACKAGE}}'
|
||||||
|
- 'GOOS=openbsd GOARCH=386 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-openbsd-386 {{.MAIN_PACKAGE}}'
|
||||||
|
- 'GOOS=openbsd GOARCH=arm64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-openbsd-arm64 {{.MAIN_PACKAGE}}'
|
||||||
|
- 'GOOS=openbsd GOARCH=arm {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-openbsd-arm {{.MAIN_PACKAGE}}'
|
||||||
|
- 'GOOS=openbsd GOARCH=ppc64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-openbsd-ppc64 {{.MAIN_PACKAGE}}'
|
||||||
|
- 'GOOS=openbsd GOARCH=riscv64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-openbsd-riscv64 {{.MAIN_PACKAGE}}'
|
||||||
|
|
||||||
|
build-netbsd:
|
||||||
|
desc: Build for NetBSD (amd64, 386, arm64, arm)
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: '0'
|
||||||
|
cmds:
|
||||||
|
- mkdir -p {{.BUILD_DIR}}
|
||||||
|
- 'GOOS=netbsd GOARCH=amd64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-netbsd-amd64 {{.MAIN_PACKAGE}}'
|
||||||
|
- 'GOOS=netbsd GOARCH=386 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-netbsd-386 {{.MAIN_PACKAGE}}'
|
||||||
|
- 'GOOS=netbsd GOARCH=arm64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-netbsd-arm64 {{.MAIN_PACKAGE}}'
|
||||||
|
- 'GOOS=netbsd GOARCH=arm {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-netbsd-arm {{.MAIN_PACKAGE}}'
|
||||||
|
|
||||||
build-all:
|
build-all:
|
||||||
desc: Build for all Linux architectures
|
desc: Build for all platforms and architectures
|
||||||
deps: [build-linux]
|
deps: [build-linux, build-windows, build-darwin, build-freebsd, build-openbsd, build-netbsd]
|
||||||
|
|
||||||
run:
|
run:
|
||||||
desc: Run with go run
|
desc: Run with go run
|
||||||
@@ -171,20 +266,37 @@ tasks:
|
|||||||
- mkdir -p {{.BUILD_DIR}}
|
- mkdir -p {{.BUILD_DIR}}
|
||||||
- tinygo build -target wasm -o {{.BUILD_DIR}}/{{.BINARY_NAME}}.wasm ./cmd/reticulum-wasm
|
- tinygo build -target wasm -o {{.BUILD_DIR}}/{{.BINARY_NAME}}.wasm ./cmd/reticulum-wasm
|
||||||
|
|
||||||
|
test-wasm:
|
||||||
|
desc: Run WebAssembly tests using Node.js
|
||||||
|
vars:
|
||||||
|
ROOT_DIR:
|
||||||
|
sh: pwd
|
||||||
|
env:
|
||||||
|
GOOS: js
|
||||||
|
GOARCH: wasm
|
||||||
|
cmds:
|
||||||
|
- chmod +x {{.ROOT_DIR}}/misc/wasm/go_js_wasm_exec
|
||||||
|
- PATH="$PATH:{{.ROOT_DIR}}/misc/wasm" {{.GOCMD}} test -v ./pkg/wasm/ ./cmd/reticulum-wasm/
|
||||||
|
- |
|
||||||
|
export PATH="$PATH:{{.ROOT_DIR}}/misc/wasm"
|
||||||
|
cd examples/wasm && {{.GOCMD}} test -v .
|
||||||
|
|
||||||
build-wasm:
|
build-wasm:
|
||||||
desc: Build WebAssembly binary with standard Go compiler
|
desc: Build WebAssembly binary with standard Go compiler
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: '0'
|
CGO_ENABLED: '0'
|
||||||
|
GOOS: js
|
||||||
|
GOARCH: wasm
|
||||||
cmds:
|
cmds:
|
||||||
- mkdir -p {{.BUILD_DIR}}
|
- mkdir -p {{.BUILD_DIR}}
|
||||||
- 'GOOS=js GOARCH=wasm {{.GOCMD}} build -ldflags="-s -w" -o {{.BUILD_DIR}}/{{.BINARY_NAME}}.wasm ./cmd/reticulum-wasm'
|
- '{{.GOCMD}} build -ldflags="-s -w" -o {{.BUILD_DIR}}/{{.BINARY_NAME}}.wasm ./cmd/reticulum-wasm'
|
||||||
|
|
||||||
build-wasm-example:
|
example:wasm:build:
|
||||||
desc: Build WebAssembly example
|
desc: Build WebAssembly example
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: '0'
|
CGO_ENABLED: '0'
|
||||||
cmds:
|
cmds:
|
||||||
- mkdir -p examples/wasm/public/static
|
- mkdir -p examples/wasm/public/static examples/wasm/public/js
|
||||||
- 'cd examples/wasm && GOOS=js GOARCH=wasm {{.GOCMD}} build -o public/static/reticulum-go.wasm .'
|
- 'cd examples/wasm && GOOS=js GOARCH=wasm {{.GOCMD}} build -o public/static/reticulum-go.wasm .'
|
||||||
- |
|
- |
|
||||||
GOROOT=$({{.GOCMD}} env GOROOT)
|
GOROOT=$({{.GOCMD}} env GOROOT)
|
||||||
@@ -199,6 +311,19 @@ tasks:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
example:wasm:run:
|
||||||
|
desc: Run WebAssembly example using a simple HTTP server
|
||||||
|
deps: [example:wasm:build]
|
||||||
|
cmds:
|
||||||
|
- echo "Starting server at http://localhost:8080"
|
||||||
|
- echo "Press Ctrl+C to stop"
|
||||||
|
- 'cd examples/wasm/public && python3 -m http.server 8080'
|
||||||
|
|
||||||
|
example:wasm:test:
|
||||||
|
desc: Run tests for WASM example
|
||||||
|
cmds:
|
||||||
|
- task: test-wasm
|
||||||
|
|
||||||
install:
|
install:
|
||||||
desc: Install dependencies
|
desc: Install dependencies
|
||||||
cmds:
|
cmds:
|
||||||
@@ -285,3 +410,289 @@ tasks:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
cd examples/filetransfer && {{.GOCMD}} run . --destination="${DESTINATION}"
|
cd examples/filetransfer && {{.GOCMD}} run . --destination="${DESTINATION}"
|
||||||
|
|
||||||
|
trivy:install:
|
||||||
|
desc: Install Trivy scanner
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if ! command -v trivy &> /dev/null; then
|
||||||
|
curl -L -o /tmp/trivy.deb https://git.quad4.io/Quad4-Extra/assets/raw/commit/90fdcea1bb71d91df2de6ff2e3897f278413f300/bin/trivy_0.68.2_Linux-64bit.deb
|
||||||
|
sudo dpkg -i /tmp/trivy.deb || sudo apt-get install -f -y
|
||||||
|
else
|
||||||
|
echo "Trivy is already installed: $(trivy --version)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
trivy:scan:
|
||||||
|
desc: Run Trivy vulnerability scan
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if ! command -v trivy &> /dev/null; then
|
||||||
|
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
trivy fs --scanners vuln --severity HIGH,CRITICAL --timeout 90m .
|
||||||
|
|
||||||
|
trivy:scan-all:
|
||||||
|
desc: Run Trivy full scan (vulnerabilities, secrets, misconfig)
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if ! command -v trivy &> /dev/null; then
|
||||||
|
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
trivy fs --scanners vuln,secret,misconfig .
|
||||||
|
|
||||||
|
sbom:
|
||||||
|
desc: Generate SBOM files (SPDX and CycloneDX formats)
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if ! command -v trivy &> /dev/null; then
|
||||||
|
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p sbom
|
||||||
|
trivy fs --format spdx-json --include-dev-deps --output sbom/sbom.spdx.json .
|
||||||
|
trivy fs --format cyclonedx --include-dev-deps --output sbom/sbom.cyclonedx.json .
|
||||||
|
echo "SBOM files generated in sbom/ directory"
|
||||||
|
|
||||||
|
sbom:spdx:
|
||||||
|
desc: Generate SPDX JSON SBOM
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if ! command -v trivy &> /dev/null; then
|
||||||
|
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p sbom
|
||||||
|
trivy fs --format spdx-json --include-dev-deps --output sbom/sbom.spdx.json .
|
||||||
|
echo "SPDX SBOM generated: sbom/sbom.spdx.json"
|
||||||
|
|
||||||
|
sbom:cyclonedx:
|
||||||
|
desc: Generate CycloneDX SBOM
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if ! command -v trivy &> /dev/null; then
|
||||||
|
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p sbom
|
||||||
|
trivy fs --format cyclonedx --include-dev-deps --output sbom/sbom.cyclonedx.json .
|
||||||
|
echo "CycloneDX SBOM generated: sbom/sbom.cyclonedx.json"
|
||||||
|
|
||||||
|
trivy:scan:json:
|
||||||
|
desc: Run Trivy vulnerability scan with JSON output
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if ! command -v trivy &> /dev/null; then
|
||||||
|
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p reports
|
||||||
|
trivy fs --scanners vuln --format json --output reports/trivy-vuln.json --timeout 90m .
|
||||||
|
|
||||||
|
trivy:scan:sarif:
|
||||||
|
desc: Run Trivy scan with SARIF output (for GitHub/GitLab integration)
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if ! command -v trivy &> /dev/null; then
|
||||||
|
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p reports
|
||||||
|
trivy fs --scanners vuln,secret --format sarif --output reports/trivy.sarif --timeout 90m .
|
||||||
|
|
||||||
|
trivy:scan:secrets:
|
||||||
|
desc: Scan for hardcoded secrets
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if ! command -v trivy &> /dev/null; then
|
||||||
|
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
trivy fs --scanners secret .
|
||||||
|
|
||||||
|
trivy:scan:licenses:
|
||||||
|
desc: Scan for licenses in dependencies
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if ! command -v trivy &> /dev/null; then
|
||||||
|
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
trivy fs --scanners license .
|
||||||
|
|
||||||
|
trivy:scan:misconfig:
|
||||||
|
desc: Scan for misconfigurations in config files
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if ! command -v trivy &> /dev/null; then
|
||||||
|
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
trivy fs --scanners misconfig .
|
||||||
|
|
||||||
|
trivy:db-update:
|
||||||
|
desc: Update Trivy vulnerability database
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if ! command -v trivy &> /dev/null; then
|
||||||
|
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
trivy image --download-db-only
|
||||||
|
|
||||||
|
trivy:cache-clean:
|
||||||
|
desc: Clean Trivy cache
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if ! command -v trivy &> /dev/null; then
|
||||||
|
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
trivy clean --cache
|
||||||
|
|
||||||
|
trivy:compliance:
|
||||||
|
desc: "Generate compliance report (specify COMPLIANCE env var: docker-bench-cis, k8s-nsa, etc.)"
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if ! command -v trivy &> /dev/null; then
|
||||||
|
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -z "${COMPLIANCE}" ]; then
|
||||||
|
echo "Error: COMPLIANCE environment variable required"
|
||||||
|
echo "Example: COMPLIANCE=docker-bench-cis task trivy:compliance"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p reports
|
||||||
|
trivy fs --compliance "${COMPLIANCE}" --format json --output "reports/compliance-${COMPLIANCE}.json" .
|
||||||
|
|
||||||
|
trivy:ci:
|
||||||
|
desc: Run Trivy scan for CI (exits with non-zero code on findings)
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if ! command -v trivy &> /dev/null; then
|
||||||
|
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
trivy fs --scanners vuln --severity HIGH,CRITICAL --exit-code 1 --timeout 90m .
|
||||||
|
|
||||||
|
docker:build:
|
||||||
|
desc: Build Docker image (runtime image)
|
||||||
|
vars:
|
||||||
|
IMAGE_NAME: reticulum-go
|
||||||
|
IMAGE_TAG: latest
|
||||||
|
cmds:
|
||||||
|
- docker build -f docker/Dockerfile -t {{.IMAGE_NAME}}:{{.IMAGE_TAG}} .
|
||||||
|
|
||||||
|
docker:build:tag:
|
||||||
|
desc: Build Docker image with custom tag (use IMAGE_TAG env var)
|
||||||
|
vars:
|
||||||
|
IMAGE_NAME: reticulum-go
|
||||||
|
IMAGE_TAG: ${IMAGE_TAG:-latest}
|
||||||
|
cmds:
|
||||||
|
- docker build -f docker/Dockerfile -t {{.IMAGE_NAME}}:{{.IMAGE_TAG}} .
|
||||||
|
|
||||||
|
docker:build:build:
|
||||||
|
desc: Build Docker image for building binaries only
|
||||||
|
vars:
|
||||||
|
IMAGE_NAME: reticulum-go-build
|
||||||
|
IMAGE_TAG: latest
|
||||||
|
cmds:
|
||||||
|
- docker build -f docker/Dockerfile.build -t {{.IMAGE_NAME}}:{{.IMAGE_TAG}} .
|
||||||
|
|
||||||
|
docker:run:
|
||||||
|
desc: Run Docker container (runtime image)
|
||||||
|
vars:
|
||||||
|
IMAGE_NAME: reticulum-go
|
||||||
|
IMAGE_TAG: latest
|
||||||
|
CONTAINER_NAME: reticulum-go
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
docker run --rm -it \
|
||||||
|
--name {{.CONTAINER_NAME}} \
|
||||||
|
-p 4242:4242 \
|
||||||
|
{{.IMAGE_NAME}}:{{.IMAGE_TAG}}
|
||||||
|
|
||||||
|
docker:run:detached:
|
||||||
|
desc: Run Docker container in detached mode
|
||||||
|
vars:
|
||||||
|
IMAGE_NAME: reticulum-go
|
||||||
|
IMAGE_TAG: latest
|
||||||
|
CONTAINER_NAME: reticulum-go
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
docker run -d \
|
||||||
|
--name {{.CONTAINER_NAME}} \
|
||||||
|
-p 4242:4242 \
|
||||||
|
{{.IMAGE_NAME}}:{{.IMAGE_TAG}}
|
||||||
|
|
||||||
|
docker:stop:
|
||||||
|
desc: Stop running Docker container
|
||||||
|
vars:
|
||||||
|
CONTAINER_NAME: reticulum-go
|
||||||
|
cmds:
|
||||||
|
- docker stop {{.CONTAINER_NAME}} || true
|
||||||
|
- docker rm {{.CONTAINER_NAME}} || true
|
||||||
|
|
||||||
|
docker:extract:
|
||||||
|
desc: Extract binary from build container
|
||||||
|
vars:
|
||||||
|
IMAGE_NAME: reticulum-go-build
|
||||||
|
IMAGE_TAG: latest
|
||||||
|
BINARY_NAME: reticulum-go
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
CONTAINER_ID=$(docker create {{.IMAGE_NAME}}:{{.IMAGE_TAG}})
|
||||||
|
docker cp $CONTAINER_ID:/dist/{{.BINARY_NAME}} {{.BUILD_DIR}}/{{.BINARY_NAME}}
|
||||||
|
docker rm $CONTAINER_ID
|
||||||
|
echo "Binary extracted to {{.BUILD_DIR}}/{{.BINARY_NAME}}"
|
||||||
|
|
||||||
|
docker:buildx:setup:
|
||||||
|
desc: Setup Docker buildx for multi-platform builds
|
||||||
|
cmds:
|
||||||
|
- docker buildx create --name reticulum-builder --use || docker buildx use reticulum-builder
|
||||||
|
- docker buildx inspect --bootstrap
|
||||||
|
|
||||||
|
docker:buildx:build:
|
||||||
|
desc: Build multi-platform Docker image
|
||||||
|
vars:
|
||||||
|
IMAGE_NAME: reticulum-go
|
||||||
|
IMAGE_TAG: latest
|
||||||
|
PLATFORMS: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
docker buildx build \
|
||||||
|
--platform {{.PLATFORMS}} \
|
||||||
|
-f docker/Dockerfile \
|
||||||
|
-t {{.IMAGE_NAME}}:{{.IMAGE_TAG}} \
|
||||||
|
--load \
|
||||||
|
.
|
||||||
|
|
||||||
|
docker:buildx:build:push:
|
||||||
|
desc: Build and push multi-platform Docker image
|
||||||
|
vars:
|
||||||
|
IMAGE_NAME: reticulum-go
|
||||||
|
IMAGE_TAG: latest
|
||||||
|
PLATFORMS: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if [ -z "${DOCKER_REGISTRY}" ]; then
|
||||||
|
echo "Error: DOCKER_REGISTRY environment variable required"
|
||||||
|
echo "Example: DOCKER_REGISTRY=registry.example.com task docker:buildx:build:push"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
docker buildx build \
|
||||||
|
--platform {{.PLATFORMS}} \
|
||||||
|
-f docker/Dockerfile \
|
||||||
|
-t ${DOCKER_REGISTRY}/{{.IMAGE_NAME}}:{{.IMAGE_TAG}} \
|
||||||
|
--push \
|
||||||
|
.
|
||||||
|
|
||||||
|
docker:clean:
|
||||||
|
desc: Clean Docker images and containers
|
||||||
|
cmds:
|
||||||
|
- docker stop reticulum-go || true
|
||||||
|
- docker rm reticulum-go || true
|
||||||
|
- docker rmi reticulum-go:latest || true
|
||||||
|
- docker rmi reticulum-go-build:latest || true
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -193,6 +195,18 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
|
|||||||
)
|
)
|
||||||
case "AutoInterface":
|
case "AutoInterface":
|
||||||
iface, err = interfaces.NewAutoInterface(name, ifaceConfig)
|
iface, err = interfaces.NewAutoInterface(name, ifaceConfig)
|
||||||
|
case "WebSocketInterface":
|
||||||
|
wsURL := ifaceConfig.Address
|
||||||
|
if wsURL == "" {
|
||||||
|
wsURL = ifaceConfig.TargetHost
|
||||||
|
}
|
||||||
|
debug.Log(debug.DEBUG_INFO, "Creating WebSocket interface", common.STR_NAME, name, "url", wsURL, "enabled", ifaceConfig.Enabled)
|
||||||
|
iface, err = interfaces.NewWebSocketInterface(name, wsURL, ifaceConfig.Enabled)
|
||||||
|
if err != nil {
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Failed to create WebSocket interface", common.STR_NAME, name, common.STR_ERROR, err)
|
||||||
|
} else {
|
||||||
|
debug.Log(debug.DEBUG_INFO, "WebSocket interface created successfully", common.STR_NAME, name)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
debug.Log(debug.DEBUG_CRITICAL, "Unknown interface type", common.STR_TYPE, ifaceConfig.Type)
|
debug.Log(debug.DEBUG_CRITICAL, "Unknown interface type", common.STR_TYPE, ifaceConfig.Type)
|
||||||
continue
|
continue
|
||||||
@@ -289,34 +303,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
debug.Log(debug.DEBUG_ERROR, "Configuration loaded", "path", cfg.ConfigPath)
|
debug.Log(debug.DEBUG_ERROR, "Configuration loaded", "path", cfg.ConfigPath)
|
||||||
|
|
||||||
if len(cfg.Interfaces) == 0 {
|
|
||||||
debug.Log(debug.DEBUG_ERROR, "No interfaces configured, adding default interfaces")
|
|
||||||
cfg.Interfaces = make(map[string]*common.InterfaceConfig)
|
|
||||||
|
|
||||||
// Auto interface for local discovery
|
|
||||||
cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{
|
|
||||||
Type: "AutoInterface",
|
|
||||||
Enabled: true,
|
|
||||||
Name: "Auto Discovery",
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
|
|
||||||
Type: common.STR_TCP_CLIENT,
|
|
||||||
Enabled: false,
|
|
||||||
TargetHost: "127.0.0.1",
|
|
||||||
TargetPort: common.NUM_4242,
|
|
||||||
Name: "Go-RNS-Testnet",
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Interfaces["Quad4 TCP"] = &common.InterfaceConfig{
|
|
||||||
Type: common.STR_TCP_CLIENT,
|
|
||||||
Enabled: true,
|
|
||||||
TargetHost: "rns2.quad4.io",
|
|
||||||
TargetPort: common.NUM_4242,
|
|
||||||
Name: "Quad4 TCP",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := NewReticulum(cfg)
|
r, err := NewReticulum(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debug.GetLogger().Error("Failed to create Reticulum instance", common.STR_ERROR, err)
|
debug.GetLogger().Error("Failed to create Reticulum instance", common.STR_ERROR, err)
|
||||||
@@ -542,10 +528,10 @@ func (h *AnnounceHandler) AspectFilter() []string {
|
|||||||
return h.aspectFilter
|
return h.aspectFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appData []byte) error {
|
func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appData []byte, hops uint8) error {
|
||||||
debug.Log(debug.DEBUG_INFO, "Received announce", "hash", fmt.Sprintf("%x", destHash))
|
debug.Log(debug.DEBUG_INFO, "Received announce", "hash", fmt.Sprintf("%x", destHash), "hops", hops)
|
||||||
debug.Log(debug.DEBUG_PACKETS, "Raw announce data", "data", fmt.Sprintf("%x", appData))
|
debug.Log(debug.DEBUG_PACKETS, "Raw announce data", "data", fmt.Sprintf("%x", appData))
|
||||||
debug.Log(debug.DEBUG_INFO, "MAIN HANDLER: Received announce", "hash", fmt.Sprintf("%x", destHash), "appData_len", len(appData))
|
debug.Log(debug.DEBUG_INFO, "MAIN HANDLER: Received announce", "hash", fmt.Sprintf("%x", destHash), "appData_len", len(appData), "hops", hops)
|
||||||
|
|
||||||
var isNode bool
|
var isNode bool
|
||||||
var nodeEnabled bool
|
var nodeEnabled bool
|
||||||
@@ -639,7 +625,6 @@ func (r *Reticulum) createNodeAppData() []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Element 1: Int32 timestamp (current time)
|
// Element 1: Int32 timestamp (current time)
|
||||||
// Update the timestamp when creating new announcements
|
|
||||||
r.nodeTimestamp = time.Now().Unix()
|
r.nodeTimestamp = time.Now().Unix()
|
||||||
appData = append(appData, common.HEX_0xD2) // int32 format
|
appData = append(appData, common.HEX_0xD2) // int32 format
|
||||||
timeBytes := make([]byte, common.FOUR)
|
timeBytes := make([]byte, common.FOUR)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
//go:build js && wasm
|
//go:build js && wasm
|
||||||
// +build js,wasm
|
// +build js,wasm
|
||||||
|
|
||||||
@@ -11,6 +13,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
run()
|
||||||
|
// Keep the Go program running
|
||||||
|
select {}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() {
|
||||||
debug.Init()
|
debug.Init()
|
||||||
debug.SetDebugLevel(debug.DEBUG_INFO)
|
debug.SetDebugLevel(debug.DEBUG_INFO)
|
||||||
|
|
||||||
@@ -18,8 +26,5 @@ func main() {
|
|||||||
|
|
||||||
// Notify JS that reticulum is ready
|
// Notify JS that reticulum is ready
|
||||||
js.Global().Call("reticulumReady")
|
js.Global().Call("reticulumReady")
|
||||||
|
|
||||||
// Keep the Go program running
|
|
||||||
select {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
29
cmd/reticulum-wasm/main_test.go
Normal file
29
cmd/reticulum-wasm/main_test.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
//go:build js && wasm
|
||||||
|
// +build js,wasm
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall/js"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRun(t *testing.T) {
|
||||||
|
readyCalled := false
|
||||||
|
js.Global().Set("reticulumReady", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
|
readyCalled = true
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
run()
|
||||||
|
|
||||||
|
if !readyCalled {
|
||||||
|
t.Error("reticulumReady was not called by run()")
|
||||||
|
}
|
||||||
|
|
||||||
|
reticulum := js.Global().Get("reticulum")
|
||||||
|
if reticulum.IsUndefined() {
|
||||||
|
t.Error("reticulum functions were not registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ RUN go build \
|
|||||||
-o reticulum-go \
|
-o reticulum-go \
|
||||||
./cmd/reticulum-go
|
./cmd/reticulum-go
|
||||||
|
|
||||||
FROM busybox:latest
|
FROM busybox:1.37.0@sha256:870e815c3a50dd0f6b40efddb319c72c32c3ee340b5a3e8945904232ccd12f44
|
||||||
|
|
||||||
RUN adduser -D -s /bin/sh app
|
RUN adduser -D -s /bin/sh app
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ ENV CGO_ENABLED=0
|
|||||||
ENV GOOS=linux
|
ENV GOOS=linux
|
||||||
ENV GOARCH=amd64
|
ENV GOARCH=amd64
|
||||||
|
|
||||||
RUN apk add --no-cache git
|
RUN apk add --no-cache git && \
|
||||||
|
adduser -D -s /bin/sh builder
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
|
USER builder
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
|
|||||||
16
examples/wasm/go.mod
Normal file
16
examples/wasm/go.mod
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
module git.quad4.io/Networks/Reticulum-Go/examples/wasm
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.quad4.io/Networks/Reticulum-Go v0.6.0
|
||||||
|
git.quad4.io/RNS-Things/reticulum-go-mf v0.0.0-20251231170406-60b810424de0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
|
golang.org/x/crypto v0.46.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
replace git.quad4.io/Networks/Reticulum-Go => ../../
|
||||||
16
examples/wasm/go.sum
Normal file
16
examples/wasm/go.sum
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
git.quad4.io/RNS-Things/reticulum-go-mf v0.0.0-20251231170406-60b810424de0 h1:Yne2IbESHud2fmsj9kjsTYR3QBj+vY9fTqvsEzaKfy8=
|
||||||
|
git.quad4.io/RNS-Things/reticulum-go-mf v0.0.0-20251231170406-60b810424de0/go.mod h1:vhZm1vAMuWJtoFGGAHPlnFsVqTzHkBuYWDMGo6KjVPk=
|
||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||||
|
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
|
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
80
examples/wasm/main.go
Normal file
80
examples/wasm/main.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
//go:build js && wasm
|
||||||
|
// +build js,wasm
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"syscall/js"
|
||||||
|
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/wasm"
|
||||||
|
"git.quad4.io/RNS-Things/reticulum-go-mf/pkg/mf"
|
||||||
|
)
|
||||||
|
|
||||||
|
var messenger *mf.Messenger
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Register the generic WASM bridge functions first
|
||||||
|
wasm.RegisterJSFunctions()
|
||||||
|
|
||||||
|
// Add chat-specific functions to the "reticulum" JS object
|
||||||
|
reticulum := js.Global().Get("reticulum")
|
||||||
|
reticulum.Set("sendMessage", js.FuncOf(SendMessage))
|
||||||
|
reticulum.Set("sendAnnounce", js.FuncOf(SendAnnounce))
|
||||||
|
|
||||||
|
// Keep the Go program running
|
||||||
|
select {}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendMessage(this js.Value, args []js.Value) interface{} {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return js.ValueOf(map[string]interface{}{
|
||||||
|
"error": "Destination hash and message required",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
destHashHex := args[0].String()
|
||||||
|
message := args[1].String()
|
||||||
|
|
||||||
|
destHash, err := hex.DecodeString(destHashHex)
|
||||||
|
if err != nil {
|
||||||
|
return js.ValueOf(map[string]interface{}{
|
||||||
|
"error": fmt.Sprintf("Invalid destination hash: %v", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize messenger if not already done
|
||||||
|
if messenger == nil {
|
||||||
|
t := wasm.GetTransport()
|
||||||
|
d := wasm.GetDestinationPointer()
|
||||||
|
if t == nil || d == nil {
|
||||||
|
return js.ValueOf(map[string]interface{}{
|
||||||
|
"error": "Reticulum not initialized",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
messenger = mf.NewMessenger(t, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the high-level Messenger from mf package
|
||||||
|
if err := messenger.SendMessage(destHash, message); err != nil {
|
||||||
|
return js.ValueOf(map[string]interface{}{
|
||||||
|
"error": fmt.Sprintf("Send failed: %v", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return js.ValueOf(map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendAnnounce(this js.Value, args []js.Value) interface{} {
|
||||||
|
var appData []byte
|
||||||
|
if len(args) >= 1 && args[0].String() != "" {
|
||||||
|
appData = []byte(args[0].String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return wasm.SendAnnounce(appData)
|
||||||
|
}
|
||||||
33
examples/wasm/main_test.go
Normal file
33
examples/wasm/main_test.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
//go:build js && wasm
|
||||||
|
// +build js,wasm
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall/js"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/wasm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRegisterFunctions(t *testing.T) {
|
||||||
|
// Register functions
|
||||||
|
wasm.RegisterJSFunctions()
|
||||||
|
|
||||||
|
reticulum := js.Global().Get("reticulum")
|
||||||
|
if reticulum.IsUndefined() {
|
||||||
|
t.Fatal("reticulum object not registered")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manually register chat functions since main() has select{}
|
||||||
|
reticulum.Set("sendMessage", js.FuncOf(SendMessage))
|
||||||
|
reticulum.Set("sendAnnounce", js.FuncOf(SendAnnounce))
|
||||||
|
|
||||||
|
tests := []string{"sendMessage", "sendAnnounce", "init", "getIdentity", "sendData"}
|
||||||
|
for _, name := range tests {
|
||||||
|
if reticulum.Get(name).Type() != js.TypeFunction {
|
||||||
|
t.Errorf("function %s not registered correctly", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
43
examples/wasm/public/index.html
Normal file
43
examples/wasm/public/index.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Reticulum WASM Chat Example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="js/wasm_exec.js"></script>
|
||||||
|
<script>
|
||||||
|
if (!WebAssembly.instantiateStreaming) { // polyfill
|
||||||
|
WebAssembly.instantiateStreaming = async (resp, importObject) => {
|
||||||
|
const source = await (await resp).arrayBuffer();
|
||||||
|
return await WebAssembly.instantiate(source, importObject);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const go = new Go();
|
||||||
|
let mod, inst;
|
||||||
|
WebAssembly.instantiateStreaming(fetch("static/reticulum-go.wasm"), go.importObject).then(async (result) => {
|
||||||
|
mod = result.module;
|
||||||
|
inst = result.instance;
|
||||||
|
console.log("WASM loaded");
|
||||||
|
await go.run(inst);
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Basic chat interface helper
|
||||||
|
window.onChatMessage = (msg) => {
|
||||||
|
console.log("Chat message received:", msg);
|
||||||
|
};
|
||||||
|
window.onPeerDiscovered = (peer) => {
|
||||||
|
console.log("Peer discovered:", peer);
|
||||||
|
};
|
||||||
|
window.log = (msg, level) => {
|
||||||
|
console.log(`[${level}] ${msg}`);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<h1>Reticulum WASM Chat</h1>
|
||||||
|
<p>Open console to see output.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
575
examples/wasm/public/js/wasm_exec.js
Normal file
575
examples/wasm/public/js/wasm_exec.js
Normal file
@@ -0,0 +1,575 @@
|
|||||||
|
// Copyright 2018 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const enosys = () => {
|
||||||
|
const err = new Error("not implemented");
|
||||||
|
err.code = "ENOSYS";
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!globalThis.fs) {
|
||||||
|
let outputBuf = "";
|
||||||
|
globalThis.fs = {
|
||||||
|
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused
|
||||||
|
writeSync(fd, buf) {
|
||||||
|
outputBuf += decoder.decode(buf);
|
||||||
|
const nl = outputBuf.lastIndexOf("\n");
|
||||||
|
if (nl != -1) {
|
||||||
|
console.log(outputBuf.substring(0, nl));
|
||||||
|
outputBuf = outputBuf.substring(nl + 1);
|
||||||
|
}
|
||||||
|
return buf.length;
|
||||||
|
},
|
||||||
|
write(fd, buf, offset, length, position, callback) {
|
||||||
|
if (offset !== 0 || length !== buf.length || position !== null) {
|
||||||
|
callback(enosys());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const n = this.writeSync(fd, buf);
|
||||||
|
callback(null, n);
|
||||||
|
},
|
||||||
|
chmod(path, mode, callback) { callback(enosys()); },
|
||||||
|
chown(path, uid, gid, callback) { callback(enosys()); },
|
||||||
|
close(fd, callback) { callback(enosys()); },
|
||||||
|
fchmod(fd, mode, callback) { callback(enosys()); },
|
||||||
|
fchown(fd, uid, gid, callback) { callback(enosys()); },
|
||||||
|
fstat(fd, callback) { callback(enosys()); },
|
||||||
|
fsync(fd, callback) { callback(null); },
|
||||||
|
ftruncate(fd, length, callback) { callback(enosys()); },
|
||||||
|
lchown(path, uid, gid, callback) { callback(enosys()); },
|
||||||
|
link(path, link, callback) { callback(enosys()); },
|
||||||
|
lstat(path, callback) { callback(enosys()); },
|
||||||
|
mkdir(path, perm, callback) { callback(enosys()); },
|
||||||
|
open(path, flags, mode, callback) { callback(enosys()); },
|
||||||
|
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
|
||||||
|
readdir(path, callback) { callback(enosys()); },
|
||||||
|
readlink(path, callback) { callback(enosys()); },
|
||||||
|
rename(from, to, callback) { callback(enosys()); },
|
||||||
|
rmdir(path, callback) { callback(enosys()); },
|
||||||
|
stat(path, callback) { callback(enosys()); },
|
||||||
|
symlink(path, link, callback) { callback(enosys()); },
|
||||||
|
truncate(path, length, callback) { callback(enosys()); },
|
||||||
|
unlink(path, callback) { callback(enosys()); },
|
||||||
|
utimes(path, atime, mtime, callback) { callback(enosys()); },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.process) {
|
||||||
|
globalThis.process = {
|
||||||
|
getuid() { return -1; },
|
||||||
|
getgid() { return -1; },
|
||||||
|
geteuid() { return -1; },
|
||||||
|
getegid() { return -1; },
|
||||||
|
getgroups() { throw enosys(); },
|
||||||
|
pid: -1,
|
||||||
|
ppid: -1,
|
||||||
|
umask() { throw enosys(); },
|
||||||
|
cwd() { throw enosys(); },
|
||||||
|
chdir() { throw enosys(); },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.path) {
|
||||||
|
globalThis.path = {
|
||||||
|
resolve(...pathSegments) {
|
||||||
|
return pathSegments.join("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.crypto) {
|
||||||
|
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.performance) {
|
||||||
|
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.TextEncoder) {
|
||||||
|
throw new Error("globalThis.TextEncoder is not available, polyfill required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.TextDecoder) {
|
||||||
|
throw new Error("globalThis.TextDecoder is not available, polyfill required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const encoder = new TextEncoder("utf-8");
|
||||||
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
|
||||||
|
globalThis.Go = class {
|
||||||
|
constructor() {
|
||||||
|
this.argv = ["js"];
|
||||||
|
this.env = {};
|
||||||
|
this.exit = (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
console.warn("exit code:", code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this._exitPromise = new Promise((resolve) => {
|
||||||
|
this._resolveExitPromise = resolve;
|
||||||
|
});
|
||||||
|
this._pendingEvent = null;
|
||||||
|
this._scheduledTimeouts = new Map();
|
||||||
|
this._nextCallbackTimeoutID = 1;
|
||||||
|
|
||||||
|
const setInt64 = (addr, v) => {
|
||||||
|
this.mem.setUint32(addr + 0, v, true);
|
||||||
|
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const setInt32 = (addr, v) => {
|
||||||
|
this.mem.setUint32(addr + 0, v, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInt64 = (addr) => {
|
||||||
|
const low = this.mem.getUint32(addr + 0, true);
|
||||||
|
const high = this.mem.getInt32(addr + 4, true);
|
||||||
|
return low + high * 4294967296;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadValue = (addr) => {
|
||||||
|
const f = this.mem.getFloat64(addr, true);
|
||||||
|
if (f === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!isNaN(f)) {
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = this.mem.getUint32(addr, true);
|
||||||
|
return this._values[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
const storeValue = (addr, v) => {
|
||||||
|
const nanHead = 0x7FF80000;
|
||||||
|
|
||||||
|
if (typeof v === "number" && v !== 0) {
|
||||||
|
if (isNaN(v)) {
|
||||||
|
this.mem.setUint32(addr + 4, nanHead, true);
|
||||||
|
this.mem.setUint32(addr, 0, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.mem.setFloat64(addr, v, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v === undefined) {
|
||||||
|
this.mem.setFloat64(addr, 0, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = this._ids.get(v);
|
||||||
|
if (id === undefined) {
|
||||||
|
id = this._idPool.pop();
|
||||||
|
if (id === undefined) {
|
||||||
|
id = this._values.length;
|
||||||
|
}
|
||||||
|
this._values[id] = v;
|
||||||
|
this._goRefCounts[id] = 0;
|
||||||
|
this._ids.set(v, id);
|
||||||
|
}
|
||||||
|
this._goRefCounts[id]++;
|
||||||
|
let typeFlag = 0;
|
||||||
|
switch (typeof v) {
|
||||||
|
case "object":
|
||||||
|
if (v !== null) {
|
||||||
|
typeFlag = 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "string":
|
||||||
|
typeFlag = 2;
|
||||||
|
break;
|
||||||
|
case "symbol":
|
||||||
|
typeFlag = 3;
|
||||||
|
break;
|
||||||
|
case "function":
|
||||||
|
typeFlag = 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
|
||||||
|
this.mem.setUint32(addr, id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSlice = (addr) => {
|
||||||
|
const array = getInt64(addr + 0);
|
||||||
|
const len = getInt64(addr + 8);
|
||||||
|
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSliceOfValues = (addr) => {
|
||||||
|
const array = getInt64(addr + 0);
|
||||||
|
const len = getInt64(addr + 8);
|
||||||
|
const a = new Array(len);
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
a[i] = loadValue(array + i * 8);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadString = (addr) => {
|
||||||
|
const saddr = getInt64(addr + 0);
|
||||||
|
const len = getInt64(addr + 8);
|
||||||
|
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
|
||||||
|
}
|
||||||
|
|
||||||
|
const testCallExport = (a, b) => {
|
||||||
|
this._inst.exports.testExport0();
|
||||||
|
return this._inst.exports.testExport(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeOrigin = Date.now() - performance.now();
|
||||||
|
this.importObject = {
|
||||||
|
_gotest: {
|
||||||
|
add: (a, b) => a + b,
|
||||||
|
callExport: testCallExport,
|
||||||
|
},
|
||||||
|
gojs: {
|
||||||
|
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
|
||||||
|
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
|
||||||
|
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
|
||||||
|
// This changes the SP, thus we have to update the SP used by the imported function.
|
||||||
|
|
||||||
|
// func wasmExit(code int32)
|
||||||
|
"runtime.wasmExit": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const code = this.mem.getInt32(sp + 8, true);
|
||||||
|
this.exited = true;
|
||||||
|
delete this._inst;
|
||||||
|
delete this._values;
|
||||||
|
delete this._goRefCounts;
|
||||||
|
delete this._ids;
|
||||||
|
delete this._idPool;
|
||||||
|
this.exit(code);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
|
||||||
|
"runtime.wasmWrite": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const fd = getInt64(sp + 8);
|
||||||
|
const p = getInt64(sp + 16);
|
||||||
|
const n = this.mem.getInt32(sp + 24, true);
|
||||||
|
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
|
||||||
|
},
|
||||||
|
|
||||||
|
// func resetMemoryDataView()
|
||||||
|
"runtime.resetMemoryDataView": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func nanotime1() int64
|
||||||
|
"runtime.nanotime1": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func walltime() (sec int64, nsec int32)
|
||||||
|
"runtime.walltime": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const msec = (new Date).getTime();
|
||||||
|
setInt64(sp + 8, msec / 1000);
|
||||||
|
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func scheduleTimeoutEvent(delay int64) int32
|
||||||
|
"runtime.scheduleTimeoutEvent": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const id = this._nextCallbackTimeoutID;
|
||||||
|
this._nextCallbackTimeoutID++;
|
||||||
|
this._scheduledTimeouts.set(id, setTimeout(
|
||||||
|
() => {
|
||||||
|
this._resume();
|
||||||
|
while (this._scheduledTimeouts.has(id)) {
|
||||||
|
// for some reason Go failed to register the timeout event, log and try again
|
||||||
|
// (temporary workaround for https://github.com/golang/go/issues/28975)
|
||||||
|
console.warn("scheduleTimeoutEvent: missed timeout event");
|
||||||
|
this._resume();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getInt64(sp + 8),
|
||||||
|
));
|
||||||
|
this.mem.setInt32(sp + 16, id, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func clearTimeoutEvent(id int32)
|
||||||
|
"runtime.clearTimeoutEvent": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const id = this.mem.getInt32(sp + 8, true);
|
||||||
|
clearTimeout(this._scheduledTimeouts.get(id));
|
||||||
|
this._scheduledTimeouts.delete(id);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func getRandomData(r []byte)
|
||||||
|
"runtime.getRandomData": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
crypto.getRandomValues(loadSlice(sp + 8));
|
||||||
|
},
|
||||||
|
|
||||||
|
// func finalizeRef(v ref)
|
||||||
|
"syscall/js.finalizeRef": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const id = this.mem.getUint32(sp + 8, true);
|
||||||
|
this._goRefCounts[id]--;
|
||||||
|
if (this._goRefCounts[id] === 0) {
|
||||||
|
const v = this._values[id];
|
||||||
|
this._values[id] = null;
|
||||||
|
this._ids.delete(v);
|
||||||
|
this._idPool.push(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// func stringVal(value string) ref
|
||||||
|
"syscall/js.stringVal": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
storeValue(sp + 24, loadString(sp + 8));
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueGet(v ref, p string) ref
|
||||||
|
"syscall/js.valueGet": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 32, result);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueSet(v ref, p string, x ref)
|
||||||
|
"syscall/js.valueSet": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueDelete(v ref, p string)
|
||||||
|
"syscall/js.valueDelete": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueIndex(v ref, i int) ref
|
||||||
|
"syscall/js.valueIndex": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
|
||||||
|
},
|
||||||
|
|
||||||
|
// valueSetIndex(v ref, i int, x ref)
|
||||||
|
"syscall/js.valueSetIndex": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueCall(v ref, m string, args []ref) (ref, bool)
|
||||||
|
"syscall/js.valueCall": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
try {
|
||||||
|
const v = loadValue(sp + 8);
|
||||||
|
const m = Reflect.get(v, loadString(sp + 16));
|
||||||
|
const args = loadSliceOfValues(sp + 32);
|
||||||
|
const result = Reflect.apply(m, v, args);
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 56, result);
|
||||||
|
this.mem.setUint8(sp + 64, 1);
|
||||||
|
} catch (err) {
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 56, err);
|
||||||
|
this.mem.setUint8(sp + 64, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueInvoke(v ref, args []ref) (ref, bool)
|
||||||
|
"syscall/js.valueInvoke": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
try {
|
||||||
|
const v = loadValue(sp + 8);
|
||||||
|
const args = loadSliceOfValues(sp + 16);
|
||||||
|
const result = Reflect.apply(v, undefined, args);
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 40, result);
|
||||||
|
this.mem.setUint8(sp + 48, 1);
|
||||||
|
} catch (err) {
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 40, err);
|
||||||
|
this.mem.setUint8(sp + 48, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueNew(v ref, args []ref) (ref, bool)
|
||||||
|
"syscall/js.valueNew": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
try {
|
||||||
|
const v = loadValue(sp + 8);
|
||||||
|
const args = loadSliceOfValues(sp + 16);
|
||||||
|
const result = Reflect.construct(v, args);
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 40, result);
|
||||||
|
this.mem.setUint8(sp + 48, 1);
|
||||||
|
} catch (err) {
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 40, err);
|
||||||
|
this.mem.setUint8(sp + 48, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueLength(v ref) int
|
||||||
|
"syscall/js.valueLength": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
|
||||||
|
},
|
||||||
|
|
||||||
|
// valuePrepareString(v ref) (ref, int)
|
||||||
|
"syscall/js.valuePrepareString": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const str = encoder.encode(String(loadValue(sp + 8)));
|
||||||
|
storeValue(sp + 16, str);
|
||||||
|
setInt64(sp + 24, str.length);
|
||||||
|
},
|
||||||
|
|
||||||
|
// valueLoadString(v ref, b []byte)
|
||||||
|
"syscall/js.valueLoadString": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const str = loadValue(sp + 8);
|
||||||
|
loadSlice(sp + 16).set(str);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueInstanceOf(v ref, t ref) bool
|
||||||
|
"syscall/js.valueInstanceOf": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||||
|
"syscall/js.copyBytesToGo": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const dst = loadSlice(sp + 8);
|
||||||
|
const src = loadValue(sp + 32);
|
||||||
|
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
|
||||||
|
this.mem.setUint8(sp + 48, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const toCopy = src.subarray(0, dst.length);
|
||||||
|
dst.set(toCopy);
|
||||||
|
setInt64(sp + 40, toCopy.length);
|
||||||
|
this.mem.setUint8(sp + 48, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||||
|
"syscall/js.copyBytesToJS": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const dst = loadValue(sp + 8);
|
||||||
|
const src = loadSlice(sp + 16);
|
||||||
|
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
|
||||||
|
this.mem.setUint8(sp + 48, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const toCopy = src.subarray(0, dst.length);
|
||||||
|
dst.set(toCopy);
|
||||||
|
setInt64(sp + 40, toCopy.length);
|
||||||
|
this.mem.setUint8(sp + 48, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
"debug": (value) => {
|
||||||
|
console.log(value);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(instance) {
|
||||||
|
if (!(instance instanceof WebAssembly.Instance)) {
|
||||||
|
throw new Error("Go.run: WebAssembly.Instance expected");
|
||||||
|
}
|
||||||
|
this._inst = instance;
|
||||||
|
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||||
|
this._values = [ // JS values that Go currently has references to, indexed by reference id
|
||||||
|
NaN,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
globalThis,
|
||||||
|
this,
|
||||||
|
];
|
||||||
|
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
|
||||||
|
this._ids = new Map([ // mapping from JS values to reference ids
|
||||||
|
[0, 1],
|
||||||
|
[null, 2],
|
||||||
|
[true, 3],
|
||||||
|
[false, 4],
|
||||||
|
[globalThis, 5],
|
||||||
|
[this, 6],
|
||||||
|
]);
|
||||||
|
this._idPool = []; // unused ids that have been garbage collected
|
||||||
|
this.exited = false; // whether the Go program has exited
|
||||||
|
|
||||||
|
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
|
||||||
|
let offset = 4096;
|
||||||
|
|
||||||
|
const strPtr = (str) => {
|
||||||
|
const ptr = offset;
|
||||||
|
const bytes = encoder.encode(str + "\0");
|
||||||
|
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
|
||||||
|
offset += bytes.length;
|
||||||
|
if (offset % 8 !== 0) {
|
||||||
|
offset += 8 - (offset % 8);
|
||||||
|
}
|
||||||
|
return ptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
const argc = this.argv.length;
|
||||||
|
|
||||||
|
const argvPtrs = [];
|
||||||
|
this.argv.forEach((arg) => {
|
||||||
|
argvPtrs.push(strPtr(arg));
|
||||||
|
});
|
||||||
|
argvPtrs.push(0);
|
||||||
|
|
||||||
|
const keys = Object.keys(this.env).sort();
|
||||||
|
keys.forEach((key) => {
|
||||||
|
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
|
||||||
|
});
|
||||||
|
argvPtrs.push(0);
|
||||||
|
|
||||||
|
const argv = offset;
|
||||||
|
argvPtrs.forEach((ptr) => {
|
||||||
|
this.mem.setUint32(offset, ptr, true);
|
||||||
|
this.mem.setUint32(offset + 4, 0, true);
|
||||||
|
offset += 8;
|
||||||
|
});
|
||||||
|
|
||||||
|
// The linker guarantees global data starts from at least wasmMinDataAddr.
|
||||||
|
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
|
||||||
|
const wasmMinDataAddr = 4096 + 8192;
|
||||||
|
if (offset >= wasmMinDataAddr) {
|
||||||
|
throw new Error("total length of command line and environment variables exceeds limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
this._inst.exports.run(argc, argv);
|
||||||
|
if (this.exited) {
|
||||||
|
this._resolveExitPromise();
|
||||||
|
}
|
||||||
|
await this._exitPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
_resume() {
|
||||||
|
if (this.exited) {
|
||||||
|
throw new Error("Go program has already exited");
|
||||||
|
}
|
||||||
|
this._inst.exports.resume();
|
||||||
|
if (this.exited) {
|
||||||
|
this._resolveExitPromise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_makeFuncWrapper(id) {
|
||||||
|
const go = this;
|
||||||
|
return function () {
|
||||||
|
const event = { id: id, this: this, args: arguments };
|
||||||
|
go._pendingEvent = event;
|
||||||
|
go._resume();
|
||||||
|
return event.result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
inherit system;
|
inherit system;
|
||||||
};
|
};
|
||||||
|
|
||||||
go = pkgs.go_1_24;
|
go = pkgs.go_1_25;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
revive
|
revive
|
||||||
gosec
|
gosec
|
||||||
gnumake
|
gnumake
|
||||||
|
tinygo
|
||||||
];
|
];
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
echo "Task version: $(task --version 2>/dev/null || echo 'not available')"
|
echo "Task version: $(task --version 2>/dev/null || echo 'not available')"
|
||||||
echo "Revive version: $(revive --version 2>/dev/null || echo 'not available')"
|
echo "Revive version: $(revive --version 2>/dev/null || echo 'not available')"
|
||||||
echo "Gosec version: $(gosec --version 2>/dev/null || echo 'not available')"
|
echo "Gosec version: $(gosec --version 2>/dev/null || echo 'not available')"
|
||||||
|
echo "TinyGo version: $(tinygo version 2>/dev/null || echo 'not available')"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -211,7 +213,6 @@ func CreateDefaultConfig(path string) error {
|
|||||||
cfg := DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
cfg.ConfigPath = path
|
cfg.ConfigPath = path
|
||||||
|
|
||||||
// Add Auto Interface
|
|
||||||
cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{
|
cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{
|
||||||
Type: "AutoInterface",
|
Type: "AutoInterface",
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
@@ -221,7 +222,6 @@ func CreateDefaultConfig(path string) error {
|
|||||||
DataPort: 42671,
|
DataPort: 42671,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add default interfaces
|
|
||||||
cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
|
cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
|
||||||
Type: "TCPClientInterface",
|
Type: "TCPClientInterface",
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
17
misc/wasm/go_js_wasm_exec
Executable file
17
misc/wasm/go_js_wasm_exec
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Copyright 2018 The Go Authors. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style
|
||||||
|
# license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
SOURCE="${BASH_SOURCE[0]}"
|
||||||
|
while [ -h "$SOURCE" ]; do
|
||||||
|
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||||
|
SOURCE="$(readlink "$SOURCE")"
|
||||||
|
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
|
||||||
|
done
|
||||||
|
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||||
|
|
||||||
|
# Increase the V8 stack size from the default of 984K
|
||||||
|
# to 8192K to ensure all tests can pass without hitting
|
||||||
|
# stack size limits.
|
||||||
|
exec node --stack-size=8192 "$DIR/wasm_exec_node.js" "$@"
|
||||||
575
misc/wasm/wasm_exec.js
Normal file
575
misc/wasm/wasm_exec.js
Normal file
@@ -0,0 +1,575 @@
|
|||||||
|
// Copyright 2018 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const enosys = () => {
|
||||||
|
const err = new Error("not implemented");
|
||||||
|
err.code = "ENOSYS";
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!globalThis.fs) {
|
||||||
|
let outputBuf = "";
|
||||||
|
globalThis.fs = {
|
||||||
|
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused
|
||||||
|
writeSync(fd, buf) {
|
||||||
|
outputBuf += decoder.decode(buf);
|
||||||
|
const nl = outputBuf.lastIndexOf("\n");
|
||||||
|
if (nl != -1) {
|
||||||
|
console.log(outputBuf.substring(0, nl));
|
||||||
|
outputBuf = outputBuf.substring(nl + 1);
|
||||||
|
}
|
||||||
|
return buf.length;
|
||||||
|
},
|
||||||
|
write(fd, buf, offset, length, position, callback) {
|
||||||
|
if (offset !== 0 || length !== buf.length || position !== null) {
|
||||||
|
callback(enosys());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const n = this.writeSync(fd, buf);
|
||||||
|
callback(null, n);
|
||||||
|
},
|
||||||
|
chmod(path, mode, callback) { callback(enosys()); },
|
||||||
|
chown(path, uid, gid, callback) { callback(enosys()); },
|
||||||
|
close(fd, callback) { callback(enosys()); },
|
||||||
|
fchmod(fd, mode, callback) { callback(enosys()); },
|
||||||
|
fchown(fd, uid, gid, callback) { callback(enosys()); },
|
||||||
|
fstat(fd, callback) { callback(enosys()); },
|
||||||
|
fsync(fd, callback) { callback(null); },
|
||||||
|
ftruncate(fd, length, callback) { callback(enosys()); },
|
||||||
|
lchown(path, uid, gid, callback) { callback(enosys()); },
|
||||||
|
link(path, link, callback) { callback(enosys()); },
|
||||||
|
lstat(path, callback) { callback(enosys()); },
|
||||||
|
mkdir(path, perm, callback) { callback(enosys()); },
|
||||||
|
open(path, flags, mode, callback) { callback(enosys()); },
|
||||||
|
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
|
||||||
|
readdir(path, callback) { callback(enosys()); },
|
||||||
|
readlink(path, callback) { callback(enosys()); },
|
||||||
|
rename(from, to, callback) { callback(enosys()); },
|
||||||
|
rmdir(path, callback) { callback(enosys()); },
|
||||||
|
stat(path, callback) { callback(enosys()); },
|
||||||
|
symlink(path, link, callback) { callback(enosys()); },
|
||||||
|
truncate(path, length, callback) { callback(enosys()); },
|
||||||
|
unlink(path, callback) { callback(enosys()); },
|
||||||
|
utimes(path, atime, mtime, callback) { callback(enosys()); },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.process) {
|
||||||
|
globalThis.process = {
|
||||||
|
getuid() { return -1; },
|
||||||
|
getgid() { return -1; },
|
||||||
|
geteuid() { return -1; },
|
||||||
|
getegid() { return -1; },
|
||||||
|
getgroups() { throw enosys(); },
|
||||||
|
pid: -1,
|
||||||
|
ppid: -1,
|
||||||
|
umask() { throw enosys(); },
|
||||||
|
cwd() { throw enosys(); },
|
||||||
|
chdir() { throw enosys(); },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.path) {
|
||||||
|
globalThis.path = {
|
||||||
|
resolve(...pathSegments) {
|
||||||
|
return pathSegments.join("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.crypto) {
|
||||||
|
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.performance) {
|
||||||
|
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.TextEncoder) {
|
||||||
|
throw new Error("globalThis.TextEncoder is not available, polyfill required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.TextDecoder) {
|
||||||
|
throw new Error("globalThis.TextDecoder is not available, polyfill required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const encoder = new TextEncoder("utf-8");
|
||||||
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
|
||||||
|
globalThis.Go = class {
|
||||||
|
constructor() {
|
||||||
|
this.argv = ["js"];
|
||||||
|
this.env = {};
|
||||||
|
this.exit = (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
console.warn("exit code:", code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this._exitPromise = new Promise((resolve) => {
|
||||||
|
this._resolveExitPromise = resolve;
|
||||||
|
});
|
||||||
|
this._pendingEvent = null;
|
||||||
|
this._scheduledTimeouts = new Map();
|
||||||
|
this._nextCallbackTimeoutID = 1;
|
||||||
|
|
||||||
|
const setInt64 = (addr, v) => {
|
||||||
|
this.mem.setUint32(addr + 0, v, true);
|
||||||
|
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const setInt32 = (addr, v) => {
|
||||||
|
this.mem.setUint32(addr + 0, v, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInt64 = (addr) => {
|
||||||
|
const low = this.mem.getUint32(addr + 0, true);
|
||||||
|
const high = this.mem.getInt32(addr + 4, true);
|
||||||
|
return low + high * 4294967296;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadValue = (addr) => {
|
||||||
|
const f = this.mem.getFloat64(addr, true);
|
||||||
|
if (f === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!isNaN(f)) {
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = this.mem.getUint32(addr, true);
|
||||||
|
return this._values[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
const storeValue = (addr, v) => {
|
||||||
|
const nanHead = 0x7FF80000;
|
||||||
|
|
||||||
|
if (typeof v === "number" && v !== 0) {
|
||||||
|
if (isNaN(v)) {
|
||||||
|
this.mem.setUint32(addr + 4, nanHead, true);
|
||||||
|
this.mem.setUint32(addr, 0, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.mem.setFloat64(addr, v, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v === undefined) {
|
||||||
|
this.mem.setFloat64(addr, 0, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = this._ids.get(v);
|
||||||
|
if (id === undefined) {
|
||||||
|
id = this._idPool.pop();
|
||||||
|
if (id === undefined) {
|
||||||
|
id = this._values.length;
|
||||||
|
}
|
||||||
|
this._values[id] = v;
|
||||||
|
this._goRefCounts[id] = 0;
|
||||||
|
this._ids.set(v, id);
|
||||||
|
}
|
||||||
|
this._goRefCounts[id]++;
|
||||||
|
let typeFlag = 0;
|
||||||
|
switch (typeof v) {
|
||||||
|
case "object":
|
||||||
|
if (v !== null) {
|
||||||
|
typeFlag = 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "string":
|
||||||
|
typeFlag = 2;
|
||||||
|
break;
|
||||||
|
case "symbol":
|
||||||
|
typeFlag = 3;
|
||||||
|
break;
|
||||||
|
case "function":
|
||||||
|
typeFlag = 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
|
||||||
|
this.mem.setUint32(addr, id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSlice = (addr) => {
|
||||||
|
const array = getInt64(addr + 0);
|
||||||
|
const len = getInt64(addr + 8);
|
||||||
|
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSliceOfValues = (addr) => {
|
||||||
|
const array = getInt64(addr + 0);
|
||||||
|
const len = getInt64(addr + 8);
|
||||||
|
const a = new Array(len);
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
a[i] = loadValue(array + i * 8);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadString = (addr) => {
|
||||||
|
const saddr = getInt64(addr + 0);
|
||||||
|
const len = getInt64(addr + 8);
|
||||||
|
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
|
||||||
|
}
|
||||||
|
|
||||||
|
const testCallExport = (a, b) => {
|
||||||
|
this._inst.exports.testExport0();
|
||||||
|
return this._inst.exports.testExport(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeOrigin = Date.now() - performance.now();
|
||||||
|
this.importObject = {
|
||||||
|
_gotest: {
|
||||||
|
add: (a, b) => a + b,
|
||||||
|
callExport: testCallExport,
|
||||||
|
},
|
||||||
|
gojs: {
|
||||||
|
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
|
||||||
|
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
|
||||||
|
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
|
||||||
|
// This changes the SP, thus we have to update the SP used by the imported function.
|
||||||
|
|
||||||
|
// func wasmExit(code int32)
|
||||||
|
"runtime.wasmExit": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const code = this.mem.getInt32(sp + 8, true);
|
||||||
|
this.exited = true;
|
||||||
|
delete this._inst;
|
||||||
|
delete this._values;
|
||||||
|
delete this._goRefCounts;
|
||||||
|
delete this._ids;
|
||||||
|
delete this._idPool;
|
||||||
|
this.exit(code);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
|
||||||
|
"runtime.wasmWrite": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const fd = getInt64(sp + 8);
|
||||||
|
const p = getInt64(sp + 16);
|
||||||
|
const n = this.mem.getInt32(sp + 24, true);
|
||||||
|
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
|
||||||
|
},
|
||||||
|
|
||||||
|
// func resetMemoryDataView()
|
||||||
|
"runtime.resetMemoryDataView": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func nanotime1() int64
|
||||||
|
"runtime.nanotime1": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func walltime() (sec int64, nsec int32)
|
||||||
|
"runtime.walltime": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const msec = (new Date).getTime();
|
||||||
|
setInt64(sp + 8, msec / 1000);
|
||||||
|
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func scheduleTimeoutEvent(delay int64) int32
|
||||||
|
"runtime.scheduleTimeoutEvent": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const id = this._nextCallbackTimeoutID;
|
||||||
|
this._nextCallbackTimeoutID++;
|
||||||
|
this._scheduledTimeouts.set(id, setTimeout(
|
||||||
|
() => {
|
||||||
|
this._resume();
|
||||||
|
while (this._scheduledTimeouts.has(id)) {
|
||||||
|
// for some reason Go failed to register the timeout event, log and try again
|
||||||
|
// (temporary workaround for https://github.com/golang/go/issues/28975)
|
||||||
|
console.warn("scheduleTimeoutEvent: missed timeout event");
|
||||||
|
this._resume();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getInt64(sp + 8),
|
||||||
|
));
|
||||||
|
this.mem.setInt32(sp + 16, id, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func clearTimeoutEvent(id int32)
|
||||||
|
"runtime.clearTimeoutEvent": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const id = this.mem.getInt32(sp + 8, true);
|
||||||
|
clearTimeout(this._scheduledTimeouts.get(id));
|
||||||
|
this._scheduledTimeouts.delete(id);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func getRandomData(r []byte)
|
||||||
|
"runtime.getRandomData": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
crypto.getRandomValues(loadSlice(sp + 8));
|
||||||
|
},
|
||||||
|
|
||||||
|
// func finalizeRef(v ref)
|
||||||
|
"syscall/js.finalizeRef": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const id = this.mem.getUint32(sp + 8, true);
|
||||||
|
this._goRefCounts[id]--;
|
||||||
|
if (this._goRefCounts[id] === 0) {
|
||||||
|
const v = this._values[id];
|
||||||
|
this._values[id] = null;
|
||||||
|
this._ids.delete(v);
|
||||||
|
this._idPool.push(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// func stringVal(value string) ref
|
||||||
|
"syscall/js.stringVal": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
storeValue(sp + 24, loadString(sp + 8));
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueGet(v ref, p string) ref
|
||||||
|
"syscall/js.valueGet": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 32, result);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueSet(v ref, p string, x ref)
|
||||||
|
"syscall/js.valueSet": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueDelete(v ref, p string)
|
||||||
|
"syscall/js.valueDelete": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueIndex(v ref, i int) ref
|
||||||
|
"syscall/js.valueIndex": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
|
||||||
|
},
|
||||||
|
|
||||||
|
// valueSetIndex(v ref, i int, x ref)
|
||||||
|
"syscall/js.valueSetIndex": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueCall(v ref, m string, args []ref) (ref, bool)
|
||||||
|
"syscall/js.valueCall": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
try {
|
||||||
|
const v = loadValue(sp + 8);
|
||||||
|
const m = Reflect.get(v, loadString(sp + 16));
|
||||||
|
const args = loadSliceOfValues(sp + 32);
|
||||||
|
const result = Reflect.apply(m, v, args);
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 56, result);
|
||||||
|
this.mem.setUint8(sp + 64, 1);
|
||||||
|
} catch (err) {
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 56, err);
|
||||||
|
this.mem.setUint8(sp + 64, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueInvoke(v ref, args []ref) (ref, bool)
|
||||||
|
"syscall/js.valueInvoke": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
try {
|
||||||
|
const v = loadValue(sp + 8);
|
||||||
|
const args = loadSliceOfValues(sp + 16);
|
||||||
|
const result = Reflect.apply(v, undefined, args);
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 40, result);
|
||||||
|
this.mem.setUint8(sp + 48, 1);
|
||||||
|
} catch (err) {
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 40, err);
|
||||||
|
this.mem.setUint8(sp + 48, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueNew(v ref, args []ref) (ref, bool)
|
||||||
|
"syscall/js.valueNew": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
try {
|
||||||
|
const v = loadValue(sp + 8);
|
||||||
|
const args = loadSliceOfValues(sp + 16);
|
||||||
|
const result = Reflect.construct(v, args);
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 40, result);
|
||||||
|
this.mem.setUint8(sp + 48, 1);
|
||||||
|
} catch (err) {
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 40, err);
|
||||||
|
this.mem.setUint8(sp + 48, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueLength(v ref) int
|
||||||
|
"syscall/js.valueLength": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
|
||||||
|
},
|
||||||
|
|
||||||
|
// valuePrepareString(v ref) (ref, int)
|
||||||
|
"syscall/js.valuePrepareString": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const str = encoder.encode(String(loadValue(sp + 8)));
|
||||||
|
storeValue(sp + 16, str);
|
||||||
|
setInt64(sp + 24, str.length);
|
||||||
|
},
|
||||||
|
|
||||||
|
// valueLoadString(v ref, b []byte)
|
||||||
|
"syscall/js.valueLoadString": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const str = loadValue(sp + 8);
|
||||||
|
loadSlice(sp + 16).set(str);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueInstanceOf(v ref, t ref) bool
|
||||||
|
"syscall/js.valueInstanceOf": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||||
|
"syscall/js.copyBytesToGo": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const dst = loadSlice(sp + 8);
|
||||||
|
const src = loadValue(sp + 32);
|
||||||
|
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
|
||||||
|
this.mem.setUint8(sp + 48, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const toCopy = src.subarray(0, dst.length);
|
||||||
|
dst.set(toCopy);
|
||||||
|
setInt64(sp + 40, toCopy.length);
|
||||||
|
this.mem.setUint8(sp + 48, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||||
|
"syscall/js.copyBytesToJS": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const dst = loadValue(sp + 8);
|
||||||
|
const src = loadSlice(sp + 16);
|
||||||
|
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
|
||||||
|
this.mem.setUint8(sp + 48, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const toCopy = src.subarray(0, dst.length);
|
||||||
|
dst.set(toCopy);
|
||||||
|
setInt64(sp + 40, toCopy.length);
|
||||||
|
this.mem.setUint8(sp + 48, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
"debug": (value) => {
|
||||||
|
console.log(value);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(instance) {
|
||||||
|
if (!(instance instanceof WebAssembly.Instance)) {
|
||||||
|
throw new Error("Go.run: WebAssembly.Instance expected");
|
||||||
|
}
|
||||||
|
this._inst = instance;
|
||||||
|
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||||
|
this._values = [ // JS values that Go currently has references to, indexed by reference id
|
||||||
|
NaN,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
globalThis,
|
||||||
|
this,
|
||||||
|
];
|
||||||
|
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
|
||||||
|
this._ids = new Map([ // mapping from JS values to reference ids
|
||||||
|
[0, 1],
|
||||||
|
[null, 2],
|
||||||
|
[true, 3],
|
||||||
|
[false, 4],
|
||||||
|
[globalThis, 5],
|
||||||
|
[this, 6],
|
||||||
|
]);
|
||||||
|
this._idPool = []; // unused ids that have been garbage collected
|
||||||
|
this.exited = false; // whether the Go program has exited
|
||||||
|
|
||||||
|
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
|
||||||
|
let offset = 4096;
|
||||||
|
|
||||||
|
const strPtr = (str) => {
|
||||||
|
const ptr = offset;
|
||||||
|
const bytes = encoder.encode(str + "\0");
|
||||||
|
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
|
||||||
|
offset += bytes.length;
|
||||||
|
if (offset % 8 !== 0) {
|
||||||
|
offset += 8 - (offset % 8);
|
||||||
|
}
|
||||||
|
return ptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
const argc = this.argv.length;
|
||||||
|
|
||||||
|
const argvPtrs = [];
|
||||||
|
this.argv.forEach((arg) => {
|
||||||
|
argvPtrs.push(strPtr(arg));
|
||||||
|
});
|
||||||
|
argvPtrs.push(0);
|
||||||
|
|
||||||
|
const keys = Object.keys(this.env).sort();
|
||||||
|
keys.forEach((key) => {
|
||||||
|
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
|
||||||
|
});
|
||||||
|
argvPtrs.push(0);
|
||||||
|
|
||||||
|
const argv = offset;
|
||||||
|
argvPtrs.forEach((ptr) => {
|
||||||
|
this.mem.setUint32(offset, ptr, true);
|
||||||
|
this.mem.setUint32(offset + 4, 0, true);
|
||||||
|
offset += 8;
|
||||||
|
});
|
||||||
|
|
||||||
|
// The linker guarantees global data starts from at least wasmMinDataAddr.
|
||||||
|
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
|
||||||
|
const wasmMinDataAddr = 4096 + 8192;
|
||||||
|
if (offset >= wasmMinDataAddr) {
|
||||||
|
throw new Error("total length of command line and environment variables exceeds limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
this._inst.exports.run(argc, argv);
|
||||||
|
if (this.exited) {
|
||||||
|
this._resolveExitPromise();
|
||||||
|
}
|
||||||
|
await this._exitPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
_resume() {
|
||||||
|
if (this.exited) {
|
||||||
|
throw new Error("Go program has already exited");
|
||||||
|
}
|
||||||
|
this._inst.exports.resume();
|
||||||
|
if (this.exited) {
|
||||||
|
this._resolveExitPromise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_makeFuncWrapper(id) {
|
||||||
|
const go = this;
|
||||||
|
return function () {
|
||||||
|
const event = { id: id, this: this, args: arguments };
|
||||||
|
go._pendingEvent = event;
|
||||||
|
go._resume();
|
||||||
|
return event.result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
41
misc/wasm/wasm_exec_node.js
Normal file
41
misc/wasm/wasm_exec_node.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// Copyright 2021 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
if (process.argv.length < 3) {
|
||||||
|
console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.require = require;
|
||||||
|
globalThis.fs = require("fs");
|
||||||
|
globalThis.path = require("path");
|
||||||
|
globalThis.TextEncoder = require("util").TextEncoder;
|
||||||
|
globalThis.TextDecoder = require("util").TextDecoder;
|
||||||
|
|
||||||
|
globalThis.performance ??= require("performance");
|
||||||
|
|
||||||
|
globalThis.crypto ??= require("crypto");
|
||||||
|
|
||||||
|
require("./wasm_exec");
|
||||||
|
|
||||||
|
const go = new Go();
|
||||||
|
go.argv = process.argv.slice(2);
|
||||||
|
go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
|
||||||
|
go.exit = process.exit;
|
||||||
|
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
|
||||||
|
process.on("exit", (code) => { // Node.js exits if no event handler is pending
|
||||||
|
if (code === 0 && !go.exited) {
|
||||||
|
// deadlock, make Go print error and stack traces
|
||||||
|
go._pendingEvent = { id: 0 };
|
||||||
|
go._resume();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return go.run(result.instance);
|
||||||
|
}).catch((err) => {
|
||||||
|
// bearer:disable javascript_lang_logger_leak
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package announce
|
package announce
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -49,12 +51,6 @@ const (
|
|||||||
MAX_RETRIES = 3
|
MAX_RETRIES = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
type AnnounceHandler interface {
|
|
||||||
AspectFilter() []string
|
|
||||||
ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte) error
|
|
||||||
ReceivePathResponses() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Announce struct {
|
type Announce struct {
|
||||||
mutex *sync.RWMutex
|
mutex *sync.RWMutex
|
||||||
destinationHash []byte
|
destinationHash []byte
|
||||||
@@ -67,7 +63,7 @@ type Announce struct {
|
|||||||
signature []byte
|
signature []byte
|
||||||
pathResponse bool
|
pathResponse bool
|
||||||
retries int
|
retries int
|
||||||
handlers []AnnounceHandler
|
handlers []Handler
|
||||||
ratchetID []byte
|
ratchetID []byte
|
||||||
packet []byte
|
packet []byte
|
||||||
hash []byte
|
hash []byte
|
||||||
@@ -97,7 +93,7 @@ func New(dest *identity.Identity, destinationHash []byte, destinationName string
|
|||||||
timestamp: time.Now().Unix(),
|
timestamp: time.Now().Unix(),
|
||||||
pathResponse: pathResponse,
|
pathResponse: pathResponse,
|
||||||
retries: 0,
|
retries: 0,
|
||||||
handlers: make([]AnnounceHandler, 0),
|
handlers: make([]Handler, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current ratchet ID if enabled
|
// Get current ratchet ID if enabled
|
||||||
@@ -156,13 +152,13 @@ func (a *Announce) Propagate(interfaces []common.NetworkInterface) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Announce) RegisterHandler(handler AnnounceHandler) {
|
func (a *Announce) RegisterHandler(handler Handler) {
|
||||||
a.mutex.Lock()
|
a.mutex.Lock()
|
||||||
defer a.mutex.Unlock()
|
defer a.mutex.Unlock()
|
||||||
a.handlers = append(a.handlers, handler)
|
a.handlers = append(a.handlers, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Announce) DeregisterHandler(handler AnnounceHandler) {
|
func (a *Announce) DeregisterHandler(handler Handler) {
|
||||||
a.mutex.Lock()
|
a.mutex.Lock()
|
||||||
defer a.mutex.Unlock()
|
defer a.mutex.Unlock()
|
||||||
for i, h := range a.handlers {
|
for i, h := range a.handlers {
|
||||||
@@ -283,7 +279,7 @@ func (a *Announce) HandleAnnounce(data []byte) error {
|
|||||||
// Process with handlers
|
// Process with handlers
|
||||||
for _, handler := range a.handlers {
|
for _, handler := range a.handlers {
|
||||||
if handler.ReceivePathResponses() || !a.pathResponse {
|
if handler.ReceivePathResponses() || !a.pathResponse {
|
||||||
if err := handler.ReceivedAnnounce(destHash, announcedIdentity, appData); err != nil {
|
if err := handler.ReceivedAnnounce(destHash, announcedIdentity, appData, hopCount); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -480,7 +476,7 @@ func NewAnnounce(identity *identity.Identity, destinationHash []byte, appData []
|
|||||||
destinationHash: destHash,
|
destinationHash: destHash,
|
||||||
hops: 0,
|
hops: 0,
|
||||||
mutex: &sync.RWMutex{},
|
mutex: &sync.RWMutex{},
|
||||||
handlers: make([]AnnounceHandler, 0),
|
handlers: make([]Handler, 0),
|
||||||
config: config,
|
config: config,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func (m *mockAnnounceHandler) AspectFilter() []string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockAnnounceHandler) ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte) error {
|
func (m *mockAnnounceHandler) ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte, hops uint8) error {
|
||||||
m.received = true
|
m.received = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package announce
|
package announce
|
||||||
|
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
AspectFilter() []string
|
AspectFilter() []string
|
||||||
ReceivedAnnounce(destHash []byte, identity interface{}, appData []byte) error
|
ReceivedAnnounce(destHash []byte, identity interface{}, appData []byte, hops uint8) error
|
||||||
ReceivePathResponses() bool
|
ReceivePathResponses() bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package buffer
|
package buffer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package channel
|
package channel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -19,25 +21,26 @@ type ConfigProvider interface {
|
|||||||
|
|
||||||
// InterfaceConfig represents interface configuration
|
// InterfaceConfig represents interface configuration
|
||||||
type InterfaceConfig struct {
|
type InterfaceConfig struct {
|
||||||
Name string
|
Name string
|
||||||
Type string
|
Type string
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Address string
|
Address string
|
||||||
Port int
|
Port int
|
||||||
TargetHost string
|
TargetHost string
|
||||||
TargetPort int
|
TargetPort int
|
||||||
TargetAddress string
|
TargetAddress string
|
||||||
Interface string
|
Interface string
|
||||||
KISSFraming bool
|
KISSFraming bool
|
||||||
I2PTunneled bool
|
I2PTunneled bool
|
||||||
PreferIPv6 bool
|
PreferIPv6 bool
|
||||||
MaxReconnTries int
|
MaxReconnTries int
|
||||||
Bitrate int64
|
Bitrate int64
|
||||||
MTU int
|
MTU int
|
||||||
GroupID string
|
GroupID string
|
||||||
DiscoveryScope string
|
DiscoveryScope string
|
||||||
DiscoveryPort int
|
DiscoveryPort int
|
||||||
DataPort int
|
DataPort int
|
||||||
|
MulticastAddrType string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReticulumConfig represents the main configuration structure
|
// ReticulumConfig represents the main configuration structure
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package common
|
package common
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -37,6 +39,10 @@ type NetworkInterface interface {
|
|||||||
SendLinkPacket([]byte, []byte, time.Time) error
|
SendLinkPacket([]byte, []byte, time.Time) error
|
||||||
SetPacketCallback(PacketCallback)
|
SetPacketCallback(PacketCallback)
|
||||||
GetPacketCallback() PacketCallback
|
GetPacketCallback() PacketCallback
|
||||||
|
GetTxBytes() uint64
|
||||||
|
GetRxBytes() uint64
|
||||||
|
GetTxPackets() uint64
|
||||||
|
GetRxPackets() uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// BaseInterface provides common implementation for network interfaces
|
// BaseInterface provides common implementation for network interfaces
|
||||||
@@ -54,9 +60,11 @@ type BaseInterface struct {
|
|||||||
MTU int
|
MTU int
|
||||||
Bitrate int64
|
Bitrate int64
|
||||||
|
|
||||||
TxBytes uint64
|
TxBytes uint64
|
||||||
RxBytes uint64
|
RxBytes uint64
|
||||||
lastTx time.Time
|
TxPackets uint64
|
||||||
|
RxPackets uint64
|
||||||
|
lastTx time.Time
|
||||||
|
|
||||||
Mutex sync.RWMutex
|
Mutex sync.RWMutex
|
||||||
Owner interface{}
|
Owner interface{}
|
||||||
@@ -123,6 +131,30 @@ func (i *BaseInterface) GetPacketCallback() PacketCallback {
|
|||||||
return i.PacketCallback
|
return i.PacketCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetTxBytes() uint64 {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.TxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetRxBytes() uint64 {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.RxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetTxPackets() uint64 {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.TxPackets
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetRxPackets() uint64 {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.RxPackets
|
||||||
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) Detach() {
|
func (i *BaseInterface) Detach() {
|
||||||
i.Mutex.Lock()
|
i.Mutex.Lock()
|
||||||
defer i.Mutex.Unlock()
|
defer i.Mutex.Unlock()
|
||||||
@@ -158,10 +190,20 @@ func (i *BaseInterface) GetConn() net.Conn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) Send(data []byte, address string) error {
|
func (i *BaseInterface) Send(data []byte, address string) error {
|
||||||
|
i.Mutex.Lock()
|
||||||
|
i.TxBytes += uint64(len(data))
|
||||||
|
i.TxPackets++
|
||||||
|
i.lastTx = time.Now()
|
||||||
|
i.Mutex.Unlock()
|
||||||
return i.ProcessOutgoing(data)
|
return i.ProcessOutgoing(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
||||||
|
i.Mutex.Lock()
|
||||||
|
i.RxBytes += uint64(len(data))
|
||||||
|
i.RxPackets++
|
||||||
|
i.Mutex.Unlock()
|
||||||
|
|
||||||
if i.PacketCallback != nil {
|
if i.PacketCallback != nil {
|
||||||
i.PacketCallback(data, i)
|
i.PacketCallback(data, i)
|
||||||
}
|
}
|
||||||
@@ -181,12 +223,10 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
|
|||||||
packet = append(packet, 0x02) // Link packet type
|
packet = append(packet, 0x02) // Link packet type
|
||||||
packet = append(packet, dest...)
|
packet = append(packet, dest...)
|
||||||
|
|
||||||
// Add timestamp
|
|
||||||
ts := make([]byte, 8)
|
ts := make([]byte, 8)
|
||||||
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) // #nosec G115
|
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) // #nosec G115
|
||||||
packet = append(packet, ts...)
|
packet = append(packet, ts...)
|
||||||
|
|
||||||
// Add data
|
|
||||||
packet = append(packet, data...)
|
packet = append(packet, data...)
|
||||||
|
|
||||||
return i.Send(packet, "")
|
return i.Send(packet, "")
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -223,7 +225,6 @@ func InitConfig() (*Config, error) {
|
|||||||
cfg.Logging.Level = "info"
|
cfg.Logging.Level = "info"
|
||||||
cfg.Logging.File = filepath.Join(GetConfigDir(), "reticulum.log")
|
cfg.Logging.File = filepath.Join(GetConfigDir(), "reticulum.log")
|
||||||
|
|
||||||
// Add default interfaces
|
|
||||||
cfg.Interfaces = append(cfg.Interfaces, struct {
|
cfg.Interfaces = append(cfg.Interfaces, struct {
|
||||||
Name string
|
Name string
|
||||||
Type string
|
Type string
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package cryptography
|
package cryptography
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package cryptography
|
package cryptography
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package cryptography
|
package cryptography
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package cryptography
|
package cryptography
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package cryptography
|
package cryptography
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package cryptography
|
package cryptography
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package debug
|
package debug
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package destination
|
package destination
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -105,9 +107,9 @@ type Destination struct {
|
|||||||
func New(id *identity.Identity, direction byte, destType byte, appName string, transport Transport, aspects ...string) (*Destination, error) {
|
func New(id *identity.Identity, direction byte, destType byte, appName string, transport Transport, aspects ...string) (*Destination, error) {
|
||||||
debug.Log(debug.DEBUG_INFO, "Creating new destination", "app", appName, "type", destType, "direction", direction)
|
debug.Log(debug.DEBUG_INFO, "Creating new destination", "app", appName, "type", destType, "direction", direction)
|
||||||
|
|
||||||
if id == nil {
|
if id == nil && destType != PLAIN {
|
||||||
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil")
|
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil for non-PLAIN destination")
|
||||||
return nil, errors.New("identity cannot be nil")
|
return nil, errors.New("identity cannot be nil for non-PLAIN destination")
|
||||||
}
|
}
|
||||||
|
|
||||||
d := &Destination{
|
d := &Destination{
|
||||||
@@ -142,9 +144,9 @@ func New(id *identity.Identity, direction byte, destType byte, appName string, t
|
|||||||
func FromHash(hash []byte, id *identity.Identity, destType byte, transport Transport) (*Destination, error) {
|
func FromHash(hash []byte, id *identity.Identity, destType byte, transport Transport) (*Destination, error) {
|
||||||
debug.Log(debug.DEBUG_INFO, "Creating destination from hash", "hash", fmt.Sprintf("%x", hash))
|
debug.Log(debug.DEBUG_INFO, "Creating destination from hash", "hash", fmt.Sprintf("%x", hash))
|
||||||
|
|
||||||
if id == nil {
|
if id == nil && destType != PLAIN {
|
||||||
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil")
|
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil for non-PLAIN destination")
|
||||||
return nil, errors.New("identity cannot be nil")
|
return nil, errors.New("identity cannot be nil for non-PLAIN destination")
|
||||||
}
|
}
|
||||||
|
|
||||||
d := &Destination{
|
d := &Destination{
|
||||||
@@ -167,19 +169,25 @@ func FromHash(hash []byte, id *identity.Identity, destType byte, transport Trans
|
|||||||
func (d *Destination) calculateHash() []byte {
|
func (d *Destination) calculateHash() []byte {
|
||||||
debug.Log(debug.DEBUG_TRACE, "Calculating hash for destination", "name", d.ExpandName())
|
debug.Log(debug.DEBUG_TRACE, "Calculating hash for destination", "name", d.ExpandName())
|
||||||
|
|
||||||
// destination_hash = SHA256(name_hash_10bytes + identity_hash_16bytes)[:16]
|
|
||||||
// Identity hash is the truncated hash of the public key (16 bytes)
|
|
||||||
identityHash := identity.TruncatedHash(d.identity.GetPublicKey())
|
|
||||||
|
|
||||||
// Name hash is the FULL 32-byte SHA256, then we take first 10 bytes for concatenation
|
// Name hash is the FULL 32-byte SHA256, then we take first 10 bytes for concatenation
|
||||||
nameHashFull := sha256.Sum256([]byte(d.ExpandName()))
|
nameHashFull := sha256.Sum256([]byte(d.ExpandName()))
|
||||||
nameHash10 := nameHashFull[:10] // Only use 10 bytes
|
nameHash10 := nameHashFull[:10] // Only use 10 bytes
|
||||||
|
|
||||||
debug.Log(debug.DEBUG_ALL, "Identity hash", "hash", fmt.Sprintf("%x", identityHash))
|
var combined []byte
|
||||||
debug.Log(debug.DEBUG_ALL, "Name hash (10 bytes)", "hash", fmt.Sprintf("%x", nameHash10))
|
if d.identity != nil {
|
||||||
|
// destination_hash = SHA256(name_hash_10bytes + identity_hash_16bytes)[:16]
|
||||||
|
// Identity hash is the truncated hash of the public key (16 bytes)
|
||||||
|
identityHash := identity.TruncatedHash(d.identity.GetPublicKey())
|
||||||
|
debug.Log(debug.DEBUG_ALL, "Identity hash", "hash", fmt.Sprintf("%x", identityHash))
|
||||||
|
debug.Log(debug.DEBUG_ALL, "Name hash (10 bytes)", "hash", fmt.Sprintf("%x", nameHash10))
|
||||||
|
|
||||||
// Concatenate name_hash (10 bytes) + identity_hash (16 bytes) = 26 bytes
|
// Concatenate name_hash (10 bytes) + identity_hash (16 bytes) = 26 bytes
|
||||||
combined := append(nameHash10, identityHash...)
|
combined = append(nameHash10, identityHash...)
|
||||||
|
} else {
|
||||||
|
// PLAIN destination has no identity hash
|
||||||
|
combined = nameHash10
|
||||||
|
debug.Log(debug.DEBUG_ALL, "Name hash (10 bytes)", "hash", fmt.Sprintf("%x", nameHash10))
|
||||||
|
}
|
||||||
|
|
||||||
// Then hash again and truncate to 16 bytes
|
// Then hash again and truncate to 16 bytes
|
||||||
finalHashFull := sha256.Sum256(combined)
|
finalHashFull := sha256.Sum256(combined)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package destination
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -150,3 +151,28 @@ func TestPlainDestination(t *testing.T) {
|
|||||||
t.Error("Plain destination should not decrypt")
|
t.Error("Plain destination should not decrypt")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPlainDestinationHash(t *testing.T) {
|
||||||
|
// A PLAIN destination with no identity should have a hash based only on its name
|
||||||
|
transport := &mockTransport{}
|
||||||
|
dest, err := New(nil, IN|OUT, PLAIN, "testapp", transport, "testaspect")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := dest.GetHash()
|
||||||
|
if len(hash) != 16 {
|
||||||
|
t.Fatalf("Expected hash length 16, got %d", len(hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate manually: SHA256(SHA256("testapp.testaspect")[:10])[:16]
|
||||||
|
name := "testapp.testaspect"
|
||||||
|
nameHashFull := sha256.Sum256([]byte(name))
|
||||||
|
nameHash10 := nameHashFull[:10]
|
||||||
|
finalHashFull := sha256.Sum256(nameHash10)
|
||||||
|
expectedHash := finalHashFull[:16]
|
||||||
|
|
||||||
|
if !bytes.Equal(hash, expectedHash) {
|
||||||
|
t.Errorf("Expected hash %x, got %x", expectedHash, hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package identity
|
package identity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -56,9 +58,10 @@ type Identity struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
knownDestinations = make(map[string][]interface{})
|
knownDestinations = make(map[string][]interface{})
|
||||||
knownRatchets = make(map[string][]byte)
|
knownDestinationsLock sync.RWMutex
|
||||||
ratchetPersistLock sync.Mutex
|
knownRatchets = make(map[string][]byte)
|
||||||
|
ratchetPersistLock sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
func New() (*Identity, error) {
|
func New() (*Identity, error) {
|
||||||
@@ -189,12 +192,14 @@ func Remember(packet []byte, destHash []byte, publicKey []byte, appData []byte)
|
|||||||
|
|
||||||
// Store destination data as [packet, destHash, identity, appData]
|
// Store destination data as [packet, destHash, identity, appData]
|
||||||
id := FromPublicKey(publicKey)
|
id := FromPublicKey(publicKey)
|
||||||
|
knownDestinationsLock.Lock()
|
||||||
knownDestinations[hashStr] = []interface{}{
|
knownDestinations[hashStr] = []interface{}{
|
||||||
packet,
|
packet,
|
||||||
destHash,
|
destHash,
|
||||||
id,
|
id,
|
||||||
appData,
|
appData,
|
||||||
}
|
}
|
||||||
|
knownDestinationsLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidateAnnounce(packet []byte, destHash []byte, publicKey []byte, signature []byte, appData []byte) bool {
|
func ValidateAnnounce(packet []byte, destHash []byte, publicKey []byte, signature []byte, appData []byte) bool {
|
||||||
@@ -251,7 +256,11 @@ func (i *Identity) String() string {
|
|||||||
func Recall(hash []byte) (*Identity, error) {
|
func Recall(hash []byte) (*Identity, error) {
|
||||||
hashStr := hex.EncodeToString(hash)
|
hashStr := hex.EncodeToString(hash)
|
||||||
|
|
||||||
if data, exists := knownDestinations[hashStr]; exists {
|
knownDestinationsLock.RLock()
|
||||||
|
data, exists := knownDestinations[hashStr]
|
||||||
|
knownDestinationsLock.RUnlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
// data is [packet, destHash, identity, appData]
|
// data is [packet, destHash, identity, appData]
|
||||||
if len(data) >= 3 {
|
if len(data) >= 3 {
|
||||||
if id, ok := data[2].(*Identity); ok {
|
if id, ok := data[2].(*Identity); ok {
|
||||||
@@ -636,7 +645,6 @@ func (i *Identity) loadPrivateKey(privateKey, signingSeed []byte) error {
|
|||||||
signingKey := ed25519.NewKeyFromSeed(i.signingSeed)
|
signingKey := ed25519.NewKeyFromSeed(i.signingSeed)
|
||||||
i.verificationKey = signingKey.Public().(ed25519.PublicKey)
|
i.verificationKey = signingKey.Public().(ed25519.PublicKey)
|
||||||
|
|
||||||
// Update hash
|
|
||||||
publicKeyBytes := make([]byte, 0, len(i.publicKey)+len(i.verificationKey))
|
publicKeyBytes := make([]byte, 0, len(i.publicKey)+len(i.verificationKey))
|
||||||
publicKeyBytes = append(publicKeyBytes, i.publicKey...)
|
publicKeyBytes = append(publicKeyBytes, i.publicKey...)
|
||||||
publicKeyBytes = append(publicKeyBytes, i.verificationKey...)
|
publicKeyBytes = append(publicKeyBytes, i.verificationKey...)
|
||||||
@@ -854,7 +862,10 @@ func (i *Identity) GetRatchetID(ratchetPubBytes []byte) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetKnownDestination(hash string) ([]interface{}, bool) {
|
func GetKnownDestination(hash string) ([]interface{}, bool) {
|
||||||
if data, exists := knownDestinations[hash]; exists {
|
knownDestinationsLock.RLock()
|
||||||
|
data, exists := knownDestinations[hash]
|
||||||
|
knownDestinationsLock.RUnlock()
|
||||||
|
if exists {
|
||||||
return data, true
|
return data, true
|
||||||
}
|
}
|
||||||
return nil, false
|
return nil, false
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package interfaces
|
package interfaces
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -32,8 +34,16 @@ const (
|
|||||||
|
|
||||||
MCAST_ADDR_TYPE_PERMANENT = "0"
|
MCAST_ADDR_TYPE_PERMANENT = "0"
|
||||||
MCAST_ADDR_TYPE_TEMPORARY = "1"
|
MCAST_ADDR_TYPE_TEMPORARY = "1"
|
||||||
|
|
||||||
|
MULTI_IF_DEQUE_LEN = 48
|
||||||
|
MULTI_IF_DEQUE_TTL = 750 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type DequeEntry struct {
|
||||||
|
hash [32]byte
|
||||||
|
timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
type AutoInterface struct {
|
type AutoInterface struct {
|
||||||
BaseInterface
|
BaseInterface
|
||||||
groupID []byte
|
groupID []byte
|
||||||
@@ -43,7 +53,6 @@ type AutoInterface struct {
|
|||||||
discoveryScope string
|
discoveryScope string
|
||||||
multicastAddrType string
|
multicastAddrType string
|
||||||
mcastDiscoveryAddr string
|
mcastDiscoveryAddr string
|
||||||
ifacNetname string
|
|
||||||
peers map[string]*Peer
|
peers map[string]*Peer
|
||||||
linkLocalAddrs []string
|
linkLocalAddrs []string
|
||||||
adoptedInterfaces map[string]*AdoptedInterface
|
adoptedInterfaces map[string]*AdoptedInterface
|
||||||
@@ -53,12 +62,14 @@ type AutoInterface struct {
|
|||||||
timedOutInterfaces map[string]time.Time
|
timedOutInterfaces map[string]time.Time
|
||||||
allowedInterfaces []string
|
allowedInterfaces []string
|
||||||
ignoredInterfaces []string
|
ignoredInterfaces []string
|
||||||
mutex sync.RWMutex
|
|
||||||
outboundConn *net.UDPConn
|
outboundConn *net.UDPConn
|
||||||
announceInterval time.Duration
|
announceInterval time.Duration
|
||||||
peerJobInterval time.Duration
|
peerJobInterval time.Duration
|
||||||
peeringTimeout time.Duration
|
peeringTimeout time.Duration
|
||||||
mcastEchoTimeout time.Duration
|
mcastEchoTimeout time.Duration
|
||||||
|
mifDeque []DequeEntry
|
||||||
|
done chan struct{}
|
||||||
|
stopOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdoptedInterface struct {
|
type AdoptedInterface struct {
|
||||||
@@ -73,6 +84,24 @@ type Peer struct {
|
|||||||
addr *net.UDPAddr
|
addr *net.UDPAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func descopeLinkLocal(addr string) string {
|
||||||
|
// Drop scope specifier expressed as %ifname (macOS)
|
||||||
|
if i := strings.Index(addr, "%"); i != -1 {
|
||||||
|
addr = addr[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop embedded scope specifier (NetBSD, OpenBSD)
|
||||||
|
// Python: re.sub(r"fe80:[0-9a-f]*::","fe80::", link_local_addr)
|
||||||
|
if strings.HasPrefix(addr, "fe80:") {
|
||||||
|
parts := strings.Split(addr, ":")
|
||||||
|
// Check for fe80:[scope]::...
|
||||||
|
if len(parts) >= 3 && parts[2] == "" && parts[1] != "" {
|
||||||
|
return "fe80::" + strings.Join(parts[3:], ":")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterface, error) {
|
func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterface, error) {
|
||||||
groupID := DEFAULT_GROUP_ID
|
groupID := DEFAULT_GROUP_ID
|
||||||
if config.GroupID != "" {
|
if config.GroupID != "" {
|
||||||
@@ -85,6 +114,9 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
|
|||||||
}
|
}
|
||||||
|
|
||||||
multicastAddrType := MCAST_ADDR_TYPE_TEMPORARY
|
multicastAddrType := MCAST_ADDR_TYPE_TEMPORARY
|
||||||
|
if config.MulticastAddrType != "" {
|
||||||
|
multicastAddrType = normalizeMulticastType(config.MulticastAddrType)
|
||||||
|
}
|
||||||
|
|
||||||
discoveryPort := DEFAULT_DISCOVERY_PORT
|
discoveryPort := DEFAULT_DISCOVERY_PORT
|
||||||
if config.DiscoveryPort != 0 {
|
if config.DiscoveryPort != 0 {
|
||||||
@@ -98,8 +130,13 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
|
|||||||
|
|
||||||
groupHash := sha256.Sum256([]byte(groupID))
|
groupHash := sha256.Sum256([]byte(groupID))
|
||||||
|
|
||||||
ifacNetname := hex.EncodeToString(groupHash[:])[:16]
|
// Python-compatible multicast address generation
|
||||||
mcastAddr := fmt.Sprintf("ff%s%s::%s", discoveryScope, multicastAddrType, ifacNetname)
|
// gt = "0:" + "{:02x}".format(g[3]+(g[2]<<8)) + ":" + ...
|
||||||
|
gt := "0"
|
||||||
|
for i := 1; i <= 6; i++ {
|
||||||
|
gt += fmt.Sprintf(":%02x%02x", groupHash[i*2], groupHash[i*2+1])
|
||||||
|
}
|
||||||
|
mcastAddr := fmt.Sprintf("ff%s%s:%s", multicastAddrType, discoveryScope, gt)
|
||||||
|
|
||||||
ai := &AutoInterface{
|
ai := &AutoInterface{
|
||||||
BaseInterface: BaseInterface{
|
BaseInterface: BaseInterface{
|
||||||
@@ -121,7 +158,6 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
|
|||||||
discoveryScope: discoveryScope,
|
discoveryScope: discoveryScope,
|
||||||
multicastAddrType: multicastAddrType,
|
multicastAddrType: multicastAddrType,
|
||||||
mcastDiscoveryAddr: mcastAddr,
|
mcastDiscoveryAddr: mcastAddr,
|
||||||
ifacNetname: ifacNetname,
|
|
||||||
peers: make(map[string]*Peer),
|
peers: make(map[string]*Peer),
|
||||||
linkLocalAddrs: make([]string, 0),
|
linkLocalAddrs: make([]string, 0),
|
||||||
adoptedInterfaces: make(map[string]*AdoptedInterface),
|
adoptedInterfaces: make(map[string]*AdoptedInterface),
|
||||||
@@ -135,6 +171,8 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
|
|||||||
peerJobInterval: PEER_JOB_INTERVAL,
|
peerJobInterval: PEER_JOB_INTERVAL,
|
||||||
peeringTimeout: PEERING_TIMEOUT,
|
peeringTimeout: PEERING_TIMEOUT,
|
||||||
mcastEchoTimeout: MCAST_ECHO_TIMEOUT,
|
mcastEchoTimeout: MCAST_ECHO_TIMEOUT,
|
||||||
|
mifDeque: make([]DequeEntry, 0, MULTI_IF_DEQUE_LEN),
|
||||||
|
done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.Log(debug.DEBUG_INFO, "AutoInterface configured", "name", name, "group", groupID, "mcast_addr", mcastAddr)
|
debug.Log(debug.DEBUG_INFO, "AutoInterface configured", "name", name, "group", groupID, "mcast_addr", mcastAddr)
|
||||||
@@ -170,6 +208,20 @@ func normalizeMulticastType(mtype string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ai *AutoInterface) Start() error {
|
func (ai *AutoInterface) Start() error {
|
||||||
|
ai.Mutex.Lock()
|
||||||
|
// Only recreate done if it's nil or was closed
|
||||||
|
select {
|
||||||
|
case <-ai.done:
|
||||||
|
ai.done = make(chan struct{})
|
||||||
|
ai.stopOnce = sync.Once{}
|
||||||
|
default:
|
||||||
|
if ai.done == nil {
|
||||||
|
ai.done = make(chan struct{})
|
||||||
|
ai.stopOnce = sync.Once{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ai.Mutex.Unlock()
|
||||||
|
|
||||||
interfaces, err := net.Interfaces()
|
interfaces, err := net.Interfaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to list interfaces: %v", err)
|
return fmt.Errorf("failed to list interfaces: %v", err)
|
||||||
@@ -186,7 +238,9 @@ func (ai *AutoInterface) Start() error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ai.configureInterface(&iface); err != nil {
|
ifaceCopy := iface
|
||||||
|
// bearer:disable go_gosec_memory_memory_aliasing
|
||||||
|
if err := ai.configureInterface(&ifaceCopy); err != nil {
|
||||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to configure interface", "name", iface.Name, "error", err)
|
debug.Log(debug.DEBUG_VERBOSE, "Failed to configure interface", "name", iface.Name, "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -252,7 +306,7 @@ func (ai *AutoInterface) configureInterface(iface *net.Interface) error {
|
|||||||
for _, addr := range addrs {
|
for _, addr := range addrs {
|
||||||
if ipnet, ok := addr.(*net.IPNet); ok {
|
if ipnet, ok := addr.(*net.IPNet); ok {
|
||||||
if ipnet.IP.To4() == nil && ipnet.IP.IsLinkLocalUnicast() {
|
if ipnet.IP.To4() == nil && ipnet.IP.IsLinkLocalUnicast() {
|
||||||
linkLocalAddr = ipnet.IP.String()
|
linkLocalAddr = descopeLinkLocal(ipnet.IP.String())
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,7 +316,7 @@ func (ai *AutoInterface) configureInterface(iface *net.Interface) error {
|
|||||||
return fmt.Errorf("no link-local IPv6 address found")
|
return fmt.Errorf("no link-local IPv6 address found")
|
||||||
}
|
}
|
||||||
|
|
||||||
ai.mutex.Lock()
|
ai.Mutex.Lock()
|
||||||
ai.adoptedInterfaces[iface.Name] = &AdoptedInterface{
|
ai.adoptedInterfaces[iface.Name] = &AdoptedInterface{
|
||||||
name: iface.Name,
|
name: iface.Name,
|
||||||
linkLocalAddr: linkLocalAddr,
|
linkLocalAddr: linkLocalAddr,
|
||||||
@@ -270,7 +324,7 @@ func (ai *AutoInterface) configureInterface(iface *net.Interface) error {
|
|||||||
}
|
}
|
||||||
ai.linkLocalAddrs = append(ai.linkLocalAddrs, linkLocalAddr)
|
ai.linkLocalAddrs = append(ai.linkLocalAddrs, linkLocalAddr)
|
||||||
ai.multicastEchoes[iface.Name] = time.Now()
|
ai.multicastEchoes[iface.Name] = time.Now()
|
||||||
ai.mutex.Unlock()
|
ai.Mutex.Unlock()
|
||||||
|
|
||||||
if err := ai.startDiscoveryListener(iface); err != nil {
|
if err := ai.startDiscoveryListener(iface); err != nil {
|
||||||
return fmt.Errorf("failed to start discovery listener: %v", err)
|
return fmt.Errorf("failed to start discovery listener: %v", err)
|
||||||
@@ -296,13 +350,13 @@ func (ai *AutoInterface) startDiscoveryListener(iface *net.Interface) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := conn.SetReadBuffer(1024); err != nil {
|
if err := conn.SetReadBuffer(common.NUM_1024); err != nil {
|
||||||
debug.Log(debug.DEBUG_ERROR, "Failed to set discovery read buffer", "error", err)
|
debug.Log(debug.DEBUG_ERROR, "Failed to set discovery read buffer", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ai.mutex.Lock()
|
ai.Mutex.Lock()
|
||||||
ai.discoveryServers[iface.Name] = conn
|
ai.discoveryServers[iface.Name] = conn
|
||||||
ai.mutex.Unlock()
|
ai.Mutex.Unlock()
|
||||||
|
|
||||||
go ai.handleDiscovery(conn, iface.Name)
|
go ai.handleDiscovery(conn, iface.Name)
|
||||||
debug.Log(debug.DEBUG_VERBOSE, "Discovery listener started", "interface", iface.Name, "addr", ai.mcastDiscoveryAddr)
|
debug.Log(debug.DEBUG_VERBOSE, "Discovery listener started", "interface", iface.Name, "addr", ai.mcastDiscoveryAddr)
|
||||||
@@ -331,9 +385,9 @@ func (ai *AutoInterface) startDataListener(iface *net.Interface) error {
|
|||||||
debug.Log(debug.DEBUG_ERROR, "Failed to set data read buffer", "error", err)
|
debug.Log(debug.DEBUG_ERROR, "Failed to set data read buffer", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ai.mutex.Lock()
|
ai.Mutex.Lock()
|
||||||
ai.interfaceServers[iface.Name] = conn
|
ai.interfaceServers[iface.Name] = conn
|
||||||
ai.mutex.Unlock()
|
ai.Mutex.Unlock()
|
||||||
|
|
||||||
go ai.handleData(conn, iface.Name)
|
go ai.handleData(conn, iface.Name)
|
||||||
debug.Log(debug.DEBUG_VERBOSE, "Data listener started", "interface", iface.Name, "addr", addr)
|
debug.Log(debug.DEBUG_VERBOSE, "Data listener started", "interface", iface.Name, "addr", addr)
|
||||||
@@ -341,8 +395,18 @@ func (ai *AutoInterface) startDataListener(iface *net.Interface) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) {
|
func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) {
|
||||||
buf := make([]byte, 1024)
|
buf := make([]byte, common.NUM_1024)
|
||||||
for {
|
for {
|
||||||
|
ai.Mutex.RLock()
|
||||||
|
done := ai.done
|
||||||
|
ai.Mutex.RUnlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
n, remoteAddr, err := conn.ReadFromUDP(buf)
|
n, remoteAddr, err := conn.ReadFromUDP(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if ai.IsOnline() {
|
if ai.IsOnline() {
|
||||||
@@ -351,12 +415,17 @@ func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if n >= len(ai.groupHash) {
|
// Python: discovery_token = RNS.Identity.full_hash(self.group_id+ipv6_src[0].encode("utf-8"))
|
||||||
receivedHash := buf[:len(ai.groupHash)]
|
peerIP := descopeLinkLocal(remoteAddr.IP.String())
|
||||||
if bytes.Equal(receivedHash, ai.groupHash) {
|
tokenSource := append(ai.groupID, []byte(peerIP)...)
|
||||||
|
expectedHash := sha256.Sum256(tokenSource)
|
||||||
|
|
||||||
|
if n >= len(expectedHash) {
|
||||||
|
receivedHash := buf[:len(expectedHash)]
|
||||||
|
if bytes.Equal(receivedHash, expectedHash[:]) {
|
||||||
ai.handlePeerAnnounce(remoteAddr, ifaceName)
|
ai.handlePeerAnnounce(remoteAddr, ifaceName)
|
||||||
} else {
|
} else {
|
||||||
debug.Log(debug.DEBUG_TRACE, "Received discovery with mismatched group hash", "interface", ifaceName)
|
debug.Log(debug.DEBUG_TRACE, "Received discovery with mismatched group hash", "interface", ifaceName, "peer", peerIP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,7 +434,17 @@ func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) {
|
|||||||
func (ai *AutoInterface) handleData(conn *net.UDPConn, ifaceName string) {
|
func (ai *AutoInterface) handleData(conn *net.UDPConn, ifaceName string) {
|
||||||
buf := make([]byte, ai.GetMTU())
|
buf := make([]byte, ai.GetMTU())
|
||||||
for {
|
for {
|
||||||
n, _, err := conn.ReadFromUDP(buf)
|
ai.Mutex.RLock()
|
||||||
|
done := ai.done
|
||||||
|
ai.Mutex.RUnlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
n, remoteAddr, err := conn.ReadFromUDP(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if ai.IsOnline() {
|
if ai.IsOnline() {
|
||||||
debug.Log(debug.DEBUG_ERROR, "Data read error", "interface", ifaceName, "error", err)
|
debug.Log(debug.DEBUG_ERROR, "Data read error", "interface", ifaceName, "error", err)
|
||||||
@@ -373,15 +452,48 @@ func (ai *AutoInterface) handleData(conn *net.UDPConn, ifaceName string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data := buf[:n]
|
||||||
|
dataHash := sha256.Sum256(data)
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
ai.Mutex.Lock()
|
||||||
|
// Check for duplicate in mifDeque
|
||||||
|
isDuplicate := false
|
||||||
|
for i := 0; i < len(ai.mifDeque); i++ {
|
||||||
|
if ai.mifDeque[i].hash == dataHash && now.Sub(ai.mifDeque[i].timestamp) < MULTI_IF_DEQUE_TTL {
|
||||||
|
isDuplicate = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDuplicate {
|
||||||
|
ai.Mutex.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to deque
|
||||||
|
ai.mifDeque = append(ai.mifDeque, DequeEntry{hash: dataHash, timestamp: now})
|
||||||
|
if len(ai.mifDeque) > MULTI_IF_DEQUE_LEN {
|
||||||
|
ai.mifDeque = ai.mifDeque[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh peer if known
|
||||||
|
peerIP := descopeLinkLocal(remoteAddr.IP.String())
|
||||||
|
peerKey := peerIP + "%" + ifaceName
|
||||||
|
if peer, exists := ai.peers[peerKey]; exists {
|
||||||
|
peer.lastHeard = now
|
||||||
|
}
|
||||||
|
ai.Mutex.Unlock()
|
||||||
|
|
||||||
if callback := ai.GetPacketCallback(); callback != nil {
|
if callback := ai.GetPacketCallback(); callback != nil {
|
||||||
callback(buf[:n], ai)
|
callback(data, ai)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ai *AutoInterface) handlePeerAnnounce(addr *net.UDPAddr, ifaceName string) {
|
func (ai *AutoInterface) handlePeerAnnounce(addr *net.UDPAddr, ifaceName string) {
|
||||||
ai.mutex.Lock()
|
ai.Mutex.Lock()
|
||||||
defer ai.mutex.Unlock()
|
defer ai.Mutex.Unlock()
|
||||||
|
|
||||||
peerIP := addr.IP.String()
|
peerIP := addr.IP.String()
|
||||||
|
|
||||||
@@ -412,17 +524,22 @@ func (ai *AutoInterface) announceLoop() {
|
|||||||
ticker := time.NewTicker(ai.announceInterval)
|
ticker := time.NewTicker(ai.announceInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for range ticker.C {
|
for {
|
||||||
if !ai.IsOnline() {
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if !ai.IsOnline() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ai.sendPeerAnnounce()
|
||||||
|
case <-ai.done:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ai.sendPeerAnnounce()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ai *AutoInterface) sendPeerAnnounce() {
|
func (ai *AutoInterface) sendPeerAnnounce() {
|
||||||
ai.mutex.RLock()
|
ai.Mutex.RLock()
|
||||||
defer ai.mutex.RUnlock()
|
defer ai.Mutex.RUnlock()
|
||||||
|
|
||||||
for ifaceName, adoptedIface := range ai.adoptedInterfaces {
|
for ifaceName, adoptedIface := range ai.adoptedInterfaces {
|
||||||
mcastAddr := &net.UDPAddr{
|
mcastAddr := &net.UDPAddr{
|
||||||
@@ -440,7 +557,11 @@ func (ai *AutoInterface) sendPeerAnnounce() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := ai.outboundConn.WriteToUDP(ai.groupHash, mcastAddr); err != nil {
|
// Python: discovery_token = RNS.Identity.full_hash(self.group_id+link_local_address.encode("utf-8"))
|
||||||
|
tokenSource := append(ai.groupID, []byte(adoptedIface.linkLocalAddr)...)
|
||||||
|
token := sha256.Sum256(tokenSource)
|
||||||
|
|
||||||
|
if _, err := ai.outboundConn.WriteToUDP(token[:], mcastAddr); err != nil {
|
||||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to send peer announce", "interface", ifaceName, "error", err)
|
debug.Log(debug.DEBUG_VERBOSE, "Failed to send peer announce", "interface", ifaceName, "error", err)
|
||||||
} else {
|
} else {
|
||||||
debug.Log(debug.DEBUG_TRACE, "Sent peer announce", "interface", adoptedIface.name)
|
debug.Log(debug.DEBUG_TRACE, "Sent peer announce", "interface", adoptedIface.name)
|
||||||
@@ -452,33 +573,38 @@ func (ai *AutoInterface) peerJobs() {
|
|||||||
ticker := time.NewTicker(ai.peerJobInterval)
|
ticker := time.NewTicker(ai.peerJobInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for range ticker.C {
|
for {
|
||||||
if !ai.IsOnline() {
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if !ai.IsOnline() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ai.Mutex.Lock()
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
for peerKey, peer := range ai.peers {
|
||||||
|
if now.Sub(peer.lastHeard) > ai.peeringTimeout {
|
||||||
|
delete(ai.peers, peerKey)
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "Removed timed out peer", "peer", peerKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ifaceName, echoTime := range ai.multicastEchoes {
|
||||||
|
if now.Sub(echoTime) > ai.mcastEchoTimeout {
|
||||||
|
if _, exists := ai.timedOutInterfaces[ifaceName]; !exists {
|
||||||
|
debug.Log(debug.DEBUG_INFO, "Interface timed out", "interface", ifaceName)
|
||||||
|
ai.timedOutInterfaces[ifaceName] = now
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete(ai.timedOutInterfaces, ifaceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ai.Mutex.Unlock()
|
||||||
|
case <-ai.done:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ai.mutex.Lock()
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
for peerKey, peer := range ai.peers {
|
|
||||||
if now.Sub(peer.lastHeard) > ai.peeringTimeout {
|
|
||||||
delete(ai.peers, peerKey)
|
|
||||||
debug.Log(debug.DEBUG_VERBOSE, "Removed timed out peer", "peer", peerKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for ifaceName, echoTime := range ai.multicastEchoes {
|
|
||||||
if now.Sub(echoTime) > ai.mcastEchoTimeout {
|
|
||||||
if _, exists := ai.timedOutInterfaces[ifaceName]; !exists {
|
|
||||||
debug.Log(debug.DEBUG_INFO, "Interface timed out", "interface", ifaceName)
|
|
||||||
ai.timedOutInterfaces[ifaceName] = now
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete(ai.timedOutInterfaces, ifaceName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ai.mutex.Unlock()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,8 +613,8 @@ func (ai *AutoInterface) Send(data []byte, address string) error {
|
|||||||
return fmt.Errorf("interface offline")
|
return fmt.Errorf("interface offline")
|
||||||
}
|
}
|
||||||
|
|
||||||
ai.mutex.RLock()
|
ai.Mutex.RLock()
|
||||||
defer ai.mutex.RUnlock()
|
defer ai.Mutex.RUnlock()
|
||||||
|
|
||||||
if len(ai.peers) == 0 {
|
if len(ai.peers) == 0 {
|
||||||
debug.Log(debug.DEBUG_TRACE, "No peers available for sending")
|
debug.Log(debug.DEBUG_TRACE, "No peers available for sending")
|
||||||
@@ -526,9 +652,7 @@ func (ai *AutoInterface) Send(data []byte, address string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ai *AutoInterface) Stop() error {
|
func (ai *AutoInterface) Stop() error {
|
||||||
ai.mutex.Lock()
|
ai.Mutex.Lock()
|
||||||
defer ai.mutex.Unlock()
|
|
||||||
|
|
||||||
ai.Online = false
|
ai.Online = false
|
||||||
ai.IN = false
|
ai.IN = false
|
||||||
ai.OUT = false
|
ai.OUT = false
|
||||||
@@ -544,6 +668,13 @@ func (ai *AutoInterface) Stop() error {
|
|||||||
if ai.outboundConn != nil {
|
if ai.outboundConn != nil {
|
||||||
ai.outboundConn.Close() // #nosec G104
|
ai.outboundConn.Close() // #nosec G104
|
||||||
}
|
}
|
||||||
|
ai.Mutex.Unlock()
|
||||||
|
|
||||||
|
ai.stopOnce.Do(func() {
|
||||||
|
if ai.done != nil {
|
||||||
|
close(ai.done)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
debug.Log(debug.DEBUG_INFO, "AutoInterface stopped")
|
debug.Log(debug.DEBUG_INFO, "AutoInterface stopped")
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -144,14 +144,14 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
ai.mutex.Lock()
|
ai.Mutex.Lock()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
for addr, peer := range ai.peers {
|
for addr, peer := range ai.peers {
|
||||||
if now.Sub(peer.lastHeard) > testTimeout {
|
if now.Sub(peer.lastHeard) > testTimeout {
|
||||||
delete(ai.peers, addr)
|
delete(ai.peers, addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ai.mutex.Unlock()
|
ai.Mutex.Unlock()
|
||||||
case <-done:
|
case <-done:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -173,27 +173,26 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
|
|||||||
peer2Addr := &net.UDPAddr{IP: net.ParseIP("fe80::2"), Zone: "eth0"}
|
peer2Addr := &net.UDPAddr{IP: net.ParseIP("fe80::2"), Zone: "eth0"}
|
||||||
localAddr := &net.UDPAddr{IP: net.ParseIP("fe80::aaaa"), Zone: "eth0"}
|
localAddr := &net.UDPAddr{IP: net.ParseIP("fe80::aaaa"), Zone: "eth0"}
|
||||||
|
|
||||||
// Add a simulated local address to avoid adding it as a peer
|
ai.Mutex.Lock()
|
||||||
ai.mutex.Lock()
|
|
||||||
ai.linkLocalAddrs = append(ai.linkLocalAddrs, localAddrStr)
|
ai.linkLocalAddrs = append(ai.linkLocalAddrs, localAddrStr)
|
||||||
ai.mutex.Unlock()
|
ai.Mutex.Unlock()
|
||||||
|
|
||||||
t.Run("AddPeer1", func(t *testing.T) {
|
t.Run("AddPeer1", func(t *testing.T) {
|
||||||
ai.mutex.Lock()
|
ai.Mutex.Lock()
|
||||||
ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
|
ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
|
||||||
ai.mutex.Unlock()
|
ai.Mutex.Unlock()
|
||||||
|
|
||||||
// Give a small amount of time for the peer to be processed
|
// Give a small amount of time for the peer to be processed
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
ai.mutex.RLock()
|
ai.Mutex.RLock()
|
||||||
count := len(ai.peers)
|
count := len(ai.peers)
|
||||||
peer, exists := ai.peers[peer1AddrStr]
|
peer, exists := ai.peers[peer1AddrStr]
|
||||||
var ifaceName string
|
var ifaceName string
|
||||||
if exists {
|
if exists {
|
||||||
ifaceName = peer.ifaceName
|
ifaceName = peer.ifaceName
|
||||||
}
|
}
|
||||||
ai.mutex.RUnlock()
|
ai.Mutex.RUnlock()
|
||||||
|
|
||||||
if count != 1 {
|
if count != 1 {
|
||||||
t.Fatalf("Expected 1 peer, got %d", count)
|
t.Fatalf("Expected 1 peer, got %d", count)
|
||||||
@@ -207,17 +206,17 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("AddPeer2", func(t *testing.T) {
|
t.Run("AddPeer2", func(t *testing.T) {
|
||||||
ai.mutex.Lock()
|
ai.Mutex.Lock()
|
||||||
ai.mockHandlePeerAnnounce(peer2Addr, "eth0")
|
ai.mockHandlePeerAnnounce(peer2Addr, "eth0")
|
||||||
ai.mutex.Unlock()
|
ai.Mutex.Unlock()
|
||||||
|
|
||||||
// Give a small amount of time for the peer to be processed
|
// Give a small amount of time for the peer to be processed
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
ai.mutex.RLock()
|
ai.Mutex.RLock()
|
||||||
count := len(ai.peers)
|
count := len(ai.peers)
|
||||||
_, exists := ai.peers[peer2AddrStr]
|
_, exists := ai.peers[peer2AddrStr]
|
||||||
ai.mutex.RUnlock()
|
ai.Mutex.RUnlock()
|
||||||
|
|
||||||
if count != 2 {
|
if count != 2 {
|
||||||
t.Fatalf("Expected 2 peers, got %d", count)
|
t.Fatalf("Expected 2 peers, got %d", count)
|
||||||
@@ -228,16 +227,16 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("IgnoreLocalAnnounce", func(t *testing.T) {
|
t.Run("IgnoreLocalAnnounce", func(t *testing.T) {
|
||||||
ai.mutex.Lock()
|
ai.Mutex.Lock()
|
||||||
ai.mockHandlePeerAnnounce(localAddr, "eth0")
|
ai.mockHandlePeerAnnounce(localAddr, "eth0")
|
||||||
ai.mutex.Unlock()
|
ai.Mutex.Unlock()
|
||||||
|
|
||||||
// Give a small amount of time for the peer to be processed
|
// Give a small amount of time for the peer to be processed
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
ai.mutex.RLock()
|
ai.Mutex.RLock()
|
||||||
count := len(ai.peers)
|
count := len(ai.peers)
|
||||||
ai.mutex.RUnlock()
|
ai.Mutex.RUnlock()
|
||||||
|
|
||||||
if count != 2 {
|
if count != 2 {
|
||||||
t.Fatalf("Expected 2 peers after local announce, got %d", count)
|
t.Fatalf("Expected 2 peers after local announce, got %d", count)
|
||||||
@@ -245,32 +244,32 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("UpdatePeerTimestamp", func(t *testing.T) {
|
t.Run("UpdatePeerTimestamp", func(t *testing.T) {
|
||||||
ai.mutex.RLock()
|
ai.Mutex.RLock()
|
||||||
peer, exists := ai.peers[peer1AddrStr]
|
peer, exists := ai.peers[peer1AddrStr]
|
||||||
var initialTime time.Time
|
var initialTime time.Time
|
||||||
if exists {
|
if exists {
|
||||||
initialTime = peer.lastHeard
|
initialTime = peer.lastHeard
|
||||||
}
|
}
|
||||||
ai.mutex.RUnlock()
|
ai.Mutex.RUnlock()
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
t.Fatalf("Peer %s not found before timestamp update", peer1AddrStr)
|
t.Fatalf("Peer %s not found before timestamp update", peer1AddrStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
ai.mutex.Lock()
|
ai.Mutex.Lock()
|
||||||
ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
|
ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
|
||||||
ai.mutex.Unlock()
|
ai.Mutex.Unlock()
|
||||||
|
|
||||||
// Give a small amount of time for the peer to be processed
|
// Give a small amount of time for the peer to be processed
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
ai.mutex.RLock()
|
ai.Mutex.RLock()
|
||||||
peer, exists = ai.peers[peer1AddrStr]
|
peer, exists = ai.peers[peer1AddrStr]
|
||||||
var updatedTime time.Time
|
var updatedTime time.Time
|
||||||
if exists {
|
if exists {
|
||||||
updatedTime = peer.lastHeard
|
updatedTime = peer.lastHeard
|
||||||
}
|
}
|
||||||
ai.mutex.RUnlock()
|
ai.Mutex.RUnlock()
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
t.Fatalf("Peer %s not found after timestamp update", peer1AddrStr)
|
t.Fatalf("Peer %s not found after timestamp update", peer1AddrStr)
|
||||||
@@ -285,9 +284,9 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
|
|||||||
// Wait for peer timeout
|
// Wait for peer timeout
|
||||||
time.Sleep(testTimeout * 2)
|
time.Sleep(testTimeout * 2)
|
||||||
|
|
||||||
ai.mutex.RLock()
|
ai.Mutex.RLock()
|
||||||
count := len(ai.peers)
|
count := len(ai.peers)
|
||||||
ai.mutex.RUnlock()
|
ai.Mutex.RUnlock()
|
||||||
|
|
||||||
if count != 0 {
|
if count != 0 {
|
||||||
t.Errorf("Expected all peers to timeout, got %d peers", count)
|
t.Errorf("Expected all peers to timeout, got %d peers", count)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package interfaces
|
package interfaces
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -54,60 +56,69 @@ type Interface interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BaseInterface struct {
|
type BaseInterface struct {
|
||||||
Name string
|
Name string
|
||||||
Mode common.InterfaceMode
|
Mode common.InterfaceMode
|
||||||
Type common.InterfaceType
|
Type common.InterfaceType
|
||||||
Online bool
|
Online bool
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Detached bool
|
Detached bool
|
||||||
IN bool
|
IN bool
|
||||||
OUT bool
|
OUT bool
|
||||||
MTU int
|
MTU int
|
||||||
Bitrate int64
|
Bitrate int64
|
||||||
TxBytes uint64
|
TxBytes uint64
|
||||||
RxBytes uint64
|
RxBytes uint64
|
||||||
lastTx time.Time
|
TxPackets uint64
|
||||||
|
RxPackets uint64
|
||||||
|
lastTx time.Time
|
||||||
|
lastRx time.Time
|
||||||
|
|
||||||
mutex sync.RWMutex
|
Mutex sync.RWMutex
|
||||||
packetCallback common.PacketCallback
|
packetCallback common.PacketCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBaseInterface(name string, ifType common.InterfaceType, enabled bool) BaseInterface {
|
func NewBaseInterface(name string, ifType common.InterfaceType, enabled bool) BaseInterface {
|
||||||
return BaseInterface{
|
return BaseInterface{
|
||||||
Name: name,
|
Name: name,
|
||||||
Mode: common.IF_MODE_FULL,
|
Mode: common.IF_MODE_FULL,
|
||||||
Type: ifType,
|
Type: ifType,
|
||||||
Online: false,
|
Online: false,
|
||||||
Enabled: enabled,
|
Enabled: enabled,
|
||||||
Detached: false,
|
Detached: false,
|
||||||
IN: false,
|
IN: false,
|
||||||
OUT: false,
|
OUT: false,
|
||||||
MTU: common.DEFAULT_MTU,
|
MTU: common.DEFAULT_MTU,
|
||||||
Bitrate: BITRATE_MINIMUM,
|
Bitrate: BITRATE_MINIMUM,
|
||||||
lastTx: time.Now(),
|
TxBytes: 0,
|
||||||
|
RxBytes: 0,
|
||||||
|
TxPackets: 0,
|
||||||
|
RxPackets: 0,
|
||||||
|
lastTx: time.Now(),
|
||||||
|
lastRx: time.Now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {
|
func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||||
i.mutex.Lock()
|
i.Mutex.Lock()
|
||||||
defer i.mutex.Unlock()
|
defer i.Mutex.Unlock()
|
||||||
i.packetCallback = callback
|
i.packetCallback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) GetPacketCallback() common.PacketCallback {
|
func (i *BaseInterface) GetPacketCallback() common.PacketCallback {
|
||||||
i.mutex.RLock()
|
i.Mutex.RLock()
|
||||||
defer i.mutex.RUnlock()
|
defer i.Mutex.RUnlock()
|
||||||
return i.packetCallback
|
return i.packetCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
||||||
i.mutex.Lock()
|
i.Mutex.Lock()
|
||||||
i.RxBytes += uint64(len(data))
|
i.RxBytes += uint64(len(data))
|
||||||
i.mutex.Unlock()
|
i.RxPackets++
|
||||||
|
i.Mutex.Unlock()
|
||||||
|
|
||||||
i.mutex.RLock()
|
i.Mutex.RLock()
|
||||||
callback := i.packetCallback
|
callback := i.packetCallback
|
||||||
i.mutex.RUnlock()
|
i.Mutex.RUnlock()
|
||||||
|
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
callback(data, i)
|
callback(data, i)
|
||||||
@@ -120,9 +131,10 @@ func (i *BaseInterface) ProcessOutgoing(data []byte) error {
|
|||||||
return fmt.Errorf("interface offline or detached")
|
return fmt.Errorf("interface offline or detached")
|
||||||
}
|
}
|
||||||
|
|
||||||
i.mutex.Lock()
|
i.Mutex.Lock()
|
||||||
i.TxBytes += uint64(len(data))
|
i.TxBytes += uint64(len(data))
|
||||||
i.mutex.Unlock()
|
i.TxPackets++
|
||||||
|
i.Mutex.Unlock()
|
||||||
|
|
||||||
debug.Log(debug.DEBUG_VERBOSE, "Interface processed outgoing packet", "name", i.Name, "bytes", len(data), "total_tx", i.TxBytes)
|
debug.Log(debug.DEBUG_VERBOSE, "Interface processed outgoing packet", "name", i.Name, "bytes", len(data), "total_tx", i.TxBytes)
|
||||||
return nil
|
return nil
|
||||||
@@ -134,7 +146,7 @@ func (i *BaseInterface) SendPathRequest(packet []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
frame := make([]byte, 0, len(packet)+1)
|
frame := make([]byte, 0, len(packet)+1)
|
||||||
frame = append(frame, 0x01)
|
frame = append(frame, common.HEX_0x01)
|
||||||
frame = append(frame, packet...)
|
frame = append(frame, packet...)
|
||||||
|
|
||||||
return i.ProcessOutgoing(frame)
|
return i.ProcessOutgoing(frame)
|
||||||
@@ -146,7 +158,7 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
|
|||||||
}
|
}
|
||||||
|
|
||||||
frame := make([]byte, 0, len(dest)+len(data)+9)
|
frame := make([]byte, 0, len(dest)+len(data)+9)
|
||||||
frame = append(frame, 0x02)
|
frame = append(frame, common.HEX_0x02)
|
||||||
frame = append(frame, dest...)
|
frame = append(frame, dest...)
|
||||||
|
|
||||||
ts := make([]byte, 8)
|
ts := make([]byte, 8)
|
||||||
@@ -158,21 +170,21 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) Detach() {
|
func (i *BaseInterface) Detach() {
|
||||||
i.mutex.Lock()
|
i.Mutex.Lock()
|
||||||
defer i.mutex.Unlock()
|
defer i.Mutex.Unlock()
|
||||||
i.Detached = true
|
i.Detached = true
|
||||||
i.Online = false
|
i.Online = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) IsEnabled() bool {
|
func (i *BaseInterface) IsEnabled() bool {
|
||||||
i.mutex.RLock()
|
i.Mutex.RLock()
|
||||||
defer i.mutex.RUnlock()
|
defer i.Mutex.RUnlock()
|
||||||
return i.Enabled && i.Online && !i.Detached
|
return i.Enabled && i.Online && !i.Detached
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) Enable() {
|
func (i *BaseInterface) Enable() {
|
||||||
i.mutex.Lock()
|
i.Mutex.Lock()
|
||||||
defer i.mutex.Unlock()
|
defer i.Mutex.Unlock()
|
||||||
|
|
||||||
prevState := i.Enabled
|
prevState := i.Enabled
|
||||||
i.Enabled = true
|
i.Enabled = true
|
||||||
@@ -182,8 +194,8 @@ func (i *BaseInterface) Enable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) Disable() {
|
func (i *BaseInterface) Disable() {
|
||||||
i.mutex.Lock()
|
i.Mutex.Lock()
|
||||||
defer i.mutex.Unlock()
|
defer i.Mutex.Unlock()
|
||||||
i.Enabled = false
|
i.Enabled = false
|
||||||
i.Online = false
|
i.Online = false
|
||||||
debug.Log(debug.DEBUG_ERROR, "Interface disabled and offline", "name", i.Name)
|
debug.Log(debug.DEBUG_ERROR, "Interface disabled and offline", "name", i.Name)
|
||||||
@@ -206,17 +218,41 @@ func (i *BaseInterface) GetMTU() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) IsOnline() bool {
|
func (i *BaseInterface) IsOnline() bool {
|
||||||
i.mutex.RLock()
|
i.Mutex.RLock()
|
||||||
defer i.mutex.RUnlock()
|
defer i.Mutex.RUnlock()
|
||||||
return i.Online
|
return i.Online
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) IsDetached() bool {
|
func (i *BaseInterface) IsDetached() bool {
|
||||||
i.mutex.RLock()
|
i.Mutex.RLock()
|
||||||
defer i.mutex.RUnlock()
|
defer i.Mutex.RUnlock()
|
||||||
return i.Detached
|
return i.Detached
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetTxBytes() uint64 {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.TxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetRxBytes() uint64 {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.RxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetTxPackets() uint64 {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.TxPackets
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetRxPackets() uint64 {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.RxPackets
|
||||||
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) Start() error {
|
func (i *BaseInterface) Start() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -243,8 +279,8 @@ func (i *BaseInterface) GetConn() net.Conn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) GetBandwidthAvailable() bool {
|
func (i *BaseInterface) GetBandwidthAvailable() bool {
|
||||||
i.mutex.RLock()
|
i.Mutex.RLock()
|
||||||
defer i.mutex.RUnlock()
|
defer i.Mutex.RUnlock()
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
timeSinceLastTx := now.Sub(i.lastTx)
|
timeSinceLastTx := now.Sub(i.lastTx)
|
||||||
@@ -265,10 +301,9 @@ func (i *BaseInterface) GetBandwidthAvailable() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) updateBandwidthStats(bytes uint64) {
|
func (i *BaseInterface) updateBandwidthStats(bytes uint64) {
|
||||||
i.mutex.Lock()
|
i.Mutex.Lock()
|
||||||
defer i.mutex.Unlock()
|
defer i.Mutex.Unlock()
|
||||||
|
|
||||||
i.TxBytes += bytes
|
|
||||||
i.lastTx = time.Now()
|
i.lastTx = time.Now()
|
||||||
|
|
||||||
debug.Log(debug.DEBUG_VERBOSE, "Interface updated bandwidth stats", "name", i.Name, "tx_bytes", i.TxBytes, "last_tx", i.lastTx)
|
debug.Log(debug.DEBUG_VERBOSE, "Interface updated bandwidth stats", "name", i.Name, "tx_bytes", i.TxBytes, "last_tx", i.lastTx)
|
||||||
|
|||||||
@@ -183,7 +183,6 @@ func (m *mockInterface) Send(data []byte, addr string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add other methods to satisfy the Interface interface (can be minimal/panic)
|
|
||||||
func (m *mockInterface) GetType() common.InterfaceType { return common.IF_TYPE_NONE }
|
func (m *mockInterface) GetType() common.InterfaceType { return common.IF_TYPE_NONE }
|
||||||
func (m *mockInterface) GetMode() common.InterfaceMode { return common.IF_MODE_FULL }
|
func (m *mockInterface) GetMode() common.InterfaceMode { return common.IF_MODE_FULL }
|
||||||
func (m *mockInterface) ProcessIncoming(data []byte) {}
|
func (m *mockInterface) ProcessIncoming(data []byte) {}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package interfaces
|
package interfaces
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -32,11 +34,15 @@ const (
|
|||||||
TCP_PROBE_AFTER_SEC = 5
|
TCP_PROBE_AFTER_SEC = 5
|
||||||
TCP_PROBE_INTERVAL_SEC = 2
|
TCP_PROBE_INTERVAL_SEC = 2
|
||||||
TCP_PROBES_COUNT = 12
|
TCP_PROBES_COUNT = 12
|
||||||
|
TCP_CONNECT_TIMEOUT = 10 * time.Second
|
||||||
|
TCP_MILLISECONDS = 1000
|
||||||
|
|
||||||
I2P_USER_TIMEOUT_SEC = 45
|
I2P_USER_TIMEOUT_SEC = 45
|
||||||
I2P_PROBE_AFTER_SEC = 10
|
I2P_PROBE_AFTER_SEC = 10
|
||||||
I2P_PROBE_INTERVAL_SEC = 9
|
I2P_PROBE_INTERVAL_SEC = 9
|
||||||
I2P_PROBES_COUNT = 5
|
I2P_PROBES_COUNT = 5
|
||||||
|
|
||||||
|
SO_KEEPALIVE_ENABLE = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
type TCPClientInterface struct {
|
type TCPClientInterface struct {
|
||||||
@@ -53,12 +59,8 @@ type TCPClientInterface struct {
|
|||||||
maxReconnectTries int
|
maxReconnectTries int
|
||||||
packetBuffer []byte
|
packetBuffer []byte
|
||||||
packetType byte
|
packetType byte
|
||||||
mutex sync.RWMutex
|
done chan struct{}
|
||||||
enabled bool
|
stopOnce sync.Once
|
||||||
TxBytes uint64
|
|
||||||
RxBytes uint64
|
|
||||||
lastTx time.Time
|
|
||||||
lastRx time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTCPClientInterface(name string, targetHost string, targetPort int, kissFraming bool, i2pTunneled bool, enabled bool) (*TCPClientInterface, error) {
|
func NewTCPClientInterface(name string, targetHost string, targetPort int, kissFraming bool, i2pTunneled bool, enabled bool) (*TCPClientInterface, error) {
|
||||||
@@ -69,10 +71,10 @@ func NewTCPClientInterface(name string, targetHost string, targetPort int, kissF
|
|||||||
kissFraming: kissFraming,
|
kissFraming: kissFraming,
|
||||||
i2pTunneled: i2pTunneled,
|
i2pTunneled: i2pTunneled,
|
||||||
initiator: true,
|
initiator: true,
|
||||||
enabled: enabled,
|
|
||||||
maxReconnectTries: RECONNECT_WAIT * TCP_PROBES_COUNT,
|
maxReconnectTries: RECONNECT_WAIT * TCP_PROBES_COUNT,
|
||||||
packetBuffer: make([]byte, 0),
|
packetBuffer: make([]byte, 0),
|
||||||
neverConnected: true,
|
neverConnected: true,
|
||||||
|
done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
if enabled {
|
if enabled {
|
||||||
@@ -90,25 +92,41 @@ func NewTCPClientInterface(name string, targetHost string, targetPort int, kissF
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) Start() error {
|
func (tc *TCPClientInterface) Start() error {
|
||||||
tc.mutex.Lock()
|
tc.Mutex.Lock()
|
||||||
defer tc.mutex.Unlock()
|
if !tc.Enabled || tc.Detached {
|
||||||
|
tc.Mutex.Unlock()
|
||||||
if !tc.Enabled {
|
return fmt.Errorf("interface not enabled or detached")
|
||||||
return fmt.Errorf("interface not enabled")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if tc.conn != nil {
|
if tc.conn != nil {
|
||||||
tc.Online = true
|
tc.Online = true
|
||||||
go tc.readLoop()
|
go tc.readLoop()
|
||||||
|
tc.Mutex.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only recreate done if it's nil or was closed
|
||||||
|
select {
|
||||||
|
case <-tc.done:
|
||||||
|
tc.done = make(chan struct{})
|
||||||
|
tc.stopOnce = sync.Once{}
|
||||||
|
default:
|
||||||
|
if tc.done == nil {
|
||||||
|
tc.done = make(chan struct{})
|
||||||
|
tc.stopOnce = sync.Once{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tc.Mutex.Unlock()
|
||||||
|
|
||||||
addr := net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort))
|
addr := net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort))
|
||||||
conn, err := net.Dial("tcp", addr)
|
conn, err := net.DialTimeout("tcp", addr, TCP_CONNECT_TIMEOUT)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tc.Mutex.Lock()
|
||||||
tc.conn = conn
|
tc.conn = conn
|
||||||
|
tc.Mutex.Unlock()
|
||||||
|
|
||||||
// Set platform-specific timeouts
|
// Set platform-specific timeouts
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
@@ -122,11 +140,67 @@ func (tc *TCPClientInterface) Start() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tc.Mutex.Lock()
|
||||||
tc.Online = true
|
tc.Online = true
|
||||||
|
tc.Mutex.Unlock()
|
||||||
|
|
||||||
go tc.readLoop()
|
go tc.readLoop()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) Stop() error {
|
||||||
|
tc.Mutex.Lock()
|
||||||
|
tc.Enabled = false
|
||||||
|
tc.Online = false
|
||||||
|
if tc.conn != nil {
|
||||||
|
_ = tc.conn.Close()
|
||||||
|
tc.conn = nil
|
||||||
|
}
|
||||||
|
tc.Mutex.Unlock()
|
||||||
|
|
||||||
|
tc.stopOnce.Do(func() {
|
||||||
|
if tc.done != nil {
|
||||||
|
close(tc.done)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) ProcessOutgoing(data []byte) error {
|
||||||
|
tc.Mutex.RLock()
|
||||||
|
online := tc.Online
|
||||||
|
tc.Mutex.RUnlock()
|
||||||
|
|
||||||
|
if !online {
|
||||||
|
return fmt.Errorf("interface offline")
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.writing = true
|
||||||
|
defer func() { tc.writing = false }()
|
||||||
|
|
||||||
|
// For TCP connections, use HDLC framing
|
||||||
|
var frame []byte
|
||||||
|
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
|
||||||
|
frame = append(frame, HDLC_FLAG)
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_ALL, "TCP interface writing to network", "name", tc.Name, "bytes", len(frame))
|
||||||
|
|
||||||
|
tc.Mutex.RLock()
|
||||||
|
conn := tc.conn
|
||||||
|
tc.Mutex.RUnlock()
|
||||||
|
|
||||||
|
if conn == nil {
|
||||||
|
return fmt.Errorf("connection closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := conn.Write(frame)
|
||||||
|
if err != nil {
|
||||||
|
debug.Log(debug.DEBUG_CRITICAL, "TCP interface write failed", "name", tc.Name, "error", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) readLoop() {
|
func (tc *TCPClientInterface) readLoop() {
|
||||||
buffer := make([]byte, tc.MTU)
|
buffer := make([]byte, tc.MTU)
|
||||||
inFrame := false
|
inFrame := false
|
||||||
@@ -134,10 +208,30 @@ func (tc *TCPClientInterface) readLoop() {
|
|||||||
dataBuffer := make([]byte, 0)
|
dataBuffer := make([]byte, 0)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
n, err := tc.conn.Read(buffer)
|
tc.Mutex.RLock()
|
||||||
|
conn := tc.conn
|
||||||
|
done := tc.done
|
||||||
|
tc.Mutex.RUnlock()
|
||||||
|
|
||||||
|
if conn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := conn.Read(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
tc.Mutex.Lock()
|
||||||
tc.Online = false
|
tc.Online = false
|
||||||
if tc.initiator && !tc.Detached {
|
detached := tc.Detached
|
||||||
|
initiator := tc.initiator
|
||||||
|
tc.Mutex.Unlock()
|
||||||
|
|
||||||
|
if initiator && !detached {
|
||||||
go tc.reconnect()
|
go tc.reconnect()
|
||||||
} else {
|
} else {
|
||||||
tc.teardown()
|
tc.teardown()
|
||||||
@@ -145,9 +239,6 @@ func (tc *TCPClientInterface) readLoop() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update RX bytes for raw received data
|
|
||||||
tc.UpdateStats(uint64(n), true) // #nosec G115
|
|
||||||
|
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
b := buffer[i]
|
b := buffer[i]
|
||||||
|
|
||||||
@@ -181,58 +272,14 @@ func (tc *TCPClientInterface) handlePacket(data []byte) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tc.mutex.Lock()
|
tc.Mutex.Lock()
|
||||||
tc.RxBytes += uint64(len(data))
|
|
||||||
lastRx := time.Now()
|
lastRx := time.Now()
|
||||||
tc.lastRx = lastRx
|
tc.lastRx = lastRx
|
||||||
tc.mutex.Unlock()
|
tc.Mutex.Unlock()
|
||||||
|
|
||||||
debug.Log(debug.DEBUG_ALL, "Received packet", "type", fmt.Sprintf("0x%02x", data[0]), "size", len(data))
|
debug.Log(debug.DEBUG_ALL, "Received packet", "type", fmt.Sprintf("0x%02x", data[0]), "size", len(data))
|
||||||
|
|
||||||
// For RNS packets, call the packet callback directly
|
tc.ProcessIncoming(data)
|
||||||
if callback := tc.GetPacketCallback(); callback != nil {
|
|
||||||
debug.Log(debug.DEBUG_ALL, "Calling packet callback for RNS packet")
|
|
||||||
callback(data, tc)
|
|
||||||
} else {
|
|
||||||
debug.Log(debug.DEBUG_ALL, "No packet callback set for TCP interface")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send implements the interface Send method for TCP interface
|
|
||||||
func (tc *TCPClientInterface) Send(data []byte, address string) error {
|
|
||||||
debug.Log(debug.DEBUG_ALL, "TCP interface sending bytes", "name", tc.Name, "bytes", len(data))
|
|
||||||
|
|
||||||
if !tc.IsEnabled() || !tc.IsOnline() {
|
|
||||||
return fmt.Errorf("TCP interface %s is not online", tc.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send data directly - packet type is already in the first byte of data
|
|
||||||
// TCP interface uses HDLC framing around the raw packet
|
|
||||||
return tc.ProcessOutgoing(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tc *TCPClientInterface) ProcessOutgoing(data []byte) error {
|
|
||||||
if !tc.Online {
|
|
||||||
return fmt.Errorf("interface offline")
|
|
||||||
}
|
|
||||||
|
|
||||||
tc.writing = true
|
|
||||||
defer func() { tc.writing = false }()
|
|
||||||
|
|
||||||
// For TCP connections, use HDLC framing
|
|
||||||
var frame []byte
|
|
||||||
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
|
|
||||||
frame = append(frame, HDLC_FLAG)
|
|
||||||
|
|
||||||
// Update TX stats before sending
|
|
||||||
tc.UpdateStats(uint64(len(frame)), false)
|
|
||||||
|
|
||||||
debug.Log(debug.DEBUG_ALL, "TCP interface writing to network", "name", tc.Name, "bytes", len(frame))
|
|
||||||
_, err := tc.conn.Write(frame)
|
|
||||||
if err != nil {
|
|
||||||
debug.Log(debug.DEBUG_CRITICAL, "TCP interface write failed", "name", tc.Name, "error", err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) teardown() {
|
func (tc *TCPClientInterface) teardown() {
|
||||||
@@ -240,7 +287,7 @@ func (tc *TCPClientInterface) teardown() {
|
|||||||
tc.IN = false
|
tc.IN = false
|
||||||
tc.OUT = false
|
tc.OUT = false
|
||||||
if tc.conn != nil {
|
if tc.conn != nil {
|
||||||
tc.conn.Close() // #nosec G104
|
_ = tc.conn.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,9 +323,9 @@ func (tc *TCPClientInterface) SetPacketCallback(cb common.PacketCallback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) IsEnabled() bool {
|
func (tc *TCPClientInterface) IsEnabled() bool {
|
||||||
tc.mutex.RLock()
|
tc.Mutex.RLock()
|
||||||
defer tc.mutex.RUnlock()
|
defer tc.Mutex.RUnlock()
|
||||||
return tc.enabled && tc.Online && !tc.Detached
|
return tc.Enabled && tc.Online && !tc.Detached
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) GetName() string {
|
func (tc *TCPClientInterface) GetName() string {
|
||||||
@@ -286,31 +333,31 @@ func (tc *TCPClientInterface) GetName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) GetPacketCallback() common.PacketCallback {
|
func (tc *TCPClientInterface) GetPacketCallback() common.PacketCallback {
|
||||||
tc.mutex.RLock()
|
tc.Mutex.RLock()
|
||||||
defer tc.mutex.RUnlock()
|
defer tc.Mutex.RUnlock()
|
||||||
return tc.packetCallback
|
return tc.packetCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) IsDetached() bool {
|
func (tc *TCPClientInterface) IsDetached() bool {
|
||||||
tc.mutex.RLock()
|
tc.Mutex.RLock()
|
||||||
defer tc.mutex.RUnlock()
|
defer tc.Mutex.RUnlock()
|
||||||
return tc.Detached
|
return tc.Detached
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) IsOnline() bool {
|
func (tc *TCPClientInterface) IsOnline() bool {
|
||||||
tc.mutex.RLock()
|
tc.Mutex.RLock()
|
||||||
defer tc.mutex.RUnlock()
|
defer tc.Mutex.RUnlock()
|
||||||
return tc.Online
|
return tc.Online
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) reconnect() {
|
func (tc *TCPClientInterface) reconnect() {
|
||||||
tc.mutex.Lock()
|
tc.Mutex.Lock()
|
||||||
if tc.reconnecting {
|
if tc.reconnecting {
|
||||||
tc.mutex.Unlock()
|
tc.Mutex.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tc.reconnecting = true
|
tc.reconnecting = true
|
||||||
tc.mutex.Unlock()
|
tc.Mutex.Unlock()
|
||||||
|
|
||||||
backoff := time.Second
|
backoff := time.Second
|
||||||
maxBackoff := time.Minute * 5
|
maxBackoff := time.Minute * 5
|
||||||
@@ -323,13 +370,13 @@ func (tc *TCPClientInterface) reconnect() {
|
|||||||
|
|
||||||
conn, err := net.Dial("tcp", addr)
|
conn, err := net.Dial("tcp", addr)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
tc.mutex.Lock()
|
tc.Mutex.Lock()
|
||||||
tc.conn = conn
|
tc.conn = conn
|
||||||
tc.Online = true
|
tc.Online = true
|
||||||
|
|
||||||
tc.neverConnected = false
|
tc.neverConnected = false
|
||||||
tc.reconnecting = false
|
tc.reconnecting = false
|
||||||
tc.mutex.Unlock()
|
tc.Mutex.Unlock()
|
||||||
|
|
||||||
go tc.readLoop()
|
go tc.readLoop()
|
||||||
return
|
return
|
||||||
@@ -349,35 +396,35 @@ func (tc *TCPClientInterface) reconnect() {
|
|||||||
retries++
|
retries++
|
||||||
}
|
}
|
||||||
|
|
||||||
tc.mutex.Lock()
|
tc.Mutex.Lock()
|
||||||
tc.reconnecting = false
|
tc.reconnecting = false
|
||||||
tc.mutex.Unlock()
|
tc.Mutex.Unlock()
|
||||||
|
|
||||||
tc.teardown()
|
tc.teardown()
|
||||||
debug.Log(debug.DEBUG_ERROR, "Failed to reconnect after all attempts", "target", net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort)), "maxTries", tc.maxReconnectTries)
|
debug.Log(debug.DEBUG_ERROR, "Failed to reconnect after all attempts", "target", net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort)), "maxTries", tc.maxReconnectTries)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) Enable() {
|
func (tc *TCPClientInterface) Enable() {
|
||||||
tc.mutex.Lock()
|
tc.Mutex.Lock()
|
||||||
defer tc.mutex.Unlock()
|
defer tc.Mutex.Unlock()
|
||||||
tc.Online = true
|
tc.Online = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) Disable() {
|
func (tc *TCPClientInterface) Disable() {
|
||||||
tc.mutex.Lock()
|
tc.Mutex.Lock()
|
||||||
defer tc.mutex.Unlock()
|
defer tc.Mutex.Unlock()
|
||||||
tc.Online = false
|
tc.Online = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) IsConnected() bool {
|
func (tc *TCPClientInterface) IsConnected() bool {
|
||||||
tc.mutex.RLock()
|
tc.Mutex.RLock()
|
||||||
defer tc.mutex.RUnlock()
|
defer tc.Mutex.RUnlock()
|
||||||
return tc.conn != nil && tc.Online && !tc.reconnecting
|
return tc.conn != nil && tc.Online && !tc.reconnecting
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) GetRTT() time.Duration {
|
func (tc *TCPClientInterface) GetRTT() time.Duration {
|
||||||
tc.mutex.RLock()
|
tc.Mutex.RLock()
|
||||||
defer tc.mutex.RUnlock()
|
defer tc.Mutex.RUnlock()
|
||||||
|
|
||||||
if !tc.IsConnected() {
|
if !tc.IsConnected() {
|
||||||
return 0
|
return 0
|
||||||
@@ -400,52 +447,17 @@ func (tc *TCPClientInterface) GetRTT() time.Duration {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) GetTxBytes() uint64 {
|
|
||||||
tc.mutex.RLock()
|
|
||||||
defer tc.mutex.RUnlock()
|
|
||||||
return tc.TxBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tc *TCPClientInterface) GetRxBytes() uint64 {
|
|
||||||
tc.mutex.RLock()
|
|
||||||
defer tc.mutex.RUnlock()
|
|
||||||
return tc.RxBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tc *TCPClientInterface) UpdateStats(bytes uint64, isRx bool) {
|
|
||||||
tc.mutex.Lock()
|
|
||||||
defer tc.mutex.Unlock()
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
if isRx {
|
|
||||||
tc.RxBytes += bytes
|
|
||||||
tc.lastRx = now
|
|
||||||
debug.Log(debug.DEBUG_TRACE, "Interface RX stats", "name", tc.Name, "bytes", bytes, "total", tc.RxBytes, "last", tc.lastRx)
|
|
||||||
} else {
|
|
||||||
tc.TxBytes += bytes
|
|
||||||
tc.lastTx = now
|
|
||||||
debug.Log(debug.DEBUG_TRACE, "Interface TX stats", "name", tc.Name, "bytes", bytes, "total", tc.TxBytes, "last", tc.lastTx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tc *TCPClientInterface) GetStats() (tx uint64, rx uint64, lastTx time.Time, lastRx time.Time) {
|
|
||||||
tc.mutex.RLock()
|
|
||||||
defer tc.mutex.RUnlock()
|
|
||||||
return tc.TxBytes, tc.RxBytes, tc.lastTx, tc.lastRx
|
|
||||||
}
|
|
||||||
|
|
||||||
type TCPServerInterface struct {
|
type TCPServerInterface struct {
|
||||||
BaseInterface
|
BaseInterface
|
||||||
connections map[string]net.Conn
|
connections map[string]net.Conn
|
||||||
mutex sync.RWMutex
|
listener net.Listener
|
||||||
bindAddr string
|
bindAddr string
|
||||||
bindPort int
|
bindPort int
|
||||||
preferIPv6 bool
|
preferIPv6 bool
|
||||||
kissFraming bool
|
kissFraming bool
|
||||||
i2pTunneled bool
|
i2pTunneled bool
|
||||||
packetCallback common.PacketCallback
|
done chan struct{}
|
||||||
TxBytes uint64
|
stopOnce sync.Once
|
||||||
RxBytes uint64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFraming bool, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) {
|
func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFraming bool, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) {
|
||||||
@@ -456,6 +468,7 @@ func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFrami
|
|||||||
Type: common.IF_TYPE_TCP,
|
Type: common.IF_TYPE_TCP,
|
||||||
Online: false,
|
Online: false,
|
||||||
MTU: common.DEFAULT_MTU,
|
MTU: common.DEFAULT_MTU,
|
||||||
|
Enabled: true,
|
||||||
Detached: false,
|
Detached: false,
|
||||||
},
|
},
|
||||||
connections: make(map[string]net.Conn),
|
connections: make(map[string]net.Conn),
|
||||||
@@ -464,6 +477,7 @@ func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFrami
|
|||||||
preferIPv6: preferIPv6,
|
preferIPv6: preferIPv6,
|
||||||
kissFraming: kissFraming,
|
kissFraming: kissFraming,
|
||||||
i2pTunneled: i2pTunneled,
|
i2pTunneled: i2pTunneled,
|
||||||
|
done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
return ts, nil
|
return ts, nil
|
||||||
@@ -482,21 +496,21 @@ func (ts *TCPServerInterface) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) SetPacketCallback(callback common.PacketCallback) {
|
func (ts *TCPServerInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||||
ts.mutex.Lock()
|
ts.Mutex.Lock()
|
||||||
defer ts.mutex.Unlock()
|
defer ts.Mutex.Unlock()
|
||||||
ts.packetCallback = callback
|
ts.packetCallback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) GetPacketCallback() common.PacketCallback {
|
func (ts *TCPServerInterface) GetPacketCallback() common.PacketCallback {
|
||||||
ts.mutex.RLock()
|
ts.Mutex.RLock()
|
||||||
defer ts.mutex.RUnlock()
|
defer ts.Mutex.RUnlock()
|
||||||
return ts.packetCallback
|
return ts.packetCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) IsEnabled() bool {
|
func (ts *TCPServerInterface) IsEnabled() bool {
|
||||||
ts.mutex.RLock()
|
ts.Mutex.RLock()
|
||||||
defer ts.mutex.RUnlock()
|
defer ts.Mutex.RUnlock()
|
||||||
return ts.BaseInterface.Enabled && ts.BaseInterface.Online && !ts.BaseInterface.Detached
|
return ts.Enabled && ts.Online && !ts.Detached
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) GetName() string {
|
func (ts *TCPServerInterface) GetName() string {
|
||||||
@@ -504,32 +518,47 @@ func (ts *TCPServerInterface) GetName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) IsDetached() bool {
|
func (ts *TCPServerInterface) IsDetached() bool {
|
||||||
ts.mutex.RLock()
|
ts.Mutex.RLock()
|
||||||
defer ts.mutex.RUnlock()
|
defer ts.Mutex.RUnlock()
|
||||||
return ts.BaseInterface.Detached
|
return ts.Detached
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) IsOnline() bool {
|
func (ts *TCPServerInterface) IsOnline() bool {
|
||||||
ts.mutex.RLock()
|
ts.Mutex.RLock()
|
||||||
defer ts.mutex.RUnlock()
|
defer ts.Mutex.RUnlock()
|
||||||
return ts.Online
|
return ts.Online
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) Enable() {
|
func (ts *TCPServerInterface) Enable() {
|
||||||
ts.mutex.Lock()
|
ts.Mutex.Lock()
|
||||||
defer ts.mutex.Unlock()
|
defer ts.Mutex.Unlock()
|
||||||
ts.Online = true
|
ts.Online = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) Disable() {
|
func (ts *TCPServerInterface) Disable() {
|
||||||
ts.mutex.Lock()
|
ts.Mutex.Lock()
|
||||||
defer ts.mutex.Unlock()
|
defer ts.Mutex.Unlock()
|
||||||
ts.Online = false
|
ts.Online = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) Start() error {
|
func (ts *TCPServerInterface) Start() error {
|
||||||
ts.mutex.Lock()
|
ts.Mutex.Lock()
|
||||||
defer ts.mutex.Unlock()
|
if ts.listener != nil {
|
||||||
|
ts.Mutex.Unlock()
|
||||||
|
return fmt.Errorf("TCP server already started")
|
||||||
|
}
|
||||||
|
// Only recreate done if it's nil or was closed
|
||||||
|
select {
|
||||||
|
case <-ts.done:
|
||||||
|
ts.done = make(chan struct{})
|
||||||
|
ts.stopOnce = sync.Once{}
|
||||||
|
default:
|
||||||
|
if ts.done == nil {
|
||||||
|
ts.done = make(chan struct{})
|
||||||
|
ts.stopOnce = sync.Once{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ts.Mutex.Unlock()
|
||||||
|
|
||||||
addr := net.JoinHostPort(ts.bindAddr, fmt.Sprintf("%d", ts.bindPort))
|
addr := net.JoinHostPort(ts.bindAddr, fmt.Sprintf("%d", ts.bindPort))
|
||||||
listener, err := net.Listen("tcp", addr)
|
listener, err := net.Listen("tcp", addr)
|
||||||
@@ -537,14 +566,30 @@ func (ts *TCPServerInterface) Start() error {
|
|||||||
return fmt.Errorf("failed to start TCP server: %w", err)
|
return fmt.Errorf("failed to start TCP server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ts.Mutex.Lock()
|
||||||
|
ts.listener = listener
|
||||||
ts.Online = true
|
ts.Online = true
|
||||||
|
ts.Mutex.Unlock()
|
||||||
|
|
||||||
// Accept connections in a goroutine
|
// Accept connections in a goroutine
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
|
ts.Mutex.RLock()
|
||||||
|
done := ts.done
|
||||||
|
ts.Mutex.RUnlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
conn, err := listener.Accept()
|
conn, err := listener.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !ts.Online {
|
ts.Mutex.RLock()
|
||||||
|
online := ts.Online
|
||||||
|
ts.Mutex.RUnlock()
|
||||||
|
if !online {
|
||||||
return // Normal shutdown
|
return // Normal shutdown
|
||||||
}
|
}
|
||||||
debug.Log(debug.DEBUG_ERROR, "Error accepting connection", "error", err)
|
debug.Log(debug.DEBUG_ERROR, "Error accepting connection", "error", err)
|
||||||
@@ -560,60 +605,68 @@ func (ts *TCPServerInterface) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) Stop() error {
|
func (ts *TCPServerInterface) Stop() error {
|
||||||
ts.mutex.Lock()
|
ts.Mutex.Lock()
|
||||||
defer ts.mutex.Unlock()
|
|
||||||
|
|
||||||
ts.Online = false
|
ts.Online = false
|
||||||
|
if ts.listener != nil {
|
||||||
|
_ = ts.listener.Close()
|
||||||
|
ts.listener = nil
|
||||||
|
}
|
||||||
|
// Close all client connections
|
||||||
|
for addr, conn := range ts.connections {
|
||||||
|
_ = conn.Close()
|
||||||
|
delete(ts.connections, addr)
|
||||||
|
}
|
||||||
|
ts.Mutex.Unlock()
|
||||||
|
|
||||||
|
ts.stopOnce.Do(func() {
|
||||||
|
if ts.done != nil {
|
||||||
|
close(ts.done)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) GetTxBytes() uint64 {
|
|
||||||
ts.mutex.RLock()
|
|
||||||
defer ts.mutex.RUnlock()
|
|
||||||
return ts.TxBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *TCPServerInterface) GetRxBytes() uint64 {
|
|
||||||
ts.mutex.RLock()
|
|
||||||
defer ts.mutex.RUnlock()
|
|
||||||
return ts.RxBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
|
func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
|
||||||
addr := conn.RemoteAddr().String()
|
addr := conn.RemoteAddr().String()
|
||||||
ts.mutex.Lock()
|
ts.Mutex.Lock()
|
||||||
ts.connections[addr] = conn
|
ts.connections[addr] = conn
|
||||||
ts.mutex.Unlock()
|
ts.Mutex.Unlock()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
ts.mutex.Lock()
|
ts.Mutex.Lock()
|
||||||
delete(ts.connections, addr)
|
delete(ts.connections, addr)
|
||||||
ts.mutex.Unlock()
|
ts.Mutex.Unlock()
|
||||||
conn.Close() // #nosec G104
|
_ = conn.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
buffer := make([]byte, ts.MTU)
|
buffer := make([]byte, ts.MTU)
|
||||||
for {
|
for {
|
||||||
|
ts.Mutex.RLock()
|
||||||
|
done := ts.done
|
||||||
|
ts.Mutex.RUnlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
n, err := conn.Read(buffer)
|
n, err := conn.Read(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ts.mutex.Lock()
|
ts.ProcessIncoming(buffer[:n])
|
||||||
ts.RxBytes += uint64(n) // #nosec G115
|
|
||||||
ts.mutex.Unlock()
|
|
||||||
|
|
||||||
if ts.packetCallback != nil {
|
|
||||||
ts.packetCallback(buffer[:n], ts)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
|
func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
|
||||||
ts.mutex.RLock()
|
ts.Mutex.RLock()
|
||||||
defer ts.mutex.RUnlock()
|
online := ts.Online
|
||||||
|
ts.Mutex.RUnlock()
|
||||||
|
|
||||||
if !ts.Online {
|
if !online {
|
||||||
return fmt.Errorf("interface offline")
|
return fmt.Errorf("interface offline")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -626,9 +679,14 @@ func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
|
|||||||
frame = append(frame, HDLC_FLAG)
|
frame = append(frame, HDLC_FLAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
ts.TxBytes += uint64(len(frame))
|
ts.Mutex.Lock()
|
||||||
|
conns := make([]net.Conn, 0, len(ts.connections))
|
||||||
for _, conn := range ts.connections {
|
for _, conn := range ts.connections {
|
||||||
|
conns = append(conns, conn)
|
||||||
|
}
|
||||||
|
ts.Mutex.Unlock()
|
||||||
|
|
||||||
|
for _, conn := range conns {
|
||||||
if _, err := conn.Write(frame); err != nil {
|
if _, err := conn.Write(frame); err != nil {
|
||||||
debug.Log(debug.DEBUG_VERBOSE, "Error writing to connection", "address", conn.RemoteAddr(), "error", err)
|
debug.Log(debug.DEBUG_VERBOSE, "Error writing to connection", "address", conn.RemoteAddr(), "error", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
//go:build !linux
|
//go:build !linux
|
||||||
// +build !linux
|
// +build !linux
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
//go:build darwin
|
//go:build darwin
|
||||||
// +build darwin
|
// +build darwin
|
||||||
|
|
||||||
@@ -37,7 +39,7 @@ func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
|||||||
probeAfter = TCP_PROBE_AFTER_SEC
|
probeAfter = TCP_PROBE_AFTER_SEC
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, 1); err != nil {
|
if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, SO_KEEPALIVE_ENABLE); err != nil {
|
||||||
sockoptErr = fmt.Errorf("failed to enable SO_KEEPALIVE: %v", err)
|
sockoptErr = fmt.Errorf("failed to enable SO_KEEPALIVE: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
//go:build freebsd
|
//go:build freebsd
|
||||||
// +build freebsd
|
// +build freebsd
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
//go:build linux
|
//go:build linux
|
||||||
// +build linux
|
// +build linux
|
||||||
|
|
||||||
@@ -29,35 +31,40 @@ func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
|||||||
var userTimeout, probeAfter, probeInterval, probeCount int
|
var userTimeout, probeAfter, probeInterval, probeCount int
|
||||||
|
|
||||||
if tc.i2pTunneled {
|
if tc.i2pTunneled {
|
||||||
userTimeout = I2P_USER_TIMEOUT_SEC * 1000
|
userTimeout = I2P_USER_TIMEOUT_SEC * TCP_MILLISECONDS
|
||||||
probeAfter = I2P_PROBE_AFTER_SEC
|
probeAfter = I2P_PROBE_AFTER_SEC
|
||||||
probeInterval = I2P_PROBE_INTERVAL_SEC
|
probeInterval = I2P_PROBE_INTERVAL_SEC
|
||||||
probeCount = I2P_PROBES_COUNT
|
probeCount = I2P_PROBES_COUNT
|
||||||
} else {
|
} else {
|
||||||
userTimeout = TCP_USER_TIMEOUT_SEC * 1000
|
userTimeout = TCP_USER_TIMEOUT_SEC * TCP_MILLISECONDS
|
||||||
probeAfter = TCP_PROBE_AFTER_SEC
|
probeAfter = TCP_PROBE_AFTER_SEC
|
||||||
probeInterval = TCP_PROBE_INTERVAL_SEC
|
probeInterval = TCP_PROBE_INTERVAL_SEC
|
||||||
probeCount = TCP_PROBES_COUNT
|
probeCount = TCP_PROBES_COUNT
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, 18, userTimeout); err != nil {
|
const TCP_USER_TIMEOUT = 18
|
||||||
|
const TCP_KEEPIDLE = 4
|
||||||
|
const TCP_KEEPINTVL = 5
|
||||||
|
const TCP_KEEPCNT = 6
|
||||||
|
|
||||||
|
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_USER_TIMEOUT, userTimeout); err != nil {
|
||||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_USER_TIMEOUT", "error", err)
|
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_USER_TIMEOUT", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, 1); err != nil {
|
if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, SO_KEEPALIVE_ENABLE); err != nil {
|
||||||
sockoptErr = fmt.Errorf("failed to enable SO_KEEPALIVE: %v", err)
|
sockoptErr = fmt.Errorf("failed to enable SO_KEEPALIVE: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, 4, probeAfter); err != nil {
|
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_KEEPIDLE, probeAfter); err != nil {
|
||||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_KEEPIDLE", "error", err)
|
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_KEEPIDLE", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, 5, probeInterval); err != nil {
|
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_KEEPINTVL, probeInterval); err != nil {
|
||||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_KEEPINTVL", "error", err)
|
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_KEEPINTVL", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, 6, probeCount); err != nil {
|
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_KEEPCNT, probeCount); err != nil {
|
||||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_KEEPCNT", "error", err)
|
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_KEEPCNT", "error", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -82,13 +89,13 @@ func platformGetRTT(fd uintptr) time.Duration {
|
|||||||
// bearer:disable go_gosec_unsafe_unsafe
|
// bearer:disable go_gosec_unsafe_unsafe
|
||||||
infoLen := uint32(unsafe.Sizeof(info))
|
infoLen := uint32(unsafe.Sizeof(info))
|
||||||
|
|
||||||
// TCP_INFO is 11 on Linux
|
const TCP_INFO = 11
|
||||||
// #nosec G103
|
// #nosec G103
|
||||||
_, _, errno := syscall.Syscall6(
|
_, _, errno := syscall.Syscall6(
|
||||||
syscall.SYS_GETSOCKOPT,
|
syscall.SYS_GETSOCKOPT,
|
||||||
fd,
|
fd,
|
||||||
syscall.IPPROTO_TCP,
|
syscall.IPPROTO_TCP,
|
||||||
11, // TCP_INFO
|
TCP_INFO,
|
||||||
// bearer:disable go_gosec_unsafe_unsafe
|
// bearer:disable go_gosec_unsafe_unsafe
|
||||||
uintptr(unsafe.Pointer(&info)),
|
uintptr(unsafe.Pointer(&info)),
|
||||||
// bearer:disable go_gosec_unsafe_unsafe
|
// bearer:disable go_gosec_unsafe_unsafe
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
//go:build netbsd
|
//go:build netbsd
|
||||||
// +build netbsd
|
// +build netbsd
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
//go:build openbsd
|
//go:build openbsd
|
||||||
// +build openbsd
|
// +build openbsd
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
//go:build js && wasm
|
//go:build js && wasm
|
||||||
// +build js,wasm
|
// +build js,wasm
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package interfaces
|
package interfaces
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package interfaces
|
package interfaces
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -14,8 +16,9 @@ type UDPInterface struct {
|
|||||||
conn *net.UDPConn
|
conn *net.UDPConn
|
||||||
addr *net.UDPAddr
|
addr *net.UDPAddr
|
||||||
targetAddr *net.UDPAddr
|
targetAddr *net.UDPAddr
|
||||||
mutex sync.RWMutex
|
|
||||||
readBuffer []byte
|
readBuffer []byte
|
||||||
|
done chan struct{}
|
||||||
|
stopOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) {
|
func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) {
|
||||||
@@ -36,10 +39,11 @@ func NewUDPInterface(name string, addr string, target string, enabled bool) (*UD
|
|||||||
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
|
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
|
||||||
addr: udpAddr,
|
addr: udpAddr,
|
||||||
targetAddr: targetAddr,
|
targetAddr: targetAddr,
|
||||||
readBuffer: make([]byte, 1064),
|
readBuffer: make([]byte, common.NUM_1064),
|
||||||
|
done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.MTU = 1064
|
ui.MTU = common.NUM_1064
|
||||||
|
|
||||||
return ui, nil
|
return ui, nil
|
||||||
}
|
}
|
||||||
@@ -57,60 +61,41 @@ func (ui *UDPInterface) GetMode() common.InterfaceMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) IsOnline() bool {
|
func (ui *UDPInterface) IsOnline() bool {
|
||||||
ui.mutex.RLock()
|
ui.Mutex.RLock()
|
||||||
defer ui.mutex.RUnlock()
|
defer ui.Mutex.RUnlock()
|
||||||
return ui.Online
|
return ui.Online
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) IsDetached() bool {
|
func (ui *UDPInterface) IsDetached() bool {
|
||||||
ui.mutex.RLock()
|
ui.Mutex.RLock()
|
||||||
defer ui.mutex.RUnlock()
|
defer ui.Mutex.RUnlock()
|
||||||
return ui.Detached
|
return ui.Detached
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) Detach() {
|
func (ui *UDPInterface) Detach() {
|
||||||
ui.mutex.Lock()
|
ui.Mutex.Lock()
|
||||||
defer ui.mutex.Unlock()
|
defer ui.Mutex.Unlock()
|
||||||
ui.Detached = true
|
ui.Detached = true
|
||||||
|
ui.Online = false
|
||||||
if ui.conn != nil {
|
if ui.conn != nil {
|
||||||
ui.conn.Close() // #nosec G104
|
ui.conn.Close() // #nosec G104
|
||||||
}
|
}
|
||||||
}
|
ui.stopOnce.Do(func() {
|
||||||
|
if ui.done != nil {
|
||||||
func (ui *UDPInterface) Send(data []byte, addr string) error {
|
close(ui.done)
|
||||||
debug.Log(debug.DEBUG_ALL, "UDP interface sending bytes", "name", ui.Name, "bytes", len(data))
|
}
|
||||||
|
})
|
||||||
if !ui.IsEnabled() {
|
|
||||||
return fmt.Errorf("interface not enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.targetAddr == nil {
|
|
||||||
return fmt.Errorf("no target address configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update TX stats before sending
|
|
||||||
ui.mutex.Lock()
|
|
||||||
ui.TxBytes += uint64(len(data))
|
|
||||||
ui.mutex.Unlock()
|
|
||||||
|
|
||||||
_, err := ui.conn.WriteTo(data, ui.targetAddr)
|
|
||||||
if err != nil {
|
|
||||||
debug.Log(debug.DEBUG_CRITICAL, "UDP interface write failed", "name", ui.Name, "error", err)
|
|
||||||
} else {
|
|
||||||
debug.Log(debug.DEBUG_ALL, "UDP interface sent bytes successfully", "name", ui.Name, "bytes", len(data))
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) {
|
func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||||
ui.mutex.Lock()
|
ui.Mutex.Lock()
|
||||||
defer ui.mutex.Unlock()
|
defer ui.Mutex.Unlock()
|
||||||
ui.packetCallback = callback
|
ui.packetCallback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) GetPacketCallback() common.PacketCallback {
|
func (ui *UDPInterface) GetPacketCallback() common.PacketCallback {
|
||||||
ui.mutex.RLock()
|
ui.Mutex.RLock()
|
||||||
defer ui.mutex.RUnlock()
|
defer ui.Mutex.RUnlock()
|
||||||
return ui.packetCallback
|
return ui.packetCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,10 +119,6 @@ func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
|
|||||||
return fmt.Errorf("UDP write failed: %v", err)
|
return fmt.Errorf("UDP write failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.mutex.Lock()
|
|
||||||
ui.TxBytes += uint64(len(data))
|
|
||||||
ui.mutex.Unlock()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,14 +127,14 @@ func (ui *UDPInterface) GetConn() net.Conn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) GetTxBytes() uint64 {
|
func (ui *UDPInterface) GetTxBytes() uint64 {
|
||||||
ui.mutex.RLock()
|
ui.Mutex.RLock()
|
||||||
defer ui.mutex.RUnlock()
|
defer ui.Mutex.RUnlock()
|
||||||
return ui.TxBytes
|
return ui.TxBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) GetRxBytes() uint64 {
|
func (ui *UDPInterface) GetRxBytes() uint64 {
|
||||||
ui.mutex.RLock()
|
ui.Mutex.RLock()
|
||||||
defer ui.mutex.RUnlock()
|
defer ui.Mutex.RUnlock()
|
||||||
return ui.RxBytes
|
return ui.RxBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,18 +147,36 @@ func (ui *UDPInterface) GetBitrate() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) Enable() {
|
func (ui *UDPInterface) Enable() {
|
||||||
ui.mutex.Lock()
|
ui.Mutex.Lock()
|
||||||
defer ui.mutex.Unlock()
|
defer ui.Mutex.Unlock()
|
||||||
ui.Online = true
|
ui.Online = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) Disable() {
|
func (ui *UDPInterface) Disable() {
|
||||||
ui.mutex.Lock()
|
ui.Mutex.Lock()
|
||||||
defer ui.mutex.Unlock()
|
defer ui.Mutex.Unlock()
|
||||||
ui.Online = false
|
ui.Online = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) Start() error {
|
func (ui *UDPInterface) Start() error {
|
||||||
|
ui.Mutex.Lock()
|
||||||
|
if ui.conn != nil {
|
||||||
|
ui.Mutex.Unlock()
|
||||||
|
return fmt.Errorf("UDP interface already started")
|
||||||
|
}
|
||||||
|
// Only recreate done if it's nil or was closed
|
||||||
|
select {
|
||||||
|
case <-ui.done:
|
||||||
|
ui.done = make(chan struct{})
|
||||||
|
ui.stopOnce = sync.Once{}
|
||||||
|
default:
|
||||||
|
if ui.done == nil {
|
||||||
|
ui.done = make(chan struct{})
|
||||||
|
ui.stopOnce = sync.Once{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.Mutex.Unlock()
|
||||||
|
|
||||||
conn, err := net.ListenUDP("udp", ui.addr)
|
conn, err := net.ListenUDP("udp", ui.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -187,15 +186,17 @@ func (ui *UDPInterface) Start() error {
|
|||||||
// Enable broadcast mode if we have a target address
|
// Enable broadcast mode if we have a target address
|
||||||
if ui.targetAddr != nil {
|
if ui.targetAddr != nil {
|
||||||
// Get the raw connection file descriptor to set SO_BROADCAST
|
// Get the raw connection file descriptor to set SO_BROADCAST
|
||||||
if err := conn.SetReadBuffer(1064); err != nil {
|
if err := conn.SetReadBuffer(common.NUM_1064); err != nil {
|
||||||
debug.Log(debug.DEBUG_ERROR, "Failed to set read buffer size", "error", err)
|
debug.Log(debug.DEBUG_ERROR, "Failed to set read buffer size", "error", err)
|
||||||
}
|
}
|
||||||
if err := conn.SetWriteBuffer(1064); err != nil {
|
if err := conn.SetWriteBuffer(common.NUM_1064); err != nil {
|
||||||
debug.Log(debug.DEBUG_ERROR, "Failed to set write buffer size", "error", err)
|
debug.Log(debug.DEBUG_ERROR, "Failed to set write buffer size", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ui.Mutex.Lock()
|
||||||
ui.Online = true
|
ui.Online = true
|
||||||
|
ui.Mutex.Unlock()
|
||||||
|
|
||||||
// Start the read loop in a goroutine
|
// Start the read loop in a goroutine
|
||||||
go ui.readLoop()
|
go ui.readLoop()
|
||||||
@@ -203,37 +204,56 @@ func (ui *UDPInterface) Start() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) Stop() error {
|
||||||
|
ui.Detach()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) readLoop() {
|
func (ui *UDPInterface) readLoop() {
|
||||||
buffer := make([]byte, 1064)
|
buffer := make([]byte, common.NUM_1064)
|
||||||
for ui.IsOnline() && !ui.IsDetached() {
|
for {
|
||||||
n, remoteAddr, err := ui.conn.ReadFromUDP(buffer)
|
ui.Mutex.RLock()
|
||||||
|
online := ui.Online
|
||||||
|
detached := ui.Detached
|
||||||
|
conn := ui.conn
|
||||||
|
done := ui.done
|
||||||
|
ui.Mutex.RUnlock()
|
||||||
|
|
||||||
|
if !online || detached || conn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
n, remoteAddr, err := conn.ReadFromUDP(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if ui.IsOnline() {
|
ui.Mutex.RLock()
|
||||||
|
stillOnline := ui.Online
|
||||||
|
ui.Mutex.RUnlock()
|
||||||
|
if stillOnline {
|
||||||
debug.Log(debug.DEBUG_ERROR, "Error reading from UDP interface", "name", ui.Name, "error", err)
|
debug.Log(debug.DEBUG_ERROR, "Error reading from UDP interface", "name", ui.Name, "error", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update RX stats
|
ui.Mutex.Lock()
|
||||||
ui.mutex.Lock()
|
|
||||||
// #nosec G115 - Network read sizes are always positive and within safe range
|
|
||||||
ui.RxBytes += uint64(n)
|
|
||||||
|
|
||||||
// Auto-discover target address from first packet if not set
|
// Auto-discover target address from first packet if not set
|
||||||
if ui.targetAddr == nil {
|
if ui.targetAddr == nil {
|
||||||
debug.Log(debug.DEBUG_ALL, "UDP interface discovered peer", "name", ui.Name, "peer", remoteAddr.String())
|
debug.Log(debug.DEBUG_ALL, "UDP interface discovered peer", "name", ui.Name, "peer", remoteAddr.String())
|
||||||
ui.targetAddr = remoteAddr
|
ui.targetAddr = remoteAddr
|
||||||
}
|
}
|
||||||
ui.mutex.Unlock()
|
ui.Mutex.Unlock()
|
||||||
|
|
||||||
if ui.packetCallback != nil {
|
ui.ProcessIncoming(buffer[:n])
|
||||||
ui.packetCallback(buffer[:n], ui)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) IsEnabled() bool {
|
func (ui *UDPInterface) IsEnabled() bool {
|
||||||
ui.mutex.RLock()
|
ui.Mutex.RLock()
|
||||||
defer ui.mutex.RUnlock()
|
defer ui.Mutex.RUnlock()
|
||||||
return ui.Enabled && ui.Online && !ui.Detached
|
return ui.Enabled && ui.Online && !ui.Detached
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ func TestNewUDPInterface(t *testing.T) {
|
|||||||
|
|
||||||
func TestUDPInterfaceState(t *testing.T) {
|
func TestUDPInterfaceState(t *testing.T) {
|
||||||
// Basic state tests are covered by BaseInterface tests
|
// Basic state tests are covered by BaseInterface tests
|
||||||
// Add specific UDP ones if needed, e.g., involving the conn
|
|
||||||
addr := "127.0.0.1:0"
|
addr := "127.0.0.1:0"
|
||||||
ui, _ := NewUDPInterface("udpState", addr, "", true)
|
ui, _ := NewUDPInterface("udpState", addr, "", true)
|
||||||
|
|
||||||
|
|||||||
714
pkg/interfaces/websocket_native.go
Normal file
714
pkg/interfaces/websocket_native.go
Normal file
@@ -0,0 +1,714 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
//go:build !js
|
||||||
|
// +build !js
|
||||||
|
|
||||||
|
// WebSocketInterface is a native implementation of the WebSocket interface.
|
||||||
|
// It is used to connect to the WebSocket server and send/receive data.
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/rand"
|
||||||
|
// bearer:disable go_gosec_blocklist_sha1
|
||||||
|
"crypto/sha1" // #nosec G505
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
wsGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||||
|
|
||||||
|
WS_BUFFER_SIZE = 4096
|
||||||
|
WS_MTU = 1064
|
||||||
|
WS_BITRATE = 10000000
|
||||||
|
WS_HTTPS_PORT = 443
|
||||||
|
WS_HTTP_PORT = 80
|
||||||
|
WS_VERSION = "13"
|
||||||
|
WS_CONNECT_TIMEOUT = 10 * time.Second
|
||||||
|
WS_RECONNECT_DELAY = 2 * time.Second
|
||||||
|
WS_KEY_SIZE = 16
|
||||||
|
WS_MASK_KEY_SIZE = 4
|
||||||
|
WS_HEADER_SIZE = 2
|
||||||
|
WS_PAYLOAD_LEN_16BIT = 126
|
||||||
|
WS_PAYLOAD_LEN_64BIT = 127
|
||||||
|
WS_MAX_PAYLOAD_16BIT = 65536
|
||||||
|
WS_FRAME_HEADER_FIN = 0x80
|
||||||
|
WS_FRAME_HEADER_OPCODE = 0x0F
|
||||||
|
WS_FRAME_HEADER_MASKED = 0x80
|
||||||
|
WS_FRAME_HEADER_LEN = 0x7F
|
||||||
|
WS_OPCODE_CONTINUATION = 0x00
|
||||||
|
WS_OPCODE_TEXT = 0x01
|
||||||
|
WS_OPCODE_BINARY = 0x02
|
||||||
|
WS_OPCODE_CLOSE = 0x08
|
||||||
|
WS_OPCODE_PING = 0x09
|
||||||
|
WS_OPCODE_PONG = 0x0A
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebSocketInterface struct {
|
||||||
|
BaseInterface
|
||||||
|
wsURL string
|
||||||
|
conn net.Conn
|
||||||
|
reader *bufio.Reader
|
||||||
|
connected bool
|
||||||
|
messageQueue [][]byte
|
||||||
|
readBuffer []byte
|
||||||
|
writeBuffer []byte
|
||||||
|
done chan struct{}
|
||||||
|
stopOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebSocketInterface(name string, wsURL string, enabled bool) (*WebSocketInterface, error) {
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "NewWebSocketInterface called", "name", name, "url", wsURL, "enabled", enabled)
|
||||||
|
ws := &WebSocketInterface{
|
||||||
|
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
|
||||||
|
wsURL: wsURL,
|
||||||
|
messageQueue: make([][]byte, 0),
|
||||||
|
readBuffer: make([]byte, WS_BUFFER_SIZE),
|
||||||
|
writeBuffer: make([]byte, WS_BUFFER_SIZE),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.MTU = WS_MTU
|
||||||
|
ws.Bitrate = WS_BITRATE
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "WebSocket interface initialized", "name", name, "mtu", ws.MTU, "bitrate", ws.Bitrate)
|
||||||
|
return ws, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) GetName() string {
|
||||||
|
return wsi.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) GetType() common.InterfaceType {
|
||||||
|
return wsi.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) GetMode() common.InterfaceMode {
|
||||||
|
return wsi.Mode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) IsOnline() bool {
|
||||||
|
wsi.Mutex.RLock()
|
||||||
|
defer wsi.Mutex.RUnlock()
|
||||||
|
return wsi.Online && wsi.connected
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) IsDetached() bool {
|
||||||
|
wsi.Mutex.RLock()
|
||||||
|
defer wsi.Mutex.RUnlock()
|
||||||
|
return wsi.Detached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) Detach() {
|
||||||
|
wsi.Mutex.Lock()
|
||||||
|
defer wsi.Mutex.Unlock()
|
||||||
|
wsi.Detached = true
|
||||||
|
wsi.Online = false
|
||||||
|
wsi.closeWebSocketLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) Enable() {
|
||||||
|
wsi.Mutex.Lock()
|
||||||
|
defer wsi.Mutex.Unlock()
|
||||||
|
wsi.Enabled = true
|
||||||
|
wsi.Online = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) Disable() {
|
||||||
|
wsi.Mutex.Lock()
|
||||||
|
defer wsi.Mutex.Unlock()
|
||||||
|
wsi.Enabled = false
|
||||||
|
wsi.closeWebSocketLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) Start() error {
|
||||||
|
wsi.Mutex.Lock()
|
||||||
|
if !wsi.Enabled || wsi.Detached {
|
||||||
|
wsi.Mutex.Unlock()
|
||||||
|
debug.Log(debug.DEBUG_INFO, "WebSocket interface not enabled or detached", "name", wsi.Name)
|
||||||
|
return fmt.Errorf("interface not enabled or detached")
|
||||||
|
}
|
||||||
|
if wsi.conn != nil {
|
||||||
|
wsi.Mutex.Unlock()
|
||||||
|
debug.Log(debug.DEBUG_INFO, "WebSocket already started", "name", wsi.Name)
|
||||||
|
return fmt.Errorf("WebSocket already started")
|
||||||
|
}
|
||||||
|
// Only recreate done if it's nil or was closed
|
||||||
|
select {
|
||||||
|
case <-wsi.done:
|
||||||
|
wsi.done = make(chan struct{})
|
||||||
|
wsi.stopOnce = sync.Once{}
|
||||||
|
default:
|
||||||
|
if wsi.done == nil {
|
||||||
|
wsi.done = make(chan struct{})
|
||||||
|
wsi.stopOnce = sync.Once{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wsi.Mutex.Unlock()
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_INFO, "Starting WebSocket connection", "name", wsi.Name, "url", wsi.wsURL)
|
||||||
|
|
||||||
|
u, err := url.Parse(wsi.wsURL)
|
||||||
|
if err != nil {
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Invalid WebSocket URL", "name", wsi.Name, "url", wsi.wsURL, "error", err)
|
||||||
|
return fmt.Errorf("invalid WebSocket URL: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var conn net.Conn
|
||||||
|
var host string
|
||||||
|
|
||||||
|
if u.Scheme == "wss" {
|
||||||
|
host = u.Host
|
||||||
|
if !strings.Contains(host, ":") {
|
||||||
|
host += fmt.Sprintf(":%d", WS_HTTPS_PORT)
|
||||||
|
}
|
||||||
|
tcpConn, err := net.DialTimeout("tcp", host, WS_CONNECT_TIMEOUT)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect: %v", err)
|
||||||
|
}
|
||||||
|
tlsConn := tls.Client(tcpConn, &tls.Config{
|
||||||
|
ServerName: u.Hostname(),
|
||||||
|
InsecureSkipVerify: false,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
})
|
||||||
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
|
_ = tcpConn.Close()
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "TLS handshake failed", "name", wsi.Name, "host", host, "error", err)
|
||||||
|
return fmt.Errorf("TLS handshake failed: %v", err)
|
||||||
|
}
|
||||||
|
conn = tlsConn
|
||||||
|
} else if u.Scheme == "ws" {
|
||||||
|
host = u.Host
|
||||||
|
if !strings.Contains(host, ":") {
|
||||||
|
host += fmt.Sprintf(":%d", WS_HTTP_PORT)
|
||||||
|
}
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "Connecting to WebSocket server", "name", wsi.Name, "host", host)
|
||||||
|
tcpConn, err := net.DialTimeout("tcp", host, WS_CONNECT_TIMEOUT)
|
||||||
|
if err != nil {
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Failed to connect to WebSocket server", "name", wsi.Name, "host", host, "error", err)
|
||||||
|
return fmt.Errorf("failed to connect: %v", err)
|
||||||
|
}
|
||||||
|
conn = tcpConn
|
||||||
|
} else {
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Unsupported WebSocket scheme", "name", wsi.Name, "scheme", u.Scheme)
|
||||||
|
return fmt.Errorf("unsupported scheme: %s (use ws:// or wss://)", u.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := generateWebSocketKey()
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return fmt.Errorf("failed to generate key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := u.Path
|
||||||
|
if path == "" {
|
||||||
|
path = "/"
|
||||||
|
}
|
||||||
|
if u.RawQuery != "" {
|
||||||
|
path += "?" + u.RawQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", path, nil)
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return fmt.Errorf("failed to create request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Host = u.Host
|
||||||
|
req.Header.Set("Upgrade", "websocket")
|
||||||
|
req.Header.Set("Connection", "Upgrade")
|
||||||
|
req.Header.Set("Sec-WebSocket-Key", key)
|
||||||
|
req.Header.Set("Sec-WebSocket-Version", WS_VERSION)
|
||||||
|
req.Header.Set("User-Agent", "Reticulum-Go/1.0")
|
||||||
|
|
||||||
|
if err := req.Write(conn); err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return fmt.Errorf("failed to send handshake: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.ReadResponse(bufio.NewReader(conn), req)
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return fmt.Errorf("failed to read handshake response: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||||
|
_ = conn.Close()
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "WebSocket handshake failed", "name", wsi.Name, "status", resp.StatusCode)
|
||||||
|
return fmt.Errorf("handshake failed: status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" {
|
||||||
|
_ = conn.Close()
|
||||||
|
return fmt.Errorf("invalid upgrade header")
|
||||||
|
}
|
||||||
|
|
||||||
|
accept := resp.Header.Get("Sec-WebSocket-Accept")
|
||||||
|
expectedAccept := computeAcceptKey(key)
|
||||||
|
if accept != expectedAccept {
|
||||||
|
_ = conn.Close()
|
||||||
|
return fmt.Errorf("invalid accept key")
|
||||||
|
}
|
||||||
|
|
||||||
|
wsi.Mutex.Lock()
|
||||||
|
wsi.conn = conn
|
||||||
|
wsi.reader = bufio.NewReader(conn)
|
||||||
|
wsi.connected = true
|
||||||
|
wsi.Online = true
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_INFO, "WebSocket connected", "name", wsi.Name, "url", wsi.wsURL)
|
||||||
|
|
||||||
|
queue := make([][]byte, len(wsi.messageQueue))
|
||||||
|
copy(queue, wsi.messageQueue)
|
||||||
|
wsi.messageQueue = wsi.messageQueue[:0]
|
||||||
|
wsi.Mutex.Unlock() // Unlock after copying queue, before I/O
|
||||||
|
|
||||||
|
for _, msg := range queue {
|
||||||
|
_ = wsi.sendWebSocketMessage(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
go wsi.readLoop()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) Stop() error {
|
||||||
|
wsi.Mutex.Lock()
|
||||||
|
defer wsi.Mutex.Unlock()
|
||||||
|
|
||||||
|
wsi.Enabled = false
|
||||||
|
wsi.Online = false
|
||||||
|
|
||||||
|
wsi.stopOnce.Do(func() {
|
||||||
|
if wsi.done != nil {
|
||||||
|
close(wsi.done)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
wsi.closeWebSocketLocked()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) closeWebSocket() {
|
||||||
|
wsi.Mutex.Lock()
|
||||||
|
defer wsi.Mutex.Unlock()
|
||||||
|
wsi.closeWebSocketLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) closeWebSocketLocked() {
|
||||||
|
if wsi.conn != nil {
|
||||||
|
wsi.sendCloseFrameLocked()
|
||||||
|
_ = wsi.conn.Close()
|
||||||
|
wsi.conn = nil
|
||||||
|
wsi.reader = nil
|
||||||
|
}
|
||||||
|
wsi.connected = false
|
||||||
|
wsi.Online = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) readLoop() {
|
||||||
|
for {
|
||||||
|
wsi.Mutex.RLock()
|
||||||
|
conn := wsi.conn
|
||||||
|
reader := wsi.reader
|
||||||
|
done := wsi.done
|
||||||
|
wsi.Mutex.RUnlock()
|
||||||
|
|
||||||
|
if conn == nil || reader == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := wsi.readFrame()
|
||||||
|
if err != nil {
|
||||||
|
wsi.Mutex.Lock()
|
||||||
|
wsi.connected = false
|
||||||
|
wsi.Online = false
|
||||||
|
if wsi.conn != nil {
|
||||||
|
_ = wsi.conn.Close()
|
||||||
|
wsi.conn = nil
|
||||||
|
wsi.reader = nil
|
||||||
|
}
|
||||||
|
wsi.Mutex.Unlock()
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_INFO, "WebSocket closed", "name", wsi.Name, "error", err)
|
||||||
|
|
||||||
|
time.Sleep(WS_RECONNECT_DELAY)
|
||||||
|
|
||||||
|
wsi.Mutex.RLock()
|
||||||
|
stillEnabled := wsi.Enabled && !wsi.Detached
|
||||||
|
wsi.Mutex.RUnlock()
|
||||||
|
|
||||||
|
if stillEnabled {
|
||||||
|
go wsi.Start()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) > 0 {
|
||||||
|
wsi.Mutex.Lock()
|
||||||
|
wsi.RxBytes += uint64(len(data))
|
||||||
|
wsi.Mutex.Unlock()
|
||||||
|
|
||||||
|
wsi.ProcessIncoming(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) readFrame() ([]byte, error) {
|
||||||
|
wsi.Mutex.RLock()
|
||||||
|
reader := wsi.reader
|
||||||
|
wsi.Mutex.RUnlock()
|
||||||
|
|
||||||
|
if reader == nil {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
header := make([]byte, WS_HEADER_SIZE)
|
||||||
|
if _, err := io.ReadFull(reader, header); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fin := (header[0] & WS_FRAME_HEADER_FIN) != 0
|
||||||
|
opcode := header[0] & WS_FRAME_HEADER_OPCODE
|
||||||
|
masked := (header[1] & WS_FRAME_HEADER_MASKED) != 0
|
||||||
|
payloadLen := int(header[1] & WS_FRAME_HEADER_LEN)
|
||||||
|
|
||||||
|
if opcode == WS_OPCODE_CLOSE {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if opcode == WS_OPCODE_PING {
|
||||||
|
return wsi.handlePingFrame(reader, payloadLen, masked)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opcode == WS_OPCODE_PONG {
|
||||||
|
return wsi.handlePongFrame(reader, payloadLen, masked)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opcode != WS_OPCODE_BINARY {
|
||||||
|
return nil, fmt.Errorf("unsupported opcode: %d", opcode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if payloadLen == WS_PAYLOAD_LEN_16BIT {
|
||||||
|
lenBytes := make([]byte, 2)
|
||||||
|
if _, err := io.ReadFull(reader, lenBytes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
payloadLen = int(binary.BigEndian.Uint16(lenBytes))
|
||||||
|
} else if payloadLen == WS_PAYLOAD_LEN_64BIT {
|
||||||
|
lenBytes := make([]byte, 8)
|
||||||
|
if _, err := io.ReadFull(reader, lenBytes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
val := binary.BigEndian.Uint64(lenBytes)
|
||||||
|
if val > uint64(math.MaxInt) {
|
||||||
|
return nil, fmt.Errorf("payload length exceeds maximum integer value")
|
||||||
|
}
|
||||||
|
payloadLen = int(val) // #nosec G115
|
||||||
|
}
|
||||||
|
|
||||||
|
maskKey := make([]byte, WS_MASK_KEY_SIZE)
|
||||||
|
if masked {
|
||||||
|
if _, err := io.ReadFull(reader, maskKey); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := make([]byte, payloadLen)
|
||||||
|
if _, err := io.ReadFull(reader, payload); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if masked {
|
||||||
|
for i := 0; i < payloadLen; i++ {
|
||||||
|
payload[i] ^= maskKey[i%WS_MASK_KEY_SIZE]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fin {
|
||||||
|
nextFrame, err := wsi.readFrame()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return append(payload, nextFrame...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) Send(data []byte, addr string) error {
|
||||||
|
wsi.Mutex.RLock()
|
||||||
|
enabled := wsi.Enabled
|
||||||
|
detached := wsi.Detached
|
||||||
|
connected := wsi.connected
|
||||||
|
wsi.Mutex.RUnlock()
|
||||||
|
|
||||||
|
if !enabled || detached {
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "WebSocket interface not enabled or detached, dropping packet", "name", wsi.Name, "bytes", len(data))
|
||||||
|
return fmt.Errorf("interface not enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
wsi.Mutex.Lock()
|
||||||
|
wsi.TxBytes += uint64(len(data))
|
||||||
|
wsi.Mutex.Unlock()
|
||||||
|
|
||||||
|
if !connected {
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "WebSocket not connected, queuing packet", "name", wsi.Name, "bytes", len(data), "queue_size", len(wsi.messageQueue))
|
||||||
|
wsi.Mutex.Lock()
|
||||||
|
wsi.messageQueue = append(wsi.messageQueue, data)
|
||||||
|
wsi.Mutex.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
packetType := "unknown"
|
||||||
|
if len(data) > 0 {
|
||||||
|
switch data[0] {
|
||||||
|
case 0x01:
|
||||||
|
packetType = "announce"
|
||||||
|
case 0x02:
|
||||||
|
packetType = "link"
|
||||||
|
default:
|
||||||
|
packetType = fmt.Sprintf("0x%02x", data[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug.Log(debug.DEBUG_INFO, "Sending packet over WebSocket", "name", wsi.Name, "bytes", len(data), "packet_type", packetType)
|
||||||
|
return wsi.sendWebSocketMessage(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) sendWebSocketMessage(data []byte) error {
|
||||||
|
wsi.Mutex.RLock()
|
||||||
|
conn := wsi.conn
|
||||||
|
wsi.Mutex.RUnlock()
|
||||||
|
|
||||||
|
if conn == nil {
|
||||||
|
return fmt.Errorf("WebSocket not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
frame := wsi.createFrame(data, WS_OPCODE_BINARY, true)
|
||||||
|
wsi.Mutex.Lock()
|
||||||
|
_, err := conn.Write(frame)
|
||||||
|
wsi.Mutex.Unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_INFO, "WebSocket sent packet successfully", "name", wsi.Name, "bytes", len(data), "frame_bytes", len(frame))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) sendCloseFrame() {
|
||||||
|
wsi.Mutex.RLock()
|
||||||
|
defer wsi.Mutex.RUnlock()
|
||||||
|
wsi.sendCloseFrameLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) sendCloseFrameLocked() {
|
||||||
|
conn := wsi.conn
|
||||||
|
if conn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
frame := wsi.createFrame(nil, WS_OPCODE_CLOSE, true)
|
||||||
|
_, _ = conn.Write(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) handlePingFrame(reader *bufio.Reader, payloadLen int, masked bool) ([]byte, error) {
|
||||||
|
if payloadLen == WS_PAYLOAD_LEN_16BIT {
|
||||||
|
lenBytes := make([]byte, 2)
|
||||||
|
if _, err := io.ReadFull(reader, lenBytes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
payloadLen = int(binary.BigEndian.Uint16(lenBytes))
|
||||||
|
} else if payloadLen == WS_PAYLOAD_LEN_64BIT {
|
||||||
|
lenBytes := make([]byte, 8)
|
||||||
|
if _, err := io.ReadFull(reader, lenBytes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
val := binary.BigEndian.Uint64(lenBytes)
|
||||||
|
if val > uint64(math.MaxInt) {
|
||||||
|
return nil, fmt.Errorf("payload length exceeds maximum integer value")
|
||||||
|
}
|
||||||
|
payloadLen = int(val) // #nosec G115
|
||||||
|
}
|
||||||
|
|
||||||
|
maskKey := make([]byte, WS_MASK_KEY_SIZE)
|
||||||
|
if masked {
|
||||||
|
if _, err := io.ReadFull(reader, maskKey); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := make([]byte, payloadLen)
|
||||||
|
if payloadLen > 0 {
|
||||||
|
if _, err := io.ReadFull(reader, payload); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if masked {
|
||||||
|
for i := 0; i < payloadLen; i++ {
|
||||||
|
payload[i] ^= maskKey[i%WS_MASK_KEY_SIZE]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wsi.sendPongFrame(payload)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) handlePongFrame(reader *bufio.Reader, payloadLen int, masked bool) ([]byte, error) {
|
||||||
|
if payloadLen == WS_PAYLOAD_LEN_16BIT {
|
||||||
|
lenBytes := make([]byte, 2)
|
||||||
|
if _, err := io.ReadFull(reader, lenBytes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
payloadLen = int(binary.BigEndian.Uint16(lenBytes))
|
||||||
|
} else if payloadLen == WS_PAYLOAD_LEN_64BIT {
|
||||||
|
lenBytes := make([]byte, 8)
|
||||||
|
if _, err := io.ReadFull(reader, lenBytes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
val := binary.BigEndian.Uint64(lenBytes)
|
||||||
|
if val > uint64(math.MaxInt) {
|
||||||
|
return nil, fmt.Errorf("payload length exceeds maximum integer value")
|
||||||
|
}
|
||||||
|
payloadLen = int(val) // #nosec G115
|
||||||
|
}
|
||||||
|
|
||||||
|
maskKey := make([]byte, WS_MASK_KEY_SIZE)
|
||||||
|
if masked {
|
||||||
|
if _, err := io.ReadFull(reader, maskKey); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if payloadLen > 0 {
|
||||||
|
payload := make([]byte, payloadLen)
|
||||||
|
if _, err := io.ReadFull(reader, payload); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) sendPongFrame(data []byte) {
|
||||||
|
wsi.Mutex.RLock()
|
||||||
|
conn := wsi.conn
|
||||||
|
wsi.Mutex.RUnlock()
|
||||||
|
|
||||||
|
if conn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
frame := wsi.createFrame(data, WS_OPCODE_PONG, true)
|
||||||
|
wsi.Mutex.Lock()
|
||||||
|
_, _ = conn.Write(frame)
|
||||||
|
wsi.Mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) createFrame(data []byte, opcode byte, fin bool) []byte {
|
||||||
|
payloadLen := len(data)
|
||||||
|
frame := make([]byte, WS_HEADER_SIZE)
|
||||||
|
|
||||||
|
if fin {
|
||||||
|
frame[0] |= WS_FRAME_HEADER_FIN
|
||||||
|
}
|
||||||
|
frame[0] |= opcode
|
||||||
|
|
||||||
|
if payloadLen < WS_PAYLOAD_LEN_16BIT {
|
||||||
|
frame[1] = byte(payloadLen)
|
||||||
|
frame = append(frame, data...)
|
||||||
|
} else if payloadLen < WS_MAX_PAYLOAD_16BIT {
|
||||||
|
frame[1] = WS_PAYLOAD_LEN_16BIT // #nosec G602
|
||||||
|
lenBytes := make([]byte, 2)
|
||||||
|
binary.BigEndian.PutUint16(lenBytes, uint16(payloadLen)) // #nosec G115
|
||||||
|
frame = append(frame, lenBytes...)
|
||||||
|
frame = append(frame, data...)
|
||||||
|
} else {
|
||||||
|
frame[1] = WS_PAYLOAD_LEN_64BIT // #nosec G602
|
||||||
|
lenBytes := make([]byte, 8)
|
||||||
|
binary.BigEndian.PutUint64(lenBytes, uint64(payloadLen)) // #nosec G115
|
||||||
|
frame = append(frame, lenBytes...)
|
||||||
|
frame = append(frame, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return frame
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) ProcessOutgoing(data []byte) error {
|
||||||
|
return wsi.Send(data, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) GetConn() net.Conn {
|
||||||
|
wsi.Mutex.RLock()
|
||||||
|
defer wsi.Mutex.RUnlock()
|
||||||
|
return wsi.conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) GetMTU() int {
|
||||||
|
return wsi.MTU
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) IsEnabled() bool {
|
||||||
|
wsi.Mutex.RLock()
|
||||||
|
defer wsi.Mutex.RUnlock()
|
||||||
|
return wsi.Enabled && wsi.Online && !wsi.Detached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) SendPathRequest(packet []byte) error {
|
||||||
|
return wsi.Send(packet, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.Time) error {
|
||||||
|
frame := make([]byte, 0, len(dest)+len(data)+9)
|
||||||
|
frame = append(frame, WS_OPCODE_BINARY)
|
||||||
|
frame = append(frame, dest...)
|
||||||
|
ts := make([]byte, 8)
|
||||||
|
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) // #nosec G115
|
||||||
|
frame = append(frame, ts...)
|
||||||
|
frame = append(frame, data...)
|
||||||
|
return wsi.Send(frame, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) GetBandwidthAvailable() bool {
|
||||||
|
return wsi.BaseInterface.GetBandwidthAvailable()
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateWebSocketKey() (string, error) {
|
||||||
|
key := make([]byte, WS_KEY_SIZE)
|
||||||
|
if _, err := rand.Read(key); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(key), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeAcceptKey(key string) string {
|
||||||
|
// bearer:disable go_gosec_crypto_weak_crypto
|
||||||
|
h := sha1.New() // #nosec G401
|
||||||
|
h.Write([]byte(key))
|
||||||
|
h.Write([]byte(wsGUID))
|
||||||
|
// bearer:disable go_lang_weak_hash_sha1
|
||||||
|
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
||||||
280
pkg/interfaces/websocket_native_test.go
Normal file
280
pkg/interfaces/websocket_native_test.go
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWebSocketGUID(t *testing.T) {
|
||||||
|
if wsGUID != "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" {
|
||||||
|
t.Errorf("wsGUID mismatch: expected RFC 6455 GUID, got %s", wsGUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateWebSocketKey(t *testing.T) {
|
||||||
|
key1, err := generateWebSocketKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key2, err := generateWebSocketKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if key1 == key2 {
|
||||||
|
t.Error("Generated keys should be unique")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(key1) != 24 {
|
||||||
|
t.Errorf("Expected base64-encoded key length 24, got %d", len(key1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeAcceptKey(t *testing.T) {
|
||||||
|
testKey := "dGhlIHNhbXBsZSBub25jZQ=="
|
||||||
|
expectedAccept := "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
|
||||||
|
|
||||||
|
accept := computeAcceptKey(testKey)
|
||||||
|
if accept != expectedAccept {
|
||||||
|
t.Errorf("Accept key mismatch: expected %s, got %s", expectedAccept, accept)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewWebSocketInterface(t *testing.T) {
|
||||||
|
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ws.GetName() != "test" {
|
||||||
|
t.Errorf("Expected name 'test', got %s", ws.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
if ws.GetType() != common.IF_TYPE_UDP {
|
||||||
|
t.Errorf("Expected type IF_TYPE_UDP, got %v", ws.GetType())
|
||||||
|
}
|
||||||
|
|
||||||
|
if ws.GetMTU() != 1064 {
|
||||||
|
t.Errorf("Expected MTU 1064, got %d", ws.GetMTU())
|
||||||
|
}
|
||||||
|
|
||||||
|
if ws.IsOnline() {
|
||||||
|
t.Error("Interface should not be online before Start()")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebSocketConnection(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping network test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
|
||||||
|
t.Logf("Received packet: %d bytes", len(data))
|
||||||
|
})
|
||||||
|
|
||||||
|
err = ws.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start WebSocket: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
if !ws.IsOnline() {
|
||||||
|
t.Error("WebSocket should be online after Start()")
|
||||||
|
}
|
||||||
|
|
||||||
|
testData := []byte{0x01, 0x02, 0x03, 0x04}
|
||||||
|
err = ws.Send(testData, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to send data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
if err := ws.Stop(); err != nil {
|
||||||
|
t.Errorf("Failed to stop WebSocket: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
if ws.IsOnline() {
|
||||||
|
t.Error("WebSocket should be offline after Stop()")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebSocketReconnection(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping network test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ws.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start WebSocket: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
if !ws.IsOnline() {
|
||||||
|
t.Error("WebSocket should be online")
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := ws.GetConn()
|
||||||
|
if conn == nil {
|
||||||
|
t.Error("GetConn() should return a connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.Close()
|
||||||
|
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
if ws.IsOnline() {
|
||||||
|
t.Log("WebSocket reconnected successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ws.Stop(); err != nil {
|
||||||
|
t.Errorf("Failed to stop WebSocket: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebSocketMessageQueue(t *testing.T) {
|
||||||
|
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.Enable()
|
||||||
|
|
||||||
|
testData := []byte{0x01, 0x02, 0x03}
|
||||||
|
err = ws.Send(testData, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Send should queue message when offline, got error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if testing.Short() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ws.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start WebSocket: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for interface to be online (up to 10 seconds)
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
if ws.IsOnline() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ws.IsOnline() {
|
||||||
|
t.Error("WebSocket should be online")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
if err := ws.Stop(); err != nil {
|
||||||
|
t.Errorf("Failed to stop WebSocket: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebSocketFrameEncoding(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping frame encoding test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ws.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start WebSocket: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
data []byte
|
||||||
|
}{
|
||||||
|
{"small frame", []byte{0x01, 0x02, 0x03}},
|
||||||
|
{"medium frame", make([]byte, 200)},
|
||||||
|
{"large frame", make([]byte, 1000)},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := ws.Send(tc.data, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to send %s: %v", tc.name, err)
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ws.Stop(); err != nil {
|
||||||
|
t.Errorf("Failed to stop WebSocket: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebSocketEnableDisable(t *testing.T) {
|
||||||
|
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ws.IsEnabled() {
|
||||||
|
t.Error("Interface should not be enabled initially")
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.Enable()
|
||||||
|
if !ws.IsEnabled() {
|
||||||
|
t.Error("Interface should be enabled after Enable()")
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.Disable()
|
||||||
|
if ws.IsEnabled() {
|
||||||
|
t.Error("Interface should not be enabled after Disable()")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebSocketDetach(t *testing.T) {
|
||||||
|
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ws.IsDetached() {
|
||||||
|
t.Error("Interface should not be detached initially")
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.Detach()
|
||||||
|
if !ws.IsDetached() {
|
||||||
|
t.Error("Interface should be detached after Detach()")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ws.IsOnline() {
|
||||||
|
t.Error("Interface should be offline after Detach()")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
//go:build js && wasm
|
//go:build js && wasm
|
||||||
// +build js,wasm
|
// +build js,wasm
|
||||||
|
|
||||||
package interfaces
|
package interfaces
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -15,12 +15,17 @@ import (
|
|||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
WS_MTU = 1064
|
||||||
|
WS_BITRATE = 10000000
|
||||||
|
WS_RECONNECT_DELAY = 2 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
type WebSocketInterface struct {
|
type WebSocketInterface struct {
|
||||||
BaseInterface
|
BaseInterface
|
||||||
wsURL string
|
wsURL string
|
||||||
ws js.Value
|
ws js.Value
|
||||||
connected bool
|
connected bool
|
||||||
mutex sync.RWMutex
|
|
||||||
messageQueue [][]byte
|
messageQueue [][]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,8 +36,8 @@ func NewWebSocketInterface(name string, wsURL string, enabled bool) (*WebSocketI
|
|||||||
messageQueue: make([][]byte, 0),
|
messageQueue: make([][]byte, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.MTU = 1064
|
ws.MTU = WS_MTU
|
||||||
ws.Bitrate = 10000000
|
ws.Bitrate = WS_BITRATE
|
||||||
|
|
||||||
return ws, nil
|
return ws, nil
|
||||||
}
|
}
|
||||||
@@ -50,62 +55,67 @@ func (wsi *WebSocketInterface) GetMode() common.InterfaceMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (wsi *WebSocketInterface) IsOnline() bool {
|
func (wsi *WebSocketInterface) IsOnline() bool {
|
||||||
wsi.mutex.RLock()
|
wsi.Mutex.RLock()
|
||||||
defer wsi.mutex.RUnlock()
|
defer wsi.Mutex.RUnlock()
|
||||||
return wsi.Online && wsi.connected
|
return wsi.Online && wsi.connected
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wsi *WebSocketInterface) IsDetached() bool {
|
func (wsi *WebSocketInterface) IsDetached() bool {
|
||||||
wsi.mutex.RLock()
|
wsi.Mutex.RLock()
|
||||||
defer wsi.mutex.RUnlock()
|
defer wsi.Mutex.RUnlock()
|
||||||
return wsi.Detached
|
return wsi.Detached
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wsi *WebSocketInterface) Detach() {
|
func (wsi *WebSocketInterface) Detach() {
|
||||||
wsi.mutex.Lock()
|
wsi.Mutex.Lock()
|
||||||
defer wsi.mutex.Unlock()
|
defer wsi.Mutex.Unlock()
|
||||||
wsi.Detached = true
|
wsi.Detached = true
|
||||||
wsi.Online = false
|
wsi.Online = false
|
||||||
wsi.closeWebSocket()
|
wsi.closeWebSocket()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wsi *WebSocketInterface) Enable() {
|
func (wsi *WebSocketInterface) Enable() {
|
||||||
wsi.mutex.Lock()
|
wsi.Mutex.Lock()
|
||||||
defer wsi.mutex.Unlock()
|
defer wsi.Mutex.Unlock()
|
||||||
wsi.Enabled = true
|
wsi.Enabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wsi *WebSocketInterface) Disable() {
|
func (wsi *WebSocketInterface) Disable() {
|
||||||
wsi.mutex.Lock()
|
wsi.Mutex.Lock()
|
||||||
defer wsi.mutex.Unlock()
|
defer wsi.Mutex.Unlock()
|
||||||
wsi.Enabled = false
|
wsi.Enabled = false
|
||||||
wsi.closeWebSocket()
|
wsi.closeWebSocket()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wsi *WebSocketInterface) Start() error {
|
func (wsi *WebSocketInterface) Start() error {
|
||||||
wsi.mutex.Lock()
|
wsi.Mutex.Lock()
|
||||||
defer wsi.mutex.Unlock()
|
defer wsi.Mutex.Unlock()
|
||||||
|
|
||||||
if wsi.ws.Truthy() {
|
if wsi.ws.Truthy() {
|
||||||
return fmt.Errorf("WebSocket already started")
|
readyState := wsi.ws.Get("readyState").Int()
|
||||||
|
if readyState == 1 { // OPEN
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// If connecting, closing or closed, clean up first
|
||||||
|
wsi.closeWebSocket()
|
||||||
}
|
}
|
||||||
|
|
||||||
ws := js.Global().Get("WebSocket").New(wsi.wsURL)
|
ws := js.Global().Get("WebSocket").New(wsi.wsURL)
|
||||||
ws.Set("binaryType", "arraybuffer")
|
ws.Set("binaryType", "arraybuffer")
|
||||||
|
|
||||||
ws.Set("onopen", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
ws.Set("onopen", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
wsi.mutex.Lock()
|
wsi.Mutex.Lock()
|
||||||
wsi.connected = true
|
wsi.connected = true
|
||||||
wsi.Online = true
|
wsi.Online = true
|
||||||
wsi.mutex.Unlock()
|
wsi.Mutex.Unlock()
|
||||||
|
|
||||||
debug.Log(debug.DEBUG_INFO, "WebSocket connected", "name", wsi.Name, "url", wsi.wsURL)
|
debug.Log(debug.DEBUG_INFO, "WebSocket connected", "name", wsi.Name, "url", wsi.wsURL)
|
||||||
|
|
||||||
wsi.mutex.Lock()
|
wsi.Mutex.Lock()
|
||||||
queue := make([][]byte, len(wsi.messageQueue))
|
queue := make([][]byte, len(wsi.messageQueue))
|
||||||
copy(queue, wsi.messageQueue)
|
copy(queue, wsi.messageQueue)
|
||||||
wsi.messageQueue = wsi.messageQueue[:0]
|
wsi.messageQueue = wsi.messageQueue[:0]
|
||||||
wsi.mutex.Unlock()
|
wsi.Mutex.Unlock()
|
||||||
|
|
||||||
for _, msg := range queue {
|
for _, msg := range queue {
|
||||||
wsi.sendWebSocketMessage(msg)
|
wsi.sendWebSocketMessage(msg)
|
||||||
@@ -122,38 +132,39 @@ func (wsi *WebSocketInterface) Start() error {
|
|||||||
event := args[0]
|
event := args[0]
|
||||||
data := event.Get("data")
|
data := event.Get("data")
|
||||||
|
|
||||||
var packetData []byte
|
handlePacket := func(buf js.Value) {
|
||||||
if data.Type() == js.TypeString {
|
array := js.Global().Get("Uint8Array").New(buf)
|
||||||
packetData = []byte(data.String())
|
|
||||||
} else if data.Type() == js.TypeObject {
|
|
||||||
array := js.Global().Get("Uint8Array").New(data)
|
|
||||||
length := array.Get("length").Int()
|
length := array.Get("length").Int()
|
||||||
packetData = make([]byte, length)
|
if length < 1 {
|
||||||
js.CopyBytesToGo(packetData, array)
|
return
|
||||||
|
}
|
||||||
|
packet := make([]byte, length)
|
||||||
|
js.CopyBytesToGo(packet, array)
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "WASM WebSocket received binary data", "name", wsi.Name, "length", length, "first_byte", fmt.Sprintf("0x%02x", packet[0]))
|
||||||
|
wsi.ProcessIncoming(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Type() == js.TypeString {
|
||||||
|
packet := []byte(data.String())
|
||||||
|
debug.Log(debug.DEBUG_TRACE, "WebSocket received string data", "name", wsi.Name, "length", len(packet))
|
||||||
|
wsi.ProcessIncoming(packet)
|
||||||
|
} else if data.InstanceOf(js.Global().Get("ArrayBuffer")) {
|
||||||
|
handlePacket(data)
|
||||||
|
} else if data.InstanceOf(js.Global().Get("Blob")) {
|
||||||
|
// Handle Blob by converting to ArrayBuffer
|
||||||
|
data.Call("arrayBuffer").Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
|
if len(args) > 0 {
|
||||||
|
handlePacket(args[0])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
} else if data.Type() == js.TypeObject {
|
||||||
|
// Fallback for other object types that might be TypedArrays
|
||||||
|
handlePacket(data)
|
||||||
} else {
|
} else {
|
||||||
debug.Log(debug.DEBUG_ERROR, "Unknown WebSocket message type", "type", data.Type().String())
|
debug.Log(debug.DEBUG_ERROR, "Unknown WebSocket message type", "type", data.Type().String())
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(packetData) < 4 {
|
|
||||||
debug.Log(debug.DEBUG_ERROR, "WebSocket message too short", "bytes", len(packetData))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
packetLen := binary.BigEndian.Uint32(packetData[:4])
|
|
||||||
if len(packetData) < int(packetLen)+4 {
|
|
||||||
debug.Log(debug.DEBUG_ERROR, "WebSocket message incomplete", "expected", packetLen+4, "got", len(packetData))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
packet := packetData[4 : 4+packetLen]
|
|
||||||
|
|
||||||
wsi.mutex.Lock()
|
|
||||||
wsi.RxBytes += uint64(len(packet))
|
|
||||||
wsi.mutex.Unlock()
|
|
||||||
|
|
||||||
wsi.ProcessIncoming(packet)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -163,16 +174,18 @@ func (wsi *WebSocketInterface) Start() error {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
ws.Set("onclose", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
ws.Set("onclose", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
wsi.mutex.Lock()
|
wsi.Mutex.Lock()
|
||||||
wsi.connected = false
|
wsi.connected = false
|
||||||
wsi.Online = false
|
wsi.Online = false
|
||||||
wsi.mutex.Unlock()
|
wsi.Mutex.Unlock()
|
||||||
|
|
||||||
debug.Log(debug.DEBUG_INFO, "WebSocket closed", "name", wsi.Name)
|
debug.Log(debug.DEBUG_INFO, "WebSocket closed", "name", wsi.Name)
|
||||||
|
|
||||||
if wsi.Enabled && !wsi.Detached {
|
if wsi.Enabled && !wsi.Detached {
|
||||||
time.Sleep(2 * time.Second)
|
go func() {
|
||||||
go wsi.Start()
|
time.Sleep(WS_RECONNECT_DELAY)
|
||||||
|
_ = wsi.Start()
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -184,8 +197,8 @@ func (wsi *WebSocketInterface) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (wsi *WebSocketInterface) Stop() error {
|
func (wsi *WebSocketInterface) Stop() error {
|
||||||
wsi.mutex.Lock()
|
wsi.Mutex.Lock()
|
||||||
defer wsi.mutex.Unlock()
|
defer wsi.Mutex.Unlock()
|
||||||
wsi.Enabled = false
|
wsi.Enabled = false
|
||||||
wsi.closeWebSocket()
|
wsi.closeWebSocket()
|
||||||
return nil
|
return nil
|
||||||
@@ -200,19 +213,11 @@ func (wsi *WebSocketInterface) closeWebSocket() {
|
|||||||
wsi.Online = false
|
wsi.Online = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wsi *WebSocketInterface) Send(data []byte, addr string) error {
|
func (wsi *WebSocketInterface) ProcessOutgoing(data []byte) error {
|
||||||
if !wsi.IsEnabled() {
|
|
||||||
return fmt.Errorf("interface not enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
wsi.mutex.Lock()
|
|
||||||
wsi.TxBytes += uint64(len(data))
|
|
||||||
wsi.mutex.Unlock()
|
|
||||||
|
|
||||||
if !wsi.connected {
|
if !wsi.connected {
|
||||||
wsi.mutex.Lock()
|
wsi.Mutex.Lock()
|
||||||
wsi.messageQueue = append(wsi.messageQueue, data)
|
wsi.messageQueue = append(wsi.messageQueue, data)
|
||||||
wsi.mutex.Unlock()
|
wsi.Mutex.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,13 +233,8 @@ func (wsi *WebSocketInterface) sendWebSocketMessage(data []byte) error {
|
|||||||
return fmt.Errorf("WebSocket not open")
|
return fmt.Errorf("WebSocket not open")
|
||||||
}
|
}
|
||||||
|
|
||||||
packetLen := uint32(len(data))
|
array := js.Global().Get("Uint8Array").New(len(data))
|
||||||
packet := make([]byte, 4+len(data))
|
js.CopyBytesToJS(array, data)
|
||||||
binary.BigEndian.PutUint32(packet[:4], packetLen)
|
|
||||||
copy(packet[4:], data)
|
|
||||||
|
|
||||||
array := js.Global().Get("Uint8Array").New(len(packet))
|
|
||||||
js.CopyBytesToJS(array, packet)
|
|
||||||
|
|
||||||
wsi.ws.Call("send", array)
|
wsi.ws.Call("send", array)
|
||||||
|
|
||||||
@@ -242,10 +242,6 @@ func (wsi *WebSocketInterface) sendWebSocketMessage(data []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wsi *WebSocketInterface) ProcessOutgoing(data []byte) error {
|
|
||||||
return wsi.Send(data, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wsi *WebSocketInterface) GetConn() net.Conn {
|
func (wsi *WebSocketInterface) GetConn() net.Conn {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -255,7 +251,7 @@ func (wsi *WebSocketInterface) GetMTU() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (wsi *WebSocketInterface) IsEnabled() bool {
|
func (wsi *WebSocketInterface) IsEnabled() bool {
|
||||||
wsi.mutex.RLock()
|
wsi.Mutex.RLock()
|
||||||
defer wsi.mutex.RUnlock()
|
defer wsi.Mutex.RUnlock()
|
||||||
return wsi.Enabled && wsi.Online && !wsi.Detached
|
return wsi.Enabled && wsi.Online && !wsi.Detached
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package link
|
package link
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package packet
|
package packet
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package packet
|
package packet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -142,8 +144,6 @@ func (p *Packet) Pack() error {
|
|||||||
header := []byte{flags, p.Hops}
|
header := []byte{flags, p.Hops}
|
||||||
debug.Log(debug.DEBUG_TRACE, "Created packet header", "flags", fmt.Sprintf("%08b", flags), "hops", p.Hops)
|
debug.Log(debug.DEBUG_TRACE, "Created packet header", "flags", fmt.Sprintf("%08b", flags), "hops", p.Hops)
|
||||||
|
|
||||||
header = append(header, p.DestinationHash...)
|
|
||||||
|
|
||||||
if p.HeaderType == HeaderType2 {
|
if p.HeaderType == HeaderType2 {
|
||||||
if p.TransportID == nil {
|
if p.TransportID == nil {
|
||||||
return errors.New("transport ID required for header type 2")
|
return errors.New("transport ID required for header type 2")
|
||||||
@@ -152,6 +152,8 @@ func (p *Packet) Pack() error {
|
|||||||
debug.Log(debug.DEBUG_ALL, "Added transport ID to header", "transport_id", fmt.Sprintf("%x", p.TransportID))
|
debug.Log(debug.DEBUG_ALL, "Added transport ID to header", "transport_id", fmt.Sprintf("%x", p.TransportID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header = append(header, p.DestinationHash...)
|
||||||
|
|
||||||
header = append(header, p.Context)
|
header = append(header, p.Context)
|
||||||
debug.Log(debug.DEBUG_PACKETS, "Final header length", "bytes", len(header))
|
debug.Log(debug.DEBUG_PACKETS, "Final header length", "bytes", len(header))
|
||||||
|
|
||||||
@@ -185,12 +187,12 @@ func (p *Packet) Unpack() error {
|
|||||||
dstLen := 16 // Truncated hash length
|
dstLen := 16 // Truncated hash length
|
||||||
|
|
||||||
if p.HeaderType == HeaderType2 {
|
if p.HeaderType == HeaderType2 {
|
||||||
// Header Type 2: Header(2) + DestHash(16) + TransportID(16) + Context(1) + Data
|
// Header Type 2: Header(2) + TransportID(16) + DestHash(16) + Context(1) + Data
|
||||||
if len(p.Raw) < 2*dstLen+3 {
|
if len(p.Raw) < 2*dstLen+3 {
|
||||||
return errors.New("packet too short for header type 2")
|
return errors.New("packet too short for header type 2")
|
||||||
}
|
}
|
||||||
p.DestinationHash = p.Raw[2 : dstLen+2] // Destination hash first
|
p.TransportID = p.Raw[2 : dstLen+2] // Transport ID first
|
||||||
p.TransportID = p.Raw[dstLen+2 : 2*dstLen+2] // Transport ID second
|
p.DestinationHash = p.Raw[dstLen+2 : 2*dstLen+2] // Destination hash second
|
||||||
p.Context = p.Raw[2*dstLen+2]
|
p.Context = p.Raw[2*dstLen+2]
|
||||||
p.Data = p.Raw[2*dstLen+3:]
|
p.Data = p.Raw[2*dstLen+3:]
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
40
pkg/packet/packet_fuzz_test.go
Normal file
40
pkg/packet/packet_fuzz_test.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
package packet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FuzzPacketUnpack(f *testing.F) {
|
||||||
|
// Add some valid packets as seeds
|
||||||
|
p1 := &Packet{
|
||||||
|
HeaderType: HeaderType1,
|
||||||
|
PacketType: PacketTypeData,
|
||||||
|
DestinationType: 0x01,
|
||||||
|
DestinationHash: make([]byte, 16),
|
||||||
|
Context: ContextNone,
|
||||||
|
Data: []byte("hello"),
|
||||||
|
}
|
||||||
|
if err := p1.Pack(); err == nil {
|
||||||
|
f.Add(p1.Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
p2 := &Packet{
|
||||||
|
HeaderType: HeaderType2,
|
||||||
|
PacketType: PacketTypeAnnounce,
|
||||||
|
TransportID: make([]byte, 16),
|
||||||
|
DestinationHash: make([]byte, 16),
|
||||||
|
Context: ContextNone,
|
||||||
|
Data: []byte("announce"),
|
||||||
|
}
|
||||||
|
if err := p2.Pack(); err == nil {
|
||||||
|
f.Add(p2.Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, data []byte) {
|
||||||
|
p := &Packet{Raw: data}
|
||||||
|
// We don't care about the error, just that it doesn't panic
|
||||||
|
_ = p.Unpack()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package packet
|
package packet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package pathfinder
|
package pathfinder
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package rate
|
package rate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package resolver
|
package resolver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package resource
|
package resource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package resource
|
package resource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package transport
|
package transport
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
39
pkg/transport/leak_test.go
Normal file
39
pkg/transport/leak_test.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
package transport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTransportLeak(t *testing.T) {
|
||||||
|
// Baseline goroutine count
|
||||||
|
runtime.GC()
|
||||||
|
baseline := runtime.NumGoroutine()
|
||||||
|
|
||||||
|
cfg := &common.ReticulumConfig{}
|
||||||
|
|
||||||
|
// Create and close many transport instances
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
tr := NewTransport(cfg)
|
||||||
|
// Give it a tiny bit of time to start the goroutine
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
tr.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for goroutines to finish
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
runtime.GC()
|
||||||
|
|
||||||
|
final := runtime.NumGoroutine()
|
||||||
|
|
||||||
|
// We allow a small margin for other system goroutines,
|
||||||
|
// but 100 leaks would be very obvious.
|
||||||
|
if final > baseline+5 {
|
||||||
|
t.Errorf("Potential goroutine leak: baseline %d, final %d", baseline, final)
|
||||||
|
}
|
||||||
|
}
|
||||||
66
pkg/transport/network_test.go
Normal file
66
pkg/transport/network_test.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
package transport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockInterface struct {
|
||||||
|
common.BaseInterface
|
||||||
|
sentData [][]byte
|
||||||
|
dropRate float64 // 0.0 to 1.0
|
||||||
|
onReceive func([]byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockInterface) Send(data []byte, destination string) error {
|
||||||
|
m.Mutex.Lock()
|
||||||
|
defer m.Mutex.Unlock()
|
||||||
|
|
||||||
|
// Simulate packet loss
|
||||||
|
if m.dropRate > 0 {
|
||||||
|
// In a real test we'd use rand.Float64()
|
||||||
|
// For deterministic testing, let's just record everything for now
|
||||||
|
}
|
||||||
|
|
||||||
|
m.sentData = append(m.sentData, data)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockInterface) Receive(data []byte) {
|
||||||
|
if m.onReceive != nil {
|
||||||
|
m.onReceive(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransportNetworkSimulation(t *testing.T) {
|
||||||
|
cfg := &common.ReticulumConfig{}
|
||||||
|
tr := NewTransport(cfg)
|
||||||
|
defer tr.Close()
|
||||||
|
|
||||||
|
iface1 := &MockInterface{BaseInterface: common.NewBaseInterface("iface1", common.IF_TYPE_UDP, true)}
|
||||||
|
iface1.Enable()
|
||||||
|
|
||||||
|
iface2 := &MockInterface{BaseInterface: common.NewBaseInterface("iface2", common.IF_TYPE_UDP, true)}
|
||||||
|
iface2.Enable()
|
||||||
|
|
||||||
|
tr.RegisterInterface(iface1.GetName(), iface1)
|
||||||
|
tr.RegisterInterface(iface2.GetName(), iface2)
|
||||||
|
|
||||||
|
// Simulate receiving an announce on iface1
|
||||||
|
// [header][hops][dest_hash(16)][payload...]
|
||||||
|
announcePacket := make([]byte, 100)
|
||||||
|
announcePacket[0] = PACKET_TYPE_ANNOUNCE
|
||||||
|
announcePacket[1] = 0 // 0 hops
|
||||||
|
copy(announcePacket[2:18], []byte("destination_hash"))
|
||||||
|
|
||||||
|
// Mock the handler to avoid complex identity logic in this basic test
|
||||||
|
tr.HandlePacket(announcePacket, iface1)
|
||||||
|
|
||||||
|
// In a real scenario, it would be rebroadcast to iface2
|
||||||
|
// But HandlePacket runs in a goroutine, so we'd need to wait or use a better mock
|
||||||
|
fmt.Println("Network simulation test initialized")
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package transport
|
package transport
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -16,7 +18,6 @@ import (
|
|||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/destination"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/destination"
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/interfaces"
|
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/pathfinder"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/pathfinder"
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/rate"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/rate"
|
||||||
@@ -125,6 +126,8 @@ type Transport struct {
|
|||||||
heldAnnounces map[string]*PathAnnounceEntry
|
heldAnnounces map[string]*PathAnnounceEntry
|
||||||
transportIdentity *identity.Identity
|
transportIdentity *identity.Identity
|
||||||
pathRequestDest interface{}
|
pathRequestDest interface{}
|
||||||
|
done chan struct{}
|
||||||
|
stopOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
type DiscoveryPathRequest struct {
|
type DiscoveryPathRequest struct {
|
||||||
@@ -176,6 +179,7 @@ func NewTransport(cfg *common.ReticulumConfig) *Transport {
|
|||||||
discoveryPRTags: make(map[string]bool),
|
discoveryPRTags: make(map[string]bool),
|
||||||
announceTable: make(map[string]*PathAnnounceEntry),
|
announceTable: make(map[string]*PathAnnounceEntry),
|
||||||
heldAnnounces: make(map[string]*PathAnnounceEntry),
|
heldAnnounces: make(map[string]*PathAnnounceEntry),
|
||||||
|
done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Path table persistence
|
// TODO: Path table persistence
|
||||||
@@ -194,11 +198,16 @@ func (t *Transport) startMaintenanceJobs() {
|
|||||||
ticker := time.NewTicker(common.FIVE * time.Second)
|
ticker := time.NewTicker(common.FIVE * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for range ticker.C {
|
for {
|
||||||
t.cleanupExpiredPaths()
|
select {
|
||||||
t.cleanupExpiredDiscoveryRequests()
|
case <-ticker.C:
|
||||||
t.cleanupExpiredAnnounces()
|
t.cleanupExpiredPaths()
|
||||||
t.cleanupExpiredReceipts()
|
t.cleanupExpiredDiscoveryRequests()
|
||||||
|
t.cleanupExpiredAnnounces()
|
||||||
|
t.cleanupExpiredReceipts()
|
||||||
|
case <-t.done:
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,14 +317,18 @@ func (t *Transport) CreateIncomingLink(dest interface{}, networkIface common.Net
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add GetTransportInstance function
|
|
||||||
func GetTransportInstance() *Transport {
|
func GetTransportInstance() *Transport {
|
||||||
transportMutex.Lock()
|
transportMutex.Lock()
|
||||||
defer transportMutex.Unlock()
|
defer transportMutex.Unlock()
|
||||||
return transportInstance
|
return transportInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the interface methods
|
func SetTransportInstance(t *Transport) {
|
||||||
|
transportMutex.Lock()
|
||||||
|
defer transportMutex.Unlock()
|
||||||
|
transportInstance = t
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Transport) RegisterInterface(name string, iface common.NetworkInterface) error {
|
func (t *Transport) RegisterInterface(name string, iface common.NetworkInterface) error {
|
||||||
t.mutex.Lock()
|
t.mutex.Lock()
|
||||||
defer t.mutex.Unlock()
|
defer t.mutex.Unlock()
|
||||||
@@ -340,8 +353,11 @@ func (t *Transport) GetInterface(name string) (common.NetworkInterface, error) {
|
|||||||
return iface, nil
|
return iface, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the Close method
|
|
||||||
func (t *Transport) Close() error {
|
func (t *Transport) Close() error {
|
||||||
|
t.stopOnce.Do(func() {
|
||||||
|
close(t.done)
|
||||||
|
})
|
||||||
|
|
||||||
t.mutex.Lock()
|
t.mutex.Lock()
|
||||||
defer t.mutex.Unlock()
|
defer t.mutex.Unlock()
|
||||||
|
|
||||||
@@ -483,14 +499,14 @@ func (t *Transport) UnregisterAnnounceHandler(handler announce.Handler) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) notifyAnnounceHandlers(destHash []byte, identity interface{}, appData []byte) {
|
func (t *Transport) notifyAnnounceHandlers(destHash []byte, identity interface{}, appData []byte, hops uint8) {
|
||||||
t.mutex.RLock()
|
t.mutex.RLock()
|
||||||
handlers := make([]announce.Handler, len(t.announceHandlers))
|
handlers := make([]announce.Handler, len(t.announceHandlers))
|
||||||
copy(handlers, t.announceHandlers)
|
copy(handlers, t.announceHandlers)
|
||||||
t.mutex.RUnlock()
|
t.mutex.RUnlock()
|
||||||
|
|
||||||
for _, handler := range handlers {
|
for _, handler := range handlers {
|
||||||
if err := handler.ReceivedAnnounce(destHash, identity, appData); err != nil {
|
if err := handler.ReceivedAnnounce(destHash, identity, appData, hops); err != nil {
|
||||||
debug.Log(debug.DEBUG_ERROR, "Error in announce handler", "error", err)
|
debug.Log(debug.DEBUG_ERROR, "Error in announce handler", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -566,8 +582,11 @@ func (t *Transport) RequestPath(destinationHash []byte, onInterface string, tag
|
|||||||
pathRequestData = append(destinationHash, tag...)
|
pathRequestData = append(destinationHash, tag...)
|
||||||
}
|
}
|
||||||
|
|
||||||
destHashFull := sha256.Sum256([]byte("rnstransport.path.request"))
|
pathRequestName := "rnstransport.path.request"
|
||||||
pathRequestDestHash := destHashFull[:common.SIZE_16]
|
nameHashFull := sha256.Sum256([]byte(pathRequestName))
|
||||||
|
nameHash10 := nameHashFull[:10]
|
||||||
|
finalHashFull := sha256.Sum256(nameHash10)
|
||||||
|
pathRequestDestHash := finalHashFull[:16]
|
||||||
|
|
||||||
pkt := packet.NewPacket(
|
pkt := packet.NewPacket(
|
||||||
packet.DestinationPlain,
|
packet.DestinationPlain,
|
||||||
@@ -575,11 +594,12 @@ func (t *Transport) RequestPath(destinationHash []byte, onInterface string, tag
|
|||||||
0x00,
|
0x00,
|
||||||
0x00,
|
0x00,
|
||||||
packet.PropagationBroadcast,
|
packet.PropagationBroadcast,
|
||||||
0x01,
|
0x00, // Header Type 1
|
||||||
pathRequestDestHash,
|
nil,
|
||||||
false,
|
false,
|
||||||
0x00,
|
0x00,
|
||||||
)
|
)
|
||||||
|
pkt.DestinationHash = pathRequestDestHash
|
||||||
|
|
||||||
if err := pkt.Pack(); err != nil {
|
if err := pkt.Pack(); err != nil {
|
||||||
return fmt.Errorf("failed to pack path request: %w", err)
|
return fmt.Errorf("failed to pack path request: %w", err)
|
||||||
@@ -607,7 +627,6 @@ func (t *Transport) RequestPath(destinationHash []byte, onInterface string, tag
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updatePathUnlocked updates path without acquiring mutex (caller must hold lock)
|
|
||||||
func (t *Transport) updatePathUnlocked(destinationHash []byte, nextHop []byte, interfaceName string, hops uint8) {
|
func (t *Transport) updatePathUnlocked(destinationHash []byte, nextHop []byte, interfaceName string, hops uint8) {
|
||||||
// Direct access to interfaces map since caller holds the lock
|
// Direct access to interfaces map since caller holds the lock
|
||||||
iface, exists := t.interfaces[interfaceName]
|
iface, exists := t.interfaces[interfaceName]
|
||||||
@@ -644,7 +663,11 @@ func (t *Transport) HandleAnnounce(data []byte, sourceIface common.NetworkInterf
|
|||||||
appData := data[common.SIZE_16+common.SIZE_32+common.ONE:]
|
appData := data[common.SIZE_16+common.SIZE_32+common.ONE:]
|
||||||
|
|
||||||
// Generate announce hash to check for duplicates
|
// Generate announce hash to check for duplicates
|
||||||
announceHash := sha256.Sum256(data)
|
// We exclude the hop count (byte 1) from the hash since it changes during propagation
|
||||||
|
// We also exclude the header (byte 0) just in case propagation flags change
|
||||||
|
// The destination hash (bytes 2-18) + payload (including random hash) is unique enough
|
||||||
|
hashData := data[common.TWO:]
|
||||||
|
announceHash := sha256.Sum256(hashData)
|
||||||
hashStr := string(announceHash[:])
|
hashStr := string(announceHash[:])
|
||||||
|
|
||||||
t.mutex.Lock()
|
t.mutex.Lock()
|
||||||
@@ -704,7 +727,7 @@ func (t *Transport) HandleAnnounce(data []byte, sourceIface common.NetworkInterf
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Notify handlers
|
// Notify handlers
|
||||||
t.notifyAnnounceHandlers(destHash, identity, appData)
|
t.notifyAnnounceHandlers(destHash, identity, appData, data[0])
|
||||||
|
|
||||||
return lastErr
|
return lastErr
|
||||||
}
|
}
|
||||||
@@ -745,7 +768,6 @@ func (p *LinkPacket) send() error {
|
|||||||
header = append(header, 0x02) // Link packet type
|
header = append(header, 0x02) // Link packet type
|
||||||
header = append(header, p.Destination...)
|
header = append(header, p.Destination...)
|
||||||
|
|
||||||
// Add timestamp
|
|
||||||
ts := make([]byte, 8)
|
ts := make([]byte, 8)
|
||||||
binary.BigEndian.PutUint64(ts, uint64(p.Timestamp.Unix())) // #nosec G115
|
binary.BigEndian.PutUint64(ts, uint64(p.Timestamp.Unix())) // #nosec G115
|
||||||
header = append(header, ts...)
|
header = append(header, ts...)
|
||||||
@@ -868,11 +890,6 @@ func (t *Transport) HandlePacket(data []byte, iface common.NetworkInterface) {
|
|||||||
debug.Log(debug.DEBUG_ERROR, "67-byte packet detected", "header", fmt.Sprintf(common.STR_FMT_HEX, headerByte), "packet_type_bits", fmt.Sprintf(common.STR_FMT_HEX, packetType), "first_32_bytes", fmt.Sprintf("%x", data[:common.SIZE_32]))
|
debug.Log(debug.DEBUG_ERROR, "67-byte packet detected", "header", fmt.Sprintf(common.STR_FMT_HEX, headerByte), "packet_type_bits", fmt.Sprintf(common.STR_FMT_HEX, packetType), "first_32_bytes", fmt.Sprintf("%x", data[:common.SIZE_32]))
|
||||||
}
|
}
|
||||||
|
|
||||||
if tcpIface, ok := iface.(*interfaces.TCPClientInterface); ok {
|
|
||||||
tcpIface.UpdateStats(uint64(len(data)), true)
|
|
||||||
debug.Log(debug.DEBUG_PACKETS, "Updated TCP interface stats", "rx_bytes", len(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
dataCopy := make([]byte, len(data))
|
dataCopy := make([]byte, len(data))
|
||||||
copy(dataCopy, data)
|
copy(dataCopy, data)
|
||||||
|
|
||||||
@@ -1074,7 +1091,11 @@ func (t *Transport) handleAnnouncePacket(data []byte, iface common.NetworkInterf
|
|||||||
identity.Remember(data, destinationHash, pubKey, appData)
|
identity.Remember(data, destinationHash, pubKey, appData)
|
||||||
|
|
||||||
// Generate announce hash to check for duplicates
|
// Generate announce hash to check for duplicates
|
||||||
announceHash := sha256.Sum256(data)
|
// We exclude the hop count (byte 1) from the hash since it changes during propagation
|
||||||
|
// We also exclude the header (byte 0) just in case propagation flags change
|
||||||
|
// The destination hash (bytes 2-18) + payload (including random hash) is unique enough
|
||||||
|
hashData := data[common.TWO:]
|
||||||
|
announceHash := sha256.Sum256(hashData)
|
||||||
hashStr := string(announceHash[:])
|
hashStr := string(announceHash[:])
|
||||||
|
|
||||||
debug.Log(debug.DEBUG_INFO, "Announce hash", "hash", fmt.Sprintf("%x", announceHash[:8]))
|
debug.Log(debug.DEBUG_INFO, "Announce hash", "hash", fmt.Sprintf("%x", announceHash[:8]))
|
||||||
@@ -1093,16 +1114,15 @@ func (t *Transport) handleAnnouncePacket(data []byte, iface common.NetworkInterf
|
|||||||
// Register the path from this announce
|
// Register the path from this announce
|
||||||
// The destination is reachable via the interface that received this announce
|
// The destination is reachable via the interface that received this announce
|
||||||
if iface != nil {
|
if iface != nil {
|
||||||
// Use unlocked version since we may be called in a locked context
|
|
||||||
t.mutex.Lock()
|
t.mutex.Lock()
|
||||||
t.updatePathUnlocked(destinationHash, nil, iface.GetName(), hopCount)
|
t.updatePathUnlocked(destinationHash, nil, iface.GetName(), hopCount+1)
|
||||||
t.mutex.Unlock()
|
t.mutex.Unlock()
|
||||||
debug.Log(debug.DEBUG_INFO, "Registered path", "hash", fmt.Sprintf("%x", destinationHash), "interface", iface.GetName(), "hops", hopCount)
|
debug.Log(debug.DEBUG_INFO, "Registered path", "hash", fmt.Sprintf("%x", destinationHash), "interface", iface.GetName(), "hops", hopCount+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify handlers first, regardless of forwarding limits
|
// Notify handlers first, regardless of forwarding limits
|
||||||
debug.Log(debug.DEBUG_INFO, "Notifying announce handlers", "destHash", fmt.Sprintf("%x", destinationHash), "appDataLen", len(appData))
|
debug.Log(debug.DEBUG_INFO, "Notifying announce handlers", "destHash", fmt.Sprintf("%x", destinationHash), "appDataLen", len(appData))
|
||||||
t.notifyAnnounceHandlers(destinationHash, id, appData)
|
t.notifyAnnounceHandlers(destinationHash, id, appData, hopCount+1)
|
||||||
debug.Log(debug.DEBUG_INFO, "Announce handlers notified")
|
debug.Log(debug.DEBUG_INFO, "Announce handlers notified")
|
||||||
|
|
||||||
// Don't forward if max hops reached
|
// Don't forward if max hops reached
|
||||||
@@ -1359,7 +1379,7 @@ func (t *Transport) InitializePathRequestHandler() error {
|
|||||||
return errors.New("transport identity not initialized")
|
return errors.New("transport identity not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
pathRequestDest, err := destination.New(t.transportIdentity, destination.IN, destination.PLAIN, "rnstransport", t, "path", "request")
|
pathRequestDest, err := destination.New(nil, destination.IN, destination.PLAIN, "rnstransport", t, "path", "request")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create path request destination: %w", err)
|
return fmt.Errorf("failed to create path request destination: %w", err)
|
||||||
}
|
}
|
||||||
@@ -1675,6 +1695,14 @@ func (l *Link) HandleResource(resource interface{}) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetIdentity sets the identity for the Transport.
|
||||||
|
func (t *Transport) SetIdentity(id *identity.Identity) {
|
||||||
|
t.mutex.Lock()
|
||||||
|
defer t.mutex.Unlock()
|
||||||
|
t.transportIdentity = id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start initializes the Transport.
|
||||||
func (t *Transport) Start() error {
|
func (t *Transport) Start() error {
|
||||||
t.mutex.Lock()
|
t.mutex.Lock()
|
||||||
defer t.mutex.Unlock()
|
defer t.mutex.Unlock()
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ package transport
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/destination"
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockInterface struct {
|
type mockInterface struct {
|
||||||
@@ -118,3 +122,68 @@ func TestTransportStatus(t *testing.T) {
|
|||||||
t.Error("Path should be responsive again")
|
t.Error("Path should be responsive again")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAnnounceHopCount(t *testing.T) {
|
||||||
|
config := common.DefaultConfig()
|
||||||
|
tr := NewTransport(config)
|
||||||
|
defer tr.Close()
|
||||||
|
|
||||||
|
iface := &mockInterface{}
|
||||||
|
iface.Name = "wasm0"
|
||||||
|
iface.Enabled = true
|
||||||
|
_ = tr.RegisterInterface("wasm0", iface)
|
||||||
|
|
||||||
|
// Create an identity for the announce
|
||||||
|
id, _ := identity.New()
|
||||||
|
|
||||||
|
// Create a destination to get a valid hash for this identity
|
||||||
|
// NewAnnouncePacket uses "reticulum-go.node" by default
|
||||||
|
dest, _ := destination.New(id, destination.IN, destination.SINGLE, "reticulum-go.node", tr)
|
||||||
|
destHash := dest.GetHash()
|
||||||
|
|
||||||
|
// Create a raw announce packet manually to control hop count
|
||||||
|
// Header(2) + DestHash(16) + Context(1) + Payload...
|
||||||
|
// Header: 0x21 (Announce, Header Type 1, Broadcast, Destination Type Single)
|
||||||
|
// Hop count: 0
|
||||||
|
raw := make([]byte, 2+16+1+148) // header + dest + context + min_announce_payload
|
||||||
|
raw[0] = 0x21
|
||||||
|
raw[1] = 0 // Initial hop count
|
||||||
|
copy(raw[2:18], destHash)
|
||||||
|
raw[18] = 0 // context
|
||||||
|
|
||||||
|
// Announce payload: pubKey(64) + nameHash(10) + randomHash(10) + signature(64)
|
||||||
|
payload := raw[19:]
|
||||||
|
copy(payload[0:64], id.GetPublicKey())
|
||||||
|
// Name hash, random hash, signature - filling with dummy data but valid length
|
||||||
|
// Normally we would sign it properly, but handleAnnouncePacket validates it.
|
||||||
|
// Actually, handleAnnouncePacket WILL fail if signature is invalid.
|
||||||
|
// Use NewAnnouncePacket to get a valid signed packet
|
||||||
|
transportID := make([]byte, 16)
|
||||||
|
annPkt, err := packet.NewAnnouncePacket(destHash, id, []byte("test"), transportID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewAnnouncePacket failed: %v", err)
|
||||||
|
}
|
||||||
|
annRaw, err := annPkt.Serialize()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Serialize failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override hop count to 0 to simulate neighbor
|
||||||
|
annRaw[1] = 0
|
||||||
|
|
||||||
|
// Handle the packet
|
||||||
|
tr.HandlePacket(annRaw, iface)
|
||||||
|
|
||||||
|
// Wait a bit for the async processing
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// Check stored hops
|
||||||
|
if !tr.HasPath(destHash) {
|
||||||
|
t.Fatal("Path not registered from announce")
|
||||||
|
}
|
||||||
|
|
||||||
|
hops := tr.HopsTo(destHash)
|
||||||
|
if hops != 1 {
|
||||||
|
t.Errorf("Expected 1 hop for neighbor (received 0), got %d", hops)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
303
pkg/wasm/wasm.go
303
pkg/wasm/wasm.go
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
//go:build js && wasm
|
//go:build js && wasm
|
||||||
// +build js,wasm
|
// +build js,wasm
|
||||||
|
|
||||||
@@ -21,37 +23,111 @@ var (
|
|||||||
reticulumTransport *transport.Transport
|
reticulumTransport *transport.Transport
|
||||||
reticulumDest *destination.Destination
|
reticulumDest *destination.Destination
|
||||||
reticulumIdentity *identity.Identity
|
reticulumIdentity *identity.Identity
|
||||||
userName string
|
|
||||||
peerMap = make(map[string]string)
|
|
||||||
stats = struct {
|
stats = struct {
|
||||||
packetsSent int
|
packetsSent int
|
||||||
packetsReceived int
|
packetsReceived int
|
||||||
bytesSent int
|
bytesSent int
|
||||||
bytesReceived int
|
bytesReceived int
|
||||||
|
announcesSent int
|
||||||
|
announcesReceived int
|
||||||
}{}
|
}{}
|
||||||
|
packetCallback js.Value
|
||||||
|
announceHandler js.Value
|
||||||
)
|
)
|
||||||
|
|
||||||
// RegisterJSFunctions registers the Reticulum WASM API to the JavaScript global scope.
|
// RegisterJSFunctions registers the Reticulum WASM API to the JavaScript global scope.
|
||||||
func RegisterJSFunctions() {
|
func RegisterJSFunctions() {
|
||||||
js.Global().Set("reticulum", js.ValueOf(map[string]interface{}{
|
js.Global().Set("reticulum", js.ValueOf(map[string]interface{}{
|
||||||
"init": js.FuncOf(InitReticulum),
|
"init": js.FuncOf(InitReticulum),
|
||||||
"getIdentity": js.FuncOf(GetIdentity),
|
"getIdentity": js.FuncOf(GetIdentity),
|
||||||
"getDestination": js.FuncOf(GetDestination),
|
"getDestination": js.FuncOf(GetDestination),
|
||||||
"announce": js.FuncOf(SendAnnounce),
|
"connect": js.FuncOf(ConnectWebSocket),
|
||||||
"connect": js.FuncOf(ConnectWebSocket),
|
"disconnect": js.FuncOf(DisconnectWebSocket),
|
||||||
"disconnect": js.FuncOf(DisconnectWebSocket),
|
"isConnected": js.FuncOf(IsConnected),
|
||||||
"isConnected": js.FuncOf(IsConnected),
|
"requestPath": js.FuncOf(RequestPath),
|
||||||
"sendMessage": js.FuncOf(SendMessage),
|
"getStats": js.FuncOf(GetStats),
|
||||||
"getStats": js.FuncOf(GetStats),
|
"setPacketCallback": js.FuncOf(SetPacketCallback),
|
||||||
|
"setAnnounceCallback": js.FuncOf(SetAnnounceCallback),
|
||||||
|
"sendData": js.FuncOf(SendDataJS),
|
||||||
|
"sendMessage": js.FuncOf(SendDataJS),
|
||||||
|
"announce": js.FuncOf(SendAnnounceJS),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetStats(this js.Value, args []js.Value) interface{} {
|
func SetPacketCallback(this js.Value, args []js.Value) interface{} {
|
||||||
|
if len(args) > 0 && args[0].Type() == js.TypeFunction {
|
||||||
|
packetCallback = args[0]
|
||||||
|
return js.ValueOf(true)
|
||||||
|
}
|
||||||
|
return js.ValueOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetAnnounceCallback(this js.Value, args []js.Value) interface{} {
|
||||||
|
if len(args) > 0 && args[0].Type() == js.TypeFunction {
|
||||||
|
announceHandler = args[0]
|
||||||
|
return js.ValueOf(true)
|
||||||
|
}
|
||||||
|
return js.ValueOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RequestPath(this js.Value, args []js.Value) interface{} {
|
||||||
|
if len(args) < 1 {
|
||||||
|
return js.ValueOf(map[string]interface{}{
|
||||||
|
"error": "Destination hash required",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
destHashHex := args[0].String()
|
||||||
|
destHash, err := hex.DecodeString(destHashHex)
|
||||||
|
if err != nil {
|
||||||
|
return js.ValueOf(map[string]interface{}{
|
||||||
|
"error": fmt.Sprintf("Invalid destination hash: %v", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if reticulumTransport == nil {
|
||||||
|
return js.ValueOf(map[string]interface{}{
|
||||||
|
"error": "Reticulum not initialized",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := reticulumTransport.RequestPath(destHash, "", nil, true); err != nil {
|
||||||
|
return js.ValueOf(map[string]interface{}{
|
||||||
|
"error": fmt.Sprintf("Failed to request path: %v", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return js.ValueOf(map[string]interface{}{
|
return js.ValueOf(map[string]interface{}{
|
||||||
"packetsSent": stats.packetsSent,
|
"success": true,
|
||||||
"packetsReceived": stats.packetsReceived,
|
})
|
||||||
"bytesSent": stats.bytesSent,
|
}
|
||||||
"bytesReceived": stats.bytesReceived,
|
|
||||||
|
func GetStats(this js.Value, args []js.Value) interface{} {
|
||||||
|
if reticulumTransport != nil {
|
||||||
|
ifaces := reticulumTransport.GetInterfaces()
|
||||||
|
totalTxBytes := 0
|
||||||
|
totalRxBytes := 0
|
||||||
|
totalTxPackets := 0
|
||||||
|
totalRxPackets := 0
|
||||||
|
for _, iface := range ifaces {
|
||||||
|
totalTxBytes += int(iface.GetTxBytes())
|
||||||
|
totalRxBytes += int(iface.GetRxBytes())
|
||||||
|
totalTxPackets += int(iface.GetTxPackets())
|
||||||
|
totalRxPackets += int(iface.GetRxPackets())
|
||||||
|
}
|
||||||
|
stats.bytesSent = totalTxBytes
|
||||||
|
stats.bytesReceived = totalRxBytes
|
||||||
|
stats.packetsSent = totalTxPackets
|
||||||
|
stats.packetsReceived = totalRxPackets
|
||||||
|
}
|
||||||
|
|
||||||
|
return js.ValueOf(map[string]interface{}{
|
||||||
|
"packetsSent": stats.packetsSent,
|
||||||
|
"packetsReceived": stats.packetsReceived,
|
||||||
|
"bytesSent": stats.bytesSent,
|
||||||
|
"bytesReceived": stats.bytesReceived,
|
||||||
|
"announcesSent": stats.announcesSent,
|
||||||
|
"announcesReceived": stats.announcesReceived,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,8 +144,9 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
wsURL := args[0].String()
|
wsURL := args[0].String()
|
||||||
if len(args) >= 2 {
|
appName := "wasm_core"
|
||||||
userName = args[1].String()
|
if len(args) >= 2 && args[1].Type() == js.TypeString {
|
||||||
|
appName = args[1].String()
|
||||||
}
|
}
|
||||||
|
|
||||||
var id *identity.Identity
|
var id *identity.Identity
|
||||||
@@ -100,12 +177,20 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
|
|||||||
|
|
||||||
cfg := common.DefaultConfig()
|
cfg := common.DefaultConfig()
|
||||||
t := transport.NewTransport(cfg)
|
t := transport.NewTransport(cfg)
|
||||||
|
// Ensure the global instance is set for internal RNS calls (like Announce)
|
||||||
|
transport.SetTransportInstance(t)
|
||||||
|
|
||||||
|
// Set transport identity to the same as the node identity for now in WASM
|
||||||
|
t.SetIdentity(id)
|
||||||
|
if err := t.InitializePathRequestHandler(); err != nil {
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Failed to initialize path request handler", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
dest, err := destination.New(
|
dest, err := destination.New(
|
||||||
id,
|
id,
|
||||||
destination.IN,
|
destination.IN,
|
||||||
destination.SINGLE,
|
destination.SINGLE,
|
||||||
"wasm_core",
|
appName,
|
||||||
t,
|
t,
|
||||||
"browser",
|
"browser",
|
||||||
)
|
)
|
||||||
@@ -116,18 +201,17 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dest.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
|
dest.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
|
||||||
stats.packetsReceived++
|
if !packetCallback.IsUndefined() {
|
||||||
stats.bytesReceived += len(data)
|
// Convert bytes to JS Uint8Array for performance and compatibility
|
||||||
|
uint8Array := js.Global().Get("Uint8Array").New(len(data))
|
||||||
js.Global().Call("onChatMessage", js.ValueOf(map[string]interface{}{
|
js.CopyBytesToJS(uint8Array, data)
|
||||||
"text": string(data),
|
packetCallback.Invoke(uint8Array)
|
||||||
"from": "",
|
}
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
dest.SetProofStrategy(destination.PROVE_ALL)
|
dest.SetProofStrategy(destination.PROVE_ALL)
|
||||||
|
|
||||||
t.RegisterAnnounceHandler(&announceHandler{})
|
t.RegisterAnnounceHandler(&genericAnnounceHandler{})
|
||||||
|
|
||||||
wsInterface, err := interfaces.NewWebSocketInterface("wasm0", wsURL, true)
|
wsInterface, err := interfaces.NewWebSocketInterface("wasm0", wsURL, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -136,12 +220,8 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
wsInterface.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
|
// Wire the interface to the transport
|
||||||
msg := fmt.Sprintf("Received packet: %d bytes (type: 0x%02x)", len(data), data[0])
|
wsInterface.SetPacketCallback(t.HandlePacket)
|
||||||
js.Global().Call("log", msg, "success")
|
|
||||||
debug.Log(debug.DEBUG_INFO, "WASM received packet", "bytes", len(data), "type", fmt.Sprintf("0x%02x", data[0]))
|
|
||||||
t.HandlePacket(data, ni)
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := t.RegisterInterface("wasm0", wsInterface); err != nil {
|
if err := t.RegisterInterface("wasm0", wsInterface); err != nil {
|
||||||
return js.ValueOf(map[string]interface{}{
|
return js.ValueOf(map[string]interface{}{
|
||||||
@@ -167,6 +247,16 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTransport returns the internal transport pointer.
|
||||||
|
func GetTransport() *transport.Transport {
|
||||||
|
return reticulumTransport
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDestinationPointer returns the internal destination pointer.
|
||||||
|
func GetDestinationPointer() *destination.Destination {
|
||||||
|
return reticulumDest
|
||||||
|
}
|
||||||
|
|
||||||
func GetIdentity(this js.Value, args []js.Value) interface{} {
|
func GetIdentity(this js.Value, args []js.Value) interface{} {
|
||||||
if reticulumIdentity == nil {
|
if reticulumIdentity == nil {
|
||||||
return js.ValueOf(map[string]interface{}{
|
return js.ValueOf(map[string]interface{}{
|
||||||
@@ -191,30 +281,19 @@ func GetDestination(this js.Value, args []js.Value) interface{} {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendAnnounce(this js.Value, args []js.Value) interface{} {
|
func IsConnected(this js.Value, args []js.Value) interface{} {
|
||||||
if reticulumDest == nil {
|
if reticulumTransport == nil {
|
||||||
return js.ValueOf(map[string]interface{}{
|
return js.ValueOf(false)
|
||||||
"error": "Reticulum not initialized",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var appData []byte
|
ifaces := reticulumTransport.GetInterfaces()
|
||||||
if len(args) >= 1 && args[0].String() != "" {
|
for _, iface := range ifaces {
|
||||||
appData = []byte(args[0].String())
|
if iface.IsOnline() {
|
||||||
userName = args[0].String()
|
return js.ValueOf(true)
|
||||||
} else if userName != "" {
|
}
|
||||||
appData = []byte(userName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := reticulumDest.Announce(false, appData, nil); err != nil {
|
return js.ValueOf(false)
|
||||||
return js.ValueOf(map[string]interface{}{
|
|
||||||
"error": fmt.Sprintf("Failed to send announce: %v", err),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return js.ValueOf(map[string]interface{}{
|
|
||||||
"success": true,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConnectWebSocket(this js.Value, args []js.Value) interface{} {
|
func ConnectWebSocket(this js.Value, args []js.Value) interface{} {
|
||||||
@@ -228,7 +307,7 @@ func ConnectWebSocket(this js.Value, args []js.Value) interface{} {
|
|||||||
for name, iface := range ifaces {
|
for name, iface := range ifaces {
|
||||||
if iface.IsOnline() {
|
if iface.IsOnline() {
|
||||||
return js.ValueOf(map[string]interface{}{
|
return js.ValueOf(map[string]interface{}{
|
||||||
"success": true,
|
"success": true,
|
||||||
"interface": name,
|
"interface": name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -238,7 +317,7 @@ func ConnectWebSocket(this js.Value, args []js.Value) interface{} {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return js.ValueOf(map[string]interface{}{
|
return js.ValueOf(map[string]interface{}{
|
||||||
"success": true,
|
"success": true,
|
||||||
"interface": name,
|
"interface": name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -272,51 +351,39 @@ func DisconnectWebSocket(this js.Value, args []js.Value) interface{} {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsConnected(this js.Value, args []js.Value) interface{} {
|
type genericAnnounceHandler struct{}
|
||||||
if reticulumTransport == nil {
|
|
||||||
return js.ValueOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
ifaces := reticulumTransport.GetInterfaces()
|
func (h *genericAnnounceHandler) AspectFilter() []string {
|
||||||
for _, iface := range ifaces {
|
|
||||||
if iface.IsOnline() {
|
|
||||||
return js.ValueOf(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return js.ValueOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
type announceHandler struct{}
|
|
||||||
|
|
||||||
func (h *announceHandler) AspectFilter() []string {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *announceHandler) ReceivePathResponses() bool {
|
func (h *genericAnnounceHandler) ReceivePathResponses() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *announceHandler) ReceivedAnnounce(destHash []byte, ident interface{}, appData []byte) error {
|
func (h *genericAnnounceHandler) ReceivedAnnounce(destHash []byte, ident interface{}, appData []byte, hops uint8) error {
|
||||||
hashStr := hex.EncodeToString(destHash)
|
debug.Log(debug.DEBUG_INFO, "WASM Announce Handler received announce", "dest", hex.EncodeToString(destHash), "hops", hops)
|
||||||
peerMap[hashStr] = string(appData)
|
stats.announcesReceived++
|
||||||
js.Global().Call("onPeerDiscovered", js.ValueOf(map[string]interface{}{
|
if !announceHandler.IsUndefined() {
|
||||||
"hash": hashStr,
|
hashStr := hex.EncodeToString(destHash)
|
||||||
"appData": string(appData),
|
announceHandler.Invoke(js.ValueOf(map[string]interface{}{
|
||||||
}))
|
"hash": hashStr,
|
||||||
|
"appData": string(appData),
|
||||||
|
"hops": int(hops),
|
||||||
|
}))
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendMessage(this js.Value, args []js.Value) interface{} {
|
// SendDataJS is the JS-facing wrapper for SendData
|
||||||
|
func SendDataJS(this js.Value, args []js.Value) interface{} {
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
return js.ValueOf(map[string]interface{}{
|
return js.ValueOf(map[string]interface{}{
|
||||||
"error": "Destination hash and message required",
|
"error": "Destination hash and data required",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
destHashHex := args[0].String()
|
destHashHex := args[0].String()
|
||||||
message := args[1].String()
|
|
||||||
|
|
||||||
destHash, err := hex.DecodeString(destHashHex)
|
destHash, err := hex.DecodeString(destHashHex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return js.ValueOf(map[string]interface{}{
|
return js.ValueOf(map[string]interface{}{
|
||||||
@@ -324,6 +391,26 @@ func SendMessage(this js.Value, args []js.Value) interface{} {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Support both string and Uint8Array data from JS
|
||||||
|
var data []byte
|
||||||
|
if args[1].Type() == js.TypeString {
|
||||||
|
data = []byte(args[1].String())
|
||||||
|
} else {
|
||||||
|
data = make([]byte, args[1].Length())
|
||||||
|
js.CopyBytesToGo(data, args[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return SendData(destHash, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendData is a generic function to send raw bytes to a destination
|
||||||
|
func SendData(destHash []byte, data []byte) interface{} {
|
||||||
|
if reticulumTransport == nil {
|
||||||
|
return js.ValueOf(map[string]interface{}{
|
||||||
|
"error": "Reticulum not initialized",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
remoteIdentity, err := identity.Recall(destHash)
|
remoteIdentity, err := identity.Recall(destHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return js.ValueOf(map[string]interface{}{
|
return js.ValueOf(map[string]interface{}{
|
||||||
@@ -338,7 +425,7 @@ func SendMessage(this js.Value, args []js.Value) interface{} {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
encrypted, err := targetDest.Encrypt([]byte(message))
|
encrypted, err := targetDest.Encrypt(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return js.ValueOf(map[string]interface{}{
|
return js.ValueOf(map[string]interface{}{
|
||||||
"error": fmt.Sprintf("Encryption failed: %v", err),
|
"error": fmt.Sprintf("Encryption failed: %v", err),
|
||||||
@@ -370,8 +457,42 @@ func SendMessage(this js.Value, args []js.Value) interface{} {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.packetsSent++
|
return js.ValueOf(map[string]interface{}{
|
||||||
stats.bytesSent += len(message)
|
"success": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendAnnounceJS is the JS-facing wrapper for SendAnnounce
|
||||||
|
func SendAnnounceJS(this js.Value, args []js.Value) interface{} {
|
||||||
|
var appData []byte
|
||||||
|
if len(args) >= 1 && args[0].Type() == js.TypeString {
|
||||||
|
appData = []byte(args[0].String())
|
||||||
|
} else if len(args) >= 1 && args[0].Type() == js.TypeObject {
|
||||||
|
appData = make([]byte, args[0].Length())
|
||||||
|
js.CopyBytesToGo(appData, args[0])
|
||||||
|
}
|
||||||
|
return SendAnnounce(appData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendAnnounce is a generic function to send an announce
|
||||||
|
func SendAnnounce(appData []byte) interface{} {
|
||||||
|
if reticulumDest == nil {
|
||||||
|
return js.ValueOf(map[string]interface{}{
|
||||||
|
"error": "Reticulum not initialized",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(appData) > 0 {
|
||||||
|
reticulumDest.SetDefaultAppData(appData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := reticulumDest.Announce(false, nil, nil); err != nil {
|
||||||
|
return js.ValueOf(map[string]interface{}{
|
||||||
|
"error": fmt.Sprintf("Failed to send announce: %v", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.announcesSent++
|
||||||
|
|
||||||
return js.ValueOf(map[string]interface{}{
|
return js.ValueOf(map[string]interface{}{
|
||||||
"success": true,
|
"success": true,
|
||||||
|
|||||||
147
pkg/wasm/wasm_test.go
Normal file
147
pkg/wasm/wasm_test.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
//go:build js && wasm
|
||||||
|
// +build js,wasm
|
||||||
|
|
||||||
|
package wasm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"syscall/js"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRegisterJSFunctions(t *testing.T) {
|
||||||
|
RegisterJSFunctions()
|
||||||
|
|
||||||
|
reticulum := js.Global().Get("reticulum")
|
||||||
|
if reticulum.IsUndefined() {
|
||||||
|
t.Fatal("reticulum object not registered in global scope")
|
||||||
|
}
|
||||||
|
|
||||||
|
functions := []string{
|
||||||
|
"init", "getIdentity", "getDestination", "announce",
|
||||||
|
"connect", "disconnect", "isConnected", "requestPath", "getStats",
|
||||||
|
"setPacketCallback", "setAnnounceCallback", "sendData",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fn := range functions {
|
||||||
|
if reticulum.Get(fn).Type() != js.TypeFunction {
|
||||||
|
t.Errorf("function %s not registered or not a function", fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetStats(t *testing.T) {
|
||||||
|
// Reset stats
|
||||||
|
stats.packetsSent = 10
|
||||||
|
stats.packetsReceived = 5
|
||||||
|
stats.bytesSent = 100
|
||||||
|
stats.bytesReceived = 50
|
||||||
|
|
||||||
|
result := GetStats(js.Undefined(), nil)
|
||||||
|
val := result.(js.Value)
|
||||||
|
|
||||||
|
if val.Get("packetsSent").Int() != 10 {
|
||||||
|
t.Errorf("expected packetsSent 10, got %d", val.Get("packetsSent").Int())
|
||||||
|
}
|
||||||
|
if val.Get("packetsReceived").Int() != 5 {
|
||||||
|
t.Errorf("expected packetsReceived 5, got %d", val.Get("packetsReceived").Int())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsConnected(t *testing.T) {
|
||||||
|
reticulumTransport = nil
|
||||||
|
connected := IsConnected(js.Undefined(), nil).(js.Value).Bool()
|
||||||
|
if connected {
|
||||||
|
t.Error("expected connected to be false when transport is nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitReticulum(t *testing.T) {
|
||||||
|
// Mock JS global functions
|
||||||
|
js.Global().Set("log", js.FuncOf(func(this js.Value, args []js.Value) interface{} { return nil }))
|
||||||
|
|
||||||
|
// Test without arguments
|
||||||
|
result := InitReticulum(js.Undefined(), []js.Value{})
|
||||||
|
val := result.(js.Value)
|
||||||
|
if val.Get("error").IsUndefined() || val.Get("error").String() != "WebSocket URL required" {
|
||||||
|
t.Errorf("expected error 'WebSocket URL required', got %v", val.Get("error"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with valid URL and app name
|
||||||
|
wsURL := "ws://localhost:8080"
|
||||||
|
appName := "test_app"
|
||||||
|
result = InitReticulum(js.Undefined(), []js.Value{js.ValueOf(wsURL), js.ValueOf(appName)})
|
||||||
|
val = result.(js.Value)
|
||||||
|
|
||||||
|
if !val.Get("success").Bool() {
|
||||||
|
t.Errorf("InitReticulum failed: %v", val.Get("error"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if reticulumIdentity == nil {
|
||||||
|
t.Fatal("reticulumIdentity should not be nil after successful init")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with provided identity
|
||||||
|
id, _ := identity.NewIdentity()
|
||||||
|
idHex := id.GetHexHash()
|
||||||
|
// InitReticulum expects the FULL identity bytes in hex (64 bytes).
|
||||||
|
idBytes := id.GetPrivateKey()
|
||||||
|
idHexFull := hex.EncodeToString(idBytes)
|
||||||
|
|
||||||
|
result = InitReticulum(js.Undefined(), []js.Value{js.ValueOf(wsURL), js.ValueOf(appName), js.ValueOf(idHexFull)})
|
||||||
|
val = result.(js.Value)
|
||||||
|
|
||||||
|
if !val.Get("success").Bool() {
|
||||||
|
t.Errorf("InitReticulum with identity failed: %v", val.Get("error"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if reticulumIdentity.GetHexHash() != idHex {
|
||||||
|
t.Errorf("expected identity hash %s, got %s", idHex, reticulumIdentity.GetHexHash())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIdentityAndDestination(t *testing.T) {
|
||||||
|
// Ensure initialized
|
||||||
|
js.Global().Set("log", js.FuncOf(func(this js.Value, args []js.Value) interface{} { return nil }))
|
||||||
|
InitReticulum(js.Undefined(), []js.Value{js.ValueOf("ws://localhost")})
|
||||||
|
|
||||||
|
idResult := GetIdentity(js.Undefined(), nil).(js.Value)
|
||||||
|
if idResult.Get("hash").String() != reticulumIdentity.GetHexHash() {
|
||||||
|
t.Error("GetIdentity returned wrong hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
destResult := GetDestination(js.Undefined(), nil).(js.Value)
|
||||||
|
expectedDest := fmt.Sprintf("%x", reticulumDest.GetHash())
|
||||||
|
if destResult.Get("hash").String() != expectedDest {
|
||||||
|
t.Errorf("GetDestination returned wrong hash, expected %s got %s", expectedDest, destResult.Get("hash").String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendDataJS(t *testing.T) {
|
||||||
|
// Ensure initialized
|
||||||
|
InitReticulum(js.Undefined(), []js.Value{js.ValueOf("ws://localhost")})
|
||||||
|
|
||||||
|
// Create a mock peer
|
||||||
|
peerId, _ := identity.NewIdentity()
|
||||||
|
peerHash := peerId.Hash()
|
||||||
|
peerHashHex := hex.EncodeToString(peerHash)
|
||||||
|
|
||||||
|
// Manually add to known destinations so Recall works
|
||||||
|
identity.Remember([]byte("mock_packet"), peerHash, peerId.GetPublicKey(), []byte("peer_app_data"))
|
||||||
|
|
||||||
|
// Test SendDataJS with string
|
||||||
|
data := "Hello Peer!"
|
||||||
|
result := SendDataJS(js.Undefined(), []js.Value{js.ValueOf(peerHashHex), js.ValueOf(data)}).(js.Value)
|
||||||
|
|
||||||
|
if !result.Get("error").IsUndefined() {
|
||||||
|
errStr := result.Get("error").String()
|
||||||
|
if errStr != "Packet sending failed: no path to destination" {
|
||||||
|
t.Errorf("SendDataJS failed with unexpected error: %s", errStr)
|
||||||
|
}
|
||||||
|
} else if !result.Get("success").Bool() {
|
||||||
|
t.Errorf("SendDataJS failed without error message")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user