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

View File

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

3
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {
ui.Mutex.Lock()
defer ui.Mutex.Unlock()
@@ -143,10 +119,6 @@ func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
return fmt.Errorf("UDP write failed: %v", err)
}
ui.Mutex.Lock()
ui.TxBytes += uint64(len(data))
ui.Mutex.Unlock()
return nil
}
@@ -269,20 +241,14 @@ func (ui *UDPInterface) readLoop() {
}
ui.Mutex.Lock()
// #nosec G115 - Network read sizes are always positive and within safe range
ui.RxBytes += uint64(n)
// Auto-discover target address from first packet if not set
if ui.targetAddr == nil {
debug.Log(debug.DEBUG_ALL, "UDP interface discovered peer", "name", ui.Name, "peer", remoteAddr.String())
ui.targetAddr = remoteAddr
}
callback := ui.packetCallback
ui.Mutex.Unlock()
if callback != nil {
callback(buffer[:n], ui)
}
ui.ProcessIncoming(buffer[:n])
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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