23 Commits

Author SHA1 Message Date
4e3e1b9104 feat(transport): add SetTransportInstance function to allow setting the transport instance
All checks were successful
Go Build Test / Build (darwin, amd64) (pull_request) Successful in 38s
Go Build Test / Build (freebsd, arm) (pull_request) Successful in 35s
Bearer / scan (pull_request) Successful in 40s
Go Build Test / Build (linux, amd64) (pull_request) Successful in 37s
Go Build Test / Build (darwin, arm64) (pull_request) Successful in 41s
Go Build Test / Build (freebsd, arm64) (pull_request) Successful in 42s
Go Build Test / Build (windows, arm64) (pull_request) Successful in 38s
Go Build Test / Build (linux, arm64) (pull_request) Successful in 41s
Go Build Test / Build (js, wasm) (pull_request) Successful in 31s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (pull_request) Successful in 1m20s
Go Revive Lint / lint (pull_request) Successful in 57s
Run Gosec / tests (pull_request) Successful in 1m29s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (pull_request) Successful in 2m52s
Go Build Test / Build (linux, arm) (pull_request) Successful in 9m26s
Go Build Test / Build (windows, amd64) (pull_request) Successful in 9m28s
Go Benchmarks / Run Benchmarks (pull_request) Successful in 9m48s
Go Build Test / Build (windows, arm) (pull_request) Successful in 9m24s
Go Build Test / Build (freebsd, amd64) (pull_request) Successful in 9m30s
2026-01-02 17:57:29 -06:00
41bcb65e16 feat(wasm): update statistics tracking by adding announce metrics and updating packet handling
Some checks failed
Go Build Test / Build (freebsd, amd64) (pull_request) Successful in 9m24s
Bearer / scan (pull_request) Successful in 48s
Go Benchmarks / Run Benchmarks (pull_request) Successful in 1m10s
Go Build Test / Build (darwin, amd64) (pull_request) Successful in 38s
Go Build Test / Build (linux, amd64) (pull_request) Successful in 39s
Go Build Test / Build (windows, amd64) (pull_request) Successful in 38s
Go Build Test / Build (freebsd, arm) (pull_request) Successful in 41s
Go Build Test / Build (windows, arm) (pull_request) Successful in 38s
Go Build Test / Build (linux, arm) (pull_request) Successful in 40s
Go Build Test / Build (darwin, arm64) (pull_request) Successful in 33s
Go Build Test / Build (freebsd, arm64) (pull_request) Successful in 29s
Go Build Test / Build (linux, arm64) (pull_request) Successful in 45s
Go Build Test / Build (js, wasm) (pull_request) Failing after 41s
Go Build Test / Build (windows, arm64) (pull_request) Successful in 44s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (pull_request) Successful in 1m9s
Go Revive Lint / lint (pull_request) Successful in 1m5s
Run Gosec / tests (pull_request) Successful in 1m14s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (pull_request) Failing after 2m36s
2026-01-02 17:49:13 -06:00
0ba311b25d refactor(transport): remove unused TCPClientInterface statistics update from HandlePacket function 2026-01-02 17:48:59 -06:00
c22aa0cb45 refactor(interfaces): update WebSocketInterface packet handling and streamline message processing 2026-01-02 17:48:55 -06:00
25e04b1b80 refactor(interfaces): remove Send method from UDPInterface and streamline packet processing 2026-01-02 17:48:43 -06:00
e508f63b83 feat(interfaces): implement ProcessOutgoing method for TCPClientInterface and remove unused statistics methods 2026-01-02 17:48:39 -06:00
f28ba4d69e feat(interfaces): update AutoInterface with multicast address generation and duplicate data handling 2026-01-02 17:48:34 -06:00
62b5d6a4d2 feat(config): add MulticastAddrType field to InterfaceConfig structure 2026-01-02 17:48:19 -06:00
8325666301 feat(interfaces): add transmission and reception metrics to BaseInterface 2026-01-02 17:48:15 -06:00
f80d50c27b refactor(tests): clean up whitespace in TestTransportLeak function
All checks were successful
Go Build Test / Build (windows, arm) (pull_request) Successful in 9m26s
Go Build Test / Build (darwin, arm64) (pull_request) Successful in 9m24s
Bearer / scan (pull_request) Successful in 9s
Go Benchmarks / Run Benchmarks (pull_request) Successful in 55s
Go Build Test / Build (linux, arm64) (pull_request) Successful in 39s
Go Build Test / Build (windows, arm64) (pull_request) Successful in 38s
Go Build Test / Build (windows, amd64) (pull_request) Successful in 44s
Go Build Test / Build (linux, arm) (pull_request) Successful in 42s
Go Build Test / Build (freebsd, amd64) (pull_request) Successful in 46s
Go Build Test / Build (freebsd, arm64) (pull_request) Successful in 30s
Go Build Test / Build (js, wasm) (pull_request) Successful in 34s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (pull_request) Successful in 1m18s
Go Revive Lint / lint (pull_request) Successful in 1m14s
Run Gosec / tests (pull_request) Successful in 1m28s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (pull_request) Successful in 3m6s
Go Build Test / Build (darwin, amd64) (pull_request) Successful in 9m23s
Go Build Test / Build (freebsd, arm) (pull_request) Successful in 9m23s
Go Build Test / Build (linux, amd64) (pull_request) Successful in 9m26s
2026-01-02 17:47:34 -06:00
f6b5f3ee82 refactor(tests): remove unnecessary blank line in packet fuzz test file 2026-01-02 17:47:29 -06:00
14d62efd17 refactor(tests): simplify MockInterface by embedding BaseInterface and removing redundant fields 2026-01-02 17:47:23 -06:00
c9f7f12a03 chore(.gitignore): add test/compat/ directory to ignore list 2026-01-02 17:46:48 -06:00
548ec55248 feat(tests): add TestTransportLeak to check for goroutine leaks in transport instances
Some checks failed
Bearer / scan (pull_request) Successful in 43s
Go Build Test / Build (darwin, amd64) (pull_request) Successful in 41s
Go Benchmarks / Run Benchmarks (pull_request) Failing after 1m2s
Go Build Test / Build (linux, amd64) (pull_request) Successful in 31s
Go Build Test / Build (windows, amd64) (pull_request) Successful in 38s
Go Build Test / Build (freebsd, amd64) (pull_request) Successful in 27s
Go Build Test / Build (freebsd, arm) (pull_request) Successful in 38s
Go Build Test / Build (linux, arm) (pull_request) Successful in 36s
Go Build Test / Build (darwin, arm64) (pull_request) Successful in 51s
Go Build Test / Build (windows, arm64) (pull_request) Successful in 38s
Go Build Test / Build (linux, arm64) (pull_request) Successful in 40s
Go Build Test / Build (windows, arm) (pull_request) Successful in 27s
Go Build Test / Build (freebsd, arm64) (pull_request) Successful in 49s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (pull_request) Failing after 58s
Go Build Test / Build (js, wasm) (pull_request) Successful in 21s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (pull_request) Failing after 1m16s
Go Revive Lint / lint (pull_request) Successful in 59s
Run Gosec / tests (pull_request) Successful in 1m20s
2026-01-02 16:39:50 -06:00
03753bf9bc feat(tests): add fuzz testing for packet unpacking functionality 2026-01-02 16:39:45 -06:00
012c0eec62 feat(tests): add network simulation test for transport layer functionality 2026-01-02 16:39:40 -06:00
6fe193d75a feat(tests): add new fuzz, resource leak, and network simulation tests; introduce benchmark and build-test workflows
All checks were successful
Go Build Test / Build (linux, arm) (pull_request) Successful in 41s
Go Build Test / Build (windows, amd64) (pull_request) Successful in 43s
Go Build Test / Build (freebsd, amd64) (pull_request) Successful in 45s
Go Benchmarks / Run Benchmarks (pull_request) Successful in 54s
Go Build Test / Build (js, wasm) (pull_request) Successful in 44s
Go Build Test / Build (linux, arm64) (pull_request) Successful in 48s
Go Build Test / Build (windows, arm64) (pull_request) Successful in 46s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (pull_request) Successful in 1m12s
Go Revive Lint / lint (pull_request) Successful in 1m8s
Run Gosec / tests (pull_request) Successful in 1m21s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (pull_request) Successful in 2m16s
Go Build Test / Build (darwin, amd64) (pull_request) Successful in 9m23s
Go Build Test / Build (freebsd, arm) (pull_request) Successful in 9m23s
Go Build Test / Build (linux, amd64) (pull_request) Successful in 9m26s
Go Build Test / Build (windows, arm) (pull_request) Successful in 9m26s
Go Build Test / Build (darwin, arm64) (pull_request) Successful in 9m24s
Go Build Test / Build (freebsd, arm64) (pull_request) Successful in 9m25s
Bearer / scan (pull_request) Successful in 10s
2026-01-02 16:39:19 -06:00
6b011144cf feat(wasm): set transport identity to node identity and initialize path request handler
All checks were successful
Bearer / scan (pull_request) Successful in 9s
Go Build Multi-Platform / build (arm, windows) (pull_request) Successful in 42s
Go Build Multi-Platform / build (arm64, darwin) (pull_request) Successful in 45s
Go Build Multi-Platform / build (arm64, freebsd) (pull_request) Successful in 45s
Go Build Multi-Platform / build (arm64, linux) (pull_request) Successful in 45s
Go Build Multi-Platform / build (wasm, js) (pull_request) Successful in 54s
Go Build Multi-Platform / build (arm64, windows) (pull_request) Successful in 1m0s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (pull_request) Successful in 1m12s
Go Revive Lint / lint (pull_request) Successful in 47s
Run Gosec / tests (pull_request) Successful in 1m24s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (pull_request) Successful in 2m26s
Go Build Multi-Platform / build (amd64, darwin) (pull_request) Successful in 9m30s
Go Build Multi-Platform / build (amd64, freebsd) (pull_request) Successful in 9m28s
Go Build Multi-Platform / build (amd64, linux) (pull_request) Successful in 9m30s
Go Build Multi-Platform / build (arm, freebsd) (pull_request) Successful in 9m27s
Go Build Multi-Platform / build (amd64, windows) (pull_request) Successful in 9m29s
Go Build Multi-Platform / build (arm, linux) (pull_request) Successful in 9m28s
Go Build Multi-Platform / Create Release (pull_request) Has been skipped
2026-01-02 15:42:36 -06:00
c26c50cc3a feat(transport): add TestAnnounceHopCount to validate hop count registration and update path handling logic 2026-01-02 15:42:21 -06:00
b972d87e91 fix(websocket): handle WebSocket connection states to prevent errors when starting an already initiated WebSocket 2026-01-02 15:42:12 -06:00
82bfa43240 fix(packet): reorder fields in Header Type 2 for correct unpacking of TransportID and DestinationHash 2026-01-02 15:42:03 -06:00
43aa622846 feat(destination): implement hash calculation for PLAIN destination and update identity handling in destination creation 2026-01-02 15:41:53 -06:00
97353d430b chore: add CONTRIBUTORS file to document project contributors and their contributions
All checks were successful
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 45s
Bearer / scan (push) Successful in 9s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 46s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 41s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 43s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 45s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 47s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m21s
Go Revive Lint / lint (push) Successful in 57s
Run Gosec / tests (push) Successful in 1m9s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 2m33s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 9m27s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 9m30s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 9m30s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m28s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m30s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 9m28s
Go Build Multi-Platform / Create Release (push) Has been skipped
2026-01-01 13:10:55 -06:00
44 changed files with 1017 additions and 1630 deletions

