1 Commits

Author SHA1 Message Date
d446c3f3df Add microVM setup files 2025-11-07 13:19:56 -06:00
122 changed files with 2778 additions and 14603 deletions

7
.deepsource.toml Normal file
View File

@@ -0,0 +1,7 @@
version = 1
[[analyzers]]
name = "go"
[analyzers.meta]
import_root = "github.com/Sudo-Ivan/Reticulum-Go"

View File

@@ -1,36 +0,0 @@
# Binaries and build folders
bin/
*.exe
*.dll
*.so
*.dylib
# Go modules' cache
vendor/
# Local test/coverage/log artifacts
*.test
*.out
*.log
logs/
coverage.out
# Environment and secret files
.env
# User/IDE/Editor config
.idea/
.vscode/
*.swp
.DS_Store
Thumbs.db
# Example and generated files
examples/
*.json
# SBOM and analysis artifacts
bom.json
dependency-results.sbom.json
*.sbom.json

View File

@@ -1,27 +0,0 @@
name: Bearer
on:
push:
branches:
- main
- master
pull_request:
branches:
- main
- master
workflow_dispatch:
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Checkout Source
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Run Bearer Security Scanner
uses: https://git.quad4.io/actions/bearer-action@828eeb928ce2f4a7ca5ed57fb8b59508cb8c79bc # v2
with:
path: ./

View File

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

View File

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

View File

@@ -1,84 +0,0 @@
name: Go Test Multi-Platform
on:
push:
branches:
- main
- master
pull_request:
branches:
- main
- master
permissions:
contents: read
jobs:
test:
name: Test (${{ matrix.os }}, ${{ matrix.goarch }})
strategy:
matrix:
include:
# AMD64 testing on Linux
- os: ubuntu-latest
goarch: amd64
# ARM64 testing on Linux
- os: ubuntu-latest
goarch: arm64
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Source
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Go 1.25
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: Cache Go modules
uses: https://git.quad4.io/actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ matrix.goarch }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ matrix.goarch }}-
- name: Set up Node.js
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
uses: https://git.quad4.io/actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
node-version: '22'
- name: Run tests
run: task test
- name: Run tests with race detector (Linux AMD64 only)
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

View File

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

View File

@@ -1,53 +0,0 @@
name: Generate SBOM
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
generate-sbom:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
- name: Setup Go
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '1.25.5'
- name: Setup Task
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
with:
version: '3.46.3'
- name: Install dependencies
run: task deps
- name: Install Trivy
run: task trivy:install
- name: Generate SBOM
run: task sbom
- name: Commit and Push Changes
run: |
git config --global user.name "Gitea Action"
git config --global user.email "actions@noreply.quad4.io"
git remote set-url origin https://${{ secrets.GITEA_TOKEN }}@git.quad4.io/${{ github.repository }}.git
git fetch origin main || git fetch origin master
git checkout main || git checkout master
git add sbom/
if ! git diff --quiet || ! git diff --staged --quiet; then
git commit -m "Auto-update SBOM [skip ci]"
git push origin main || git push origin master
fi
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}

43
.github/workflows/benchmark-gc.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: Benchmark GC Performance
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
workflow_dispatch:
permissions:
contents: read
actions: write
jobs:
benchmark:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: '1.25'
- name: Cache Go modules
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Install dependencies
run: go mod download
- name: Run benchmark (standard GC)
run: make bench
- name: Run benchmark (experimental GC)
run: make bench-experimental

87
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,87 @@
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]
exclude:
- goos: darwin
goarch: arm
runs-on: ubuntu-latest
outputs:
build_complete: ${{ steps.build_step.outcome == 'success' }}
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: '1.25'
- name: Build
id: build_step
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarch == 'arm' && '6' || '' }}
run: |
output_name="reticulum-go-${GOOS}-${GOARCH}"
if [ "$GOOS" = "windows" ]; then
output_name+=".exe"
fi
go build -v -ldflags="-s -w" -o "${output_name}" ./cmd/reticulum-go
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"
fi
sha256sum "${output_name}" > "${output_name}.sha256"
echo "Calculated SHA256 for ${output_name}"
- name: Upload Artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
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: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
path: ./release-assets
- name: List downloaded files (for debugging)
run: ls -R ./release-assets
- name: Create GitHub Release
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
with:
files: ./release-assets/*/*

103
.github/workflows/go-test.yml vendored Normal file
View File

@@ -0,0 +1,103 @@
name: Go Test Multi-Platform
on:
push:
branches:
- main
- master
pull_request:
branches:
- main
- master
permissions:
contents: read
jobs:
test:
name: Test (${{ matrix.os }}, ${{ matrix.goarch }})
strategy:
matrix:
include:
# AMD64 testing across major platforms
- os: ubuntu-latest
goarch: amd64
- os: windows-latest
goarch: amd64
- os: macos-latest
goarch: amd64
# ARM64 testing on supported platforms
- os: ubuntu-latest
goarch: arm64
- os: macos-latest
goarch: arm64
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Source
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Go 1.25
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: '1.25'
- name: Cache Go modules
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ matrix.goarch }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ matrix.goarch }}-
- name: Run Go tests
run: go test -v ./...
- name: Run Go tests with race detector (Linux AMD64 only)
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
run: go test -race -v ./...
- name: Test build (ensure compilation works)
run: |
# Test that we can build for the current platform
echo "Testing build for current platform (${{ matrix.os }}, ${{ matrix.goarch }})..."
go build -v ./cmd/reticulum-go
- name: Test binary execution (Linux/macOS)
if: matrix.os != 'windows-latest'
run: |
echo "Testing binary execution on (${{ matrix.os }}, ${{ matrix.goarch }})..."
timeout 5s ./reticulum-go || echo "Binary started successfully (timeout expected)"
- name: Test binary execution (Windows)
if: matrix.os == 'windows-latest'
run: |
echo "Testing binary execution on (${{ matrix.os }}, ${{ matrix.goarch }})..."
# Start the binary and kill after 5 seconds to verify it can start
Start-Process -FilePath ".\reticulum-go.exe" -NoNewWindow
Start-Sleep -Seconds 5
Stop-Process -Name "reticulum-go" -Force -ErrorAction SilentlyContinue
echo "Binary started successfully"
shell: pwsh
- name: Test cross-compilation (AMD64 runners only)
if: matrix.goarch == 'amd64'
run: |
echo "Testing ARM64 cross-compilation from AMD64..."
go build -v ./cmd/reticulum-go
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..."
go build -v ./cmd/reticulum-go
env:
GOOS: linux
GOARCH: arm
GOARM: 6

View File

@@ -20,8 +20,8 @@ jobs:
GO111MODULE: on GO111MODULE: on
steps: steps:
- name: Checkout Source - name: Checkout Source
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Run Gosec Security Scanner - name: Run Gosec Security Scanner
uses: https://git.quad4.io/actions/gosec@c073629009897d89e03229bc81232c7375892086 uses: securego/gosec@master
with: with:
args: ./... args: ./...

View File

@@ -0,0 +1,31 @@
name: Performance Monitor
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
workflow_dispatch:
jobs:
performance-monitor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: '1.25'
- name: Build
run: |
go build -o bin/reticulum-go ./cmd/reticulum-go
- name: Run Performance Monitor
id: monitor
run: |
cp tests/scripts/monitor_performance.sh .
chmod +x monitor_performance.sh
./monitor_performance.sh

View File

@@ -14,20 +14,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Go - name: Set up Go
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: '1.25' go-version: '1.25'
- name: Setup Task
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
with:
version: '3.46.3'
- name: Install revive - name: Install revive
run: go install github.com/mgechev/revive@latest run: go install github.com/mgechev/revive@latest
- name: Run lint - name: Run revive
run: task lint run: |
revive -config revive.toml -formatter stylish ./...

View File

@@ -30,10 +30,10 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Go - name: Set up Go
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
with: with:
go-version: '1.24' go-version: '1.24'
@@ -58,7 +58,7 @@ jobs:
fi fi
- name: Upload Artifact - name: Upload Artifact
uses: https://git.quad4.io/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: ${{ matrix.name }} name: ${{ matrix.name }}
path: bin/${{ matrix.output }}* path: bin/${{ matrix.output }}*

37
.gitignore vendored
View File

@@ -1,32 +1,9 @@
# Build artifacts logs/
*.log
.env
.json
bin/ bin/
# Test coverage reports examples/
coverage.out
# Log files
*.log
logs/
# Local environment variables
.env
# JSON assets and auto-generated exports
*.json
# Example files, not adding them just yet.
/examples/*
!/examples/wasm/
# OS / Editor files
.DS_Store # macOS Finder metadata
Thumbs.db # Windows Explorer thumbnail cache
# IDE / Editor config directories
.idea/ # JetBrains IDEs
.vscode/ # Visual Studio Code
# Swap and test binaries
*.swp # Swap files (e.g. vim)
*.test # Go test binaries
test/compat/

View File

@@ -1,7 +1,35 @@
# Contributing # Contributing
Send issues, suggestions, `.patch` files, or any feedback to one of the preferred methods: Be good to each other.
## Communication
Feel free to join our telegram or matrix channels for this implementation.
- [Matrix](https://matrix.to/#/#reticulum-go-dev:matrix.org)
- [Telegram](https://t.me/reticulum_go)
## Usage of LLMs and other Generative AI tools
You should not use LLMs and other generative AI tools to write critical parts of the code. They can produce lots of security issues and outdated code when used incorrectly. You are not required to report that you are using these tools.
## Static Analysis Tools
You are welcome to use the following tools, however there are actions in place to ensure the code is linted and checked with gosec.
### Linting (optional)
[Revive](https://github.com/mgechev/revive)
```bash
revive -config revive.toml -formatter friendly ./pkg/* ./cmd/* ./internal/*
```
### Security (optional)
[Gosec](https://github.com/securego/gosec)
```bash
gosec ./...
```
1. Reticulum LXMF: `7cc8d66b4f6a0e0e49d34af7f6077b5a` - Ivan (main developer)
2. XMPP: `ivan@chat.quad4.io` - Ivan (main developer)
3. Email: `team@quad4.io` - Quad4 Team

View File

@@ -1,14 +0,0 @@
CONTRIBUTORS
This file lists all contributors to the Reticulum-Go project.
Sudo-Ivan
Total commits: 442
First contribution: 2024-12-30
Last contribution: 2026-01-01
Mike Coles
Total commits: 1
First contribution: 2025-08-07
Last contribution: 2025-08-07

17
LICENSE
View File

@@ -1,12 +1,9 @@
Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH Copyright 2024-2025 Sudo-Ivan / Quad4.io
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

146
Makefile Normal file
View File

@@ -0,0 +1,146 @@
GOCMD=go
GOBUILD=$(GOCMD) build
GOBUILD_EXPERIMENTAL=GOEXPERIMENT=greenteagc $(GOCMD) build
GOBUILD_RELEASE=CGO_ENABLED=0 $(GOCMD) build -ldflags="-s -w"
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOGET=$(GOCMD) get
GOMOD=$(GOCMD) mod
BINARY_NAME=reticulum-go
BINARY_UNIX=$(BINARY_NAME)_unix
BUILD_DIR=bin
MAIN_PACKAGE=./cmd/reticulum-go
ALL_PACKAGES=$$(go list ./... | grep -v /vendor/)
.PHONY: all build build-experimental experimental release lint bench bench-experimental bench-compare clean test coverage deps help tinygo-build tinygo-wasm
all: clean deps build test
build:
@mkdir -p $(BUILD_DIR)
$(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_PACKAGE)
build-experimental:
@mkdir -p $(BUILD_DIR)
$(GOBUILD_EXPERIMENTAL) -o $(BUILD_DIR)/$(BINARY_NAME)-experimental $(MAIN_PACKAGE)
experimental: build-experimental
release:
@mkdir -p $(BUILD_DIR)
$(GOBUILD_RELEASE) -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_PACKAGE)
lint:
revive -config revive.toml -formatter friendly ./pkg/* ./cmd/* ./internal/*
bench:
$(GOTEST) -bench=. -benchmem ./...
bench-experimental:
GOEXPERIMENT=greenteagc $(GOTEST) -bench=. -benchmem ./...
bench-compare: bench bench-experimental
clean:
@rm -rf $(BUILD_DIR)
$(GOCLEAN)
test:
$(GOTEST) -v $(ALL_PACKAGES)
coverage:
$(GOTEST) -coverprofile=coverage.out $(ALL_PACKAGES)
$(GOCMD) tool cover -html=coverage.out
deps:
$(GOMOD) download
$(GOMOD) verify
build-linux:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=linux GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm $(MAIN_PACKAGE)
build-windows:
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-arm64.exe $(MAIN_PACKAGE)
build-darwin:
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 $(MAIN_PACKAGE)
build-freebsd:
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-amd64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=freebsd GOARCH=386 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-386 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-arm64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-arm $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=freebsd GOARCH=riscv64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-riscv64 $(MAIN_PACKAGE)
build-openbsd:
CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-amd64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=openbsd GOARCH=386 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-386 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-arm64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=openbsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-arm $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=openbsd GOARCH=ppc64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-ppc64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=openbsd GOARCH=riscv64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-riscv64 $(MAIN_PACKAGE)
build-netbsd:
CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-amd64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=netbsd GOARCH=386 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-386 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm $(MAIN_PACKAGE)
build-arm:
CGO_ENABLED=0 GOOS=linux GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-arm $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-arm64 $(MAIN_PACKAGE)
build-riscv:
CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-riscv64 $(MAIN_PACKAGE)
build-all: build-linux build-windows build-darwin build-freebsd build-openbsd build-netbsd build-arm build-riscv
run:
@./$(BUILD_DIR)/$(BINARY_NAME)
tinygo-build:
@mkdir -p $(BUILD_DIR)
tinygo build -o $(BUILD_DIR)/$(BINARY_NAME)-tinygo -size short $(MAIN_PACKAGE)
tinygo-wasm:
@mkdir -p $(BUILD_DIR)
tinygo build -target wasm -o $(BUILD_DIR)/$(BINARY_NAME).wasm $(MAIN_PACKAGE)
install:
$(GOMOD) download
help:
@echo "Available targets:"
@echo " all - Clean, download dependencies, build and test"
@echo " build - Build binary"
@echo " build-experimental - Build binary with experimental features (GOEXPERIMENT=greenteagc)"
@echo " experimental - Alias for build-experimental"
@echo " release - Build stripped static binary for release"
@echo " lint - Run revive linter"
@echo " bench - Run benchmarks with standard GC"
@echo " bench-experimental - Run benchmarks with experimental GC"
@echo " bench-compare - Run benchmarks with both GC settings"
@echo " clean - Remove build artifacts"
@echo " test - Run tests"
@echo " coverage - Generate test coverage report"
@echo " deps - Download dependencies"
@echo " build-linux - Build for Linux (amd64, arm64, arm)"
@echo " build-windows- Build for Windows (amd64, arm64)"
@echo " build-darwin - Build for MacOS (amd64, arm64)"
@echo " build-freebsd- Build for FreeBSD (amd64, 386, arm64, arm, riscv64)"
@echo " build-openbsd- Build for OpenBSD (amd64, 386, arm64, arm, ppc64, riscv64)"
@echo " build-netbsd - Build for NetBSD (amd64, 386, arm64, arm)"
@echo " build-arm - Build for ARM architectures (arm, arm64)"
@echo " build-riscv - Build for RISC-V architecture (riscv64)"
@echo " build-all - Build for all platforms and architectures"
@echo " run - Run reticulum binary"
@echo " tinygo-build - Build binary with TinyGo compiler"
@echo " tinygo-wasm - Build WebAssembly binary with TinyGo compiler"
@echo " install - Install dependencies"

219
README.md
View File

@@ -1,215 +1,64 @@
[![Socket Badge](https://socket.dev/api/badge/go/package/github.com/sudo-ivan/reticulum-go?version=v0.4.0)](https://socket.dev/go/package/github.com/sudo-ivan/reticulum-go)
![Multi-Platform Tests](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/go-test.yml/badge.svg)
![Gosec Scan](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/gosec.yml/badge.svg)
[![Multi-Platform Build](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/build.yml/badge.svg)](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/build.yml)
[![Revive Linter](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/revive.yml/badge.svg)](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/revive.yml)
# Reticulum-Go # Reticulum-Go
[![Revive Lint](https://git.quad4.io/Networks/Reticulum-Go/actions/workflows/revive.yml/badge.svg?branch=main)](https://git.quad4.io/Networks/Reticulum-Go/actions/workflows/revive.yml) A Go implementation of the [Reticulum Network Protocol](https://github.com/markqvist/Reticulum).
[![Go Build](https://git.quad4.io/Networks/Reticulum-Go/actions/workflows/build.yml/badge.svg?branch=main)](https://git.quad4.io/Networks/Reticulum-Go/actions/workflows/build.yml)
[![Go Test](https://git.quad4.io/Networks/Reticulum-Go/actions/workflows/go-test.yml/badge.svg?branch=main)](https://git.quad4.io/Networks/Reticulum-Go/actions/workflows/go-test.yml)
[![Gosec](https://git.quad4.io/Networks/Reticulum-Go/actions/workflows/gosec.yml/badge.svg?branch=main)](https://git.quad4.io/Networks/Reticulum-Go/actions/workflows/gosec.yml)
[![Bearer](https://git.quad4.io/Networks/Reticulum-Go/actions/workflows/bearer.yml/badge.svg?branch=main)](https://git.quad4.io/Networks/Reticulum-Go/actions/workflows/bearer.yml)
A high-performance Go implementation of the [Reticulum Network Stack](https://github.com/markqvist/Reticulum) > [!WARNING]
> This project is currently in development and is not yet compatible with the Python reference implementation.
## Project Goals: ## Goals
- **Full Protocol Compatibility**: Maintain complete interoperability with the Python reference implementation - To be fully compatible with the Python reference implementation.
- **Cross-Platform Support**: Support for legacy and modern platforms across multiple architectures - Additional privacy and security features.
- **Performance**: Leverage Go's concurrency model and runtime for improved throughput and latency - Support for a broader range of platforms and architectures old and new.
- **More Privacy and Security**: Additional privacy and security features beyond the base specification
## Prerequisites
- Go 1.24 or later
- [Task](https://taskfile.dev/) for build automation
Note: You may need to set `alias task='go-task'` in your shell configuration to use `task` instead of `go-task`.
### Nix
If you have Nix installed, you can use the development shell which automatically provides all dependencies including Task:
```bash
nix develop
```
This will enter a development environment with Go and Task pre-configured.
## Quick Start ## Quick Start
### Building the Binary ### Prerequisites
- Go 1.24 or later
### Build
```bash ```bash
task build make build
``` ```
The compiled binary will be located in `bin/reticulum-go`. ### Run
### Running the Application
```bash ```bash
task run make run
``` ```
### Running Tests ### Test
```bash ```bash
task test make test
``` ```
## Development ## Embedded systems and WebAssembly
### Code Quality For building for WebAssembly and embedded systems, see the [tinygo branch](https://github.com/Sudo-Ivan/Reticulum-Go/tree/tinygo). Requires TinyGo 0.37.0+.
Format code:
```bash ```bash
task fmt make tinygo-build
make tinygo-wasm
``` ```
Run static analysis checks (formatting, vet, linting): ### Experimental Features
Build with experimental Green Tea GC (Go 1.25+):
```bash ```bash
task check make build-experimental
``` ```
### Testing ## Official Channels
Run all tests: - [Telegram](https://t.me/reticulum_go)
- [Matrix](https://matrix.to/#/#reticulum-go-dev:matrix.org)
```bash
task test
```
Run short tests only:
```bash
task test-short
```
Generate coverage report:
```bash
task coverage
```
### Benchmarking
Run benchmarks with standard GC:
```bash
task bench
```
Run benchmarks with experimental Green Tea GC:
```bash
task bench-experimental
```
Compare both GC implementations:
```bash
task bench-compare
```
## Tasks
The project uses [Task](https://taskfile.dev/) for all development and build operations.
```
| Task | Description |
|---------------------|------------------------------------------------------|
| default | Show available tasks |
| all | Clean, download dependencies, build and test |
| build | Build release binary (stripped, static) |
| debug | Build debug binary |
| build-experimental | Build with experimental Green Tea GC (Go 1.25+) |
| experimental | Alias for build-experimental |
| release | Build stripped static binary for release |
| fmt | Format Go code |
| fmt-check | Check if code is formatted (CI-friendly) |
| vet | Run go vet |
| lint | Run revive linter |
| scan | Run gosec security scanner |
| check | Run fmt-check, vet, and lint |
| bench | Run benchmarks with standard GC |
| bench-experimental | Run benchmarks with experimental GC |
| bench-compare | Run benchmarks with both GC settings |
| clean | Remove build artifacts |
| test | Run all tests |
| test-short | Run short tests only |
| test-race | Run tests with race detector |
| coverage | Generate test coverage report |
| checksum | Generate SHA256 checksum for binary |
| deps | Download and verify dependencies |
| mod-tidy | Tidy go.mod file |
| mod-verify | Verify dependencies |
| build-linux | Build for Linux (amd64, arm64, arm, riscv64) |
| build-all | Build for all Linux architectures |
| build-wasm | Build WebAssembly binary with standard Go compiler |
| test-wasm | Run WebAssembly tests using Node.js |
| run | Run with go run |
| tinygo-build | Build binary with TinyGo compiler |
| tinygo-wasm | Build WebAssembly binary with TinyGo |
| install | Install dependencies |
example: task build
```
## Cross-Platform Builds
### Linux Builds
Build for all Linux architectures:
```bash
task build-all
```
Build for specific Linux architecture:
```bash
task build-linux
```
## Embedded Systems and WebAssembly
For building for embedded systems, see the [tinygo branch](https://git.quad4.io/Networks/Reticulum-Go/src/branch/tinygo/). Requires TinyGo 0.37.0+.
Build WebAssembly binary with standard Go compiler:
```bash
task build-wasm
```
Run WebAssembly unit tests (requires Node.js):
```bash
task test-wasm
```
Build with TinyGo:
```bash
task tinygo-build
```
Build WebAssembly binary with TinyGo:
```bash
task tinygo-wasm
```
## Experimental Features
### Green Tea Garbage Collector
Build with experimental Green Tea GC (requires Go 1.25+):
```bash
task build-experimental
```
This enables the experimental garbage collector for performance evaluation and testing.
## License
This project is licensed under the [0BSD](LICENSE) license.

View File

@@ -1,13 +1,14 @@
# Security Policy # Security Policy
We use [Socket](https://socket.dev/), [Deepsource](https://deepsource.com/) and [gosec](https://github.com/securego/gosec) for this project.
## Supply Chain Security ## Supply Chain Security
- All actions are pinned to a full-length commit hash and have been forked to my Gitea instance in https://git.quad4.io/actions - All actions are pinned to a commit hash.
- BOM generation using CycloneDX
## Cryptography Dependencies ## Cryptography Dependencies
- golang.org/x/crypto `v0.46.0` for core cryptographic primitives - golang.org/x/crypto for core cryptographic primitives
- hkdf - hkdf
- curve25519 - curve25519
@@ -21,4 +22,4 @@
## Reporting a Vulnerability ## Reporting a Vulnerability
Refer to [https://quad4.io/security](https://quad4.io/security) for how to report vulnerabilities. Please report any security vulnerabilities using Github reporting tool or email to [rns@quad4.io](mailto:rns@quad4.io)

179
TODO.md
View File

@@ -1,11 +1,172 @@
Working on creating a project and issues to better track things. Check out https://git.quad4.io/Networks/Reticulum-Go/projects/2 ### Core Components (In Progress)
## Todo *Needs verification with Reticulum 1.0.0.*
- Created dedicated constants.go for each section. Last Updated: 2025-09-25
- Link Request/Response System (in-progress)
- Resource Transfer System (in-progress) - [x] Basic Configuration System
- Link Keep-Alive & Timeout (in-progress) - [x] Basic config structure
- Examples (in-progress) - [x] Default settings
- Tests - [x] Config file loading/saving
- Documentation - [x] Path management
- [x] Constants Definition (Testing required)
- [x] Packet constants
- [x] MTU constants
- [x] Header types
- [x] Additional protocol constants
- [x] Identity Management (Testing required)
- [x] Identity creation
- [x] Key pair generation
- [x] Identity storage/recall
- [x] Public key handling
- [x] Signature verification
- [x] Hash functions
- [x] Cryptographic Primitives (Testing required)
- [x] Ed25519
- [x] Curve25519
- [x] ~~AES-128-CBC~~ (Deprecated)
- [x] AES-256-CBC
- [x] SHA-256
- [x] HKDF
- [x] Secure random number generation
- [x] HMAC
- [x] Packet Handling (In Progress)
- [x] Packet creation
- [x] Packet validation
- [x] Basic proof system
- [x] Packet encryption/decryption
- [x] Signature verification
- [x] Announce packet structure
- [ ] Testing of packet encrypt/decrypt/sign/proof
- [ ] Cross-client packet compatibility
- [x] Transport Layer (In Progress)
- [x] Path management
- [x] Basic packet routing
- [x] Announce handling
- [x] Link management
- [x] Resource cleanup
- [x] Network layer integration
- [x] Basic announce implementation
- [ ] Testing announce from go client to python client
- [ ] Testing path finding and caching
- [ ] Announce propagation optimization
- [x] Channel System (Testing Required)
- [x] Channel creation and management
- [x] Message handling
- [x] Channel encryption
- [x] Channel authentication
- [x] Channel callbacks
- [x] Integration with Buffer system
- [ ] Testing with real network conditions
- [ ] Cross-client compatibility testing
- [x] Buffer System (Testing Required)
- [x] Raw channel reader/writer
- [x] Buffered stream implementation
- [x] Compression support
- [ ] Testing with Channel system
- [ ] Cross-client compatibility testing
- [x] Resolver System (Testing Required)
- [x] Name resolution
- [x] Cache management
- [x] Announce handling
- [x] Path resolution
- [x] Integration with Transport layer
- [ ] Testing with live network
- [ ] Cross-client compatibility testing
### Interface Implementation (In Progress)
- [x] UDP Interface
- [x] TCP Interface
- [x] Auto Interface
- [ ] Local Interface (In Progress)
- [ ] I2P Interface
- [ ] Pipe Interface
- [ ] RNode Interface
- [ ] RNode Multiinterface
- [ ] Serial Interface
- [ ] AX25KISS Interface
- [ ] Interface Discovery
- [ ] Interface Modes
- [ ] Full mode
- [ ] Gateway mode
- [ ] Access point mode
- [ ] Roaming mode
- [ ] Boundary mode
- [ ] Hot reloading interfaces
### Destination System (Testing required)
- [x] Destination creation
- [x] Destination types (IN/OUT)
- [x] Destination aspects
- [ ] Announce implementation (Fixing)
- [x] Ratchet support
- [x] Request handlers
### Link System (Testing required)
- [x] Link establishment
- [x] Link teardown
- [x] Basic packet transfer
- [x] Encryption/Decryption
- [x] Identity verification
- [x] Request/Response handling
- [x] Session key management
- [x] Link state tracking
### Resource System (Testing required)
- [x] Resource creation
- [x] Resource transfer
- [x] Compression
- [x] Progress tracking
- [x] Segmentation
- [x] Cleanup routines
### Compatibility
- [ ] RNS Utilities.
- [ ] Reticulum config.
### Testing & Validation (Priority)
- [ ] Unit tests for all components
- [ ] Identity tests
- [ ] Packet tests
- [ ] Transport tests
- [ ] Interface tests
- [ ] Announce tests
- [ ] Channel tests
- [ ] Buffer tests
- [ ] Resolver tests
- [ ] Link tests
- [ ] Resource tests
- [ ] Integration tests
- [ ] Go client to Go client
- [ ] Go client to Python client
- [ ] Interface compatibility
- [ ] Path finding and resolution
- [ ] Channel system end-to-end
- [ ] Buffer system performance
- [ ] Cross-client compatibility tests
- [ ] Performance and memory benchmarks
### Documentation
- [ ] API documentation
- [ ] Usage examples
### Cleanup
- [ ] Separate Cryptography from identity.go to their own files
- [ ] Move constants to their own files
- [ ] Remove default community interfaces in default config creation after testing
- [ ] Optimize announce packet creation and caching
- [ ] Improve debug logging system
### Experimental Features
- [x] Experimental Green Tea GC (build option) (Go 1.25+)
- [ ] MicroVM (firecracker)
- [ ] Kata Container Support

View File

@@ -1,698 +0,0 @@
version: '3'
env:
GOPRIVATE: git.quad4.io
vars:
GOCMD: go
BINARY_NAME: reticulum-go
BUILD_DIR: bin
MAIN_PACKAGE: ./cmd/reticulum-go
tasks:
default:
desc: Show available tasks
cmds:
- task --list
all:
desc: Clean, download dependencies, build and test
deps: [clean, deps, build, test]
build:
desc: Build release binary (no debug symbols, static)
env:
CGO_ENABLED: '0'
cmds:
- mkdir -p {{.BUILD_DIR}}
- '{{.GOCMD}} build -ldflags="-s -w" -o {{.BUILD_DIR}}/{{.BINARY_NAME}} {{.MAIN_PACKAGE}}'
debug:
desc: Build debug binary
cmds:
- mkdir -p {{.BUILD_DIR}}
- '{{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}} {{.MAIN_PACKAGE}}'
build-experimental:
desc: Build binary with experimental features (GOEXPERIMENT=greenteagc)
env:
GOEXPERIMENT: greenteagc
cmds:
- mkdir -p {{.BUILD_DIR}}
- '{{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-experimental {{.MAIN_PACKAGE}}'
experimental:
desc: Alias for build-experimental
cmds:
- task: build-experimental
release:
desc: Build stripped static binary for release (alias for build)
cmds:
- task: build
fmt:
desc: Format Go code
cmds:
- '{{.GOCMD}} fmt ./...'
fmt-check:
desc: Check if code is formatted (useful for CI)
cmds:
- '{{.GOCMD}} fmt -d ./... > fmt.diff 2>&1 || true'
- 'test -s fmt.diff && (echo "Code is not formatted. Run ''task fmt'' to fix." && cat fmt.diff && rm -f fmt.diff && exit 1) || (rm -f fmt.diff && exit 0)'
vet:
desc: Run go vet
cmds:
- '{{.GOCMD}} vet ./...'
lint:
desc: Run revive linter
cmds:
- revive -config revive.toml -formatter friendly ./pkg/* ./cmd/* ./internal/*
scan:
desc: Run gosec security scanner
cmds:
- gosec ./...
check:
desc: Run fmt-check, vet, lint, test-short, and scan with summary
cmds:
- |
FAILED_TASKS=""
FAIL_COUNT=0
TOTAL_TASKS=5
echo "--- Running all checks ---"
task fmt-check || { FAILED_TASKS="$FAILED_TASKS fmt-check"; FAIL_COUNT=$((FAIL_COUNT + 1)); }
task vet || { FAILED_TASKS="$FAILED_TASKS vet"; FAIL_COUNT=$((FAIL_COUNT + 1)); }
task lint || { FAILED_TASKS="$FAILED_TASKS lint"; FAIL_COUNT=$((FAIL_COUNT + 1)); }
task test-short || { FAILED_TASKS="$FAILED_TASKS test-short"; FAIL_COUNT=$((FAIL_COUNT + 1)); }
task scan || { FAILED_TASKS="$FAILED_TASKS scan"; FAIL_COUNT=$((FAIL_COUNT + 1)); }
echo "------------------------------------------"
if [ $FAIL_COUNT -eq 0 ]; then
echo "OK: All checks passed!"
elif [ $FAIL_COUNT -eq $TOTAL_TASKS ]; then
echo "ERROR: All tasks failed!"
echo "Failed tasks:$FAILED_TASKS"
exit 1
else
echo "ERROR: $FAIL_COUNT task(s) failed out of $TOTAL_TASKS!"
echo "Failed tasks:$FAILED_TASKS"
exit 1
fi
bench:
desc: Run benchmarks with standard GC
cmds:
- '{{.GOCMD}} test -bench=. -benchmem ./...'
bench-experimental:
desc: Run benchmarks with experimental GC
env:
GOEXPERIMENT: greenteagc
cmds:
- '{{.GOCMD}} test -bench=. -benchmem ./...'
bench-compare:
desc: Run benchmarks with both GC settings
deps: [bench, bench-experimental]
clean:
desc: Remove build artifacts
cmds:
- '{{.GOCMD}} clean'
- rm -rf {{.BUILD_DIR}}
test:
desc: Run tests
cmds:
- '{{.GOCMD}} test -v ./...'
test-short:
desc: Run short tests
cmds:
- '{{.GOCMD}} test -short -v ./...'
test-race:
desc: Run tests with race detector
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:
- '{{.GOCMD}} test -coverprofile=coverage.out ./...'
- '{{.GOCMD}} tool cover -html=coverage.out'
deps:
desc: Download and verify dependencies
env:
GOPROXY: '{{.GOPROXY | default "https://proxy.golang.org,direct"}}'
cmds:
- '{{.GOCMD}} mod download'
- '{{.GOCMD}} mod verify'
mod-tidy:
desc: Tidy go.mod file
cmds:
- '{{.GOCMD}} mod tidy'
mod-verify:
desc: Verify dependencies
cmds:
- '{{.GOCMD}} mod verify'
build-linux:
desc: Build for Linux (amd64, arm64, arm, riscv64)
env:
CGO_ENABLED: '0'
cmds:
- mkdir -p {{.BUILD_DIR}}
- 'GOOS=linux GOARCH=amd64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-linux-amd64 {{.MAIN_PACKAGE}}'
- 'GOOS=linux GOARCH=arm64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-linux-arm64 {{.MAIN_PACKAGE}}'
- 'GOOS=linux GOARCH=arm {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-linux-arm {{.MAIN_PACKAGE}}'
- 'GOOS=linux GOARCH=riscv64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-linux-riscv64 {{.MAIN_PACKAGE}}'
build-windows:
desc: Build for Windows (amd64, arm64)
env:
CGO_ENABLED: '0'
cmds:
- mkdir -p {{.BUILD_DIR}}
- 'GOOS=windows GOARCH=amd64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-windows-amd64.exe {{.MAIN_PACKAGE}}'
- 'GOOS=windows GOARCH=arm64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-windows-arm64.exe {{.MAIN_PACKAGE}}'
build-darwin:
desc: Build for MacOS (amd64, arm64)
env:
CGO_ENABLED: '0'
cmds:
- mkdir -p {{.BUILD_DIR}}
- 'GOOS=darwin GOARCH=amd64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-darwin-amd64 {{.MAIN_PACKAGE}}'
- 'GOOS=darwin GOARCH=arm64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-darwin-arm64 {{.MAIN_PACKAGE}}'
build-freebsd:
desc: Build for FreeBSD (amd64, 386, arm64, arm, riscv64)
env:
CGO_ENABLED: '0'
cmds:
- mkdir -p {{.BUILD_DIR}}
- 'GOOS=freebsd GOARCH=amd64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-freebsd-amd64 {{.MAIN_PACKAGE}}'
- 'GOOS=freebsd GOARCH=386 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-freebsd-386 {{.MAIN_PACKAGE}}'
- 'GOOS=freebsd GOARCH=arm64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-freebsd-arm64 {{.MAIN_PACKAGE}}'
- 'GOOS=freebsd GOARCH=arm {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-freebsd-arm {{.MAIN_PACKAGE}}'
- 'GOOS=freebsd GOARCH=riscv64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-freebsd-riscv64 {{.MAIN_PACKAGE}}'
build-openbsd:
desc: Build for OpenBSD (amd64, 386, arm64, arm, ppc64, riscv64)
env:
CGO_ENABLED: '0'
cmds:
- mkdir -p {{.BUILD_DIR}}
- 'GOOS=openbsd GOARCH=amd64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-openbsd-amd64 {{.MAIN_PACKAGE}}'
- 'GOOS=openbsd GOARCH=386 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-openbsd-386 {{.MAIN_PACKAGE}}'
- 'GOOS=openbsd GOARCH=arm64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-openbsd-arm64 {{.MAIN_PACKAGE}}'
- 'GOOS=openbsd GOARCH=arm {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-openbsd-arm {{.MAIN_PACKAGE}}'
- 'GOOS=openbsd GOARCH=ppc64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-openbsd-ppc64 {{.MAIN_PACKAGE}}'
- 'GOOS=openbsd GOARCH=riscv64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-openbsd-riscv64 {{.MAIN_PACKAGE}}'
build-netbsd:
desc: Build for NetBSD (amd64, 386, arm64, arm)
env:
CGO_ENABLED: '0'
cmds:
- mkdir -p {{.BUILD_DIR}}
- 'GOOS=netbsd GOARCH=amd64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-netbsd-amd64 {{.MAIN_PACKAGE}}'
- 'GOOS=netbsd GOARCH=386 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-netbsd-386 {{.MAIN_PACKAGE}}'
- 'GOOS=netbsd GOARCH=arm64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-netbsd-arm64 {{.MAIN_PACKAGE}}'
- 'GOOS=netbsd GOARCH=arm {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-netbsd-arm {{.MAIN_PACKAGE}}'
build-all:
desc: Build for all platforms and architectures
deps: [build-linux, build-windows, build-darwin, build-freebsd, build-openbsd, build-netbsd]
run:
desc: Run with go run
cmds:
- '{{.GOCMD}} run {{.MAIN_PACKAGE}}'
tinygo-build:
desc: Build binary with TinyGo compiler
cmds:
- mkdir -p {{.BUILD_DIR}}
- tinygo build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-tinygo -size short {{.MAIN_PACKAGE}}
tinygo-wasm:
desc: Build WebAssembly binary with TinyGo compiler
cmds:
- mkdir -p {{.BUILD_DIR}}
- tinygo build -target wasm -o {{.BUILD_DIR}}/{{.BINARY_NAME}}.wasm ./cmd/reticulum-wasm
test-wasm:
desc: Run WebAssembly tests using Node.js
vars:
ROOT_DIR:
sh: pwd
env:
GOOS: js
GOARCH: wasm
cmds:
- chmod +x {{.ROOT_DIR}}/misc/wasm/go_js_wasm_exec
- PATH="$PATH:{{.ROOT_DIR}}/misc/wasm" {{.GOCMD}} test -v ./pkg/wasm/ ./cmd/reticulum-wasm/
- |
export PATH="$PATH:{{.ROOT_DIR}}/misc/wasm"
cd examples/wasm && {{.GOCMD}} test -v .
build-wasm:
desc: Build WebAssembly binary with standard Go compiler
env:
CGO_ENABLED: '0'
GOOS: js
GOARCH: wasm
cmds:
- mkdir -p {{.BUILD_DIR}}
- '{{.GOCMD}} build -ldflags="-s -w" -o {{.BUILD_DIR}}/{{.BINARY_NAME}}.wasm ./cmd/reticulum-wasm'
example:wasm:build:
desc: Build WebAssembly example
env:
CGO_ENABLED: '0'
cmds:
- mkdir -p examples/wasm/public/static examples/wasm/public/js
- 'cd examples/wasm && GOOS=js GOARCH=wasm {{.GOCMD}} build -o public/static/reticulum-go.wasm .'
- |
GOROOT=$({{.GOCMD}} env GOROOT)
if [ -f "$GOROOT/lib/wasm/wasm_exec.js" ]; then
cp "$GOROOT/lib/wasm/wasm_exec.js" examples/wasm/public/js/
echo "wasm_exec.js copied successfully from $GOROOT/lib/wasm/"
elif [ -f "$GOROOT/misc/wasm/wasm_exec.js" ]; then
cp "$GOROOT/misc/wasm/wasm_exec.js" examples/wasm/public/js/
echo "wasm_exec.js copied successfully from $GOROOT/misc/wasm/"
else
echo "Warning: wasm_exec.js not found"
exit 1
fi
example:wasm:run:
desc: Run WebAssembly example using a simple HTTP server
deps: [example:wasm:build]
cmds:
- echo "Starting server at http://localhost:8080"
- echo "Press Ctrl+C to stop"
- 'cd examples/wasm/public && python3 -m http.server 8080'
example:wasm:test:
desc: Run tests for WASM example
cmds:
- task: test-wasm
install:
desc: Install dependencies
cmds:
- '{{.GOCMD}} mod download'
checksum:
desc: Generate SHA256 checksum for binary (uses BINARY_PATH env var if set, otherwise defaults to bin/reticulum-go)
cmds:
- |
BINARY_PATH="${BINARY_PATH:-{{.BUILD_DIR}}/{{.BINARY_NAME}}}"
if [ -f "$BINARY_PATH" ]; then
sha256sum "$BINARY_PATH" > "${BINARY_PATH}.sha256"
echo "Generated checksum: ${BINARY_PATH}.sha256"
else
echo "Error: Binary not found at $BINARY_PATH"
exit 1
fi
example:announce:
desc: Run announce example
cmds:
- 'cd examples/announce && {{.GOCMD}} run .'
example:minimal:
desc: Run minimal example
cmds:
- 'cd examples/minimal && {{.GOCMD}} run .'
example:pageserver:
desc: Run pageserver example
cmds:
- 'cd examples/pageserver && {{.GOCMD}} run .'
example:echo-listen:
desc: Run echo example (waits for incoming connections, P2P peer)
cmds:
- 'cd examples/echo && {{.GOCMD}} run . --server'
example:echo-connect:
desc: Run echo example (initiates connection to peer, requires DESTINATION env var)
cmds:
- |
if [ -z "${DESTINATION}" ]; then
echo "Error: DESTINATION environment variable required (hexadecimal hash of peer)"
echo "Example: DESTINATION=abc123... task example:echo-connect"
exit 1
fi
cd examples/echo && {{.GOCMD}} run . --destination="${DESTINATION}"
example:link-listen:
desc: Run link example (waits for incoming link requests, P2P peer)
cmds:
- 'cd examples/link && {{.GOCMD}} run . --server'
example:link-connect:
desc: Run link example (initiates link to peer, requires DESTINATION env var)
cmds:
- |
if [ -z "${DESTINATION}" ]; then
echo "Error: DESTINATION environment variable required (hexadecimal hash of peer)"
echo "Example: DESTINATION=abc123... task example:link-connect"
exit 1
fi
cd examples/link && {{.GOCMD}} run . --destination="${DESTINATION}"
example:filetransfer-share:
desc: Run filetransfer example (shares files from directory, P2P peer)
cmds:
- |
if [ -z "${SERVE_PATH}" ]; then
echo "Error: SERVE_PATH environment variable required (directory to share)"
echo "Example: SERVE_PATH=/path/to/files task example:filetransfer-share"
exit 1
fi
cd examples/filetransfer && {{.GOCMD}} run . --server --serve="${SERVE_PATH}"
example:filetransfer-fetch:
desc: Run filetransfer example (fetches files from peer, requires DESTINATION env var)
cmds:
- |
if [ -z "${DESTINATION}" ]; then
echo "Error: DESTINATION environment variable required (hexadecimal hash of peer)"
echo "Example: DESTINATION=abc123... task example:filetransfer-fetch"
exit 1
fi
cd examples/filetransfer && {{.GOCMD}} run . --destination="${DESTINATION}"
trivy:install:
desc: Install Trivy scanner
cmds:
- |
if ! command -v trivy &> /dev/null; then
curl -L -o /tmp/trivy.deb https://git.quad4.io/Quad4-Extra/assets/raw/commit/90fdcea1bb71d91df2de6ff2e3897f278413f300/bin/trivy_0.68.2_Linux-64bit.deb
sudo dpkg -i /tmp/trivy.deb || sudo apt-get install -f -y
else
echo "Trivy is already installed: $(trivy --version)"
fi
trivy:scan:
desc: Run Trivy vulnerability scan
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
trivy fs --scanners vuln --severity HIGH,CRITICAL --timeout 90m .
trivy:scan-all:
desc: Run Trivy full scan (vulnerabilities, secrets, misconfig)
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
trivy fs --scanners vuln,secret,misconfig .
sbom:
desc: Generate SBOM files (SPDX and CycloneDX formats)
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
mkdir -p sbom
trivy fs --format spdx-json --include-dev-deps --output sbom/sbom.spdx.json .
trivy fs --format cyclonedx --include-dev-deps --output sbom/sbom.cyclonedx.json .
echo "SBOM files generated in sbom/ directory"
sbom:spdx:
desc: Generate SPDX JSON SBOM
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
mkdir -p sbom
trivy fs --format spdx-json --include-dev-deps --output sbom/sbom.spdx.json .
echo "SPDX SBOM generated: sbom/sbom.spdx.json"
sbom:cyclonedx:
desc: Generate CycloneDX SBOM
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
mkdir -p sbom
trivy fs --format cyclonedx --include-dev-deps --output sbom/sbom.cyclonedx.json .
echo "CycloneDX SBOM generated: sbom/sbom.cyclonedx.json"
trivy:scan:json:
desc: Run Trivy vulnerability scan with JSON output
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
mkdir -p reports
trivy fs --scanners vuln --format json --output reports/trivy-vuln.json --timeout 90m .
trivy:scan:sarif:
desc: Run Trivy scan with SARIF output (for GitHub/GitLab integration)
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
mkdir -p reports
trivy fs --scanners vuln,secret --format sarif --output reports/trivy.sarif --timeout 90m .
trivy:scan:secrets:
desc: Scan for hardcoded secrets
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
trivy fs --scanners secret .
trivy:scan:licenses:
desc: Scan for licenses in dependencies
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
trivy fs --scanners license .
trivy:scan:misconfig:
desc: Scan for misconfigurations in config files
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
trivy fs --scanners misconfig .
trivy:db-update:
desc: Update Trivy vulnerability database
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
trivy image --download-db-only
trivy:cache-clean:
desc: Clean Trivy cache
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
trivy clean --cache
trivy:compliance:
desc: "Generate compliance report (specify COMPLIANCE env var: docker-bench-cis, k8s-nsa, etc.)"
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
if [ -z "${COMPLIANCE}" ]; then
echo "Error: COMPLIANCE environment variable required"
echo "Example: COMPLIANCE=docker-bench-cis task trivy:compliance"
exit 1
fi
mkdir -p reports
trivy fs --compliance "${COMPLIANCE}" --format json --output "reports/compliance-${COMPLIANCE}.json" .
trivy:ci:
desc: Run Trivy scan for CI (exits with non-zero code on findings)
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
trivy fs --scanners vuln --severity HIGH,CRITICAL --exit-code 1 --timeout 90m .
docker:build:
desc: Build Docker image (runtime image)
vars:
IMAGE_NAME: reticulum-go
IMAGE_TAG: latest
cmds:
- docker build -f docker/Dockerfile -t {{.IMAGE_NAME}}:{{.IMAGE_TAG}} .
docker:build:tag:
desc: Build Docker image with custom tag (use IMAGE_TAG env var)
vars:
IMAGE_NAME: reticulum-go
IMAGE_TAG: ${IMAGE_TAG:-latest}
cmds:
- docker build -f docker/Dockerfile -t {{.IMAGE_NAME}}:{{.IMAGE_TAG}} .
docker:build:build:
desc: Build Docker image for building binaries only
vars:
IMAGE_NAME: reticulum-go-build
IMAGE_TAG: latest
cmds:
- docker build -f docker/Dockerfile.build -t {{.IMAGE_NAME}}:{{.IMAGE_TAG}} .
docker:run:
desc: Run Docker container (runtime image)
vars:
IMAGE_NAME: reticulum-go
IMAGE_TAG: latest
CONTAINER_NAME: reticulum-go
cmds:
- |
docker run --rm -it \
--name {{.CONTAINER_NAME}} \
-p 4242:4242 \
{{.IMAGE_NAME}}:{{.IMAGE_TAG}}
docker:run:detached:
desc: Run Docker container in detached mode
vars:
IMAGE_NAME: reticulum-go
IMAGE_TAG: latest
CONTAINER_NAME: reticulum-go
cmds:
- |
docker run -d \
--name {{.CONTAINER_NAME}} \
-p 4242:4242 \
{{.IMAGE_NAME}}:{{.IMAGE_TAG}}
docker:stop:
desc: Stop running Docker container
vars:
CONTAINER_NAME: reticulum-go
cmds:
- docker stop {{.CONTAINER_NAME}} || true
- docker rm {{.CONTAINER_NAME}} || true
docker:extract:
desc: Extract binary from build container
vars:
IMAGE_NAME: reticulum-go-build
IMAGE_TAG: latest
BINARY_NAME: reticulum-go
cmds:
- |
CONTAINER_ID=$(docker create {{.IMAGE_NAME}}:{{.IMAGE_TAG}})
docker cp $CONTAINER_ID:/dist/{{.BINARY_NAME}} {{.BUILD_DIR}}/{{.BINARY_NAME}}
docker rm $CONTAINER_ID
echo "Binary extracted to {{.BUILD_DIR}}/{{.BINARY_NAME}}"
docker:buildx:setup:
desc: Setup Docker buildx for multi-platform builds
cmds:
- docker buildx create --name reticulum-builder --use || docker buildx use reticulum-builder
- docker buildx inspect --bootstrap
docker:buildx:build:
desc: Build multi-platform Docker image
vars:
IMAGE_NAME: reticulum-go
IMAGE_TAG: latest
PLATFORMS: linux/amd64,linux/arm64,linux/arm/v7
cmds:
- |
docker buildx build \
--platform {{.PLATFORMS}} \
-f docker/Dockerfile \
-t {{.IMAGE_NAME}}:{{.IMAGE_TAG}} \
--load \
.
docker:buildx:build:push:
desc: Build and push multi-platform Docker image
vars:
IMAGE_NAME: reticulum-go
IMAGE_TAG: latest
PLATFORMS: linux/amd64,linux/arm64,linux/arm/v7
cmds:
- |
if [ -z "${DOCKER_REGISTRY}" ]; then
echo "Error: DOCKER_REGISTRY environment variable required"
echo "Example: DOCKER_REGISTRY=registry.example.com task docker:buildx:build:push"
exit 1
fi
docker buildx build \
--platform {{.PLATFORMS}} \
-f docker/Dockerfile \
-t ${DOCKER_REGISTRY}/{{.IMAGE_NAME}}:{{.IMAGE_TAG}} \
--push \
.
docker:clean:
desc: Clean Docker images and containers
cmds:
- docker stop reticulum-go || true
- docker rm reticulum-go || true
- docker rmi reticulum-go:latest || true
- docker rmi reticulum-go-build:latest || true

View File

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package main package main
import ( import (
@@ -8,23 +6,21 @@ import (
"fmt" "fmt"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"runtime" "runtime"
"sync" "sync"
"syscall" "syscall"
"time" "time"
"git.quad4.io/Networks/Reticulum-Go/internal/config" "github.com/Sudo-Ivan/reticulum-go/internal/config"
"git.quad4.io/Networks/Reticulum-Go/internal/storage" "github.com/Sudo-Ivan/reticulum-go/pkg/buffer"
"git.quad4.io/Networks/Reticulum-Go/pkg/buffer" "github.com/Sudo-Ivan/reticulum-go/pkg/channel"
"git.quad4.io/Networks/Reticulum-Go/pkg/channel" "github.com/Sudo-Ivan/reticulum-go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/common" "github.com/Sudo-Ivan/reticulum-go/pkg/debug"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug" "github.com/Sudo-Ivan/reticulum-go/pkg/destination"
"git.quad4.io/Networks/Reticulum-Go/pkg/destination" "github.com/Sudo-Ivan/reticulum-go/pkg/identity"
"git.quad4.io/Networks/Reticulum-Go/pkg/identity" "github.com/Sudo-Ivan/reticulum-go/pkg/interfaces"
"git.quad4.io/Networks/Reticulum-Go/pkg/interfaces" "github.com/Sudo-Ivan/reticulum-go/pkg/packet"
"git.quad4.io/Networks/Reticulum-Go/pkg/packet" "github.com/Sudo-Ivan/reticulum-go/pkg/transport"
"git.quad4.io/Networks/Reticulum-Go/pkg/transport"
) )
var ( var (
@@ -37,7 +33,7 @@ const (
ANNOUNCE_RATE_GRACE = 3 // Number of grace announces before enforcing rate ANNOUNCE_RATE_GRACE = 3 // Number of grace announces before enforcing rate
ANNOUNCE_RATE_PENALTY = 7200 // Additional penalty time for rate violations ANNOUNCE_RATE_PENALTY = 7200 // Additional penalty time for rate violations
MAX_ANNOUNCE_HOPS = 128 // Maximum number of hops for announces MAX_ANNOUNCE_HOPS = 128 // Maximum number of hops for announces
APP_NAME = "Reticulum-Go Test Node" APP_NAME = "Go-Client"
APP_ASPECT = "node" // Always use "node" for node announces APP_ASPECT = "node" // Always use "node" for node announces
) )
@@ -52,7 +48,6 @@ type Reticulum struct {
announceHistoryMu sync.RWMutex announceHistoryMu sync.RWMutex
identity *identity.Identity identity *identity.Identity
destination *destination.Destination destination *destination.Destination
storage *storage.Manager
// Node-specific information // Node-specific information
maxTransferSize int16 // Max transfer size in KB maxTransferSize int16 // Max transfer size in KB
@@ -83,48 +78,19 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
} }
debug.Log(debug.DEBUG_INFO, "Directories initialized") debug.Log(debug.DEBUG_INFO, "Directories initialized")
// Initialize storage manager
storageMgr, err := storage.NewManager()
if err != nil {
return nil, fmt.Errorf("failed to initialize storage manager: %v", err)
}
debug.Log(debug.DEBUG_INFO, "Storage manager initialized")
t := transport.NewTransport(cfg) t := transport.NewTransport(cfg)
debug.Log(debug.DEBUG_INFO, "Transport initialized") debug.Log(debug.DEBUG_INFO, "Transport initialized")
// Load or create identity identity, err := identity.NewIdentity()
identityPath := storageMgr.GetIdentityPath()
var ident *identity.Identity
if _, err := os.Stat(identityPath); err == nil {
// Identity file exists, load it
ident, err = identity.FromFile(identityPath)
if err != nil {
return nil, fmt.Errorf("failed to load identity: %v", err)
}
debug.Log(debug.DEBUG_ERROR, "Loaded existing identity", common.STR_HASH, fmt.Sprintf(common.STR_FMT_HEX_LOW, ident.Hash()))
} else {
// Create new identity
ident, err = identity.NewIdentity()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create identity: %v", err) return nil, fmt.Errorf("failed to create identity: %v", err)
} }
debug.Log(debug.DEBUG_ERROR, "Created new identity", common.STR_HASH, fmt.Sprintf(common.STR_FMT_HEX_LOW, ident.Hash())) debug.Log(debug.DEBUG_ERROR, "Created new identity", "hash", fmt.Sprintf("%x", identity.Hash()))
// Save it to disk
if err := ident.ToFile(identityPath); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to save identity to file", common.STR_ERROR, err)
} else {
debug.Log(debug.DEBUG_INFO, "Identity saved to file", "path", identityPath)
}
}
// Create destination // Create destination
debug.Log(debug.DEBUG_INFO, "Creating destination...") debug.Log(debug.DEBUG_INFO, "Creating destination...")
dest, err := destination.New( dest, err := destination.New(
ident, identity,
destination.IN, destination.IN,
destination.SINGLE, destination.SINGLE,
"nomadnetwork", "nomadnetwork",
@@ -134,7 +100,7 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create destination: %v", err) return nil, fmt.Errorf("failed to create destination: %v", err)
} }
debug.Log(debug.DEBUG_INFO, "Created destination with hash", common.STR_HASH, fmt.Sprintf(common.STR_FMT_HEX_LOW, dest.GetHash())) debug.Log(debug.DEBUG_INFO, "Created destination with hash", "hash", fmt.Sprintf("%x", dest.GetHash()))
// Set node metadata // Set node metadata
nodeTimestamp := time.Now().Unix() nodeTimestamp := time.Now().Unix()
@@ -147,12 +113,11 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
buffers: make(map[string]*buffer.Buffer), buffers: make(map[string]*buffer.Buffer),
pathRequests: make(map[string]*common.PathRequest), pathRequests: make(map[string]*common.PathRequest),
announceHistory: make(map[string]announceRecord), announceHistory: make(map[string]announceRecord),
identity: ident, identity: identity,
destination: dest, destination: dest,
storage: storageMgr,
// Node-specific information // Node-specific information
maxTransferSize: common.NUM_500, // Default 500KB maxTransferSize: 500, // Default 500KB
nodeEnabled: true, // Enabled by default nodeEnabled: true, // Enabled by default
nodeTimestamp: nodeTimestamp, nodeTimestamp: nodeTimestamp,
} }
@@ -161,10 +126,9 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
dest.AcceptsLinks(true) dest.AcceptsLinks(true)
// Enable ratchets and point to a file for persistence. // Enable ratchets and point to a file for persistence.
// The actual path should probably be configurable. // The actual path should probably be configurable.
ratchetPath := ".git.quad4.io/Networks/Reticulum-Go/storage/ratchets/" + r.identity.GetHexHash() ratchetPath := ".reticulum-go/storage/ratchets/" + r.identity.GetHexHash()
dest.EnableRatchets(ratchetPath) dest.EnableRatchets(ratchetPath)
dest.SetProofStrategy(destination.PROVE_APP) dest.SetProofStrategy(destination.PROVE_APP)
debug.Log(debug.DEBUG_VERBOSE, "Configured destination features") debug.Log(debug.DEBUG_VERBOSE, "Configured destination features")
// Initialize interfaces from config // Initialize interfaces from config
@@ -177,7 +141,7 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
var err error var err error
switch ifaceConfig.Type { switch ifaceConfig.Type {
case common.STR_TCP_CLIENT: case "TCPClientInterface":
iface, err = interfaces.NewTCPClientInterface( iface, err = interfaces.NewTCPClientInterface(
name, name,
ifaceConfig.TargetHost, ifaceConfig.TargetHost,
@@ -195,20 +159,8 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
) )
case "AutoInterface": case "AutoInterface":
iface, err = interfaces.NewAutoInterface(name, ifaceConfig) iface, err = interfaces.NewAutoInterface(name, ifaceConfig)
case "WebSocketInterface":
wsURL := ifaceConfig.Address
if wsURL == "" {
wsURL = ifaceConfig.TargetHost
}
debug.Log(debug.DEBUG_INFO, "Creating WebSocket interface", common.STR_NAME, name, "url", wsURL, "enabled", ifaceConfig.Enabled)
iface, err = interfaces.NewWebSocketInterface(name, wsURL, ifaceConfig.Enabled)
if err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to create WebSocket interface", common.STR_NAME, name, common.STR_ERROR, err)
} else {
debug.Log(debug.DEBUG_INFO, "WebSocket interface created successfully", common.STR_NAME, name)
}
default: default:
debug.Log(debug.DEBUG_CRITICAL, "Unknown interface type", common.STR_TYPE, ifaceConfig.Type) debug.Log(debug.DEBUG_CRITICAL, "Unknown interface type", "type", ifaceConfig.Type)
continue continue
} }
@@ -216,13 +168,13 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
if cfg.PanicOnInterfaceErr { if cfg.PanicOnInterfaceErr {
return nil, fmt.Errorf("failed to create interface %s: %v", name, err) return nil, fmt.Errorf("failed to create interface %s: %v", name, err)
} }
debug.Log(debug.DEBUG_CRITICAL, "Error creating interface", common.STR_NAME, name, common.STR_ERROR, err) debug.Log(debug.DEBUG_CRITICAL, "Error creating interface", "name", name, "error", err)
continue continue
} }
// Set packet callback // Set packet callback
iface.SetPacketCallback(func(data []byte, ni common.NetworkInterface) { iface.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
debug.Log(debug.DEBUG_INFO, "Packet callback called for interface", common.STR_NAME, ni.GetName(), "data_len", len(data)) debug.Log(debug.DEBUG_INFO, "Packet callback called for interface", "name", ni.GetName(), "data_len", len(data))
if r.transport != nil { if r.transport != nil {
r.transport.HandlePacket(data, ni) r.transport.HandlePacket(data, ni)
} else { } else {
@@ -230,16 +182,16 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
} }
}) })
debug.Log(debug.DEBUG_ERROR, "Configuring interface", common.STR_NAME, name, common.STR_TYPE, ifaceConfig.Type) debug.Log(debug.DEBUG_ERROR, "Configuring interface", "name", name, "type", ifaceConfig.Type)
r.interfaces = append(r.interfaces, iface) r.interfaces = append(r.interfaces, iface)
debug.Log(debug.DEBUG_INFO, "Interface started successfully", common.STR_NAME, name) debug.Log(debug.DEBUG_INFO, "Interface started successfully", "name", name)
} }
return r, nil return r, nil
} }
func (r *Reticulum) handleInterface(iface common.NetworkInterface) { func (r *Reticulum) handleInterface(iface common.NetworkInterface) {
debug.Log(debug.DEBUG_INFO, "Setting up interface", common.STR_NAME, iface.GetName(), common.STR_TYPE, fmt.Sprintf("%T", iface)) debug.Log(debug.DEBUG_INFO, "Setting up interface", "name", iface.GetName(), "type", fmt.Sprintf("%T", iface))
ch := channel.NewChannel(&transportWrapper{r.transport}) ch := channel.NewChannel(&transportWrapper{r.transport})
r.channels[iface.GetName()] = ch r.channels[iface.GetName()] = ch
@@ -250,11 +202,11 @@ func (r *Reticulum) handleInterface(iface common.NetworkInterface) {
ch, ch,
func(size int) { func(size int) {
data := make([]byte, size) data := make([]byte, size)
debug.Log(debug.DEBUG_PACKETS, "Interface reading bytes from buffer", common.STR_NAME, iface.GetName(), "size", size) debug.Log(debug.DEBUG_PACKETS, "Interface reading bytes from buffer", "name", iface.GetName(), "size", size)
iface.ProcessIncoming(data) iface.ProcessIncoming(data)
if len(data) > common.ZERO { if len(data) > 0 {
debug.Log(debug.DEBUG_TRACE, "Interface received packet type", common.STR_NAME, iface.GetName(), common.STR_TYPE, fmt.Sprintf("0x%02x", data[0])) debug.Log(debug.DEBUG_TRACE, "Interface received packet type", "name", iface.GetName(), "type", fmt.Sprintf("0x%02x", data[0]))
r.transport.HandlePacket(data, iface) r.transport.HandlePacket(data, iface)
} }
}, },
@@ -298,14 +250,42 @@ func main() {
cfg, err := config.InitConfig() cfg, err := config.InitConfig()
if err != nil { if err != nil {
debug.GetLogger().Error("Failed to initialize config", common.STR_ERROR, err) debug.GetLogger().Error("Failed to initialize config", "error", err)
os.Exit(1) os.Exit(1)
} }
debug.Log(debug.DEBUG_ERROR, "Configuration loaded", "path", cfg.ConfigPath) debug.Log(debug.DEBUG_ERROR, "Configuration loaded", "path", cfg.ConfigPath)
if len(cfg.Interfaces) == 0 {
debug.Log(debug.DEBUG_ERROR, "No interfaces configured, adding default interfaces")
cfg.Interfaces = make(map[string]*common.InterfaceConfig)
// Auto interface for local discovery
cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{
Type: "AutoInterface",
Enabled: true,
Name: "Auto Discovery",
}
cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
Type: "TCPClientInterface",
Enabled: false,
TargetHost: "127.0.0.1",
TargetPort: 4242,
Name: "Go-RNS-Testnet",
}
cfg.Interfaces["Quad4 TCP"] = &common.InterfaceConfig{
Type: "TCPClientInterface",
Enabled: true,
TargetHost: "rns.quad4.io",
TargetPort: 4242,
Name: "Quad4 TCP",
}
}
r, err := NewReticulum(cfg) r, err := NewReticulum(cfg)
if err != nil { if err != nil {
debug.GetLogger().Error("Failed to create Reticulum instance", common.STR_ERROR, err) debug.GetLogger().Error("Failed to create Reticulum instance", "error", err)
os.Exit(1) os.Exit(1)
} }
@@ -318,7 +298,7 @@ func main() {
// Start Reticulum // Start Reticulum
if err := r.Start(); err != nil { if err := r.Start(); err != nil {
debug.GetLogger().Error("Failed to start Reticulum", common.STR_ERROR, err) debug.GetLogger().Error("Failed to start Reticulum", "error", err)
os.Exit(1) os.Exit(1)
} }
@@ -328,7 +308,7 @@ func main() {
debug.Log(debug.DEBUG_CRITICAL, "Shutting down...") debug.Log(debug.DEBUG_CRITICAL, "Shutting down...")
if err := r.Stop(); err != nil { if err := r.Stop(); err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Error during shutdown", common.STR_ERROR, err) debug.Log(debug.DEBUG_CRITICAL, "Error during shutdown", "error", err)
} }
debug.Log(debug.DEBUG_CRITICAL, "Goodbye!") debug.Log(debug.DEBUG_CRITICAL, "Goodbye!")
} }
@@ -345,7 +325,7 @@ func (tw *transportWrapper) RTT() float64 {
return tw.GetRTT() return tw.GetRTT()
} }
func (tw *transportWrapper) GetStatus() byte { func (tw *transportWrapper) GetStatus() int {
return transport.STATUS_ACTIVE return transport.STATUS_ACTIVE
} }
@@ -381,38 +361,17 @@ func (tw *transportWrapper) SetPacketDelivered(packet interface{}, callback func
callback(packet) callback(packet)
} }
func (tw *transportWrapper) GetLinkID() []byte {
return nil
}
func (tw *transportWrapper) HandleInbound(pkt *packet.Packet) error {
return nil
}
func (tw *transportWrapper) ValidateLinkProof(pkt *packet.Packet, networkIface common.NetworkInterface) error {
return nil
}
func initializeDirectories() error { func initializeDirectories() error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %v", err)
}
basePath := filepath.Join(homeDir, ".reticulum-go")
dirs := []string{ dirs := []string{
basePath, ".reticulum-go",
filepath.Join(basePath, common.STR_STORAGE), ".reticulum-go/storage",
filepath.Join(basePath, common.STR_STORAGE, "destinations"), ".reticulum-go/storage/destinations",
filepath.Join(basePath, common.STR_STORAGE, "identities"), ".reticulum-go/storage/identities",
filepath.Join(basePath, common.STR_STORAGE, "ratchets"), ".reticulum-go/storage/ratchets",
filepath.Join(basePath, common.STR_STORAGE, "cache"),
filepath.Join(basePath, common.STR_STORAGE, "cache", "announces"),
filepath.Join(basePath, common.STR_STORAGE, "resources"),
} }
for _, dir := range dirs { for _, dir := range dirs {
if err := os.MkdirAll(dir, common.NUM_0700); err != nil { // #nosec G301 if err := os.MkdirAll(dir, 0700); err != nil { // #nosec G301
return fmt.Errorf("failed to create directory %s: %v", dir, err) return fmt.Errorf("failed to create directory %s: %v", dir, err)
} }
} }
@@ -455,9 +414,8 @@ func (r *Reticulum) Start() error {
// Send initial announce // Send initial announce
debug.Log(debug.DEBUG_ERROR, "Sending initial announce") debug.Log(debug.DEBUG_ERROR, "Sending initial announce")
nodeName := "Reticulum-Go Test Node" nodeName := "Go-Client"
r.destination.SetDefaultAppData([]byte(nodeName)) if err := r.destination.Announce([]byte(nodeName)); err != nil {
if err := r.destination.Announce(false, nil, nil); err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Failed to send initial announce", "error", err) debug.Log(debug.DEBUG_CRITICAL, "Failed to send initial announce", "error", err)
} }
@@ -468,7 +426,7 @@ func (r *Reticulum) Start() error {
for { for {
debug.Log(debug.DEBUG_INFO, "Announcing destination...") debug.Log(debug.DEBUG_INFO, "Announcing destination...")
err := r.destination.Announce(false, nil, nil) err := r.destination.Announce([]byte(nodeName))
if err != nil { if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Could not send announce", "error", err) debug.Log(debug.DEBUG_CRITICAL, "Could not send announce", "error", err)
} }
@@ -528,10 +486,10 @@ func (h *AnnounceHandler) AspectFilter() []string {
return h.aspectFilter return h.aspectFilter
} }
func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appData []byte, hops uint8) error { func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appData []byte) error {
debug.Log(debug.DEBUG_INFO, "Received announce", "hash", fmt.Sprintf("%x", destHash), "hops", hops) debug.Log(debug.DEBUG_INFO, "Received announce", "hash", fmt.Sprintf("%x", destHash))
debug.Log(debug.DEBUG_PACKETS, "Raw announce data", "data", fmt.Sprintf("%x", appData)) debug.Log(debug.DEBUG_PACKETS, "Raw announce data", "data", fmt.Sprintf("%x", appData))
debug.Log(debug.DEBUG_INFO, "MAIN HANDLER: Received announce", "hash", fmt.Sprintf("%x", destHash), "appData_len", len(appData), "hops", hops) debug.Log(debug.DEBUG_INFO, "MAIN HANDLER: Received announce", "hash", fmt.Sprintf("%x", destHash), "appData_len", len(appData))
var isNode bool var isNode bool
var nodeEnabled bool var nodeEnabled bool
@@ -539,15 +497,15 @@ func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appD
var nodeMaxSize int16 var nodeMaxSize int16
// Parse msgpack appData from transport announce format // Parse msgpack appData from transport announce format
if len(appData) > common.ZERO { if len(appData) > 0 {
// appData is msgpack array [name, customData] // appData is msgpack array [name, customData]
if appData[0] == common.HEX_0x92 { // array of 2 elements if appData[0] == 0x92 { // array of 2 elements
// Skip array header and first element (name) // Skip array header and first element (name)
pos := common.ONE pos := 1
if pos < len(appData) && appData[pos] == common.HEX_0xC4 { // bin 8 if pos < len(appData) && appData[pos] == 0xc4 { // bin 8
nameLen := int(appData[pos+1]) nameLen := int(appData[pos+1])
pos += common.TWO + nameLen pos += 2 + nameLen
if pos < len(appData) && appData[pos] == common.HEX_0xC4 { // bin 8 if pos < len(appData) && appData[pos] == 0xc4 { // bin 8
dataLen := int(appData[pos+1]) dataLen := int(appData[pos+1])
if pos+2+dataLen <= len(appData) { if pos+2+dataLen <= len(appData) {
customData := appData[pos+2 : pos+2+dataLen] customData := appData[pos+2 : pos+2+dataLen]
@@ -615,25 +573,26 @@ func (r *Reticulum) GetDestination() *destination.Destination {
func (r *Reticulum) createNodeAppData() []byte { func (r *Reticulum) createNodeAppData() []byte {
// Create a msgpack array with 3 elements // Create a msgpack array with 3 elements
// [Bool, Int32, Int16] for [enable, timestamp, max_transfer_size] // [Bool, Int32, Int16] for [enable, timestamp, max_transfer_size]
appData := []byte{common.HEX_0x93} // Array with 3 elements appData := []byte{0x93} // Array with 3 elements
// Element 0: Boolean for enable/disable peer // Element 0: Boolean for enable/disable peer
if r.nodeEnabled { if r.nodeEnabled {
appData = append(appData, common.HEX_0xC3) // true appData = append(appData, 0xc3) // true
} else { } else {
appData = append(appData, common.HEX_0xC2) // false appData = append(appData, 0xc2) // false
} }
// Element 1: Int32 timestamp (current time) // Element 1: Int32 timestamp (current time)
// Update the timestamp when creating new announcements
r.nodeTimestamp = time.Now().Unix() r.nodeTimestamp = time.Now().Unix()
appData = append(appData, common.HEX_0xD2) // int32 format appData = append(appData, 0xd2) // int32 format
timeBytes := make([]byte, common.FOUR) timeBytes := make([]byte, 4)
binary.BigEndian.PutUint32(timeBytes, uint32(r.nodeTimestamp)) // #nosec G115 binary.BigEndian.PutUint32(timeBytes, uint32(r.nodeTimestamp)) // #nosec G115
appData = append(appData, timeBytes...) appData = append(appData, timeBytes...)
// Element 2: Int16 max transfer size in KB // Element 2: Int16 max transfer size in KB
appData = append(appData, common.HEX_0xD1) // int16 format appData = append(appData, 0xd1) // int16 format
sizeBytes := make([]byte, common.TWO) sizeBytes := make([]byte, 2)
binary.BigEndian.PutUint16(sizeBytes, uint16(r.maxTransferSize)) // #nosec G115 binary.BigEndian.PutUint16(sizeBytes, uint16(r.maxTransferSize)) // #nosec G115
appData = append(appData, sizeBytes...) appData = append(appData, sizeBytes...)

View File

@@ -1,61 +0,0 @@
package main
import (
"os"
"path/filepath"
"testing"
"git.quad4.io/Networks/Reticulum-Go/internal/config"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
)
func TestNewReticulum(t *testing.T) {
// Set up a temporary home directory for testing
tmpDir := t.TempDir()
originalHome := os.Getenv(common.STR_HOME)
os.Setenv(common.STR_HOME, tmpDir)
defer os.Setenv(common.STR_HOME, originalHome)
cfg := config.DefaultConfig()
// Disable interfaces for simple test
cfg.Interfaces = make(map[string]*common.InterfaceConfig)
r, err := NewReticulum(cfg)
if err != nil {
t.Fatalf("NewReticulum failed: %v", err)
}
if r == nil {
t.Fatal("NewReticulum returned nil")
}
if r.identity == nil {
t.Error("Reticulum identity should not be nil")
}
if r.destination == nil {
t.Error("Reticulum destination should not be nil")
}
// Verify directories were created
basePath := filepath.Join(tmpDir, ".reticulum-go")
if _, err := os.Stat(basePath); os.IsNotExist(err) {
t.Error("Base directory not created")
}
}
func TestNodeAppData(t *testing.T) {
tmpDir := t.TempDir()
os.Setenv(common.STR_HOME, tmpDir)
r := &Reticulum{
nodeEnabled: true,
maxTransferSize: common.NUM_500,
}
data := r.createNodeAppData()
if len(data) == common.ZERO {
t.Error("createNodeAppData returned empty data")
}
if data[0] != common.HEX_0x93 {
t.Errorf("Expected array header 0x93, got 0x%x", data[0])
}
}

View File

@@ -1,30 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build js && wasm
// +build js,wasm
package main
import (
"syscall/js"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
"git.quad4.io/Networks/Reticulum-Go/pkg/wasm"
)
func main() {
run()
// Keep the Go program running
select {}
}
func run() {
debug.Init()
debug.SetDebugLevel(debug.DEBUG_INFO)
wasm.RegisterJSFunctions()
// Notify JS that reticulum is ready
js.Global().Call("reticulumReady")
}

View File

@@ -1,29 +0,0 @@
//go:build js && wasm
// +build js,wasm
package main
import (
"syscall/js"
"testing"
)
func TestRun(t *testing.T) {
readyCalled := false
js.Global().Set("reticulumReady", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
readyCalled = true
return nil
}))
run()
if !readyCalled {
t.Error("reticulumReady was not called by run()")
}
reticulum := js.Global().Get("reticulum")
if reticulum.IsUndefined() {
t.Error("reticulum functions were not registered")
}
}

View File

@@ -22,7 +22,7 @@ RUN go build \
-o reticulum-go \ -o reticulum-go \
./cmd/reticulum-go ./cmd/reticulum-go
FROM busybox:1.37.0@sha256:870e815c3a50dd0f6b40efddb319c72c32c3ee340b5a3e8945904232ccd12f44 FROM busybox:latest
RUN adduser -D -s /bin/sh app RUN adduser -D -s /bin/sh app

View File

@@ -5,13 +5,10 @@ ENV CGO_ENABLED=0
ENV GOOS=linux ENV GOOS=linux
ENV GOARCH=amd64 ENV GOARCH=amd64
RUN apk add --no-cache git && \ RUN apk add --no-cache git
adduser -D -s /bin/sh builder
WORKDIR /build WORKDIR /build
USER builder
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download

View File

@@ -1,16 +0,0 @@
module git.quad4.io/Networks/Reticulum-Go/examples/wasm
go 1.24.0
require (
git.quad4.io/Networks/Reticulum-Go v0.6.0
git.quad4.io/RNS-Things/reticulum-go-mf v0.0.0-20251231170406-60b810424de0
)
require (
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
)
replace git.quad4.io/Networks/Reticulum-Go => ../../

View File

@@ -1,16 +0,0 @@
git.quad4.io/RNS-Things/reticulum-go-mf v0.0.0-20251231170406-60b810424de0 h1:Yne2IbESHud2fmsj9kjsTYR3QBj+vY9fTqvsEzaKfy8=
git.quad4.io/RNS-Things/reticulum-go-mf v0.0.0-20251231170406-60b810424de0/go.mod h1:vhZm1vAMuWJtoFGGAHPlnFsVqTzHkBuYWDMGo6KjVPk=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,80 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build js && wasm
// +build js,wasm
package main
import (
"encoding/hex"
"fmt"
"syscall/js"
"git.quad4.io/Networks/Reticulum-Go/pkg/wasm"
"git.quad4.io/RNS-Things/reticulum-go-mf/pkg/mf"
)
var messenger *mf.Messenger
func main() {
// Register the generic WASM bridge functions first
wasm.RegisterJSFunctions()
// Add chat-specific functions to the "reticulum" JS object
reticulum := js.Global().Get("reticulum")
reticulum.Set("sendMessage", js.FuncOf(SendMessage))
reticulum.Set("sendAnnounce", js.FuncOf(SendAnnounce))
// Keep the Go program running
select {}
}
func SendMessage(this js.Value, args []js.Value) interface{} {
if len(args) < 2 {
return js.ValueOf(map[string]interface{}{
"error": "Destination hash and message required",
})
}
destHashHex := args[0].String()
message := args[1].String()
destHash, err := hex.DecodeString(destHashHex)
if err != nil {
return js.ValueOf(map[string]interface{}{
"error": fmt.Sprintf("Invalid destination hash: %v", err),
})
}
// Initialize messenger if not already done
if messenger == nil {
t := wasm.GetTransport()
d := wasm.GetDestinationPointer()
if t == nil || d == nil {
return js.ValueOf(map[string]interface{}{
"error": "Reticulum not initialized",
})
}
messenger = mf.NewMessenger(t, d)
}
// Use the high-level Messenger from mf package
if err := messenger.SendMessage(destHash, message); err != nil {
return js.ValueOf(map[string]interface{}{
"error": fmt.Sprintf("Send failed: %v", err),
})
}
return js.ValueOf(map[string]interface{}{
"success": true,
})
}
func SendAnnounce(this js.Value, args []js.Value) interface{} {
var appData []byte
if len(args) >= 1 && args[0].String() != "" {
appData = []byte(args[0].String())
}
return wasm.SendAnnounce(appData)
}

View File

@@ -1,33 +0,0 @@
//go:build js && wasm
// +build js,wasm
package main
import (
"syscall/js"
"testing"
"git.quad4.io/Networks/Reticulum-Go/pkg/wasm"
)
func TestRegisterFunctions(t *testing.T) {
// Register functions
wasm.RegisterJSFunctions()
reticulum := js.Global().Get("reticulum")
if reticulum.IsUndefined() {
t.Fatal("reticulum object not registered")
}
// Manually register chat functions since main() has select{}
reticulum.Set("sendMessage", js.FuncOf(SendMessage))
reticulum.Set("sendAnnounce", js.FuncOf(SendAnnounce))
tests := []string{"sendMessage", "sendAnnounce", "init", "getIdentity", "sendData"}
for _, name := range tests {
if reticulum.Get(name).Type() != js.TypeFunction {
t.Errorf("function %s not registered correctly", name)
}
}
}

View File

@@ -1,43 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Reticulum WASM Chat Example</title>
</head>
<body>
<script src="js/wasm_exec.js"></script>
<script>
if (!WebAssembly.instantiateStreaming) { // polyfill
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
}
const go = new Go();
let mod, inst;
WebAssembly.instantiateStreaming(fetch("static/reticulum-go.wasm"), go.importObject).then(async (result) => {
mod = result.module;
inst = result.instance;
console.log("WASM loaded");
await go.run(inst);
}).catch((err) => {
console.error(err);
});
// Basic chat interface helper
window.onChatMessage = (msg) => {
console.log("Chat message received:", msg);
};
window.onPeerDiscovered = (peer) => {
console.log("Peer discovered:", peer);
};
window.log = (msg, level) => {
console.log(`[${level}] ${msg}`);
};
</script>
<h1>Reticulum WASM Chat</h1>
<p>Open console to see output.</p>
</body>
</html>

View File

@@ -1,575 +0,0 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
"use strict";
(() => {
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!globalThis.fs) {
let outputBuf = "";
globalThis.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substring(0, nl));
outputBuf = outputBuf.substring(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!globalThis.process) {
globalThis.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!globalThis.path) {
globalThis.path = {
resolve(...pathSegments) {
return pathSegments.join("/");
}
}
}
if (!globalThis.crypto) {
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
}
if (!globalThis.performance) {
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
}
if (!globalThis.TextEncoder) {
throw new Error("globalThis.TextEncoder is not available, polyfill required");
}
if (!globalThis.TextDecoder) {
throw new Error("globalThis.TextDecoder is not available, polyfill required");
}
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
globalThis.Go = class {
constructor() {
this.argv = ["js"];
this.env = {};
this.exit = (code) => {
if (code !== 0) {
console.warn("exit code:", code);
}
};
this._exitPromise = new Promise((resolve) => {
this._resolveExitPromise = resolve;
});
this._pendingEvent = null;
this._scheduledTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const setInt64 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
}
const setInt32 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
}
const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
}
const loadValue = (addr) => {
const f = this.mem.getFloat64(addr, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = this.mem.getUint32(addr, true);
return this._values[id];
}
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
if (typeof v === "number" && v !== 0) {
if (isNaN(v)) {
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
return;
}
this.mem.setFloat64(addr, v, true);
return;
}
if (v === undefined) {
this.mem.setFloat64(addr, 0, true);
return;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 0;
switch (typeof v) {
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case "string":
typeFlag = 2;
break;
case "symbol":
typeFlag = 3;
break;
case "function":
typeFlag = 4;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
}
const loadSlice = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
}
const loadSliceOfValues = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (addr) => {
const saddr = getInt64(addr + 0);
const len = getInt64(addr + 8);
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
}
const testCallExport = (a, b) => {
this._inst.exports.testExport0();
return this._inst.exports.testExport(a, b);
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
_gotest: {
add: (a, b) => a + b,
callExport: testCallExport,
},
gojs: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
// This changes the SP, thus we have to update the SP used by the imported function.
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
sp >>>= 0;
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
delete this._inst;
delete this._values;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
},
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
"runtime.wasmWrite": (sp) => {
sp >>>= 0;
const fd = getInt64(sp + 8);
const p = getInt64(sp + 16);
const n = this.mem.getInt32(sp + 24, true);
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
},
// func resetMemoryDataView()
"runtime.resetMemoryDataView": (sp) => {
sp >>>= 0;
this.mem = new DataView(this._inst.exports.mem.buffer);
},
// func nanotime1() int64
"runtime.nanotime1": (sp) => {
sp >>>= 0;
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},
// func walltime() (sec int64, nsec int32)
"runtime.walltime": (sp) => {
sp >>>= 0;
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},
// func scheduleTimeoutEvent(delay int64) int32
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(id, setTimeout(
() => {
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
}
},
getInt64(sp + 8),
));
this.mem.setInt32(sp + 16, id, true);
},
// func clearTimeoutEvent(id int32)
"runtime.clearTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
this._scheduledTimeouts.delete(id);
},
// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
sp >>>= 0;
crypto.getRandomValues(loadSlice(sp + 8));
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
sp >>>= 0;
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
sp >>>= 0;
storeValue(sp + 24, loadString(sp + 8));
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (sp) => {
sp >>>= 0;
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 32, result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (sp) => {
sp >>>= 0;
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (sp) => {
sp >>>= 0;
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const m = Reflect.get(v, loadString(sp + 16));
const args = loadSliceOfValues(sp + 32);
const result = Reflect.apply(m, v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, result);
this.mem.setUint8(sp + 64, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, err);
this.mem.setUint8(sp + 64, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.apply(v, undefined, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.construct(v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (sp) => {
sp >>>= 0;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (sp) => {
sp >>>= 0;
const str = encoder.encode(String(loadValue(sp + 8)));
storeValue(sp + 16, str);
setInt64(sp + 24, str.length);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (sp) => {
sp >>>= 0;
const str = loadValue(sp + 8);
loadSlice(sp + 16).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => {
sp >>>= 0;
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (sp) => {
sp >>>= 0;
const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
// func copyBytesToJS(dst ref, src []byte) (int, bool)
"syscall/js.copyBytesToJS": (sp) => {
sp >>>= 0;
const dst = loadValue(sp + 8);
const src = loadSlice(sp + 16);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
"debug": (value) => {
console.log(value);
},
}
};
}
async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
globalThis,
this,
];
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map([ // mapping from JS values to reference ids
[0, 1],
[null, 2],
[true, 3],
[false, 4],
[globalThis, 5],
[this, 6],
]);
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
const strPtr = (str) => {
const ptr = offset;
const bytes = encoder.encode(str + "\0");
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
offset += bytes.length;
if (offset % 8 !== 0) {
offset += 8 - (offset % 8);
}
return ptr;
};
const argc = this.argv.length;
const argvPtrs = [];
this.argv.forEach((arg) => {
argvPtrs.push(strPtr(arg));
});
argvPtrs.push(0);
const keys = Object.keys(this.env).sort();
keys.forEach((key) => {
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
});
argvPtrs.push(0);
const argv = offset;
argvPtrs.forEach((ptr) => {
this.mem.setUint32(offset, ptr, true);
this.mem.setUint32(offset + 4, 0, true);
offset += 8;
});
// The linker guarantees global data starts from at least wasmMinDataAddr.
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
const wasmMinDataAddr = 4096 + 8192;
if (offset >= wasmMinDataAddr) {
throw new Error("total length of command line and environment variables exceeds limit");
}
this._inst.exports.run(argc, argv);
if (this.exited) {
this._resolveExitPromise();
}
await this._exitPromise;
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
})();

61
flake.lock generated
View File

@@ -1,61 +0,0 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1766902085,
"narHash": "sha256-coBu0ONtFzlwwVBzmjacUQwj3G+lybcZ1oeNSQkgC0M=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c0b0e0fddf73fd517c3471e546c0df87a42d53f4",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,50 +0,0 @@
{
description = "Reticulum-Go development environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
};
go = pkgs.go_1_25;
in
{
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
go
go-task
revive
gosec
gnumake
tinygo
];
shellHook = ''
echo "Reticulum-Go development environment"
echo "Go version: $(go version)"
echo "Task version: $(task --version 2>/dev/null || echo 'not available')"
echo "Revive version: $(revive --version 2>/dev/null || echo 'not available')"
echo "Gosec version: $(gosec --version 2>/dev/null || echo 'not available')"
echo "TinyGo version: $(tinygo version 2>/dev/null || echo 'not available')"
'';
};
packages.default = pkgs.buildGoModule {
pname = "reticulum-go";
version = "dev";
src = ./.;
vendorHash = "";
subPackages = [ "cmd/reticulum-go" ];
ldflags = [ "-s" "-w" ];
CGO_ENABLED = "0";
};
});
}

4
go.mod
View File

@@ -1,10 +1,10 @@
module git.quad4.io/Networks/Reticulum-Go module github.com/Sudo-Ivan/reticulum-go
go 1.24.0 go 1.24.0
require ( require (
github.com/vmihailenco/msgpack/v5 v5.4.1 github.com/vmihailenco/msgpack/v5 v5.4.1
golang.org/x/crypto v0.46.0 golang.org/x/crypto v0.43.0
) )
require github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect require github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect

4
go.sum
View File

@@ -8,7 +8,7 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package config package config
import ( import (
@@ -10,7 +8,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"git.quad4.io/Networks/Reticulum-Go/pkg/common" "github.com/Sudo-Ivan/reticulum-go/pkg/common"
) )
const ( const (
@@ -72,7 +70,6 @@ func parseValue(value string) interface{} {
// LoadConfig loads the configuration from the specified path // LoadConfig loads the configuration from the specified path
func LoadConfig(path string) (*common.ReticulumConfig, error) { func LoadConfig(path string) (*common.ReticulumConfig, error) {
// bearer:disable go_gosec_filesystem_filereadtaint
file, err := os.Open(path) // #nosec G304 file, err := os.Open(path) // #nosec G304
if err != nil { if err != nil {
return nil, err return nil, err
@@ -213,6 +210,7 @@ func CreateDefaultConfig(path string) error {
cfg := DefaultConfig() cfg := DefaultConfig()
cfg.ConfigPath = path cfg.ConfigPath = path
// Add Auto Interface
cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{ cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{
Type: "AutoInterface", Type: "AutoInterface",
Enabled: true, Enabled: true,
@@ -222,6 +220,7 @@ func CreateDefaultConfig(path string) error {
DataPort: 42671, DataPort: 42671,
} }
// Add default interfaces
cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{ cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
Type: "TCPClientInterface", Type: "TCPClientInterface",
Enabled: true, Enabled: true,

View File

@@ -1,136 +0,0 @@
package config
import (
"os"
"path/filepath"
"testing"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
)
func TestDefaultConfig(t *testing.T) {
cfg := DefaultConfig()
if cfg == nil {
t.Fatal("DefaultConfig() returned nil")
}
if !cfg.EnableTransport {
t.Error("EnableTransport should be true by default")
}
if cfg.LogLevel != DefaultLogLevel {
t.Errorf("LogLevel should be %d, got %d", DefaultLogLevel, cfg.LogLevel)
}
}
func TestParseValue(t *testing.T) {
tests := []struct {
input string
expected interface{}
}{
{"true", true},
{"false", false},
{"123", 123},
{"hello", "hello"},
{" 456 ", 456},
{" world ", "world"},
}
for _, tt := range tests {
result := parseValue(tt.input)
if result != tt.expected {
t.Errorf("parseValue(%q) = %v; want %v", tt.input, result, tt.expected)
}
}
}
func TestLoadSaveConfig(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "config")
cfg := DefaultConfig()
cfg.ConfigPath = configPath
cfg.LogLevel = 1
cfg.EnableTransport = false
cfg.Interfaces["TestInterface"] = &common.InterfaceConfig{
Name: "TestInterface",
Type: "UDPInterface",
Enabled: true,
Address: "1.2.3.4",
Port: 1234,
}
err := SaveConfig(cfg)
if err != nil {
t.Fatalf("SaveConfig failed: %v", err)
}
loadedCfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig failed: %v", err)
}
if loadedCfg.LogLevel != 1 {
t.Errorf("Expected LogLevel 1, got %d", loadedCfg.LogLevel)
}
if loadedCfg.EnableTransport {
t.Error("Expected EnableTransport false")
}
iface, ok := loadedCfg.Interfaces["TestInterface"]
if !ok {
t.Fatal("TestInterface not found in loaded config")
}
if iface.Type != "UDPInterface" {
t.Errorf("Expected type UDPInterface, got %s", iface.Type)
}
if iface.Address != "1.2.3.4" {
t.Errorf("Expected address 1.2.3.4, got %s", iface.Address)
}
if iface.Port != 1234 {
t.Errorf("Expected port 1234, got %d", iface.Port)
}
}
func TestCreateDefaultConfig(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "config")
err := CreateDefaultConfig(configPath)
if err != nil {
t.Fatalf("CreateDefaultConfig failed: %v", err)
}
if _, err := os.Stat(configPath); os.IsNotExist(err) {
t.Fatal("Config file was not created")
}
cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig failed: %v", err)
}
if _, ok := cfg.Interfaces["Auto Discovery"]; !ok {
t.Error("Auto Discovery interface missing")
}
}
func TestGetConfigPath(t *testing.T) {
path, err := GetConfigPath()
if err != nil {
t.Fatalf("GetConfigPath failed: %v", err)
}
if path == "" {
t.Error("GetConfigPath returned empty string")
}
}
func TestEnsureConfigDir(t *testing.T) {
// This might modify the actual home directory if not careful,
// but EnsureConfigDir uses os.UserHomeDir().
// For testing purposes, we can't easily mock os.UserHomeDir() without
// changing the code or environment variables.
// Since we are in a sandbox, it should be fine.
err := EnsureConfigDir()
if err != nil {
t.Errorf("EnsureConfigDir failed: %v", err)
}
}

View File

@@ -1,191 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package storage
import (
"encoding/hex"
"fmt"
"os"
"path/filepath"
"sync"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
"github.com/vmihailenco/msgpack/v5"
)
type Manager struct {
basePath string
ratchetsPath string
identitiesPath string
destinationTable string
knownDestinations string
transportIdentity string
mutex sync.RWMutex
}
type RatchetData struct {
RatchetKey []byte `msgpack:"ratchet_key"`
Received int64 `msgpack:"received"`
}
func NewManager() (*Manager, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("failed to get home directory: %w", err)
}
basePath := filepath.Join(homeDir, ".reticulum-go", "storage")
m := &Manager{
basePath: basePath,
ratchetsPath: filepath.Join(basePath, "ratchets"),
identitiesPath: filepath.Join(basePath, "identities"),
destinationTable: filepath.Join(basePath, "destination_table"),
knownDestinations: filepath.Join(basePath, "known_destinations"),
transportIdentity: filepath.Join(basePath, "transport_identity"),
}
if err := m.initializeDirectories(); err != nil {
return nil, err
}
return m, nil
}
func (m *Manager) initializeDirectories() error {
dirs := []string{
m.basePath,
m.ratchetsPath,
m.identitiesPath,
filepath.Join(m.basePath, "cache"),
filepath.Join(m.basePath, "cache", "announces"),
filepath.Join(m.basePath, "resources"),
}
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0700); err != nil {
return fmt.Errorf("failed to create directory %s: %w", dir, err)
}
}
return nil
}
func (m *Manager) SaveRatchet(identityHash []byte, ratchetKey []byte) error {
m.mutex.Lock()
defer m.mutex.Unlock()
hexHash := hex.EncodeToString(identityHash)
ratchetDir := filepath.Join(m.ratchetsPath, hexHash)
if err := os.MkdirAll(ratchetDir, 0700); err != nil {
return fmt.Errorf("failed to create ratchet directory: %w", err)
}
ratchetData := RatchetData{
RatchetKey: ratchetKey,
Received: time.Now().Unix(),
}
data, err := msgpack.Marshal(ratchetData)
if err != nil {
return fmt.Errorf("failed to marshal ratchet data: %w", err)
}
ratchetHash := hex.EncodeToString(ratchetKey[:16])
outPath := filepath.Join(ratchetDir, ratchetHash+".out")
finalPath := filepath.Join(ratchetDir, ratchetHash)
if err := os.WriteFile(outPath, data, 0600); err != nil {
return fmt.Errorf("failed to write ratchet file: %w", err)
}
if err := os.Rename(outPath, finalPath); err != nil {
_ = os.Remove(outPath)
return fmt.Errorf("failed to move ratchet file: %w", err)
}
debug.Log(debug.DEBUG_VERBOSE, "Saved ratchet to storage", "identity", hexHash, "ratchet", ratchetHash)
return nil
}
func (m *Manager) LoadRatchets(identityHash []byte) (map[string][]byte, error) {
m.mutex.RLock()
defer m.mutex.RUnlock()
hexHash := hex.EncodeToString(identityHash)
ratchetDir := filepath.Join(m.ratchetsPath, hexHash)
ratchets := make(map[string][]byte)
if _, err := os.Stat(ratchetDir); os.IsNotExist(err) {
debug.Log(debug.DEBUG_VERBOSE, "No ratchet directory found", "identity", hexHash)
return ratchets, nil
}
entries, err := os.ReadDir(ratchetDir)
if err != nil {
return nil, fmt.Errorf("failed to read ratchet directory: %w", err)
}
now := time.Now().Unix()
expiry := int64(2592000) // 30 days
for _, entry := range entries {
if entry.IsDir() {
continue
}
filePath := filepath.Join(ratchetDir, entry.Name())
// bearer:disable go_gosec_filesystem_filereadtaint
data, err := os.ReadFile(filePath) // #nosec G304 - reading from controlled directory
if err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to read ratchet file", "file", entry.Name(), "error", err)
continue
}
var ratchetData RatchetData
if err := msgpack.Unmarshal(data, &ratchetData); err != nil {
debug.Log(debug.DEBUG_ERROR, "Corrupted ratchet data", "file", entry.Name(), "error", err)
_ = os.Remove(filePath)
continue
}
if now > ratchetData.Received+expiry {
debug.Log(debug.DEBUG_VERBOSE, "Removing expired ratchet", "file", entry.Name())
_ = os.Remove(filePath)
continue
}
ratchetHash := entry.Name()
ratchets[ratchetHash] = ratchetData.RatchetKey
}
debug.Log(debug.DEBUG_VERBOSE, "Loaded ratchets from storage", "identity", hexHash, "count", len(ratchets))
return ratchets, nil
}
func (m *Manager) GetBasePath() string {
return m.basePath
}
func (m *Manager) GetRatchetsPath() string {
return m.ratchetsPath
}
func (m *Manager) GetIdentityPath() string {
return filepath.Join(m.basePath, "identity")
}
func (m *Manager) GetTransportIdentityPath() string {
return m.transportIdentity
}
func (m *Manager) GetDestinationTablePath() string {
return m.destinationTable
}
func (m *Manager) GetKnownDestinationsPath() string {
return m.knownDestinations
}

View File

@@ -1,117 +0,0 @@
package storage
import (
"bytes"
"os"
"path/filepath"
"testing"
)
func TestNewManager(t *testing.T) {
tmpDir := t.TempDir()
originalHome := os.Getenv("HOME")
os.Setenv("HOME", tmpDir)
defer os.Setenv("HOME", originalHome)
m, err := NewManager()
if err != nil {
t.Fatalf("NewManager failed: %v", err)
}
if m == nil {
t.Fatal("NewManager returned nil")
}
expectedBase := filepath.Join(tmpDir, ".reticulum-go", "storage")
if m.basePath != expectedBase {
t.Errorf("Expected basePath %s, got %s", expectedBase, m.basePath)
}
// Verify directories were created
dirs := []string{
m.basePath,
m.ratchetsPath,
m.identitiesPath,
filepath.Join(m.basePath, "cache"),
filepath.Join(m.basePath, "cache", "announces"),
filepath.Join(m.basePath, "resources"),
}
for _, dir := range dirs {
if _, err := os.Stat(dir); os.IsNotExist(err) {
t.Errorf("Directory %s was not created", dir)
}
}
}
func TestSaveLoadRatchets(t *testing.T) {
tmpDir := t.TempDir()
originalHome := os.Getenv("HOME")
os.Setenv("HOME", tmpDir)
defer os.Setenv("HOME", originalHome)
m, err := NewManager()
if err != nil {
t.Fatalf("NewManager failed: %v", err)
}
identityHash := []byte("test-identity-hash")
ratchetKey := make([]byte, 32)
for i := range ratchetKey {
ratchetKey[i] = byte(i)
}
err = m.SaveRatchet(identityHash, ratchetKey)
if err != nil {
t.Fatalf("SaveRatchet failed: %v", err)
}
ratchets, err := m.LoadRatchets(identityHash)
if err != nil {
t.Fatalf("LoadRatchets failed: %v", err)
}
if len(ratchets) != 1 {
t.Errorf("Expected 1 ratchet, got %d", len(ratchets))
}
// The key in the map is the hex of first 16 bytes of ratchetKey
found := false
for _, key := range ratchets {
if bytes.Equal(key, ratchetKey) {
found = true
break
}
}
if !found {
t.Error("Saved ratchet key not found in loaded ratchets")
}
}
func TestGetters(t *testing.T) {
tmpDir := t.TempDir()
originalHome := os.Getenv("HOME")
os.Setenv("HOME", tmpDir)
defer os.Setenv("HOME", originalHome)
m, _ := NewManager()
if m.GetBasePath() == "" {
t.Error("GetBasePath returned empty string")
}
if m.GetRatchetsPath() == "" {
t.Error("GetRatchetsPath returned empty string")
}
if m.GetIdentityPath() == "" {
t.Error("GetIdentityPath returned empty string")
}
if m.GetTransportIdentityPath() == "" {
t.Error("GetTransportIdentityPath returned empty string")
}
if m.GetDestinationTablePath() == "" {
t.Error("GetDestinationTablePath returned empty string")
}
if m.GetKnownDestinationsPath() == "" {
t.Error("GetKnownDestinationsPath returned empty string")
}
}

5
microvm/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
firecracker-config.json
rootfs.ext4
vmlinux.bin
reticulum-go
vsock.sock

121
microvm/README.md Normal file
View File

@@ -0,0 +1,121 @@
# Reticulum-Go MicroVM
Minimal Firecracker microVM setup for running Reticulum-Go.
## Prerequisites
- Firecracker binary installed
- Go compiler
- Root privileges (for network setup and KVM access)
- Linux host system with KVM support
- Access to `/dev/kvm`
## Important: Nested Virtualization
**If running inside a QEMU/KVM VM**, nested virtualization must be enabled:
1. **Host QEMU configuration**: Start your QEMU VM with nested KVM:
```bash
qemu-system-x86_64 -cpu host -enable-kvm -machine q35,accel=kvm ...
```
2. **Enable nested KVM on host** (if not already):
```bash
# Check if nested is enabled
cat /sys/module/kvm_intel/parameters/nested # Intel
cat /sys/module/kvm_amd/parameters/nested # AMD
# Enable nested (Intel)
echo "options kvm_intel nested=1" | sudo tee /etc/modprobe.d/kvm.conf
# Enable nested (AMD)
echo "options kvm_amd nested=1" | sudo tee /etc/modprobe.d/kvm.conf
# Reboot host
```
3. **Inside the VM**, check if `/dev/kvm` exists:
```bash
ls -l /dev/kvm
```
**Alternative**: If nested virtualization isn't available, consider:
- Running Firecracker directly on the host machine
- Using QEMU directly instead of Firecracker
- Using Docker/LXC containers instead
## KVM Setup
Ensure your user has access to `/dev/kvm`:
```bash
# Check if /dev/kvm exists
ls -l /dev/kvm
# Add your user to the kvm group (recommended)
sudo usermod -aG kvm $USER
# Or set ACL (alternative)
sudo setfacl -m u:$USER:rw /dev/kvm
# Log out and back in for group changes to take effect
```
## Setup
Run the setup script:
```bash
./setup.sh
```
This will:
- Check for Firecracker installation
- Download vmlinux.bin kernel
- Build Reticulum-Go binary
- Create rootfs.ext4 disk image
- Generate firecracker-config.json
## Running
1. Create tap interface:
```bash
sudo ip tuntap add tap0 mode tap
sudo ip addr add 172.16.0.1/24 dev tap0
sudo ip link set tap0 up
```
2. Enable IP forwarding:
```bash
sudo sysctl -w net.ipv4.ip_forward=1
```
3. Start Firecracker:
```bash
# Clean up any old socket files first
rm -f /tmp/firecracker.sock microvm/vsock.sock
firecracker --api-sock /tmp/firecracker.sock --config-file firecracker-config.json
```
4. Connect to console (in another terminal):
```bash
firecracker --api-sock /tmp/firecracker.sock
```
## Configuration
- **CPU**: 1 vCPU
- **Memory**: 128 MiB
- **Network**: tap0 interface
- **Disk**: rootfs.ext4 (100MB)
Modify `firecracker-config.json` to adjust resources.
## Files
- `vmlinux.bin` - Linux kernel
- `rootfs.ext4` - Root filesystem with binary
- `firecracker-config.json` - Firecracker configuration
- `reticulum-go` - Compiled binary

198
microvm/setup.sh Executable file
View File

@@ -0,0 +1,198 @@
#!/bin/sh
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
MICROVM_DIR="$SCRIPT_DIR"
BINARY_NAME="reticulum-go"
FIRECRACKER_VERSION="v1.8.0"
FIRECRACKER_REPO="firecracker-microvm/firecracker"
VMLINUX_URL="https://s3.amazonaws.com/spec.ccfc.min/img/hello/kernel/hello-vmlinux.bin"
check_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Error: $1 is not installed" >&2
exit 1
fi
}
check_firecracker() {
if ! command -v firecracker >/dev/null 2>&1; then
echo "Error: firecracker binary is not installed" >&2
echo "Install from: https://github.com/firecracker-microvm/firecracker/releases" >&2
exit 1
fi
echo "Firecracker found: $(firecracker --version 2>&1 || echo 'version check failed')"
}
download_vmlinux() {
VMLINUX_PATH="$MICROVM_DIR/vmlinux.bin"
if [ -f "$VMLINUX_PATH" ]; then
echo "vmlinux.bin already exists, skipping download"
return
fi
echo "Downloading vmlinux.bin from AWS S3..."
if ! command -v curl >/dev/null 2>&1; then
echo "Error: curl required to download vmlinux.bin" >&2
exit 1
fi
curl -fsSL -o "$VMLINUX_PATH" "$VMLINUX_URL"
chmod +x "$VMLINUX_PATH"
echo "Downloaded: $VMLINUX_PATH"
}
build_binary() {
echo "Building binary..."
cd "$PROJECT_ROOT"
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o "$MICROVM_DIR/$BINARY_NAME" ./cmd/reticulum-go
echo "Binary built: $MICROVM_DIR/$BINARY_NAME"
}
create_rootfs() {
ROOTFS_PATH="$MICROVM_DIR/rootfs.ext4"
if [ -f "$ROOTFS_PATH" ]; then
echo "rootfs.ext4 already exists, skipping creation"
return
fi
echo "Creating rootfs..."
TMP_DIR=$(mktemp -d)
trap "rm -rf $TMP_DIR" EXIT
mkdir -p "$TMP_DIR/bin" "$TMP_DIR/etc" "$TMP_DIR/dev" "$TMP_DIR/proc" "$TMP_DIR/sys" "$TMP_DIR/tmp"
cp "$MICROVM_DIR/$BINARY_NAME" "$TMP_DIR/bin/"
chmod +x "$TMP_DIR/bin/$BINARY_NAME"
cat > "$TMP_DIR/etc/inittab" <<EOF
::sysinit:/bin/sh /etc/rc
::respawn:/bin/sh
ttyS0::respawn:/bin/sh
EOF
cat > "$TMP_DIR/etc/rc" <<EOF
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
/bin/$BINARY_NAME
EOF
chmod +x "$TMP_DIR/etc/rc"
ROOTFS_SIZE_MB=100
dd if=/dev/zero of="$ROOTFS_PATH" bs=1M count="$ROOTFS_SIZE_MB" 2>/dev/null
mkfs.ext4 -F "$ROOTFS_PATH" >/dev/null 2>&1
TMP_MOUNT=$(mktemp -d)
mount -o loop "$ROOTFS_PATH" "$TMP_MOUNT" 2>/dev/null || {
echo "Error: Failed to mount rootfs. You may need root privileges or use a different method." >&2
rm -rf "$TMP_DIR" "$TMP_MOUNT"
exit 1
}
trap "umount $TMP_MOUNT 2>/dev/null; rm -rf $TMP_DIR $TMP_MOUNT" EXIT
cp -r "$TMP_DIR"/* "$TMP_MOUNT/"
umount "$TMP_MOUNT"
rm -rf "$TMP_DIR" "$TMP_MOUNT"
echo "Rootfs created: $ROOTFS_PATH"
}
create_config() {
CONFIG_PATH="$MICROVM_DIR/firecracker-config.json"
API_SOCK="${API_SOCK:-/tmp/firecracker.sock}"
VSOCK_SOCK="${VSOCK_SOCK:-$MICROVM_DIR/vsock.sock}"
cat > "$CONFIG_PATH" <<EOF
{
"boot-source": {
"kernel_image_path": "$MICROVM_DIR/vmlinux.bin",
"boot_args": "console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw"
},
"drives": [
{
"drive_id": "rootfs",
"path_on_host": "$MICROVM_DIR/rootfs.ext4",
"is_root_device": true,
"is_read_only": false
}
],
"machine-config": {
"vcpu_count": 1,
"mem_size_mib": 128,
"smt": false
},
"network-interfaces": [
{
"iface_id": "eth0",
"guest_mac": "AA:FC:00:00:00:01",
"host_dev_name": "tap0"
}
],
"vsock": {
"guest_cid": 3,
"uds_path": "$VSOCK_SOCK"
}
}
EOF
echo "Config created: $CONFIG_PATH"
echo "API socket: $API_SOCK"
echo "VSock socket: $VSOCK_SOCK"
}
check_firecracker() {
if ! command -v firecracker >/dev/null 2>&1; then
echo "Error: firecracker binary is not installed" >&2
echo "Install from: https://github.com/firecracker-microvm/firecracker/releases" >&2
exit 1
fi
echo "Firecracker found: $(firecracker --version 2>&1 || echo 'version check failed')"
}
check_kvm() {
if [ ! -c /dev/kvm ]; then
echo "Warning: /dev/kvm not found. KVM may not be available." >&2
return
fi
if [ ! -r /dev/kvm ] || [ ! -w /dev/kvm ]; then
echo "Warning: /dev/kvm exists but you may not have read/write access." >&2
echo "Add yourself to the kvm group: sudo usermod -aG kvm $USER" >&2
echo "Or set ACL: sudo setfacl -m u:$USER:rw /dev/kvm" >&2
else
echo "KVM access OK"
fi
}
cleanup_sockets() {
echo "Cleaning up old socket files..."
rm -f /tmp/firecracker.sock "$MICROVM_DIR/vsock.sock"
echo "Cleanup complete"
}
main() {
echo "Setting up microVM..."
cleanup_sockets
check_command go
check_firecracker
check_kvm
download_vmlinux
build_binary
create_rootfs
create_config
echo ""
echo "Setup complete!"
echo "Files created in: $MICROVM_DIR"
echo ""
echo "To run the microVM:"
echo " 1. Ensure KVM access: sudo usermod -aG kvm $USER (then logout/login)"
echo " 2. Create tap interface: sudo ip tuntap add tap0 mode tap"
echo " 3. Start firecracker: firecracker --api-sock /tmp/firecracker.sock --config-file $CONFIG_PATH"
}
main "$@"

View File

@@ -1,17 +0,0 @@
#!/usr/bin/env bash
# Copyright 2018 The Go Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
SOURCE="$(readlink "$SOURCE")"
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
done
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
# Increase the V8 stack size from the default of 984K
# to 8192K to ensure all tests can pass without hitting
# stack size limits.
exec node --stack-size=8192 "$DIR/wasm_exec_node.js" "$@"

View File

@@ -1,575 +0,0 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
"use strict";
(() => {
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!globalThis.fs) {
let outputBuf = "";
globalThis.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substring(0, nl));
outputBuf = outputBuf.substring(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!globalThis.process) {
globalThis.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!globalThis.path) {
globalThis.path = {
resolve(...pathSegments) {
return pathSegments.join("/");
}
}
}
if (!globalThis.crypto) {
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
}
if (!globalThis.performance) {
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
}
if (!globalThis.TextEncoder) {
throw new Error("globalThis.TextEncoder is not available, polyfill required");
}
if (!globalThis.TextDecoder) {
throw new Error("globalThis.TextDecoder is not available, polyfill required");
}
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
globalThis.Go = class {
constructor() {
this.argv = ["js"];
this.env = {};
this.exit = (code) => {
if (code !== 0) {
console.warn("exit code:", code);
}
};
this._exitPromise = new Promise((resolve) => {
this._resolveExitPromise = resolve;
});
this._pendingEvent = null;
this._scheduledTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const setInt64 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
}
const setInt32 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
}
const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
}
const loadValue = (addr) => {
const f = this.mem.getFloat64(addr, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = this.mem.getUint32(addr, true);
return this._values[id];
}
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
if (typeof v === "number" && v !== 0) {
if (isNaN(v)) {
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
return;
}
this.mem.setFloat64(addr, v, true);
return;
}
if (v === undefined) {
this.mem.setFloat64(addr, 0, true);
return;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 0;
switch (typeof v) {
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case "string":
typeFlag = 2;
break;
case "symbol":
typeFlag = 3;
break;
case "function":
typeFlag = 4;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
}
const loadSlice = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
}
const loadSliceOfValues = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (addr) => {
const saddr = getInt64(addr + 0);
const len = getInt64(addr + 8);
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
}
const testCallExport = (a, b) => {
this._inst.exports.testExport0();
return this._inst.exports.testExport(a, b);
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
_gotest: {
add: (a, b) => a + b,
callExport: testCallExport,
},
gojs: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
// This changes the SP, thus we have to update the SP used by the imported function.
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
sp >>>= 0;
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
delete this._inst;
delete this._values;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
},
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
"runtime.wasmWrite": (sp) => {
sp >>>= 0;
const fd = getInt64(sp + 8);
const p = getInt64(sp + 16);
const n = this.mem.getInt32(sp + 24, true);
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
},
// func resetMemoryDataView()
"runtime.resetMemoryDataView": (sp) => {
sp >>>= 0;
this.mem = new DataView(this._inst.exports.mem.buffer);
},
// func nanotime1() int64
"runtime.nanotime1": (sp) => {
sp >>>= 0;
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},
// func walltime() (sec int64, nsec int32)
"runtime.walltime": (sp) => {
sp >>>= 0;
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},
// func scheduleTimeoutEvent(delay int64) int32
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(id, setTimeout(
() => {
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
}
},
getInt64(sp + 8),
));
this.mem.setInt32(sp + 16, id, true);
},
// func clearTimeoutEvent(id int32)
"runtime.clearTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
this._scheduledTimeouts.delete(id);
},
// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
sp >>>= 0;
crypto.getRandomValues(loadSlice(sp + 8));
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
sp >>>= 0;
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
sp >>>= 0;
storeValue(sp + 24, loadString(sp + 8));
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (sp) => {
sp >>>= 0;
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 32, result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (sp) => {
sp >>>= 0;
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (sp) => {
sp >>>= 0;
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const m = Reflect.get(v, loadString(sp + 16));
const args = loadSliceOfValues(sp + 32);
const result = Reflect.apply(m, v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, result);
this.mem.setUint8(sp + 64, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, err);
this.mem.setUint8(sp + 64, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.apply(v, undefined, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.construct(v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (sp) => {
sp >>>= 0;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (sp) => {
sp >>>= 0;
const str = encoder.encode(String(loadValue(sp + 8)));
storeValue(sp + 16, str);
setInt64(sp + 24, str.length);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (sp) => {
sp >>>= 0;
const str = loadValue(sp + 8);
loadSlice(sp + 16).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => {
sp >>>= 0;
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (sp) => {
sp >>>= 0;
const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
// func copyBytesToJS(dst ref, src []byte) (int, bool)
"syscall/js.copyBytesToJS": (sp) => {
sp >>>= 0;
const dst = loadValue(sp + 8);
const src = loadSlice(sp + 16);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
"debug": (value) => {
console.log(value);
},
}
};
}
async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
globalThis,
this,
];
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map([ // mapping from JS values to reference ids
[0, 1],
[null, 2],
[true, 3],
[false, 4],
[globalThis, 5],
[this, 6],
]);
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
const strPtr = (str) => {
const ptr = offset;
const bytes = encoder.encode(str + "\0");
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
offset += bytes.length;
if (offset % 8 !== 0) {
offset += 8 - (offset % 8);
}
return ptr;
};
const argc = this.argv.length;
const argvPtrs = [];
this.argv.forEach((arg) => {
argvPtrs.push(strPtr(arg));
});
argvPtrs.push(0);
const keys = Object.keys(this.env).sort();
keys.forEach((key) => {
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
});
argvPtrs.push(0);
const argv = offset;
argvPtrs.forEach((ptr) => {
this.mem.setUint32(offset, ptr, true);
this.mem.setUint32(offset + 4, 0, true);
offset += 8;
});
// The linker guarantees global data starts from at least wasmMinDataAddr.
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
const wasmMinDataAddr = 4096 + 8192;
if (offset >= wasmMinDataAddr) {
throw new Error("total length of command line and environment variables exceeds limit");
}
this._inst.exports.run(argc, argv);
if (this.exited) {
this._resolveExitPromise();
}
await this._exitPromise;
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
})();

View File

@@ -1,41 +0,0 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
"use strict";
if (process.argv.length < 3) {
console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
process.exit(1);
}
globalThis.require = require;
globalThis.fs = require("fs");
globalThis.path = require("path");
globalThis.TextEncoder = require("util").TextEncoder;
globalThis.TextDecoder = require("util").TextDecoder;
globalThis.performance ??= require("performance");
globalThis.crypto ??= require("crypto");
require("./wasm_exec");
const go = new Go();
go.argv = process.argv.slice(2);
go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
go.exit = process.exit;
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
process.on("exit", (code) => { // Node.js exits if no event handler is pending
if (code === 0 && !go.exited) {
// deadlock, make Go print error and stack traces
go._pendingEvent = { id: 0 };
go._resume();
}
});
return go.run(result.instance);
}).catch((err) => {
// bearer:disable javascript_lang_logger_leak
console.error(err);
process.exit(1);
});

View File

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package announce package announce
import ( import (
@@ -8,12 +6,12 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"log"
"sync" "sync"
"time" "time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common" "github.com/Sudo-Ivan/reticulum-go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug" "github.com/Sudo-Ivan/reticulum-go/pkg/identity"
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
"golang.org/x/crypto/curve25519" "golang.org/x/crypto/curve25519"
) )
@@ -51,6 +49,12 @@ const (
MAX_RETRIES = 3 MAX_RETRIES = 3
) )
type AnnounceHandler interface {
AspectFilter() []string
ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte) error
ReceivePathResponses() bool
}
type Announce struct { type Announce struct {
mutex *sync.RWMutex mutex *sync.RWMutex
destinationHash []byte destinationHash []byte
@@ -63,7 +67,7 @@ type Announce struct {
signature []byte signature []byte
pathResponse bool pathResponse bool
retries int retries int
handlers []Handler handlers []AnnounceHandler
ratchetID []byte ratchetID []byte
packet []byte packet []byte
hash []byte hash []byte
@@ -93,7 +97,7 @@ func New(dest *identity.Identity, destinationHash []byte, destinationName string
timestamp: time.Now().Unix(), timestamp: time.Now().Unix(),
pathResponse: pathResponse, pathResponse: pathResponse,
retries: 0, retries: 0,
handlers: make([]Handler, 0), handlers: make([]AnnounceHandler, 0),
} }
// Get current ratchet ID if enabled // Get current ratchet ID if enabled
@@ -119,46 +123,46 @@ func (a *Announce) Propagate(interfaces []common.NetworkInterface) error {
a.mutex.RLock() a.mutex.RLock()
defer a.mutex.RUnlock() defer a.mutex.RUnlock()
debug.Log(debug.DEBUG_TRACE, "Propagating announce across interfaces", "count", len(interfaces)) log.Printf("[DEBUG-7] Propagating announce across %d interfaces", len(interfaces))
var packet []byte var packet []byte
if a.packet != nil { if a.packet != nil {
debug.Log(debug.DEBUG_TRACE, "Using cached packet", "bytes", len(a.packet)) log.Printf("[DEBUG-7] Using cached packet (%d bytes)", len(a.packet))
packet = a.packet packet = a.packet
} else { } else {
debug.Log(debug.DEBUG_TRACE, "Creating new packet") log.Printf("[DEBUG-7] Creating new packet")
packet = a.CreatePacket() packet = a.CreatePacket()
a.packet = packet a.packet = packet
} }
for _, iface := range interfaces { for _, iface := range interfaces {
if !iface.IsEnabled() { if !iface.IsEnabled() {
debug.Log(debug.DEBUG_TRACE, "Skipping disabled interface", "name", iface.GetName()) log.Printf("[DEBUG-7] Skipping disabled interface: %s", iface.GetName())
continue continue
} }
if !iface.GetBandwidthAvailable() { if !iface.GetBandwidthAvailable() {
debug.Log(debug.DEBUG_TRACE, "Skipping interface with insufficient bandwidth", "name", iface.GetName()) log.Printf("[DEBUG-7] Skipping interface with insufficient bandwidth: %s", iface.GetName())
continue continue
} }
debug.Log(debug.DEBUG_TRACE, "Sending announce on interface", "name", iface.GetName()) log.Printf("[DEBUG-7] Sending announce on interface %s", iface.GetName())
if err := iface.Send(packet, ""); err != nil { if err := iface.Send(packet, ""); err != nil {
debug.Log(debug.DEBUG_TRACE, "Failed to send on interface", "name", iface.GetName(), "error", err) log.Printf("[DEBUG-7] Failed to send on interface %s: %v", iface.GetName(), err)
return fmt.Errorf("failed to propagate on interface %s: %w", iface.GetName(), err) return fmt.Errorf("failed to propagate on interface %s: %w", iface.GetName(), err)
} }
debug.Log(debug.DEBUG_TRACE, "Successfully sent announce on interface", "name", iface.GetName()) log.Printf("[DEBUG-7] Successfully sent announce on interface %s", iface.GetName())
} }
return nil return nil
} }
func (a *Announce) RegisterHandler(handler Handler) { func (a *Announce) RegisterHandler(handler AnnounceHandler) {
a.mutex.Lock() a.mutex.Lock()
defer a.mutex.Unlock() defer a.mutex.Unlock()
a.handlers = append(a.handlers, handler) a.handlers = append(a.handlers, handler)
} }
func (a *Announce) DeregisterHandler(handler Handler) { func (a *Announce) DeregisterHandler(handler AnnounceHandler) {
a.mutex.Lock() a.mutex.Lock()
defer a.mutex.Unlock() defer a.mutex.Unlock()
for i, h := range a.handlers { for i, h := range a.handlers {
@@ -173,13 +177,13 @@ func (a *Announce) HandleAnnounce(data []byte) error {
a.mutex.Lock() a.mutex.Lock()
defer a.mutex.Unlock() defer a.mutex.Unlock()
debug.Log(debug.DEBUG_TRACE, "Handling announce packet", "bytes", len(data)) log.Printf("[DEBUG-7] Handling announce packet of %d bytes", len(data))
// Minimum packet size validation // Minimum packet size validation
// header(2) + desthash(16) + context(1) + enckey(32) + signkey(32) + namehash(10) + // header(2) + desthash(16) + context(1) + enckey(32) + signkey(32) + namehash(10) +
// randomhash(10) + signature(64) + min app data(3) // randomhash(10) + signature(64) + min app data(3)
if len(data) < 170 { if len(data) < 170 {
debug.Log(debug.DEBUG_TRACE, "Invalid announce data length", "bytes", len(data), "minimum", 170) log.Printf("[DEBUG-7] Invalid announce data length: %d bytes (minimum 170)", len(data))
return errors.New("invalid announce data length") return errors.New("invalid announce data length")
} }
@@ -192,7 +196,7 @@ func (a *Announce) HandleAnnounce(data []byte) error {
// Get hop count // Get hop count
hopCount := header[1] hopCount := header[1]
if hopCount > MAX_HOPS { if hopCount > MAX_HOPS {
debug.Log(debug.DEBUG_TRACE, "Announce exceeded max hops", "hops", hopCount) log.Printf("[DEBUG-7] Announce exceeded max hops: %d", hopCount)
return errors.New("announce exceeded maximum hop count") return errors.New("announce exceeded maximum hop count")
} }
@@ -211,7 +215,8 @@ func (a *Announce) HandleAnnounce(data []byte) error {
contextByte = data[34] contextByte = data[34]
packetData = data[35:] packetData = data[35:]
debug.Log(debug.DEBUG_TRACE, "Header type 2 announce", "destHash", fmt.Sprintf("%x", destHash), "transportID", fmt.Sprintf("%x", transportID), "context", contextByte) log.Printf("[DEBUG-7] Header type 2 announce: destHash=%x, transportID=%x, context=%d",
destHash, transportID, contextByte)
} else { } else {
// Header type 1 format: header(2) + desthash(16) + context(1) + data // Header type 1 format: header(2) + desthash(16) + context(1) + data
if len(data) < 19 { if len(data) < 19 {
@@ -221,7 +226,8 @@ func (a *Announce) HandleAnnounce(data []byte) error {
contextByte = data[18] contextByte = data[18]
packetData = data[19:] packetData = data[19:]
debug.Log(debug.DEBUG_TRACE, "Header type 1 announce", "destHash", fmt.Sprintf("%x", destHash), "context", contextByte) log.Printf("[DEBUG-7] Header type 1 announce: destHash=%x, context=%d",
destHash, contextByte)
} }
// Now parse the data portion according to the spec // Now parse the data portion according to the spec
@@ -240,10 +246,10 @@ func (a *Announce) HandleAnnounce(data []byte) error {
signature := packetData[116:180] signature := packetData[116:180]
appData := packetData[180:] appData := packetData[180:]
debug.Log(debug.DEBUG_TRACE, "Announce fields", "encKey", fmt.Sprintf("%x", encKey), "signKey", fmt.Sprintf("%x", signKey)) log.Printf("[DEBUG-7] Announce fields: encKey=%x, signKey=%x", encKey, signKey)
debug.Log(debug.DEBUG_TRACE, "Name and random hash", "nameHash", fmt.Sprintf("%x", nameHash), "randomHash", fmt.Sprintf("%x", randomHash)) log.Printf("[DEBUG-7] Name hash=%x, random hash=%x", nameHash, randomHash)
debug.Log(debug.DEBUG_TRACE, "Ratchet data", "ratchet", fmt.Sprintf("%x", ratchetData[:8])) log.Printf("[DEBUG-7] Ratchet=%x", ratchetData[:8])
debug.Log(debug.DEBUG_TRACE, "Signature and app data", "signature", fmt.Sprintf("%x", signature[:8]), "appDataLen", len(appData)) log.Printf("[DEBUG-7] Signature=%x, appDataLen=%d", signature[:8], len(appData))
// Get the destination hash from header // Get the destination hash from header
var destHash []byte var destHash []byte
@@ -279,7 +285,7 @@ func (a *Announce) HandleAnnounce(data []byte) error {
// Process with handlers // Process with handlers
for _, handler := range a.handlers { for _, handler := range a.handlers {
if handler.ReceivePathResponses() || !a.pathResponse { if handler.ReceivePathResponses() || !a.pathResponse {
if err := handler.ReceivedAnnounce(destHash, announcedIdentity, appData, hopCount); err != nil { if err := handler.ReceivedAnnounce(destHash, announcedIdentity, appData); err != nil {
return err return err
} }
} }
@@ -298,7 +304,11 @@ func (a *Announce) RequestPath(destHash []byte, onInterface common.NetworkInterf
packet = append(packet, byte(0)) // Initial hop count packet = append(packet, byte(0)) // Initial hop count
// Send path request // Send path request
return onInterface.Send(packet, "") if err := onInterface.Send(packet, ""); err != nil {
return err
}
return nil
} }
// CreateHeader creates a Reticulum packet header according to spec // CreateHeader creates a Reticulum packet header according to spec
@@ -318,40 +328,36 @@ func CreateHeader(ifacFlag byte, headerType byte, contextFlag byte, propType byt
func (a *Announce) CreatePacket() []byte { func (a *Announce) CreatePacket() []byte {
// This function creates the complete announce packet according to the Reticulum specification. // This function creates the complete announce packet according to the Reticulum specification.
// Announce Packet Structure: // Announce Packet Structure:
// [Header (2 bytes)][Dest Hash (16 bytes)][Context (1 byte)][Announce Data] // [Header (2 bytes)][Dest Hash (16 bytes)][Transport ID (16 bytes)][Context (1 byte)][Announce Data]
// Announce Data Structure: // Announce Data Structure:
// [Public Key (64 bytes)][Name Hash (10 bytes)][Random Hash (10 bytes)][Ratchet (32 bytes optional)][Signature (64 bytes)][App Data] // [Public Key (32 bytes)][Signing Key (32 bytes)][Name Hash (10 bytes)][Random Hash (10 bytes)][Ratchet (32 bytes)][Signature (64 bytes)][App Data]
// 2. Destination Hash // 2. Destination Hash
destHash := a.destinationHash destHash := a.destinationHash
if len(destHash) > 16 { if len(destHash) == 0 {
destHash = destHash[:16]
} }
// 3. Announce Data // 3. Transport ID (zeros for broadcast announce)
// 3.1 Public Key (full 64 bytes - not split into enc/sign keys in packet) transportID := make([]byte, 16)
// 5. Announce Data
// 5.1 Public Keys
pubKey := a.identity.GetPublicKey() pubKey := a.identity.GetPublicKey()
if len(pubKey) != 64 { encKey := pubKey[:32]
debug.Log(debug.DEBUG_TRACE, "Invalid public key length", "expected", 64, "got", len(pubKey)) signKey := pubKey[32:]
}
// 3.2 Name Hash // 5.2 Name Hash
nameHash := sha256.Sum256([]byte(a.destinationName)) nameHash := sha256.Sum256([]byte(a.destinationName))
nameHash10 := nameHash[:10] nameHash10 := nameHash[:10]
// 3.3 Random Hash (5 bytes random + 5 bytes timestamp) // 5.3 Random Hash
randomHash := make([]byte, 10) randomHash := make([]byte, 10)
_, err := rand.Read(randomHash[:5]) _, err := rand.Read(randomHash)
if err != nil { if err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to read random bytes for announce", "error", err) log.Printf("Error reading random bytes for announce: %v", err)
} }
// Add 5 bytes of timestamp
timeBytes := make([]byte, 8)
// #nosec G115 - Unix timestamp is always positive, no overflow risk
binary.BigEndian.PutUint64(timeBytes, uint64(time.Now().Unix()))
copy(randomHash[5:], timeBytes[:5])
// 3.4 Ratchet (only include if exists) // 5.4 Ratchet (only include if exists)
var ratchetData []byte var ratchetData []byte
currentRatchetKey := a.identity.GetCurrentRatchetKey() currentRatchetKey := a.identity.GetCurrentRatchetKey()
if currentRatchetKey != nil { if currentRatchetKey != nil {
@@ -368,10 +374,10 @@ func (a *Announce) CreatePacket() []byte {
contextFlag = 1 // FLAG_SET contextFlag = 1 // FLAG_SET
} }
// 1. Create Header - Use HEADER_TYPE_1 // 1. Create Header (now that we know context flag)
header := CreateHeader( header := CreateHeader(
IFAC_NONE, IFAC_NONE,
HEADER_TYPE_1, HEADER_TYPE_2,
contextFlag, contextFlag,
PROP_TYPE_BROADCAST, PROP_TYPE_BROADCAST,
DEST_TYPE_SINGLE, DEST_TYPE_SINGLE,
@@ -381,15 +387,13 @@ func (a *Announce) CreatePacket() []byte {
// 4. Context Byte // 4. Context Byte
contextByte := byte(0) contextByte := byte(0)
if a.pathResponse {
contextByte = 0x0B // PATH_RESPONSE context
}
// 3.5 Signature // 5.5 Signature
// The signature is calculated over: Dest Hash + Public Key (64 bytes) + Name Hash + Random Hash + Ratchet (if exists) + App Data // The signature is calculated over: Dest Hash + Public Keys + Name Hash + Random Hash + Ratchet (if exists) + App Data
validationData := make([]byte, 0) validationData := make([]byte, 0)
validationData = append(validationData, destHash...) validationData = append(validationData, destHash...)
validationData = append(validationData, pubKey...) validationData = append(validationData, encKey...)
validationData = append(validationData, signKey...)
validationData = append(validationData, nameHash10...) validationData = append(validationData, nameHash10...)
validationData = append(validationData, randomHash...) validationData = append(validationData, randomHash...)
if len(ratchetData) > 0 { if len(ratchetData) > 0 {
@@ -398,14 +402,14 @@ func (a *Announce) CreatePacket() []byte {
validationData = append(validationData, a.appData...) validationData = append(validationData, a.appData...)
signature := a.identity.Sign(validationData) signature := a.identity.Sign(validationData)
debug.Log(debug.DEBUG_TRACE, "Creating announce packet", "destHash", fmt.Sprintf("%x", destHash), "pubKeyLen", len(pubKey), "nameHash", fmt.Sprintf("%x", nameHash10), "randomHash", fmt.Sprintf("%x", randomHash), "ratchetLen", len(ratchetData), "sigLen", len(signature), "appDataLen", len(a.appData)) // 6. Assemble the packet
// 5. Assemble the packet (HEADER_TYPE_1 format)
packet := make([]byte, 0) packet := make([]byte, 0)
packet = append(packet, header...) packet = append(packet, header...)
packet = append(packet, destHash...) packet = append(packet, destHash...)
packet = append(packet, transportID...)
packet = append(packet, contextByte) packet = append(packet, contextByte)
packet = append(packet, pubKey...) packet = append(packet, encKey...)
packet = append(packet, signKey...)
packet = append(packet, nameHash10...) packet = append(packet, nameHash10...)
packet = append(packet, randomHash...) packet = append(packet, randomHash...)
if len(ratchetData) > 0 { if len(ratchetData) > 0 {
@@ -414,8 +418,6 @@ func (a *Announce) CreatePacket() []byte {
packet = append(packet, signature...) packet = append(packet, signature...)
packet = append(packet, a.appData...) packet = append(packet, a.appData...)
debug.Log(debug.DEBUG_TRACE, "Final announce packet", "totalBytes", len(packet), "ratchetLen", len(ratchetData), "appDataLen", len(a.appData))
return packet return packet
} }
@@ -450,10 +452,11 @@ func NewAnnouncePacket(pubKey []byte, appData []byte, announceID []byte) *Announ
// NewAnnounce creates a new announce packet for a destination // NewAnnounce creates a new announce packet for a destination
func NewAnnounce(identity *identity.Identity, destinationHash []byte, appData []byte, ratchetID []byte, pathResponse bool, config *common.ReticulumConfig) (*Announce, error) { func NewAnnounce(identity *identity.Identity, destinationHash []byte, appData []byte, ratchetID []byte, pathResponse bool, config *common.ReticulumConfig) (*Announce, error) {
debug.Log(debug.DEBUG_TRACE, "Creating new announce", "destHash", fmt.Sprintf("%x", destinationHash), "appDataLen", len(appData), "hasRatchet", ratchetID != nil, "pathResponse", pathResponse) log.Printf("[DEBUG-7] Creating new announce: destHash=%x, appDataLen=%d, hasRatchet=%v, pathResponse=%v",
destinationHash, len(appData), ratchetID != nil, pathResponse)
if identity == nil { if identity == nil {
debug.Log(debug.DEBUG_ERROR, "Nil identity provided") log.Printf("[DEBUG-7] Error: nil identity provided")
return nil, errors.New("identity cannot be nil") return nil, errors.New("identity cannot be nil")
} }
@@ -466,7 +469,7 @@ func NewAnnounce(identity *identity.Identity, destinationHash []byte, appData []
} }
destHash := destinationHash destHash := destinationHash
debug.Log(debug.DEBUG_TRACE, "Using provided destination hash", "destHash", fmt.Sprintf("%x", destHash)) log.Printf("[DEBUG-7] Using provided destination hash: %x", destHash)
a := &Announce{ a := &Announce{
identity: identity, identity: identity,
@@ -476,11 +479,12 @@ func NewAnnounce(identity *identity.Identity, destinationHash []byte, appData []
destinationHash: destHash, destinationHash: destHash,
hops: 0, hops: 0,
mutex: &sync.RWMutex{}, mutex: &sync.RWMutex{},
handlers: make([]Handler, 0), handlers: make([]AnnounceHandler, 0),
config: config, config: config,
} }
debug.Log(debug.DEBUG_TRACE, "Created announce object", "destHash", fmt.Sprintf("%x", a.destinationHash), "hops", a.hops) log.Printf("[DEBUG-7] Created announce object: destHash=%x, hops=%d",
a.destinationHash, a.hops)
// Create initial packet // Create initial packet
packet := a.CreatePacket() packet := a.CreatePacket()
@@ -488,7 +492,7 @@ func NewAnnounce(identity *identity.Identity, destinationHash []byte, appData []
// Generate hash // Generate hash
hash := a.Hash() hash := a.Hash()
debug.Log(debug.DEBUG_TRACE, "Generated announce hash", "hash", fmt.Sprintf("%x", hash)) log.Printf("[DEBUG-7] Generated announce hash: %x", hash)
return a, nil return a, nil
} }

View File

@@ -1,123 +0,0 @@
package announce
import (
"bytes"
"sync"
"testing"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
)
type mockAnnounceHandler struct {
received bool
}
func (m *mockAnnounceHandler) AspectFilter() []string {
return nil
}
func (m *mockAnnounceHandler) ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte, hops uint8) error {
m.received = true
return nil
}
func (m *mockAnnounceHandler) ReceivePathResponses() bool {
return true
}
type mockInterface struct {
common.BaseInterface
sent bool
}
func (m *mockInterface) Send(data []byte, address string) error {
m.sent = true
return nil
}
func (m *mockInterface) GetBandwidthAvailable() bool {
return true
}
func (m *mockInterface) IsEnabled() bool {
return true
}
func TestNewAnnounce(t *testing.T) {
id, _ := identity.New()
destHash := make([]byte, 16)
config := &common.ReticulumConfig{}
ann, err := New(id, destHash, "testapp", []byte("appdata"), false, config)
if err != nil {
t.Fatalf("New failed: %v", err)
}
if ann == nil {
t.Fatal("New returned nil")
}
if !bytes.Equal(ann.destinationHash, destHash) {
t.Error("Destination hash doesn't match")
}
}
func TestCreateAndHandleAnnounce(t *testing.T) {
id, _ := identity.New()
destHash := make([]byte, 16)
config := &common.ReticulumConfig{}
ann, _ := New(id, destHash, "testapp", []byte("appdata"), false, config)
packet := ann.CreatePacket()
handler := &mockAnnounceHandler{}
ann.RegisterHandler(handler)
err := ann.HandleAnnounce(packet)
if err != nil {
t.Fatalf("HandleAnnounce failed: %v", err)
}
if !handler.received {
t.Error("Handler did not receive announce")
}
}
func TestPropagate(t *testing.T) {
id, _ := identity.New()
destHash := make([]byte, 16)
config := &common.ReticulumConfig{}
ann, _ := New(id, destHash, "testapp", []byte("appdata"), false, config)
iface := &mockInterface{}
iface.Name = "testiface"
iface.Online = true
iface.Enabled = true
err := ann.Propagate([]common.NetworkInterface{iface})
if err != nil {
t.Fatalf("Propagate failed: %v", err)
}
if !iface.sent {
t.Error("Packet was not sent on interface")
}
}
func TestHandlerRegistration(t *testing.T) {
ann := &Announce{
mutex: &sync.RWMutex{},
}
handler := &mockAnnounceHandler{}
ann.RegisterHandler(handler)
if len(ann.handlers) != 1 {
t.Errorf("Expected 1 handler, got %d", len(ann.handlers))
}
ann.DeregisterHandler(handler)
if len(ann.handlers) != 0 {
t.Errorf("Expected 0 handlers, got %d", len(ann.handlers))
}
}

View File

@@ -1,9 +1,7 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package announce package announce
type Handler interface { type Handler interface {
AspectFilter() []string AspectFilter() []string
ReceivedAnnounce(destHash []byte, identity interface{}, appData []byte, hops uint8) error ReceivedAnnounce(destHash []byte, identity interface{}, appData []byte) error
ReceivePathResponses() bool ReceivePathResponses() bool
} }

View File

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package buffer package buffer
import ( import (
@@ -10,7 +8,7 @@ import (
"io" "io"
"sync" "sync"
"git.quad4.io/Networks/Reticulum-Go/pkg/channel" "github.com/Sudo-Ivan/reticulum-go/pkg/channel"
) )
const ( const (
@@ -18,19 +16,6 @@ const (
MaxChunkLen = 16 * 1024 MaxChunkLen = 16 * 1024
MaxDataLen = 457 // MDU - 2 - 6 (2 for stream header, 6 for channel envelope) MaxDataLen = 457 // MDU - 2 - 6 (2 for stream header, 6 for channel envelope)
CompressTries = 4 CompressTries = 4
// Stream header flags
StreamHeaderEOF = 0x8000
StreamHeaderCompressed = 0x4000
// Message type
StreamDataMessageType = 0x01
// Header size
StreamHeaderSize = 2
// Compression threshold
CompressThreshold = 32
) )
type StreamDataMessage struct { type StreamDataMessage struct {
@@ -43,10 +28,10 @@ type StreamDataMessage struct {
func (m *StreamDataMessage) Pack() ([]byte, error) { func (m *StreamDataMessage) Pack() ([]byte, error) {
headerVal := uint16(m.StreamID & StreamIDMax) headerVal := uint16(m.StreamID & StreamIDMax)
if m.EOF { if m.EOF {
headerVal |= StreamHeaderEOF headerVal |= 0x8000
} }
if m.Compressed { if m.Compressed {
headerVal |= StreamHeaderCompressed headerVal |= 0x4000
} }
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
@@ -58,19 +43,19 @@ func (m *StreamDataMessage) Pack() ([]byte, error) {
} }
func (m *StreamDataMessage) GetType() uint16 { func (m *StreamDataMessage) GetType() uint16 {
return StreamDataMessageType return 0x01 // Assign appropriate message type constant
} }
func (m *StreamDataMessage) Unpack(data []byte) error { func (m *StreamDataMessage) Unpack(data []byte) error {
if len(data) < StreamHeaderSize { if len(data) < 2 {
return io.ErrShortBuffer return io.ErrShortBuffer
} }
header := binary.BigEndian.Uint16(data[:StreamHeaderSize]) header := binary.BigEndian.Uint16(data[:2])
m.StreamID = header & StreamIDMax m.StreamID = header & StreamIDMax
m.EOF = (header & StreamHeaderEOF) != 0 m.EOF = (header & 0x8000) != 0
m.Compressed = (header & StreamHeaderCompressed) != 0 m.Compressed = (header & 0x4000) != 0
m.Data = data[StreamHeaderSize:] m.Data = data[2:]
return nil return nil
} }
@@ -80,9 +65,7 @@ type RawChannelReader struct {
channel *channel.Channel channel *channel.Channel
buffer *bytes.Buffer buffer *bytes.Buffer
eof bool eof bool
callbacks map[int]func(int) callbacks []func(int)
nextCallbackID int
messageHandlerID int
mutex sync.RWMutex mutex sync.RWMutex
} }
@@ -91,26 +74,28 @@ func NewRawChannelReader(streamID int, ch *channel.Channel) *RawChannelReader {
streamID: streamID, streamID: streamID,
channel: ch, channel: ch,
buffer: bytes.NewBuffer(nil), buffer: bytes.NewBuffer(nil),
callbacks: make(map[int]func(int)), callbacks: make([]func(int), 0),
} }
reader.messageHandlerID = ch.AddMessageHandler(reader.HandleMessage) ch.AddMessageHandler(reader.HandleMessage)
return reader return reader
} }
func (r *RawChannelReader) AddReadyCallback(cb func(int)) int { func (r *RawChannelReader) AddReadyCallback(cb func(int)) {
r.mutex.Lock() r.mutex.Lock()
defer r.mutex.Unlock() defer r.mutex.Unlock()
id := r.nextCallbackID r.callbacks = append(r.callbacks, cb)
r.nextCallbackID++
r.callbacks[id] = cb
return id
} }
func (r *RawChannelReader) RemoveReadyCallback(id int) { func (r *RawChannelReader) RemoveReadyCallback(cb func(int)) {
r.mutex.Lock() r.mutex.Lock()
defer r.mutex.Unlock() defer r.mutex.Unlock()
delete(r.callbacks, id) for i, fn := range r.callbacks {
if &fn == &cb {
r.callbacks = append(r.callbacks[:i], r.callbacks[i+1:]...)
break
}
}
} }
func (r *RawChannelReader) Read(p []byte) (n int, err error) { func (r *RawChannelReader) Read(p []byte) (n int, err error) {
@@ -125,7 +110,7 @@ func (r *RawChannelReader) Read(p []byte) (n int, err error) {
if err == io.EOF && !r.eof { if err == io.EOF && !r.eof {
err = nil err = nil
} }
return n, err return
} }
func (r *RawChannelReader) HandleMessage(msg channel.MessageBase) bool { // #nosec G115 func (r *RawChannelReader) HandleMessage(msg channel.MessageBase) bool { // #nosec G115
@@ -178,7 +163,7 @@ func (w *RawChannelWriter) Write(p []byte) (n int, err error) {
EOF: w.eof, EOF: w.eof,
} }
if len(p) > CompressThreshold { if len(p) > 32 {
for try := 1; try < CompressTries; try++ { for try := 1; try < CompressTries; try++ {
chunkLen := len(p) / try chunkLen := len(p) / try
compressed := compressData(p[:chunkLen]) compressed := compressData(p[:chunkLen])
@@ -216,7 +201,10 @@ func (b *Buffer) Read(p []byte) (n int, err error) {
} }
func (b *Buffer) Close() error { func (b *Buffer) Close() error {
return b.ReadWriter.Writer.Flush() if err := b.ReadWriter.Writer.Flush(); err != nil {
return err
}
return nil
} }
func CreateReader(streamID int, ch *channel.Channel, readyCallback func(int)) *bufio.Reader { func CreateReader(streamID int, ch *channel.Channel, readyCallback func(int)) *bufio.Reader {
@@ -242,7 +230,6 @@ func compressData(data []byte) []byte {
var compressed bytes.Buffer var compressed bytes.Buffer
w := bytes.NewBuffer(data) w := bytes.NewBuffer(data)
r := bzip2.NewReader(w) r := bzip2.NewReader(w)
// bearer:disable go_gosec_filesystem_decompression_bomb
_, err := io.Copy(&compressed, r) // #nosec G104 #nosec G110 _, err := io.Copy(&compressed, r) // #nosec G104 #nosec G110
if err != nil { if err != nil {
// Handle error, e.g., log it or return an error // Handle error, e.g., log it or return an error
@@ -256,7 +243,6 @@ func decompressData(data []byte) []byte {
var decompressed bytes.Buffer var decompressed bytes.Buffer
// Limit the amount of data read to prevent decompression bombs // Limit the amount of data read to prevent decompression bombs
limitedReader := io.LimitReader(reader, MaxChunkLen) // #nosec G110 limitedReader := io.LimitReader(reader, MaxChunkLen) // #nosec G110
// bearer:disable go_gosec_filesystem_decompression_bomb
_, err := io.Copy(&decompressed, limitedReader) _, err := io.Copy(&decompressed, limitedReader)
if err != nil { if err != nil {
// Handle error, e.g., log it or return an error // Handle error, e.g., log it or return an error

View File

@@ -1,449 +0,0 @@
package buffer
import (
"bufio"
"bytes"
"io"
"testing"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/channel"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
"git.quad4.io/Networks/Reticulum-Go/pkg/transport"
)
func TestStreamDataMessage_Pack(t *testing.T) {
tests := []struct {
name string
streamID uint16
data []byte
eof bool
compressed bool
}{
{
name: "NormalMessage",
streamID: 123,
data: []byte("test data"),
eof: false,
compressed: false,
},
{
name: "EOFMessage",
streamID: 456,
data: []byte("final data"),
eof: true,
compressed: false,
},
{
name: "CompressedMessage",
streamID: 789,
data: []byte("compressed data"),
eof: false,
compressed: true,
},
{
name: "EmptyData",
streamID: 0,
data: []byte{},
eof: false,
compressed: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
msg := &StreamDataMessage{
StreamID: tt.streamID,
Data: tt.data,
EOF: tt.eof,
Compressed: tt.compressed,
}
packed, err := msg.Pack()
if err != nil {
t.Fatalf("Pack() failed: %v", err)
}
if len(packed) < 2 {
t.Error("Packed message too short")
}
unpacked := &StreamDataMessage{}
if err := unpacked.Unpack(packed); err != nil {
t.Fatalf("Unpack() failed: %v", err)
}
if unpacked.StreamID != tt.streamID {
t.Errorf("StreamID = %d, want %d", unpacked.StreamID, tt.streamID)
}
if unpacked.EOF != tt.eof {
t.Errorf("EOF = %v, want %v", unpacked.EOF, tt.eof)
}
if unpacked.Compressed != tt.compressed {
t.Errorf("Compressed = %v, want %v", unpacked.Compressed, tt.compressed)
}
if !bytes.Equal(unpacked.Data, tt.data) {
t.Errorf("Data = %v, want %v", unpacked.Data, tt.data)
}
})
}
}
func TestStreamDataMessage_Unpack(t *testing.T) {
tests := []struct {
name string
data []byte
wantError bool
}{
{
name: "ValidMessage",
data: []byte{0x00, 0x7B, 'h', 'e', 'l', 'l', 'o'},
wantError: false,
},
{
name: "TooShort",
data: []byte{0x00},
wantError: true,
},
{
name: "Empty",
data: []byte{},
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
msg := &StreamDataMessage{}
err := msg.Unpack(tt.data)
if (err != nil) != tt.wantError {
t.Errorf("Unpack() error = %v, wantError %v", err, tt.wantError)
}
})
}
}
func TestStreamDataMessage_GetType(t *testing.T) {
msg := &StreamDataMessage{}
if msg.GetType() != 0x01 {
t.Errorf("GetType() = %d, want 0x01", msg.GetType())
}
}
func TestRawChannelReader_AddCallback(t *testing.T) {
reader := &RawChannelReader{
streamID: 1,
buffer: bytes.NewBuffer(nil),
callbacks: make(map[int]func(int)),
}
cb := func(int) {}
reader.AddReadyCallback(cb)
if len(reader.callbacks) != 1 {
t.Error("Callback should be added")
}
}
func TestBuffer_Write(t *testing.T) {
buf := &Buffer{
ReadWriter: bufio.NewReadWriter(bufio.NewReader(bytes.NewBuffer(nil)), bufio.NewWriter(bytes.NewBuffer(nil))),
}
data := []byte("test")
n, err := buf.Write(data)
if err != nil {
t.Errorf("Write() error = %v", err)
}
if n != len(data) {
t.Errorf("Write() = %d bytes, want %d", n, len(data))
}
}
func TestBuffer_Read(t *testing.T) {
buf := &Buffer{
ReadWriter: bufio.NewReadWriter(bufio.NewReader(bytes.NewBuffer([]byte("test data"))), bufio.NewWriter(bytes.NewBuffer(nil))),
}
data := make([]byte, 10)
n, err := buf.Read(data)
if err != nil && err != io.EOF {
t.Errorf("Read() error = %v", err)
}
if n <= 0 {
t.Errorf("Read() = %d bytes, want > 0", n)
}
}
func TestBuffer_Close(t *testing.T) {
buf := &Buffer{
ReadWriter: bufio.NewReadWriter(bufio.NewReader(bytes.NewBuffer(nil)), bufio.NewWriter(bytes.NewBuffer(nil))),
}
if err := buf.Close(); err != nil {
t.Errorf("Close() error = %v", err)
}
}
func TestStreamIDMax(t *testing.T) {
if StreamIDMax != 0x3fff {
t.Errorf("StreamIDMax = %d, want %d", StreamIDMax, 0x3fff)
}
}
func TestMaxChunkLen(t *testing.T) {
if MaxChunkLen != 16*1024 {
t.Errorf("MaxChunkLen = %d, want %d", MaxChunkLen, 16*1024)
}
}
func TestMaxDataLen(t *testing.T) {
if MaxDataLen != 457 {
t.Errorf("MaxDataLen = %d, want %d", MaxDataLen, 457)
}
}
type mockLink struct {
status byte
rtt float64
}
func (m *mockLink) GetStatus() byte { return m.status }
func (m *mockLink) GetRTT() float64 { return m.rtt }
func (m *mockLink) RTT() float64 { return m.rtt }
func (m *mockLink) GetLinkID() []byte { return []byte("testlink") }
func (m *mockLink) Send(data []byte) interface{} { return &packet.Packet{Raw: data} }
func (m *mockLink) Resend(p interface{}) error { return nil }
func (m *mockLink) SetPacketTimeout(p interface{}, cb func(interface{}), t time.Duration) {}
func (m *mockLink) SetPacketDelivered(p interface{}, cb func(interface{})) {}
func (m *mockLink) HandleInbound(pkt *packet.Packet) error { return nil }
func (m *mockLink) ValidateLinkProof(pkt *packet.Packet, networkIface common.NetworkInterface) error {
return nil
}
func TestNewRawChannelReader(t *testing.T) {
link := &mockLink{status: transport.STATUS_ACTIVE}
ch := channel.NewChannel(link)
reader := NewRawChannelReader(123, ch)
if reader.streamID != 123 {
t.Errorf("streamID = %d, want %d", reader.streamID, 123)
}
if reader.channel != ch {
t.Error("channel not set correctly")
}
if reader.buffer == nil {
t.Error("buffer is nil")
}
if reader.callbacks == nil {
t.Error("callbacks is nil")
}
}
func TestRawChannelReader_RemoveReadyCallback(t *testing.T) {
reader := &RawChannelReader{
streamID: 1,
buffer: bytes.NewBuffer(nil),
callbacks: make(map[int]func(int)),
}
cb1 := func(int) {}
cb2 := func(int) {}
id1 := reader.AddReadyCallback(cb1)
reader.AddReadyCallback(cb2)
if len(reader.callbacks) != 2 {
t.Errorf("callbacks length = %d, want 2", len(reader.callbacks))
}
reader.RemoveReadyCallback(id1)
if len(reader.callbacks) != 1 {
t.Errorf("RemoveReadyCallback did not remove callback, length = %d", len(reader.callbacks))
}
}
func TestRawChannelReader_Read(t *testing.T) {
reader := &RawChannelReader{
streamID: 1,
buffer: bytes.NewBuffer([]byte("test data")),
eof: false,
}
data := make([]byte, 10)
n, err := reader.Read(data)
if err != nil {
t.Errorf("Read() error = %v", err)
}
if n == 0 {
t.Error("Read() returned 0 bytes")
}
reader.eof = true
reader.buffer = bytes.NewBuffer(nil)
n, err = reader.Read(data)
if err != io.EOF {
t.Errorf("Read() error = %v, want io.EOF", err)
}
if n != 0 {
t.Errorf("Read() = %d bytes, want 0", n)
}
}
func TestRawChannelReader_HandleMessage(t *testing.T) {
reader := &RawChannelReader{
streamID: 1,
buffer: bytes.NewBuffer(nil),
callbacks: make(map[int]func(int)),
}
msg := &StreamDataMessage{
StreamID: 1,
Data: []byte("test"),
EOF: false,
Compressed: false,
}
called := false
reader.AddReadyCallback(func(int) {
called = true
})
result := reader.HandleMessage(msg)
if !result {
t.Error("HandleMessage() = false, want true")
}
if !called {
t.Error("callback was not called")
}
if reader.buffer.Len() == 0 {
t.Error("buffer is empty after HandleMessage")
}
msg.StreamID = 2
result = reader.HandleMessage(msg)
if result {
t.Error("HandleMessage() = true, want false for different streamID")
}
msg.StreamID = 1
msg.EOF = true
reader.HandleMessage(msg)
if !reader.eof {
t.Error("EOF not set after HandleMessage with EOF flag")
}
}
func TestNewRawChannelWriter(t *testing.T) {
link := &mockLink{status: transport.STATUS_ACTIVE}
ch := channel.NewChannel(link)
writer := NewRawChannelWriter(456, ch)
if writer.streamID != 456 {
t.Errorf("streamID = %d, want %d", writer.streamID, 456)
}
if writer.channel != ch {
t.Error("channel not set correctly")
}
if writer.eof {
t.Error("eof should be false initially")
}
}
func TestRawChannelWriter_Write(t *testing.T) {
link := &mockLink{status: transport.STATUS_ACTIVE}
ch := channel.NewChannel(link)
writer := NewRawChannelWriter(1, ch)
data := []byte("test data")
n, err := writer.Write(data)
if err != nil {
t.Errorf("Write() error = %v", err)
}
if n != len(data) {
t.Errorf("Write() = %d bytes, want %d", n, len(data))
}
largeData := make([]byte, MaxChunkLen+100)
n, err = writer.Write(largeData)
if err != nil {
t.Errorf("Write() error = %v", err)
}
if n != MaxChunkLen {
t.Errorf("Write() = %d bytes, want %d", n, MaxChunkLen)
}
}
func TestRawChannelWriter_Close(t *testing.T) {
link := &mockLink{status: transport.STATUS_ACTIVE}
ch := channel.NewChannel(link)
writer := NewRawChannelWriter(1, ch)
if writer.eof {
t.Error("EOF should be false before Close()")
}
err := writer.Close()
if err != nil {
t.Errorf("Close() error = %v", err)
}
if !writer.eof {
t.Error("EOF should be true after Close()")
}
}
func TestCreateReader(t *testing.T) {
link := &mockLink{status: transport.STATUS_ACTIVE}
ch := channel.NewChannel(link)
callback := func(int) {}
reader := CreateReader(789, ch, callback)
if reader == nil {
t.Error("CreateReader() returned nil")
}
}
func TestCreateWriter(t *testing.T) {
link := &mockLink{status: transport.STATUS_ACTIVE}
ch := channel.NewChannel(link)
writer := CreateWriter(101, ch)
if writer == nil {
t.Error("CreateWriter() returned nil")
}
}
func TestCreateBidirectionalBuffer(t *testing.T) {
link := &mockLink{status: transport.STATUS_ACTIVE}
ch := channel.NewChannel(link)
callback := func(int) {}
buf := CreateBidirectionalBuffer(1, 2, ch, callback)
if buf == nil {
t.Error("CreateBidirectionalBuffer() returned nil")
}
}
func TestCompressData(t *testing.T) {
data := []byte("test data for compression")
compressed := compressData(data)
if compressed == nil {
t.Skip("compressData() returned nil (compression implementation may be incomplete)")
}
}
func TestDecompressData(t *testing.T) {
data := []byte("test data")
compressed := compressData(data)
if compressed == nil {
t.Skip("compression not working, skipping decompression test")
}
decompressed := decompressData(compressed)
if decompressed == nil {
t.Error("decompressData() returned nil")
}
}

View File

@@ -1,16 +1,13 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package channel package channel
import ( import (
"errors" "errors"
"log"
"math" "math"
"sync" "sync"
"time" "time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common" "github.com/Sudo-Ivan/reticulum-go/pkg/transport"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
"git.quad4.io/Networks/Reticulum-Go/pkg/transport"
) )
const ( const (
@@ -36,19 +33,6 @@ const (
SeqModulus uint16 = SeqMax SeqModulus uint16 = SeqMax
FastRateThreshold = 10 FastRateThreshold = 10
// Timeout calculation constants
RTTMinThreshold = 0.025
TimeoutBaseMultiplier = 1.5
TimeoutRingMultiplier = 2.5
TimeoutRingOffset = 2
// Packet header constants
ChannelHeaderSize = 6
ChannelHeaderBits = 8
// Default retry count
DefaultMaxTries = 3
) )
// MessageState represents the state of a message // MessageState represents the state of a message
@@ -83,13 +67,7 @@ type Channel struct {
maxTries int maxTries int
fastRateRounds int fastRateRounds int
medRateRounds int medRateRounds int
messageHandlers []messageHandlerEntry messageHandlers []func(MessageBase) bool
nextHandlerID int
}
type messageHandlerEntry struct {
id int
handler func(MessageBase) bool
} }
// Envelope wraps a message with metadata for transmission // Envelope wraps a message with metadata for transmission
@@ -106,12 +84,12 @@ type Envelope struct {
func NewChannel(link transport.LinkInterface) *Channel { func NewChannel(link transport.LinkInterface) *Channel {
return &Channel{ return &Channel{
link: link, link: link,
messageHandlers: make([]messageHandlerEntry, 0), messageHandlers: make([]func(MessageBase) bool, 0),
mutex: sync.RWMutex{}, mutex: sync.RWMutex{},
windowMax: WindowMaxSlow, windowMax: WindowMaxSlow,
windowMin: WindowMinSlow, windowMin: WindowMinSlow,
window: WindowInitial, window: WindowInitial,
maxTries: DefaultMaxTries, maxTries: 3,
} }
} }
@@ -128,7 +106,7 @@ func (c *Channel) Send(msg MessageBase) error {
} }
c.mutex.Lock() c.mutex.Lock()
c.nextSequence = (c.nextSequence + common.ONE) % SeqModulus c.nextSequence = (c.nextSequence + 1) % SeqModulus
c.txRing = append(c.txRing, env) c.txRing = append(c.txRing, env)
c.mutex.Unlock() c.mutex.Unlock()
@@ -163,7 +141,7 @@ func (c *Channel) handleTimeout(packet interface{}) {
env.Tries++ env.Tries++
if err := c.link.Resend(packet); err != nil { // #nosec G104 if err := c.link.Resend(packet); err != nil { // #nosec G104
// Handle resend error, e.g., log it or mark envelope as failed // Handle resend error, e.g., log it or mark envelope as failed
debug.Log(debug.DEBUG_INFO, "Failed to resend packet", "error", err) log.Printf("Failed to resend packet: %v", err)
// Optionally, mark the envelope as failed or remove it from txRing // Optionally, mark the envelope as failed or remove it from txRing
// env.State = MsgStateFailed // env.State = MsgStateFailed
// c.txRing = append(c.txRing[:i], c.txRing[i+1:]...) // c.txRing = append(c.txRing[:i], c.txRing[i+1:]...)
@@ -191,28 +169,25 @@ func (c *Channel) handleDelivered(packet interface{}) {
func (c *Channel) getPacketTimeout(tries int) time.Duration { func (c *Channel) getPacketTimeout(tries int) time.Duration {
rtt := c.link.GetRTT() rtt := c.link.GetRTT()
if rtt < RTTMinThreshold { if rtt < 0.025 {
rtt = RTTMinThreshold rtt = 0.025
} }
timeout := math.Pow(TimeoutBaseMultiplier, float64(tries-common.ONE)) * rtt * TimeoutRingMultiplier * float64(len(c.txRing)+TimeoutRingOffset) timeout := math.Pow(1.5, float64(tries-1)) * rtt * 2.5 * float64(len(c.txRing)+2)
return time.Duration(timeout * float64(time.Second)) return time.Duration(timeout * float64(time.Second))
} }
func (c *Channel) AddMessageHandler(handler func(MessageBase) bool) int { func (c *Channel) AddMessageHandler(handler func(MessageBase) bool) {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
id := c.nextHandlerID c.messageHandlers = append(c.messageHandlers, handler)
c.nextHandlerID++
c.messageHandlers = append(c.messageHandlers, messageHandlerEntry{id: id, handler: handler})
return id
} }
func (c *Channel) RemoveMessageHandler(id int) { func (c *Channel) RemoveMessageHandler(handler func(MessageBase) bool) {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
for i, entry := range c.messageHandlers { for i, h := range c.messageHandlers {
if entry.id == id { if &h == &handler {
c.messageHandlers = append(c.messageHandlers[:i], c.messageHandlers[i+1:]...) c.messageHandlers = append(c.messageHandlers[:i], c.messageHandlers[i+1:]...)
break break
} }
@@ -223,10 +198,10 @@ func (c *Channel) updateRateThresholds() {
rtt := c.link.RTT() rtt := c.link.RTT()
if rtt > RTTFast { if rtt > RTTFast {
c.fastRateRounds = common.ZERO c.fastRateRounds = 0
if rtt > RTTMedium { if rtt > RTTMedium {
c.medRateRounds = common.ZERO c.medRateRounds = 0
} else { } else {
c.medRateRounds++ c.medRateRounds++
if c.windowMax < WindowMaxMedium && c.medRateRounds == FastRateThreshold { if c.windowMax < WindowMaxMedium && c.medRateRounds == FastRateThreshold {
@@ -243,59 +218,6 @@ func (c *Channel) updateRateThresholds() {
} }
} }
func (c *Channel) HandleInbound(data []byte) error {
if len(data) < ChannelHeaderSize {
return errors.New("channel packet too short")
}
msgType := uint16(data[0])<<ChannelHeaderBits | uint16(data[1])
sequence := uint16(data[2])<<ChannelHeaderBits | uint16(data[3])
length := uint16(data[4])<<ChannelHeaderBits | uint16(data[5])
if len(data) < ChannelHeaderSize+int(length) {
return errors.New("channel packet incomplete")
}
msgData := data[ChannelHeaderSize : ChannelHeaderSize+length]
c.mutex.Lock()
defer c.mutex.Unlock()
for _, entry := range c.messageHandlers {
if entry.handler != nil {
msg := &GenericMessage{
Type: msgType,
Data: msgData,
Seq: sequence,
}
if entry.handler(msg) {
break
}
}
}
return nil
}
type GenericMessage struct {
Type uint16
Data []byte
Seq uint16
}
func (g *GenericMessage) Pack() ([]byte, error) {
return g.Data, nil
}
func (g *GenericMessage) Unpack(data []byte) error {
g.Data = data
return nil
}
func (g *GenericMessage) GetType() uint16 {
return g.Type
}
func (c *Channel) Close() error { func (c *Channel) Close() error {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()

View File

@@ -1,130 +0,0 @@
package channel
import (
"bytes"
"testing"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
)
type mockLink struct {
status byte
rtt float64
sent [][]byte
timeouts map[interface{}]func(interface{})
delivered map[interface{}]func(interface{})
}
func (m *mockLink) GetStatus() byte { return m.status }
func (m *mockLink) GetRTT() float64 { return m.rtt }
func (m *mockLink) RTT() float64 { return m.rtt }
func (m *mockLink) GetLinkID() []byte { return []byte("testlink") }
func (m *mockLink) Send(data []byte) interface{} {
m.sent = append(m.sent, data)
p := &packet.Packet{Raw: data}
return p
}
func (m *mockLink) Resend(p interface{}) error { return nil }
func (m *mockLink) SetPacketTimeout(p interface{}, cb func(interface{}), t time.Duration) {
if m.timeouts == nil {
m.timeouts = make(map[interface{}]func(interface{}))
}
m.timeouts[p] = cb
}
func (m *mockLink) SetPacketDelivered(p interface{}, cb func(interface{})) {
if m.delivered == nil {
m.delivered = make(map[interface{}]func(interface{}))
}
m.delivered[p] = cb
}
func (m *mockLink) HandleInbound(pkt *packet.Packet) error { return nil }
func (m *mockLink) ValidateLinkProof(pkt *packet.Packet, networkIface common.NetworkInterface) error {
return nil
}
type testMessage struct {
data []byte
}
func (m *testMessage) Pack() ([]byte, error) { return m.data, nil }
func (m *testMessage) Unpack(data []byte) error { m.data = data; return nil }
func (m *testMessage) GetType() uint16 { return 1 }
func TestNewChannel(t *testing.T) {
link := &mockLink{}
c := NewChannel(link)
if c == nil {
t.Fatal("NewChannel returned nil")
}
}
func TestChannelSend(t *testing.T) {
link := &mockLink{status: 1} // STATUS_ACTIVE
c := NewChannel(link)
msg := &testMessage{data: []byte("test")}
err := c.Send(msg)
if err != nil {
t.Fatalf("Send failed: %v", err)
}
if len(link.sent) != 1 {
t.Errorf("Expected 1 packet sent, got %d", len(link.sent))
}
}
func TestHandleInbound(t *testing.T) {
link := &mockLink{}
c := NewChannel(link)
received := false
c.AddMessageHandler(func(m MessageBase) bool {
received = true
return true
})
// Packet format: [type 2][seq 2][len 2][data]
data := []byte{0, 1, 0, 1, 0, 4, 't', 'e', 's', 't'}
err := c.HandleInbound(data)
if err != nil {
t.Fatalf("HandleInbound failed: %v", err)
}
if !received {
t.Error("Message handler was not called")
}
}
func TestMessageHandlers(t *testing.T) {
c := &Channel{
messageHandlers: make([]messageHandlerEntry, 0),
}
h := func(m MessageBase) bool { return true }
id := c.AddMessageHandler(h)
if len(c.messageHandlers) != 1 {
t.Errorf("Expected 1 handler, got %d", len(c.messageHandlers))
}
c.RemoveMessageHandler(id)
if len(c.messageHandlers) != 0 {
t.Errorf("Expected 0 handlers, got %d", len(c.messageHandlers))
}
}
func TestGenericMessage(t *testing.T) {
msg := &GenericMessage{Type: 1, Data: []byte("test")}
if msg.GetType() != 1 {
t.Error("Wrong type")
}
p, _ := msg.Pack()
if !bytes.Equal(p, []byte("test")) {
t.Error("Pack failed")
}
msg.Unpack([]byte("new"))
if !bytes.Equal(msg.Data, []byte("new")) {
t.Error("Unpack failed")
}
}

View File

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package common package common
import ( import (
@@ -40,7 +38,6 @@ 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

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package common package common
const ( const (
@@ -60,87 +58,4 @@ const (
STALE_TIME = 720 STALE_TIME = 720
PATH_REQUEST_TTL = 300 PATH_REQUEST_TTL = 300
ANNOUNCE_TIMEOUT = 15 ANNOUNCE_TIMEOUT = 15
// Common Numeric Constants
ZERO = 0
ONE = 1
TWO = 2
THREE = 3
FOUR = 4
FIVE = 5
SIX = 6
SEVEN = 7
EIGHT = 8
FIFTEEN = 15
// Common Size Constants
SIZE_16 = 16
SIZE_32 = 32
SIZE_48 = 48
SIZE_64 = 64
SIXTY_SEVEN = 67
TOKEN_OVERHEAD = 48
// Common Hex Constants
HEX_0x00 = 0x00
HEX_0x01 = 0x01
HEX_0x02 = 0x02
HEX_0x03 = 0x03
HEX_0x04 = 0x04
HEX_0x92 = 0x92
HEX_0x93 = 0x93
HEX_0xC2 = 0xC2
HEX_0xC3 = 0xC3
HEX_0xC4 = 0xC4
HEX_0xD1 = 0xD1
HEX_0xD2 = 0xD2
HEX_0xFE = 0xFE
HEX_0xFF = 0xFF
// Common Numeric Constants
NUM_11 = 11
NUM_100 = 100
NUM_500 = 500
NUM_1024 = 1024
NUM_1064 = 1064
NUM_4242 = 4242
NUM_0700 = 0700
// Common Float Constants
FLOAT_ZERO = 0.0
FLOAT_0_001 = 0.001
FLOAT_0_025 = 0.025
FLOAT_0_1 = 0.1
FLOAT_1_0 = 1.0
FLOAT_1_75 = 1.75
FLOAT_5_0 = 5.0
FLOAT_1E9 = 1e9
// Common String Constants
STR_LINK_ID = "link_id"
STR_BYTES = "bytes"
STR_FMT_HEX = "0x%02x"
STR_FMT_HEX_LOW = "%x"
STR_FMT_DEC = "%d"
STR_TEST = "test"
STR_LINK = "link"
STR_ERROR = "error"
STR_HASH = "hash"
STR_NAME = "name"
STR_TYPE = "type"
STR_STORAGE = "storage"
STR_PATH = "path"
STR_COUNT = "count"
STR_HOME = "HOME"
STR_PUBLIC_KEY = "public_key"
STR_TCP_CLIENT = "TCPClientInterface"
STR_UDP = "udp"
STR_UDP6 = "udp6"
STR_TCP = "tcp"
STR_ETH0 = "eth0"
STR_INTERFACE = "interface"
STR_PEER = "peer"
STR_ADDR = "addr"
STR_LINK_NOT_ACTIVE = "link not active"
STR_INTERFACE_OFFLINE = "interface offline or detached"
) )

View File

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package common package common
import ( import (
@@ -39,10 +37,6 @@ 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
@@ -62,8 +56,6 @@ 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
@@ -131,30 +123,6 @@ 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()
@@ -190,20 +158,10 @@ 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)
} }
@@ -223,10 +181,12 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
packet = append(packet, 0x02) // Link packet type packet = append(packet, 0x02) // Link packet type
packet = append(packet, dest...) packet = append(packet, dest...)
// Add timestamp
ts := make([]byte, 8) ts := make([]byte, 8)
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) // #nosec G115 binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) // #nosec G115
packet = append(packet, ts...) packet = append(packet, ts...)
// Add data
packet = append(packet, data...) packet = append(packet, data...)
return i.Send(packet, "") return i.Send(packet, "")

View File

@@ -1,288 +0,0 @@
package common
import (
"testing"
"time"
)
func TestNewBaseInterface(t *testing.T) {
iface := NewBaseInterface("test0", IF_TYPE_UDP, true)
if iface.Name != "test0" {
t.Errorf("Name = %q, want %q", iface.Name, "test0")
}
if iface.Type != IF_TYPE_UDP {
t.Errorf("Type = %v, want %v", iface.Type, IF_TYPE_UDP)
}
if iface.Mode != IF_MODE_FULL {
t.Errorf("Mode = %v, want %v", iface.Mode, IF_MODE_FULL)
}
if !iface.Enabled {
t.Errorf("Enabled = %v, want true", iface.Enabled)
}
if iface.MTU != DEFAULT_MTU {
t.Errorf("MTU = %d, want %d", iface.MTU, DEFAULT_MTU)
}
if iface.Bitrate != BITRATE_MINIMUM {
t.Errorf("Bitrate = %d, want %d", iface.Bitrate, BITRATE_MINIMUM)
}
}
func TestBaseInterface_GetType(t *testing.T) {
iface := NewBaseInterface("test1", IF_TYPE_TCP, true)
if iface.GetType() != IF_TYPE_TCP {
t.Errorf("GetType() = %v, want %v", iface.GetType(), IF_TYPE_TCP)
}
}
func TestBaseInterface_GetMode(t *testing.T) {
iface := NewBaseInterface("test2", IF_TYPE_UDP, true)
if iface.GetMode() != IF_MODE_FULL {
t.Errorf("GetMode() = %v, want %v", iface.GetMode(), IF_MODE_FULL)
}
}
func TestBaseInterface_GetMTU(t *testing.T) {
iface := NewBaseInterface("test3", IF_TYPE_UDP, true)
if iface.GetMTU() != DEFAULT_MTU {
t.Errorf("GetMTU() = %d, want %d", iface.GetMTU(), DEFAULT_MTU)
}
}
func TestBaseInterface_GetName(t *testing.T) {
iface := NewBaseInterface("test4", IF_TYPE_UDP, true)
if iface.GetName() != "test4" {
t.Errorf("GetName() = %q, want %q", iface.GetName(), "test4")
}
}
func TestBaseInterface_IsEnabled(t *testing.T) {
iface := NewBaseInterface("test5", IF_TYPE_UDP, true)
iface.Online = true
iface.Detached = false
if !iface.IsEnabled() {
t.Error("IsEnabled() = false, want true")
}
iface.Enabled = false
if iface.IsEnabled() {
t.Error("IsEnabled() = true, want false when disabled")
}
iface.Enabled = true
iface.Online = false
if iface.IsEnabled() {
t.Error("IsEnabled() = true, want false when offline")
}
iface.Online = true
iface.Detached = true
if iface.IsEnabled() {
t.Error("IsEnabled() = true, want false when detached")
}
}
func TestBaseInterface_IsOnline(t *testing.T) {
iface := NewBaseInterface("test6", IF_TYPE_UDP, true)
iface.Online = true
if !iface.IsOnline() {
t.Error("IsOnline() = false, want true")
}
iface.Online = false
if iface.IsOnline() {
t.Error("IsOnline() = true, want false")
}
}
func TestBaseInterface_IsDetached(t *testing.T) {
iface := NewBaseInterface("test7", IF_TYPE_UDP, true)
iface.Detached = true
if !iface.IsDetached() {
t.Error("IsDetached() = false, want true")
}
iface.Detached = false
if iface.IsDetached() {
t.Error("IsDetached() = true, want false")
}
}
func TestBaseInterface_SetPacketCallback(t *testing.T) {
iface := NewBaseInterface("test8", IF_TYPE_UDP, true)
callback := func(data []byte, ni NetworkInterface) {}
iface.SetPacketCallback(callback)
if iface.GetPacketCallback() == nil {
t.Error("GetPacketCallback() = nil, want callback")
}
}
func TestBaseInterface_GetPacketCallback(t *testing.T) {
iface := NewBaseInterface("test9", IF_TYPE_UDP, true)
if iface.GetPacketCallback() != nil {
t.Error("GetPacketCallback() != nil, want nil")
}
callback := func(data []byte, ni NetworkInterface) {}
iface.SetPacketCallback(callback)
if iface.GetPacketCallback() == nil {
t.Error("GetPacketCallback() = nil, want callback")
}
}
func TestBaseInterface_Detach(t *testing.T) {
iface := NewBaseInterface("test10", IF_TYPE_UDP, true)
iface.Online = true
iface.Detached = false
iface.Detach()
if !iface.IsDetached() {
t.Error("IsDetached() = false, want true after Detach()")
}
if iface.IsOnline() {
t.Error("IsOnline() = true, want false after Detach()")
}
}
func TestBaseInterface_Enable(t *testing.T) {
iface := NewBaseInterface("test11", IF_TYPE_UDP, false)
iface.Online = false
iface.Enable()
if !iface.Enabled {
t.Error("Enabled = false, want true after Enable()")
}
if !iface.IsOnline() {
t.Error("IsOnline() = false, want true after Enable()")
}
}
func TestBaseInterface_Disable(t *testing.T) {
iface := NewBaseInterface("test12", IF_TYPE_UDP, true)
iface.Online = true
iface.Disable()
if iface.Enabled {
t.Error("Enabled = true, want false after Disable()")
}
if iface.IsOnline() {
t.Error("IsOnline() = true, want false after Disable()")
}
}
func TestBaseInterface_Start(t *testing.T) {
iface := NewBaseInterface("test13", IF_TYPE_UDP, true)
if err := iface.Start(); err != nil {
t.Errorf("Start() error = %v, want nil", err)
}
}
func TestBaseInterface_Stop(t *testing.T) {
iface := NewBaseInterface("test14", IF_TYPE_UDP, true)
if err := iface.Stop(); err != nil {
t.Errorf("Stop() error = %v, want nil", err)
}
}
func TestBaseInterface_GetConn(t *testing.T) {
iface := NewBaseInterface("test15", IF_TYPE_UDP, true)
if iface.GetConn() != nil {
t.Error("GetConn() != nil, want nil")
}
}
func TestBaseInterface_Send(t *testing.T) {
iface := NewBaseInterface("test16", IF_TYPE_UDP, true)
data := []byte("test data")
if err := iface.Send(data, ""); err != nil {
t.Errorf("Send() error = %v, want nil", err)
}
}
func TestBaseInterface_ProcessIncoming(t *testing.T) {
iface := NewBaseInterface("test17", IF_TYPE_UDP, true)
called := false
callback := func(data []byte, ni NetworkInterface) {
called = true
}
iface.SetPacketCallback(callback)
data := []byte("test")
iface.ProcessIncoming(data)
if !called {
t.Error("ProcessIncoming() did not call callback")
}
iface.SetPacketCallback(nil)
iface.ProcessIncoming(data)
}
func TestBaseInterface_ProcessOutgoing(t *testing.T) {
iface := NewBaseInterface("test18", IF_TYPE_UDP, true)
data := []byte("test data")
if err := iface.ProcessOutgoing(data); err != nil {
t.Errorf("ProcessOutgoing() error = %v, want nil", err)
}
}
func TestBaseInterface_SendPathRequest(t *testing.T) {
iface := NewBaseInterface("test19", IF_TYPE_UDP, true)
data := []byte("path request")
if err := iface.SendPathRequest(data); err != nil {
t.Errorf("SendPathRequest() error = %v, want nil", err)
}
}
func TestBaseInterface_SendLinkPacket(t *testing.T) {
iface := NewBaseInterface("test20", IF_TYPE_UDP, true)
dest := []byte("destination")
data := []byte("link data")
timestamp := time.Now()
if err := iface.SendLinkPacket(dest, data, timestamp); err != nil {
t.Errorf("SendLinkPacket() error = %v, want nil", err)
}
}
func TestBaseInterface_GetBandwidthAvailable(t *testing.T) {
iface := NewBaseInterface("test21", IF_TYPE_UDP, true)
if !iface.GetBandwidthAvailable() {
t.Error("GetBandwidthAvailable() = false, want true when no recent transmission")
}
iface.lastTx = time.Now()
iface.TxBytes = 0
if !iface.GetBandwidthAvailable() {
t.Error("GetBandwidthAvailable() = false, want true when TxBytes is 0")
}
iface.lastTx = time.Now().Add(-500 * time.Millisecond)
iface.TxBytes = 1000
iface.Bitrate = 1000000
if !iface.GetBandwidthAvailable() {
t.Error("GetBandwidthAvailable() = false, want true when usage is below threshold")
}
iface.TxBytes = 10000000
iface.Bitrate = 1000
if iface.GetBandwidthAvailable() {
t.Error("GetBandwidthAvailable() = true, want false when usage exceeds threshold")
}
}

View File

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package common package common
import ( import (

View File

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package config package config
import ( import (
@@ -41,7 +39,6 @@ type Config struct {
} }
func LoadConfig(path string) (*Config, error) { func LoadConfig(path string) (*Config, error) {
// bearer:disable go_gosec_filesystem_filereadtaint
file, err := os.Open(path) // #nosec G304 file, err := os.Open(path) // #nosec G304
if err != nil { if err != nil {
return nil, err return nil, err
@@ -225,6 +222,7 @@ func InitConfig() (*Config, error) {
cfg.Logging.Level = "info" cfg.Logging.Level = "info"
cfg.Logging.File = filepath.Join(GetConfigDir(), "reticulum.log") cfg.Logging.File = filepath.Join(GetConfigDir(), "reticulum.log")
// Add default interfaces
cfg.Interfaces = append(cfg.Interfaces, struct { cfg.Interfaces = append(cfg.Interfaces, struct {
Name string Name string
Type string Type string

View File

@@ -1,192 +0,0 @@
package config
import (
"os"
"path/filepath"
"testing"
)
func TestLoadConfig(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "test_config")
configContent := `[identity]
name = test-identity
storage_path = /tmp/test-storage
[transport]
announce_interval = 300
path_request_timeout = 15
max_hops = 8
bitrate_limit = 1000000
[logging]
level = info
file = /tmp/test.log
[interface test-interface]
type = UDPInterface
enabled = true
listen_ip = 127.0.0.1
listen_port = 37696
`
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
if cfg == nil {
t.Fatal("LoadConfig() returned nil")
}
if len(cfg.Interfaces) == 0 {
t.Error("No interfaces loaded")
}
iface := cfg.Interfaces[0]
if iface.Type != "UDPInterface" {
t.Errorf("Interface type = %s, want UDPInterface", iface.Type)
}
if !iface.Enabled {
t.Error("Interface should be enabled")
}
if iface.ListenIP != "127.0.0.1" {
t.Errorf("Interface ListenIP = %s, want 127.0.0.1", iface.ListenIP)
}
if iface.ListenPort != 37696 {
t.Errorf("Interface ListenPort = %d, want 37696", iface.ListenPort)
}
}
func TestLoadConfig_NonexistentFile(t *testing.T) {
_, err := LoadConfig("/nonexistent/path/config")
if err == nil {
t.Error("LoadConfig() should return error for nonexistent file")
}
}
func TestLoadConfig_EmptyFile(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "empty_config")
if err := os.WriteFile(configPath, []byte(""), 0600); err != nil {
t.Fatalf("Failed to write empty config: %v", err)
}
cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
if cfg == nil {
t.Fatal("LoadConfig() returned nil")
}
}
func TestLoadConfig_CommentsAndEmptyLines(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "test_config")
configContent := `# Comment line
[identity]
name = test
# Another comment
[interface test-interface]
# Interface comment
type = UDPInterface
enabled = true
`
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
if cfg == nil {
t.Fatal("LoadConfig() returned nil")
}
if cfg.Identity.Name != "test" {
t.Errorf("Identity.Name = %s, want test", cfg.Identity.Name)
}
}
func TestSaveConfig(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "test_config")
cfg := &Config{}
cfg.Identity.Name = "test-identity"
cfg.Identity.StoragePath = "/tmp/test"
cfg.Transport.AnnounceInterval = 600
cfg.Logging.Level = "debug"
cfg.Logging.File = "/tmp/test.log"
if err := SaveConfig(cfg, configPath); err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
loaded, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
if loaded.Identity.Name != "test-identity" {
t.Errorf("Identity.Name = %s, want test-identity", loaded.Identity.Name)
}
if loaded.Transport.AnnounceInterval != 600 {
t.Errorf("Transport.AnnounceInterval = %d, want 600", loaded.Transport.AnnounceInterval)
}
}
func TestGetConfigDir(t *testing.T) {
dir := GetConfigDir()
if dir == "" {
t.Error("GetConfigDir() returned empty string")
}
}
func TestGetDefaultConfigPath(t *testing.T) {
path := GetDefaultConfigPath()
if path == "" {
t.Error("GetDefaultConfigPath() returned empty string")
}
}
func TestEnsureConfigDir(t *testing.T) {
if err := EnsureConfigDir(); err != nil {
t.Fatalf("EnsureConfigDir() error = %v", err)
}
}
func TestInitConfig(t *testing.T) {
tmpDir := t.TempDir()
originalHome := os.Getenv("HOME")
defer func() {
if originalHome != "" {
os.Setenv("HOME", originalHome)
}
}()
os.Setenv("HOME", tmpDir)
cfg, err := InitConfig()
if err != nil {
t.Fatalf("InitConfig() error = %v", err)
}
if cfg == nil {
t.Fatal("InitConfig() returned nil")
}
}

View File

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package cryptography package cryptography
import ( import (

View File

@@ -86,6 +86,7 @@ func TestAES256CBC_InvalidKeySize(t *testing.T) {
} }
} }
func TestDecryptAES256CBCErrorCases(t *testing.T) { func TestDecryptAES256CBCErrorCases(t *testing.T) {
key, err := GenerateAES256Key() key, err := GenerateAES256Key()
if err != nil { if err != nil {
@@ -118,16 +119,10 @@ func TestDecryptAES256CBCErrorCases(t *testing.T) {
t.Fatalf("Failed to create test ciphertext: %v", err) t.Fatalf("Failed to create test ciphertext: %v", err)
} }
// Corrupt the byte that XORs with the last padding byte. // Corrupt the last byte (which affects padding)
// In CBC, P[i] = D(C[i]) ^ C[i-1].
// The last byte of plaintext P[len-1] depends on C[len-1] and C[len-1-BlockSize].
// If we modify C[len-1-BlockSize], we flip the bits of P[len-1] predictably.
// If we modify C[len-1] (the last byte of ciphertext), we scramble the whole block D(C[len-1]),
// which might accidentally result in valid padding (e.g. 0x01).
// So we corrupt the IV (or previous block) corresponding to the last byte.
corruptedCiphertext := make([]byte, len(ciphertext)) corruptedCiphertext := make([]byte, len(ciphertext))
copy(corruptedCiphertext, ciphertext) copy(corruptedCiphertext, ciphertext)
corruptedCiphertext[len(ciphertext)-aes.BlockSize-1] ^= 0xFF corruptedCiphertext[len(corruptedCiphertext)-1] ^= 0xFF
_, err = DecryptAES256CBC(key, corruptedCiphertext) _, err = DecryptAES256CBC(key, corruptedCiphertext)
if err == nil { if err == nil {

View File

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package cryptography package cryptography
import ( import (

View File

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package cryptography package cryptography
import ( import (

View File

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package cryptography package cryptography
import ( import (

View File

@@ -1,50 +1,17 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package cryptography package cryptography
import ( import (
"crypto/hmac"
"crypto/sha256" "crypto/sha256"
"errors" "io"
"math"
"golang.org/x/crypto/hkdf"
) )
func DeriveKey(secret, salt, info []byte, length int) ([]byte, error) { func DeriveKey(secret, salt, info []byte, length int) ([]byte, error) {
hashLen := 32 hkdfReader := hkdf.New(sha256.New, secret, salt, info)
key := make([]byte, length)
if length < 1 { if _, err := io.ReadFull(hkdfReader, key); err != nil {
return nil, errors.New("invalid output key length") return nil, err
} }
return key, nil
if len(secret) == 0 {
return nil, errors.New("cannot derive key from empty input material")
}
if len(salt) == 0 {
salt = make([]byte, hashLen)
}
if info == nil {
info = []byte{}
}
pseudorandomKey := hmac.New(sha256.New, salt)
pseudorandomKey.Write(secret)
prk := pseudorandomKey.Sum(nil)
block := []byte{}
derived := []byte{}
iterations := int(math.Ceil(float64(length) / float64(hashLen)))
for i := 0; i < iterations; i++ {
h := hmac.New(sha256.New, prk)
h.Write(block)
h.Write(info)
counter := byte((i + 1) % (0xFF + 1))
h.Write([]byte{counter})
block = h.Sum(nil)
derived = append(derived, block...)
}
return derived[:length], nil
} }

View File

@@ -77,8 +77,8 @@ func TestDeriveKeyEdgeCases(t *testing.T) {
t.Run("EmptySecret", func(t *testing.T) { t.Run("EmptySecret", func(t *testing.T) {
_, err := DeriveKey([]byte{}, salt, info, 32) _, err := DeriveKey([]byte{}, salt, info, 32)
if err == nil { if err != nil {
t.Errorf("DeriveKey should fail with empty secret") t.Errorf("DeriveKey failed with empty secret: %v", err)
} }
}) })
@@ -97,9 +97,12 @@ func TestDeriveKeyEdgeCases(t *testing.T) {
}) })
t.Run("ZeroLength", func(t *testing.T) { t.Run("ZeroLength", func(t *testing.T) {
_, err := DeriveKey(secret, salt, info, 0) key, err := DeriveKey(secret, salt, info, 0)
if err == nil { if err != nil {
t.Errorf("DeriveKey should fail with zero length") t.Errorf("DeriveKey failed with zero length: %v", err)
}
if len(key) != 0 {
t.Errorf("DeriveKey with zero length returned non-empty key: %x", key)
} }
}) })
} }

View File

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package cryptography package cryptography
import ( import (

View File

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package debug package debug
import ( import (
@@ -115,3 +113,4 @@ func SetDebugLevel(level int) {
func GetDebugLevel() int { func GetDebugLevel() int {
return *debugLevel return *debugLevel
} }

View File

@@ -1,185 +0,0 @@
package debug
import (
"flag"
"testing"
)
func TestInit(t *testing.T) {
originalFlag := flag.CommandLine
defer func() {
flag.CommandLine = originalFlag
initialized = false
}()
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
debugLevel = flag.Int("debug", 3, "debug level")
Init()
if !initialized {
t.Error("Init() should set initialized to true")
}
if GetLogger() == nil {
t.Error("GetLogger() should return non-nil logger after Init()")
}
}
func TestGetLogger(t *testing.T) {
originalFlag := flag.CommandLine
defer func() {
flag.CommandLine = originalFlag
initialized = false
}()
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
debugLevel = flag.Int("debug", 3, "debug level")
initialized = false
logger := GetLogger()
if logger == nil {
t.Error("GetLogger() should return non-nil logger")
}
if !initialized {
t.Error("GetLogger() should initialize if not already initialized")
}
}
func TestLog(t *testing.T) {
originalFlag := flag.CommandLine
defer func() {
flag.CommandLine = originalFlag
initialized = false
}()
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
debugLevel = flag.Int("debug", 7, "debug level")
initialized = false
Log(DEBUG_INFO, "test message", "key", "value")
}
func TestSetDebugLevel(t *testing.T) {
originalFlag := flag.CommandLine
defer func() {
flag.CommandLine = originalFlag
initialized = false
}()
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
debugLevel = flag.Int("debug", 3, "debug level")
initialized = false
SetDebugLevel(5)
if GetDebugLevel() != 5 {
t.Errorf("SetDebugLevel(5) did not set level correctly, got %d", GetDebugLevel())
}
}
func TestGetDebugLevel(t *testing.T) {
originalFlag := flag.CommandLine
defer func() {
flag.CommandLine = originalFlag
initialized = false
}()
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
debugLevel = flag.Int("debug", 4, "debug level")
level := GetDebugLevel()
if level != 4 {
t.Errorf("GetDebugLevel() = %d, want 4", level)
}
}
func TestLog_LevelFiltering(t *testing.T) {
originalFlag := flag.CommandLine
defer func() {
flag.CommandLine = originalFlag
initialized = false
}()
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
debugLevel = flag.Int("debug", 3, "debug level")
initialized = false
Log(DEBUG_TRACE, "trace message")
Log(DEBUG_INFO, "info message")
Log(DEBUG_ERROR, "error message")
}
func TestConstants(t *testing.T) {
if DEBUG_CRITICAL != 1 {
t.Errorf("DEBUG_CRITICAL = %d, want 1", DEBUG_CRITICAL)
}
if DEBUG_ERROR != 2 {
t.Errorf("DEBUG_ERROR = %d, want 2", DEBUG_ERROR)
}
if DEBUG_INFO != 3 {
t.Errorf("DEBUG_INFO = %d, want 3", DEBUG_INFO)
}
if DEBUG_VERBOSE != 4 {
t.Errorf("DEBUG_VERBOSE = %d, want 4", DEBUG_VERBOSE)
}
if DEBUG_TRACE != 5 {
t.Errorf("DEBUG_TRACE = %d, want 5", DEBUG_TRACE)
}
if DEBUG_PACKETS != 6 {
t.Errorf("DEBUG_PACKETS = %d, want 6", DEBUG_PACKETS)
}
if DEBUG_ALL != 7 {
t.Errorf("DEBUG_ALL = %d, want 7", DEBUG_ALL)
}
}
func TestLog_WithArgs(t *testing.T) {
originalFlag := flag.CommandLine
defer func() {
flag.CommandLine = originalFlag
initialized = false
}()
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
debugLevel = flag.Int("debug", 7, "debug level")
initialized = false
Log(DEBUG_INFO, "test message", "key1", "value1", "key2", "value2")
}
func TestInit_MultipleCalls(t *testing.T) {
originalFlag := flag.CommandLine
defer func() {
flag.CommandLine = originalFlag
initialized = false
}()
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
debugLevel = flag.Int("debug", 3, "debug level")
initialized = false
Init()
firstLogger := GetLogger()
Init()
secondLogger := GetLogger()
if firstLogger != secondLogger {
t.Error("Multiple Init() calls should not create new loggers")
}
}
func TestLog_DisabledLevel(t *testing.T) {
originalFlag := flag.CommandLine
defer func() {
flag.CommandLine = originalFlag
initialized = false
}()
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
debugLevel = flag.Int("debug", 1, "debug level")
initialized = false
Log(DEBUG_TRACE, "this should be filtered")
}

View File

@@ -1,24 +1,16 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package destination package destination
import ( import (
"crypto/rand"
"crypto/sha256" "crypto/sha256"
"errors" "errors"
"fmt" "fmt"
"io" "log"
"os"
"sync" "sync"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/announce" "github.com/Sudo-Ivan/reticulum-go/pkg/announce"
"git.quad4.io/Networks/Reticulum-Go/pkg/common" "github.com/Sudo-Ivan/reticulum-go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug" "github.com/Sudo-Ivan/reticulum-go/pkg/identity"
"git.quad4.io/Networks/Reticulum-Go/pkg/identity" "github.com/Sudo-Ivan/reticulum-go/pkg/transport"
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
"github.com/vmihailenco/msgpack/v5"
"golang.org/x/crypto/curve25519"
) )
const ( const (
@@ -44,6 +36,15 @@ const (
RATCHET_COUNT = 512 // Default number of retained ratchet keys RATCHET_COUNT = 512 // Default number of retained ratchet keys
RATCHET_INTERVAL = 1800 // Minimum interval between ratchet rotations in seconds RATCHET_INTERVAL = 1800 // Minimum interval between ratchet rotations in seconds
// Debug levels
DEBUG_CRITICAL = 1 // Critical errors
DEBUG_ERROR = 2 // Non-critical errors
DEBUG_INFO = 3 // Important information
DEBUG_VERBOSE = 4 // Detailed information
DEBUG_TRACE = 5 // Very detailed tracing
DEBUG_PACKETS = 6 // Packet-level details
DEBUG_ALL = 7 // Everything
) )
type PacketCallback = common.PacketCallback type PacketCallback = common.PacketCallback
@@ -55,21 +56,6 @@ type RequestHandler struct {
ResponseGenerator func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity *identity.Identity, requestedAt int64) []byte ResponseGenerator func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity *identity.Identity, requestedAt int64) []byte
AllowMode byte AllowMode byte
AllowedList [][]byte AllowedList [][]byte
AutoCompress bool
}
type Transport interface {
GetConfig() *common.ReticulumConfig
GetInterfaces() map[string]common.NetworkInterface
RegisterDestination(hash []byte, dest interface{})
}
type IncomingLinkHandler func(pkt *packet.Packet, dest *Destination, transport interface{}, networkIface common.NetworkInterface) (interface{}, error)
var incomingLinkHandler IncomingLinkHandler
func RegisterIncomingLinkHandler(handler IncomingLinkHandler) {
incomingLinkHandler = handler
} }
type Destination struct { type Destination struct {
@@ -79,7 +65,7 @@ type Destination struct {
appName string appName string
aspects []string aspects []string
hashValue []byte hashValue []byte
transport Transport transport *transport.Transport
acceptsLinks bool acceptsLinks bool
proofStrategy byte proofStrategy byte
@@ -93,10 +79,6 @@ type Destination struct {
ratchetCount int ratchetCount int
ratchetInterval int ratchetInterval int
enforceRatchets bool enforceRatchets bool
latestRatchetTime time.Time
latestRatchetID []byte
ratchets [][]byte
ratchetFileLock sync.Mutex
defaultAppData []byte defaultAppData []byte
mutex sync.RWMutex mutex sync.RWMutex
@@ -104,12 +86,16 @@ type Destination struct {
requestHandlers map[string]*RequestHandler requestHandlers map[string]*RequestHandler
} }
func New(id *identity.Identity, direction byte, destType byte, appName string, transport Transport, aspects ...string) (*Destination, error) { func debugLog(level int, format string, v ...interface{}) {
debug.Log(debug.DEBUG_INFO, "Creating new destination", "app", appName, "type", destType, "direction", direction) log.Printf("[DEBUG-%d] %s", level, fmt.Sprintf(format, v...))
}
if id == nil && destType != PLAIN { func New(id *identity.Identity, direction byte, destType byte, appName string, transport *transport.Transport, aspects ...string) (*Destination, error) {
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil for non-PLAIN destination") debugLog(DEBUG_INFO, "Creating new destination: app=%s type=%d direction=%d", appName, destType, direction)
return nil, errors.New("identity cannot be nil for non-PLAIN destination")
if id == nil {
debugLog(DEBUG_ERROR, "Cannot create destination: identity is nil")
return nil, errors.New("identity cannot be nil")
} }
d := &Destination{ d := &Destination{
@@ -128,25 +114,19 @@ func New(id *identity.Identity, direction byte, destType byte, appName string, t
// Generate destination hash // Generate destination hash
d.hashValue = d.calculateHash() d.hashValue = d.calculateHash()
debug.Log(debug.DEBUG_VERBOSE, "Created destination with hash", "hash", fmt.Sprintf("%x", d.hashValue)) debugLog(DEBUG_VERBOSE, "Created destination with hash: %x", d.hashValue)
// Auto-register with transport if direction is IN
if (direction & IN) != 0 {
transport.RegisterDestination(d.hashValue, d)
debug.Log(debug.DEBUG_INFO, "Destination auto-registered with transport", "hash", fmt.Sprintf("%x", d.hashValue))
}
return d, nil return d, nil
} }
// FromHash creates a destination from a known hash (e.g., from an announce). // FromHash creates a destination from a known hash (e.g., from an announce).
// This is used by clients to create destination objects for servers they've discovered. // This is used by clients to create destination objects for servers they've discovered.
func FromHash(hash []byte, id *identity.Identity, destType byte, transport Transport) (*Destination, error) { func FromHash(hash []byte, id *identity.Identity, destType byte, transport *transport.Transport) (*Destination, error) {
debug.Log(debug.DEBUG_INFO, "Creating destination from hash", "hash", fmt.Sprintf("%x", hash)) debugLog(DEBUG_INFO, "Creating destination from hash: %x", hash)
if id == nil && destType != PLAIN { if id == nil {
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil for non-PLAIN destination") debugLog(DEBUG_ERROR, "Cannot create destination: identity is nil")
return nil, errors.New("identity cannot be nil for non-PLAIN destination") return nil, errors.New("identity cannot be nil")
} }
d := &Destination{ d := &Destination{
@@ -162,38 +142,32 @@ func FromHash(hash []byte, id *identity.Identity, destType byte, transport Trans
requestHandlers: make(map[string]*RequestHandler), requestHandlers: make(map[string]*RequestHandler),
} }
debug.Log(debug.DEBUG_VERBOSE, "Created destination from hash", "hash", fmt.Sprintf("%x", hash)) debugLog(DEBUG_VERBOSE, "Created destination from hash: %x", hash)
return d, nil return d, nil
} }
func (d *Destination) calculateHash() []byte { func (d *Destination) calculateHash() []byte {
debug.Log(debug.DEBUG_TRACE, "Calculating hash for destination", "name", d.ExpandName()) debugLog(DEBUG_TRACE, "Calculating hash for destination %s", 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 debugLog(DEBUG_ALL, "Identity hash: %x", identityHash)
if d.identity != nil { debugLog(DEBUG_ALL, "Name hash (10 bytes): %x", nameHash10)
// destination_hash = SHA256(name_hash_10bytes + identity_hash_16bytes)[:16]
// Identity hash is the truncated hash of the public key (16 bytes)
identityHash := identity.TruncatedHash(d.identity.GetPublicKey())
debug.Log(debug.DEBUG_ALL, "Identity hash", "hash", fmt.Sprintf("%x", identityHash))
debug.Log(debug.DEBUG_ALL, "Name hash (10 bytes)", "hash", fmt.Sprintf("%x", nameHash10))
// Concatenate name_hash (10 bytes) + identity_hash (16 bytes) = 26 bytes // Concatenate name_hash (10 bytes) + identity_hash (16 bytes) = 26 bytes
combined = append(nameHash10, identityHash...) combined := append(nameHash10, identityHash...)
} else {
// PLAIN destination has no identity hash
combined = nameHash10
debug.Log(debug.DEBUG_ALL, "Name hash (10 bytes)", "hash", fmt.Sprintf("%x", nameHash10))
}
// Then hash again and truncate to 16 bytes // Then hash again and truncate to 16 bytes
finalHashFull := sha256.Sum256(combined) finalHashFull := sha256.Sum256(combined)
finalHash := finalHashFull[:16] finalHash := finalHashFull[:16]
debug.Log(debug.DEBUG_VERBOSE, "Calculated destination hash", "hash", fmt.Sprintf("%x", finalHash)) debugLog(DEBUG_VERBOSE, "Calculated destination hash: %x", finalHash)
return finalHash return finalHash
} }
@@ -206,52 +180,50 @@ func (d *Destination) ExpandName() string {
return name return name
} }
func (d *Destination) Announce(pathResponse bool, tag []byte, attachedInterface common.NetworkInterface) error { func (d *Destination) Announce(appData []byte) error {
d.mutex.Lock() d.mutex.Lock()
defer d.mutex.Unlock() defer d.mutex.Unlock()
debug.Log(debug.DEBUG_VERBOSE, "Announcing destination", "name", d.ExpandName(), "path_response", pathResponse) log.Printf("[DEBUG-4] Announcing destination %s", d.ExpandName())
appData := d.defaultAppData if appData == nil {
appData = d.defaultAppData
}
// Create announce packet using announce package // Create announce packet using announce package
announceObj, err := announce.New(d.identity, d.hashValue, d.ExpandName(), appData, pathResponse, d.transport.GetConfig()) // Pass the destination hash, name, and app data
announce, err := announce.New(d.identity, d.hashValue, d.ExpandName(), appData, false, d.transport.GetConfig())
if err != nil { if err != nil {
return fmt.Errorf("failed to create announce: %w", err) return fmt.Errorf("failed to create announce: %w", err)
} }
packet := announceObj.GetPacket() packet := announce.GetPacket()
if packet == nil { if packet == nil {
return errors.New("failed to create announce packet") return errors.New("failed to create announce packet")
} }
if pathResponse && tag != nil { // Send announce packet to all interfaces
debug.Log(debug.DEBUG_INFO, "Sending path response announce", "tag", fmt.Sprintf("%x", tag)) log.Printf("[DEBUG-4] Sending announce packet to all interfaces")
}
if d.transport == nil { if d.transport == nil {
return errors.New("transport not initialized") return errors.New("transport not initialized")
} }
interfaces := d.transport.GetInterfaces()
log.Printf("[DEBUG-7] Got %d interfaces from transport", len(interfaces))
var lastErr error var lastErr error
if attachedInterface != nil { for name, iface := range interfaces {
if attachedInterface.IsEnabled() && attachedInterface.IsOnline() { log.Printf("[DEBUG-7] Checking interface %s: enabled=%v, online=%v", name, iface.IsEnabled(), iface.IsOnline())
debug.Log(debug.DEBUG_VERBOSE, "Sending announce to attached interface", "name", attachedInterface.GetName()) if iface.IsEnabled() && iface.IsOnline() {
if err := attachedInterface.Send(packet, ""); err != nil { log.Printf("[DEBUG-7] Sending announce to interface %s (%d bytes)", name, len(packet))
debug.Log(debug.DEBUG_ERROR, "Failed to send announce on attached interface", "error", err) if err := iface.Send(packet, ""); err != nil {
log.Printf("[ERROR] Failed to send announce on interface %s: %v", name, err)
lastErr = err lastErr = err
} } else {
log.Printf("[DEBUG-7] Successfully sent announce to interface %s", name)
} }
} else { } else {
interfaces := d.transport.GetInterfaces() log.Printf("[DEBUG-7] Skipping interface %s (not enabled or not online)", name)
for name, iface := range interfaces {
if iface.IsEnabled() && iface.IsOnline() {
debug.Log(debug.DEBUG_VERBOSE, "Sending announce to interface", "name", name)
if err := iface.Send(packet, ""); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to send announce on interface", "name", name, "error", err)
lastErr = err
}
}
} }
} }
@@ -266,7 +238,7 @@ func (d *Destination) AcceptsLinks(accepts bool) {
// Register with transport if accepting links // Register with transport if accepting links
if accepts && d.transport != nil { if accepts && d.transport != nil {
d.transport.RegisterDestination(d.hashValue, d) d.transport.RegisterDestination(d.hashValue, d)
debug.Log(debug.DEBUG_VERBOSE, "Destination registered with transport for link requests", "hash", fmt.Sprintf("%x", d.hashValue)) debugLog(DEBUG_VERBOSE, "Destination %x registered with transport for link requests", d.hashValue)
} }
} }
@@ -282,26 +254,20 @@ func (d *Destination) GetLinkCallback() common.LinkEstablishedCallback {
return d.linkCallback return d.linkCallback
} }
func (d *Destination) HandleIncomingLinkRequest(pkt interface{}, transport interface{}, networkIface common.NetworkInterface) error { func (d *Destination) HandleIncomingLinkRequest(linkID []byte, transport interface{}, networkIface common.NetworkInterface) error {
debug.Log(debug.DEBUG_INFO, "Handling incoming link request for destination", "hash", fmt.Sprintf("%x", d.GetHash())) debugLog(DEBUG_INFO, "Handling incoming link request for destination %x", d.GetHash())
pktObj, ok := pkt.(*packet.Packet) // Import link package here to avoid circular dependency at package level
if !ok { // We'll use dynamic import by having the caller create the link
return errors.New("invalid packet type") // For now, just call the callback with a placeholder
}
if incomingLinkHandler == nil { if d.linkCallback != nil {
return errors.New("no incoming link handler registered") debugLog(DEBUG_INFO, "Calling link established callback")
} // Pass linkID as the link object for now
// The callback will need to handle creating the actual link
linkIface, err := incomingLinkHandler(pktObj, d, transport, networkIface) d.linkCallback(linkID)
if err != nil { } else {
return fmt.Errorf("failed to handle link request: %w", err) debugLog(DEBUG_VERBOSE, "No link callback set")
}
if d.linkCallback != nil && linkIface != nil {
debug.Log(debug.DEBUG_INFO, "Calling link established callback")
d.linkCallback(linkIface)
} }
return nil return nil
@@ -313,35 +279,6 @@ func (d *Destination) SetPacketCallback(callback common.PacketCallback) {
d.packetCallback = callback d.packetCallback = callback
} }
func (d *Destination) Receive(pkt *packet.Packet, iface common.NetworkInterface) {
d.mutex.RLock()
callback := d.packetCallback
d.mutex.RUnlock()
if callback == nil {
debug.Log(debug.DEBUG_VERBOSE, "No packet callback set for destination")
return
}
if pkt.PacketType == packet.PacketTypeLinkReq {
debug.Log(debug.DEBUG_INFO, "Received link request for destination")
if err := d.HandleIncomingLinkRequest(pkt, d.transport, iface); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to handle incoming link request", "error", err)
}
return
}
plaintext, err := d.Decrypt(pkt.Data)
if err != nil {
debug.Log(debug.DEBUG_INFO, "Failed to decrypt packet data", "error", err)
return
}
debug.Log(debug.DEBUG_INFO, "Destination received packet", "bytes", len(plaintext))
callback(plaintext, iface)
}
func (d *Destination) SetProofRequestedCallback(callback common.ProofRequestedCallback) { func (d *Destination) SetProofRequestedCallback(callback common.ProofRequestedCallback) {
d.mutex.Lock() d.mutex.Lock()
defer d.mutex.Unlock() defer d.mutex.Unlock()
@@ -358,27 +295,8 @@ func (d *Destination) EnableRatchets(path string) bool {
d.mutex.Lock() d.mutex.Lock()
defer d.mutex.Unlock() defer d.mutex.Unlock()
if path == "" {
debug.Log(debug.DEBUG_ERROR, "No ratchet file path specified")
return false
}
d.ratchetsEnabled = true d.ratchetsEnabled = true
d.ratchetPath = path d.ratchetPath = path
d.latestRatchetTime = time.Time{} // Zero time to force rotation
// Load or initialize ratchets
if err := d.reloadRatchets(); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to load ratchets", "error", err)
// Initialize empty ratchet list
d.ratchets = make([][]byte, 0)
if err := d.persistRatchets(); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to create initial ratchet file", "error", err)
return false
}
}
debug.Log(debug.DEBUG_INFO, "Ratchets enabled", "path", path)
return true return true
} }
@@ -459,88 +377,34 @@ func (d *Destination) DeregisterRequestHandler(path string) bool {
return false return false
} }
func (d *Destination) GetRequestHandler(pathHash []byte) func([]byte, []byte, []byte, []byte, *identity.Identity, time.Time) interface{} {
d.mutex.RLock()
defer d.mutex.RUnlock()
for _, handler := range d.requestHandlers {
handlerPathHash := identity.TruncatedHash([]byte(handler.Path))
if string(handlerPathHash) == string(pathHash) {
return func(pathHash []byte, data []byte, requestID []byte, linkID []byte, remoteIdentity *identity.Identity, requestedAt time.Time) interface{} {
allowed := false
if handler.AllowMode == ALLOW_ALL {
allowed = true
} else if handler.AllowMode == ALLOW_LIST && remoteIdentity != nil {
remoteHash := remoteIdentity.Hash()
for _, allowedHash := range handler.AllowedList {
if string(remoteHash) == string(allowedHash) {
allowed = true
break
}
}
}
if !allowed {
return nil
}
result := handler.ResponseGenerator(handler.Path, data, requestID, linkID, remoteIdentity, requestedAt.Unix())
if result == nil {
return nil
}
return result
}
}
}
return nil
}
func (d *Destination) HandleRequest(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity *identity.Identity, requestedAt int64) []byte {
d.mutex.RLock()
handler, exists := d.requestHandlers[path]
d.mutex.RUnlock()
if !exists {
debug.Log(debug.DEBUG_INFO, "No handler registered for path", "path", path)
return []byte(">Not Found\n\nThe requested resource was not found.")
}
debug.Log(debug.DEBUG_VERBOSE, "Calling request handler", "path", path)
result := handler.ResponseGenerator(path, data, requestID, linkID, remoteIdentity, requestedAt)
if result == nil {
return []byte(">Not Found\n\nThe requested resource was not found.")
}
return result
}
func (d *Destination) Encrypt(plaintext []byte) ([]byte, error) { func (d *Destination) Encrypt(plaintext []byte) ([]byte, error) {
if d.destType == PLAIN { if d.destType == PLAIN {
debug.Log(debug.DEBUG_VERBOSE, "Using plaintext transmission for PLAIN destination") log.Printf("[DEBUG-4] Using plaintext transmission for PLAIN destination")
return plaintext, nil return plaintext, nil
} }
if d.identity == nil { if d.identity == nil {
debug.Log(debug.DEBUG_INFO, "Cannot encrypt: no identity available") log.Printf("[DEBUG-3] Cannot encrypt: no identity available")
return nil, errors.New("no identity available for encryption") return nil, errors.New("no identity available for encryption")
} }
debug.Log(debug.DEBUG_VERBOSE, "Encrypting bytes for destination", "bytes", len(plaintext), "destType", d.destType) log.Printf("[DEBUG-4] Encrypting %d bytes for destination type %d", len(plaintext), d.destType)
switch d.destType { switch d.destType {
case SINGLE: case SINGLE:
recipientKey := d.identity.GetEncryptionKey() recipientKey := d.identity.GetPublicKey()
debug.Log(debug.DEBUG_VERBOSE, "Encrypting for single recipient", "key", fmt.Sprintf("%x", recipientKey[:8])) log.Printf("[DEBUG-4] Encrypting for single recipient with key %x", recipientKey[:8])
return d.identity.Encrypt(plaintext, recipientKey) return d.identity.Encrypt(plaintext, recipientKey)
case GROUP: case GROUP:
key := d.identity.GetCurrentRatchetKey() key := d.identity.GetCurrentRatchetKey()
if key == nil { if key == nil {
debug.Log(debug.DEBUG_INFO, "Cannot encrypt: no ratchet key available") log.Printf("[DEBUG-3] Cannot encrypt: no ratchet key available")
return nil, errors.New("no ratchet key available") return nil, errors.New("no ratchet key available")
} }
debug.Log(debug.DEBUG_VERBOSE, "Encrypting for group with ratchet key", "key", fmt.Sprintf("%x", key[:8])) log.Printf("[DEBUG-4] Encrypting for group with ratchet key %x", key[:8])
return d.identity.EncryptWithHMAC(plaintext, key) return d.identity.EncryptWithHMAC(plaintext, key)
default: default:
debug.Log(debug.DEBUG_INFO, "Unsupported destination type for encryption", "destType", d.destType) log.Printf("[DEBUG-3] Unsupported destination type %d for encryption", d.destType)
return nil, errors.New("unsupported destination type for encryption") return nil, errors.New("unsupported destination type for encryption")
} }
} }
@@ -601,186 +465,3 @@ func (d *Destination) GetHash() []byte {
} }
return d.hashValue return d.hashValue
} }
func (d *Destination) persistRatchets() error {
d.ratchetFileLock.Lock()
defer d.ratchetFileLock.Unlock()
if !d.ratchetsEnabled || d.ratchetPath == "" {
return errors.New("ratchets not enabled or no path specified")
}
debug.Log(debug.DEBUG_PACKETS, "Persisting ratchets", "count", len(d.ratchets), "path", d.ratchetPath)
// Pack ratchets using msgpack
packedRatchets, err := msgpack.Marshal(d.ratchets)
if err != nil {
return fmt.Errorf("failed to pack ratchets: %w", err)
}
// Sign the packed ratchets
signature, err := d.Sign(packedRatchets)
if err != nil {
return fmt.Errorf("failed to sign ratchets: %w", err)
}
// Create structure
persistedData := map[string][]byte{
"signature": signature,
"ratchets": packedRatchets,
}
// Pack the entire structure
finalData, err := msgpack.Marshal(persistedData)
if err != nil {
return fmt.Errorf("failed to pack ratchet data: %w", err)
}
// Write to temporary file first, then rename (atomic operation)
tempPath := d.ratchetPath + ".tmp"
file, err := os.Create(tempPath) // #nosec G304
if err != nil {
return fmt.Errorf("failed to create temp ratchet file: %w", err)
}
if _, err := file.Write(finalData); err != nil {
// #nosec G104 - Error already being handled, cleanup errors are non-critical
file.Close()
// #nosec G104 - Error already being handled, cleanup errors are non-critical
os.Remove(tempPath)
return fmt.Errorf("failed to write ratchet data: %w", err)
}
// #nosec G104 - File is being closed after successful write, error is non-critical
file.Close()
// Remove old file if exists
if _, err := os.Stat(d.ratchetPath); err == nil {
// #nosec G104 - Removing old file, error is non-critical if it doesn't exist
os.Remove(d.ratchetPath)
}
// Atomic rename
if err := os.Rename(tempPath, d.ratchetPath); err != nil {
// #nosec G104 - Error already being handled, cleanup errors are non-critical
os.Remove(tempPath)
return fmt.Errorf("failed to rename ratchet file: %w", err)
}
debug.Log(debug.DEBUG_PACKETS, "Ratchets persisted successfully")
return nil
}
func (d *Destination) reloadRatchets() error {
d.ratchetFileLock.Lock()
defer d.ratchetFileLock.Unlock()
if _, err := os.Stat(d.ratchetPath); os.IsNotExist(err) {
debug.Log(debug.DEBUG_INFO, "No existing ratchet data found, initializing new ratchet file")
d.ratchets = make([][]byte, 0)
return nil
}
file, err := os.Open(d.ratchetPath) // #nosec G304
if err != nil {
return fmt.Errorf("failed to open ratchet file: %w", err)
}
defer file.Close()
// Read all data
fileData, err := io.ReadAll(file)
if err != nil {
return fmt.Errorf("failed to read ratchet file: %w", err)
}
// Unpack outer structure
var persistedData map[string][]byte
if err := msgpack.Unmarshal(fileData, &persistedData); err != nil {
return fmt.Errorf("failed to unpack ratchet data: %w", err)
}
signature, hasSignature := persistedData["signature"]
packedRatchets, hasRatchets := persistedData["ratchets"]
if !hasSignature || !hasRatchets {
return fmt.Errorf("invalid ratchet file format")
}
// Verify signature
if !d.identity.Verify(packedRatchets, signature) {
return fmt.Errorf("invalid ratchet file signature")
}
// Unpack ratchet list
if err := msgpack.Unmarshal(packedRatchets, &d.ratchets); err != nil {
return fmt.Errorf("failed to unpack ratchet list: %w", err)
}
debug.Log(debug.DEBUG_INFO, "Ratchets reloaded successfully", "count", len(d.ratchets))
return nil
}
func (d *Destination) RotateRatchets() error {
d.mutex.Lock()
defer d.mutex.Unlock()
if !d.ratchetsEnabled {
return errors.New("ratchets not enabled")
}
now := time.Now()
if !d.latestRatchetTime.IsZero() && now.Before(d.latestRatchetTime.Add(time.Duration(d.ratchetInterval)*time.Second)) {
debug.Log(debug.DEBUG_TRACE, "Ratchet rotation interval not reached")
return nil
}
debug.Log(debug.DEBUG_INFO, "Rotating ratchets", "destination", d.ExpandName())
// Generate new ratchet key (32 bytes for X25519 private key)
newRatchet := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, newRatchet); err != nil {
return fmt.Errorf("failed to generate new ratchet: %w", err)
}
// Insert at beginning (most recent first)
d.ratchets = append([][]byte{newRatchet}, d.ratchets...)
d.latestRatchetTime = now
// Get ratchet public key for ID
ratchetPub, err := curve25519.X25519(newRatchet, curve25519.Basepoint)
if err == nil {
d.latestRatchetID = identity.TruncatedHash(ratchetPub)[:identity.NAME_HASH_LENGTH/8]
}
// Clean old ratchets
d.cleanRatchets()
// Persist to disk
if err := d.persistRatchets(); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to persist ratchets after rotation", "error", err)
return err
}
debug.Log(debug.DEBUG_INFO, "Ratchet rotation completed", "total_ratchets", len(d.ratchets))
return nil
}
func (d *Destination) cleanRatchets() {
if len(d.ratchets) > d.ratchetCount {
debug.Log(debug.DEBUG_TRACE, "Cleaning old ratchets", "before", len(d.ratchets), "keeping", d.ratchetCount)
d.ratchets = d.ratchets[:d.ratchetCount]
}
}
func (d *Destination) GetRatchets() [][]byte {
d.mutex.RLock()
defer d.mutex.RUnlock()
if !d.ratchetsEnabled {
return nil
}
// Return copy to prevent external modification
ratchetsCopy := make([][]byte, len(d.ratchets))
copy(ratchetsCopy, d.ratchets)
return ratchetsCopy
}

View File

@@ -1,178 +0,0 @@
package destination
import (
"bytes"
"crypto/sha256"
"path/filepath"
"testing"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
)
type mockTransport struct {
config *common.ReticulumConfig
interfaces map[string]common.NetworkInterface
}
func (m *mockTransport) GetConfig() *common.ReticulumConfig {
return m.config
}
func (m *mockTransport) GetInterfaces() map[string]common.NetworkInterface {
return m.interfaces
}
func (m *mockTransport) RegisterDestination(hash []byte, dest interface{}) {
}
type mockInterface struct {
common.BaseInterface
}
func (m *mockInterface) Send(data []byte, address string) error {
return nil
}
func TestNewDestination(t *testing.T) {
id, _ := identity.New()
transport := &mockTransport{config: &common.ReticulumConfig{}}
dest, err := New(id, IN|OUT, SINGLE, "testapp", transport, "testaspect")
if err != nil {
t.Fatalf("New failed: %v", err)
}
if dest == nil {
t.Fatal("New returned nil")
}
if dest.ExpandName() != "testapp.testaspect" {
t.Errorf("Expected name testapp.testaspect, got %s", dest.ExpandName())
}
hash := dest.GetHash()
if len(hash) != 16 {
t.Errorf("Expected hash length 16, got %d", len(hash))
}
}
func TestFromHash(t *testing.T) {
id, _ := identity.New()
transport := &mockTransport{}
hash := make([]byte, 16)
dest, err := FromHash(hash, id, SINGLE, transport)
if err != nil {
t.Fatalf("FromHash failed: %v", err)
}
if !bytes.Equal(dest.GetHash(), hash) {
t.Error("Hashes don't match")
}
}
func TestRequestHandlers(t *testing.T) {
id, _ := identity.New()
dest, _ := New(id, IN, SINGLE, "test", &mockTransport{})
path := "test/path"
response := []byte("hello")
err := dest.RegisterRequestHandler(path, func(p string, d []byte, rid []byte, lid []byte, ri *identity.Identity, ra int64) []byte {
return response
}, ALLOW_ALL, nil)
if err != nil {
t.Fatalf("RegisterRequestHandler failed: %v", err)
}
result := dest.HandleRequest(path, nil, nil, nil, nil, 0)
if !bytes.Equal(result, response) {
t.Errorf("Expected response %q, got %q", response, result)
}
if !dest.DeregisterRequestHandler(path) {
t.Error("DeregisterRequestHandler failed")
}
}
func TestEncryptDecrypt(t *testing.T) {
id, _ := identity.New()
dest, _ := New(id, IN|OUT, SINGLE, "test", &mockTransport{})
plaintext := []byte("hello world")
ciphertext, err := dest.Encrypt(plaintext)
if err != nil {
t.Fatalf("Encrypt failed: %v", err)
}
decrypted, err := dest.Decrypt(ciphertext)
if err != nil {
t.Fatalf("Decrypt failed: %v", err)
}
if !bytes.Equal(plaintext, decrypted) {
t.Errorf("Decrypted data doesn't match: %q vs %q", decrypted, plaintext)
}
}
func TestRatchets(t *testing.T) {
tmpDir := t.TempDir()
ratchetPath := filepath.Join(tmpDir, "ratchets")
id, _ := identity.New()
dest, _ := New(id, IN|OUT, SINGLE, "test", &mockTransport{})
if !dest.EnableRatchets(ratchetPath) {
t.Fatal("EnableRatchets failed")
}
err := dest.RotateRatchets()
if err != nil {
t.Fatalf("RotateRatchets failed: %v", err)
}
ratchets := dest.GetRatchets()
if len(ratchets) != 1 {
t.Errorf("Expected 1 ratchet, got %d", len(ratchets))
}
}
func TestPlainDestination(t *testing.T) {
id, _ := identity.New()
dest, _ := New(id, IN|OUT, PLAIN, "test", &mockTransport{})
plaintext := []byte("plain text")
ciphertext, _ := dest.Encrypt(plaintext)
if !bytes.Equal(plaintext, ciphertext) {
t.Error("Plain destination should not encrypt")
}
decrypted, _ := dest.Decrypt(ciphertext)
if !bytes.Equal(plaintext, decrypted) {
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

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package identity package identity
import ( import (
@@ -10,17 +8,17 @@ import (
"crypto/rand" "crypto/rand"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log"
"os" "os"
"sync" "sync"
"time" "time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common" "github.com/Sudo-Ivan/reticulum-go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/cryptography" "github.com/Sudo-Ivan/reticulum-go/pkg/cryptography"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
"github.com/vmihailenco/msgpack/v5"
"golang.org/x/crypto/curve25519" "golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf" "golang.org/x/crypto/hkdf"
) )
@@ -46,7 +44,7 @@ const (
type Identity struct { type Identity struct {
privateKey []byte privateKey []byte
publicKey []byte publicKey []byte
signingSeed []byte // 32-byte Ed25519 seed signingSeed []byte // 32-byte Ed25519 seed (compatible with Python RNS)
verificationKey ed25519.PublicKey verificationKey ed25519.PublicKey
hash []byte hash []byte
hexHash string hexHash string
@@ -59,7 +57,6 @@ type Identity struct {
var ( var (
knownDestinations = make(map[string][]interface{}) knownDestinations = make(map[string][]interface{})
knownDestinationsLock sync.RWMutex
knownRatchets = make(map[string][]byte) knownRatchets = make(map[string][]byte)
ratchetPersistLock sync.Mutex ratchetPersistLock sync.Mutex
) )
@@ -79,7 +76,7 @@ func New() (*Identity, error) {
i.privateKey = privKey i.privateKey = privKey
i.publicKey = pubKey i.publicKey = pubKey
// Generate 32-byte Ed25519 seed // Generate 32-byte Ed25519 seed (compatible with Python RNS)
var ed25519Seed [32]byte var ed25519Seed [32]byte
if _, err := io.ReadFull(rand.Reader, ed25519Seed[:]); err != nil { if _, err := io.ReadFull(rand.Reader, ed25519Seed[:]); err != nil {
return nil, fmt.Errorf("failed to generate Ed25519 seed: %v", err) return nil, fmt.Errorf("failed to generate Ed25519 seed: %v", err)
@@ -108,7 +105,7 @@ func (i *Identity) GetPrivateKey() []byte {
} }
func (i *Identity) Sign(data []byte) []byte { func (i *Identity) Sign(data []byte) []byte {
// Derive Ed25519 private key from seed // Derive Ed25519 private key from seed (compatible with Python RNS)
privKey := ed25519.NewKeyFromSeed(i.signingSeed) privKey := ed25519.NewKeyFromSeed(i.signingSeed)
return cryptography.Sign(privKey, data) return cryptography.Sign(privKey, data)
} }
@@ -136,25 +133,20 @@ func (i *Identity) Encrypt(plaintext []byte, ratchet []byte) ([]byte, error) {
return nil, err return nil, err
} }
// Derive key material (64 bytes: first 32 for HMAC, last 32 for encryption) // Derive encryption key
salt := i.GetSalt() key, err := cryptography.DeriveKey(sharedSecret, i.GetSalt(), i.GetContext(), 32)
debug.Log(debug.DEBUG_ALL, "Encrypt: using salt", "salt", fmt.Sprintf("%x", salt), "identity_hash", fmt.Sprintf("%x", i.Hash()))
key, err := cryptography.DeriveKey(sharedSecret, salt, i.GetContext(), 64)
if err != nil { if err != nil {
return nil, err return nil, err
} }
hmacKey := key[:32]
encryptionKey := key[32:64]
// Encrypt data // Encrypt data
ciphertext, err := cryptography.EncryptAES256CBC(encryptionKey, plaintext) ciphertext, err := cryptography.EncryptAES256CBC(key[:32], plaintext)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Calculate HMAC over ciphertext only (iv + encrypted_data) // Calculate HMAC
mac := cryptography.ComputeHMAC(hmacKey, ciphertext) mac := cryptography.ComputeHMAC(key, append(ephemeralPubKey, ciphertext...))
// Combine components // Combine components
token := make([]byte, 0, len(ephemeralPubKey)+len(ciphertext)+len(mac)) token := make([]byte, 0, len(ephemeralPubKey)+len(ciphertext)+len(mac))
@@ -181,7 +173,7 @@ func GetRandomHash() []byte {
randomData := make([]byte, TRUNCATED_HASHLENGTH/8) randomData := make([]byte, TRUNCATED_HASHLENGTH/8)
_, err := rand.Read(randomData) // #nosec G104 _, err := rand.Read(randomData) // #nosec G104
if err != nil { if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Failed to read random data for hash", "error", err) log.Printf("[DEBUG-1] Failed to read random data for hash: %v", err)
return nil // Or handle the error appropriately return nil // Or handle the error appropriately
} }
return TruncatedHash(randomData) return TruncatedHash(randomData)
@@ -192,14 +184,12 @@ func Remember(packet []byte, destHash []byte, publicKey []byte, appData []byte)
// Store destination data as [packet, destHash, identity, appData] // Store destination data as [packet, destHash, identity, appData]
id := FromPublicKey(publicKey) id := FromPublicKey(publicKey)
knownDestinationsLock.Lock()
knownDestinations[hashStr] = []interface{}{ knownDestinations[hashStr] = []interface{}{
packet, packet,
destHash, destHash,
id, id,
appData, appData,
} }
knownDestinationsLock.Unlock()
} }
func ValidateAnnounce(packet []byte, destHash []byte, publicKey []byte, signature []byte, appData []byte) bool { func ValidateAnnounce(packet []byte, destHash []byte, publicKey []byte, signature []byte, appData []byte) bool {
@@ -231,18 +221,13 @@ func FromPublicKey(publicKey []byte) *Identity {
return nil return nil
} }
id := &Identity{ return &Identity{
publicKey: publicKey[:KEYSIZE/16], publicKey: publicKey[:KEYSIZE/16],
verificationKey: publicKey[KEYSIZE/16:], verificationKey: publicKey[KEYSIZE/16:],
ratchets: make(map[string][]byte), ratchets: make(map[string][]byte),
ratchetExpiry: make(map[string]int64), ratchetExpiry: make(map[string]int64),
mutex: &sync.RWMutex{}, mutex: &sync.RWMutex{},
} }
hash := cryptography.Hash(id.GetPublicKey())
id.hash = hash[:TRUNCATED_HASHLENGTH/8]
return id
} }
func (i *Identity) Hex() string { func (i *Identity) Hex() string {
@@ -256,11 +241,7 @@ func (i *Identity) String() string {
func Recall(hash []byte) (*Identity, error) { func Recall(hash []byte) (*Identity, error) {
hashStr := hex.EncodeToString(hash) hashStr := hex.EncodeToString(hash)
knownDestinationsLock.RLock() if data, exists := knownDestinations[hashStr]; exists {
data, exists := knownDestinations[hashStr]
knownDestinationsLock.RUnlock()
if exists {
// data is [packet, destHash, identity, appData] // data is [packet, destHash, identity, appData]
if len(data) >= 3 { if len(data) >= 3 {
if id, ok := data[2].(*Identity); ok { if id, ok := data[2].(*Identity); ok {
@@ -298,13 +279,13 @@ func (i *Identity) GetCurrentRatchetKey() []byte {
if len(i.ratchets) == 0 { if len(i.ratchets) == 0 {
// If no ratchets exist, generate one. // If no ratchets exist, generate one.
// This should ideally be handled by an explicit setup process. // This should ideally be handled by an explicit setup process.
debug.Log(debug.DEBUG_TRACE, "No ratchets found, generating a new one on-the-fly") log.Println("[DEBUG-5] No ratchets found, generating a new one on-the-fly.")
// Temporarily unlock to call RotateRatchet, which locks internally. // Temporarily unlock to call RotateRatchet, which locks internally.
i.mutex.RUnlock() i.mutex.RUnlock()
newRatchet, err := i.RotateRatchet() newRatchet, err := i.RotateRatchet()
i.mutex.RLock() i.mutex.RLock()
if err != nil { if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Failed to generate initial ratchet key", "error", err) log.Printf("[DEBUG-1] Failed to generate initial ratchet key: %v", err)
return nil return nil
} }
return newRatchet return newRatchet
@@ -312,7 +293,7 @@ func (i *Identity) GetCurrentRatchetKey() []byte {
// Return the most recently generated ratchet key // Return the most recently generated ratchet key
var latestKey []byte var latestKey []byte
var latestTime int64 var latestTime int64 = 0
for id, expiry := range i.ratchetExpiry { for id, expiry := range i.ratchetExpiry {
if expiry > latestTime { if expiry > latestTime {
latestTime = expiry latestTime = expiry
@@ -321,7 +302,7 @@ func (i *Identity) GetCurrentRatchetKey() []byte {
} }
if latestKey == nil { if latestKey == nil {
debug.Log(debug.DEBUG_ERROR, "Could not determine the latest ratchet key", "ratchet_count", len(i.ratchets)) log.Printf("[DEBUG-2] Could not determine the latest ratchet key from %d ratchets.", len(i.ratchets))
} }
return latestKey return latestKey
@@ -329,13 +310,13 @@ func (i *Identity) GetCurrentRatchetKey() []byte {
func (i *Identity) Decrypt(ciphertextToken []byte, ratchets [][]byte, enforceRatchets bool, ratchetIDReceiver *common.RatchetIDReceiver) ([]byte, error) { func (i *Identity) Decrypt(ciphertextToken []byte, ratchets [][]byte, enforceRatchets bool, ratchetIDReceiver *common.RatchetIDReceiver) ([]byte, error) {
if i.privateKey == nil { if i.privateKey == nil {
debug.Log(debug.DEBUG_CRITICAL, "Decryption failed: identity has no private key") log.Printf("[DEBUG-1] Decryption failed: identity has no private key")
return nil, errors.New("decryption failed because identity does not hold a private key") return nil, errors.New("decryption failed because identity does not hold a private key")
} }
debug.Log(debug.DEBUG_ALL, "Starting decryption for identity", "hash", i.GetHexHash()) log.Printf("[DEBUG-7] Starting decryption for identity %s", i.GetHexHash())
if len(ratchets) > 0 { if len(ratchets) > 0 {
debug.Log(debug.DEBUG_ALL, "Attempting decryption with ratchets", "count", len(ratchets)) log.Printf("[DEBUG-7] Attempting decryption with %d ratchets", len(ratchets))
} }
if len(ciphertextToken) <= KEYSIZE/8/2 { if len(ciphertextToken) <= KEYSIZE/8/2 {
@@ -354,7 +335,7 @@ func (i *Identity) Decrypt(ciphertextToken []byte, ratchets [][]byte, enforceRat
// Try decryption with ratchets first if provided // Try decryption with ratchets first if provided
if len(ratchets) > 0 { if len(ratchets) > 0 {
for _, ratchet := range ratchets { for _, ratchet := range ratchets {
if decrypted, ratchetID, err := i.tryRatchetDecryption(peerPubBytes, ciphertext, mac, ratchet); err == nil { if decrypted, ratchetID, err := i.tryRatchetDecryption(peerPubBytes, ciphertext, ratchet); err == nil {
if ratchetIDReceiver != nil { if ratchetIDReceiver != nil {
ratchetIDReceiver.LatestRatchetID = ratchetID ratchetIDReceiver.LatestRatchetID = ratchetID
} }
@@ -376,25 +357,20 @@ func (i *Identity) Decrypt(ciphertextToken []byte, ratchets [][]byte, enforceRat
return nil, fmt.Errorf("failed to generate shared key: %v", err) return nil, fmt.Errorf("failed to generate shared key: %v", err)
} }
// Derive key material (64 bytes: first 32 for HMAC, last 32 for encryption) // Derive key using HKDF
salt := i.GetSalt() hkdfReader := hkdf.New(sha256.New, sharedKey, i.GetSalt(), i.GetContext())
debug.Log(debug.DEBUG_ALL, "Decrypt: using salt", "salt", fmt.Sprintf("%x", salt), "identity_hash", fmt.Sprintf("%x", i.Hash())) derivedKey := make([]byte, 32)
hkdfReader := hkdf.New(sha256.New, sharedKey, salt, i.GetContext())
derivedKey := make([]byte, 64)
if _, err := io.ReadFull(hkdfReader, derivedKey); err != nil { if _, err := io.ReadFull(hkdfReader, derivedKey); err != nil {
return nil, fmt.Errorf("failed to derive key: %v", err) return nil, fmt.Errorf("failed to derive key: %v", err)
} }
hmacKey := derivedKey[:32] // Validate HMAC
encryptionKey := derivedKey[32:64] if !cryptography.ValidateHMAC(derivedKey, append(peerPubBytes, ciphertext...), mac) {
// Validate HMAC over ciphertext only (iv + encrypted_data)
if !cryptography.ValidateHMAC(hmacKey, ciphertext, mac) {
return nil, errors.New("invalid HMAC") return nil, errors.New("invalid HMAC")
} }
// Create AES cipher // Create AES cipher
block, err := aes.NewCipher(encryptionKey) block, err := aes.NewCipher(derivedKey)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create cipher: %v", err) return nil, fmt.Errorf("failed to create cipher: %v", err)
} }
@@ -431,42 +407,34 @@ func (i *Identity) Decrypt(ciphertextToken []byte, ratchets [][]byte, enforceRat
ratchetIDReceiver.LatestRatchetID = nil ratchetIDReceiver.LatestRatchetID = nil
} }
debug.Log(debug.DEBUG_ALL, "Decryption completed successfully") log.Printf("[DEBUG-7] Decryption completed successfully")
return plaintext[:len(plaintext)-padding], nil return plaintext[:len(plaintext)-padding], nil
} }
// Helper function to attempt decryption using a ratchet // Helper function to attempt decryption using a ratchet
func (i *Identity) tryRatchetDecryption(peerPubBytes, ciphertext, mac, ratchet []byte) (plaintext, ratchetID []byte, err error) { func (i *Identity) tryRatchetDecryption(peerPubBytes, ciphertext, ratchet []byte) ([]byte, []byte, error) {
// Convert ratchet to private key // Convert ratchet to private key
ratchetPriv := ratchet ratchetPriv := ratchet
// Get ratchet ID // Get ratchet ID
ratchetPubBytes, err := curve25519.X25519(ratchetPriv, cryptography.GetBasepoint()) ratchetPubBytes, err := curve25519.X25519(ratchetPriv, cryptography.GetBasepoint())
if err != nil { if err != nil {
debug.Log(debug.DEBUG_ALL, "Failed to generate ratchet public key", "error", err) log.Printf("[DEBUG-7] Failed to generate ratchet public key: %v", err)
return nil, nil, err return nil, nil, err
} }
ratchetID = i.GetRatchetID(ratchetPubBytes) ratchetID := i.GetRatchetID(ratchetPubBytes)
sharedSecret, err := cryptography.DeriveSharedSecret(ratchet, peerPubBytes) sharedSecret, err := cryptography.DeriveSharedSecret(ratchet, peerPubBytes)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
key, err := cryptography.DeriveKey(sharedSecret, i.GetSalt(), i.GetContext(), 64) key, err := cryptography.DeriveKey(sharedSecret, i.GetSalt(), i.GetContext(), 32)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
hmacKey := key[:32] plaintext, err := cryptography.DecryptAES256CBC(key, ciphertext)
encryptionKey := key[32:64]
// Validate HMAC over ciphertext only (iv + encrypted_data)
if !cryptography.ValidateHMAC(hmacKey, ciphertext, mac) {
return nil, nil, errors.New("invalid HMAC")
}
plaintext, err = cryptography.DecryptAES256CBC(encryptionKey, ciphertext)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -475,23 +443,12 @@ func (i *Identity) tryRatchetDecryption(peerPubBytes, ciphertext, mac, ratchet [
} }
func (i *Identity) EncryptWithHMAC(plaintext []byte, key []byte) ([]byte, error) { func (i *Identity) EncryptWithHMAC(plaintext []byte, key []byte) ([]byte, error) {
var hmacKey, encryptionKey []byte ciphertext, err := cryptography.EncryptAES256CBC(key, plaintext)
if len(key) == 64 {
hmacKey = key[:32]
encryptionKey = key[32:64]
} else if len(key) == 32 {
hmacKey = key[:16]
encryptionKey = key[16:32]
} else {
return nil, errors.New("invalid key length for EncryptWithHMAC")
}
ciphertext, err := cryptography.EncryptAES256CBC(encryptionKey, plaintext)
if err != nil { if err != nil {
return nil, err return nil, err
} }
mac := cryptography.ComputeHMAC(hmacKey, ciphertext) mac := cryptography.ComputeHMAC(key, ciphertext)
return append(ciphertext, mac...), nil return append(ciphertext, mac...), nil
} }
@@ -500,158 +457,48 @@ func (i *Identity) DecryptWithHMAC(data []byte, key []byte) ([]byte, error) {
return nil, errors.New("data too short") return nil, errors.New("data too short")
} }
var hmacKey, encryptionKey []byte
if len(key) == 64 {
hmacKey = key[:32]
encryptionKey = key[32:64]
} else if len(key) == 32 {
hmacKey = key[:16]
encryptionKey = key[16:32]
} else {
return nil, errors.New("invalid key length for DecryptWithHMAC")
}
macStart := len(data) - cryptography.SHA256Size macStart := len(data) - cryptography.SHA256Size
ciphertext := data[:macStart] ciphertext := data[:macStart]
messageMAC := data[macStart:] messageMAC := data[macStart:]
if !cryptography.ValidateHMAC(hmacKey, ciphertext, messageMAC) { if !cryptography.ValidateHMAC(key, ciphertext, messageMAC) {
return nil, errors.New("invalid HMAC") return nil, errors.New("invalid HMAC")
} }
return cryptography.DecryptAES256CBC(encryptionKey, ciphertext) return cryptography.DecryptAES256CBC(key, ciphertext)
} }
func (i *Identity) ToFile(path string) error { func (i *Identity) ToFile(path string) error {
debug.Log(debug.DEBUG_ALL, "Saving identity to file", "hash", i.GetHexHash(), "path", path) log.Printf("[DEBUG-7] Saving identity %s to file: %s", i.GetHexHash(), path)
if i.privateKey == nil || i.signingSeed == nil { // Persist ratchets to a separate file
return errors.New("cannot save identity without private keys") ratchetPath := path + ".ratchets"
if err := i.saveRatchets(ratchetPath); err != nil {
log.Printf("[DEBUG-1] Failed to save ratchets: %v", err)
// Continue saving the main identity file even if ratchets fail
} }
// Store private keys as raw bytes data := map[string]interface{}{
// Format: [X25519 PrivKey (32 bytes)][Ed25519 PrivKey (32 bytes)] "private_key": i.privateKey,
// Total: 64 bytes "public_key": i.publicKey,
privateKeyBytes := make([]byte, 64) "signing_seed": i.signingSeed,
copy(privateKeyBytes[:32], i.privateKey) "verification_key": i.verificationKey,
copy(privateKeyBytes[32:], i.signingSeed) "app_data": i.appData,
}
// Write raw bytes to file
file, err := os.Create(path) // #nosec G304 file, err := os.Create(path) // #nosec G304
if err != nil { if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Failed to create identity file", "error", err) log.Printf("[DEBUG-1] Failed to create identity file: %v", err)
return err return err
} }
defer file.Close() defer file.Close()
if _, err := file.Write(privateKeyBytes); err != nil { if err := json.NewEncoder(file).Encode(data); err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Failed to write identity data", "error", err) log.Printf("[DEBUG-1] Failed to encode identity data: %v", err)
return err return err
} }
debug.Log(debug.DEBUG_ALL, "Identity saved successfully", "bytes", len(privateKeyBytes)) log.Printf("[DEBUG-7] Identity saved successfully")
return nil
}
func FromFile(path string) (*Identity, error) {
debug.Log(debug.DEBUG_ALL, "Loading identity from file", "path", path)
// Read the private key bytes from file
// bearer:disable go_gosec_filesystem_filereadtaint
data, err := os.ReadFile(path) // #nosec G304
if err != nil {
return nil, fmt.Errorf("failed to read identity file: %w", err)
}
if len(data) != 64 {
return nil, fmt.Errorf("invalid identity file: expected 64 bytes, got %d", len(data))
}
// Parse the private keys
// Format: [X25519 PrivKey (32 bytes)][Ed25519 PrivKey (32 bytes)]
privateKey := data[:32]
signingSeed := data[32:64]
// Create identity with initialized maps and mutex
ident := &Identity{
ratchets: make(map[string][]byte),
ratchetExpiry: make(map[string]int64),
mutex: &sync.RWMutex{},
}
if err := ident.loadPrivateKey(privateKey, signingSeed); err != nil {
return nil, fmt.Errorf("failed to load private key: %w", err)
}
debug.Log(debug.DEBUG_INFO, "Identity loaded from file", "hash", ident.GetHexHash())
return ident, nil
}
func LoadOrCreateTransportIdentity() (*Identity, error) {
storagePath := os.Getenv("RETICULUM_STORAGE_PATH")
if storagePath == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("failed to get home directory: %w", err)
}
storagePath = fmt.Sprintf("%s/.reticulum/storage", homeDir)
}
if err := os.MkdirAll(storagePath, 0700); err != nil {
return nil, fmt.Errorf("failed to create storage directory: %w", err)
}
transportIdentityPath := fmt.Sprintf("%s/transport_identity", storagePath)
if ident, err := FromFile(transportIdentityPath); err == nil {
debug.Log(debug.DEBUG_INFO, "Loaded transport identity from storage")
return ident, nil
}
debug.Log(debug.DEBUG_INFO, "No valid transport identity in storage, creating new one")
ident, err := New()
if err != nil {
return nil, fmt.Errorf("failed to create transport identity: %w", err)
}
if err := ident.ToFile(transportIdentityPath); err != nil {
return nil, fmt.Errorf("failed to save transport identity: %w", err)
}
debug.Log(debug.DEBUG_INFO, "Created and saved transport identity")
return ident, nil
}
func (i *Identity) loadPrivateKey(privateKey, signingSeed []byte) error {
if len(privateKey) != 32 || len(signingSeed) != 32 {
return errors.New("invalid private key length")
}
// Load X25519 private key
i.privateKey = make([]byte, 32)
copy(i.privateKey, privateKey)
// Load Ed25519 signing seed
i.signingSeed = make([]byte, 32)
copy(i.signingSeed, signingSeed)
// Derive public keys from private keys
var err error
i.publicKey, err = curve25519.X25519(i.privateKey, curve25519.Basepoint)
if err != nil {
return fmt.Errorf("failed to derive X25519 public key: %w", err)
}
signingKey := ed25519.NewKeyFromSeed(i.signingSeed)
i.verificationKey = signingKey.Public().(ed25519.PublicKey)
publicKeyBytes := make([]byte, 0, len(i.publicKey)+len(i.verificationKey))
publicKeyBytes = append(publicKeyBytes, i.publicKey...)
publicKeyBytes = append(publicKeyBytes, i.verificationKey...)
i.hash = TruncatedHash(publicKeyBytes)[:TRUNCATED_HASHLENGTH/8]
i.hexHash = hex.EncodeToString(i.hash)
debug.Log(debug.DEBUG_VERBOSE, "Private key loaded successfully", "hash", i.GetHexHash())
return nil return nil
} }
@@ -663,117 +510,70 @@ func (i *Identity) saveRatchets(path string) error {
return nil // Nothing to save return nil // Nothing to save
} }
debug.Log(debug.DEBUG_PACKETS, "Saving ratchets", "count", len(i.ratchets), "path", path) log.Printf("[DEBUG-6] Saving %d ratchets to %s", len(i.ratchets), path)
data := map[string]interface{}{
// Convert ratchets to list format for msgpack "ratchets": i.ratchets,
ratchetList := make([][]byte, 0, len(i.ratchets)) "ratchet_expiry": i.ratchetExpiry,
for _, ratchet := range i.ratchets {
ratchetList = append(ratchetList, ratchet)
} }
// Pack ratchets using msgpack file, err := os.Create(path) // #nosec G304
packedRatchets, err := msgpack.Marshal(ratchetList)
if err != nil { if err != nil {
return fmt.Errorf("failed to pack ratchets: %w", err) return fmt.Errorf("failed to create ratchet file: %w", err)
} }
defer file.Close()
// Sign the packed ratchets return json.NewEncoder(file).Encode(data)
signature := i.Sign(packedRatchets)
// Create structure: {"signature": ..., "ratchets": ...}
persistedData := map[string][]byte{
"signature": signature,
"ratchets": packedRatchets,
}
// Pack the entire structure
finalData, err := msgpack.Marshal(persistedData)
if err != nil {
return fmt.Errorf("failed to pack ratchet data: %w", err)
}
// Write to temporary file first, then rename (atomic operation)
tempPath := path + ".tmp"
file, err := os.Create(tempPath) // #nosec G304
if err != nil {
return fmt.Errorf("failed to create temp ratchet file: %w", err)
}
if _, err := file.Write(finalData); err != nil {
// #nosec G104 - Error already being handled, cleanup errors are non-critical
file.Close()
// #nosec G104 - Error already being handled, cleanup errors are non-critical
os.Remove(tempPath)
return fmt.Errorf("failed to write ratchet data: %w", err)
}
// #nosec G104 - File is being closed after successful write, error is non-critical
file.Close()
// Atomic rename
if err := os.Rename(tempPath, path); err != nil {
// #nosec G104 - Error already being handled, cleanup errors are non-critical
os.Remove(tempPath)
return fmt.Errorf("failed to rename ratchet file: %w", err)
}
debug.Log(debug.DEBUG_PACKETS, "Ratchets saved successfully")
return nil
} }
func RecallIdentity(path string) (*Identity, error) { func RecallIdentity(path string) (*Identity, error) {
debug.Log(debug.DEBUG_ALL, "Attempting to recall identity", "path", path) log.Printf("[DEBUG-7] Attempting to recall identity from: %s", path)
// bearer:disable go_gosec_filesystem_filereadtaint
file, err := os.Open(path) // #nosec G304 file, err := os.Open(path) // #nosec G304
if err != nil { if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Failed to open identity file", "error", err) log.Printf("[DEBUG-1] Failed to open identity file: %v", err)
return nil, err return nil, err
} }
defer file.Close() defer file.Close()
// Read raw bytes var data map[string]interface{}
// Format: [X25519 PrivKey (32 bytes)][Ed25519 PrivKey (32 bytes)] if err := json.NewDecoder(file).Decode(&data); err != nil {
privateKeyBytes := make([]byte, 64) log.Printf("[DEBUG-1] Failed to decode identity data: %v", err)
n, err := io.ReadFull(file, privateKeyBytes)
if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Failed to read identity data", "error", err)
return nil, err return nil, err
} }
if n != 64 {
return nil, fmt.Errorf("invalid identity file: expected 64 bytes, got %d", n) var signingSeed []byte
var verificationKey ed25519.PublicKey
if seedData, exists := data["signing_seed"]; exists {
signingSeed = seedData.([]byte)
verificationKey = data["verification_key"].(ed25519.PublicKey)
} else if keyData, exists := data["signing_key"]; exists {
oldKey := keyData.(ed25519.PrivateKey)
signingSeed = oldKey[:32]
verificationKey = data["verification_key"].(ed25519.PublicKey)
} else {
return nil, fmt.Errorf("no signing key data found in identity file")
} }
// Extract keys
x25519PrivKey := privateKeyBytes[:32]
ed25519Seed := privateKeyBytes[32:]
// Derive public keys
x25519PubKey, err := curve25519.X25519(x25519PrivKey, curve25519.Basepoint)
if err != nil {
return nil, fmt.Errorf("failed to derive X25519 public key: %v", err)
}
ed25519PrivKey := ed25519.NewKeyFromSeed(ed25519Seed)
ed25519PubKey := ed25519PrivKey.Public().(ed25519.PublicKey)
id := &Identity{ id := &Identity{
privateKey: x25519PrivKey, privateKey: data["private_key"].([]byte),
publicKey: x25519PubKey, publicKey: data["public_key"].([]byte),
signingSeed: ed25519Seed, signingSeed: signingSeed,
verificationKey: ed25519PubKey, verificationKey: verificationKey,
appData: data["app_data"].([]byte),
ratchets: make(map[string][]byte), ratchets: make(map[string][]byte),
ratchetExpiry: make(map[string]int64), ratchetExpiry: make(map[string]int64),
mutex: &sync.RWMutex{}, mutex: &sync.RWMutex{},
} }
// Generate hash // Load ratchets if they exist
combinedPub := make([]byte, KEYSIZE/8) ratchetPath := path + ".ratchets"
copy(combinedPub[:KEYSIZE/16], id.publicKey) if err := id.loadRatchets(ratchetPath); err != nil {
copy(combinedPub[KEYSIZE/16:], id.verificationKey) log.Printf("[DEBUG-2] Could not load ratchets for identity %s: %v", id.GetHexHash(), err)
hash := sha256.Sum256(combinedPub) // This is not a fatal error, the identity can still function
id.hash = hash[:TRUNCATED_HASHLENGTH/8] }
debug.Log(debug.DEBUG_ALL, "Successfully recalled identity", "hash", id.GetHexHash()) log.Printf("[DEBUG-7] Successfully recalled identity with hash: %s", id.GetHexHash())
return id, nil return id, nil
} }
@@ -781,62 +581,38 @@ func (i *Identity) loadRatchets(path string) error {
i.mutex.Lock() i.mutex.Lock()
defer i.mutex.Unlock() defer i.mutex.Unlock()
// bearer:disable go_gosec_filesystem_filereadtaint
file, err := os.Open(path) // #nosec G304 file, err := os.Open(path) // #nosec G304
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
debug.Log(debug.DEBUG_PACKETS, "No ratchet file found, skipping", "path", path) log.Printf("[DEBUG-6] No ratchet file found at %s, skipping.", path)
return nil return nil
} }
return fmt.Errorf("failed to open ratchet file: %w", err) return fmt.Errorf("failed to open ratchet file: %w", err)
} }
defer file.Close() defer file.Close()
// Read all data var data map[string]interface{}
fileData, err := io.ReadAll(file) if err := json.NewDecoder(file).Decode(&data); err != nil {
if err != nil { return fmt.Errorf("failed to decode ratchet data: %w", err)
return fmt.Errorf("failed to read ratchet file: %w", err)
} }
// Unpack outer structure: {"signature": ..., "ratchets": ...} if ratchets, ok := data["ratchets"].(map[string]interface{}); ok {
var persistedData map[string][]byte for id, key := range ratchets {
if err := msgpack.Unmarshal(fileData, &persistedData); err != nil { if keyStr, ok := key.(string); ok {
return fmt.Errorf("failed to unpack ratchet data: %w", err) i.ratchets[id] = []byte(keyStr)
}
}
} }
signature, hasSignature := persistedData["signature"] if expiry, ok := data["ratchet_expiry"].(map[string]interface{}); ok {
packedRatchets, hasRatchets := persistedData["ratchets"] for id, timeVal := range expiry {
if timeFloat, ok := timeVal.(float64); ok {
if !hasSignature || !hasRatchets { i.ratchetExpiry[id] = int64(timeFloat)
return fmt.Errorf("invalid ratchet file format: missing signature or ratchets") }
}
} }
// Verify signature log.Printf("[DEBUG-6] Loaded %d ratchets from %s", len(i.ratchets), path)
if !i.Verify(packedRatchets, signature) {
return fmt.Errorf("invalid ratchet file signature")
}
// Unpack ratchet list
var ratchetList [][]byte
if err := msgpack.Unmarshal(packedRatchets, &ratchetList); err != nil {
return fmt.Errorf("failed to unpack ratchet list: %w", err)
}
// Store ratchets with generated IDs
now := time.Now().Unix()
for _, ratchet := range ratchetList {
// Generate ratchet public key to create ID
ratchetPub, err := curve25519.X25519(ratchet, curve25519.Basepoint)
if err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to generate ratchet public key", "error", err)
continue
}
ratchetID := i.GetRatchetID(ratchetPub)
i.ratchets[string(ratchetID)] = ratchet
i.ratchetExpiry[string(ratchetID)] = now + RATCHET_EXPIRY
}
debug.Log(debug.DEBUG_PACKETS, "Loaded ratchets", "count", len(i.ratchets), "path", path)
return nil return nil
} }
@@ -862,10 +638,7 @@ func (i *Identity) GetRatchetID(ratchetPubBytes []byte) []byte {
} }
func GetKnownDestination(hash string) ([]interface{}, bool) { func GetKnownDestination(hash string) ([]interface{}, bool) {
knownDestinationsLock.RLock() if data, exists := knownDestinations[hash]; exists {
data, exists := knownDestinations[hash]
knownDestinationsLock.RUnlock()
if exists {
return data, true return data, true
} }
return nil, false return nil, false
@@ -895,7 +668,7 @@ func (i *Identity) SetRatchetKey(id string, key []byte) {
// NewIdentity creates a new Identity instance with fresh keys // NewIdentity creates a new Identity instance with fresh keys
func NewIdentity() (*Identity, error) { func NewIdentity() (*Identity, error) {
// Generate 32-byte Ed25519 seed // Generate 32-byte Ed25519 seed (compatible with Python RNS)
var ed25519Seed [32]byte var ed25519Seed [32]byte
if _, err := io.ReadFull(rand.Reader, ed25519Seed[:]); err != nil { if _, err := io.ReadFull(rand.Reader, ed25519Seed[:]); err != nil {
return nil, fmt.Errorf("failed to generate Ed25519 seed: %v", err) return nil, fmt.Errorf("failed to generate Ed25519 seed: %v", err)
@@ -931,50 +704,28 @@ func NewIdentity() (*Identity, error) {
copy(combinedPub[:KEYSIZE/16], i.publicKey) copy(combinedPub[:KEYSIZE/16], i.publicKey)
copy(combinedPub[KEYSIZE/16:], i.verificationKey) copy(combinedPub[KEYSIZE/16:], i.verificationKey)
hash := sha256.Sum256(combinedPub) hash := sha256.Sum256(combinedPub)
i.hash = hash[:TRUNCATED_HASHLENGTH/8] i.hash = hash[:]
return i, nil return i, nil
} }
// FromBytes creates an Identity from a 64-byte private key representation
func FromBytes(data []byte) (*Identity, error) {
if len(data) != 64 {
return nil, fmt.Errorf("invalid identity data: expected 64 bytes, got %d", len(data))
}
privateKey := data[:32]
signingSeed := data[32:64]
ident := &Identity{
ratchets: make(map[string][]byte),
ratchetExpiry: make(map[string]int64),
mutex: &sync.RWMutex{},
}
if err := ident.loadPrivateKey(privateKey, signingSeed); err != nil {
return nil, fmt.Errorf("failed to load private key: %w", err)
}
return ident, nil
}
func (i *Identity) RotateRatchet() ([]byte, error) { func (i *Identity) RotateRatchet() ([]byte, error) {
i.mutex.Lock() i.mutex.Lock()
defer i.mutex.Unlock() defer i.mutex.Unlock()
debug.Log(debug.DEBUG_ALL, "Rotating ratchet for identity", "hash", i.GetHexHash()) log.Printf("[DEBUG-7] Rotating ratchet for identity %s", i.GetHexHash())
// Generate new ratchet key // Generate new ratchet key
newRatchet := make([]byte, RATCHETSIZE/8) newRatchet := make([]byte, RATCHETSIZE/8)
if _, err := io.ReadFull(rand.Reader, newRatchet); err != nil { if _, err := io.ReadFull(rand.Reader, newRatchet); err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Failed to generate new ratchet", "error", err) log.Printf("[DEBUG-1] Failed to generate new ratchet: %v", err)
return nil, err return nil, err
} }
// Get public key for ratchet ID // Get public key for ratchet ID
ratchetPub, err := curve25519.X25519(newRatchet, curve25519.Basepoint) ratchetPub, err := curve25519.X25519(newRatchet, curve25519.Basepoint)
if err != nil { if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Failed to generate ratchet public key", "error", err) log.Printf("[DEBUG-1] Failed to generate ratchet public key: %v", err)
return nil, err return nil, err
} }
@@ -985,7 +736,7 @@ func (i *Identity) RotateRatchet() ([]byte, error) {
i.ratchets[string(ratchetID)] = newRatchet i.ratchets[string(ratchetID)] = newRatchet
i.ratchetExpiry[string(ratchetID)] = expiry i.ratchetExpiry[string(ratchetID)] = expiry
debug.Log(debug.DEBUG_ALL, "New ratchet generated", "id", fmt.Sprintf("%x", ratchetID), "expiry", expiry) log.Printf("[DEBUG-7] New ratchet generated with ID: %x, expiry: %d", ratchetID, expiry)
// Cleanup old ratchets if we exceed max retained // Cleanup old ratchets if we exceed max retained
if len(i.ratchets) > MAX_RETAINED_RATCHETS { if len(i.ratchets) > MAX_RETAINED_RATCHETS {
@@ -1001,10 +752,10 @@ func (i *Identity) RotateRatchet() ([]byte, error) {
delete(i.ratchets, oldestID) delete(i.ratchets, oldestID)
delete(i.ratchetExpiry, oldestID) delete(i.ratchetExpiry, oldestID)
debug.Log(debug.DEBUG_ALL, "Cleaned up oldest ratchet", "id", fmt.Sprintf("%x", []byte(oldestID))) log.Printf("[DEBUG-7] Cleaned up oldest ratchet with ID: %x", []byte(oldestID))
} }
debug.Log(debug.DEBUG_ALL, "Current number of active ratchets", "count", len(i.ratchets)) log.Printf("[DEBUG-7] Current number of active ratchets: %d", len(i.ratchets))
return newRatchet, nil return newRatchet, nil
} }
@@ -1012,7 +763,7 @@ func (i *Identity) GetRatchets() [][]byte {
i.mutex.RLock() i.mutex.RLock()
defer i.mutex.RUnlock() defer i.mutex.RUnlock()
debug.Log(debug.DEBUG_ALL, "Getting ratchets for identity", "hash", i.GetHexHash()) log.Printf("[DEBUG-7] Getting ratchets for identity %s", i.GetHexHash())
ratchets := make([][]byte, 0, len(i.ratchets)) ratchets := make([][]byte, 0, len(i.ratchets))
now := time.Now().Unix() now := time.Now().Unix()
@@ -1030,7 +781,7 @@ func (i *Identity) GetRatchets() [][]byte {
} }
} }
debug.Log(debug.DEBUG_ALL, "Retrieved active ratchets", "active", len(ratchets), "expired", expired) log.Printf("[DEBUG-7] Retrieved %d active ratchets, cleaned up %d expired", len(ratchets), expired)
return ratchets return ratchets
} }
@@ -1038,7 +789,7 @@ func (i *Identity) CleanupExpiredRatchets() {
i.mutex.Lock() i.mutex.Lock()
defer i.mutex.Unlock() defer i.mutex.Unlock()
debug.Log(debug.DEBUG_ALL, "Starting ratchet cleanup for identity", "hash", i.GetHexHash()) log.Printf("[DEBUG-7] Starting ratchet cleanup for identity %s", i.GetHexHash())
now := time.Now().Unix() now := time.Now().Unix()
cleaned := 0 cleaned := 0
@@ -1050,7 +801,7 @@ func (i *Identity) CleanupExpiredRatchets() {
} }
} }
debug.Log(debug.DEBUG_ALL, "Cleaned up expired ratchets", "cleaned", cleaned, "remaining", len(i.ratchets)) log.Printf("[DEBUG-7] Cleaned up %d expired ratchets, %d remaining", cleaned, len(i.ratchets))
} }
// ValidateAnnounce validates an announce packet's signature // ValidateAnnounce validates an announce packet's signature

View File

@@ -1,148 +0,0 @@
package identity
import (
"bytes"
"path/filepath"
"testing"
)
func TestNewIdentity(t *testing.T) {
id, err := New()
if err != nil {
t.Fatalf("New() failed: %v", err)
}
if id == nil {
t.Fatal("New() returned nil")
}
pubKey := id.GetPublicKey()
if len(pubKey) != 64 {
t.Errorf("Expected public key length 64, got %d", len(pubKey))
}
privKey := id.GetPrivateKey()
if len(privKey) != 64 {
t.Errorf("Expected private key length 64, got %d", len(privKey))
}
}
func TestSignVerify(t *testing.T) {
id, _ := New()
data := []byte("test data")
sig := id.Sign(data)
if !id.Verify(data, sig) {
t.Error("Verification failed for valid signature")
}
if id.Verify([]byte("wrong data"), sig) {
t.Error("Verification succeeded for wrong data")
}
}
func TestEncryptDecrypt(t *testing.T) {
id, _ := New()
plaintext := []byte("secret message")
ciphertext, err := id.Encrypt(plaintext, nil)
if err != nil {
t.Fatalf("Encrypt failed: %v", err)
}
decrypted, err := id.Decrypt(ciphertext, nil, false, nil)
if err != nil {
t.Fatalf("Decrypt failed: %v", err)
}
if !bytes.Equal(plaintext, decrypted) {
t.Errorf("Decrypted data doesn't match plaintext: %q vs %q", decrypted, plaintext)
}
}
func TestIdentityHash(t *testing.T) {
id, _ := New()
h := id.Hash()
if len(h) != TRUNCATED_HASHLENGTH/8 {
t.Errorf("Expected hash length %d, got %d", TRUNCATED_HASHLENGTH/8, len(h))
}
hexHash := id.Hex()
if len(hexHash) != TRUNCATED_HASHLENGTH/4 {
t.Errorf("Expected hex hash length %d, got %d", TRUNCATED_HASHLENGTH/4, len(hexHash))
}
}
func TestFileOperations(t *testing.T) {
tmpDir := t.TempDir()
idPath := filepath.Join(tmpDir, "identity")
id, _ := New()
err := id.ToFile(idPath)
if err != nil {
t.Fatalf("ToFile failed: %v", err)
}
loadedID, err := FromFile(idPath)
if err != nil {
t.Fatalf("FromFile failed: %v", err)
}
if !bytes.Equal(id.GetPublicKey(), loadedID.GetPublicKey()) {
t.Error("Loaded identity public key doesn't match original")
}
}
func TestRatchets(t *testing.T) {
id, _ := New()
ratchet, err := id.RotateRatchet()
if err != nil {
t.Fatalf("RotateRatchet failed: %v", err)
}
if len(ratchet) != RATCHETSIZE/8 {
t.Errorf("Expected ratchet size %d, got %d", RATCHETSIZE/8, len(ratchet))
}
ratchets := id.GetRatchets()
if len(ratchets) != 1 {
t.Errorf("Expected 1 ratchet, got %d", len(ratchets))
}
id.CleanupExpiredRatchets()
// Should still be there since it's not expired
if len(id.GetRatchets()) != 1 {
t.Error("Ratchet unexpectedly cleaned up")
}
}
func TestRecallIdentity(t *testing.T) {
tmpDir := t.TempDir()
idPath := filepath.Join(tmpDir, "identity_recall")
id, _ := New()
_ = id.ToFile(idPath)
recalledID, err := RecallIdentity(idPath)
if err != nil {
t.Fatalf("RecallIdentity failed: %v", err)
}
if !bytes.Equal(id.GetPublicKey(), recalledID.GetPublicKey()) {
t.Error("Recalled identity public key doesn't match original")
}
}
func TestTruncatedHash(t *testing.T) {
data := []byte("some data")
h := TruncatedHash(data)
if len(h) != TRUNCATED_HASHLENGTH/8 {
t.Errorf("Expected length %d, got %d", TRUNCATED_HASHLENGTH/8, len(h))
}
}
func TestGetRandomHash(t *testing.T) {
h := GetRandomHash()
if len(h) != TRUNCATED_HASHLENGTH/8 {
t.Errorf("Expected length %d, got %d", TRUNCATED_HASHLENGTH/8, len(h))
}
}

View File

@@ -1,143 +1,49 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package interfaces package interfaces
import ( import (
"bytes"
"crypto/sha256"
"fmt" "fmt"
"log"
"net" "net"
"strings"
"sync" "sync"
"time" "time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common" "github.com/Sudo-Ivan/reticulum-go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
) )
const ( const (
HW_MTU = 1196
DEFAULT_DISCOVERY_PORT = 29716 DEFAULT_DISCOVERY_PORT = 29716
DEFAULT_DATA_PORT = 42671 DEFAULT_DATA_PORT = 42671
DEFAULT_GROUP_ID = "reticulum"
BITRATE_GUESS = 10 * 1000 * 1000 BITRATE_GUESS = 10 * 1000 * 1000
PEERING_TIMEOUT = 22 * time.Second PEERING_TIMEOUT = 7500 * time.Millisecond
ANNOUNCE_INTERVAL = 1600 * time.Millisecond
PEER_JOB_INTERVAL = 4 * time.Second
MCAST_ECHO_TIMEOUT = 6500 * time.Millisecond
SCOPE_LINK = "2" SCOPE_LINK = "2"
SCOPE_ADMIN = "4" SCOPE_ADMIN = "4"
SCOPE_SITE = "5" SCOPE_SITE = "5"
SCOPE_ORGANISATION = "8" SCOPE_ORGANISATION = "8"
SCOPE_GLOBAL = "e" SCOPE_GLOBAL = "e"
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 { type AutoInterface struct {
BaseInterface BaseInterface
groupID []byte groupID []byte
groupHash []byte
discoveryPort int discoveryPort int
dataPort int dataPort int
discoveryScope string discoveryScope string
multicastAddrType string
mcastDiscoveryAddr string
peers map[string]*Peer peers map[string]*Peer
linkLocalAddrs []string linkLocalAddrs []string
adoptedInterfaces map[string]*AdoptedInterface adoptedInterfaces map[string]string
interfaceServers map[string]*net.UDPConn interfaceServers map[string]*net.UDPConn
discoveryServers map[string]*net.UDPConn
multicastEchoes map[string]time.Time multicastEchoes map[string]time.Time
timedOutInterfaces map[string]time.Time mutex sync.RWMutex
allowedInterfaces []string
ignoredInterfaces []string
outboundConn *net.UDPConn outboundConn *net.UDPConn
announceInterval time.Duration
peerJobInterval time.Duration
peeringTimeout time.Duration
mcastEchoTimeout time.Duration
mifDeque []DequeEntry
done chan struct{}
stopOnce sync.Once
}
type AdoptedInterface struct {
name string
linkLocalAddr string
index int
} }
type Peer struct { type Peer struct {
ifaceName string ifaceName string
lastHeard time.Time lastHeard time.Time
addr *net.UDPAddr conn *net.UDPConn
}
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
if config.GroupID != "" {
groupID = config.GroupID
}
discoveryScope := SCOPE_LINK
if config.DiscoveryScope != "" {
discoveryScope = normalizeScope(config.DiscoveryScope)
}
multicastAddrType := MCAST_ADDR_TYPE_TEMPORARY
if config.MulticastAddrType != "" {
multicastAddrType = normalizeMulticastType(config.MulticastAddrType)
}
discoveryPort := DEFAULT_DISCOVERY_PORT
if config.DiscoveryPort != 0 {
discoveryPort = config.DiscoveryPort
}
dataPort := DEFAULT_DATA_PORT
if config.DataPort != 0 {
dataPort = config.DataPort
}
groupHash := sha256.Sum256([]byte(groupID))
// Python-compatible multicast address generation
// gt = "0:" + "{:02x}".format(g[3]+(g[2]<<8)) + ":" + ...
gt := "0"
for i := 1; i <= 6; i++ {
gt += fmt.Sprintf(":%02x%02x", groupHash[i*2], groupHash[i*2+1])
}
mcastAddr := fmt.Sprintf("ff%s%s:%s", multicastAddrType, discoveryScope, gt)
ai := &AutoInterface{ ai := &AutoInterface{
BaseInterface: BaseInterface{ BaseInterface: BaseInterface{
Name: name, Name: name,
@@ -146,102 +52,43 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
Online: false, Online: false,
Enabled: config.Enabled, Enabled: config.Enabled,
Detached: false, Detached: false,
IN: true, IN: false,
OUT: false, OUT: false,
MTU: HW_MTU, MTU: common.DEFAULT_MTU,
Bitrate: BITRATE_GUESS, Bitrate: BITRATE_MINIMUM,
}, },
groupID: []byte(groupID), discoveryPort: DEFAULT_DISCOVERY_PORT,
groupHash: groupHash[:], dataPort: DEFAULT_DATA_PORT,
discoveryPort: discoveryPort, discoveryScope: SCOPE_LINK,
dataPort: dataPort,
discoveryScope: discoveryScope,
multicastAddrType: multicastAddrType,
mcastDiscoveryAddr: mcastAddr,
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]string),
interfaceServers: make(map[string]*net.UDPConn), interfaceServers: make(map[string]*net.UDPConn),
discoveryServers: make(map[string]*net.UDPConn),
multicastEchoes: make(map[string]time.Time), multicastEchoes: make(map[string]time.Time),
timedOutInterfaces: make(map[string]time.Time),
allowedInterfaces: make([]string, 0),
ignoredInterfaces: make([]string, 0),
announceInterval: ANNOUNCE_INTERVAL,
peerJobInterval: PEER_JOB_INTERVAL,
peeringTimeout: PEERING_TIMEOUT,
mcastEchoTimeout: MCAST_ECHO_TIMEOUT,
mifDeque: make([]DequeEntry, 0, MULTI_IF_DEQUE_LEN),
done: make(chan struct{}),
} }
debug.Log(debug.DEBUG_INFO, "AutoInterface configured", "name", name, "group", groupID, "mcast_addr", mcastAddr) if config.Port != 0 {
ai.discoveryPort = config.Port
}
if config.GroupID != "" {
ai.groupID = []byte(config.GroupID)
} else {
ai.groupID = []byte("reticulum")
}
return ai, nil return ai, nil
} }
func normalizeScope(scope string) string {
switch scope {
case "link", "2":
return SCOPE_LINK
case "admin", "4":
return SCOPE_ADMIN
case "site", "5":
return SCOPE_SITE
case "organisation", "organization", "8":
return SCOPE_ORGANISATION
case "global", "e":
return SCOPE_GLOBAL
default:
return SCOPE_LINK
}
}
func normalizeMulticastType(mtype string) string {
switch mtype {
case "permanent", "0":
return MCAST_ADDR_TYPE_PERMANENT
case "temporary", "1":
return MCAST_ADDR_TYPE_TEMPORARY
default:
return MCAST_ADDR_TYPE_TEMPORARY
}
}
func (ai *AutoInterface) Start() error { func (ai *AutoInterface) Start() error {
ai.Mutex.Lock()
// Only recreate done if it's nil or was closed
select {
case <-ai.done:
ai.done = make(chan struct{})
ai.stopOnce = sync.Once{}
default:
if ai.done == nil {
ai.done = make(chan struct{})
ai.stopOnce = sync.Once{}
}
}
ai.Mutex.Unlock()
interfaces, err := net.Interfaces() interfaces, err := net.Interfaces()
if err != nil { if err != nil {
return fmt.Errorf("failed to list interfaces: %v", err) return fmt.Errorf("failed to list interfaces: %v", err)
} }
for _, iface := range interfaces { for _, iface := range interfaces {
if ai.shouldIgnoreInterface(iface.Name) { if err := ai.configureInterface(&iface); err != nil {
debug.Log(debug.DEBUG_TRACE, "Ignoring interface", "name", iface.Name) log.Printf("Failed to configure interface %s: %v", iface.Name, err)
continue
}
if len(ai.allowedInterfaces) > 0 && !ai.isAllowedInterface(iface.Name) {
debug.Log(debug.DEBUG_TRACE, "Interface not in allowed list", "name", iface.Name)
continue
}
ifaceCopy := iface
// bearer:disable go_gosec_memory_memory_aliasing
if err := ai.configureInterface(&ifaceCopy); err != nil {
debug.Log(debug.DEBUG_VERBOSE, "Failed to configure interface", "name", iface.Name, "error", err)
continue continue
} }
} }
@@ -250,97 +97,43 @@ func (ai *AutoInterface) Start() error {
return fmt.Errorf("no suitable interfaces found") return fmt.Errorf("no suitable interfaces found")
} }
// Mark interface as online
ai.Online = true ai.Online = true
ai.IN = true ai.Enabled = true
ai.OUT = true
go ai.peerJobs() go ai.peerJobs()
go ai.announceLoop()
debug.Log(debug.DEBUG_INFO, "AutoInterface started", "adopted", len(ai.adoptedInterfaces))
return nil return nil
} }
func (ai *AutoInterface) shouldIgnoreInterface(name string) bool {
ignoreList := []string{"lo", "lo0", "tun0", "awdl0", "llw0", "en5", "dummy0"}
for _, ignored := range ai.ignoredInterfaces {
if name == ignored {
return true
}
}
for _, ignored := range ignoreList {
if name == ignored {
return true
}
}
return false
}
func (ai *AutoInterface) isAllowedInterface(name string) bool {
for _, allowed := range ai.allowedInterfaces {
if name == allowed {
return true
}
}
return false
}
func (ai *AutoInterface) configureInterface(iface *net.Interface) error { func (ai *AutoInterface) configureInterface(iface *net.Interface) error {
if iface.Flags&net.FlagUp == 0 {
return fmt.Errorf("interface is down")
}
if iface.Flags&net.FlagLoopback != 0 {
return fmt.Errorf("loopback interface")
}
addrs, err := iface.Addrs() addrs, err := iface.Addrs()
if err != nil { if err != nil {
return err return err
} }
var linkLocalAddr string
for _, addr := range addrs { for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok { if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.IsLinkLocalUnicast() {
if ipnet.IP.To4() == nil && ipnet.IP.IsLinkLocalUnicast() { ai.adoptedInterfaces[iface.Name] = ipnet.IP.String()
linkLocalAddr = descopeLinkLocal(ipnet.IP.String())
break
}
}
}
if linkLocalAddr == "" {
return fmt.Errorf("no link-local IPv6 address found")
}
ai.Mutex.Lock()
ai.adoptedInterfaces[iface.Name] = &AdoptedInterface{
name: iface.Name,
linkLocalAddr: linkLocalAddr,
index: iface.Index,
}
ai.linkLocalAddrs = append(ai.linkLocalAddrs, linkLocalAddr)
ai.multicastEchoes[iface.Name] = time.Now() ai.multicastEchoes[iface.Name] = time.Now()
ai.Mutex.Unlock()
if err := ai.startDiscoveryListener(iface); err != nil { if err := ai.startDiscoveryListener(iface); err != nil {
return fmt.Errorf("failed to start discovery listener: %v", err) return err
} }
if err := ai.startDataListener(iface); err != nil { if err := ai.startDataListener(iface); err != nil {
return fmt.Errorf("failed to start data listener: %v", err) return err
}
break
}
} }
debug.Log(debug.DEBUG_INFO, "Configured interface", "name", iface.Name, "addr", linkLocalAddr)
return nil return nil
} }
func (ai *AutoInterface) startDiscoveryListener(iface *net.Interface) error { func (ai *AutoInterface) startDiscoveryListener(iface *net.Interface) error {
addr := &net.UDPAddr{ addr := &net.UDPAddr{
IP: net.ParseIP(ai.mcastDiscoveryAddr), IP: net.ParseIP(fmt.Sprintf("ff%s%s::1", ai.discoveryScope, SCOPE_LINK)),
Port: ai.discoveryPort, Port: ai.discoveryPort,
Zone: iface.Name, Zone: iface.Name,
} }
@@ -350,332 +143,137 @@ func (ai *AutoInterface) startDiscoveryListener(iface *net.Interface) error {
return err return err
} }
if err := conn.SetReadBuffer(common.NUM_1024); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to set discovery read buffer", "error", err)
}
ai.Mutex.Lock()
ai.discoveryServers[iface.Name] = conn
ai.Mutex.Unlock()
go ai.handleDiscovery(conn, iface.Name) go ai.handleDiscovery(conn, iface.Name)
debug.Log(debug.DEBUG_VERBOSE, "Discovery listener started", "interface", iface.Name, "addr", ai.mcastDiscoveryAddr)
return nil return nil
} }
func (ai *AutoInterface) startDataListener(iface *net.Interface) error { func (ai *AutoInterface) startDataListener(iface *net.Interface) error {
adoptedIface, exists := ai.adoptedInterfaces[iface.Name]
if !exists {
return fmt.Errorf("interface not adopted")
}
addr := &net.UDPAddr{ addr := &net.UDPAddr{
IP: net.ParseIP(adoptedIface.linkLocalAddr), IP: net.IPv6zero,
Port: ai.dataPort, Port: ai.dataPort,
Zone: iface.Name, Zone: iface.Name,
} }
conn, err := net.ListenUDP("udp6", addr) conn, err := net.ListenUDP("udp6", addr)
if err != nil { if err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to listen on data port", "addr", addr, "error", err)
return err return err
} }
if err := conn.SetReadBuffer(ai.MTU); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to set data read buffer", "error", err)
}
ai.Mutex.Lock()
ai.interfaceServers[iface.Name] = conn ai.interfaceServers[iface.Name] = conn
ai.Mutex.Unlock() go ai.handleData(conn)
go ai.handleData(conn, iface.Name)
debug.Log(debug.DEBUG_VERBOSE, "Data listener started", "interface", iface.Name, "addr", addr)
return nil return nil
} }
func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) { func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) {
buf := make([]byte, common.NUM_1024) buf := make([]byte, 1024)
for { for {
ai.Mutex.RLock() _, remoteAddr, err := conn.ReadFromUDP(buf)
done := ai.done
ai.Mutex.RUnlock()
select {
case <-done:
return
default:
}
n, remoteAddr, err := conn.ReadFromUDP(buf)
if err != nil { if err != nil {
if ai.IsOnline() { log.Printf("Discovery read error: %v", err)
debug.Log(debug.DEBUG_ERROR, "Discovery read error", "interface", ifaceName, "error", err)
}
return
}
// Python: discovery_token = RNS.Identity.full_hash(self.group_id+ipv6_src[0].encode("utf-8"))
peerIP := descopeLinkLocal(remoteAddr.IP.String())
tokenSource := append(ai.groupID, []byte(peerIP)...)
expectedHash := sha256.Sum256(tokenSource)
if n >= len(expectedHash) {
receivedHash := buf[:len(expectedHash)]
if bytes.Equal(receivedHash, expectedHash[:]) {
ai.handlePeerAnnounce(remoteAddr, ifaceName)
} else {
debug.Log(debug.DEBUG_TRACE, "Received discovery with mismatched group hash", "interface", ifaceName, "peer", peerIP)
}
}
}
}
func (ai *AutoInterface) handleData(conn *net.UDPConn, ifaceName string) {
buf := make([]byte, ai.GetMTU())
for {
ai.Mutex.RLock()
done := ai.done
ai.Mutex.RUnlock()
select {
case <-done:
return
default:
}
n, remoteAddr, err := conn.ReadFromUDP(buf)
if err != nil {
if ai.IsOnline() {
debug.Log(debug.DEBUG_ERROR, "Data read error", "interface", ifaceName, "error", err)
}
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 continue
} }
// Add to deque ai.handlePeerAnnounce(remoteAddr, ifaceName)
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 func (ai *AutoInterface) handleData(conn *net.UDPConn) {
peerIP := descopeLinkLocal(remoteAddr.IP.String()) buf := make([]byte, ai.GetMTU())
peerKey := peerIP + "%" + ifaceName for {
if peer, exists := ai.peers[peerKey]; exists { n, _, err := conn.ReadFromUDP(buf)
peer.lastHeard = now if err != nil {
if !ai.IsDetached() {
log.Printf("Data read error: %v", err)
}
return
} }
ai.Mutex.Unlock()
if callback := ai.GetPacketCallback(); callback != nil { if callback := ai.GetPacketCallback(); callback != nil {
callback(data, ai) callback(buf[:n], ai)
} }
} }
} }
func (ai *AutoInterface) handlePeerAnnounce(addr *net.UDPAddr, ifaceName string) { func (ai *AutoInterface) handlePeerAnnounce(addr *net.UDPAddr, ifaceName string) {
ai.Mutex.Lock() ai.mutex.Lock()
defer ai.Mutex.Unlock() defer ai.mutex.Unlock()
peerIP := addr.IP.String() peerAddr := addr.IP.String()
for _, localAddr := range ai.linkLocalAddrs { for _, localAddr := range ai.linkLocalAddrs {
if peerIP == localAddr { if peerAddr == localAddr {
ai.multicastEchoes[ifaceName] = time.Now() ai.multicastEchoes[ifaceName] = time.Now()
debug.Log(debug.DEBUG_TRACE, "Received own multicast echo", "interface", ifaceName)
return return
} }
} }
peerKey := peerIP + "%" + ifaceName if _, exists := ai.peers[peerAddr]; !exists {
ai.peers[peerAddr] = &Peer{
if peer, exists := ai.peers[peerKey]; exists {
peer.lastHeard = time.Now()
debug.Log(debug.DEBUG_TRACE, "Updated peer", "peer", peerIP, "interface", ifaceName)
} else {
ai.peers[peerKey] = &Peer{
ifaceName: ifaceName, ifaceName: ifaceName,
lastHeard: time.Now(), lastHeard: time.Now(),
addr: addr,
} }
debug.Log(debug.DEBUG_INFO, "Discovered new peer", "peer", peerIP, "interface", ifaceName) log.Printf("Added peer %s on %s", peerAddr, ifaceName)
}
}
func (ai *AutoInterface) announceLoop() {
ticker := time.NewTicker(ai.announceInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if !ai.IsOnline() {
return
}
ai.sendPeerAnnounce()
case <-ai.done:
return
}
}
}
func (ai *AutoInterface) sendPeerAnnounce() {
ai.Mutex.RLock()
defer ai.Mutex.RUnlock()
for ifaceName, adoptedIface := range ai.adoptedInterfaces {
mcastAddr := &net.UDPAddr{
IP: net.ParseIP(ai.mcastDiscoveryAddr),
Port: ai.discoveryPort,
Zone: ifaceName,
}
if ai.outboundConn == nil {
var err error
ai.outboundConn, err = net.ListenUDP("udp6", &net.UDPAddr{Port: 0})
if err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to create outbound socket", "error", err)
return
}
}
// 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 { } else {
debug.Log(debug.DEBUG_TRACE, "Sent peer announce", "interface", adoptedIface.name) ai.peers[peerAddr].lastHeard = time.Now()
}
} }
} }
func (ai *AutoInterface) peerJobs() { func (ai *AutoInterface) peerJobs() {
ticker := time.NewTicker(ai.peerJobInterval) ticker := time.NewTicker(PEERING_TIMEOUT)
defer ticker.Stop() for range ticker.C {
ai.mutex.Lock()
for {
select {
case <-ticker.C:
if !ai.IsOnline() {
return
}
ai.Mutex.Lock()
now := time.Now() now := time.Now()
for peerKey, peer := range ai.peers { for addr, peer := range ai.peers {
if now.Sub(peer.lastHeard) > ai.peeringTimeout { if now.Sub(peer.lastHeard) > PEERING_TIMEOUT {
delete(ai.peers, peerKey) delete(ai.peers, addr)
debug.Log(debug.DEBUG_VERBOSE, "Removed timed out peer", "peer", peerKey) log.Printf("Removed timed out peer %s", addr)
} }
} }
for ifaceName, echoTime := range ai.multicastEchoes { ai.mutex.Unlock()
if now.Sub(echoTime) > ai.mcastEchoTimeout {
if _, exists := ai.timedOutInterfaces[ifaceName]; !exists {
debug.Log(debug.DEBUG_INFO, "Interface timed out", "interface", ifaceName)
ai.timedOutInterfaces[ifaceName] = now
}
} else {
delete(ai.timedOutInterfaces, ifaceName)
}
}
ai.Mutex.Unlock()
case <-ai.done:
return
}
} }
} }
func (ai *AutoInterface) Send(data []byte, address string) error { func (ai *AutoInterface) Send(data []byte, address string) error {
if !ai.IsOnline() { ai.mutex.RLock()
return fmt.Errorf("interface offline") defer ai.mutex.RUnlock()
}
ai.Mutex.RLock() for _, peer := range ai.peers {
defer ai.Mutex.RUnlock() addr := &net.UDPAddr{
IP: net.ParseIP(address),
if len(ai.peers) == 0 { Port: ai.dataPort,
debug.Log(debug.DEBUG_TRACE, "No peers available for sending") Zone: peer.ifaceName,
return nil
} }
if ai.outboundConn == nil { if ai.outboundConn == nil {
var err error var err error
ai.outboundConn, err = net.ListenUDP("udp6", &net.UDPAddr{Port: 0}) ai.outboundConn, err = net.ListenUDP("udp6", &net.UDPAddr{Port: 0})
if err != nil { if err != nil {
return fmt.Errorf("failed to create outbound socket: %v", err) return err
} }
} }
sentCount := 0 if _, err := ai.outboundConn.WriteToUDP(data, addr); err != nil {
for _, peer := range ai.peers { log.Printf("Failed to send to peer %s: %v", address, err)
targetAddr := &net.UDPAddr{
IP: peer.addr.IP,
Port: ai.dataPort,
Zone: peer.ifaceName,
}
if _, err := ai.outboundConn.WriteToUDP(data, targetAddr); err != nil {
debug.Log(debug.DEBUG_VERBOSE, "Failed to send to peer", "interface", peer.ifaceName, "error", err)
continue continue
} }
sentCount++
}
if sentCount > 0 {
debug.Log(debug.DEBUG_TRACE, "Sent data to peers", "count", sentCount, "bytes", len(data))
} }
return nil return nil
} }
func (ai *AutoInterface) Stop() error { func (ai *AutoInterface) Stop() error {
ai.Mutex.Lock() ai.mutex.Lock()
ai.Online = false defer ai.mutex.Unlock()
ai.IN = false
ai.OUT = false
for _, server := range ai.interfaceServers { for _, server := range ai.interfaceServers {
server.Close() // #nosec G104 server.Close() // #nosec G104
} }
for _, server := range ai.discoveryServers {
server.Close() // #nosec G104
}
if ai.outboundConn != nil { if ai.outboundConn != nil {
ai.outboundConn.Close() // #nosec G104 ai.outboundConn.Close() // #nosec G104
} }
ai.Mutex.Unlock()
ai.stopOnce.Do(func() {
if ai.done != nil {
close(ai.done)
}
})
debug.Log(debug.DEBUG_INFO, "AutoInterface stopped")
return nil return nil
} }

View File

@@ -5,7 +5,7 @@ import (
"testing" "testing"
"time" "time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common" "github.com/Sudo-Ivan/reticulum-go/pkg/common"
) )
func TestNewAutoInterface(t *testing.T) { func TestNewAutoInterface(t *testing.T) {
@@ -45,8 +45,7 @@ func TestNewAutoInterface(t *testing.T) {
t.Run("CustomConfig", func(t *testing.T) { t.Run("CustomConfig", func(t *testing.T) {
config := &common.InterfaceConfig{ config := &common.InterfaceConfig{
Enabled: true, Enabled: true,
DiscoveryPort: 12345, Port: 12345, // Custom discovery port
DataPort: 54321,
GroupID: "customGroup", GroupID: "customGroup",
} }
ai, err := NewAutoInterface("autoCustom", config) ai, err := NewAutoInterface("autoCustom", config)
@@ -60,9 +59,6 @@ func TestNewAutoInterface(t *testing.T) {
if ai.discoveryPort != 12345 { if ai.discoveryPort != 12345 {
t.Errorf("discoveryPort = %d; want 12345", ai.discoveryPort) t.Errorf("discoveryPort = %d; want 12345", ai.discoveryPort)
} }
if ai.dataPort != 54321 {
t.Errorf("dataPort = %d; want 54321", ai.dataPort)
}
if string(ai.groupID) != "customGroup" { if string(ai.groupID) != "customGroup" {
t.Errorf("groupID = %s; want customGroup", string(ai.groupID)) t.Errorf("groupID = %s; want customGroup", string(ai.groupID))
} }
@@ -83,11 +79,9 @@ func newMockAutoInterface(name string, config *common.InterfaceConfig) (*mockAut
// Initialize maps that would normally be initialized in Start() // Initialize maps that would normally be initialized in Start()
ai.peers = make(map[string]*Peer) ai.peers = make(map[string]*Peer)
ai.linkLocalAddrs = make([]string, 0) ai.linkLocalAddrs = make([]string, 0)
ai.adoptedInterfaces = make(map[string]*AdoptedInterface) ai.adoptedInterfaces = make(map[string]string)
ai.interfaceServers = make(map[string]*net.UDPConn) ai.interfaceServers = make(map[string]*net.UDPConn)
ai.discoveryServers = make(map[string]*net.UDPConn)
ai.multicastEchoes = make(map[string]time.Time) ai.multicastEchoes = make(map[string]time.Time)
ai.timedOutInterfaces = make(map[string]time.Time)
return &mockAutoInterface{AutoInterface: ai}, nil return &mockAutoInterface{AutoInterface: ai}, nil
} }
@@ -144,14 +138,14 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
ai.Mutex.Lock() ai.mutex.Lock()
now := time.Now() now := time.Now()
for addr, peer := range ai.peers { for addr, peer := range ai.peers {
if now.Sub(peer.lastHeard) > testTimeout { if now.Sub(peer.lastHeard) > testTimeout {
delete(ai.peers, addr) delete(ai.peers, addr)
} }
} }
ai.Mutex.Unlock() ai.mutex.Unlock()
case <-done: case <-done:
return return
} }
@@ -173,26 +167,27 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
peer2Addr := &net.UDPAddr{IP: net.ParseIP("fe80::2"), Zone: "eth0"} peer2Addr := &net.UDPAddr{IP: net.ParseIP("fe80::2"), Zone: "eth0"}
localAddr := &net.UDPAddr{IP: net.ParseIP("fe80::aaaa"), Zone: "eth0"} localAddr := &net.UDPAddr{IP: net.ParseIP("fe80::aaaa"), Zone: "eth0"}
ai.Mutex.Lock() // Add a simulated local address to avoid adding it as a peer
ai.mutex.Lock()
ai.linkLocalAddrs = append(ai.linkLocalAddrs, localAddrStr) ai.linkLocalAddrs = append(ai.linkLocalAddrs, localAddrStr)
ai.Mutex.Unlock() ai.mutex.Unlock()
t.Run("AddPeer1", func(t *testing.T) { t.Run("AddPeer1", func(t *testing.T) {
ai.Mutex.Lock() ai.mutex.Lock()
ai.mockHandlePeerAnnounce(peer1Addr, "eth0") ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
ai.Mutex.Unlock() ai.mutex.Unlock()
// Give a small amount of time for the peer to be processed // Give a small amount of time for the peer to be processed
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
ai.Mutex.RLock() ai.mutex.RLock()
count := len(ai.peers) count := len(ai.peers)
peer, exists := ai.peers[peer1AddrStr] peer, exists := ai.peers[peer1AddrStr]
var ifaceName string var ifaceName string
if exists { if exists {
ifaceName = peer.ifaceName ifaceName = peer.ifaceName
} }
ai.Mutex.RUnlock() ai.mutex.RUnlock()
if count != 1 { if count != 1 {
t.Fatalf("Expected 1 peer, got %d", count) t.Fatalf("Expected 1 peer, got %d", count)
@@ -206,17 +201,17 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
}) })
t.Run("AddPeer2", func(t *testing.T) { t.Run("AddPeer2", func(t *testing.T) {
ai.Mutex.Lock() ai.mutex.Lock()
ai.mockHandlePeerAnnounce(peer2Addr, "eth0") ai.mockHandlePeerAnnounce(peer2Addr, "eth0")
ai.Mutex.Unlock() ai.mutex.Unlock()
// Give a small amount of time for the peer to be processed // Give a small amount of time for the peer to be processed
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
ai.Mutex.RLock() ai.mutex.RLock()
count := len(ai.peers) count := len(ai.peers)
_, exists := ai.peers[peer2AddrStr] _, exists := ai.peers[peer2AddrStr]
ai.Mutex.RUnlock() ai.mutex.RUnlock()
if count != 2 { if count != 2 {
t.Fatalf("Expected 2 peers, got %d", count) t.Fatalf("Expected 2 peers, got %d", count)
@@ -227,16 +222,16 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
}) })
t.Run("IgnoreLocalAnnounce", func(t *testing.T) { t.Run("IgnoreLocalAnnounce", func(t *testing.T) {
ai.Mutex.Lock() ai.mutex.Lock()
ai.mockHandlePeerAnnounce(localAddr, "eth0") ai.mockHandlePeerAnnounce(localAddr, "eth0")
ai.Mutex.Unlock() ai.mutex.Unlock()
// Give a small amount of time for the peer to be processed // Give a small amount of time for the peer to be processed
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
ai.Mutex.RLock() ai.mutex.RLock()
count := len(ai.peers) count := len(ai.peers)
ai.Mutex.RUnlock() ai.mutex.RUnlock()
if count != 2 { if count != 2 {
t.Fatalf("Expected 2 peers after local announce, got %d", count) t.Fatalf("Expected 2 peers after local announce, got %d", count)
@@ -244,32 +239,32 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
}) })
t.Run("UpdatePeerTimestamp", func(t *testing.T) { t.Run("UpdatePeerTimestamp", func(t *testing.T) {
ai.Mutex.RLock() ai.mutex.RLock()
peer, exists := ai.peers[peer1AddrStr] peer, exists := ai.peers[peer1AddrStr]
var initialTime time.Time var initialTime time.Time
if exists { if exists {
initialTime = peer.lastHeard initialTime = peer.lastHeard
} }
ai.Mutex.RUnlock() ai.mutex.RUnlock()
if !exists { if !exists {
t.Fatalf("Peer %s not found before timestamp update", peer1AddrStr) t.Fatalf("Peer %s not found before timestamp update", peer1AddrStr)
} }
ai.Mutex.Lock() ai.mutex.Lock()
ai.mockHandlePeerAnnounce(peer1Addr, "eth0") ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
ai.Mutex.Unlock() ai.mutex.Unlock()
// Give a small amount of time for the peer to be processed // Give a small amount of time for the peer to be processed
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
ai.Mutex.RLock() ai.mutex.RLock()
peer, exists = ai.peers[peer1AddrStr] peer, exists = ai.peers[peer1AddrStr]
var updatedTime time.Time var updatedTime time.Time
if exists { if exists {
updatedTime = peer.lastHeard updatedTime = peer.lastHeard
} }
ai.Mutex.RUnlock() ai.mutex.RUnlock()
if !exists { if !exists {
t.Fatalf("Peer %s not found after timestamp update", peer1AddrStr) t.Fatalf("Peer %s not found after timestamp update", peer1AddrStr)
@@ -284,9 +279,9 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
// Wait for peer timeout // Wait for peer timeout
time.Sleep(testTimeout * 2) time.Sleep(testTimeout * 2)
ai.Mutex.RLock() ai.mutex.RLock()
count := len(ai.peers) count := len(ai.peers)
ai.Mutex.RUnlock() ai.mutex.RUnlock()
if count != 0 { if count != 0 {
t.Errorf("Expected all peers to timeout, got %d peers", count) t.Errorf("Expected all peers to timeout, got %d peers", count)

View File

@@ -1,16 +1,14 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package interfaces package interfaces
import ( import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"log"
"net" "net"
"sync" "sync"
"time" "time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common" "github.com/Sudo-Ivan/reticulum-go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
) )
const ( const (
@@ -28,6 +26,17 @@ const (
TYPE_TCP = 0x02 TYPE_TCP = 0x02
PROPAGATION_RATE = 0.02 // 2% of interface bandwidth PROPAGATION_RATE = 0.02 // 2% of interface bandwidth
DEBUG_LEVEL = 4 // Default debug level for interface logging
// Debug levels
DEBUG_CRITICAL = 1
DEBUG_ERROR = 2
DEBUG_INFO = 3
DEBUG_VERBOSE = 4
DEBUG_TRACE = 5
DEBUG_PACKETS = 6
DEBUG_ALL = 7
) )
type Interface interface { type Interface interface {
@@ -68,12 +77,9 @@ 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
Mutex sync.RWMutex mutex sync.RWMutex
packetCallback common.PacketCallback packetCallback common.PacketCallback
} }
@@ -89,36 +95,30 @@ 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(),
} }
} }
func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) { func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {
i.Mutex.Lock() i.mutex.Lock()
defer i.Mutex.Unlock() defer i.mutex.Unlock()
i.packetCallback = callback i.packetCallback = callback
} }
func (i *BaseInterface) GetPacketCallback() common.PacketCallback { func (i *BaseInterface) GetPacketCallback() common.PacketCallback {
i.Mutex.RLock() i.mutex.RLock()
defer i.Mutex.RUnlock() defer i.mutex.RUnlock()
return i.packetCallback return i.packetCallback
} }
func (i *BaseInterface) ProcessIncoming(data []byte) { func (i *BaseInterface) ProcessIncoming(data []byte) {
i.Mutex.Lock() i.mutex.Lock()
i.RxBytes += uint64(len(data)) i.RxBytes += uint64(len(data))
i.RxPackets++ i.mutex.Unlock()
i.Mutex.Unlock()
i.Mutex.RLock() i.mutex.RLock()
callback := i.packetCallback callback := i.packetCallback
i.Mutex.RUnlock() i.mutex.RUnlock()
if callback != nil { if callback != nil {
callback(data, i) callback(data, i)
@@ -127,16 +127,15 @@ func (i *BaseInterface) ProcessIncoming(data []byte) {
func (i *BaseInterface) ProcessOutgoing(data []byte) error { func (i *BaseInterface) ProcessOutgoing(data []byte) error {
if !i.Online || i.Detached { if !i.Online || i.Detached {
debug.Log(debug.DEBUG_CRITICAL, "Interface cannot process outgoing packet - interface offline or detached", "name", i.Name) log.Printf("[DEBUG-1] Interface %s: Cannot process outgoing packet - interface offline or detached", i.Name)
return fmt.Errorf("interface offline or detached") return fmt.Errorf("interface offline or detached")
} }
i.Mutex.Lock() i.mutex.Lock()
i.TxBytes += uint64(len(data)) i.TxBytes += uint64(len(data))
i.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) log.Printf("[DEBUG-%d] Interface %s: Processed outgoing packet of %d bytes, total TX: %d", DEBUG_LEVEL, i.Name, len(data), i.TxBytes)
return nil return nil
} }
@@ -146,7 +145,7 @@ func (i *BaseInterface) SendPathRequest(packet []byte) error {
} }
frame := make([]byte, 0, len(packet)+1) frame := make([]byte, 0, len(packet)+1)
frame = append(frame, common.HEX_0x01) frame = append(frame, 0x01)
frame = append(frame, packet...) frame = append(frame, packet...)
return i.ProcessOutgoing(frame) return i.ProcessOutgoing(frame)
@@ -158,7 +157,7 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
} }
frame := make([]byte, 0, len(dest)+len(data)+9) frame := make([]byte, 0, len(dest)+len(data)+9)
frame = append(frame, common.HEX_0x02) frame = append(frame, 0x02)
frame = append(frame, dest...) frame = append(frame, dest...)
ts := make([]byte, 8) ts := make([]byte, 8)
@@ -170,35 +169,35 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
} }
func (i *BaseInterface) Detach() { func (i *BaseInterface) Detach() {
i.Mutex.Lock() i.mutex.Lock()
defer i.Mutex.Unlock() defer i.mutex.Unlock()
i.Detached = true i.Detached = true
i.Online = false i.Online = false
} }
func (i *BaseInterface) IsEnabled() bool { func (i *BaseInterface) IsEnabled() bool {
i.Mutex.RLock() i.mutex.RLock()
defer i.Mutex.RUnlock() defer i.mutex.RUnlock()
return i.Enabled && i.Online && !i.Detached return i.Enabled && i.Online && !i.Detached
} }
func (i *BaseInterface) Enable() { func (i *BaseInterface) Enable() {
i.Mutex.Lock() i.mutex.Lock()
defer i.Mutex.Unlock() defer i.mutex.Unlock()
prevState := i.Enabled prevState := i.Enabled
i.Enabled = true i.Enabled = true
i.Online = true i.Online = true
debug.Log(debug.DEBUG_INFO, "Interface state changed", "name", i.Name, "enabled_prev", prevState, "enabled", i.Enabled, "online_prev", !i.Online, "online", i.Online) log.Printf("[DEBUG-%d] Interface %s: State changed - Enabled: %v->%v, Online: %v->%v", DEBUG_INFO, i.Name, prevState, i.Enabled, !i.Online, i.Online)
} }
func (i *BaseInterface) Disable() { func (i *BaseInterface) Disable() {
i.Mutex.Lock() i.mutex.Lock()
defer i.Mutex.Unlock() defer i.mutex.Unlock()
i.Enabled = false i.Enabled = false
i.Online = false i.Online = false
debug.Log(debug.DEBUG_ERROR, "Interface disabled and offline", "name", i.Name) log.Printf("[DEBUG-2] Interface %s: Disabled and offline", i.Name)
} }
func (i *BaseInterface) GetName() string { func (i *BaseInterface) GetName() string {
@@ -218,41 +217,17 @@ func (i *BaseInterface) GetMTU() int {
} }
func (i *BaseInterface) IsOnline() bool { func (i *BaseInterface) IsOnline() bool {
i.Mutex.RLock() i.mutex.RLock()
defer i.Mutex.RUnlock() defer i.mutex.RUnlock()
return i.Online return i.Online
} }
func (i *BaseInterface) IsDetached() bool { func (i *BaseInterface) IsDetached() bool {
i.Mutex.RLock() i.mutex.RLock()
defer i.Mutex.RUnlock() defer i.mutex.RUnlock()
return i.Detached return i.Detached
} }
func (i *BaseInterface) GetTxBytes() uint64 {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.TxBytes
}
func (i *BaseInterface) GetRxBytes() uint64 {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.RxBytes
}
func (i *BaseInterface) GetTxPackets() uint64 {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.TxPackets
}
func (i *BaseInterface) GetRxPackets() uint64 {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.RxPackets
}
func (i *BaseInterface) Start() error { func (i *BaseInterface) Start() error {
return nil return nil
} }
@@ -262,11 +237,11 @@ func (i *BaseInterface) Stop() error {
} }
func (i *BaseInterface) Send(data []byte, address string) error { func (i *BaseInterface) Send(data []byte, address string) error {
debug.Log(debug.DEBUG_VERBOSE, "Interface sending bytes", "name", i.Name, "bytes", len(data), "address", address) log.Printf("[DEBUG-%d] Interface %s: Sending %d bytes to %s", DEBUG_LEVEL, i.Name, len(data), address)
err := i.ProcessOutgoing(data) err := i.ProcessOutgoing(data)
if err != nil { if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Interface failed to send data", "name", i.Name, "error", err) log.Printf("[DEBUG-1] Interface %s: Failed to send data: %v", i.Name, err)
return err return err
} }
@@ -279,14 +254,14 @@ func (i *BaseInterface) GetConn() net.Conn {
} }
func (i *BaseInterface) GetBandwidthAvailable() bool { func (i *BaseInterface) GetBandwidthAvailable() bool {
i.Mutex.RLock() i.mutex.RLock()
defer i.Mutex.RUnlock() defer i.mutex.RUnlock()
now := time.Now() now := time.Now()
timeSinceLastTx := now.Sub(i.lastTx) timeSinceLastTx := now.Sub(i.lastTx)
if timeSinceLastTx > time.Second { if timeSinceLastTx > time.Second {
debug.Log(debug.DEBUG_VERBOSE, "Interface bandwidth available", "name", i.Name, "idle_seconds", timeSinceLastTx.Seconds()) log.Printf("[DEBUG-%d] Interface %s: Bandwidth available (idle for %.2fs)", DEBUG_VERBOSE, i.Name, timeSinceLastTx.Seconds())
return true return true
} }
@@ -295,18 +270,19 @@ func (i *BaseInterface) GetBandwidthAvailable() bool {
maxUsage := float64(i.Bitrate) * PROPAGATION_RATE maxUsage := float64(i.Bitrate) * PROPAGATION_RATE
available := currentUsage < maxUsage available := currentUsage < maxUsage
debug.Log(debug.DEBUG_VERBOSE, "Interface bandwidth stats", "name", i.Name, "current_bps", currentUsage, "max_bps", maxUsage, "usage_percent", (currentUsage/maxUsage)*100, "available", available) log.Printf("[DEBUG-%d] Interface %s: Bandwidth stats - Current: %.2f bps, Max: %.2f bps, Usage: %.1f%%, Available: %v", DEBUG_VERBOSE, i.Name, currentUsage, maxUsage, (currentUsage/maxUsage)*100, available)
return available return available
} }
func (i *BaseInterface) updateBandwidthStats(bytes uint64) { func (i *BaseInterface) updateBandwidthStats(bytes uint64) {
i.Mutex.Lock() i.mutex.Lock()
defer i.Mutex.Unlock() defer i.mutex.Unlock()
i.TxBytes += bytes
i.lastTx = time.Now() i.lastTx = time.Now()
debug.Log(debug.DEBUG_VERBOSE, "Interface updated bandwidth stats", "name", i.Name, "tx_bytes", i.TxBytes, "last_tx", i.lastTx) log.Printf("[DEBUG-%d] Interface %s: Updated bandwidth stats - TX bytes: %d, Last TX: %v", DEBUG_LEVEL, i.Name, i.TxBytes, i.lastTx)
} }
type InterceptedInterface struct { type InterceptedInterface struct {
@@ -329,7 +305,7 @@ func (i *InterceptedInterface) Send(data []byte, addr string) error {
// Call interceptor if provided // Call interceptor if provided
if i.interceptor != nil && len(data) > 0 { if i.interceptor != nil && len(data) > 0 {
if err := i.interceptor(data, i); err != nil { if err := i.interceptor(data, i); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to intercept outgoing packet", "error", err) log.Printf("[DEBUG-2] Failed to intercept outgoing packet: %v", err)
} }
} }

View File

@@ -7,7 +7,7 @@ import (
"testing" "testing"
"time" "time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common" "github.com/Sudo-Ivan/reticulum-go/pkg/common"
) )
func TestBaseInterfaceStateChanges(t *testing.T) { func TestBaseInterfaceStateChanges(t *testing.T) {
@@ -183,6 +183,7 @@ func (m *mockInterface) Send(data []byte, addr string) error {
return nil return nil
} }
// Add other methods to satisfy the Interface interface (can be minimal/panic)
func (m *mockInterface) GetType() common.InterfaceType { return common.IF_TYPE_NONE } func (m *mockInterface) GetType() common.InterfaceType { return common.IF_TYPE_NONE }
func (m *mockInterface) GetMode() common.InterfaceMode { return common.IF_MODE_FULL } func (m *mockInterface) GetMode() common.InterfaceMode { return common.IF_MODE_FULL }
func (m *mockInterface) ProcessIncoming(data []byte) {} func (m *mockInterface) ProcessIncoming(data []byte) {}

View File

@@ -1,16 +1,14 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package interfaces package interfaces
import ( import (
"fmt" "fmt"
"log"
"net" "net"
"runtime" "runtime"
"sync" "sync"
"time" "time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common" "github.com/Sudo-Ivan/reticulum-go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
) )
const ( const (
@@ -23,26 +21,14 @@ const (
KISS_TFEND = 0xDC KISS_TFEND = 0xDC
KISS_TFESC = 0xDD KISS_TFESC = 0xDD
DEFAULT_MTU = 1064 TCP_USER_TIMEOUT = 24
BITRATE_GUESS_VAL = 10 * 1000 * 1000 TCP_PROBE_AFTER = 5
TCP_PROBE_INTERVAL = 2
TCP_PROBES = 12
RECONNECT_WAIT = 5 RECONNECT_WAIT = 5
INITIAL_TIMEOUT = 5 INITIAL_TIMEOUT = 5
INITIAL_BACKOFF = time.Second INITIAL_BACKOFF = time.Second
MAX_BACKOFF = time.Minute * 5 MAX_BACKOFF = time.Minute * 5
TCP_USER_TIMEOUT_SEC = 24
TCP_PROBE_AFTER_SEC = 5
TCP_PROBE_INTERVAL_SEC = 2
TCP_PROBES_COUNT = 12
TCP_CONNECT_TIMEOUT = 10 * time.Second
TCP_MILLISECONDS = 1000
I2P_USER_TIMEOUT_SEC = 45
I2P_PROBE_AFTER_SEC = 10
I2P_PROBE_INTERVAL_SEC = 9
I2P_PROBES_COUNT = 5
SO_KEEPALIVE_ENABLE = 1
) )
type TCPClientInterface struct { type TCPClientInterface struct {
@@ -59,8 +45,12 @@ type TCPClientInterface struct {
maxReconnectTries int maxReconnectTries int
packetBuffer []byte packetBuffer []byte
packetType byte packetType byte
done chan struct{} mutex sync.RWMutex
stopOnce sync.Once enabled bool
TxBytes uint64
RxBytes uint64
lastTx time.Time
lastRx time.Time
} }
func NewTCPClientInterface(name string, targetHost string, targetPort int, kissFraming bool, i2pTunneled bool, enabled bool) (*TCPClientInterface, error) { func NewTCPClientInterface(name string, targetHost string, targetPort int, kissFraming bool, i2pTunneled bool, enabled bool) (*TCPClientInterface, error) {
@@ -71,10 +61,10 @@ func NewTCPClientInterface(name string, targetHost string, targetPort int, kissF
kissFraming: kissFraming, kissFraming: kissFraming,
i2pTunneled: i2pTunneled, i2pTunneled: i2pTunneled,
initiator: true, initiator: true,
maxReconnectTries: RECONNECT_WAIT * TCP_PROBES_COUNT, enabled: enabled,
maxReconnectTries: TCP_PROBES,
packetBuffer: make([]byte, 0), packetBuffer: make([]byte, 0),
neverConnected: true, neverConnected: true,
done: make(chan struct{}),
} }
if enabled { if enabled {
@@ -92,115 +82,43 @@ func NewTCPClientInterface(name string, targetHost string, targetPort int, kissF
} }
func (tc *TCPClientInterface) Start() error { func (tc *TCPClientInterface) Start() error {
tc.Mutex.Lock() tc.mutex.Lock()
if !tc.Enabled || tc.Detached { defer tc.mutex.Unlock()
tc.Mutex.Unlock()
return fmt.Errorf("interface not enabled or detached") if !tc.Enabled {
return fmt.Errorf("interface not enabled")
} }
if tc.conn != nil { if tc.conn != nil {
tc.Online = true tc.Online = true
go tc.readLoop() go tc.readLoop()
tc.Mutex.Unlock()
return nil return nil
} }
// Only recreate done if it's nil or was closed
select {
case <-tc.done:
tc.done = make(chan struct{})
tc.stopOnce = sync.Once{}
default:
if tc.done == nil {
tc.done = make(chan struct{})
tc.stopOnce = sync.Once{}
}
}
tc.Mutex.Unlock()
addr := net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort)) addr := net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort))
conn, err := net.DialTimeout("tcp", addr, TCP_CONNECT_TIMEOUT) conn, err := net.Dial("tcp", addr)
if err != nil { if err != nil {
return err return err
} }
tc.Mutex.Lock()
tc.conn = conn tc.conn = conn
tc.Mutex.Unlock()
// Set platform-specific timeouts // Set platform-specific timeouts
switch runtime.GOOS { switch runtime.GOOS {
case "linux": case "linux":
if err := tc.setTimeoutsLinux(); err != nil { if err := tc.setTimeoutsLinux(); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to set Linux TCP timeouts", "error", err) log.Printf("[DEBUG-2] Failed to set Linux TCP timeouts: %v", err)
} }
case "darwin": case "darwin":
if err := tc.setTimeoutsOSX(); err != nil { if err := tc.setTimeoutsOSX(); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to set OSX TCP timeouts", "error", err) log.Printf("[DEBUG-2] Failed to set OSX TCP timeouts: %v", err)
} }
} }
tc.Mutex.Lock()
tc.Online = true tc.Online = true
tc.Mutex.Unlock()
go tc.readLoop() go tc.readLoop()
return nil return nil
} }
func (tc *TCPClientInterface) Stop() error {
tc.Mutex.Lock()
tc.Enabled = false
tc.Online = false
if tc.conn != nil {
_ = tc.conn.Close()
tc.conn = nil
}
tc.Mutex.Unlock()
tc.stopOnce.Do(func() {
if tc.done != nil {
close(tc.done)
}
})
return nil
}
func (tc *TCPClientInterface) ProcessOutgoing(data []byte) error {
tc.Mutex.RLock()
online := tc.Online
tc.Mutex.RUnlock()
if !online {
return fmt.Errorf("interface offline")
}
tc.writing = true
defer func() { tc.writing = false }()
// For TCP connections, use HDLC framing
var frame []byte
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
frame = append(frame, HDLC_FLAG)
debug.Log(debug.DEBUG_ALL, "TCP interface writing to network", "name", tc.Name, "bytes", len(frame))
tc.Mutex.RLock()
conn := tc.conn
tc.Mutex.RUnlock()
if conn == nil {
return fmt.Errorf("connection closed")
}
_, err := conn.Write(frame)
if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "TCP interface write failed", "name", tc.Name, "error", err)
}
return err
}
func (tc *TCPClientInterface) readLoop() { func (tc *TCPClientInterface) readLoop() {
buffer := make([]byte, tc.MTU) buffer := make([]byte, tc.MTU)
inFrame := false inFrame := false
@@ -208,30 +126,10 @@ func (tc *TCPClientInterface) readLoop() {
dataBuffer := make([]byte, 0) dataBuffer := make([]byte, 0)
for { for {
tc.Mutex.RLock() n, err := tc.conn.Read(buffer)
conn := tc.conn
done := tc.done
tc.Mutex.RUnlock()
if conn == nil {
return
}
select {
case <-done:
return
default:
}
n, err := conn.Read(buffer)
if err != nil { if err != nil {
tc.Mutex.Lock()
tc.Online = false tc.Online = false
detached := tc.Detached if tc.initiator && !tc.Detached {
initiator := tc.initiator
tc.Mutex.Unlock()
if initiator && !detached {
go tc.reconnect() go tc.reconnect()
} else { } else {
tc.teardown() tc.teardown()
@@ -239,6 +137,9 @@ func (tc *TCPClientInterface) readLoop() {
return return
} }
// Update RX bytes for raw received data
tc.UpdateStats(uint64(n), true) // #nosec G115
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
b := buffer[i] b := buffer[i]
@@ -268,18 +169,66 @@ func (tc *TCPClientInterface) readLoop() {
func (tc *TCPClientInterface) handlePacket(data []byte) { func (tc *TCPClientInterface) handlePacket(data []byte) {
if len(data) < 1 { if len(data) < 1 {
debug.Log(debug.DEBUG_ALL, "Received invalid packet: empty") log.Printf("[DEBUG-7] Received invalid packet: empty")
return return
} }
tc.Mutex.Lock() tc.mutex.Lock()
tc.RxBytes += uint64(len(data))
lastRx := time.Now() lastRx := time.Now()
tc.lastRx = lastRx tc.lastRx = lastRx
tc.Mutex.Unlock() tc.mutex.Unlock()
debug.Log(debug.DEBUG_ALL, "Received packet", "type", fmt.Sprintf("0x%02x", data[0]), "size", len(data)) log.Printf("[DEBUG-7] Received packet: type=0x%02x, size=%d bytes", data[0], len(data))
tc.ProcessIncoming(data) // For RNS packets, call the packet callback directly
if callback := tc.GetPacketCallback(); callback != nil {
log.Printf("[DEBUG-7] Calling packet callback for RNS packet")
callback(data, tc)
} else {
log.Printf("[DEBUG-7] 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 {
log.Printf("[DEBUG-7] TCP interface %s: Sending %d bytes", tc.Name, len(data))
if !tc.IsEnabled() || !tc.IsOnline() {
return fmt.Errorf("TCP interface %s is not online", tc.Name)
}
// For TCP interface, we need to prepend a packet type byte for announce packets
// RNS TCP protocol expects: [packet_type][data]
frame := make([]byte, 0, len(data)+1)
frame = append(frame, 0x01) // Announce packet type
frame = append(frame, data...)
return tc.ProcessOutgoing(frame)
}
func (tc *TCPClientInterface) ProcessOutgoing(data []byte) error {
if !tc.Online {
return fmt.Errorf("interface offline")
}
tc.writing = true
defer func() { tc.writing = false }()
// For TCP connections, use HDLC framing
var frame []byte
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
frame = append(frame, HDLC_FLAG)
// Update TX stats before sending
tc.UpdateStats(uint64(len(frame)), false)
log.Printf("[DEBUG-7] TCP interface %s: Writing %d bytes to network", tc.Name, len(frame))
_, err := tc.conn.Write(frame)
if err != nil {
log.Printf("[DEBUG-1] TCP interface %s: Write failed: %v", tc.Name, err)
}
return err
} }
func (tc *TCPClientInterface) teardown() { func (tc *TCPClientInterface) teardown() {
@@ -287,7 +236,7 @@ func (tc *TCPClientInterface) teardown() {
tc.IN = false tc.IN = false
tc.OUT = false tc.OUT = false
if tc.conn != nil { if tc.conn != nil {
_ = tc.conn.Close() tc.conn.Close() // #nosec G104
} }
} }
@@ -323,9 +272,9 @@ func (tc *TCPClientInterface) SetPacketCallback(cb common.PacketCallback) {
} }
func (tc *TCPClientInterface) IsEnabled() bool { func (tc *TCPClientInterface) IsEnabled() bool {
tc.Mutex.RLock() tc.mutex.RLock()
defer tc.Mutex.RUnlock() defer tc.mutex.RUnlock()
return tc.Enabled && tc.Online && !tc.Detached return tc.enabled && tc.Online && !tc.Detached
} }
func (tc *TCPClientInterface) GetName() string { func (tc *TCPClientInterface) GetName() string {
@@ -333,31 +282,31 @@ func (tc *TCPClientInterface) GetName() string {
} }
func (tc *TCPClientInterface) GetPacketCallback() common.PacketCallback { func (tc *TCPClientInterface) GetPacketCallback() common.PacketCallback {
tc.Mutex.RLock() tc.mutex.RLock()
defer tc.Mutex.RUnlock() defer tc.mutex.RUnlock()
return tc.packetCallback return tc.packetCallback
} }
func (tc *TCPClientInterface) IsDetached() bool { func (tc *TCPClientInterface) IsDetached() bool {
tc.Mutex.RLock() tc.mutex.RLock()
defer tc.Mutex.RUnlock() defer tc.mutex.RUnlock()
return tc.Detached return tc.Detached
} }
func (tc *TCPClientInterface) IsOnline() bool { func (tc *TCPClientInterface) IsOnline() bool {
tc.Mutex.RLock() tc.mutex.RLock()
defer tc.Mutex.RUnlock() defer tc.mutex.RUnlock()
return tc.Online return tc.Online
} }
func (tc *TCPClientInterface) reconnect() { func (tc *TCPClientInterface) reconnect() {
tc.Mutex.Lock() tc.mutex.Lock()
if tc.reconnecting { if tc.reconnecting {
tc.Mutex.Unlock() tc.mutex.Unlock()
return return
} }
tc.reconnecting = true tc.reconnecting = true
tc.Mutex.Unlock() tc.mutex.Unlock()
backoff := time.Second backoff := time.Second
maxBackoff := time.Minute * 5 maxBackoff := time.Minute * 5
@@ -370,19 +319,21 @@ func (tc *TCPClientInterface) reconnect() {
conn, err := net.Dial("tcp", addr) conn, err := net.Dial("tcp", addr)
if err == nil { if err == nil {
tc.Mutex.Lock() tc.mutex.Lock()
tc.conn = conn tc.conn = conn
tc.Online = true tc.Online = true
tc.neverConnected = false tc.neverConnected = false
tc.reconnecting = false tc.reconnecting = false
tc.Mutex.Unlock() tc.mutex.Unlock()
go tc.readLoop() go tc.readLoop()
return return
} }
debug.Log(debug.DEBUG_VERBOSE, "Failed to reconnect", "target", net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort)), "attempt", retries+1, "maxTries", tc.maxReconnectTries, "error", err) // Log reconnection attempt
fmt.Printf("Failed to reconnect to %s (attempt %d/%d): %v\n",
addr, retries+1, tc.maxReconnectTries, err)
// Wait with exponential backoff // Wait with exponential backoff
time.Sleep(backoff) time.Sleep(backoff)
@@ -396,48 +347,50 @@ func (tc *TCPClientInterface) reconnect() {
retries++ retries++
} }
tc.Mutex.Lock() tc.mutex.Lock()
tc.reconnecting = false tc.reconnecting = false
tc.Mutex.Unlock() tc.mutex.Unlock()
// If we've exhausted all retries, perform final teardown
tc.teardown() tc.teardown()
debug.Log(debug.DEBUG_ERROR, "Failed to reconnect after all attempts", "target", net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort)), "maxTries", tc.maxReconnectTries) fmt.Printf("Failed to reconnect to %s after %d attempts\n",
fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort), tc.maxReconnectTries)
} }
func (tc *TCPClientInterface) Enable() { func (tc *TCPClientInterface) Enable() {
tc.Mutex.Lock() tc.mutex.Lock()
defer tc.Mutex.Unlock() defer tc.mutex.Unlock()
tc.Online = true tc.Online = true
} }
func (tc *TCPClientInterface) Disable() { func (tc *TCPClientInterface) Disable() {
tc.Mutex.Lock() tc.mutex.Lock()
defer tc.Mutex.Unlock() defer tc.mutex.Unlock()
tc.Online = false tc.Online = false
} }
func (tc *TCPClientInterface) IsConnected() bool { func (tc *TCPClientInterface) IsConnected() bool {
tc.Mutex.RLock() tc.mutex.RLock()
defer tc.Mutex.RUnlock() defer tc.mutex.RUnlock()
return tc.conn != nil && tc.Online && !tc.reconnecting return tc.conn != nil && tc.Online && !tc.reconnecting
} }
func (tc *TCPClientInterface) GetRTT() time.Duration { func (tc *TCPClientInterface) GetRTT() time.Duration {
tc.Mutex.RLock() tc.mutex.RLock()
defer tc.Mutex.RUnlock() defer tc.mutex.RUnlock()
if !tc.IsConnected() { if !tc.IsConnected() {
return 0 return 0
} }
if tcpConn, ok := tc.conn.(*net.TCPConn); ok { if tcpConn, ok := tc.conn.(*net.TCPConn); ok {
var rtt time.Duration var rtt time.Duration = 0
if runtime.GOOS == "linux" { if runtime.GOOS == "linux" {
if info, err := tcpConn.SyscallConn(); err == nil { if info, err := tcpConn.SyscallConn(); err == nil {
if err := info.Control(func(fd uintptr) { // #nosec G104 if err := info.Control(func(fd uintptr) { // #nosec G104
rtt = platformGetRTT(fd) rtt = platformGetRTT(fd)
}); err != nil { }); err != nil {
debug.Log(debug.DEBUG_ERROR, "Error in SyscallConn Control", "error", err) log.Printf("[DEBUG-2] Error in SyscallConn Control: %v", err)
} }
} }
} }
@@ -447,17 +400,85 @@ 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
log.Printf("[DEBUG-5] Interface %s RX stats: bytes=%d total=%d last=%v",
tc.Name, bytes, tc.RxBytes, tc.lastRx)
} else {
tc.TxBytes += bytes
tc.lastTx = now
log.Printf("[DEBUG-5] Interface %s TX stats: bytes=%d total=%d last=%v",
tc.Name, bytes, tc.TxBytes, 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
}
func (tc *TCPClientInterface) setTimeoutsLinux() error {
tcpConn, ok := tc.conn.(*net.TCPConn)
if !ok {
return fmt.Errorf("not a TCP connection")
}
if !tc.i2pTunneled {
if err := tcpConn.SetKeepAlive(true); err != nil {
return err
}
if err := tcpConn.SetKeepAlivePeriod(time.Duration(TCP_PROBE_INTERVAL) * time.Second); err != nil {
return err
}
}
return nil
}
func (tc *TCPClientInterface) setTimeoutsOSX() error {
tcpConn, ok := tc.conn.(*net.TCPConn)
if !ok {
return fmt.Errorf("not a TCP connection")
}
if err := tcpConn.SetKeepAlive(true); err != nil {
return err
}
return nil
}
type TCPServerInterface struct { type TCPServerInterface struct {
BaseInterface BaseInterface
connections map[string]net.Conn connections map[string]net.Conn
listener net.Listener mutex sync.RWMutex
bindAddr string bindAddr string
bindPort int bindPort int
preferIPv6 bool preferIPv6 bool
kissFraming bool kissFraming bool
i2pTunneled bool i2pTunneled bool
done chan struct{} packetCallback common.PacketCallback
stopOnce sync.Once TxBytes uint64
RxBytes uint64
} }
func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFraming bool, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) { func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFraming bool, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) {
@@ -468,7 +489,6 @@ func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFrami
Type: common.IF_TYPE_TCP, Type: common.IF_TYPE_TCP,
Online: false, Online: false,
MTU: common.DEFAULT_MTU, MTU: common.DEFAULT_MTU,
Enabled: true,
Detached: false, Detached: false,
}, },
connections: make(map[string]net.Conn), connections: make(map[string]net.Conn),
@@ -477,7 +497,6 @@ func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFrami
preferIPv6: preferIPv6, preferIPv6: preferIPv6,
kissFraming: kissFraming, kissFraming: kissFraming,
i2pTunneled: i2pTunneled, i2pTunneled: i2pTunneled,
done: make(chan struct{}),
} }
return ts, nil return ts, nil
@@ -496,21 +515,21 @@ func (ts *TCPServerInterface) String() string {
} }
func (ts *TCPServerInterface) SetPacketCallback(callback common.PacketCallback) { func (ts *TCPServerInterface) SetPacketCallback(callback common.PacketCallback) {
ts.Mutex.Lock() ts.mutex.Lock()
defer ts.Mutex.Unlock() defer ts.mutex.Unlock()
ts.packetCallback = callback ts.packetCallback = callback
} }
func (ts *TCPServerInterface) GetPacketCallback() common.PacketCallback { func (ts *TCPServerInterface) GetPacketCallback() common.PacketCallback {
ts.Mutex.RLock() ts.mutex.RLock()
defer ts.Mutex.RUnlock() defer ts.mutex.RUnlock()
return ts.packetCallback return ts.packetCallback
} }
func (ts *TCPServerInterface) IsEnabled() bool { func (ts *TCPServerInterface) IsEnabled() bool {
ts.Mutex.RLock() ts.mutex.RLock()
defer ts.Mutex.RUnlock() defer ts.mutex.RUnlock()
return ts.Enabled && ts.Online && !ts.Detached return ts.BaseInterface.Enabled && ts.BaseInterface.Online && !ts.BaseInterface.Detached
} }
func (ts *TCPServerInterface) GetName() string { func (ts *TCPServerInterface) GetName() string {
@@ -518,81 +537,50 @@ func (ts *TCPServerInterface) GetName() string {
} }
func (ts *TCPServerInterface) IsDetached() bool { func (ts *TCPServerInterface) IsDetached() bool {
ts.Mutex.RLock() ts.mutex.RLock()
defer ts.Mutex.RUnlock() defer ts.mutex.RUnlock()
return ts.Detached return ts.BaseInterface.Detached
} }
func (ts *TCPServerInterface) IsOnline() bool { func (ts *TCPServerInterface) IsOnline() bool {
ts.Mutex.RLock() ts.mutex.RLock()
defer ts.Mutex.RUnlock() defer ts.mutex.RUnlock()
return ts.Online return ts.Online
} }
func (ts *TCPServerInterface) Enable() { func (ts *TCPServerInterface) Enable() {
ts.Mutex.Lock() ts.mutex.Lock()
defer ts.Mutex.Unlock() defer ts.mutex.Unlock()
ts.Online = true ts.Online = true
} }
func (ts *TCPServerInterface) Disable() { func (ts *TCPServerInterface) Disable() {
ts.Mutex.Lock() ts.mutex.Lock()
defer ts.Mutex.Unlock() defer ts.mutex.Unlock()
ts.Online = false ts.Online = false
} }
func (ts *TCPServerInterface) Start() error { func (ts *TCPServerInterface) Start() error {
ts.Mutex.Lock() ts.mutex.Lock()
if ts.listener != nil { defer ts.mutex.Unlock()
ts.Mutex.Unlock()
return fmt.Errorf("TCP server already started")
}
// Only recreate done if it's nil or was closed
select {
case <-ts.done:
ts.done = make(chan struct{})
ts.stopOnce = sync.Once{}
default:
if ts.done == nil {
ts.done = make(chan struct{})
ts.stopOnce = sync.Once{}
}
}
ts.Mutex.Unlock()
addr := net.JoinHostPort(ts.bindAddr, fmt.Sprintf("%d", ts.bindPort)) addr := fmt.Sprintf("%s:%d", ts.bindAddr, ts.bindPort)
listener, err := net.Listen("tcp", addr) listener, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
return fmt.Errorf("failed to start TCP server: %w", err) return fmt.Errorf("failed to start TCP server: %w", err)
} }
ts.Mutex.Lock()
ts.listener = listener
ts.Online = true ts.Online = true
ts.Mutex.Unlock()
// Accept connections in a goroutine // Accept connections in a goroutine
go func() { go func() {
for { for {
ts.Mutex.RLock()
done := ts.done
ts.Mutex.RUnlock()
select {
case <-done:
return
default:
}
conn, err := listener.Accept() conn, err := listener.Accept()
if err != nil { if err != nil {
ts.Mutex.RLock() if !ts.Online {
online := ts.Online
ts.Mutex.RUnlock()
if !online {
return // Normal shutdown return // Normal shutdown
} }
debug.Log(debug.DEBUG_ERROR, "Error accepting connection", "error", err) log.Printf("[DEBUG-2] Error accepting connection: %v", err)
continue continue
} }
@@ -605,68 +593,60 @@ func (ts *TCPServerInterface) Start() error {
} }
func (ts *TCPServerInterface) Stop() error { func (ts *TCPServerInterface) Stop() error {
ts.Mutex.Lock() ts.mutex.Lock()
defer ts.mutex.Unlock()
ts.Online = false ts.Online = false
if ts.listener != nil {
_ = ts.listener.Close()
ts.listener = nil
}
// Close all client connections
for addr, conn := range ts.connections {
_ = conn.Close()
delete(ts.connections, addr)
}
ts.Mutex.Unlock()
ts.stopOnce.Do(func() {
if ts.done != nil {
close(ts.done)
}
})
return nil return nil
} }
func (ts *TCPServerInterface) GetTxBytes() uint64 {
ts.mutex.RLock()
defer ts.mutex.RUnlock()
return ts.TxBytes
}
func (ts *TCPServerInterface) GetRxBytes() uint64 {
ts.mutex.RLock()
defer ts.mutex.RUnlock()
return ts.RxBytes
}
func (ts *TCPServerInterface) handleConnection(conn net.Conn) { func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
addr := conn.RemoteAddr().String() addr := conn.RemoteAddr().String()
ts.Mutex.Lock() ts.mutex.Lock()
ts.connections[addr] = conn ts.connections[addr] = conn
ts.Mutex.Unlock() ts.mutex.Unlock()
defer func() { defer func() {
ts.Mutex.Lock() ts.mutex.Lock()
delete(ts.connections, addr) delete(ts.connections, addr)
ts.Mutex.Unlock() ts.mutex.Unlock()
_ = conn.Close() conn.Close() // #nosec G104
}() }()
buffer := make([]byte, ts.MTU) buffer := make([]byte, ts.MTU)
for { for {
ts.Mutex.RLock()
done := ts.done
ts.Mutex.RUnlock()
select {
case <-done:
return
default:
}
n, err := conn.Read(buffer) n, err := conn.Read(buffer)
if err != nil { if err != nil {
return return
} }
ts.ProcessIncoming(buffer[:n]) ts.mutex.Lock()
ts.RxBytes += uint64(n) // #nosec G115
ts.mutex.Unlock()
if ts.packetCallback != nil {
ts.packetCallback(buffer[:n], ts)
}
} }
} }
func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error { func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
ts.Mutex.RLock() ts.mutex.RLock()
online := ts.Online defer ts.mutex.RUnlock()
ts.Mutex.RUnlock()
if !online { if !ts.Online {
return fmt.Errorf("interface offline") return fmt.Errorf("interface offline")
} }
@@ -679,16 +659,12 @@ func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
frame = append(frame, HDLC_FLAG) frame = append(frame, HDLC_FLAG)
} }
ts.Mutex.Lock() ts.TxBytes += uint64(len(frame))
conns := make([]net.Conn, 0, len(ts.connections))
for _, conn := range ts.connections {
conns = append(conns, conn)
}
ts.Mutex.Unlock()
for _, conn := range conns { for _, conn := range ts.connections {
if _, err := conn.Write(frame); err != nil { if _, err := conn.Write(frame); err != nil {
debug.Log(debug.DEBUG_VERBOSE, "Error writing to connection", "address", conn.RemoteAddr(), "error", err) log.Printf("[DEBUG-4] Error writing to connection %s: %v",
conn.RemoteAddr(), err)
} }
} }

View File

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build !linux //go:build !linux
// +build !linux // +build !linux

View File

@@ -1,61 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build darwin
// +build darwin
package interfaces
import (
"fmt"
"net"
"syscall"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
)
func (tc *TCPClientInterface) setTimeoutsLinux() error {
return tc.setTimeoutsOSX()
}
func (tc *TCPClientInterface) setTimeoutsOSX() error {
tcpConn, ok := tc.conn.(*net.TCPConn)
if !ok {
return fmt.Errorf("not a TCP connection")
}
rawConn, err := tcpConn.SyscallConn()
if err != nil {
return fmt.Errorf("failed to get raw connection: %v", err)
}
var sockoptErr error
err = rawConn.Control(func(fd uintptr) {
const TCP_KEEPALIVE = 0x10
var probeAfter int
if tc.i2pTunneled {
probeAfter = I2P_PROBE_AFTER_SEC
} else {
probeAfter = TCP_PROBE_AFTER_SEC
}
if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, SO_KEEPALIVE_ENABLE); err != nil {
sockoptErr = fmt.Errorf("failed to enable SO_KEEPALIVE: %v", err)
return
}
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_KEEPALIVE, probeAfter); err != nil {
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_KEEPALIVE", "error", err)
}
})
if err != nil {
return fmt.Errorf("control failed: %v", err)
}
if sockoptErr != nil {
return sockoptErr
}
debug.Log(debug.DEBUG_VERBOSE, "TCP keepalive configured (OSX)", "i2p", tc.i2pTunneled)
return nil
}

View File

@@ -1,41 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build freebsd
// +build freebsd
package interfaces
import (
"fmt"
"net"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
)
func (tc *TCPClientInterface) setTimeoutsLinux() error {
tcpConn, ok := tc.conn.(*net.TCPConn)
if !ok {
return fmt.Errorf("not a TCP connection")
}
if err := tcpConn.SetKeepAlive(true); err != nil {
return fmt.Errorf("failed to enable keepalive: %v", err)
}
keepalivePeriod := TCP_PROBE_INTERVAL_SEC * time.Second
if tc.i2pTunneled {
keepalivePeriod = I2P_PROBE_INTERVAL_SEC * time.Second
}
if err := tcpConn.SetKeepAlivePeriod(keepalivePeriod); err != nil {
debug.Log(debug.DEBUG_VERBOSE, "Failed to set keepalive period", "error", err)
}
debug.Log(debug.DEBUG_VERBOSE, "TCP keepalive configured (FreeBSD)", "i2p", tc.i2pTunneled)
return nil
}
func (tc *TCPClientInterface) setTimeoutsOSX() error {
return tc.setTimeoutsLinux()
}

View File

@@ -1,111 +1,32 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build linux //go:build linux
// +build linux // +build linux
package interfaces package interfaces
import ( import (
"fmt"
"net"
"syscall" "syscall"
"time" "time"
"unsafe" "unsafe"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
) )
func (tc *TCPClientInterface) setTimeoutsLinux() error {
tcpConn, ok := tc.conn.(*net.TCPConn)
if !ok {
return fmt.Errorf("not a TCP connection")
}
rawConn, err := tcpConn.SyscallConn()
if err != nil {
return fmt.Errorf("failed to get raw connection: %v", err)
}
var sockoptErr error
err = rawConn.Control(func(fd uintptr) {
var userTimeout, probeAfter, probeInterval, probeCount int
if tc.i2pTunneled {
userTimeout = I2P_USER_TIMEOUT_SEC * TCP_MILLISECONDS
probeAfter = I2P_PROBE_AFTER_SEC
probeInterval = I2P_PROBE_INTERVAL_SEC
probeCount = I2P_PROBES_COUNT
} else {
userTimeout = TCP_USER_TIMEOUT_SEC * TCP_MILLISECONDS
probeAfter = TCP_PROBE_AFTER_SEC
probeInterval = TCP_PROBE_INTERVAL_SEC
probeCount = TCP_PROBES_COUNT
}
const TCP_USER_TIMEOUT = 18
const TCP_KEEPIDLE = 4
const TCP_KEEPINTVL = 5
const TCP_KEEPCNT = 6
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_USER_TIMEOUT, userTimeout); err != nil {
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_USER_TIMEOUT", "error", err)
}
if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, SO_KEEPALIVE_ENABLE); err != nil {
sockoptErr = fmt.Errorf("failed to enable SO_KEEPALIVE: %v", err)
return
}
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_KEEPIDLE, probeAfter); err != nil {
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_KEEPIDLE", "error", err)
}
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_KEEPINTVL, probeInterval); err != nil {
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_KEEPINTVL", "error", err)
}
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_KEEPCNT, probeCount); err != nil {
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_KEEPCNT", "error", err)
}
})
if err != nil {
return fmt.Errorf("control failed: %v", err)
}
if sockoptErr != nil {
return sockoptErr
}
debug.Log(debug.DEBUG_VERBOSE, "TCP keepalive configured (Linux)", "i2p", tc.i2pTunneled)
return nil
}
func (tc *TCPClientInterface) setTimeoutsOSX() error {
return tc.setTimeoutsLinux()
}
func platformGetRTT(fd uintptr) time.Duration { func platformGetRTT(fd uintptr) time.Duration {
var info syscall.TCPInfo var info syscall.TCPInfo
// bearer:disable go_gosec_unsafe_unsafe size := uint32(syscall.SizeofTCPInfo)
infoLen := uint32(unsafe.Sizeof(info))
const TCP_INFO = 11 _, _, err := syscall.Syscall6(
// #nosec G103
_, _, errno := syscall.Syscall6(
syscall.SYS_GETSOCKOPT, syscall.SYS_GETSOCKOPT,
fd, fd,
syscall.IPPROTO_TCP, syscall.SOL_TCP,
TCP_INFO, syscall.TCP_INFO,
// bearer:disable go_gosec_unsafe_unsafe uintptr(unsafe.Pointer(&info)), // #nosec G103
uintptr(unsafe.Pointer(&info)), uintptr(unsafe.Pointer(&size)), // #nosec G103
// bearer:disable go_gosec_unsafe_unsafe
uintptr(unsafe.Pointer(&infoLen)),
0, 0,
) )
if errno != 0 { if err != 0 {
return 0 return 0
} }
// RTT is in microseconds, convert to Duration
return time.Duration(info.Rtt) * time.Microsecond return time.Duration(info.Rtt) * time.Microsecond
} }

View File

@@ -1,41 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build netbsd
// +build netbsd
package interfaces
import (
"fmt"
"net"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
)
func (tc *TCPClientInterface) setTimeoutsLinux() error {
tcpConn, ok := tc.conn.(*net.TCPConn)
if !ok {
return fmt.Errorf("not a TCP connection")
}
if err := tcpConn.SetKeepAlive(true); err != nil {
return fmt.Errorf("failed to enable keepalive: %v", err)
}
keepalivePeriod := TCP_PROBE_INTERVAL_SEC * time.Second
if tc.i2pTunneled {
keepalivePeriod = I2P_PROBE_INTERVAL_SEC * time.Second
}
if err := tcpConn.SetKeepAlivePeriod(keepalivePeriod); err != nil {
debug.Log(debug.DEBUG_VERBOSE, "Failed to set keepalive period", "error", err)
}
debug.Log(debug.DEBUG_VERBOSE, "TCP keepalive configured (NetBSD)", "i2p", tc.i2pTunneled)
return nil
}
func (tc *TCPClientInterface) setTimeoutsOSX() error {
return tc.setTimeoutsLinux()
}

View File

@@ -1,41 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build openbsd
// +build openbsd
package interfaces
import (
"fmt"
"net"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
)
func (tc *TCPClientInterface) setTimeoutsLinux() error {
tcpConn, ok := tc.conn.(*net.TCPConn)
if !ok {
return fmt.Errorf("not a TCP connection")
}
if err := tcpConn.SetKeepAlive(true); err != nil {
return fmt.Errorf("failed to enable keepalive: %v", err)
}
keepalivePeriod := TCP_PROBE_INTERVAL_SEC * time.Second
if tc.i2pTunneled {
keepalivePeriod = I2P_PROBE_INTERVAL_SEC * time.Second
}
if err := tcpConn.SetKeepAlivePeriod(keepalivePeriod); err != nil {
debug.Log(debug.DEBUG_VERBOSE, "Failed to set keepalive period", "error", err)
}
debug.Log(debug.DEBUG_VERBOSE, "TCP keepalive configured (OpenBSD)", "i2p", tc.i2pTunneled)
return nil
}
func (tc *TCPClientInterface) setTimeoutsOSX() error {
return tc.setTimeoutsLinux()
}

View File

@@ -1,14 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build js && wasm
// +build js,wasm
package interfaces
func (tc *TCPClientInterface) setTimeoutsLinux() error {
return nil
}
func (tc *TCPClientInterface) setTimeoutsOSX() error {
return nil
}

View File

@@ -1,42 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package interfaces
import (
"fmt"
"net"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
)
func (tc *TCPClientInterface) setTimeoutsLinux() error {
return tc.setTimeoutsWindows()
}
func (tc *TCPClientInterface) setTimeoutsOSX() error {
return tc.setTimeoutsWindows()
}
func (tc *TCPClientInterface) setTimeoutsWindows() error {
tcpConn, ok := tc.conn.(*net.TCPConn)
if !ok {
return fmt.Errorf("not a TCP connection")
}
if err := tcpConn.SetKeepAlive(true); err != nil {
return fmt.Errorf("failed to enable keepalive: %v", err)
}
keepalivePeriod := TCP_PROBE_INTERVAL_SEC * time.Second
if tc.i2pTunneled {
keepalivePeriod = I2P_PROBE_INTERVAL_SEC * time.Second
}
if err := tcpConn.SetKeepAlivePeriod(keepalivePeriod); err != nil {
debug.Log(debug.DEBUG_VERBOSE, "Failed to set keepalive period", "error", err)
}
debug.Log(debug.DEBUG_VERBOSE, "TCP keepalive configured (Windows)", "i2p", tc.i2pTunneled)
return nil
}

View File

@@ -1,14 +1,12 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package interfaces package interfaces
import ( import (
"fmt" "fmt"
"log"
"net" "net"
"sync" "sync"
"git.quad4.io/Networks/Reticulum-Go/pkg/common" "github.com/Sudo-Ivan/reticulum-go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
) )
type UDPInterface struct { type UDPInterface struct {
@@ -16,9 +14,8 @@ type UDPInterface struct {
conn *net.UDPConn conn *net.UDPConn
addr *net.UDPAddr addr *net.UDPAddr
targetAddr *net.UDPAddr targetAddr *net.UDPAddr
mutex sync.RWMutex
readBuffer []byte readBuffer []byte
done chan struct{}
stopOnce sync.Once
} }
func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) { func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) {
@@ -39,12 +36,9 @@ func NewUDPInterface(name string, addr string, target string, enabled bool) (*UD
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled), BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
addr: udpAddr, addr: udpAddr,
targetAddr: targetAddr, targetAddr: targetAddr,
readBuffer: make([]byte, common.NUM_1064), readBuffer: make([]byte, common.DEFAULT_MTU),
done: make(chan struct{}),
} }
ui.MTU = common.NUM_1064
return ui, nil return ui, nil
} }
@@ -61,41 +55,60 @@ func (ui *UDPInterface) GetMode() common.InterfaceMode {
} }
func (ui *UDPInterface) IsOnline() bool { func (ui *UDPInterface) IsOnline() bool {
ui.Mutex.RLock() ui.mutex.RLock()
defer ui.Mutex.RUnlock() defer ui.mutex.RUnlock()
return ui.Online return ui.Online
} }
func (ui *UDPInterface) IsDetached() bool { func (ui *UDPInterface) IsDetached() bool {
ui.Mutex.RLock() ui.mutex.RLock()
defer ui.Mutex.RUnlock() defer ui.mutex.RUnlock()
return ui.Detached return ui.Detached
} }
func (ui *UDPInterface) Detach() { func (ui *UDPInterface) Detach() {
ui.Mutex.Lock() ui.mutex.Lock()
defer ui.Mutex.Unlock() defer ui.mutex.Unlock()
ui.Detached = true ui.Detached = true
ui.Online = false
if ui.conn != nil { if ui.conn != nil {
ui.conn.Close() // #nosec G104 ui.conn.Close() // #nosec G104
} }
ui.stopOnce.Do(func() {
if ui.done != nil {
close(ui.done)
} }
})
func (ui *UDPInterface) Send(data []byte, addr string) error {
log.Printf("[DEBUG-7] UDP interface %s: Sending %d bytes", ui.Name, len(data))
if !ui.IsEnabled() {
return fmt.Errorf("interface not enabled")
}
if ui.targetAddr == nil {
return fmt.Errorf("no target address configured")
}
// Update TX stats before sending
ui.mutex.Lock()
ui.TxBytes += uint64(len(data))
ui.mutex.Unlock()
_, err := ui.conn.WriteTo(data, ui.targetAddr)
if err != nil {
log.Printf("[DEBUG-1] UDP interface %s: Write failed: %v", ui.Name, err)
} else {
log.Printf("[DEBUG-7] UDP interface %s: Sent %d bytes successfully", ui.Name, len(data))
}
return err
} }
func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) { func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) {
ui.Mutex.Lock() ui.mutex.Lock()
defer ui.Mutex.Unlock() defer ui.mutex.Unlock()
ui.packetCallback = callback ui.packetCallback = callback
} }
func (ui *UDPInterface) GetPacketCallback() common.PacketCallback { func (ui *UDPInterface) GetPacketCallback() common.PacketCallback {
ui.Mutex.RLock() ui.mutex.RLock()
defer ui.Mutex.RUnlock() defer ui.mutex.RUnlock()
return ui.packetCallback return ui.packetCallback
} }
@@ -119,6 +132,10 @@ 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
} }
@@ -127,14 +144,14 @@ func (ui *UDPInterface) GetConn() net.Conn {
} }
func (ui *UDPInterface) GetTxBytes() uint64 { func (ui *UDPInterface) GetTxBytes() uint64 {
ui.Mutex.RLock() ui.mutex.RLock()
defer ui.Mutex.RUnlock() defer ui.mutex.RUnlock()
return ui.TxBytes return ui.TxBytes
} }
func (ui *UDPInterface) GetRxBytes() uint64 { func (ui *UDPInterface) GetRxBytes() uint64 {
ui.Mutex.RLock() ui.mutex.RLock()
defer ui.Mutex.RUnlock() defer ui.mutex.RUnlock()
return ui.RxBytes return ui.RxBytes
} }
@@ -147,56 +164,24 @@ func (ui *UDPInterface) GetBitrate() int {
} }
func (ui *UDPInterface) Enable() { func (ui *UDPInterface) Enable() {
ui.Mutex.Lock() ui.mutex.Lock()
defer ui.Mutex.Unlock() defer ui.mutex.Unlock()
ui.Online = true ui.Online = true
} }
func (ui *UDPInterface) Disable() { func (ui *UDPInterface) Disable() {
ui.Mutex.Lock() ui.mutex.Lock()
defer ui.Mutex.Unlock() defer ui.mutex.Unlock()
ui.Online = false ui.Online = false
} }
func (ui *UDPInterface) Start() error { func (ui *UDPInterface) Start() error {
ui.Mutex.Lock()
if ui.conn != nil {
ui.Mutex.Unlock()
return fmt.Errorf("UDP interface already started")
}
// Only recreate done if it's nil or was closed
select {
case <-ui.done:
ui.done = make(chan struct{})
ui.stopOnce = sync.Once{}
default:
if ui.done == nil {
ui.done = make(chan struct{})
ui.stopOnce = sync.Once{}
}
}
ui.Mutex.Unlock()
conn, err := net.ListenUDP("udp", ui.addr) conn, err := net.ListenUDP("udp", ui.addr)
if err != nil { if err != nil {
return err return err
} }
ui.conn = conn ui.conn = conn
// Enable broadcast mode if we have a target address
if ui.targetAddr != nil {
// Get the raw connection file descriptor to set SO_BROADCAST
if err := conn.SetReadBuffer(common.NUM_1064); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to set read buffer size", "error", err)
}
if err := conn.SetWriteBuffer(common.NUM_1064); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to set write buffer size", "error", err)
}
}
ui.Mutex.Lock()
ui.Online = true ui.Online = true
ui.Mutex.Unlock()
// Start the read loop in a goroutine // Start the read loop in a goroutine
go ui.readLoop() go ui.readLoop()
@@ -204,56 +189,51 @@ func (ui *UDPInterface) Start() error {
return nil return nil
} }
func (ui *UDPInterface) Stop() error {
ui.Detach()
return nil
}
func (ui *UDPInterface) readLoop() { func (ui *UDPInterface) readLoop() {
buffer := make([]byte, common.NUM_1064) buffer := make([]byte, common.DEFAULT_MTU)
for { for ui.IsOnline() && !ui.IsDetached() {
ui.Mutex.RLock() n, remoteAddr, err := ui.conn.ReadFromUDP(buffer)
online := ui.Online
detached := ui.Detached
conn := ui.conn
done := ui.done
ui.Mutex.RUnlock()
if !online || detached || conn == nil {
return
}
select {
case <-done:
return
default:
}
n, remoteAddr, err := conn.ReadFromUDP(buffer)
if err != nil { if err != nil {
ui.Mutex.RLock() if ui.IsOnline() {
stillOnline := ui.Online log.Printf("Error reading from UDP interface %s: %v", ui.Name, err)
ui.Mutex.RUnlock()
if stillOnline {
debug.Log(debug.DEBUG_ERROR, "Error reading from UDP interface", "name", ui.Name, "error", err)
} }
return return
} }
ui.Mutex.Lock() ui.mutex.Lock()
// 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()) log.Printf("[DEBUG-7] UDP interface %s discovered peer %s", ui.Name, remoteAddr)
ui.targetAddr = remoteAddr ui.targetAddr = remoteAddr
} }
ui.Mutex.Unlock() ui.mutex.Unlock()
ui.ProcessIncoming(buffer[:n]) if ui.packetCallback != nil {
ui.packetCallback(buffer[:n], ui)
} }
} }
}
/*
func (ui *UDPInterface) readLoop() {
buffer := make([]byte, ui.MTU)
for {
n, _, err := ui.conn.ReadFromUDP(buffer)
if err != nil {
if ui.Online {
log.Printf("Error reading from UDP interface %s: %v", ui.Name, err)
ui.Stop() // Consider if stopping is the right action or just log and continue
}
return
}
if ui.packetCallback != nil {
ui.packetCallback(buffer[:n], ui)
}
}
}
*/
func (ui *UDPInterface) IsEnabled() bool { func (ui *UDPInterface) IsEnabled() bool {
ui.Mutex.RLock() ui.mutex.RLock()
defer ui.Mutex.RUnlock() defer ui.mutex.RUnlock()
return ui.Enabled && ui.Online && !ui.Detached return ui.Enabled && ui.Online && !ui.Detached
} }

View File

@@ -3,7 +3,7 @@ package interfaces
import ( import (
"testing" "testing"
"git.quad4.io/Networks/Reticulum-Go/pkg/common" "github.com/Sudo-Ivan/reticulum-go/pkg/common"
) )
func TestNewUDPInterface(t *testing.T) { func TestNewUDPInterface(t *testing.T) {
@@ -25,6 +25,11 @@ func TestNewUDPInterface(t *testing.T) {
if ui.GetType() != common.IF_TYPE_UDP { if ui.GetType() != common.IF_TYPE_UDP {
t.Errorf("GetType() = %v; want %v", ui.GetType(), common.IF_TYPE_UDP) t.Errorf("GetType() = %v; want %v", ui.GetType(), common.IF_TYPE_UDP)
} }
if ui.addr.String() != validAddr && ui.addr.Port == 0 { // Check if address resolved, port 0 is special
// Allow OS-assigned port if 0 was specified
} else if ui.addr.String() != validAddr {
// t.Errorf("Resolved addr = %s; want %s", ui.addr.String(), validAddr) //This check is flaky with port 0
}
if ui.targetAddr.String() != validTarget { if ui.targetAddr.String() != validTarget {
t.Errorf("Resolved targetAddr = %s; want %s", ui.targetAddr.String(), validTarget) t.Errorf("Resolved targetAddr = %s; want %s", ui.targetAddr.String(), validTarget)
} }
@@ -66,6 +71,7 @@ func TestNewUDPInterface(t *testing.T) {
func TestUDPInterfaceState(t *testing.T) { func TestUDPInterfaceState(t *testing.T) {
// Basic state tests are covered by BaseInterface tests // Basic state tests are covered by BaseInterface tests
// Add specific UDP ones if needed, e.g., involving the conn
addr := "127.0.0.1:0" addr := "127.0.0.1:0"
ui, _ := NewUDPInterface("udpState", addr, "", true) ui, _ := NewUDPInterface("udpState", addr, "", true)

View File

@@ -1,714 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build !js
// +build !js
// WebSocketInterface is a native implementation of the WebSocket interface.
// It is used to connect to the WebSocket server and send/receive data.
package interfaces
import (
"bufio"
"crypto/rand"
// bearer:disable go_gosec_blocklist_sha1
"crypto/sha1" // #nosec G505
"crypto/tls"
"encoding/base64"
"encoding/binary"
"fmt"
"io"
"math"
"net"
"net/http"
"net/url"
"strings"
"sync"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
)
const (
wsGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
WS_BUFFER_SIZE = 4096
WS_MTU = 1064
WS_BITRATE = 10000000
WS_HTTPS_PORT = 443
WS_HTTP_PORT = 80
WS_VERSION = "13"
WS_CONNECT_TIMEOUT = 10 * time.Second
WS_RECONNECT_DELAY = 2 * time.Second
WS_KEY_SIZE = 16
WS_MASK_KEY_SIZE = 4
WS_HEADER_SIZE = 2
WS_PAYLOAD_LEN_16BIT = 126
WS_PAYLOAD_LEN_64BIT = 127
WS_MAX_PAYLOAD_16BIT = 65536
WS_FRAME_HEADER_FIN = 0x80
WS_FRAME_HEADER_OPCODE = 0x0F
WS_FRAME_HEADER_MASKED = 0x80
WS_FRAME_HEADER_LEN = 0x7F
WS_OPCODE_CONTINUATION = 0x00
WS_OPCODE_TEXT = 0x01
WS_OPCODE_BINARY = 0x02
WS_OPCODE_CLOSE = 0x08
WS_OPCODE_PING = 0x09
WS_OPCODE_PONG = 0x0A
)
type WebSocketInterface struct {
BaseInterface
wsURL string
conn net.Conn
reader *bufio.Reader
connected bool
messageQueue [][]byte
readBuffer []byte
writeBuffer []byte
done chan struct{}
stopOnce sync.Once
}
func NewWebSocketInterface(name string, wsURL string, enabled bool) (*WebSocketInterface, error) {
debug.Log(debug.DEBUG_VERBOSE, "NewWebSocketInterface called", "name", name, "url", wsURL, "enabled", enabled)
ws := &WebSocketInterface{
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
wsURL: wsURL,
messageQueue: make([][]byte, 0),
readBuffer: make([]byte, WS_BUFFER_SIZE),
writeBuffer: make([]byte, WS_BUFFER_SIZE),
done: make(chan struct{}),
}
ws.MTU = WS_MTU
ws.Bitrate = WS_BITRATE
debug.Log(debug.DEBUG_VERBOSE, "WebSocket interface initialized", "name", name, "mtu", ws.MTU, "bitrate", ws.Bitrate)
return ws, nil
}
func (wsi *WebSocketInterface) GetName() string {
return wsi.Name
}
func (wsi *WebSocketInterface) GetType() common.InterfaceType {
return wsi.Type
}
func (wsi *WebSocketInterface) GetMode() common.InterfaceMode {
return wsi.Mode
}
func (wsi *WebSocketInterface) IsOnline() bool {
wsi.Mutex.RLock()
defer wsi.Mutex.RUnlock()
return wsi.Online && wsi.connected
}
func (wsi *WebSocketInterface) IsDetached() bool {
wsi.Mutex.RLock()
defer wsi.Mutex.RUnlock()
return wsi.Detached
}
func (wsi *WebSocketInterface) Detach() {
wsi.Mutex.Lock()
defer wsi.Mutex.Unlock()
wsi.Detached = true
wsi.Online = false
wsi.closeWebSocketLocked()
}
func (wsi *WebSocketInterface) Enable() {
wsi.Mutex.Lock()
defer wsi.Mutex.Unlock()
wsi.Enabled = true
wsi.Online = true
}
func (wsi *WebSocketInterface) Disable() {
wsi.Mutex.Lock()
defer wsi.Mutex.Unlock()
wsi.Enabled = false
wsi.closeWebSocketLocked()
}
func (wsi *WebSocketInterface) Start() error {
wsi.Mutex.Lock()
if !wsi.Enabled || wsi.Detached {
wsi.Mutex.Unlock()
debug.Log(debug.DEBUG_INFO, "WebSocket interface not enabled or detached", "name", wsi.Name)
return fmt.Errorf("interface not enabled or detached")
}
if wsi.conn != nil {
wsi.Mutex.Unlock()
debug.Log(debug.DEBUG_INFO, "WebSocket already started", "name", wsi.Name)
return fmt.Errorf("WebSocket already started")
}
// Only recreate done if it's nil or was closed
select {
case <-wsi.done:
wsi.done = make(chan struct{})
wsi.stopOnce = sync.Once{}
default:
if wsi.done == nil {
wsi.done = make(chan struct{})
wsi.stopOnce = sync.Once{}
}
}
wsi.Mutex.Unlock()
debug.Log(debug.DEBUG_INFO, "Starting WebSocket connection", "name", wsi.Name, "url", wsi.wsURL)
u, err := url.Parse(wsi.wsURL)
if err != nil {
debug.Log(debug.DEBUG_ERROR, "Invalid WebSocket URL", "name", wsi.Name, "url", wsi.wsURL, "error", err)
return fmt.Errorf("invalid WebSocket URL: %v", err)
}
var conn net.Conn
var host string
if u.Scheme == "wss" {
host = u.Host
if !strings.Contains(host, ":") {
host += fmt.Sprintf(":%d", WS_HTTPS_PORT)
}
tcpConn, err := net.DialTimeout("tcp", host, WS_CONNECT_TIMEOUT)
if err != nil {
return fmt.Errorf("failed to connect: %v", err)
}
tlsConn := tls.Client(tcpConn, &tls.Config{
ServerName: u.Hostname(),
InsecureSkipVerify: false,
MinVersion: tls.VersionTLS12,
})
if err := tlsConn.Handshake(); err != nil {
_ = tcpConn.Close()
debug.Log(debug.DEBUG_ERROR, "TLS handshake failed", "name", wsi.Name, "host", host, "error", err)
return fmt.Errorf("TLS handshake failed: %v", err)
}
conn = tlsConn
} else if u.Scheme == "ws" {
host = u.Host
if !strings.Contains(host, ":") {
host += fmt.Sprintf(":%d", WS_HTTP_PORT)
}
debug.Log(debug.DEBUG_VERBOSE, "Connecting to WebSocket server", "name", wsi.Name, "host", host)
tcpConn, err := net.DialTimeout("tcp", host, WS_CONNECT_TIMEOUT)
if err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to connect to WebSocket server", "name", wsi.Name, "host", host, "error", err)
return fmt.Errorf("failed to connect: %v", err)
}
conn = tcpConn
} else {
debug.Log(debug.DEBUG_ERROR, "Unsupported WebSocket scheme", "name", wsi.Name, "scheme", u.Scheme)
return fmt.Errorf("unsupported scheme: %s (use ws:// or wss://)", u.Scheme)
}
key, err := generateWebSocketKey()
if err != nil {
_ = conn.Close()
return fmt.Errorf("failed to generate key: %v", err)
}
path := u.Path
if path == "" {
path = "/"
}
if u.RawQuery != "" {
path += "?" + u.RawQuery
}
req, err := http.NewRequest("GET", path, nil)
if err != nil {
_ = conn.Close()
return fmt.Errorf("failed to create request: %v", err)
}
req.Host = u.Host
req.Header.Set("Upgrade", "websocket")
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Sec-WebSocket-Key", key)
req.Header.Set("Sec-WebSocket-Version", WS_VERSION)
req.Header.Set("User-Agent", "Reticulum-Go/1.0")
if err := req.Write(conn); err != nil {
_ = conn.Close()
return fmt.Errorf("failed to send handshake: %v", err)
}
resp, err := http.ReadResponse(bufio.NewReader(conn), req)
if err != nil {
_ = conn.Close()
return fmt.Errorf("failed to read handshake response: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusSwitchingProtocols {
_ = conn.Close()
debug.Log(debug.DEBUG_ERROR, "WebSocket handshake failed", "name", wsi.Name, "status", resp.StatusCode)
return fmt.Errorf("handshake failed: status %d", resp.StatusCode)
}
if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" {
_ = conn.Close()
return fmt.Errorf("invalid upgrade header")
}
accept := resp.Header.Get("Sec-WebSocket-Accept")
expectedAccept := computeAcceptKey(key)
if accept != expectedAccept {
_ = conn.Close()
return fmt.Errorf("invalid accept key")
}
wsi.Mutex.Lock()
wsi.conn = conn
wsi.reader = bufio.NewReader(conn)
wsi.connected = true
wsi.Online = true
debug.Log(debug.DEBUG_INFO, "WebSocket connected", "name", wsi.Name, "url", wsi.wsURL)
queue := make([][]byte, len(wsi.messageQueue))
copy(queue, wsi.messageQueue)
wsi.messageQueue = wsi.messageQueue[:0]
wsi.Mutex.Unlock() // Unlock after copying queue, before I/O
for _, msg := range queue {
_ = wsi.sendWebSocketMessage(msg)
}
go wsi.readLoop()
return nil
}
func (wsi *WebSocketInterface) Stop() error {
wsi.Mutex.Lock()
defer wsi.Mutex.Unlock()
wsi.Enabled = false
wsi.Online = false
wsi.stopOnce.Do(func() {
if wsi.done != nil {
close(wsi.done)
}
})
wsi.closeWebSocketLocked()
return nil
}
func (wsi *WebSocketInterface) closeWebSocket() {
wsi.Mutex.Lock()
defer wsi.Mutex.Unlock()
wsi.closeWebSocketLocked()
}
func (wsi *WebSocketInterface) closeWebSocketLocked() {
if wsi.conn != nil {
wsi.sendCloseFrameLocked()
_ = wsi.conn.Close()
wsi.conn = nil
wsi.reader = nil
}
wsi.connected = false
wsi.Online = false
}
func (wsi *WebSocketInterface) readLoop() {
for {
wsi.Mutex.RLock()
conn := wsi.conn
reader := wsi.reader
done := wsi.done
wsi.Mutex.RUnlock()
if conn == nil || reader == nil {
return
}
select {
case <-done:
return
default:
}
data, err := wsi.readFrame()
if err != nil {
wsi.Mutex.Lock()
wsi.connected = false
wsi.Online = false
if wsi.conn != nil {
_ = wsi.conn.Close()
wsi.conn = nil
wsi.reader = nil
}
wsi.Mutex.Unlock()
debug.Log(debug.DEBUG_INFO, "WebSocket closed", "name", wsi.Name, "error", err)
time.Sleep(WS_RECONNECT_DELAY)
wsi.Mutex.RLock()
stillEnabled := wsi.Enabled && !wsi.Detached
wsi.Mutex.RUnlock()
if stillEnabled {
go wsi.Start()
}
return
}
if len(data) > 0 {
wsi.Mutex.Lock()
wsi.RxBytes += uint64(len(data))
wsi.Mutex.Unlock()
wsi.ProcessIncoming(data)
}
}
}
func (wsi *WebSocketInterface) readFrame() ([]byte, error) {
wsi.Mutex.RLock()
reader := wsi.reader
wsi.Mutex.RUnlock()
if reader == nil {
return nil, io.EOF
}
header := make([]byte, WS_HEADER_SIZE)
if _, err := io.ReadFull(reader, header); err != nil {
return nil, err
}
fin := (header[0] & WS_FRAME_HEADER_FIN) != 0
opcode := header[0] & WS_FRAME_HEADER_OPCODE
masked := (header[1] & WS_FRAME_HEADER_MASKED) != 0
payloadLen := int(header[1] & WS_FRAME_HEADER_LEN)
if opcode == WS_OPCODE_CLOSE {
return nil, io.EOF
}
if opcode == WS_OPCODE_PING {
return wsi.handlePingFrame(reader, payloadLen, masked)
}
if opcode == WS_OPCODE_PONG {
return wsi.handlePongFrame(reader, payloadLen, masked)
}
if opcode != WS_OPCODE_BINARY {
return nil, fmt.Errorf("unsupported opcode: %d", opcode)
}
if payloadLen == WS_PAYLOAD_LEN_16BIT {
lenBytes := make([]byte, 2)
if _, err := io.ReadFull(reader, lenBytes); err != nil {
return nil, err
}
payloadLen = int(binary.BigEndian.Uint16(lenBytes))
} else if payloadLen == WS_PAYLOAD_LEN_64BIT {
lenBytes := make([]byte, 8)
if _, err := io.ReadFull(reader, lenBytes); err != nil {
return nil, err
}
val := binary.BigEndian.Uint64(lenBytes)
if val > uint64(math.MaxInt) {
return nil, fmt.Errorf("payload length exceeds maximum integer value")
}
payloadLen = int(val) // #nosec G115
}
maskKey := make([]byte, WS_MASK_KEY_SIZE)
if masked {
if _, err := io.ReadFull(reader, maskKey); err != nil {
return nil, err
}
}
payload := make([]byte, payloadLen)
if _, err := io.ReadFull(reader, payload); err != nil {
return nil, err
}
if masked {
for i := 0; i < payloadLen; i++ {
payload[i] ^= maskKey[i%WS_MASK_KEY_SIZE]
}
}
if !fin {
nextFrame, err := wsi.readFrame()
if err != nil {
return nil, err
}
return append(payload, nextFrame...), nil
}
return payload, nil
}
func (wsi *WebSocketInterface) Send(data []byte, addr string) error {
wsi.Mutex.RLock()
enabled := wsi.Enabled
detached := wsi.Detached
connected := wsi.connected
wsi.Mutex.RUnlock()
if !enabled || detached {
debug.Log(debug.DEBUG_VERBOSE, "WebSocket interface not enabled or detached, dropping packet", "name", wsi.Name, "bytes", len(data))
return fmt.Errorf("interface not enabled")
}
wsi.Mutex.Lock()
wsi.TxBytes += uint64(len(data))
wsi.Mutex.Unlock()
if !connected {
debug.Log(debug.DEBUG_VERBOSE, "WebSocket not connected, queuing packet", "name", wsi.Name, "bytes", len(data), "queue_size", len(wsi.messageQueue))
wsi.Mutex.Lock()
wsi.messageQueue = append(wsi.messageQueue, data)
wsi.Mutex.Unlock()
return nil
}
packetType := "unknown"
if len(data) > 0 {
switch data[0] {
case 0x01:
packetType = "announce"
case 0x02:
packetType = "link"
default:
packetType = fmt.Sprintf("0x%02x", data[0])
}
}
debug.Log(debug.DEBUG_INFO, "Sending packet over WebSocket", "name", wsi.Name, "bytes", len(data), "packet_type", packetType)
return wsi.sendWebSocketMessage(data)
}
func (wsi *WebSocketInterface) sendWebSocketMessage(data []byte) error {
wsi.Mutex.RLock()
conn := wsi.conn
wsi.Mutex.RUnlock()
if conn == nil {
return fmt.Errorf("WebSocket not initialized")
}
frame := wsi.createFrame(data, WS_OPCODE_BINARY, true)
wsi.Mutex.Lock()
_, err := conn.Write(frame)
wsi.Mutex.Unlock()
if err != nil {
return fmt.Errorf("failed to send: %v", err)
}
debug.Log(debug.DEBUG_INFO, "WebSocket sent packet successfully", "name", wsi.Name, "bytes", len(data), "frame_bytes", len(frame))
return nil
}
func (wsi *WebSocketInterface) sendCloseFrame() {
wsi.Mutex.RLock()
defer wsi.Mutex.RUnlock()
wsi.sendCloseFrameLocked()
}
func (wsi *WebSocketInterface) sendCloseFrameLocked() {
conn := wsi.conn
if conn == nil {
return
}
frame := wsi.createFrame(nil, WS_OPCODE_CLOSE, true)
_, _ = conn.Write(frame)
}
func (wsi *WebSocketInterface) handlePingFrame(reader *bufio.Reader, payloadLen int, masked bool) ([]byte, error) {
if payloadLen == WS_PAYLOAD_LEN_16BIT {
lenBytes := make([]byte, 2)
if _, err := io.ReadFull(reader, lenBytes); err != nil {
return nil, err
}
payloadLen = int(binary.BigEndian.Uint16(lenBytes))
} else if payloadLen == WS_PAYLOAD_LEN_64BIT {
lenBytes := make([]byte, 8)
if _, err := io.ReadFull(reader, lenBytes); err != nil {
return nil, err
}
val := binary.BigEndian.Uint64(lenBytes)
if val > uint64(math.MaxInt) {
return nil, fmt.Errorf("payload length exceeds maximum integer value")
}
payloadLen = int(val) // #nosec G115
}
maskKey := make([]byte, WS_MASK_KEY_SIZE)
if masked {
if _, err := io.ReadFull(reader, maskKey); err != nil {
return nil, err
}
}
payload := make([]byte, payloadLen)
if payloadLen > 0 {
if _, err := io.ReadFull(reader, payload); err != nil {
return nil, err
}
if masked {
for i := 0; i < payloadLen; i++ {
payload[i] ^= maskKey[i%WS_MASK_KEY_SIZE]
}
}
}
wsi.sendPongFrame(payload)
return nil, nil
}
func (wsi *WebSocketInterface) handlePongFrame(reader *bufio.Reader, payloadLen int, masked bool) ([]byte, error) {
if payloadLen == WS_PAYLOAD_LEN_16BIT {
lenBytes := make([]byte, 2)
if _, err := io.ReadFull(reader, lenBytes); err != nil {
return nil, err
}
payloadLen = int(binary.BigEndian.Uint16(lenBytes))
} else if payloadLen == WS_PAYLOAD_LEN_64BIT {
lenBytes := make([]byte, 8)
if _, err := io.ReadFull(reader, lenBytes); err != nil {
return nil, err
}
val := binary.BigEndian.Uint64(lenBytes)
if val > uint64(math.MaxInt) {
return nil, fmt.Errorf("payload length exceeds maximum integer value")
}
payloadLen = int(val) // #nosec G115
}
maskKey := make([]byte, WS_MASK_KEY_SIZE)
if masked {
if _, err := io.ReadFull(reader, maskKey); err != nil {
return nil, err
}
}
if payloadLen > 0 {
payload := make([]byte, payloadLen)
if _, err := io.ReadFull(reader, payload); err != nil {
return nil, err
}
}
return nil, nil
}
func (wsi *WebSocketInterface) sendPongFrame(data []byte) {
wsi.Mutex.RLock()
conn := wsi.conn
wsi.Mutex.RUnlock()
if conn == nil {
return
}
frame := wsi.createFrame(data, WS_OPCODE_PONG, true)
wsi.Mutex.Lock()
_, _ = conn.Write(frame)
wsi.Mutex.Unlock()
}
func (wsi *WebSocketInterface) createFrame(data []byte, opcode byte, fin bool) []byte {
payloadLen := len(data)
frame := make([]byte, WS_HEADER_SIZE)
if fin {
frame[0] |= WS_FRAME_HEADER_FIN
}
frame[0] |= opcode
if payloadLen < WS_PAYLOAD_LEN_16BIT {
frame[1] = byte(payloadLen)
frame = append(frame, data...)
} else if payloadLen < WS_MAX_PAYLOAD_16BIT {
frame[1] = WS_PAYLOAD_LEN_16BIT // #nosec G602
lenBytes := make([]byte, 2)
binary.BigEndian.PutUint16(lenBytes, uint16(payloadLen)) // #nosec G115
frame = append(frame, lenBytes...)
frame = append(frame, data...)
} else {
frame[1] = WS_PAYLOAD_LEN_64BIT // #nosec G602
lenBytes := make([]byte, 8)
binary.BigEndian.PutUint64(lenBytes, uint64(payloadLen)) // #nosec G115
frame = append(frame, lenBytes...)
frame = append(frame, data...)
}
return frame
}
func (wsi *WebSocketInterface) ProcessOutgoing(data []byte) error {
return wsi.Send(data, "")
}
func (wsi *WebSocketInterface) GetConn() net.Conn {
wsi.Mutex.RLock()
defer wsi.Mutex.RUnlock()
return wsi.conn
}
func (wsi *WebSocketInterface) GetMTU() int {
return wsi.MTU
}
func (wsi *WebSocketInterface) IsEnabled() bool {
wsi.Mutex.RLock()
defer wsi.Mutex.RUnlock()
return wsi.Enabled && wsi.Online && !wsi.Detached
}
func (wsi *WebSocketInterface) SendPathRequest(packet []byte) error {
return wsi.Send(packet, "")
}
func (wsi *WebSocketInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.Time) error {
frame := make([]byte, 0, len(dest)+len(data)+9)
frame = append(frame, WS_OPCODE_BINARY)
frame = append(frame, dest...)
ts := make([]byte, 8)
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) // #nosec G115
frame = append(frame, ts...)
frame = append(frame, data...)
return wsi.Send(frame, "")
}
func (wsi *WebSocketInterface) GetBandwidthAvailable() bool {
return wsi.BaseInterface.GetBandwidthAvailable()
}
func generateWebSocketKey() (string, error) {
key := make([]byte, WS_KEY_SIZE)
if _, err := rand.Read(key); err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(key), nil
}
func computeAcceptKey(key string) string {
// bearer:disable go_gosec_crypto_weak_crypto
h := sha1.New() // #nosec G401
h.Write([]byte(key))
h.Write([]byte(wsGUID))
// bearer:disable go_lang_weak_hash_sha1
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}

View File

@@ -1,280 +0,0 @@
package interfaces
import (
"testing"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
)
func TestWebSocketGUID(t *testing.T) {
if wsGUID != "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" {
t.Errorf("wsGUID mismatch: expected RFC 6455 GUID, got %s", wsGUID)
}
}
func TestGenerateWebSocketKey(t *testing.T) {
key1, err := generateWebSocketKey()
if err != nil {
t.Fatalf("Failed to generate key: %v", err)
}
key2, err := generateWebSocketKey()
if err != nil {
t.Fatalf("Failed to generate key: %v", err)
}
if key1 == key2 {
t.Error("Generated keys should be unique")
}
if len(key1) != 24 {
t.Errorf("Expected base64-encoded key length 24, got %d", len(key1))
}
}
func TestComputeAcceptKey(t *testing.T) {
testKey := "dGhlIHNhbXBsZSBub25jZQ=="
expectedAccept := "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
accept := computeAcceptKey(testKey)
if accept != expectedAccept {
t.Errorf("Accept key mismatch: expected %s, got %s", expectedAccept, accept)
}
}
func TestNewWebSocketInterface(t *testing.T) {
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
if err != nil {
t.Fatalf("Failed to create WebSocket interface: %v", err)
}
if ws.GetName() != "test" {
t.Errorf("Expected name 'test', got %s", ws.GetName())
}
if ws.GetType() != common.IF_TYPE_UDP {
t.Errorf("Expected type IF_TYPE_UDP, got %v", ws.GetType())
}
if ws.GetMTU() != 1064 {
t.Errorf("Expected MTU 1064, got %d", ws.GetMTU())
}
if ws.IsOnline() {
t.Error("Interface should not be online before Start()")
}
}
func TestWebSocketConnection(t *testing.T) {
if testing.Short() {
t.Skip("Skipping network test in short mode")
}
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
if err != nil {
t.Fatalf("Failed to create WebSocket interface: %v", err)
}
ws.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
t.Logf("Received packet: %d bytes", len(data))
})
err = ws.Start()
if err != nil {
t.Fatalf("Failed to start WebSocket: %v", err)
}
time.Sleep(2 * time.Second)
if !ws.IsOnline() {
t.Error("WebSocket should be online after Start()")
}
testData := []byte{0x01, 0x02, 0x03, 0x04}
err = ws.Send(testData, "")
if err != nil {
t.Errorf("Failed to send data: %v", err)
}
time.Sleep(1 * time.Second)
if err := ws.Stop(); err != nil {
t.Errorf("Failed to stop WebSocket: %v", err)
}
time.Sleep(500 * time.Millisecond)
if ws.IsOnline() {
t.Error("WebSocket should be offline after Stop()")
}
}
func TestWebSocketReconnection(t *testing.T) {
if testing.Short() {
t.Skip("Skipping network test in short mode")
}
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
if err != nil {
t.Fatalf("Failed to create WebSocket interface: %v", err)
}
err = ws.Start()
if err != nil {
t.Fatalf("Failed to start WebSocket: %v", err)
}
time.Sleep(1 * time.Second)
if !ws.IsOnline() {
t.Error("WebSocket should be online")
}
conn := ws.GetConn()
if conn == nil {
t.Error("GetConn() should return a connection")
}
conn.Close()
time.Sleep(3 * time.Second)
if ws.IsOnline() {
t.Log("WebSocket reconnected successfully")
}
if err := ws.Stop(); err != nil {
t.Errorf("Failed to stop WebSocket: %v", err)
}
time.Sleep(500 * time.Millisecond)
}
func TestWebSocketMessageQueue(t *testing.T) {
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
if err != nil {
t.Fatalf("Failed to create WebSocket interface: %v", err)
}
ws.Enable()
testData := []byte{0x01, 0x02, 0x03}
err = ws.Send(testData, "")
if err != nil {
t.Errorf("Send should queue message when offline, got error: %v", err)
}
if testing.Short() {
return
}
err = ws.Start()
if err != nil {
t.Fatalf("Failed to start WebSocket: %v", err)
}
// Wait for interface to be online (up to 10 seconds)
for i := 0; i < 100; i++ {
if ws.IsOnline() {
break
}
time.Sleep(100 * time.Millisecond)
}
if !ws.IsOnline() {
t.Error("WebSocket should be online")
}
time.Sleep(2 * time.Second)
if err := ws.Stop(); err != nil {
t.Errorf("Failed to stop WebSocket: %v", err)
}
time.Sleep(500 * time.Millisecond)
}
func TestWebSocketFrameEncoding(t *testing.T) {
if testing.Short() {
t.Skip("Skipping frame encoding test in short mode")
}
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
if err != nil {
t.Fatalf("Failed to create WebSocket interface: %v", err)
}
err = ws.Start()
if err != nil {
t.Fatalf("Failed to start WebSocket: %v", err)
}
time.Sleep(1 * time.Second)
testCases := []struct {
name string
data []byte
}{
{"small frame", []byte{0x01, 0x02, 0x03}},
{"medium frame", make([]byte, 200)},
{"large frame", make([]byte, 1000)},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := ws.Send(tc.data, "")
if err != nil {
t.Errorf("Failed to send %s: %v", tc.name, err)
}
time.Sleep(100 * time.Millisecond)
})
}
if err := ws.Stop(); err != nil {
t.Errorf("Failed to stop WebSocket: %v", err)
}
time.Sleep(500 * time.Millisecond)
}
func TestWebSocketEnableDisable(t *testing.T) {
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", false)
if err != nil {
t.Fatalf("Failed to create WebSocket interface: %v", err)
}
if ws.IsEnabled() {
t.Error("Interface should not be enabled initially")
}
ws.Enable()
if !ws.IsEnabled() {
t.Error("Interface should be enabled after Enable()")
}
ws.Disable()
if ws.IsEnabled() {
t.Error("Interface should not be enabled after Disable()")
}
}
func TestWebSocketDetach(t *testing.T) {
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
if err != nil {
t.Fatalf("Failed to create WebSocket interface: %v", err)
}
if ws.IsDetached() {
t.Error("Interface should not be detached initially")
}
ws.Detach()
if !ws.IsDetached() {
t.Error("Interface should be detached after Detach()")
}
if ws.IsOnline() {
t.Error("Interface should be offline after Detach()")
}
}

View File

@@ -1,257 +0,0 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build js && wasm
// +build js,wasm
package interfaces
import (
"fmt"
"net"
"syscall/js"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
)
const (
WS_MTU = 1064
WS_BITRATE = 10000000
WS_RECONNECT_DELAY = 2 * time.Second
)
type WebSocketInterface struct {
BaseInterface
wsURL string
ws js.Value
connected bool
messageQueue [][]byte
}
func NewWebSocketInterface(name string, wsURL string, enabled bool) (*WebSocketInterface, error) {
ws := &WebSocketInterface{
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
wsURL: wsURL,
messageQueue: make([][]byte, 0),
}
ws.MTU = WS_MTU
ws.Bitrate = WS_BITRATE
return ws, nil
}
func (wsi *WebSocketInterface) GetName() string {
return wsi.Name
}
func (wsi *WebSocketInterface) GetType() common.InterfaceType {
return wsi.Type
}
func (wsi *WebSocketInterface) GetMode() common.InterfaceMode {
return wsi.Mode
}
func (wsi *WebSocketInterface) IsOnline() bool {
wsi.Mutex.RLock()
defer wsi.Mutex.RUnlock()
return wsi.Online && wsi.connected
}
func (wsi *WebSocketInterface) IsDetached() bool {
wsi.Mutex.RLock()
defer wsi.Mutex.RUnlock()
return wsi.Detached
}
func (wsi *WebSocketInterface) Detach() {
wsi.Mutex.Lock()
defer wsi.Mutex.Unlock()
wsi.Detached = true
wsi.Online = false
wsi.closeWebSocket()
}
func (wsi *WebSocketInterface) Enable() {
wsi.Mutex.Lock()
defer wsi.Mutex.Unlock()
wsi.Enabled = true
}
func (wsi *WebSocketInterface) Disable() {
wsi.Mutex.Lock()
defer wsi.Mutex.Unlock()
wsi.Enabled = false
wsi.closeWebSocket()
}
func (wsi *WebSocketInterface) Start() error {
wsi.Mutex.Lock()
defer wsi.Mutex.Unlock()
if wsi.ws.Truthy() {
readyState := wsi.ws.Get("readyState").Int()
if readyState == 1 { // OPEN
return nil
}
// If connecting, closing or closed, clean up first
wsi.closeWebSocket()
}
ws := js.Global().Get("WebSocket").New(wsi.wsURL)
ws.Set("binaryType", "arraybuffer")
ws.Set("onopen", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
wsi.Mutex.Lock()
wsi.connected = true
wsi.Online = true
wsi.Mutex.Unlock()
debug.Log(debug.DEBUG_INFO, "WebSocket connected", "name", wsi.Name, "url", wsi.wsURL)
wsi.Mutex.Lock()
queue := make([][]byte, len(wsi.messageQueue))
copy(queue, wsi.messageQueue)
wsi.messageQueue = wsi.messageQueue[:0]
wsi.Mutex.Unlock()
for _, msg := range queue {
wsi.sendWebSocketMessage(msg)
}
return nil
}))
ws.Set("onmessage", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) < 1 {
return nil
}
event := args[0]
data := event.Get("data")
handlePacket := func(buf js.Value) {
array := js.Global().Get("Uint8Array").New(buf)
length := array.Get("length").Int()
if length < 1 {
return
}
packet := make([]byte, length)
js.CopyBytesToGo(packet, array)
debug.Log(debug.DEBUG_VERBOSE, "WASM WebSocket received binary data", "name", wsi.Name, "length", length, "first_byte", fmt.Sprintf("0x%02x", packet[0]))
wsi.ProcessIncoming(packet)
}
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
}))
ws.Set("onerror", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
debug.Log(debug.DEBUG_ERROR, "WebSocket error", "name", wsi.Name)
return nil
}))
ws.Set("onclose", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
wsi.Mutex.Lock()
wsi.connected = false
wsi.Online = false
wsi.Mutex.Unlock()
debug.Log(debug.DEBUG_INFO, "WebSocket closed", "name", wsi.Name)
if wsi.Enabled && !wsi.Detached {
go func() {
time.Sleep(WS_RECONNECT_DELAY)
_ = wsi.Start()
}()
}
return nil
}))
wsi.ws = ws
return nil
}
func (wsi *WebSocketInterface) Stop() error {
wsi.Mutex.Lock()
defer wsi.Mutex.Unlock()
wsi.Enabled = false
wsi.closeWebSocket()
return nil
}
func (wsi *WebSocketInterface) closeWebSocket() {
if wsi.ws.Truthy() {
wsi.ws.Call("close")
wsi.ws = js.Value{}
}
wsi.connected = false
wsi.Online = false
}
func (wsi *WebSocketInterface) ProcessOutgoing(data []byte) error {
if !wsi.connected {
wsi.Mutex.Lock()
wsi.messageQueue = append(wsi.messageQueue, data)
wsi.Mutex.Unlock()
return nil
}
return wsi.sendWebSocketMessage(data)
}
func (wsi *WebSocketInterface) sendWebSocketMessage(data []byte) error {
if !wsi.ws.Truthy() {
return fmt.Errorf("WebSocket not initialized")
}
if wsi.ws.Get("readyState").Int() != 1 {
return fmt.Errorf("WebSocket not open")
}
array := js.Global().Get("Uint8Array").New(len(data))
js.CopyBytesToJS(array, data)
wsi.ws.Call("send", array)
debug.Log(debug.DEBUG_VERBOSE, "WebSocket sent packet", "name", wsi.Name, "bytes", len(data))
return nil
}
func (wsi *WebSocketInterface) GetConn() net.Conn {
return nil
}
func (wsi *WebSocketInterface) GetMTU() int {
return wsi.MTU
}
func (wsi *WebSocketInterface) IsEnabled() bool {
wsi.Mutex.RLock()
defer wsi.Mutex.RUnlock()
return wsi.Enabled && wsi.Online && !wsi.Detached
}

View File

@@ -1,364 +0,0 @@
package link
import (
"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"
"git.quad4.io/Networks/Reticulum-Go/pkg/transport"
)
func TestEphemeralKeyGeneration(t *testing.T) {
link := &Link{}
if err := link.generateEphemeralKeys(); err != nil {
t.Fatalf("Failed to generate ephemeral keys: %v", err)
}
if len(link.prv) != KEYSIZE {
t.Errorf("Expected private key length %d, got %d", KEYSIZE, len(link.prv))
}
if len(link.pub) != KEYSIZE {
t.Errorf("Expected public key length %d, got %d", KEYSIZE, len(link.pub))
}
if len(link.sigPriv) != 64 {
t.Errorf("Expected signing private key length 64, got %d", len(link.sigPriv))
}
if len(link.sigPub) != 32 {
t.Errorf("Expected signing public key length 32, got %d", len(link.sigPub))
}
}
func TestSignallingBytes(t *testing.T) {
mtu := 500
mode := byte(MODE_AES256_CBC)
bytes := signallingBytes(mtu, mode)
if len(bytes) != LINK_MTU_SIZE {
t.Errorf("Expected signalling bytes length %d, got %d", LINK_MTU_SIZE, len(bytes))
}
extractedMTU := (int(bytes[0]&0x1F) << 16) | (int(bytes[1]) << 8) | int(bytes[2])
if extractedMTU != mtu {
t.Errorf("Expected MTU %d, got %d", mtu, extractedMTU)
}
extractedMode := (bytes[0] & MODE_BYTEMASK) >> 5
if extractedMode != mode {
t.Errorf("Expected mode %d, got %d", mode, extractedMode)
}
}
func TestLinkIDGeneration(t *testing.T) {
responderIdent, err := identity.NewIdentity()
if err != nil {
t.Fatalf("Failed to create responder identity: %v", err)
}
cfg := &common.ReticulumConfig{}
transportInstance := transport.NewTransport(cfg)
dest, err := destination.New(responderIdent, destination.IN, destination.SINGLE, "test", transportInstance, "link")
if err != nil {
t.Fatalf("Failed to create destination: %v", err)
}
link := &Link{
destination: dest,
transport: transportInstance,
initiator: true,
}
if err := link.generateEphemeralKeys(); err != nil {
t.Fatalf("Failed to generate keys: %v", err)
}
link.mode = MODE_DEFAULT
link.mtu = 500
signalling := signallingBytes(link.mtu, link.mode)
requestData := make([]byte, 0, ECPUBSIZE+LINK_MTU_SIZE)
requestData = append(requestData, link.pub...)
requestData = append(requestData, link.sigPub...)
requestData = append(requestData, signalling...)
pkt := &packet.Packet{
HeaderType: packet.HeaderType1,
PacketType: packet.PacketTypeLinkReq,
TransportType: 0,
Context: packet.ContextNone,
ContextFlag: packet.FlagUnset,
Hops: 0,
DestinationType: dest.GetType(),
DestinationHash: dest.GetHash(),
Data: requestData,
}
if err := pkt.Pack(); err != nil {
t.Fatalf("Failed to pack packet: %v", err)
}
linkID := linkIDFromPacket(pkt)
if len(linkID) != 16 {
t.Errorf("Expected link ID length 16, got %d", len(linkID))
}
t.Logf("Generated link ID: %x", linkID)
}
func TestHandshake(t *testing.T) {
link1 := &Link{}
link2 := &Link{}
if err := link1.generateEphemeralKeys(); err != nil {
t.Fatalf("Failed to generate keys for link1: %v", err)
}
if err := link2.generateEphemeralKeys(); err != nil {
t.Fatalf("Failed to generate keys for link2: %v", err)
}
link1.peerPub = link2.pub
link2.peerPub = link1.pub
link1.linkID = []byte("test-link-id-abc")
link2.linkID = []byte("test-link-id-abc")
link1.mode = MODE_AES256_CBC
link2.mode = MODE_AES256_CBC
if err := link1.performHandshake(); err != nil {
t.Fatalf("Link1 handshake failed: %v", err)
}
if err := link2.performHandshake(); err != nil {
t.Fatalf("Link2 handshake failed: %v", err)
}
if string(link1.sharedKey) != string(link2.sharedKey) {
t.Error("Shared keys do not match")
}
if string(link1.derivedKey) != string(link2.derivedKey) {
t.Error("Derived keys do not match")
}
if link1.status != STATUS_HANDSHAKE {
t.Errorf("Expected link1 status HANDSHAKE, got %d", link1.status)
}
if link2.status != STATUS_HANDSHAKE {
t.Errorf("Expected link2 status HANDSHAKE, got %d", link2.status)
}
}
func TestLinkEstablishment(t *testing.T) {
responderIdent, err := identity.NewIdentity()
if err != nil {
t.Fatalf("Failed to create responder identity: %v", err)
}
cfg := &common.ReticulumConfig{}
transportInstance := transport.NewTransport(cfg)
dest, err := destination.New(responderIdent, destination.IN, destination.SINGLE, "test", transportInstance, "link")
if err != nil {
t.Fatalf("Failed to create destination: %v", err)
}
initiatorLink := &Link{
destination: dest,
transport: transportInstance,
initiator: true,
}
responderLink := &Link{
transport: transportInstance,
initiator: false,
}
if err := initiatorLink.generateEphemeralKeys(); err != nil {
t.Fatalf("Failed to generate initiator keys: %v", err)
}
initiatorLink.mode = MODE_DEFAULT
initiatorLink.mtu = 500
signalling := signallingBytes(initiatorLink.mtu, initiatorLink.mode)
requestData := make([]byte, 0, ECPUBSIZE+LINK_MTU_SIZE)
requestData = append(requestData, initiatorLink.pub...)
requestData = append(requestData, initiatorLink.sigPub...)
requestData = append(requestData, signalling...)
linkRequestPkt := &packet.Packet{
HeaderType: packet.HeaderType1,
PacketType: packet.PacketTypeLinkReq,
TransportType: 0,
Context: packet.ContextNone,
ContextFlag: packet.FlagUnset,
Hops: 0,
DestinationType: dest.GetType(),
DestinationHash: dest.GetHash(),
Data: requestData,
}
if err := linkRequestPkt.Pack(); err != nil {
t.Fatalf("Failed to pack link request: %v", err)
}
initiatorLink.linkID = linkIDFromPacket(linkRequestPkt)
initiatorLink.requestTime = time.Now()
initiatorLink.status = STATUS_PENDING
t.Logf("Initiator link request created, link_id=%x", initiatorLink.linkID)
responderLink.peerPub = linkRequestPkt.Data[0:KEYSIZE]
responderLink.peerSigPub = linkRequestPkt.Data[KEYSIZE:ECPUBSIZE]
responderLink.linkID = linkIDFromPacket(linkRequestPkt)
responderLink.initiator = false
t.Logf("Responder link ID=%x (len=%d)", responderLink.linkID, len(responderLink.linkID))
if len(responderLink.linkID) == 0 {
t.Fatal("Responder link ID is empty!")
}
if len(linkRequestPkt.Data) >= ECPUBSIZE+LINK_MTU_SIZE {
mtuBytes := linkRequestPkt.Data[ECPUBSIZE : ECPUBSIZE+LINK_MTU_SIZE]
responderLink.mtu = (int(mtuBytes[0]&0x1F) << 16) | (int(mtuBytes[1]) << 8) | int(mtuBytes[2])
responderLink.mode = (mtuBytes[0] & MODE_BYTEMASK) >> 5
}
if err := responderLink.generateEphemeralKeys(); err != nil {
t.Fatalf("Failed to generate responder keys: %v", err)
}
if err := responderLink.performHandshake(); err != nil {
t.Fatalf("Responder handshake failed: %v", err)
}
responderLink.status = STATUS_ACTIVE
responderLink.establishedAt = time.Now()
if string(responderLink.linkID) != string(initiatorLink.linkID) {
t.Error("Link IDs do not match between initiator and responder")
}
t.Logf("Responder handshake successful, shared_key_len=%d", len(responderLink.sharedKey))
}
func TestLinkProofValidation(t *testing.T) {
responderIdent, err := identity.NewIdentity()
if err != nil {
t.Fatalf("Failed to create responder identity: %v", err)
}
cfg := &common.ReticulumConfig{}
transportInstance := transport.NewTransport(cfg)
dest, err := destination.New(responderIdent, destination.IN, destination.SINGLE, "test", transportInstance, "link")
if err != nil {
t.Fatalf("Failed to create destination: %v", err)
}
initiatorLink := &Link{
destination: dest,
transport: transportInstance,
initiator: true,
}
responderLink := &Link{
transport: transportInstance,
initiator: false,
}
if err := initiatorLink.generateEphemeralKeys(); err != nil {
t.Fatalf("Failed to generate initiator keys: %v", err)
}
initiatorLink.mode = MODE_DEFAULT
initiatorLink.mtu = 500
signalling := signallingBytes(initiatorLink.mtu, initiatorLink.mode)
requestData := make([]byte, 0, ECPUBSIZE+LINK_MTU_SIZE)
requestData = append(requestData, initiatorLink.pub...)
requestData = append(requestData, initiatorLink.sigPub...)
requestData = append(requestData, signalling...)
linkRequestPkt := &packet.Packet{
HeaderType: packet.HeaderType1,
PacketType: packet.PacketTypeLinkReq,
TransportType: 0,
Context: packet.ContextNone,
ContextFlag: packet.FlagUnset,
Hops: 0,
DestinationType: dest.GetType(),
DestinationHash: dest.GetHash(),
Data: requestData,
}
if err := linkRequestPkt.Pack(); err != nil {
t.Fatalf("Failed to pack link request: %v", err)
}
initiatorLink.linkID = linkIDFromPacket(linkRequestPkt)
initiatorLink.requestTime = time.Now()
initiatorLink.status = STATUS_PENDING
responderLink.peerPub = linkRequestPkt.Data[0:KEYSIZE]
responderLink.peerSigPub = linkRequestPkt.Data[KEYSIZE:ECPUBSIZE]
responderLink.linkID = linkIDFromPacket(linkRequestPkt)
responderLink.initiator = false
if len(linkRequestPkt.Data) >= ECPUBSIZE+LINK_MTU_SIZE {
mtuBytes := linkRequestPkt.Data[ECPUBSIZE : ECPUBSIZE+LINK_MTU_SIZE]
responderLink.mtu = (int(mtuBytes[0]&0x1F) << 16) | (int(mtuBytes[1]) << 8) | int(mtuBytes[2])
responderLink.mode = (mtuBytes[0] & MODE_BYTEMASK) >> 5
} else {
responderLink.mtu = 500
responderLink.mode = MODE_DEFAULT
}
if err := responderLink.generateEphemeralKeys(); err != nil {
t.Fatalf("Failed to generate responder keys: %v", err)
}
if err := responderLink.performHandshake(); err != nil {
t.Fatalf("Responder handshake failed: %v", err)
}
proofPkt, err := responderLink.GenerateLinkProof(responderIdent)
if err != nil {
t.Fatalf("Failed to generate link proof: %v", err)
}
if err := initiatorLink.ValidateLinkProof(proofPkt, nil); err != nil {
t.Fatalf("Initiator failed to validate link proof: %v", err)
}
if initiatorLink.status != STATUS_ACTIVE {
t.Errorf("Expected initiator status ACTIVE, got %d", initiatorLink.status)
}
if string(initiatorLink.sharedKey) != string(responderLink.sharedKey) {
t.Error("Shared keys do not match after full handshake")
}
if string(initiatorLink.derivedKey) != string(responderLink.derivedKey) {
t.Error("Derived keys do not match after full handshake")
}
t.Logf("Full link establishment successful")
t.Logf("Link ID: %x", initiatorLink.linkID)
t.Logf("Shared key length: %d", len(initiatorLink.sharedKey))
t.Logf("Derived key length: %d", len(initiatorLink.derivedKey))
t.Logf("RTT: %.3f seconds", initiatorLink.rtt)
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,218 +0,0 @@
package link
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 mockTransport struct {
sentPackets []*packet.Packet
}
func (m *mockTransport) SendPacket(pkt *packet.Packet) error {
m.sentPackets = append(m.sentPackets, pkt)
return nil
}
func (m *mockTransport) RegisterLink(linkID []byte, link interface{}) {
}
func (m *mockTransport) GetConfig() *common.ReticulumConfig {
return &common.ReticulumConfig{}
}
func (m *mockTransport) GetInterfaces() map[string]common.NetworkInterface {
return make(map[string]common.NetworkInterface)
}
func (m *mockTransport) RegisterDestination(hash []byte, dest interface{}) {
}
type mockInterface struct {
name string
}
func (m *mockInterface) GetName() string {
return m.name
}
func (m *mockInterface) Start() error {
return nil
}
func (m *mockInterface) Stop() error {
return nil
}
func (m *mockInterface) Send(data []byte, address string) error {
return nil
}
func (m *mockInterface) ProcessIncoming(data []byte) error {
return nil
}
func (m *mockInterface) SetPacketCallback(cb func([]byte, common.NetworkInterface)) {
}
func (m *mockInterface) GetType() string {
return "mock"
}
func (m *mockInterface) GetMTU() int {
return 500
}
func (m *mockInterface) Detach() {
}
func (m *mockInterface) Enable() {
}
func (m *mockInterface) Disable() {
}
func (m *mockInterface) IsEnabled() bool {
return true
}
func (m *mockInterface) IsOnline() bool {
return true
}
func (m *mockInterface) IsDetached() bool {
return false
}
func (m *mockInterface) GetPacketCallback() func([]byte, common.NetworkInterface) {
return nil
}
func (m *mockInterface) GetConn() interface{} {
return nil
}
func (m *mockInterface) ProcessOutgoing(data []byte) ([]byte, error) {
return data, nil
}
func (m *mockInterface) SendPathRequest(destHash []byte) error {
return nil
}
func (m *mockInterface) SendLinkPacket(data []byte) error {
return nil
}
func (m *mockInterface) GetBandwidthAvailable() float64 {
return 1.0
}
func TestLinkRequestResponse(t *testing.T) {
serverIdent, err := identity.New()
if err != nil {
t.Fatalf("Failed to create server identity: %v", err)
}
clientIdent, err := identity.New()
if err != nil {
t.Fatalf("Failed to create client identity: %v", err)
}
mockTrans := &mockTransport{
sentPackets: make([]*packet.Packet, 0),
}
serverDest, err := destination.New(serverIdent, destination.IN, destination.SINGLE, "testapp", mockTrans, "server")
if err != nil {
t.Fatalf("Failed to create server destination: %v", err)
}
expectedResponse := []byte("response data")
testPath := "/test/path"
err = serverDest.RegisterRequestHandler(testPath, func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity *identity.Identity, requestedAt int64) []byte {
if path != testPath {
t.Errorf("Expected path %s, got %s", testPath, path)
}
return expectedResponse
}, destination.ALLOW_ALL, nil)
if err != nil {
t.Fatalf("Failed to register request handler: %v", err)
}
// Test the handler is registered correctly
pathHash := identity.TruncatedHash([]byte(testPath))
handler := serverDest.GetRequestHandler(pathHash)
if handler == nil {
t.Fatal("Handler not found after registration")
}
// Call the handler
testLinkID := make([]byte, 16)
result := handler(pathHash, []byte("test data"), []byte("request-id"), testLinkID, clientIdent, time.Now())
if result == nil {
t.Fatal("Handler returned nil")
}
responseBytes, ok := result.([]byte)
if !ok {
t.Fatalf("Handler returned unexpected type: %T", result)
}
if !bytes.Equal(responseBytes, expectedResponse) {
t.Errorf("Expected response %q, got %q", expectedResponse, responseBytes)
}
}
func TestLinkRequestHandlerNotFound(t *testing.T) {
serverIdent, _ := identity.New()
mockTrans := &mockTransport{sentPackets: make([]*packet.Packet, 0)}
serverDest, _ := destination.New(serverIdent, destination.IN, destination.SINGLE, "testapp", mockTrans, "server")
nonExistentPath := "/does/not/exist"
pathHash := identity.TruncatedHash([]byte(nonExistentPath))
handler := serverDest.GetRequestHandler(pathHash)
if handler != nil {
t.Error("Expected no handler for non-existent path, but found one")
}
}
func TestLinkResponseHandling(t *testing.T) {
// This test verifies the basic structure for response handling
// Full integration testing would require a proper transport setup
requestID := []byte("test-request-id-")
responseData := []byte("response payload")
receipt := &RequestReceipt{
requestID: requestID,
status: STATUS_PENDING,
}
// Verify initial state
if receipt.status != STATUS_PENDING {
t.Errorf("Expected initial status PENDING, got %d", receipt.status)
}
// Simulate setting response
receipt.response = responseData
receipt.status = STATUS_ACTIVE
if !bytes.Equal(receipt.response, responseData) {
t.Errorf("Expected response %q, got %q", responseData, receipt.response)
}
if receipt.status != STATUS_ACTIVE {
t.Errorf("Expected status ACTIVE after response, got %d", receipt.status)
}
}

View File

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package packet package packet
const ( const (

Some files were not shown because too many files have changed in this diff Show More