22 Commits

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

View File

@@ -0,0 +1,29 @@
name: Go Benchmarks
on:
push:
branches: [ "main", "master" ]
pull_request:
branches: [ "main", "master" ]
jobs:
benchmark:
name: Run Benchmarks
runs-on: ubuntu-latest
steps:
- name: Checkout Source
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Set up Go
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
with:
go-version: '1.25'
- name: Setup Task
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2
with:
version: '3.46.3'
- name: Run Benchmarks
run: task bench

View File

@@ -0,0 +1,55 @@
name: Go Build Test
on:
pull_request:
branches:
- main
- master
permissions:
contents: read
jobs:
build:
name: Build (${{ matrix.goos }}, ${{ matrix.goarch }})
strategy:
fail-fast: false
matrix:
goos: [linux, windows, darwin, freebsd]
goarch: [amd64, arm64, arm]
include:
- goos: js
goarch: wasm
exclude:
- goos: darwin
goarch: arm
runs-on: ubuntu-latest
steps:
- name: Checkout Source
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Set up Go
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
with:
go-version: '1.25'
- name: Setup Task
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2
with:
version: '3.46.3'
- name: Build
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarch == 'arm' && '6' || '' }}
CGO_ENABLED: '0'
run: |
if [ "${{ matrix.goos }}" = "js" ] && [ "${{ matrix.goarch }}" = "wasm" ]; then
task build-wasm
else
task build
fi

View File

@@ -1,105 +0,0 @@
name: Go Build Multi-Platform
on:
push:
branches: [ "main", "master" ]
tags:
- 'v*'
pull_request:
branches: [ "main", "master" ]
jobs:
build:
permissions:
contents: write
strategy:
matrix:
goos: [linux, windows, darwin, freebsd]
goarch: [amd64, arm64, arm]
include:
- goos: js
goarch: wasm
exclude:
- goos: darwin
goarch: arm
runs-on: ubuntu-latest
outputs:
build_complete: ${{ steps.build_step.outcome == 'success' }}
steps:
- name: Checkout code
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Go
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: '1.25'
- name: Setup Task
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
with:
version: '3.46.3'
- name: Build
id: build_step
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarch == 'arm' && '6' || '' }}
CGO_ENABLED: '0'
run: |
output_name="reticulum-go-${GOOS}-${GOARCH}"
if [ "$GOOS" = "js" ] && [ "$GOARCH" = "wasm" ]; then
task build-wasm
output_name+=".wasm"
mv bin/reticulum-go.wasm "${output_name}"
else
task build
if [ "$GOOS" = "windows" ]; then
output_name+=".exe"
fi
mv bin/reticulum-go "${output_name}"
fi
echo "Built: ${output_name}"
- name: Calculate SHA256 Checksum
run: |
output_name="reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}"
if [ "${{ matrix.goos }}" = "windows" ]; then
output_name+=".exe"
elif [ "${{ matrix.goos }}" = "js" ] && [ "${{ matrix.goarch }}" = "wasm" ]; then
output_name+=".wasm"
fi
BINARY_PATH="${output_name}" task checksum
- name: Upload Artifact
uses: https://git.quad4.io/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3.2.1
with:
name: reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}
path: |
reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}*
release:
name: Create Release
runs-on: ubuntu-latest
needs: build
if: startsWith(github.ref, 'refs/tags/')
permissions:
contents: write
steps:
- name: Download All Build Artifacts
uses: https://git.quad4.io/actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a
with:
path: ./release-assets
- name: List downloaded files (for debugging)
run: ls -R ./release-assets
- name: Create Gitea Release
uses: https://git.quad4.io/actions/gitea-release-action@4875285c0950474efb7ca2df55233c51333eeb74
with:
files: ./release-assets/*/*

View File

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

View File

@@ -0,0 +1,97 @@
name: Go Publish
on:
push:
branches:
- main
- master
tags:
- 'v*'
permissions:
contents: write
jobs:
build:
name: Build (${{ matrix.goos }}, ${{ matrix.goarch }})
strategy:
matrix:
goos: [linux, windows, darwin, freebsd]
goarch: [amd64, arm64, arm]
include:
- goos: js
goarch: wasm
exclude:
- goos: darwin
goarch: arm
runs-on: ubuntu-latest
steps:
- name: Checkout Source
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Set up Go
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
with:
go-version: '1.25'
- name: Setup Task
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2
with:
version: '3.46.3'
- name: Build
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarch == 'arm' && '6' || '' }}
CGO_ENABLED: '0'
run: |
output_name="reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}"
if [ "${{ matrix.goos }}" = "js" ] && [ "${{ matrix.goarch }}" = "wasm" ]; then
task build-wasm
output_name+=".wasm"
mv bin/reticulum-go.wasm "${output_name}"
else
task build
if [ "${{ matrix.goos }}" = "windows" ]; then
output_name+=".exe"
fi
mv bin/reticulum-go "${output_name}"
fi
echo "Built: ${output_name}"
- name: Calculate SHA256 Checksum
run: |
output_name="reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}"
if [ "${{ matrix.goos }}" = "windows" ]; then
output_name+=".exe"
elif [ "${{ matrix.goos }}" = "js" ] && [ "${{ matrix.goarch }}" = "wasm" ]; then
output_name+=".wasm"
fi
BINARY_PATH="${output_name}" task checksum
- name: Upload Artifact
uses: https://git.quad4.io/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5
with:
name: reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}
path: |
reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}*
release:
name: Create Release
runs-on: ubuntu-latest
needs: build
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Download All Build Artifacts
uses: https://git.quad4.io/actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a
with:
path: ./release-assets
- name: Create Gitea Release
uses: https://git.quad4.io/actions/gitea-release-action@4875285c0950474efb7ca2df55233c51333eeb74
with:
files: ./release-assets/*/*

1
.gitignore vendored
View File

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

View File

@@ -141,6 +141,21 @@ tasks:
cmds: cmds:
- '{{.GOCMD}} test -race -v ./...' - '{{.GOCMD}} test -race -v ./...'
test-fuzz:
desc: Run fuzz tests for a short duration
cmds:
- '{{.GOCMD}} test -fuzz=FuzzPacketUnpack -fuzztime=30s ./pkg/packet'
test-leaks:
desc: Run resource leak tests
cmds:
- '{{.GOCMD}} test -v ./pkg/transport -run TestTransportLeak'
test-network:
desc: Run network simulation tests
cmds:
- '{{.GOCMD}} test -v ./pkg/transport -run TestTransportNetworkSimulation'
coverage: coverage:
desc: Generate test coverage report desc: Generate test coverage report
cmds: cmds:

View File

@@ -40,6 +40,7 @@ type InterfaceConfig struct {
DiscoveryScope string DiscoveryScope string
DiscoveryPort int DiscoveryPort int
DataPort int DataPort int
MulticastAddrType string
} }
// ReticulumConfig represents the main configuration structure // ReticulumConfig represents the main configuration structure

View File

