31 Commits

Author SHA1 Message Date
6ae409c1db fix: add #nosec annotation for Bitrate conversion in SerialInterface initialization
All checks were successful
TinyGo Build / tinygo-build-all (push) Successful in 10m10s
2026-01-01 01:02:13 -06:00
bfa0669143 style: format code across all files 2026-01-01 01:01:27 -06:00
2867f68a90 refactor: replace msgpack calls with common package methods in advertisement.go for packing and unpacking 2026-01-01 01:00:06 -06:00
fbb48f2295 refactor: replace msgpack calls with common package methods in link.go for request and response handling 2026-01-01 01:00:02 -06:00
132872c2d6 refactor: remove KISS protocol escape functions and related constants from tcp.go 2026-01-01 00:59:57 -06:00
1d3590985e feat: implement SerialInterface for TinyGo with UART communication and KISS protocol handling 2026-01-01 00:59:52 -06:00
ae3f93a3bf feat: add SerialInterface stub with error handling for non-TinyGo targets 2026-01-01 00:59:47 -06:00
cbf2eaea78 feat: implement RNodeInterface for Reticulum with command handling and device initialization 2026-01-01 00:59:39 -06:00
1133a918f1 feat: add LoRaInterface implementation for TinyGo with SPI communication and packet handling 2026-01-01 00:59:31 -06:00
f5bb6a2b6d feat: introduce LoRaInterface stub with error handling for non-TinyGo targets 2026-01-01 00:59:27 -06:00
718f550180 feat: add KISS protocol escape functions in new kiss.go interface 2026-01-01 00:59:24 -06:00
07dc008a31 refactor: update Identity methods to use common package for Msgpack serialization 2026-01-01 00:59:20 -06:00
cdc512c391 refactor: replace msgpack calls with common package methods for ratchet data serialization 2026-01-01 00:59:16 -06:00
1df3e41191 feat: implement MsgpackMarshal and MsgpackUnmarshal functions for MessagePack encoding and decoding 2026-01-01 00:59:12 -06:00
dea65ad94c feat: add new configuration fields for Frequency, Bandwidth, SF, CR, and TXPower in InterfaceConfig 2026-01-01 00:59:08 -06:00
4c9a395ff2 refactor: replace msgpack functions with common package equivalents for ratchet data serialization 2026-01-01 00:59:03 -06:00
09eec1c8fc feat: add SerialInterface and RNodeInterface support in Reticulum configuration 2026-01-01 00:58:59 -06:00
fbaafacd8a refactor: update tinygo-build-all task to build targets in parallel using xargs 2026-01-01 00:58:55 -06:00
40e7a168ec chore: update msgpack dependency from vmihailenco to shamaton and remove unused tagparser dependency 2026-01-01 00:58:31 -06:00
d6e0874f05 Add missing TCP timeout stubs for TinyGo
All checks were successful
TinyGo Build / tinygo-build-all (push) Successful in 2m39s
2025-12-31 22:54:29 -06:00
9fa05cce5c Merge main into tinygo and fix conflicts 2025-12-31 22:53:35 -06:00
8725c68b24 update: tinygo workflow
Some checks failed
TinyGo Build / tinygo-build-all (push) Failing after 1h7m54s
2025-11-10 10:17:44 -06:00
07512a3c88 update 2025-11-10 10:15:42 -06:00
6217a889e1 update Makefile with tinygo build options
Some checks failed
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (push) Failing after 1m21s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (push) Failing after 2m2s
2025-11-10 10:11:45 -06:00
92e4651c0c update build constraints for TCP interfaces to support TinyGo 2025-11-10 10:11:06 -06:00
ivan
a01bad919c Merge pull request 'Update TinyGo branch with debug logging' (#2) from main into tinygo
Reviewed-on: #2
2025-11-07 18:53:54 +00:00
ivan
cfc88e887b Merge pull request 'update tinygo' (#1) from main into tinygo
Reviewed-on: #1
2025-11-06 21:16:11 +00:00
d325ee6a2d Update UDPInterface Send method to return an error for unsupported UDP functionality in TinyGo 2025-10-30 19:08:42 -05:00
a45ad400f9 Fixes 2025-10-30 19:05:20 -05:00
f599cc4d43 Update AutoInterface and UDPInterface to use net.PacketConn for better compatibility with TinyGo. Update methods to handle errors related to unsupported features in TinyGo, including interface enumeration and multicast UDP. 2025-10-30 19:01:33 -05:00
1b3f4e59db Update GitHub Actions workflow to trigger on 'tinygo' branch for builds 2025-10-30 19:01:18 -05:00
44 changed files with 1631 additions and 1018 deletions

View File

@@ -1,29 +0,0 @@
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

@@ -1,55 +0,0 @@
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

105
.gitea/workflows/build.yml Normal file
View File

@@ -0,0 +1,105 @@
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,20 +65,41 @@ 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

@@ -1,97 +0,0 @@
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,29 +5,15 @@ on:
branches: [ "tinygo" ]
pull_request:
branches: [ "tinygo" ]
workflow_dispatch:
jobs:
tinygo-build:
tinygo-build-all:
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
@@ -37,28 +23,64 @@ 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 with TinyGo
- name: Build for all TinyGo targets
id: build_step
run: |
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
task tinygo-build-all || true
echo "Build process completed (some targets may have failed)"
- name: Upload Artifact
- 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
uses: https://git.quad4.io/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5
with:
name: ${{ matrix.name }}
path: bin/${{ matrix.output }}*
name: tinygo-builds
path: |
bin/reticulum-go-*
artifacts/unsupported-microcontrollers.txt
if-no-files-found: warn

3
.gitignore vendored
View File

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

View File

@@ -1,14 +0,0 @@
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,21 +141,6 @@ 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:
@@ -258,7 +243,37 @@ tasks:
desc: Build binary with TinyGo compiler
cmds:
- mkdir -p {{.BUILD_DIR}}
- tinygo build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-tinygo -size short {{.MAIN_PACKAGE}}
- 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-wasm:
desc: Build WebAssembly binary with TinyGo compiler

View File

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

14
go.sum
View File

@@ -1,14 +1,4 @@
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=
github.com/shamaton/msgpack/v2 v2.4.0 h1:O5Z08MRmbo0lA9o2xnQ4TXx6teJbPqEurqcCOQ8Oi/4=
github.com/shamaton/msgpack/v2 v2.4.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI=
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 := msgpack.Marshal(ratchetData)
data, err := common.MsgpackMarshal(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 := msgpack.Unmarshal(data, &ratchetData); err != nil {
if err := common.MsgpackUnmarshal(data, &ratchetData); err != nil {
debug.Log(debug.DEBUG_ERROR, "Corrupted ratchet data", "file", entry.Name(), "error", err)
_ = os.Remove(filePath)
continue

View File

@@ -21,26 +21,30 @@ 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
MulticastAddrType string
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
}
// ReticulumConfig represents the main configuration structure

View File

@@ -39,10 +39,6 @@ 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
@@ -60,11 +56,9 @@ type BaseInterface struct {
MTU int
Bitrate int64
TxBytes uint64
RxBytes uint64
TxPackets uint64
RxPackets uint64
lastTx time.Time
TxBytes uint64
RxBytes uint64
lastTx time.Time
Mutex sync.RWMutex
Owner interface{}
@@ -131,30 +125,6 @@ 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()
@@ -190,20 +160,10 @@ 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)
}

17
pkg/common/msgpack.go Normal file
View File

@@ -0,0 +1,17 @@
// 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,7 +17,6 @@ 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"
)
@@ -107,9 +106,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 && 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")
if id == nil {
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil")
return nil, errors.New("identity cannot be nil")
}
d := &Destination{
@@ -144,9 +143,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 && 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")
if id == nil {
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil")
return nil, errors.New("identity cannot be nil")
}
d := &Destination{
@@ -169,25 +168,19 @@ 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
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))
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...)
} else {
// PLAIN destination has no identity hash
combined = nameHash10
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...)
// Then hash again and truncate to 16 bytes
finalHashFull := sha256.Sum256(combined)
@@ -613,7 +606,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 := msgpack.Marshal(d.ratchets)
packedRatchets, err := common.MsgpackMarshal(d.ratchets)
if err != nil {
return fmt.Errorf("failed to pack ratchets: %w", err)
}
@@ -631,7 +624,7 @@ func (d *Destination) persistRatchets() error {
}
// Pack the entire structure
finalData, err := msgpack.Marshal(persistedData)
finalData, err := common.MsgpackMarshal(persistedData)
if err != nil {
return fmt.Errorf("failed to pack ratchet data: %w", err)
}
@@ -694,7 +687,7 @@ func (d *Destination) reloadRatchets() error {
// Unpack outer structure
var persistedData map[string][]byte
if err := msgpack.Unmarshal(fileData, &persistedData); err != nil {
if err := common.MsgpackUnmarshal(fileData, &persistedData); err != nil {
return fmt.Errorf("failed to unpack ratchet data: %w", err)
}
@@ -711,7 +704,7 @@ func (d *Destination) reloadRatchets() error {
}
// Unpack ratchet list
if err := msgpack.Unmarshal(packedRatchets, &d.ratchets); err != nil {
if err := common.MsgpackUnmarshal(packedRatchets, &d.ratchets); err != nil {
return fmt.Errorf("failed to unpack ratchet list: %w", err)
}

View File

@@ -2,7 +2,6 @@ package destination
import (
"bytes"
"crypto/sha256"
"path/filepath"
"testing"
@@ -151,28 +150,3 @@ 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,7 +20,6 @@ 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"
)
@@ -672,7 +671,7 @@ func (i *Identity) saveRatchets(path string) error {
}
// Pack ratchets using msgpack
packedRatchets, err := msgpack.Marshal(ratchetList)
packedRatchets, err := common.MsgpackMarshal(ratchetList)
if err != nil {
return fmt.Errorf("failed to pack ratchets: %w", err)
}
@@ -687,7 +686,7 @@ func (i *Identity) saveRatchets(path string) error {
}
// Pack the entire structure
finalData, err := msgpack.Marshal(persistedData)
finalData, err := common.MsgpackMarshal(persistedData)
if err != nil {
return fmt.Errorf("failed to pack ratchet data: %w", err)
}
@@ -800,7 +799,7 @@ func (i *Identity) loadRatchets(path string) error {
// Unpack outer structure: {"signature": ..., "ratchets": ...}
var persistedData map[string][]byte
if err := msgpack.Unmarshal(fileData, &persistedData); err != nil {
if err := common.MsgpackUnmarshal(fileData, &persistedData); err != nil {
return fmt.Errorf("failed to unpack ratchet data: %w", err)
}
@@ -818,7 +817,7 @@ func (i *Identity) loadRatchets(path string) error {
// Unpack ratchet list
var ratchetList [][]byte
if err := msgpack.Unmarshal(packedRatchets, &ratchetList); err != nil {
if err := common.MsgpackUnmarshal(packedRatchets, &ratchetList); err != nil {
return fmt.Errorf("failed to unpack ratchet list: %w", err)
}

View File

@@ -1,13 +1,16 @@
// 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"
@@ -34,16 +37,8 @@ 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
@@ -53,6 +48,7 @@ type AutoInterface struct {
discoveryScope string
multicastAddrType string
mcastDiscoveryAddr string
ifacNetname string
peers map[string]*Peer
linkLocalAddrs []string
adoptedInterfaces map[string]*AdoptedInterface
@@ -67,7 +63,6 @@ type AutoInterface struct {
peerJobInterval time.Duration
peeringTimeout time.Duration
mcastEchoTimeout time.Duration
mifDeque []DequeEntry
done chan struct{}
stopOnce sync.Once
}
@@ -84,24 +79,6 @@ 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 != "" {
@@ -114,9 +91,6 @@ 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 {
@@ -130,13 +104,8 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
groupHash := sha256.Sum256([]byte(groupID))
// 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)
ifacNetname := hex.EncodeToString(groupHash[:])[:16]
mcastAddr := fmt.Sprintf("ff%s%s::%s", discoveryScope, multicastAddrType, ifacNetname)
ai := &AutoInterface{
BaseInterface: BaseInterface{
@@ -158,6 +127,7 @@ 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),
@@ -171,7 +141,6 @@ 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{}),
}
@@ -306,7 +275,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 = descopeLinkLocal(ipnet.IP.String())
linkLocalAddr = ipnet.IP.String()
break
}
}
@@ -415,17 +384,12 @@ func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) {
return
}
// 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[:]) {
if n >= len(ai.groupHash) {
receivedHash := buf[:len(ai.groupHash)]
if bytes.Equal(receivedHash, ai.groupHash) {
ai.handlePeerAnnounce(remoteAddr, ifaceName)
} else {
debug.Log(debug.DEBUG_TRACE, "Received discovery with mismatched group hash", "interface", ifaceName, "peer", peerIP)
debug.Log(debug.DEBUG_TRACE, "Received discovery with mismatched group hash", "interface", ifaceName)
}
}
}
@@ -444,7 +408,7 @@ func (ai *AutoInterface) handleData(conn *net.UDPConn, ifaceName string) {
default:
}
n, remoteAddr, err := conn.ReadFromUDP(buf)
n, _, err := conn.ReadFromUDP(buf)
if err != nil {
if ai.IsOnline() {
debug.Log(debug.DEBUG_ERROR, "Data read error", "interface", ifaceName, "error", err)
@@ -452,41 +416,8 @@ 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(data, ai)
callback(buf[:n], ai)
}
}
}
@@ -557,11 +488,7 @@ func (ai *AutoInterface) sendPeerAnnounce() {
}
}
// 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 {
if _, err := ai.outboundConn.WriteToUDP(ai.groupHash, 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

@@ -0,0 +1,96 @@
// 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,22 +56,20 @@ 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
TxPackets uint64
RxPackets 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
lastTx time.Time
lastRx time.Time
Mutex sync.RWMutex
packetCallback common.PacketCallback
@@ -79,22 +77,18 @@ 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,
TxBytes: 0,
RxBytes: 0,
TxPackets: 0,
RxPackets: 0,
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,
lastTx: time.Now(),
lastRx: time.Now(),
}
}
@@ -113,7 +107,6 @@ 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()
@@ -133,7 +126,6 @@ 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)
@@ -229,30 +221,6 @@ 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
}
@@ -304,6 +272,7 @@ 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)