View 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

View 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

View File

@@ -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/*/*

View File

@@ -65,41 +65,20 @@ jobs:
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
run: task test-race
- name: Run Resource Leak tests (Linux AMD64 only)
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
run: task test-leaks
- name: Run Network Simulation tests (Linux AMD64 only)
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
run: task test-network
- name: Run Fuzz tests (Linux AMD64 only)
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
run: task test-fuzz
- name: Run WebAssembly tests (Linux AMD64 only)
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
run: |
chmod +x misc/wasm/go_js_wasm_exec
task test-wasm
- name: Test build (ensure compilation works)
run: |
echo "Testing build for current platform (${{ matrix.os }}, ${{ matrix.goarch }})..."
task build
- name: Test WebAssembly build (Linux AMD64 only)
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
run: task build-wasm
- name: Test binary execution
run: |
echo "Testing binary execution on (${{ matrix.os }}, ${{ matrix.goarch }})..."
timeout 5s ./bin/reticulum-go || echo "Binary started successfully (timeout expected)"
- name: Test cross-compilation (AMD64 runners only)
if: matrix.goarch == 'amd64'
run: |
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)
if: matrix.goarch == 'amd64'
run: |
echo "Testing ARMv6 cross-compilation from AMD64..."
GOOS=linux GOARCH=arm GOARM=6 task build
env:
GOOS: linux
GOARCH: arm
GOARM: 6

View 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/*/*

View File

@@ -5,15 +5,29 @@ on:
branches: [ "tinygo" ]
pull_request:
branches: [ "tinygo" ]
workflow_dispatch:
jobs:
tinygo-build-all:
tinygo-build:
permissions:
contents: read
strategy:
matrix:
include:
- name: tinygo-default
target: ""
output: reticulum-go-tinygo
make_target: tinygo-build
- name: tinygo-wasm
target: wasm
output: reticulum-go.wasm
make_target: tinygo-wasm
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
@@ -23,64 +37,28 @@ jobs:
with:
go-version: '1.24'
- name: Setup Task
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
with:
version: '3.46.3'
- name: Install TinyGo
run: |
wget https://github.com/tinygo-org/tinygo/releases/download/v0.37.0/tinygo_0.37.0_amd64.deb
sudo dpkg -i tinygo_0.37.0_amd64.deb
- name: Build for all TinyGo targets
- name: Build with TinyGo
id: build_step
run: |
task tinygo-build-all || true
echo "Build process completed (some targets may have failed)"
make ${{ matrix.make_target }}
output_name="${{ matrix.output }}"
if [ -f "bin/${output_name}" ]; then
sha256sum "bin/${output_name}" | cut -d' ' -f1 > "bin/${output_name}.sha256"
echo "Built: ${output_name}"
echo "Generated checksum: bin/${output_name}.sha256"
else
echo "Build output not found: bin/${output_name}"
ls -la bin/
exit 1
fi
- name: Collect build results
run: |
mkdir -p artifacts
unsupported_file="artifacts/unsupported-microcontrollers.txt"
echo "# Unsupported Microcontrollers" > "$unsupported_file"
echo "# Generated: $(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> "$unsupported_file"
echo "" >> "$unsupported_file"
failed_count=0
for log_file in bin/build-*.log; do
if [ -f "$log_file" ]; then
target=$(basename "$log_file" | sed 's/build-\(.*\)\.log/\1/')
binary_file="bin/reticulum-go-${target}"
if [ ! -f "$binary_file" ] || grep -qi "error\|Error\|ERROR\|failed\|Failed\|FAILED" "$log_file"; then
failed_count=$((failed_count + 1))
echo "## $target" >> "$unsupported_file"
echo "" >> "$unsupported_file"
if grep -qi "program too large\|overflowed\|too big\|LLVM ERROR\|Error while" "$log_file"; then
grep -i "program too large\|overflowed\|too big\|LLVM ERROR\|Error while" "$log_file" | head -5 >> "$unsupported_file"
else
tail -15 "$log_file" >> "$unsupported_file"
fi
echo "" >> "$unsupported_file"
echo "\`\`\`" >> "$unsupported_file"
tail -30 "$log_file" >> "$unsupported_file"
echo "\`\`\`" >> "$unsupported_file"
echo "" >> "$unsupported_file"
fi
fi
done
echo "Total failed builds: $failed_count" >> "$unsupported_file"
echo "Generated unsupported-microcontrollers.txt with $failed_count failed targets"
- name: Upload build artifacts
- name: Upload Artifact
uses: https://git.quad4.io/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5
with:
name: tinygo-builds
path: |
bin/reticulum-go-*
artifacts/unsupported-microcontrollers.txt
if-no-files-found: warn
name: ${{ matrix.name }}
path: bin/${{ matrix.output }}*

3
.gitignore vendored
View File

@@ -28,4 +28,5 @@ Thumbs.db # Windows Explorer thumbnail cache
# Swap and test binaries
*.swp # Swap files (e.g. vim)
*.test # Go test binaries
*.test # Go test binaries
test/compat/

14
CONTRIBUTORS Normal file
View 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

View File

@@ -141,6 +141,21 @@ tasks:
cmds:
- '{{.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:
desc: Generate test coverage report
cmds:
@@ -243,37 +258,7 @@ tasks:
desc: Build binary with TinyGo compiler
cmds:
- mkdir -p {{.BUILD_DIR}}
- tinygo build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-tinygo -size short -opt=z -gc=leaking -panic=trap {{.MAIN_PACKAGE}}
tinygo-build-debug:
desc: Build binary optimized for debugging with TinyGo compiler
cmds:
- mkdir -p {{.BUILD_DIR}}
- tinygo build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-tinygo-debug -size short -opt=1 {{.MAIN_PACKAGE}}
tinygo-build-all:
desc: Build for all available TinyGo targets in parallel
cmds:
- mkdir -p {{.BUILD_DIR}}
- echo "Building for all TinyGo targets in parallel..."
- |
targets=$(tinygo targets)
failed=0
# Use xargs to build in parallel, limited to number of CPU cores
echo "$targets" | xargs -P $(nproc) -I {} sh -c '
target="{}";
if [ -n "$target" ]; then
echo "Building for target: $target";
if ! tinygo build -target "$target" -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-"$target" -size short -opt=z -gc=leaking -panic=trap {{.MAIN_PACKAGE}} 2>&1 | tee {{.BUILD_DIR}}/build-"$target".log; then
echo "Failed to build for $target" >> {{.BUILD_DIR}}/build-"$target".log;
exit 1;
fi;
fi' || failed=1
echo "Build complete. Check {{.BUILD_DIR}}/ for outputs and logs."
if [ $failed -ne 0 ]; then
echo "Some target(s) failed to build. See logs in {{.BUILD_DIR}}/build-*.log"
fi
- tinygo build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-tinygo -size short {{.MAIN_PACKAGE}}
tinygo-wasm:
desc: Build WebAssembly binary with TinyGo compiler

View File

@@ -207,34 +207,6 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
} else {
debug.Log(debug.DEBUG_INFO, "WebSocket interface created successfully", common.STR_NAME, name)
}
case "SerialInterface":
iface, err = interfaces.NewSerialInterface(
name,
ifaceConfig.Interface,
uint32(ifaceConfig.Bitrate), // #nosec G115
ifaceConfig.Enabled,
)
case "RNodeInterface":
// RNode usually runs over Serial
serial, sErr := interfaces.NewSerialInterface(
name+"_serial",
ifaceConfig.Interface,
uint32(ifaceConfig.Bitrate), // #nosec G115
ifaceConfig.Enabled,
)
if sErr != nil {
err = sErr
} else {
iface, err = interfaces.NewRNodeInterface(
name,
serial,
ifaceConfig.Frequency,
ifaceConfig.Bandwidth,
ifaceConfig.SF,
ifaceConfig.CR,
ifaceConfig.TXPower,
)
}
default:
debug.Log(debug.DEBUG_CRITICAL, "Unknown interface type", common.STR_TYPE, ifaceConfig.Type)
continue

View File

Binary file not shown.

4
go.mod
View File

@@ -3,6 +3,8 @@ module git.quad4.io/Networks/Reticulum-Go
go 1.24.0
require (
github.com/shamaton/msgpack/v2 v2.4.0
github.com/vmihailenco/msgpack/v5 v5.4.1
golang.org/x/crypto v0.46.0
)
require github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect

14
go.sum
View File

@@ -1,4 +1,14 @@
github.com/shamaton/msgpack/v2 v2.4.0 h1:O5Z08MRmbo0lA9o2xnQ4TXx6teJbPqEurqcCOQ8Oi/4=
github.com/shamaton/msgpack/v2 v2.4.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI=
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=

View File