@@ -39,6 +39,10 @@ type NetworkInterface interface {
SendLinkPacket([]byte, []byte, time.Time) error SendLinkPacket([]byte, []byte, time.Time) error
SetPacketCallback(PacketCallback) SetPacketCallback(PacketCallback)
GetPacketCallback() PacketCallback GetPacketCallback() PacketCallback
GetTxBytes() uint64
GetRxBytes() uint64
GetTxPackets() uint64
GetRxPackets() uint64
} }
// BaseInterface provides common implementation for network interfaces // BaseInterface provides common implementation for network interfaces
@@ -58,6 +62,8 @@ type BaseInterface struct {
TxBytes uint64 TxBytes uint64
RxBytes uint64 RxBytes uint64
TxPackets uint64
RxPackets uint64
lastTx time.Time lastTx time.Time
Mutex sync.RWMutex Mutex sync.RWMutex
@@ -125,6 +131,30 @@ func (i *BaseInterface) GetPacketCallback() PacketCallback {
return i.PacketCallback return i.PacketCallback
} }
func (i *BaseInterface) GetTxBytes() uint64 {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.TxBytes
}
func (i *BaseInterface) GetRxBytes() uint64 {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.RxBytes
}
func (i *BaseInterface) GetTxPackets() uint64 {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.TxPackets
}
func (i *BaseInterface) GetRxPackets() uint64 {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.RxPackets
}
func (i *BaseInterface) Detach() { func (i *BaseInterface) Detach() {
i.Mutex.Lock() i.Mutex.Lock()
defer i.Mutex.Unlock() defer i.Mutex.Unlock()
@@ -160,10 +190,20 @@ func (i *BaseInterface) GetConn() net.Conn {
} }
func (i *BaseInterface) Send(data []byte, address string) error { func (i *BaseInterface) Send(data []byte, address string) error {
i.Mutex.Lock()
i.TxBytes += uint64(len(data))
i.TxPackets++
i.lastTx = time.Now()
i.Mutex.Unlock()
return i.ProcessOutgoing(data) return i.ProcessOutgoing(data)
} }
func (i *BaseInterface) ProcessIncoming(data []byte) { func (i *BaseInterface) ProcessIncoming(data []byte) {
i.Mutex.Lock()
i.RxBytes += uint64(len(data))
i.RxPackets++
i.Mutex.Unlock()
if i.PacketCallback != nil { if i.PacketCallback != nil {
i.PacketCallback(data, i) i.PacketCallback(data, i)
} }

View File

@@ -107,9 +107,9 @@ type Destination struct {
func New(id *identity.Identity, direction byte, destType byte, appName string, transport Transport, aspects ...string) (*Destination, error) { func New(id *identity.Identity, direction byte, destType byte, appName string, transport Transport, aspects ...string) (*Destination, error) {
debug.Log(debug.DEBUG_INFO, "Creating new destination", "app", appName, "type", destType, "direction", direction) debug.Log(debug.DEBUG_INFO, "Creating new destination", "app", appName, "type", destType, "direction", direction)
if id == nil { if id == nil && destType != PLAIN {
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil") debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil for non-PLAIN destination")
return nil, errors.New("identity cannot be nil") return nil, errors.New("identity cannot be nil for non-PLAIN destination")
} }
d := &Destination{ d := &Destination{
@@ -144,9 +144,9 @@ func New(id *identity.Identity, direction byte, destType byte, appName string, t
func FromHash(hash []byte, id *identity.Identity, destType byte, transport Transport) (*Destination, error) { func FromHash(hash []byte, id *identity.Identity, destType byte, transport Transport) (*Destination, error) {
debug.Log(debug.DEBUG_INFO, "Creating destination from hash", "hash", fmt.Sprintf("%x", hash)) debug.Log(debug.DEBUG_INFO, "Creating destination from hash", "hash", fmt.Sprintf("%x", hash))
if id == nil { if id == nil && destType != PLAIN {
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil") debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil for non-PLAIN destination")
return nil, errors.New("identity cannot be nil") return nil, errors.New("identity cannot be nil for non-PLAIN destination")
} }
d := &Destination{ d := &Destination{
@@ -169,19 +169,25 @@ func FromHash(hash []byte, id *identity.Identity, destType byte, transport Trans
func (d *Destination) calculateHash() []byte { func (d *Destination) calculateHash() []byte {
debug.Log(debug.DEBUG_TRACE, "Calculating hash for destination", "name", d.ExpandName()) debug.Log(debug.DEBUG_TRACE, "Calculating hash for destination", "name", d.ExpandName())
// destination_hash = SHA256(name_hash_10bytes + identity_hash_16bytes)[:16]
// Identity hash is the truncated hash of the public key (16 bytes)
identityHash := identity.TruncatedHash(d.identity.GetPublicKey())
// Name hash is the FULL 32-byte SHA256, then we take first 10 bytes for concatenation // Name hash is the FULL 32-byte SHA256, then we take first 10 bytes for concatenation
nameHashFull := sha256.Sum256([]byte(d.ExpandName())) nameHashFull := sha256.Sum256([]byte(d.ExpandName()))
nameHash10 := nameHashFull[:10] // Only use 10 bytes nameHash10 := nameHashFull[:10] // Only use 10 bytes
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, "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, "Name hash (10 bytes)", "hash", fmt.Sprintf("%x", nameHash10))
// Concatenate name_hash (10 bytes) + identity_hash (16 bytes) = 26 bytes // Concatenate name_hash (10 bytes) + identity_hash (16 bytes) = 26 bytes
combined := append(nameHash10, identityHash...) combined = append(nameHash10, identityHash...)
} else {
// PLAIN destination has no identity hash
combined = nameHash10
debug.Log(debug.DEBUG_ALL, "Name hash (10 bytes)", "hash", fmt.Sprintf("%x", nameHash10))
}
// Then hash again and truncate to 16 bytes // Then hash again and truncate to 16 bytes
finalHashFull := sha256.Sum256(combined) finalHashFull := sha256.Sum256(combined)

View File

@@ -2,6 +2,7 @@ package destination
import ( import (
"bytes" "bytes"
"crypto/sha256"
"path/filepath" "path/filepath"
"testing" "testing"
@@ -150,3 +151,28 @@ func TestPlainDestination(t *testing.T) {
t.Error("Plain destination should not decrypt") t.Error("Plain destination should not decrypt")
} }
} }
func TestPlainDestinationHash(t *testing.T) {
// A PLAIN destination with no identity should have a hash based only on its name
transport := &mockTransport{}
dest, err := New(nil, IN|OUT, PLAIN, "testapp", transport, "testaspect")
if err != nil {
t.Fatalf("New failed: %v", err)
}
hash := dest.GetHash()
if len(hash) != 16 {
t.Fatalf("Expected hash length 16, got %d", len(hash))
}
// Calculate manually: SHA256(SHA256("testapp.testaspect")[:10])[:16]
name := "testapp.testaspect"
nameHashFull := sha256.Sum256([]byte(name))
nameHash10 := nameHashFull[:10]
finalHashFull := sha256.Sum256(nameHash10)
expectedHash := finalHashFull[:16]
if !bytes.Equal(hash, expectedHash) {
t.Errorf("Expected hash %x, got %x", expectedHash, hash)
}
}

View File

@@ -5,9 +5,9 @@ package interfaces
import ( import (
"bytes" "bytes"
"crypto/sha256" "crypto/sha256"
"encoding/hex"
"fmt" "fmt"
"net" "net"
"strings"
"sync" "sync"
"time" "time"
@@ -34,8 +34,16 @@ const (
MCAST_ADDR_TYPE_PERMANENT = "0" MCAST_ADDR_TYPE_PERMANENT = "0"
MCAST_ADDR_TYPE_TEMPORARY = "1" MCAST_ADDR_TYPE_TEMPORARY = "1"
MULTI_IF_DEQUE_LEN = 48
MULTI_IF_DEQUE_TTL = 750 * time.Millisecond
) )
type DequeEntry struct {
hash [32]byte
timestamp time.Time
}
type AutoInterface struct { type AutoInterface struct {
BaseInterface BaseInterface
groupID []byte groupID []byte
@@ -45,7 +53,6 @@ type AutoInterface struct {
discoveryScope string discoveryScope string
multicastAddrType string multicastAddrType string
mcastDiscoveryAddr string mcastDiscoveryAddr string
ifacNetname string
peers map[string]*Peer peers map[string]*Peer
linkLocalAddrs []string linkLocalAddrs []string
adoptedInterfaces map[string]*AdoptedInterface adoptedInterfaces map[string]*AdoptedInterface
@@ -60,6 +67,7 @@ type AutoInterface struct {
peerJobInterval time.Duration peerJobInterval time.Duration
peeringTimeout time.Duration peeringTimeout time.Duration
mcastEchoTimeout time.Duration mcastEchoTimeout time.Duration
mifDeque []DequeEntry
done chan struct{} done chan struct{}
stopOnce sync.Once stopOnce sync.Once
} }
@@ -76,6 +84,24 @@ type Peer struct {
addr *net.UDPAddr addr *net.UDPAddr
} }
func descopeLinkLocal(addr string) string {
// Drop scope specifier expressed as %ifname (macOS)
if i := strings.Index(addr, "%"); i != -1 {
addr = addr[:i]
}
// Drop embedded scope specifier (NetBSD, OpenBSD)
// Python: re.sub(r"fe80:[0-9a-f]*::","fe80::", link_local_addr)
if strings.HasPrefix(addr, "fe80:") {
parts := strings.Split(addr, ":")
// Check for fe80:[scope]::...
if len(parts) >= 3 && parts[2] == "" && parts[1] != "" {
return "fe80::" + strings.Join(parts[3:], ":")
}
}
return addr
}
func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterface, error) { func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterface, error) {
groupID := DEFAULT_GROUP_ID groupID := DEFAULT_GROUP_ID
if config.GroupID != "" { if config.GroupID != "" {
@@ -88,6 +114,9 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
} }
multicastAddrType := MCAST_ADDR_TYPE_TEMPORARY multicastAddrType := MCAST_ADDR_TYPE_TEMPORARY
if config.MulticastAddrType != "" {
multicastAddrType = normalizeMulticastType(config.MulticastAddrType)
}
discoveryPort := DEFAULT_DISCOVERY_PORT discoveryPort := DEFAULT_DISCOVERY_PORT
if config.DiscoveryPort != 0 { if config.DiscoveryPort != 0 {
@@ -101,8 +130,13 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
groupHash := sha256.Sum256([]byte(groupID)) groupHash := sha256.Sum256([]byte(groupID))
ifacNetname := hex.EncodeToString(groupHash[:])[:16] // Python-compatible multicast address generation
mcastAddr := fmt.Sprintf("ff%s%s::%s", discoveryScope, multicastAddrType, ifacNetname) // gt = "0:" + "{:02x}".format(g[3]+(g[2]<<8)) + ":" + ...
gt := "0"
for i := 1; i <= 6; i++ {
gt += fmt.Sprintf(":%02x%02x", groupHash[i*2], groupHash[i*2+1])
}
mcastAddr := fmt.Sprintf("ff%s%s:%s", multicastAddrType, discoveryScope, gt)
ai := &AutoInterface{ ai := &AutoInterface{
BaseInterface: BaseInterface{ BaseInterface: BaseInterface{
@@ -124,7 +158,6 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
discoveryScope: discoveryScope, discoveryScope: discoveryScope,
multicastAddrType: multicastAddrType, multicastAddrType: multicastAddrType,
mcastDiscoveryAddr: mcastAddr, mcastDiscoveryAddr: mcastAddr,
ifacNetname: ifacNetname,
peers: make(map[string]*Peer), peers: make(map[string]*Peer),
linkLocalAddrs: make([]string, 0), linkLocalAddrs: make([]string, 0),
adoptedInterfaces: make(map[string]*AdoptedInterface), adoptedInterfaces: make(map[string]*AdoptedInterface),
@@ -138,6 +171,7 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
peerJobInterval: PEER_JOB_INTERVAL, peerJobInterval: PEER_JOB_INTERVAL,
peeringTimeout: PEERING_TIMEOUT, peeringTimeout: PEERING_TIMEOUT,
mcastEchoTimeout: MCAST_ECHO_TIMEOUT, mcastEchoTimeout: MCAST_ECHO_TIMEOUT,
mifDeque: make([]DequeEntry, 0, MULTI_IF_DEQUE_LEN),
done: make(chan struct{}), done: make(chan struct{}),
} }
@@ -272,7 +306,7 @@ func (ai *AutoInterface) configureInterface(iface *net.Interface) error {
for _, addr := range addrs { for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok { if ipnet, ok := addr.(*net.IPNet); ok {
if ipnet.IP.To4() == nil && ipnet.IP.IsLinkLocalUnicast() { if ipnet.IP.To4() == nil && ipnet.IP.IsLinkLocalUnicast() {
linkLocalAddr = ipnet.IP.String() linkLocalAddr = descopeLinkLocal(ipnet.IP.String())
break break
} }
} }
@@ -381,12 +415,17 @@ func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) {
return return
} }
if n >= len(ai.groupHash) { // Python: discovery_token = RNS.Identity.full_hash(self.group_id+ipv6_src[0].encode("utf-8"))
receivedHash := buf[:len(ai.groupHash)] peerIP := descopeLinkLocal(remoteAddr.IP.String())
if bytes.Equal(receivedHash, ai.groupHash) { tokenSource := append(ai.groupID, []byte(peerIP)...)
expectedHash := sha256.Sum256(tokenSource)
if n >= len(expectedHash) {
receivedHash := buf[:len(expectedHash)]
if bytes.Equal(receivedHash, expectedHash[:]) {
ai.handlePeerAnnounce(remoteAddr, ifaceName) ai.handlePeerAnnounce(remoteAddr, ifaceName)
} else { } else {
debug.Log(debug.DEBUG_TRACE, "Received discovery with mismatched group hash", "interface", ifaceName) debug.Log(debug.DEBUG_TRACE, "Received discovery with mismatched group hash", "interface", ifaceName, "peer", peerIP)
} }
} }
} }
@@ -405,7 +444,7 @@ func (ai *AutoInterface) handleData(conn *net.UDPConn, ifaceName string) {
default: default:
} }
n, _, err := conn.ReadFromUDP(buf) n, remoteAddr, err := conn.ReadFromUDP(buf)
if err != nil { if err != nil {
if ai.IsOnline() { if ai.IsOnline() {
debug.Log(debug.DEBUG_ERROR, "Data read error", "interface", ifaceName, "error", err) debug.Log(debug.DEBUG_ERROR, "Data read error", "interface", ifaceName, "error", err)
@@ -413,8 +452,41 @@ func (ai *AutoInterface) handleData(conn *net.UDPConn, ifaceName string) {
return return
} }
data := buf[:n]
dataHash := sha256.Sum256(data)
now := time.Now()
ai.Mutex.Lock()
// Check for duplicate in mifDeque
isDuplicate := false
for i := 0; i < len(ai.mifDeque); i++ {
if ai.mifDeque[i].hash == dataHash && now.Sub(ai.mifDeque[i].timestamp) < MULTI_IF_DEQUE_TTL {
isDuplicate = true
break
}
}
if isDuplicate {
ai.Mutex.Unlock()
continue
}
// Add to deque
ai.mifDeque = append(ai.mifDeque, DequeEntry{hash: dataHash, timestamp: now})
if len(ai.mifDeque) > MULTI_IF_DEQUE_LEN {
ai.mifDeque = ai.mifDeque[1:]
}
// Refresh peer if known
peerIP := descopeLinkLocal(remoteAddr.IP.String())
peerKey := peerIP + "%" + ifaceName
if peer, exists := ai.peers[peerKey]; exists {
peer.lastHeard = now
}
ai.Mutex.Unlock()
if callback := ai.GetPacketCallback(); callback != nil { if callback := ai.GetPacketCallback(); callback != nil {
callback(buf[:n], ai) callback(data, ai)
} }
} }
} }
@@ -485,7 +557,11 @@ func (ai *AutoInterface) sendPeerAnnounce() {
} }
} }
if _, err := ai.outboundConn.WriteToUDP(ai.groupHash, mcastAddr); err != nil { // Python: discovery_token = RNS.Identity.full_hash(self.group_id+link_local_address.encode("utf-8"))
tokenSource := append(ai.groupID, []byte(adoptedIface.linkLocalAddr)...)
token := sha256.Sum256(tokenSource)
if _, err := ai.outboundConn.WriteToUDP(token[:], mcastAddr); err != nil {
debug.Log(debug.DEBUG_VERBOSE, "Failed to send peer announce", "interface", ifaceName, "error", err) debug.Log(debug.DEBUG_VERBOSE, "Failed to send peer announce", "interface", ifaceName, "error", err)
} else { } else {
debug.Log(debug.DEBUG_TRACE, "Sent peer announce", "interface", adoptedIface.name) debug.Log(debug.DEBUG_TRACE, "Sent peer announce", "interface", adoptedIface.name)

View File

@@ -68,6 +68,8 @@ type BaseInterface struct {
Bitrate int64 Bitrate int64
TxBytes uint64 TxBytes uint64
RxBytes uint64 RxBytes uint64
TxPackets uint64
RxPackets uint64
lastTx time.Time lastTx time.Time
lastRx time.Time lastRx time.Time
@@ -87,6 +89,10 @@ func NewBaseInterface(name string, ifType common.InterfaceType, enabled bool) Ba
OUT: false, OUT: false,
MTU: common.DEFAULT_MTU, MTU: common.DEFAULT_MTU,
Bitrate: BITRATE_MINIMUM, Bitrate: BITRATE_MINIMUM,
TxBytes: 0,
RxBytes: 0,
TxPackets: 0,
RxPackets: 0,
lastTx: time.Now(), lastTx: time.Now(),
lastRx: time.Now(), lastRx: time.Now(),
} }
@@ -107,6 +113,7 @@ func (i *BaseInterface) GetPacketCallback() common.PacketCallback {
func (i *BaseInterface) ProcessIncoming(data []byte) { func (i *BaseInterface) ProcessIncoming(data []byte) {
i.Mutex.Lock() i.Mutex.Lock()
i.RxBytes += uint64(len(data)) i.RxBytes += uint64(len(data))
i.RxPackets++
i.Mutex.Unlock() i.Mutex.Unlock()
i.Mutex.RLock() i.Mutex.RLock()
@@ -126,6 +133,7 @@ func (i *BaseInterface) ProcessOutgoing(data []byte) error {
i.Mutex.Lock() i.Mutex.Lock()
i.TxBytes += uint64(len(data)) i.TxBytes += uint64(len(data))
i.TxPackets++
i.Mutex.Unlock() i.Mutex.Unlock()
debug.Log(debug.DEBUG_VERBOSE, "Interface processed outgoing packet", "name", i.Name, "bytes", len(data), "total_tx", i.TxBytes) debug.Log(debug.DEBUG_VERBOSE, "Interface processed outgoing packet", "name", i.Name, "bytes", len(data), "total_tx", i.TxBytes)
@@ -221,6 +229,30 @@ func (i *BaseInterface) IsDetached() bool {
return i.Detached return i.Detached
} }
func (i *BaseInterface) GetTxBytes() uint64 {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.TxBytes
}
func (i *BaseInterface) GetRxBytes() uint64 {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.RxBytes
}
func (i *BaseInterface) GetTxPackets() uint64 {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.TxPackets
}
func (i *BaseInterface) GetRxPackets() uint64 {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.RxPackets
}
func (i *BaseInterface) Start() error { func (i *BaseInterface) Start() error {
return nil return nil
} }
@@ -272,7 +304,6 @@ func (i *BaseInterface) updateBandwidthStats(bytes uint64) {
i.Mutex.Lock() i.Mutex.Lock()
defer i.Mutex.Unlock() defer i.Mutex.Unlock()
i.TxBytes += bytes
i.lastTx = time.Now() i.lastTx = time.Now()
debug.Log(debug.DEBUG_VERBOSE, "Interface updated bandwidth stats", "name", i.Name, "tx_bytes", i.TxBytes, "last_tx", i.lastTx) debug.Log(debug.DEBUG_VERBOSE, "Interface updated bandwidth stats", "name", i.Name, "tx_bytes", i.TxBytes, "last_tx", i.lastTx)

View File

@@ -167,6 +167,40 @@ func (tc *TCPClientInterface) Stop() error {
return nil return nil
} }
func (tc *TCPClientInterface) ProcessOutgoing(data []byte) error {
tc.Mutex.RLock()
online := tc.Online
tc.Mutex.RUnlock()
if !online {
return fmt.Errorf("interface offline")
}
tc.writing = true
defer func() { tc.writing = false }()
// For TCP connections, use HDLC framing
var frame []byte
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
frame = append(frame, HDLC_FLAG)
debug.Log(debug.DEBUG_ALL, "TCP interface writing to network", "name", tc.Name, "bytes", len(frame))
tc.Mutex.RLock()
conn := tc.conn
tc.Mutex.RUnlock()
if conn == nil {
return fmt.Errorf("connection closed")
}
_, err := conn.Write(frame)
if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "TCP interface write failed", "name", tc.Name, "error", err)
}
return err
}
func (tc *TCPClientInterface) readLoop() { func (tc *TCPClientInterface) readLoop() {
buffer := make([]byte, tc.MTU) buffer := make([]byte, tc.MTU)
inFrame := false inFrame := false
@@ -205,8 +239,6 @@ func (tc *TCPClientInterface) readLoop() {
return return
} }
tc.UpdateStats(uint64(n), true) // #nosec G115
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
b := buffer[i] b := buffer[i]
@@ -241,70 +273,13 @@ func (tc *TCPClientInterface) handlePacket(data []byte) {
} }
tc.Mutex.Lock() tc.Mutex.Lock()
tc.RxBytes += uint64(len(data))
lastRx := time.Now() lastRx := time.Now()
tc.lastRx = lastRx tc.lastRx = lastRx
callback := tc.packetCallback
tc.Mutex.Unlock() tc.Mutex.Unlock()
debug.Log(debug.DEBUG_ALL, "Received packet", "type", fmt.Sprintf("0x%02x", data[0]), "size", len(data)) debug.Log(debug.DEBUG_ALL, "Received packet", "type", fmt.Sprintf("0x%02x", data[0]), "size", len(data))
// For RNS packets, call the packet callback directly tc.ProcessIncoming(data)
if callback != 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() { func (tc *TCPClientInterface) teardown() {
@@ -472,40 +447,6 @@ func (tc *TCPClientInterface) GetRTT() time.Duration {
return 0 return 0
} }
func (tc *TCPClientInterface) GetTxBytes() uint64 {
tc.Mutex.RLock()
defer tc.Mutex.RUnlock()
return tc.TxBytes
}
func (tc *TCPClientInterface) GetRxBytes() uint64 {
tc.Mutex.RLock()
defer tc.Mutex.RUnlock()
return tc.RxBytes
}
func (tc *TCPClientInterface) UpdateStats(bytes uint64, isRx bool) {
tc.Mutex.Lock()
defer tc.Mutex.Unlock()
now := time.Now()
if isRx {
tc.RxBytes += bytes
tc.lastRx = now
debug.Log(debug.DEBUG_TRACE, "Interface RX stats", "name", tc.Name, "bytes", bytes, "total", tc.RxBytes, "last", tc.lastRx)
} else {
tc.TxBytes += bytes
tc.lastTx = now
debug.Log(debug.DEBUG_TRACE, "Interface TX stats", "name", tc.Name, "bytes", bytes, "total", tc.TxBytes, "last", tc.lastTx)
}
}
func (tc *TCPClientInterface) GetStats() (tx uint64, rx uint64, lastTx time.Time, lastRx time.Time) {
tc.Mutex.RLock()
defer tc.Mutex.RUnlock()
return tc.TxBytes, tc.RxBytes, tc.lastTx, tc.lastRx
}
type TCPServerInterface struct { type TCPServerInterface struct {
BaseInterface BaseInterface
connections map[string]net.Conn connections map[string]net.Conn
@@ -686,18 +627,6 @@ func (ts *TCPServerInterface) Stop() error {
return nil return nil
} }
func (ts *TCPServerInterface) GetTxBytes() uint64 {
ts.Mutex.RLock()
defer ts.Mutex.RUnlock()
return ts.TxBytes
}
func (ts *TCPServerInterface) GetRxBytes() uint64 {
ts.Mutex.RLock()
defer ts.Mutex.RUnlock()
return ts.RxBytes
}
func (ts *TCPServerInterface) handleConnection(conn net.Conn) { func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
addr := conn.RemoteAddr().String() addr := conn.RemoteAddr().String()
ts.Mutex.Lock() ts.Mutex.Lock()
@@ -728,14 +657,7 @@ func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
return return
} }
ts.Mutex.Lock() ts.ProcessIncoming(buffer[:n])
ts.RxBytes += uint64(n) // #nosec G115
callback := ts.packetCallback
ts.Mutex.Unlock()
if callback != nil {
callback(buffer[:n], ts)
}
} }
} }
@@ -758,7 +680,6 @@ func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
} }
ts.Mutex.Lock() ts.Mutex.Lock()
ts.TxBytes += uint64(len(frame)) // #nosec G115
conns := make([]net.Conn, 0, len(ts.connections)) conns := make([]net.Conn, 0, len(ts.connections))
for _, conn := range ts.connections { for _, conn := range ts.connections {
conns = append(conns, conn) conns = append(conns, conn)

View File

@@ -87,30 +87,6 @@ func (ui *UDPInterface) Detach() {
}) })
} }
func (ui *UDPInterface) Send(data []byte, addr string) error {
debug.Log(debug.DEBUG_ALL, "UDP interface sending bytes", "name", ui.Name, "bytes", len(data))
if !ui.IsEnabled() {
return fmt.Errorf("interface not enabled")
}
if ui.targetAddr == nil {
return fmt.Errorf("no target address configured")
}
ui.Mutex.Lock()
ui.TxBytes += uint64(len(data))
ui.Mutex.Unlock()
_, err := ui.conn.WriteTo(data, ui.targetAddr)
if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "UDP interface write failed", "name", ui.Name, "error", err)
} else {
debug.Log(debug.DEBUG_ALL, "UDP interface sent bytes successfully", "name", ui.Name, "bytes", len(data))
}
return err
}
func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) { func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) {
ui.Mutex.Lock() ui.Mutex.Lock()
defer ui.Mutex.Unlock() defer ui.Mutex.Unlock()
@@ -143,10 +119,6 @@ func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
return fmt.Errorf("UDP write failed: %v", err) return fmt.Errorf("UDP write failed: %v", err)
} }
ui.Mutex.Lock()
ui.TxBytes += uint64(len(data))
ui.Mutex.Unlock()
return nil return nil
} }
@@ -269,20 +241,14 @@ func (ui *UDPInterface) readLoop() {
} }
ui.Mutex.Lock() ui.Mutex.Lock()
// #nosec G115 - Network read sizes are always positive and within safe range
ui.RxBytes += uint64(n)
// Auto-discover target address from first packet if not set // Auto-discover target address from first packet if not set
if ui.targetAddr == nil { if ui.targetAddr == nil {
debug.Log(debug.DEBUG_ALL, "UDP interface discovered peer", "name", ui.Name, "peer", remoteAddr.String()) debug.Log(debug.DEBUG_ALL, "UDP interface discovered peer", "name", ui.Name, "peer", remoteAddr.String())
ui.targetAddr = remoteAddr ui.targetAddr = remoteAddr
} }
callback := ui.packetCallback
ui.Mutex.Unlock() ui.Mutex.Unlock()
if callback != nil { ui.ProcessIncoming(buffer[:n])
callback(buffer[:n], ui)
}
} }
} }