24
pkg/interfaces/kiss.go Normal file
View File

@@ -0,0 +1,24 @@
// 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

@@ -0,0 +1,29 @@
// 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

@@ -0,0 +1,292 @@
// 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
}

262
pkg/interfaces/rnode.go Normal file
View File

@@ -0,0 +1,262 @@
// 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

@@ -0,0 +1,29 @@
// 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

@@ -0,0 +1,221 @@
// 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,11 +18,6 @@ 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
@@ -167,40 +162,6 @@ 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
@@ -239,6 +200,8 @@ func (tc *TCPClientInterface) readLoop() {
return
}
tc.UpdateStats(uint64(n), true) // #nosec G115
for i := 0; i < n; i++ {
b := buffer[i]
@@ -273,13 +236,70 @@ 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))
tc.ProcessIncoming(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
}
func (tc *TCPClientInterface) teardown() {
@@ -304,20 +324,6 @@ 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
}
@@ -447,6 +453,40 @@ 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
@@ -627,6 +667,18 @@ 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()
@@ -657,7 +709,14 @@ func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
return
}
ts.ProcessIncoming(buffer[:n])
ts.Mutex.Lock()
ts.RxBytes += uint64(n) // #nosec G115
callback := ts.packetCallback
ts.Mutex.Unlock()
if callback != nil {
callback(buffer[:n], ts)
}
}
}
@@ -680,6 +739,7 @@ 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
// +build !linux
//go:build !linux || tinygo
// +build !linux tinygo
package interfaces
@@ -14,3 +14,11 @@ 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
// +build linux
//go:build linux && !tinygo
// +build linux,!tinygo
package interfaces