@@ -10,8 +10,8 @@ import (
"sync"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
"github.com/vmihailenco/msgpack/v5"
)
type Manager struct {
@@ -88,7 +88,7 @@ func (m *Manager) SaveRatchet(identityHash []byte, ratchetKey []byte) error {
Received: time.Now().Unix(),
}
data, err := common.MsgpackMarshal(ratchetData)
data, err := msgpack.Marshal(ratchetData)
if err != nil {
return fmt.Errorf("failed to marshal ratchet data: %w", err)
}
@@ -146,7 +146,7 @@ func (m *Manager) LoadRatchets(identityHash []byte) (map[string][]byte, error) {
}
var ratchetData RatchetData
if err := common.MsgpackUnmarshal(data, &ratchetData); err != nil {
if err := msgpack.Unmarshal(data, &ratchetData); err != nil {
debug.Log(debug.DEBUG_ERROR, "Corrupted ratchet data", "file", entry.Name(), "error", err)
_ = os.Remove(filePath)
continue

View File

@@ -21,30 +21,26 @@ type ConfigProvider interface {
// InterfaceConfig represents interface configuration
type InterfaceConfig struct {
Name string
Type string
Enabled bool
Address string
Port int
TargetHost string
TargetPort int
TargetAddress string
Interface string
KISSFraming bool
I2PTunneled bool
PreferIPv6 bool
MaxReconnTries int
Bitrate int64
MTU int
GroupID string
DiscoveryScope string
DiscoveryPort int
DataPort int
Frequency uint32
Bandwidth uint32
SF uint8
CR uint8
TXPower uint8
Name string
Type string
Enabled bool
Address string
Port int
TargetHost string
TargetPort int
TargetAddress string
Interface string
KISSFraming bool
I2PTunneled bool
PreferIPv6 bool
MaxReconnTries int
Bitrate int64
MTU int
GroupID string
DiscoveryScope string
DiscoveryPort int
DataPort int
MulticastAddrType string
}
// ReticulumConfig represents the main configuration structure

View File

@@ -39,6 +39,10 @@ type NetworkInterface interface {
SendLinkPacket([]byte, []byte, time.Time) error
SetPacketCallback(PacketCallback)
GetPacketCallback() PacketCallback
GetTxBytes() uint64
GetRxBytes() uint64
GetTxPackets() uint64
GetRxPackets() uint64
}
// BaseInterface provides common implementation for network interfaces
@@ -56,9 +60,11 @@ type BaseInterface struct {
MTU int
Bitrate int64
TxBytes uint64
RxBytes uint64
lastTx time.Time
TxBytes uint64
RxBytes uint64
TxPackets uint64
RxPackets uint64
lastTx time.Time
Mutex sync.RWMutex
Owner interface{}
@@ -125,6 +131,30 @@ func (i *BaseInterface) GetPacketCallback() 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() {
i.Mutex.Lock()
defer i.Mutex.Unlock()
@@ -160,10 +190,20 @@ func (i *BaseInterface) GetConn() net.Conn {
}
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)
}
func (i *BaseInterface) ProcessIncoming(data []byte) {
i.Mutex.Lock()
i.RxBytes += uint64(len(data))
i.RxPackets++
i.Mutex.Unlock()
if i.PacketCallback != nil {
i.PacketCallback(data, i)
}

View File

@@ -1,17 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package common
import (
"github.com/shamaton/msgpack/v2"
)
// Marshal returns the MessagePack encoding of v.
func MsgpackMarshal(v interface{}) ([]byte, error) {
return msgpack.Marshal(v)
}
// Unmarshal parses the MessagePack-encoded data and stores the result in the value pointed to by v.
func MsgpackUnmarshal(data []byte, v interface{}) error {
return msgpack.Unmarshal(data, v)
}

View File

@@ -17,6 +17,7 @@ import (
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
"github.com/vmihailenco/msgpack/v5"
"golang.org/x/crypto/curve25519"
)
@@ -106,9 +107,9 @@ type Destination struct {
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)
if id == nil {
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil")
return nil, errors.New("identity cannot be nil")
if id == nil && destType != PLAIN {
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil for non-PLAIN destination")
return nil, errors.New("identity cannot be nil for non-PLAIN destination")
}
d := &Destination{
@@ -143,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) {
debug.Log(debug.DEBUG_INFO, "Creating destination from hash", "hash", fmt.Sprintf("%x", hash))
if id == nil {
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil")
return nil, errors.New("identity cannot be nil")
if id == nil && destType != PLAIN {
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil for non-PLAIN destination")
return nil, errors.New("identity cannot be nil for non-PLAIN destination")
}
d := &Destination{
@@ -168,19 +169,25 @@ func FromHash(hash []byte, id *identity.Identity, destType byte, transport Trans
func (d *Destination) calculateHash() []byte {
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
nameHashFull := sha256.Sum256([]byte(d.ExpandName()))
nameHash10 := nameHashFull[:10] // Only use 10 bytes
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))
var combined []byte
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
combined := append(nameHash10, identityHash...)
// Concatenate name_hash (10 bytes) + identity_hash (16 bytes) = 26 bytes
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
finalHashFull := sha256.Sum256(combined)
@@ -606,7 +613,7 @@ func (d *Destination) persistRatchets() error {
debug.Log(debug.DEBUG_PACKETS, "Persisting ratchets", "count", len(d.ratchets), "path", d.ratchetPath)
// Pack ratchets using msgpack
packedRatchets, err := common.MsgpackMarshal(d.ratchets)
packedRatchets, err := msgpack.Marshal(d.ratchets)
if err != nil {
return fmt.Errorf("failed to pack ratchets: %w", err)
}
@@ -624,7 +631,7 @@ func (d *Destination) persistRatchets() error {
}
// Pack the entire structure
finalData, err := common.MsgpackMarshal(persistedData)
finalData, err := msgpack.Marshal(persistedData)
if err != nil {
return fmt.Errorf("failed to pack ratchet data: %w", err)
}
@@ -687,7 +694,7 @@ func (d *Destination) reloadRatchets() error {
// Unpack outer structure
var persistedData map[string][]byte
if err := common.MsgpackUnmarshal(fileData, &persistedData); err != nil {
if err := msgpack.Unmarshal(fileData, &persistedData); err != nil {
return fmt.Errorf("failed to unpack ratchet data: %w", err)
}
@@ -704,7 +711,7 @@ func (d *Destination) reloadRatchets() error {
}
// Unpack ratchet list
if err := common.MsgpackUnmarshal(packedRatchets, &d.ratchets); err != nil {
if err := msgpack.Unmarshal(packedRatchets, &d.ratchets); err != nil {
return fmt.Errorf("failed to unpack ratchet list: %w", err)
}

View File

@@ -2,6 +2,7 @@ package destination
import (
"bytes"
"crypto/sha256"
"path/filepath"
"testing"
@@ -150,3 +151,28 @@ func TestPlainDestination(t *testing.T) {
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)
}
}

View File

@@ -20,6 +20,7 @@ import (
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/cryptography"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
"github.com/vmihailenco/msgpack/v5"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf"
)
@@ -671,7 +672,7 @@ func (i *Identity) saveRatchets(path string) error {
}
// Pack ratchets using msgpack
packedRatchets, err := common.MsgpackMarshal(ratchetList)
packedRatchets, err := msgpack.Marshal(ratchetList)
if err != nil {
return fmt.Errorf("failed to pack ratchets: %w", err)
}
@@ -686,7 +687,7 @@ func (i *Identity) saveRatchets(path string) error {
}
// Pack the entire structure
finalData, err := common.MsgpackMarshal(persistedData)
finalData, err := msgpack.Marshal(persistedData)
if err != nil {
return fmt.Errorf("failed to pack ratchet data: %w", err)
}
@@ -799,7 +800,7 @@ func (i *Identity) loadRatchets(path string) error {
// Unpack outer structure: {"signature": ..., "ratchets": ...}
var persistedData map[string][]byte
if err := common.MsgpackUnmarshal(fileData, &persistedData); err != nil {
if err := msgpack.Unmarshal(fileData, &persistedData); err != nil {
return fmt.Errorf("failed to unpack ratchet data: %w", err)
}
@@ -817,7 +818,7 @@ func (i *Identity) loadRatchets(path string) error {
// Unpack ratchet list
var ratchetList [][]byte
if err := common.MsgpackUnmarshal(packedRatchets, &ratchetList); err != nil {
if err := msgpack.Unmarshal(packedRatchets, &ratchetList); err != nil {
return fmt.Errorf("failed to unpack ratchet list: %w", err)
}

View File

@@ -1,16 +1,13 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build !tinygo
// +build !tinygo
package interfaces
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"net"
"strings"
"sync"
"time"
@@ -37,8 +34,16 @@ const (
MCAST_ADDR_TYPE_PERMANENT = "0"
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 {
BaseInterface
groupID []byte
@@ -48,7 +53,6 @@ type AutoInterface struct {
discoveryScope string
multicastAddrType string
mcastDiscoveryAddr string
ifacNetname string
peers map[string]*Peer
linkLocalAddrs []string
adoptedInterfaces map[string]*AdoptedInterface
@@ -63,6 +67,7 @@ type AutoInterface struct {
peerJobInterval time.Duration
peeringTimeout time.Duration
mcastEchoTimeout time.Duration
mifDeque []DequeEntry
done chan struct{}
stopOnce sync.Once
}
@@ -79,6 +84,24 @@ type Peer struct {
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) {
groupID := DEFAULT_GROUP_ID
if config.GroupID != "" {
@@ -91,6 +114,9 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
}
multicastAddrType := MCAST_ADDR_TYPE_TEMPORARY
if config.MulticastAddrType != "" {
multicastAddrType = normalizeMulticastType(config.MulticastAddrType)
}
discoveryPort := DEFAULT_DISCOVERY_PORT
if config.DiscoveryPort != 0 {
@@ -104,8 +130,13 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
groupHash := sha256.Sum256([]byte(groupID))
ifacNetname := hex.EncodeToString(groupHash[:])[:16]
mcastAddr := fmt.Sprintf("ff%s%s::%s", discoveryScope, multicastAddrType, ifacNetname)
// Python-compatible multicast address generation
// 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{
BaseInterface: BaseInterface{
@@ -127,7 +158,6 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
discoveryScope: discoveryScope,
multicastAddrType: multicastAddrType,
mcastDiscoveryAddr: mcastAddr,
ifacNetname: ifacNetname,
peers: make(map[string]*Peer),
linkLocalAddrs: make([]string, 0),
adoptedInterfaces: make(map[string]*AdoptedInterface),
@@ -141,6 +171,7 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
peerJobInterval: PEER_JOB_INTERVAL,
peeringTimeout: PEERING_TIMEOUT,
mcastEchoTimeout: MCAST_ECHO_TIMEOUT,
mifDeque: make([]DequeEntry, 0, MULTI_IF_DEQUE_LEN),
done: make(chan struct{}),
}
@@ -275,7 +306,7 @@ func (ai *AutoInterface) configureInterface(iface *net.Interface) error {
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok {
if ipnet.IP.To4() == nil && ipnet.IP.IsLinkLocalUnicast() {
linkLocalAddr = ipnet.IP.String()
linkLocalAddr = descopeLinkLocal(ipnet.IP.String())
break
}
}
@@ -384,12 +415,17 @@ func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) {
return
}
if n >= len(ai.groupHash) {
receivedHash := buf[:len(ai.groupHash)]
if bytes.Equal(receivedHash, ai.groupHash) {
// Python: discovery_token = RNS.Identity.full_hash(self.group_id+ipv6_src[0].encode("utf-8"))
peerIP := descopeLinkLocal(remoteAddr.IP.String())
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)
} 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)
}
}
}
@@ -408,7 +444,7 @@ func (ai *AutoInterface) handleData(conn *net.UDPConn, ifaceName string) {
default:
}
n, _, err := conn.ReadFromUDP(buf)
n, remoteAddr, err := conn.ReadFromUDP(buf)
if err != nil {
if ai.IsOnline() {
debug.Log(debug.DEBUG_ERROR, "Data read error", "interface", ifaceName, "error", err)
@@ -416,8 +452,41 @@ func (ai *AutoInterface) handleData(conn *net.UDPConn, ifaceName string) {
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 {
callback(buf[:n], ai)
callback(data, ai)
}
}
}
@@ -488,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)
} else {
debug.Log(debug.DEBUG_TRACE, "Sent peer announce", "interface", adoptedIface.name)

View File

@@ -1,96 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build tinygo
// +build tinygo
package interfaces
import (
"fmt"
"net"
"sync"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
)
const (
HW_MTU = 1196
DEFAULT_DISCOVERY_PORT = 29716
DEFAULT_DATA_PORT = 42671
DEFAULT_GROUP_ID = "reticulum"
BITRATE_GUESS = 10 * 1000 * 1000
)
type AutoInterface struct {
BaseInterface
groupID []byte
discoveryPort int
dataPort int
discoveryScope string
peers map[string]*Peer
linkLocalAddrs []string
adoptedInterfaces map[string]string
interfaceServers map[string]net.Conn
multicastEchoes map[string]time.Time
mutex sync.RWMutex
outboundConn net.Conn
}
type Peer struct {
ifaceName string
lastHeard time.Time
conn net.PacketConn
}
func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterface, error) {
ai := &AutoInterface{
BaseInterface: BaseInterface{
Name: name,
Mode: common.IF_MODE_FULL,
Type: common.IF_TYPE_AUTO,
Online: false,
Enabled: config.Enabled,
Detached: false,
IN: true,
OUT: false,
MTU: HW_MTU,
Bitrate: BITRATE_GUESS,
},
discoveryPort: DEFAULT_DISCOVERY_PORT,
dataPort: DEFAULT_DATA_PORT,
peers: make(map[string]*Peer),
linkLocalAddrs: make([]string, 0),
adoptedInterfaces: make(map[string]string),
interfaceServers: make(map[string]net.Conn),
multicastEchoes: make(map[string]time.Time),
}
if config.Port != 0 {
ai.discoveryPort = config.Port
}
if config.GroupID != "" {
ai.groupID = []byte(config.GroupID)
} else {
ai.groupID = []byte("reticulum")
}
return ai, nil
}
func (ai *AutoInterface) Start() error {
// TinyGo doesn't support net.Interfaces() or multicast UDP
return fmt.Errorf("AutoInterface not supported in TinyGo - requires interface enumeration and multicast UDP")
}
func (ai *AutoInterface) Send(data []byte, address string) error {
return fmt.Errorf("Send not supported in TinyGo - requires UDP client connections")
}
func (ai *AutoInterface) Stop() error {
ai.Mutex.Lock()
defer ai.Mutex.Unlock()
ai.Online = false
return nil
}

View File

@@ -56,20 +56,22 @@ type Interface interface {
}
type BaseInterface struct {
Name string
Mode common.InterfaceMode
Type common.InterfaceType
Online bool
Enabled bool
Detached bool
IN bool
OUT bool
MTU int
Bitrate int64
TxBytes uint64
RxBytes uint64
lastTx time.Time
lastRx time.Time
Name string
Mode common.InterfaceMode
Type common.InterfaceType
Online bool
Enabled bool
Detached bool
IN bool
OUT bool
MTU int
Bitrate int64
TxBytes uint64
RxBytes uint64
TxPackets uint64
RxPackets uint64
lastTx time.Time
lastRx time.Time
Mutex sync.RWMutex
packetCallback common.PacketCallback
@@ -77,18 +79,22 @@ type BaseInterface struct {
func NewBaseInterface(name string, ifType common.InterfaceType, enabled bool) BaseInterface {
return BaseInterface{
Name: name,
Mode: common.IF_MODE_FULL,
Type: ifType,
Online: false,
Enabled: enabled,
Detached: false,
IN: false,
OUT: false,
MTU: common.DEFAULT_MTU,
Bitrate: BITRATE_MINIMUM,
lastTx: time.Now(),
lastRx: time.Now(),
Name: name,
Mode: common.IF_MODE_FULL,
Type: ifType,
Online: false,
Enabled: enabled,
Detached: false,
IN: false,
OUT: false,
MTU: common.DEFAULT_MTU,
Bitrate: BITRATE_MINIMUM,
TxBytes: 0,
RxBytes: 0,
TxPackets: 0,
RxPackets: 0,
lastTx: time.Now(),
lastRx: time.Now(),
}
}
@@ -107,6 +113,7 @@ func (i *BaseInterface) GetPacketCallback() common.PacketCallback {
func (i *BaseInterface) ProcessIncoming(data []byte) {
i.Mutex.Lock()
i.RxBytes += uint64(len(data))
i.RxPackets++
i.Mutex.Unlock()
i.Mutex.RLock()
@@ -126,6 +133,7 @@ func (i *BaseInterface) ProcessOutgoing(data []byte) error {
i.Mutex.Lock()
i.TxBytes += uint64(len(data))
i.TxPackets++
i.Mutex.Unlock()
debug.Log(debug.DEBUG_VERBOSE, "Interface processed outgoing packet", "name", i.Name, "bytes", len(data), "total_tx", i.TxBytes)
@@ -221,6 +229,30 @@ func (i *BaseInterface) IsDetached() bool {
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 {
return nil
}
@@ -272,7 +304,6 @@ func (i *BaseInterface) updateBandwidthStats(bytes uint64) {
i.Mutex.Lock()
defer i.Mutex.Unlock()
i.TxBytes += bytes
i.lastTx = time.Now()
debug.Log(debug.DEBUG_VERBOSE, "Interface updated bandwidth stats", "name", i.Name, "tx_bytes", i.TxBytes, "last_tx", i.lastTx)

View File

@@ -1,24 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package interfaces
const (
KISS_FEND = 0xC0
KISS_FESC = 0xDB
KISS_TFEND = 0xDC
KISS_TFESC = 0xDD
)
func escapeKISS(data []byte) []byte {
escaped := make([]byte, 0, len(data)*2)
for _, b := range data {
if b == KISS_FEND {
escaped = append(escaped, KISS_FESC, KISS_TFEND)
} else if b == KISS_FESC {
escaped = append(escaped, KISS_FESC, KISS_TFESC)
} else {
escaped = append(escaped, b)
}
}
return escaped
}

View File

@@ -1,29 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build !tinygo
package interfaces
import (
"fmt"
)
type LoRaInterface struct {
BaseInterface
}
func NewLoRaInterface(name string, spi interface{}, cs, reset, dio0 interface{}, freq uint32, bw uint32, sf uint8, cr uint8, enabled bool) (*LoRaInterface, error) {
return nil, fmt.Errorf("LoRaInterface is only supported on TinyGo targets currently")
}
func (li *LoRaInterface) Start() error {
return fmt.Errorf("LoRaInterface is only supported on TinyGo targets currently")
}
func (li *LoRaInterface) Stop() error {
return nil
}
func (li *LoRaInterface) Send(data []byte, address string) error {
return fmt.Errorf("LoRaInterface is only supported on TinyGo targets currently")
}

View File

@@ -1,292 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build tinygo
package interfaces
import (
"fmt"
"machine"
"sync"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
)
const (
REG_FIFO = 0x00
REG_OP_MODE = 0x01
REG_FRF_MSB = 0x06
REG_FRF_MID = 0x07
REG_FRF_LSB = 0x08
REG_PA_CONFIG = 0x09
REG_FIFO_ADDR_PTR = 0x0D
REG_FIFO_TX_BASE_ADDR = 0x0E
REG_FIFO_RX_BASE_ADDR = 0x0F
REG_FIFO_RX_CURRENT_ADDR = 0x10
REG_IRQ_FLAGS = 0x12
REG_RX_NB_BYTES = 0x13
REG_MODEM_CONFIG_1 = 0x1D
REG_MODEM_CONFIG_2 = 0x1E
REG_PREAMBLE_MSB = 0x20
REG_PREAMBLE_LSB = 0x21
REG_PAYLOAD_LENGTH = 0x22
REG_MODEM_CONFIG_3 = 0x26
REG_RSSI_WIDEBAND = 0x2C
REG_DETECTION_OPTIMIZE = 0x31
REG_DETECTION_THRESHOLD = 0x37
REG_SYNC_WORD = 0x39
REG_DIO_MAPPING_1 = 0x40
REG_VERSION = 0x42
MODE_LONG_RANGE_MODE = 0x80
MODE_SLEEP = 0x00
MODE_STDBY = 0x01
MODE_TX = 0x03
MODE_RX_CONTINUOUS = 0x05
IRQ_RX_DONE_MASK = 0x40
IRQ_PAYLOAD_CRC_ERROR_MASK = 0x20
IRQ_TX_DONE_MASK = 0x08
MAX_PKT_LENGTH = 255
)
// LoRaInterface provides a TinyGo SPI-based LoRa interface for SX127x.
type LoRaInterface struct {
BaseInterface
spi machine.SPI
cs machine.Pin
reset machine.Pin
dio0 machine.Pin
freq uint32
bw uint32
sf uint8
cr uint8
txPower uint8
done chan struct{}
stopOnce sync.Once
}
// NewLoRaInterface initializes a new LoRaInterface.
func NewLoRaInterface(name string, spi machine.SPI, cs, reset, dio0 machine.Pin, freq uint32, bw uint32, sf uint8, cr uint8, enabled bool) (*LoRaInterface, error) {
li := &LoRaInterface{
BaseInterface: NewBaseInterface(name, common.IF_TYPE_SERIAL, enabled),
spi: spi,
cs: cs,
reset: reset,
dio0: dio0,
freq: freq,
bw: bw,
sf: sf,
cr: cr,
txPower: 17,
done: make(chan struct{}),
}
li.MTU = MAX_PKT_LENGTH
li.Bitrate = int64(bw * uint32(sf) / (1 << (sf - 1)))
if enabled {
err := li.Start()
if err != nil {
return nil, err
}
}
return li, nil
}
// Start configures and brings the LoRaInterface online.
func (li *LoRaInterface) Start() error {
li.Mutex.Lock()
defer li.Mutex.Unlock()
if li.Online {
return nil
}
li.cs.Configure(machine.PinConfig{Mode: machine.PinOutput})
li.cs.High()
li.reset.Configure(machine.PinConfig{Mode: machine.PinOutput})
li.dio0.Configure(machine.PinConfig{Mode: machine.PinInput})
li.reset.Low()
time.Sleep(10 * time.Millisecond)
li.reset.High()
time.Sleep(10 * time.Millisecond)
version := li.readReg(REG_VERSION)
if version != 0x12 {
return fmt.Errorf("LoRa chip not found, version: 0x%02x", version)
}
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_SLEEP)
time.Sleep(10 * time.Millisecond)
frf := uint64(li.freq) << 19 / 32000000
li.writeReg(REG_FRF_MSB, uint8(frf>>16))
li.writeReg(REG_FRF_MID, uint8(frf>>8))
li.writeReg(REG_FRF_LSB, uint8(frf))
li.writeReg(REG_FIFO_TX_BASE_ADDR, 0)
li.writeReg(REG_FIFO_RX_BASE_ADDR, 0)
li.writeReg(0x0C, 0x23)
li.writeReg(REG_MODEM_CONFIG_3, 0x04)
li.writeReg(REG_PA_CONFIG, 0x80|(li.txPower-2))
var bwVal uint8
switch li.bw {
case 125000:
bwVal = 7
case 250000:
bwVal = 8
case 500000:
bwVal = 9
default:
bwVal = 7
}
li.writeReg(REG_MODEM_CONFIG_1, (bwVal<<4)|(li.cr-4)<<1|0x00)
li.writeReg(REG_MODEM_CONFIG_2, (li.sf<<4)|0x04)
li.writeReg(REG_SYNC_WORD, 0x12)
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_STDBY)
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_RX_CONTINUOUS)
li.Online = true
li.Enabled = true
go li.readLoop()
return nil
}
// readReg reads a byte from the given register.
func (li *LoRaInterface) readReg(reg uint8) uint8 {
li.cs.Low()
li.spi.Transfer(reg & 0x7F)
val, _ := li.spi.Transfer(0)
li.cs.High()
return val
}
// writeReg writes a byte to the given register.
func (li *LoRaInterface) writeReg(reg uint8, val uint8) {
li.cs.Low()
li.spi.Transfer(reg | 0x80)
li.spi.Transfer(val)
li.cs.High()
}
// readLoop polls the radio for received packets and dispatches them.
func (li *LoRaInterface) readLoop() {
for {
li.Mutex.RLock()
online := li.Online
done := li.done
li.Mutex.RUnlock()
if !online {
return
}
select {
case <-done:
return
default:
}
irq := li.readReg(REG_IRQ_FLAGS)
if irq&IRQ_RX_DONE_MASK != 0 {
li.writeReg(REG_IRQ_FLAGS, IRQ_RX_DONE_MASK)
if irq&IRQ_PAYLOAD_CRC_ERROR_MASK == 0 {
currentAddr := li.readReg(REG_FIFO_RX_CURRENT_ADDR)
li.writeReg(REG_FIFO_ADDR_PTR, currentAddr)
count := li.readReg(REG_RX_NB_BYTES)
packet := make([]byte, count)
li.cs.Low()
li.spi.Transfer(REG_FIFO)
for i := uint8(0); i < count; i++ {
packet[i], _ = li.spi.Transfer(0)
}
li.cs.High()
li.ProcessIncoming(packet)
}
}
time.Sleep(10 * time.Millisecond)
}
}
// Send transmits a packet over LoRa.
func (li *LoRaInterface) Send(data []byte, address string) error {
return li.ProcessOutgoing(data)
}
// ProcessOutgoing encodes and sends a packet.
func (li *LoRaInterface) ProcessOutgoing(data []byte) error {
li.Mutex.Lock()
defer li.Mutex.Unlock()
if !li.Online {
return fmt.Errorf("interface offline")
}
if len(data) > MAX_PKT_LENGTH {
return fmt.Errorf("packet too long for LoRa: %d", len(data))
}
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_STDBY)
li.writeReg(REG_FIFO_ADDR_PTR, 0)
li.cs.Low()
li.spi.Transfer(REG_FIFO | 0x80)
for _, b := range data {
li.spi.Transfer(b)
}
li.cs.High()
li.writeReg(REG_PAYLOAD_LENGTH, uint8(len(data)))
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_TX)
start := time.Now()
for {
if li.readReg(REG_IRQ_FLAGS)&IRQ_TX_DONE_MASK != 0 {
li.writeReg(REG_IRQ_FLAGS, IRQ_TX_DONE_MASK)
break
}
if time.Since(start) > 2*time.Second {
debug.Log(debug.DEBUG_ERROR, "LoRa TX timeout")
break
}
time.Sleep(1 * time.Millisecond)
}
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_RX_CONTINUOUS)
li.TxBytes += uint64(len(data))
li.lastTx = time.Now()
return nil
}
// Stop disables the LoRaInterface.
func (li *LoRaInterface) Stop() error {
li.Mutex.Lock()
li.Online = false
li.Enabled = false
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_SLEEP)
li.Mutex.Unlock()
li.stopOnce.Do(func() {
if li.done != nil {
close(li.done)
}
})
return nil
}

View File

@@ -1,262 +0,0 @@
// SPDX-License-Identifier: 0BSD
package interfaces
import (
"encoding/binary"
"fmt"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
)
const (
RNODE_CMD_DATA = 0x00
RNODE_CMD_FREQUENCY = 0x01
RNODE_CMD_BANDWIDTH = 0x02
RNODE_CMD_TXPOWER = 0x03
RNODE_CMD_SF = 0x04
RNODE_CMD_CR = 0x05
RNODE_CMD_RADIO_STATE = 0x06
RNODE_CMD_RADIO_LOCK = 0x07
RNODE_CMD_DETECT = 0x08
RNODE_CMD_LEAVE = 0x0A
RNODE_CMD_ST_ALOCK = 0x0B
RNODE_CMD_LT_ALOCK = 0x0C
RNODE_CMD_READY = 0x0F
RNODE_CMD_STAT_RX = 0x21
RNODE_CMD_STAT_TX = 0x22
RNODE_CMD_STAT_RSSI = 0x23
RNODE_CMD_STAT_SNR = 0x24
RNODE_CMD_FW_VERSION = 0x50
RNODE_CMD_PLATFORM = 0x48
RNODE_CMD_MCU = 0x49
RNODE_DETECT_REQ = 0x73
RNODE_DETECT_RESP = 0x46
RNODE_RSSI_OFFSET = 157
)
// RNodeInterface represents a Reticulum node interface.
type RNodeInterface struct {
Interface
frequency uint32
bandwidth uint32
sf uint8
cr uint8
txPower uint8
callback common.PacketCallback
rFrequency uint32
rBandwidth uint32
rTXPower uint8
rSF uint8
rCR uint8
rState uint8
rDetected bool
rMajVer uint8
rMinVer uint8
interfaceReady bool
packetQueue [][]byte
}
// NewRNodeInterface creates a new RNodeInterface.
func NewRNodeInterface(name string, underlying Interface, freq uint32, bw uint32, sf uint8, cr uint8, txPower uint8) (*RNodeInterface, error) {
ri := &RNodeInterface{
Interface: underlying,
frequency: freq,
bandwidth: bw,
sf: sf,
cr: cr,
txPower: txPower,
}
underlying.SetPacketCallback(ri.handleIncoming)
return ri, nil
}
// SetPacketCallback sets the packet callback for the RNodeInterface.
func (ri *RNodeInterface) SetPacketCallback(cb common.PacketCallback) {
ri.callback = cb
}
func (ri *RNodeInterface) handleIncoming(data []byte, ni common.NetworkInterface) {
if len(data) < 1 {
return
}
cmd := data[0]
payload := data[1:]
switch cmd {
case RNODE_CMD_DATA:
if ri.callback != nil {
ri.callback(payload, ri)
}
case RNODE_CMD_READY:
ri.processQueue()
case RNODE_CMD_DETECT:
if len(payload) >= 1 && payload[0] == RNODE_DETECT_RESP {
ri.rDetected = true
}
case RNODE_CMD_FW_VERSION:
if len(payload) >= 2 {
ri.rMajVer = payload[0]
ri.rMinVer = payload[1]
debug.Log(debug.DEBUG_INFO, "RNode firmware version", "name", ri.GetName(), "version", fmt.Sprintf("%d.%d", ri.rMajVer, ri.rMinVer))
}
case RNODE_CMD_FREQUENCY:
if len(payload) >= 4 {
ri.rFrequency = binary.BigEndian.Uint32(payload)
}
case RNODE_CMD_BANDWIDTH:
if len(payload) >= 4 {
ri.rBandwidth = binary.BigEndian.Uint32(payload)
}
case RNODE_CMD_TXPOWER:
if len(payload) >= 1 {
ri.rTXPower = payload[0]
}
case RNODE_CMD_SF:
if len(payload) >= 1 {
ri.rSF = payload[0]
}
case RNODE_CMD_CR:
if len(payload) >= 1 {
ri.rCR = payload[0]
}
case RNODE_CMD_RADIO_STATE:
if len(payload) >= 1 {
ri.rState = payload[0]
}
case RNODE_CMD_STAT_RSSI:
if len(payload) >= 1 {
rssi := int(payload[0]) - RNODE_RSSI_OFFSET
debug.Log(debug.DEBUG_VERBOSE, "RNode RSSI", "name", ri.GetName(), "rssi", rssi)
}
case RNODE_CMD_STAT_SNR:
if len(payload) >= 1 {
snr := float32(int8(payload[0])) * 0.25
debug.Log(debug.DEBUG_VERBOSE, "RNode SNR", "name", ri.GetName(), "snr", snr)
}
default:
debug.Log(debug.DEBUG_ALL, "RNode received command", "cmd", fmt.Sprintf("0x%02x", cmd), "len", len(payload))
}
}
func (ri *RNodeInterface) processQueue() {
ri.interfaceReady = true
if len(ri.packetQueue) > 0 {
packet := ri.packetQueue[0]
ri.packetQueue = ri.packetQueue[1:]
_ = ri.Send(packet, "")
}
}
// Start initializes the RNodeInterface and configures device parameters.
func (ri *RNodeInterface) Start() error {
err := ri.Interface.Start()
if err != nil {
return err
}
time.Sleep(2 * time.Second)
if err := ri.detect(); err != nil {
return err
}
debug.Log(debug.DEBUG_INFO, "Initializing RNode...", "name", ri.GetName())
if ri.frequency != 0 {
freqBytes := make([]byte, 4)
binary.BigEndian.PutUint32(freqBytes, ri.frequency)
if err := ri.sendRNodeCommand(RNODE_CMD_FREQUENCY, freqBytes); err != nil {
return err
}
}
if ri.bandwidth != 0 {
bwBytes := make([]byte, 4)
binary.BigEndian.PutUint32(bwBytes, ri.bandwidth)
if err := ri.sendRNodeCommand(RNODE_CMD_BANDWIDTH, bwBytes); err != nil {
return err
}
}
if ri.sf != 0 {
if err := ri.sendRNodeCommand(RNODE_CMD_SF, []byte{ri.sf}); err != nil {
return err
}
}
if ri.cr != 0 {
if err := ri.sendRNodeCommand(RNODE_CMD_CR, []byte{ri.cr}); err != nil {
return err
}
}
if ri.txPower != 0 {
if err := ri.sendRNodeCommand(RNODE_CMD_TXPOWER, []byte{ri.txPower}); err != nil {
return err
}
}
if err := ri.sendRNodeCommand(RNODE_CMD_RADIO_STATE, []byte{0x01}); err != nil {
return err
}
ri.interfaceReady = true
debug.Log(debug.DEBUG_INFO, "RNode initialized", "name", ri.GetName())
return nil
}
// detect attempts to detect the RNode device and obtain firmware version.
func (ri *RNodeInterface) detect() error {
detectCmd := []byte{RNODE_DETECT_REQ}
if err := ri.sendRNodeCommand(RNODE_CMD_DETECT, detectCmd); err != nil {
return err
}
start := time.Now()
for !ri.rDetected {
if time.Since(start) > 2*time.Second {
debug.Log(debug.DEBUG_ERROR, "RNode detection timed out", "name", ri.GetName())
break
}
time.Sleep(100 * time.Millisecond)
}
if err := ri.sendRNodeCommand(RNODE_CMD_FW_VERSION, []byte{0x00}); err != nil {
return err
}
return nil
}
// sendRNodeCommand sends a command to the RNode device.
func (ri *RNodeInterface) sendRNodeCommand(cmd byte, data []byte) error {
if kissInterface, ok := ri.Interface.(interface {
SendKISS(byte, []byte) error
}); ok {
return kissInterface.SendKISS(cmd, data)
}
frame := make([]byte, 0, len(data)+1)
frame = append(frame, cmd)
frame = append(frame, data...)
return ri.Interface.Send(frame, "")
}
// Send transmits data using the underlying interface.
func (ri *RNodeInterface) Send(data []byte, addr string) error {
if !ri.interfaceReady {
ri.packetQueue = append(ri.packetQueue, data)
return nil
}
return ri.Interface.Send(data, addr)
}

View File

@@ -1,29 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build !tinygo
package interfaces
import (
"fmt"
)
type SerialInterface struct {
BaseInterface
}
func NewSerialInterface(name string, portName string, baud uint32, enabled bool) (*SerialInterface, error) {
return nil, fmt.Errorf("SerialInterface is only supported on TinyGo targets currently")
}
func (si *SerialInterface) Start() error {
return fmt.Errorf("SerialInterface is only supported on TinyGo targets currently")
}
func (si *SerialInterface) Stop() error {
return nil
}
func (si *SerialInterface) Send(data []byte, address string) error {
return fmt.Errorf("SerialInterface is only supported on TinyGo targets currently")
}

View File

@@ -1,221 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build tinygo
package interfaces
import (
"fmt"
"machine"
"sync"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
)
const (
SERIAL_DEFAULT_BAUD = 115200
SERIAL_MTU = 1500
)
// SerialInterface implements a serial interface using TinyGo UART.
type SerialInterface struct {
BaseInterface
uart *machine.UART
baud uint32
done chan struct{}
stopOnce sync.Once
}
// NewSerialInterface creates and initializes a new SerialInterface.
func NewSerialInterface(name string, portName string, baud uint32, enabled bool) (*SerialInterface, error) {
if baud == 0 {
baud = SERIAL_DEFAULT_BAUD
}
uart, err := getUART(portName)
if err != nil {
return nil, err
}
si := &SerialInterface{
BaseInterface: NewBaseInterface(name, common.IF_TYPE_SERIAL, enabled),
uart: uart,
baud: baud,
done: make(chan struct{}),
}
si.MTU = SERIAL_MTU
si.Bitrate = int64(baud)
if enabled {
err := si.Start()
if err != nil {
return nil, err
}
}
return si, nil
}
// getUART returns a TinyGo UART handle by name or index.
func getUART(name string) (*machine.UART, error) {
switch name {
case "UART0", "0":
return machine.UART0, nil
case "UART1", "1":
return machine.UART1, nil
case "UART2", "2":
return machine.UART2, nil
default:
if name == "" {
return machine.UART0, nil
}
return nil, fmt.Errorf("unknown UART: %s", name)
}
}
// Start enables the serial interface and starts the read loop.
func (si *SerialInterface) Start() error {
si.Mutex.Lock()
defer si.Mutex.Unlock()
if si.Online {
return nil
}
err := si.uart.Configure(machine.UARTConfig{
BaudRate: si.baud,
})
if err != nil {
return fmt.Errorf("failed to configure UART: %w", err)
}
si.Online = true
si.Enabled = true
go si.readLoop()
return nil
}
// Stop disables the serial interface.
func (si *SerialInterface) Stop() error {
si.Mutex.Lock()
si.Online = false
si.Enabled = false
si.Mutex.Unlock()
si.stopOnce.Do(func() {
if si.done != nil {
close(si.done)
}
})
return nil
}
// readLoop reads and processes frames from the UART, handling KISS framing.
func (si *SerialInterface) readLoop() {
buffer := make([]byte, si.MTU)
dataBuffer := make([]byte, 0, si.MTU)
inFrame := false
escape := false
for {
si.Mutex.RLock()
online := si.Online
done := si.done
si.Mutex.RUnlock()
if !online {
return
}
select {
case <-done:
return
default:
}
if si.uart.Buffered() > 0 {
n, err := si.uart.Read(buffer)
if err != nil {
debug.Log(debug.DEBUG_ERROR, "Serial read error", "name", si.Name, "error", err)
time.Sleep(100 * time.Millisecond)
continue
}
if n > 0 {
for i := 0; i < n; i++ {
b := buffer[i]
if b == KISS_FEND {
if inFrame && len(dataBuffer) > 0 {
packet := make([]byte, len(dataBuffer))
copy(packet, dataBuffer)
si.ProcessIncoming(packet)
dataBuffer = dataBuffer[:0]
}
inFrame = true
escape = false
continue
}
if inFrame {
if b == KISS_FESC {
escape = true
} else {
if escape {
if b == KISS_TFEND {
b = KISS_FEND
} else if b == KISS_TFESC {
b = KISS_FESC
}
escape = false
}
dataBuffer = append(dataBuffer, b)
}
}
}
}
} else {
time.Sleep(10 * time.Millisecond)
}
}
}
// Send transmits data using KISS protocol with the default command 0x00.
func (si *SerialInterface) Send(data []byte, address string) error {
return si.SendKISS(0x00, data)
}
// SendKISS sends a KISS-encoded packet over the serial UART.
func (si *SerialInterface) SendKISS(command byte, data []byte) error {
si.Mutex.RLock()
online := si.Online
si.Mutex.RUnlock()
if !online {
return fmt.Errorf("interface offline")
}
frame := make([]byte, 0, len(data)*2+3)
frame = append(frame, KISS_FEND)
frame = append(frame, command)
frame = append(frame, escapeKISS(data)...)
frame = append(frame, KISS_FEND)
_, err := si.uart.Write(frame)
if err != nil {
return err
}
si.Mutex.Lock()
si.TxBytes += uint64(len(frame))
si.lastTx = time.Now()
si.Mutex.Unlock()
return nil
}

View File

@@ -18,6 +18,11 @@ const (
HDLC_ESC = 0x7D
HDLC_ESC_MASK = 0x20
KISS_FEND = 0xC0
KISS_FESC = 0xDB
KISS_TFEND = 0xDC
KISS_TFESC = 0xDD
DEFAULT_MTU = 1064
BITRATE_GUESS_VAL = 10 * 1000 * 1000
RECONNECT_WAIT = 5
@@ -162,6 +167,40 @@ func (tc *TCPClientInterface) Stop() error {
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() {
buffer := make([]byte, tc.MTU)
inFrame := false
@@ -200,8 +239,6 @@ func (tc *TCPClientInterface) readLoop() {
return
}
tc.UpdateStats(uint64(n), true) // #nosec G115
for i := 0; i < n; i++ {
b := buffer[i]
@@ -236,70 +273,13 @@ func (tc *TCPClientInterface) handlePacket(data []byte) {
}
tc.Mutex.Lock()
tc.RxBytes += uint64(len(data))
lastRx := time.Now()
tc.lastRx = lastRx
callback := tc.packetCallback
tc.Mutex.Unlock()
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
if 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 {
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)
tc.UpdateStats(uint64(len(frame)), false) // #nosec G115
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
tc.ProcessIncoming(data)
}
func (tc *TCPClientInterface) teardown() {
@@ -324,6 +304,20 @@ func escapeHDLC(data []byte) []byte {
return escaped
}
func escapeKISS(data []byte) []byte {
escaped := make([]byte, 0, len(data)*2)
for _, b := range data {
if b == KISS_FEND {
escaped = append(escaped, KISS_FESC, KISS_TFEND)
} else if b == KISS_FESC {
escaped = append(escaped, KISS_FESC, KISS_TFESC)
} else {
escaped = append(escaped, b)
}
}
return escaped
}
func (tc *TCPClientInterface) SetPacketCallback(cb common.PacketCallback) {
tc.packetCallback = cb
}
@@ -453,40 +447,6 @@ func (tc *TCPClientInterface) GetRTT() time.Duration {
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 {
BaseInterface
connections map[string]net.Conn
@@ -667,18 +627,6 @@ func (ts *TCPServerInterface) Stop() error {
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) {
addr := conn.RemoteAddr().String()
ts.Mutex.Lock()
@@ -709,14 +657,7 @@ func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
return
}
ts.Mutex.Lock()
ts.RxBytes += uint64(n) // #nosec G115
callback := ts.packetCallback
ts.Mutex.Unlock()
if callback != nil {
callback(buffer[:n], ts)
}
ts.ProcessIncoming(buffer[:n])
}
}
@@ -739,7 +680,6 @@ func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
}
ts.Mutex.Lock()
ts.TxBytes += uint64(len(frame)) // #nosec G115
conns := make([]net.Conn, 0, len(ts.connections))
for _, conn := range ts.connections {
conns = append(conns, conn)

View File

@@ -1,7 +1,7 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build !linux || tinygo
// +build !linux tinygo
//go:build !linux
// +build !linux
package interfaces
@@ -14,11 +14,3 @@ import (
func platformGetRTT(fd uintptr) time.Duration {
return 0
}
func (tc *TCPClientInterface) setTimeoutsLinux() error {
return nil
}
func (tc *TCPClientInterface) setTimeoutsOSX() error {
return nil
}

View File

@@ -1,7 +1,7 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build linux && !tinygo
// +build linux,!tinygo
//go:build linux
// +build linux
package interfaces

View File

@@ -1,8 +1,5 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build !tinygo
// +build !tinygo
package interfaces
import (
@@ -51,6 +48,30 @@ func NewUDPInterface(name string, addr string, target string, enabled bool) (*UD
return ui, nil
}
func (ui *UDPInterface) GetName() string {
return ui.Name
}
func (ui *UDPInterface) GetType() common.InterfaceType {
return ui.Type
}
func (ui *UDPInterface) GetMode() common.InterfaceMode {
return ui.Mode
}
func (ui *UDPInterface) IsOnline() bool {
ui.Mutex.RLock()
defer ui.Mutex.RUnlock()
return ui.Online
}
func (ui *UDPInterface) IsDetached() bool {
ui.Mutex.RLock()
defer ui.Mutex.RUnlock()
return ui.Detached
}
func (ui *UDPInterface) Detach() {
ui.Mutex.Lock()
defer ui.Mutex.Unlock()
@@ -66,28 +87,22 @@ func (ui *UDPInterface) Detach() {
})
}
func (ui *UDPInterface) Send(data []byte, addr string) error {
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")
}
func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) {
ui.Mutex.Lock()
ui.TxBytes += uint64(len(data))
ui.Mutex.Unlock()
defer ui.Mutex.Unlock()
ui.packetCallback = callback
}
_, 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))
func (ui *UDPInterface) GetPacketCallback() common.PacketCallback {
ui.Mutex.RLock()
defer ui.Mutex.RUnlock()
return ui.packetCallback
}
func (ui *UDPInterface) ProcessIncoming(data []byte) {
if callback := ui.GetPacketCallback(); callback != nil {
callback(data, ui)
}
return err
}
func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
@@ -95,15 +110,15 @@ func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
return fmt.Errorf("interface offline")
}
_, err := ui.conn.Write(data)
if ui.targetAddr == nil {
return fmt.Errorf("no target address configured")
}
_, err := ui.conn.WriteToUDP(data, ui.targetAddr)
if err != nil {
return fmt.Errorf("UDP write failed: %v", err)
}
ui.Mutex.Lock()
ui.TxBytes += uint64(len(data))
ui.Mutex.Unlock()
return nil
}
@@ -111,6 +126,38 @@ func (ui *UDPInterface) GetConn() net.Conn {
return ui.conn
}
func (ui *UDPInterface) GetTxBytes() uint64 {
ui.Mutex.RLock()
defer ui.Mutex.RUnlock()
return ui.TxBytes
}
func (ui *UDPInterface) GetRxBytes() uint64 {
ui.Mutex.RLock()
defer ui.Mutex.RUnlock()
return ui.RxBytes
}
func (ui *UDPInterface) GetMTU() int {
return ui.MTU
}
func (ui *UDPInterface) GetBitrate() int {
return int(ui.Bitrate)
}
func (ui *UDPInterface) Enable() {
ui.Mutex.Lock()
defer ui.Mutex.Unlock()
ui.Online = true
}
func (ui *UDPInterface) Disable() {
ui.Mutex.Lock()
defer ui.Mutex.Unlock()
ui.Online = false
}
func (ui *UDPInterface) Start() error {
ui.Mutex.Lock()
if ui.conn != nil {
@@ -194,19 +241,19 @@ func (ui *UDPInterface) readLoop() {
}
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
if ui.targetAddr == nil {
debug.Log(debug.DEBUG_ALL, "UDP interface discovered peer", "name", ui.Name, "peer", remoteAddr.String())
ui.targetAddr = remoteAddr
}
callback := ui.packetCallback
ui.Mutex.Unlock()
if callback != nil {
callback(buffer[:n], ui)
}
ui.ProcessIncoming(buffer[:n])
}
}
func (ui *UDPInterface) IsEnabled() bool {
ui.Mutex.RLock()
defer ui.Mutex.RUnlock()
return ui.Enabled && ui.Online && !ui.Detached
}

View File

@@ -1,68 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build tinygo
// +build tinygo
package interfaces
import (
"fmt"
"net"
"sync"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
)
type UDPInterface struct {
BaseInterface
conn net.Conn
addr *net.UDPAddr
targetAddr *net.UDPAddr
readBuffer []byte
done chan struct{}
stopOnce sync.Once
}
func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) {
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
var targetAddr *net.UDPAddr
if target != "" {
targetAddr, err = net.ResolveUDPAddr("udp", target)
if err != nil {
return nil, err
}
}
ui := &UDPInterface{
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
addr: udpAddr,
targetAddr: targetAddr,
readBuffer: make([]byte, common.NUM_1064),
done: make(chan struct{}),
}
ui.MTU = common.NUM_1064
return ui, nil
}
func (ui *UDPInterface) Start() error {
// TinyGo doesn't support UDP servers, only clients
return fmt.Errorf("UDPInterface not supported in TinyGo - UDP server functionality requires net.ListenUDP")
}
func (ui *UDPInterface) Send(data []byte, addr string) error {
// TinyGo doesn't support UDP sending
return fmt.Errorf("UDPInterface Send not supported in TinyGo - requires UDP client functionality")
}
func (ui *UDPInterface) Stop() error {
ui.Mutex.Lock()
defer ui.Mutex.Unlock()
ui.Online = false
return nil
}

View File

@@ -92,7 +92,12 @@ func (wsi *WebSocketInterface) Start() error {
defer wsi.Mutex.Unlock()
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)
@@ -127,30 +132,39 @@ func (wsi *WebSocketInterface) Start() error {
event := args[0]
data := event.Get("data")
var packet []byte
if data.Type() == js.TypeString {
packet = []byte(data.String())
} else if data.Type() == js.TypeObject {
array := js.Global().Get("Uint8Array").New(data)
handlePacket := func(buf js.Value) {
array := js.Global().Get("Uint8Array").New(buf)
length := array.Get("length").Int()
packet = make([]byte, length)
if length < 1 {
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 {
debug.Log(debug.DEBUG_ERROR, "Unknown WebSocket message type", "type", data.Type().String())
return nil
}
if len(packet) < 1 {
debug.Log(debug.DEBUG_ERROR, "WebSocket message empty")
return nil
}
wsi.Mutex.Lock()
wsi.RxBytes += uint64(len(packet))
wsi.Mutex.Unlock()
wsi.ProcessIncoming(packet)
return nil
}))
@@ -168,8 +182,10 @@ func (wsi *WebSocketInterface) Start() error {
debug.Log(debug.DEBUG_INFO, "WebSocket closed", "name", wsi.Name)
if wsi.Enabled && !wsi.Detached {
time.Sleep(WS_RECONNECT_DELAY)
go wsi.Start()
go func() {
time.Sleep(WS_RECONNECT_DELAY)
_ = wsi.Start()
}()
}
return nil
@@ -197,15 +213,7 @@ func (wsi *WebSocketInterface) closeWebSocket() {
wsi.Online = false
}
func (wsi *WebSocketInterface) Send(data []byte, addr string) error {
if !wsi.IsEnabled() {
return fmt.Errorf("interface not enabled")
}
wsi.Mutex.Lock()
wsi.TxBytes += uint64(len(data))
wsi.Mutex.Unlock()
func (wsi *WebSocketInterface) ProcessOutgoing(data []byte) error {
if !wsi.connected {
wsi.Mutex.Lock()
wsi.messageQueue = append(wsi.messageQueue, data)
@@ -234,10 +242,6 @@ func (wsi *WebSocketInterface) sendWebSocketMessage(data []byte) error {
return nil
}
func (wsi *WebSocketInterface) ProcessOutgoing(data []byte) error {
return wsi.Send(data, "")
}
func (wsi *WebSocketInterface) GetConn() net.Conn {
return nil
}

View File

@@ -28,6 +28,7 @@ import (
"git.quad4.io/Networks/Reticulum-Go/pkg/resolver"
"git.quad4.io/Networks/Reticulum-Go/pkg/resource"
"git.quad4.io/Networks/Reticulum-Go/pkg/transport"
"github.com/vmihailenco/msgpack/v5"
)
const (
@@ -307,7 +308,7 @@ func (l *Link) Request(path string, data []byte, timeout time.Duration) (*Reques
pathHash := identity.TruncatedHash([]byte(path))
requestData := []interface{}{time.Now().Unix(), pathHash, data}
packedRequest, err := common.MsgpackMarshal(requestData)
packedRequest, err := msgpack.Marshal(requestData)
if err != nil {
return nil, fmt.Errorf("failed to pack request: %w", err)
}
@@ -1028,7 +1029,7 @@ func (l *Link) handleRequest(plaintext []byte, pkt *packet.Packet) error {
}
var requestData []interface{}
if err := common.MsgpackUnmarshal(plaintext, &requestData); err != nil {
if err := msgpack.Unmarshal(plaintext, &requestData); err != nil {
return fmt.Errorf("failed to unpack request: %w", err)
}
@@ -1059,7 +1060,7 @@ func (l *Link) handleRequest(plaintext []byte, pkt *packet.Packet) error {
case string:
requestPayload = []byte(payload)
default:
packed, err := common.MsgpackMarshal(payload)
packed, err := msgpack.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to pack request_payload: %w", err)
}
@@ -1088,7 +1089,7 @@ func (l *Link) handleRequest(plaintext []byte, pkt *packet.Packet) error {
func (l *Link) handleResponse(plaintext []byte) error {
var responseData []interface{}
if err := common.MsgpackUnmarshal(plaintext, &responseData); err != nil {
if err := msgpack.Unmarshal(plaintext, &responseData); err != nil {
return fmt.Errorf("failed to unpack response: %w", err)
}
@@ -1123,7 +1124,7 @@ func (l *Link) handleResponse(plaintext []byte) error {
func (l *Link) sendResponse(requestID []byte, response interface{}) error {
responseData := []interface{}{requestID, response}
packedResponse, err := common.MsgpackMarshal(responseData)
packedResponse, err := msgpack.Marshal(responseData)
if err != nil {
return fmt.Errorf("failed to pack response: %w", err)
}

View File

@@ -144,8 +144,6 @@ func (p *Packet) Pack() error {
header := []byte{flags, 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.TransportID == nil {
return errors.New("transport ID required for header type 2")
@@ -154,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))
}
header = append(header, p.DestinationHash...)
header = append(header, p.Context)
debug.Log(debug.DEBUG_PACKETS, "Final header length", "bytes", len(header))
@@ -187,12 +187,12 @@ func (p *Packet) Unpack() error {
dstLen := 16 // Truncated hash length
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 {
return errors.New("packet too short for header type 2")
}
p.DestinationHash = p.Raw[2 : dstLen+2] // Destination hash first
p.TransportID = p.Raw[dstLen+2 : 2*dstLen+2] // Transport ID second
p.TransportID = p.Raw[2 : dstLen+2] // Transport ID first
p.DestinationHash = p.Raw[dstLen+2 : 2*dstLen+2] // Destination hash second
p.Context = p.Raw[2*dstLen+2]
p.Data = p.Raw[2*dstLen+3:]
} else {

View 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()
})
}

View File

@@ -6,7 +6,7 @@ import (
"fmt"
"math"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"github.com/vmihailenco/msgpack/v5"
)
const (
@@ -117,12 +117,12 @@ func (ra *ResourceAdvertisement) Pack(segment int) ([]byte, error) {
"m": hashmap,
}
return common.MsgpackMarshal(dict)
return msgpack.Marshal(dict)
}
func UnpackResourceAdvertisement(data []byte) (*ResourceAdvertisement, error) {
var dict map[string]interface{}
if err := common.MsgpackUnmarshal(data, &dict); err != nil {
if err := msgpack.Unmarshal(data, &dict); err != nil {
return nil, fmt.Errorf("failed to unpack advertisement: %w", err)
}

View 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)
}
}

View 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")
}

View File

@@ -18,7 +18,6 @@ import (
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
"git.quad4.io/Networks/Reticulum-Go/pkg/destination"
"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/pathfinder"
"git.quad4.io/Networks/Reticulum-Go/pkg/rate"
@@ -324,6 +323,12 @@ func GetTransportInstance() *Transport {
return transportInstance
}
func SetTransportInstance(t *Transport) {
transportMutex.Lock()
defer transportMutex.Unlock()
transportInstance = t
}
func (t *Transport) RegisterInterface(name string, iface common.NetworkInterface) error {
t.mutex.Lock()
defer t.mutex.Unlock()
@@ -577,8 +582,11 @@ func (t *Transport) RequestPath(destinationHash []byte, onInterface string, tag
pathRequestData = append(destinationHash, tag...)
}
destHashFull := sha256.Sum256([]byte("rnstransport.path.request"))
pathRequestDestHash := destHashFull[:common.SIZE_16]
pathRequestName := "rnstransport.path.request"
nameHashFull := sha256.Sum256([]byte(pathRequestName))
nameHash10 := nameHashFull[:10]
finalHashFull := sha256.Sum256(nameHash10)
pathRequestDestHash := finalHashFull[:16]
pkt := packet.NewPacket(
packet.DestinationPlain,
@@ -586,11 +594,12 @@ func (t *Transport) RequestPath(destinationHash []byte, onInterface string, tag
0x00,
0x00,
packet.PropagationBroadcast,
0x01,
pathRequestDestHash,
0x00, // Header Type 1
nil,
false,
0x00,
)
pkt.DestinationHash = pathRequestDestHash
if err := pkt.Pack(); err != nil {
return fmt.Errorf("failed to pack path request: %w", err)
@@ -881,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]))
}
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))
copy(dataCopy, data)
@@ -1110,16 +1114,15 @@ func (t *Transport) handleAnnouncePacket(data []byte, iface common.NetworkInterf
// Register the path from this announce
// The destination is reachable via the interface that received this announce
if iface != nil {
// Use unlocked version since we may be called in a locked context
t.mutex.Lock()
t.updatePathUnlocked(destinationHash, nil, iface.GetName(), hopCount)
t.updatePathUnlocked(destinationHash, nil, iface.GetName(), hopCount+1)
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
debug.Log(debug.DEBUG_INFO, "Notifying announce handlers", "destHash", fmt.Sprintf("%x", destinationHash), "appDataLen", len(appData))
t.notifyAnnounceHandlers(destinationHash, id, appData, hopCount)
t.notifyAnnounceHandlers(destinationHash, id, appData, hopCount+1)
debug.Log(debug.DEBUG_INFO, "Announce handlers notified")
// Don't forward if max hops reached
@@ -1376,7 +1379,7 @@ func (t *Transport) InitializePathRequestHandler() error {
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 {
return fmt.Errorf("failed to create path request destination: %w", err)
}
@@ -1692,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 {
t.mutex.Lock()
defer t.mutex.Unlock()

View File

@@ -3,8 +3,12 @@ package transport
import (
"bytes"
"testing"
"time"
"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 {
@@ -118,3 +122,68 @@ func TestTransportStatus(t *testing.T) {
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)
}
}

View File

@@ -24,10 +24,12 @@ var (
reticulumDest *destination.Destination
reticulumIdentity *identity.Identity
stats = struct {
packetsSent int
packetsReceived int
bytesSent int
bytesReceived int
packetsSent int
packetsReceived int
bytesSent int
bytesReceived int
announcesSent int
announcesReceived int
}{}
packetCallback js.Value
announceHandler js.Value
@@ -47,6 +49,7 @@ func RegisterJSFunctions() {
"setPacketCallback": js.FuncOf(SetPacketCallback),
"setAnnounceCallback": js.FuncOf(SetAnnounceCallback),
"sendData": js.FuncOf(SendDataJS),
"sendMessage": js.FuncOf(SendDataJS),
"announce": js.FuncOf(SendAnnounceJS),
}))
}
@@ -100,11 +103,31 @@ func RequestPath(this js.Value, args []js.Value) interface{} {
}
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,
"packetsSent": stats.packetsSent,
"packetsReceived": stats.packetsReceived,
"bytesSent": stats.bytesSent,
"bytesReceived": stats.bytesReceived,
"announcesSent": stats.announcesSent,
"announcesReceived": stats.announcesReceived,
})
}
@@ -154,6 +177,14 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
cfg := common.DefaultConfig()
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(
id,
@@ -170,9 +201,6 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
}
dest.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
stats.packetsReceived++
stats.bytesReceived += len(data)
if !packetCallback.IsUndefined() {
// Convert bytes to JS Uint8Array for performance and compatibility
uint8Array := js.Global().Get("Uint8Array").New(len(data))
@@ -192,12 +220,8 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
})
}
wsInterface.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
msg := fmt.Sprintf("Received packet: %d bytes (type: 0x%02x)", len(data), data[0])
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)
})
// Wire the interface to the transport
wsInterface.SetPacketCallback(t.HandlePacket)
if err := t.RegisterInterface("wasm0", wsInterface); err != nil {
return js.ValueOf(map[string]interface{}{
@@ -338,6 +362,8 @@ func (h *genericAnnounceHandler) ReceivePathResponses() bool {
}
func (h *genericAnnounceHandler) ReceivedAnnounce(destHash []byte, ident interface{}, appData []byte, hops uint8) error {
debug.Log(debug.DEBUG_INFO, "WASM Announce Handler received announce", "dest", hex.EncodeToString(destHash), "hops", hops)
stats.announcesReceived++
if !announceHandler.IsUndefined() {
hashStr := hex.EncodeToString(destHash)
announceHandler.Invoke(js.ValueOf(map[string]interface{}{
@@ -431,9 +457,6 @@ func SendData(destHash []byte, data []byte) interface{} {
})
}
stats.packetsSent++
stats.bytesSent += len(data)
return js.ValueOf(map[string]interface{}{
"success": true,
})
@@ -469,6 +492,8 @@ func SendAnnounce(appData []byte) interface{} {
})
}
stats.announcesSent++
return js.ValueOf(map[string]interface{}{
"success": true,
})