View File

@@ -92,7 +92,12 @@ func (wsi *WebSocketInterface) Start() error {
defer wsi.Mutex.Unlock() defer wsi.Mutex.Unlock()
if wsi.ws.Truthy() { if wsi.ws.Truthy() {
return fmt.Errorf("WebSocket already started") readyState := wsi.ws.Get("readyState").Int()
if readyState == 1 { // OPEN
return nil
}
// If connecting, closing or closed, clean up first
wsi.closeWebSocket()
} }
ws := js.Global().Get("WebSocket").New(wsi.wsURL) ws := js.Global().Get("WebSocket").New(wsi.wsURL)
@@ -127,30 +132,39 @@ func (wsi *WebSocketInterface) Start() error {
event := args[0] event := args[0]
data := event.Get("data") data := event.Get("data")
var packet []byte handlePacket := func(buf js.Value) {
if data.Type() == js.TypeString { array := js.Global().Get("Uint8Array").New(buf)
packet = []byte(data.String())
} else if data.Type() == js.TypeObject {
array := js.Global().Get("Uint8Array").New(data)
length := array.Get("length").Int() length := array.Get("length").Int()
packet = make([]byte, length) if length < 1 {
return
}
packet := make([]byte, length)
js.CopyBytesToGo(packet, array) js.CopyBytesToGo(packet, array)
debug.Log(debug.DEBUG_VERBOSE, "WASM WebSocket received binary data", "name", wsi.Name, "length", length, "first_byte", fmt.Sprintf("0x%02x", packet[0]))
wsi.ProcessIncoming(packet)
}
if data.Type() == js.TypeString {
packet := []byte(data.String())
debug.Log(debug.DEBUG_TRACE, "WebSocket received string data", "name", wsi.Name, "length", len(packet))
wsi.ProcessIncoming(packet)
} else if data.InstanceOf(js.Global().Get("ArrayBuffer")) {
handlePacket(data)
} else if data.InstanceOf(js.Global().Get("Blob")) {
// Handle Blob by converting to ArrayBuffer
data.Call("arrayBuffer").Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) > 0 {
handlePacket(args[0])
}
return nil
}))
} else if data.Type() == js.TypeObject {
// Fallback for other object types that might be TypedArrays
handlePacket(data)
} else { } else {
debug.Log(debug.DEBUG_ERROR, "Unknown WebSocket message type", "type", data.Type().String()) debug.Log(debug.DEBUG_ERROR, "Unknown WebSocket message type", "type", data.Type().String())
return nil
} }
if len(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 return nil
})) }))
@@ -168,8 +182,10 @@ func (wsi *WebSocketInterface) Start() error {
debug.Log(debug.DEBUG_INFO, "WebSocket closed", "name", wsi.Name) debug.Log(debug.DEBUG_INFO, "WebSocket closed", "name", wsi.Name)
if wsi.Enabled && !wsi.Detached { if wsi.Enabled && !wsi.Detached {
go func() {
time.Sleep(WS_RECONNECT_DELAY) time.Sleep(WS_RECONNECT_DELAY)
go wsi.Start() _ = wsi.Start()
}()
} }
return nil return nil
@@ -197,15 +213,7 @@ func (wsi *WebSocketInterface) closeWebSocket() {
wsi.Online = false wsi.Online = false
} }
func (wsi *WebSocketInterface) Send(data []byte, addr string) error { func (wsi *WebSocketInterface) ProcessOutgoing(data []byte) error {
if !wsi.IsEnabled() {
return fmt.Errorf("interface not enabled")
}
wsi.Mutex.Lock()
wsi.TxBytes += uint64(len(data))
wsi.Mutex.Unlock()
if !wsi.connected { if !wsi.connected {
wsi.Mutex.Lock() wsi.Mutex.Lock()
wsi.messageQueue = append(wsi.messageQueue, data) wsi.messageQueue = append(wsi.messageQueue, data)
@@ -234,10 +242,6 @@ func (wsi *WebSocketInterface) sendWebSocketMessage(data []byte) error {
return nil return nil
} }
func (wsi *WebSocketInterface) ProcessOutgoing(data []byte) error {
return wsi.Send(data, "")
}
func (wsi *WebSocketInterface) GetConn() net.Conn { func (wsi *WebSocketInterface) GetConn() net.Conn {
return nil return nil
} }

View File

@@ -144,8 +144,6 @@ func (p *Packet) Pack() error {
header := []byte{flags, p.Hops} header := []byte{flags, p.Hops}
debug.Log(debug.DEBUG_TRACE, "Created packet header", "flags", fmt.Sprintf("%08b", flags), "hops", p.Hops) debug.Log(debug.DEBUG_TRACE, "Created packet header", "flags", fmt.Sprintf("%08b", flags), "hops", p.Hops)
header = append(header, p.DestinationHash...)
if p.HeaderType == HeaderType2 { if p.HeaderType == HeaderType2 {
if p.TransportID == nil { if p.TransportID == nil {
return errors.New("transport ID required for header type 2") return errors.New("transport ID required for header type 2")
@@ -154,6 +152,8 @@ func (p *Packet) Pack() error {
debug.Log(debug.DEBUG_ALL, "Added transport ID to header", "transport_id", fmt.Sprintf("%x", p.TransportID)) debug.Log(debug.DEBUG_ALL, "Added transport ID to header", "transport_id", fmt.Sprintf("%x", p.TransportID))
} }
header = append(header, p.DestinationHash...)
header = append(header, p.Context) header = append(header, p.Context)
debug.Log(debug.DEBUG_PACKETS, "Final header length", "bytes", len(header)) debug.Log(debug.DEBUG_PACKETS, "Final header length", "bytes", len(header))
@@ -187,12 +187,12 @@ func (p *Packet) Unpack() error {
dstLen := 16 // Truncated hash length dstLen := 16 // Truncated hash length
if p.HeaderType == HeaderType2 { if p.HeaderType == HeaderType2 {
// Header Type 2: Header(2) + DestHash(16) + TransportID(16) + Context(1) + Data // Header Type 2: Header(2) + TransportID(16) + DestHash(16) + Context(1) + Data
if len(p.Raw) < 2*dstLen+3 { if len(p.Raw) < 2*dstLen+3 {
return errors.New("packet too short for header type 2") return errors.New("packet too short for header type 2")
} }
p.DestinationHash = p.Raw[2 : dstLen+2] // Destination hash first p.TransportID = p.Raw[2 : dstLen+2] // Transport ID first
p.TransportID = p.Raw[dstLen+2 : 2*dstLen+2] // Transport ID second p.DestinationHash = p.Raw[dstLen+2 : 2*dstLen+2] // Destination hash second
p.Context = p.Raw[2*dstLen+2] p.Context = p.Raw[2*dstLen+2]
p.Data = p.Raw[2*dstLen+3:] p.Data = p.Raw[2*dstLen+3:]
} else { } else {

View File

@@ -0,0 +1,40 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package packet
import (
"testing"
)
func FuzzPacketUnpack(f *testing.F) {
// Add some valid packets as seeds
p1 := &Packet{
HeaderType: HeaderType1,
PacketType: PacketTypeData,
DestinationType: 0x01,
DestinationHash: make([]byte, 16),
Context: ContextNone,
Data: []byte("hello"),
}
if err := p1.Pack(); err == nil {
f.Add(p1.Raw)
}
p2 := &Packet{
HeaderType: HeaderType2,
PacketType: PacketTypeAnnounce,
TransportID: make([]byte, 16),
DestinationHash: make([]byte, 16),
Context: ContextNone,
Data: []byte("announce"),
}
if err := p2.Pack(); err == nil {
f.Add(p2.Raw)
}
f.Fuzz(func(t *testing.T, data []byte) {
p := &Packet{Raw: data}
// We don't care about the error, just that it doesn't panic
_ = p.Unpack()
})
}

View File

@@ -0,0 +1,39 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package transport
import (
"runtime"
"testing"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
)
func TestTransportLeak(t *testing.T) {
// Baseline goroutine count
runtime.GC()
baseline := runtime.NumGoroutine()
cfg := &common.ReticulumConfig{}
// Create and close many transport instances
for i := 0; i < 100; i++ {
tr := NewTransport(cfg)
// Give it a tiny bit of time to start the goroutine
time.Sleep(1 * time.Millisecond)
tr.Close()
}
// Wait for goroutines to finish
time.Sleep(100 * time.Millisecond)
runtime.GC()
final := runtime.NumGoroutine()
// We allow a small margin for other system goroutines,
// but 100 leaks would be very obvious.
if final > baseline+5 {
t.Errorf("Potential goroutine leak: baseline %d, final %d", baseline, final)
}
}

View File

@@ -0,0 +1,66 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package transport
import (
"fmt"
"testing"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
)
type MockInterface struct {
common.BaseInterface
sentData [][]byte
dropRate float64 // 0.0 to 1.0
onReceive func([]byte)
}
func (m *MockInterface) Send(data []byte, destination string) error {
m.Mutex.Lock()
defer m.Mutex.Unlock()
// Simulate packet loss
if m.dropRate > 0 {
// In a real test we'd use rand.Float64()
// For deterministic testing, let's just record everything for now
}
m.sentData = append(m.sentData, data)
return nil
}
func (m *MockInterface) Receive(data []byte) {
if m.onReceive != nil {
m.onReceive(data)
}
}
func TestTransportNetworkSimulation(t *testing.T) {
cfg := &common.ReticulumConfig{}
tr := NewTransport(cfg)
defer tr.Close()
iface1 := &MockInterface{BaseInterface: common.NewBaseInterface("iface1", common.IF_TYPE_UDP, true)}
iface1.Enable()
iface2 := &MockInterface{BaseInterface: common.NewBaseInterface("iface2", common.IF_TYPE_UDP, true)}
iface2.Enable()
tr.RegisterInterface(iface1.GetName(), iface1)
tr.RegisterInterface(iface2.GetName(), iface2)
// Simulate receiving an announce on iface1
// [header][hops][dest_hash(16)][payload...]
announcePacket := make([]byte, 100)
announcePacket[0] = PACKET_TYPE_ANNOUNCE
announcePacket[1] = 0 // 0 hops
copy(announcePacket[2:18], []byte("destination_hash"))
// Mock the handler to avoid complex identity logic in this basic test
tr.HandlePacket(announcePacket, iface1)
// In a real scenario, it would be rebroadcast to iface2
// But HandlePacket runs in a goroutine, so we'd need to wait or use a better mock
fmt.Println("Network simulation test initialized")
}

View File

@@ -18,7 +18,6 @@ import (
"git.quad4.io/Networks/Reticulum-Go/pkg/debug" "git.quad4.io/Networks/Reticulum-Go/pkg/debug"
"git.quad4.io/Networks/Reticulum-Go/pkg/destination" "git.quad4.io/Networks/Reticulum-Go/pkg/destination"
"git.quad4.io/Networks/Reticulum-Go/pkg/identity" "git.quad4.io/Networks/Reticulum-Go/pkg/identity"
"git.quad4.io/Networks/Reticulum-Go/pkg/interfaces"
"git.quad4.io/Networks/Reticulum-Go/pkg/packet" "git.quad4.io/Networks/Reticulum-Go/pkg/packet"
"git.quad4.io/Networks/Reticulum-Go/pkg/pathfinder" "git.quad4.io/Networks/Reticulum-Go/pkg/pathfinder"
"git.quad4.io/Networks/Reticulum-Go/pkg/rate" "git.quad4.io/Networks/Reticulum-Go/pkg/rate"
@@ -324,6 +323,12 @@ func GetTransportInstance() *Transport {
return transportInstance return transportInstance
} }
func SetTransportInstance(t *Transport) {
transportMutex.Lock()
defer transportMutex.Unlock()
transportInstance = t
}
func (t *Transport) RegisterInterface(name string, iface common.NetworkInterface) error { func (t *Transport) RegisterInterface(name string, iface common.NetworkInterface) error {
t.mutex.Lock() t.mutex.Lock()
defer t.mutex.Unlock() defer t.mutex.Unlock()
@@ -577,8 +582,11 @@ func (t *Transport) RequestPath(destinationHash []byte, onInterface string, tag
pathRequestData = append(destinationHash, tag...) pathRequestData = append(destinationHash, tag...)
} }
destHashFull := sha256.Sum256([]byte("rnstransport.path.request")) pathRequestName := "rnstransport.path.request"
pathRequestDestHash := destHashFull[:common.SIZE_16] nameHashFull := sha256.Sum256([]byte(pathRequestName))
nameHash10 := nameHashFull[:10]
finalHashFull := sha256.Sum256(nameHash10)
pathRequestDestHash := finalHashFull[:16]
pkt := packet.NewPacket( pkt := packet.NewPacket(
packet.DestinationPlain, packet.DestinationPlain,
@@ -586,11 +594,12 @@ func (t *Transport) RequestPath(destinationHash []byte, onInterface string, tag
0x00, 0x00,
0x00, 0x00,
packet.PropagationBroadcast, packet.PropagationBroadcast,
0x01, 0x00, // Header Type 1
pathRequestDestHash, nil,
false, false,
0x00, 0x00,
) )
pkt.DestinationHash = pathRequestDestHash
if err := pkt.Pack(); err != nil { if err := pkt.Pack(); err != nil {
return fmt.Errorf("failed to pack path request: %w", err) return fmt.Errorf("failed to pack path request: %w", err)
@@ -881,11 +890,6 @@ func (t *Transport) HandlePacket(data []byte, iface common.NetworkInterface) {
debug.Log(debug.DEBUG_ERROR, "67-byte packet detected", "header", fmt.Sprintf(common.STR_FMT_HEX, headerByte), "packet_type_bits", fmt.Sprintf(common.STR_FMT_HEX, packetType), "first_32_bytes", fmt.Sprintf("%x", data[:common.SIZE_32])) debug.Log(debug.DEBUG_ERROR, "67-byte packet detected", "header", fmt.Sprintf(common.STR_FMT_HEX, headerByte), "packet_type_bits", fmt.Sprintf(common.STR_FMT_HEX, packetType), "first_32_bytes", fmt.Sprintf("%x", data[:common.SIZE_32]))
} }
if tcpIface, ok := iface.(*interfaces.TCPClientInterface); ok {
tcpIface.UpdateStats(uint64(len(data)), true)
debug.Log(debug.DEBUG_PACKETS, "Updated TCP interface stats", "rx_bytes", len(data))
}
dataCopy := make([]byte, len(data)) dataCopy := make([]byte, len(data))
copy(dataCopy, data) copy(dataCopy, data)
@@ -1110,16 +1114,15 @@ func (t *Transport) handleAnnouncePacket(data []byte, iface common.NetworkInterf
// Register the path from this announce // Register the path from this announce
// The destination is reachable via the interface that received this announce // The destination is reachable via the interface that received this announce
if iface != nil { if iface != nil {
// Use unlocked version since we may be called in a locked context
t.mutex.Lock() t.mutex.Lock()
t.updatePathUnlocked(destinationHash, nil, iface.GetName(), hopCount) t.updatePathUnlocked(destinationHash, nil, iface.GetName(), hopCount+1)
t.mutex.Unlock() t.mutex.Unlock()
debug.Log(debug.DEBUG_INFO, "Registered path", "hash", fmt.Sprintf("%x", destinationHash), "interface", iface.GetName(), "hops", hopCount) debug.Log(debug.DEBUG_INFO, "Registered path", "hash", fmt.Sprintf("%x", destinationHash), "interface", iface.GetName(), "hops", hopCount+1)
} }
// Notify handlers first, regardless of forwarding limits // Notify handlers first, regardless of forwarding limits
debug.Log(debug.DEBUG_INFO, "Notifying announce handlers", "destHash", fmt.Sprintf("%x", destinationHash), "appDataLen", len(appData)) debug.Log(debug.DEBUG_INFO, "Notifying announce handlers", "destHash", fmt.Sprintf("%x", destinationHash), "appDataLen", len(appData))
t.notifyAnnounceHandlers(destinationHash, id, appData, hopCount) t.notifyAnnounceHandlers(destinationHash, id, appData, hopCount+1)
debug.Log(debug.DEBUG_INFO, "Announce handlers notified") debug.Log(debug.DEBUG_INFO, "Announce handlers notified")
// Don't forward if max hops reached // Don't forward if max hops reached
@@ -1376,7 +1379,7 @@ func (t *Transport) InitializePathRequestHandler() error {
return errors.New("transport identity not initialized") return errors.New("transport identity not initialized")
} }
pathRequestDest, err := destination.New(t.transportIdentity, destination.IN, destination.PLAIN, "rnstransport", t, "path", "request") pathRequestDest, err := destination.New(nil, destination.IN, destination.PLAIN, "rnstransport", t, "path", "request")
if err != nil { if err != nil {
return fmt.Errorf("failed to create path request destination: %w", err) return fmt.Errorf("failed to create path request destination: %w", err)
} }
@@ -1692,6 +1695,14 @@ func (l *Link) HandleResource(resource interface{}) bool {
} }
} }
// SetIdentity sets the identity for the Transport.
func (t *Transport) SetIdentity(id *identity.Identity) {
t.mutex.Lock()
defer t.mutex.Unlock()
t.transportIdentity = id
}
// Start initializes the Transport.
func (t *Transport) Start() error { func (t *Transport) Start() error {
t.mutex.Lock() t.mutex.Lock()
defer t.mutex.Unlock() defer t.mutex.Unlock()

View File

@@ -3,8 +3,12 @@ package transport
import ( import (
"bytes" "bytes"
"testing" "testing"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common" "git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/destination"
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
) )
type mockInterface struct { type mockInterface struct {
@@ -118,3 +122,68 @@ func TestTransportStatus(t *testing.T) {
t.Error("Path should be responsive again") t.Error("Path should be responsive again")
} }
} }
func TestAnnounceHopCount(t *testing.T) {
config := common.DefaultConfig()
tr := NewTransport(config)
defer tr.Close()
iface := &mockInterface{}
iface.Name = "wasm0"
iface.Enabled = true
_ = tr.RegisterInterface("wasm0", iface)
// Create an identity for the announce
id, _ := identity.New()
// Create a destination to get a valid hash for this identity
// NewAnnouncePacket uses "reticulum-go.node" by default
dest, _ := destination.New(id, destination.IN, destination.SINGLE, "reticulum-go.node", tr)
destHash := dest.GetHash()
// Create a raw announce packet manually to control hop count
// Header(2) + DestHash(16) + Context(1) + Payload...
// Header: 0x21 (Announce, Header Type 1, Broadcast, Destination Type Single)
// Hop count: 0
raw := make([]byte, 2+16+1+148) // header + dest + context + min_announce_payload
raw[0] = 0x21
raw[1] = 0 // Initial hop count
copy(raw[2:18], destHash)
raw[18] = 0 // context
// Announce payload: pubKey(64) + nameHash(10) + randomHash(10) + signature(64)
payload := raw[19:]
copy(payload[0:64], id.GetPublicKey())
// Name hash, random hash, signature - filling with dummy data but valid length
// Normally we would sign it properly, but handleAnnouncePacket validates it.
// Actually, handleAnnouncePacket WILL fail if signature is invalid.
// Use NewAnnouncePacket to get a valid signed packet
transportID := make([]byte, 16)
annPkt, err := packet.NewAnnouncePacket(destHash, id, []byte("test"), transportID)
if err != nil {
t.Fatalf("NewAnnouncePacket failed: %v", err)
}
annRaw, err := annPkt.Serialize()
if err != nil {
t.Fatalf("Serialize failed: %v", err)
}
// Override hop count to 0 to simulate neighbor
annRaw[1] = 0
// Handle the packet
tr.HandlePacket(annRaw, iface)
// Wait a bit for the async processing
time.Sleep(100 * time.Millisecond)
// Check stored hops
if !tr.HasPath(destHash) {
t.Fatal("Path not registered from announce")
}
hops := tr.HopsTo(destHash)
if hops != 1 {
t.Errorf("Expected 1 hop for neighbor (received 0), got %d", hops)
}
}

View File

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