View File

@@ -1,5 +1,8 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build !tinygo
// +build !tinygo
package interfaces
import (
@@ -48,30 +51,6 @@ 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()
@@ -87,22 +66,28 @@ func (ui *UDPInterface) Detach() {
})
}
func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) {
ui.Mutex.Lock()
defer ui.Mutex.Unlock()
ui.packetCallback = callback
}
func (ui *UDPInterface) Send(data []byte, addr string) error {
debug.Log(debug.DEBUG_ALL, "UDP interface sending bytes", "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)
if !ui.IsEnabled() {
return fmt.Errorf("interface not enabled")
}
if ui.targetAddr == nil {
return fmt.Errorf("no target address configured")
}
ui.Mutex.Lock()
ui.TxBytes += uint64(len(data))
ui.Mutex.Unlock()
_, err := ui.conn.WriteTo(data, ui.targetAddr)
if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "UDP interface write failed", "name", ui.Name, "error", err)
} else {
debug.Log(debug.DEBUG_ALL, "UDP interface sent bytes successfully", "name", ui.Name, "bytes", len(data))
}
return err
}
func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
@@ -110,15 +95,15 @@ func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
return fmt.Errorf("interface offline")
}
if ui.targetAddr == nil {
return fmt.Errorf("no target address configured")
}
_, err := ui.conn.WriteToUDP(data, ui.targetAddr)
_, err := ui.conn.Write(data)
if err != nil {
return fmt.Errorf("UDP write failed: %v", err)
}
ui.Mutex.Lock()
ui.TxBytes += uint64(len(data))
ui.Mutex.Unlock()
return nil
}
@@ -126,38 +111,6 @@ 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 {
@@ -241,19 +194,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()
ui.ProcessIncoming(buffer[:n])
if callback != nil {
callback(buffer[:n], ui)
}
}
}
func (ui *UDPInterface) IsEnabled() bool {
ui.Mutex.RLock()
defer ui.Mutex.RUnlock()
return ui.Enabled && ui.Online && !ui.Detached
}

View File

@@ -0,0 +1,68 @@
// 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,12 +92,7 @@ func (wsi *WebSocketInterface) Start() error {
defer wsi.Mutex.Unlock()
if wsi.ws.Truthy() {
readyState := wsi.ws.Get("readyState").Int()
if readyState == 1 { // OPEN
return nil
}
// If connecting, closing or closed, clean up first
wsi.closeWebSocket()
return fmt.Errorf("WebSocket already started")
}
ws := js.Global().Get("WebSocket").New(wsi.wsURL)
@@ -132,39 +127,30 @@ func (wsi *WebSocketInterface) Start() error {
event := args[0]
data := event.Get("data")
handlePacket := func(buf js.Value) {
array := js.Global().Get("Uint8Array").New(buf)
length := array.Get("length").Int()
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)
}
var packet []byte
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
}))
packet = []byte(data.String())
} else if data.Type() == js.TypeObject {
// Fallback for other object types that might be TypedArrays
handlePacket(data)
array := js.Global().Get("Uint8Array").New(data)
length := array.Get("length").Int()
packet = make([]byte, length)
js.CopyBytesToGo(packet, array)
} 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
}))
@@ -182,10 +168,8 @@ func (wsi *WebSocketInterface) Start() error {
debug.Log(debug.DEBUG_INFO, "WebSocket closed", "name", wsi.Name)
if wsi.Enabled && !wsi.Detached {
go func() {
time.Sleep(WS_RECONNECT_DELAY)
_ = wsi.Start()
}()
time.Sleep(WS_RECONNECT_DELAY)
go wsi.Start()
}
return nil
@@ -213,7 +197,15 @@ func (wsi *WebSocketInterface) closeWebSocket() {
wsi.Online = false
}
func (wsi *WebSocketInterface) ProcessOutgoing(data []byte) error {
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()
if !wsi.connected {
wsi.Mutex.Lock()
wsi.messageQueue = append(wsi.messageQueue, data)
@@ -242,6 +234,10 @@ 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,7 +28,6 @@ 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 (
@@ -308,7 +307,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 := msgpack.Marshal(requestData)
packedRequest, err := common.MsgpackMarshal(requestData)
if err != nil {
return nil, fmt.Errorf("failed to pack request: %w", err)
}
@@ -1029,7 +1028,7 @@ func (l *Link) handleRequest(plaintext []byte, pkt *packet.Packet) error {
}
var requestData []interface{}
if err := msgpack.Unmarshal(plaintext, &requestData); err != nil {
if err := common.MsgpackUnmarshal(plaintext, &requestData); err != nil {
return fmt.Errorf("failed to unpack request: %w", err)
}
@@ -1060,7 +1059,7 @@ func (l *Link) handleRequest(plaintext []byte, pkt *packet.Packet) error {
case string:
requestPayload = []byte(payload)
default:
packed, err := msgpack.Marshal(payload)
packed, err := common.MsgpackMarshal(payload)
if err != nil {
return fmt.Errorf("failed to pack request_payload: %w", err)
}
@@ -1089,7 +1088,7 @@ func (l *Link) handleRequest(plaintext []byte, pkt *packet.Packet) error {
func (l *Link) handleResponse(plaintext []byte) error {
var responseData []interface{}
if err := msgpack.Unmarshal(plaintext, &responseData); err != nil {
if err := common.MsgpackUnmarshal(plaintext, &responseData); err != nil {
return fmt.Errorf("failed to unpack response: %w", err)
}
@@ -1124,7 +1123,7 @@ func (l *Link) handleResponse(plaintext []byte) error {
func (l *Link) sendResponse(requestID []byte, response interface{}) error {
responseData := []interface{}{requestID, response}
packedResponse, err := msgpack.Marshal(responseData)
packedResponse, err := common.MsgpackMarshal(responseData)
if err != nil {
return fmt.Errorf("failed to pack response: %w", err)
}

View File

@@ -144,6 +144,8 @@ 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")
@@ -152,8 +154,6 @@ 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) + TransportID(16) + DestHash(16) + Context(1) + Data
// Header Type 2: Header(2) + DestHash(16) + TransportID(16) + Context(1) + Data
if len(p.Raw) < 2*dstLen+3 {
return errors.New("packet too short for header type 2")
}
p.TransportID = p.Raw[2 : dstLen+2] // Transport ID first
p.DestinationHash = p.Raw[dstLen+2 : 2*dstLen+2] // Destination hash second
p.DestinationHash = p.Raw[2 : dstLen+2] // Destination hash first
p.TransportID = p.Raw[dstLen+2 : 2*dstLen+2] // Transport ID second
p.Context = p.Raw[2*dstLen+2]
p.Data = p.Raw[2*dstLen+3:]
} else {

View File

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

View File

@@ -1,39 +0,0 @@
// 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

@@ -1,66 +0,0 @@
// 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,6 +18,7 @@ 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"
@@ -323,12 +324,6 @@ 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()
@@ -582,11 +577,8 @@ func (t *Transport) RequestPath(destinationHash []byte, onInterface string, tag
pathRequestData = append(destinationHash, tag...)
}
pathRequestName := "rnstransport.path.request"
nameHashFull := sha256.Sum256([]byte(pathRequestName))
nameHash10 := nameHashFull[:10]
finalHashFull := sha256.Sum256(nameHash10)
pathRequestDestHash := finalHashFull[:16]
destHashFull := sha256.Sum256([]byte("rnstransport.path.request"))
pathRequestDestHash := destHashFull[:common.SIZE_16]
pkt := packet.NewPacket(
packet.DestinationPlain,
@@ -594,12 +586,11 @@ func (t *Transport) RequestPath(destinationHash []byte, onInterface string, tag
0x00,
0x00,
packet.PropagationBroadcast,
0x00, // Header Type 1
nil,
0x01,
pathRequestDestHash,
false,
0x00,
)
pkt.DestinationHash = pathRequestDestHash
if err := pkt.Pack(); err != nil {
return fmt.Errorf("failed to pack path request: %w", err)
@@ -890,6 +881,11 @@ 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)
@@ -1114,15 +1110,16 @@ 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+1)
t.updatePathUnlocked(destinationHash, nil, iface.GetName(), hopCount)
t.mutex.Unlock()
debug.Log(debug.DEBUG_INFO, "Registered path", "hash", fmt.Sprintf("%x", destinationHash), "interface", iface.GetName(), "hops", hopCount+1)
debug.Log(debug.DEBUG_INFO, "Registered path", "hash", fmt.Sprintf("%x", destinationHash), "interface", iface.GetName(), "hops", hopCount)
}
// 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+1)
t.notifyAnnounceHandlers(destinationHash, id, appData, hopCount)
debug.Log(debug.DEBUG_INFO, "Announce handlers notified")
// Don't forward if max hops reached
@@ -1379,7 +1376,7 @@ func (t *Transport) InitializePathRequestHandler() error {
return errors.New("transport identity not initialized")
}
pathRequestDest, err := destination.New(nil, destination.IN, destination.PLAIN, "rnstransport", t, "path", "request")
pathRequestDest, err := destination.New(t.transportIdentity, destination.IN, destination.PLAIN, "rnstransport", t, "path", "request")
if err != nil {
return fmt.Errorf("failed to create path request destination: %w", err)
}
@@ -1695,14 +1692,6 @@ 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,12 +3,8 @@ 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 {
@@ -122,68 +118,3 @@ 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,12 +24,10 @@ var (
reticulumDest *destination.Destination
reticulumIdentity *identity.Identity
stats = struct {
packetsSent int
packetsReceived int
bytesSent int
bytesReceived int
announcesSent int
announcesReceived int
packetsSent int
packetsReceived int
bytesSent int
bytesReceived int
}{}
packetCallback js.Value
announceHandler js.Value
@@ -49,7 +47,6 @@ func RegisterJSFunctions() {
"setPacketCallback": js.FuncOf(SetPacketCallback),
"setAnnounceCallback": js.FuncOf(SetAnnounceCallback),
"sendData": js.FuncOf(SendDataJS),
"sendMessage": js.FuncOf(SendDataJS),
"announce": js.FuncOf(SendAnnounceJS),
}))
}
@@ -103,31 +100,11 @@ 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,
"announcesSent": stats.announcesSent,
"announcesReceived": stats.announcesReceived,
"packetsSent": stats.packetsSent,
"packetsReceived": stats.packetsReceived,
"bytesSent": stats.bytesSent,
"bytesReceived": stats.bytesReceived,
})
}
@@ -177,14 +154,6 @@ 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,
@@ -201,6 +170,9 @@ 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))
@@ -220,8 +192,12 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
})
}
// Wire the interface to the transport
wsInterface.SetPacketCallback(t.HandlePacket)
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)
})
if err := t.RegisterInterface("wasm0", wsInterface); err != nil {
return js.ValueOf(map[string]interface{}{
@@ -362,8 +338,6 @@ 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{}{
@@ -457,6 +431,9 @@ func SendData(destHash []byte, data []byte) interface{} {
})
}
stats.packetsSent++
stats.bytesSent += len(data)
return js.ValueOf(map[string]interface{}{
"success": true,
})
@@ -492,8 +469,6 @@ func SendAnnounce(appData []byte) interface{} {
})
}
stats.announcesSent++
return js.ValueOf(map[string]interface{}{
"success": true,
})