410 Commits
v0.1.0 ... main

Author SHA1 Message Date
97353d430b chore: add CONTRIBUTORS file to document project contributors and their contributions
All checks were successful
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 45s
Bearer / scan (push) Successful in 9s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 46s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 41s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 43s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 45s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 47s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m21s
Go Revive Lint / lint (push) Successful in 57s
Run Gosec / tests (push) Successful in 1m9s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 2m33s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 9m27s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 9m30s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 9m30s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m28s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m30s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 9m28s
Go Build Multi-Platform / Create Release (push) Has been skipped
2026-01-01 13:10:55 -06:00
1d3a969742 chore: add SPDX license identifier and copyright notice
Some checks failed
Bearer / scan (push) Successful in 9s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 42s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 44s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 41s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 39s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 1m8s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 1m6s
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (pull_request) Failing after 1m2s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (pull_request) Failing after 1m4s
Go Revive Lint / lint (push) Successful in 1m4s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m24s
Run Gosec / tests (push) Successful in 1m29s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 2m31s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 9m28s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 9m28s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 9m30s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m27s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 9m26s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m29s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-31 20:44:58 -06:00
bad92193a3 chore: update copyright year in LICENSE file to 2026 2025-12-31 20:44:31 -06:00
6560949ec4 feat: add wasm_exec.js to examples
Some checks failed
Bearer / scan (push) Successful in 9s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 43s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 43s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 40s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 42s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 1m2s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 1m5s
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (pull_request) Failing after 59s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (pull_request) Failing after 1m1s
Go Revive Lint / lint (push) Successful in 1m3s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m24s
Run Gosec / tests (push) Successful in 1m29s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 2m28s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 9m28s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 9m31s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m28s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 9m31s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m31s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 9m28s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-31 18:43:37 -06:00
d6152ccd85 refactor: replace AnnounceHandler interface with Handler and update ReceivedAnnounce method to include hops parameter 2025-12-31 18:42:50 -06:00
e6fd7188d2 fix: add hops parameter to ReceivedAnnounce method for enhanced announce handling 2025-12-31 18:41:44 -06:00
bc44ed2aaa fix: update ReceivedAnnounce method to include hops parameter for improved logging of announce data 2025-12-31 18:41:30 -06:00
e5ac206e5c chore: refactor WebAssembly test commands in Taskfile to use ROOT_DIR variable for improved path management
Some checks failed
Bearer / scan (push) Successful in 53s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 48s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 51s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 51s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 49s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 46s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 48s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 50s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 48s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 52s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 46s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 1m13s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 1m15s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (pull_request) Failing after 1m11s
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (pull_request) Failing after 1m9s
Go Revive Lint / lint (push) Successful in 1m5s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m25s
Go Build Multi-Platform / Create Release (push) Has been skipped
Run Gosec / tests (push) Successful in 1m39s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 2m45s
2025-12-31 11:57:21 -06:00
b74012099c chore: update Taskfile to set GOPRIVATE environment variable and adjust WebAssembly test commands for better organization
Some checks failed
Bearer / scan (push) Successful in 44s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 39s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 43s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 41s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 37s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 43s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 40s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 42s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 1m19s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (pull_request) Failing after 1m16s
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (pull_request) Failing after 1m15s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Failing after 1m53s
Go Revive Lint / lint (push) Successful in 57s
Run Gosec / tests (push) Successful in 1m43s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 9m27s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 9m28s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 9m30s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 9m30s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 19m2s
2025-12-31 11:54:10 -06:00
ae1d290fa7 chore: add go.mod and go.sum files for WebAssembly example to manage dependencies 2025-12-31 11:54:04 -06:00
562f850b8f feat: implement WebAssembly chat functionality with message sending and announcing capabilities, including tests for function registration 2025-12-31 11:50:38 -06:00
ff0088644e chore: update .gitignore to include all example files except wasm directory for better file management 2025-12-31 11:50:26 -06:00
aee52bf56c feat: update Taskfile check task to provide detailed summary of failed checks and overall status
Some checks failed
Bearer / scan (push) Successful in 9s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 46s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 42s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 48s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 42s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 43s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 42s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 40s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 42s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 39s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 36s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 38s
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (pull_request) Failing after 1m2s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m33s
Go Revive Lint / lint (push) Successful in 57s
Run Gosec / tests (push) Successful in 1m46s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Failing after 2m22s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (pull_request) Failing after 4m48s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 9m26s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-31 11:44:15 -06:00
fd951a10f8 feat: enhance WebAssembly API by adding requestPath, setPacketCallback, and setAnnounceCallback functions; refactor SendMessage to SendDataJS for improved data handling
Some checks failed
Bearer / scan (push) Successful in 8s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 43s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 43s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 41s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 44s
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (pull_request) Has been cancelled
Go Build Multi-Platform / build (wasm, js) (push) Successful in 51s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 55s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 53s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (pull_request) Has been cancelled
Go Revive Lint / lint (push) Successful in 1m9s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m27s
Run Gosec / tests (push) Successful in 1m31s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Failing after 2m16s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 4m42s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 9m27s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m27s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 9m29s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m29s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-31 11:43:27 -06:00
11d4c6407e feat: expand Taskfile with new build tasks for Windows, MacOS, FreeBSD, OpenBSD, and NetBSD, and enhance WebAssembly example with run and test commands 2025-12-31 11:42:43 -06:00
1be94dc0ba chore: remove Makefile to streamline build process and simplify project structure 2025-12-31 11:42:36 -06:00
b30a1ba3eb feat: add TinyGo to development environment in flake.nix 2025-12-31 11:42:32 -06:00
9f755aec21 chore: update Go version from 1.24 to 1.25 in flake.nix 2025-12-31 11:06:20 -06:00
1a579bc716 chore: update base image in Dockerfile to busybox with sha256
Some checks failed
Bearer / scan (push) Successful in 40s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 40s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 40s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 39s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 49s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 52s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 55s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (pull_request) Failing after 48s
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (pull_request) Failing after 49s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m19s
Go Revive Lint / lint (push) Successful in 55s
Run Gosec / tests (push) Successful in 1m41s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 2m26s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 4m47s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 9m30s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 9m30s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 9m28s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m30s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m28s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-30 23:46:16 -06:00
8124d95192 fix: update announce hash generation in Transport to exclude hop count and header for uniqueness 2025-12-30 23:46:00 -06:00
a59dca45a7 feat: improve logging in WebSocket interface for better debugging and error tracking 2025-12-30 23:45:46 -06:00
1106215241 feat: improve WebSocket interface logging and remove default interface configuration 2025-12-30 23:45:34 -06:00
ee61747e20 feat: add comprehensive Trivy scanning tasks to Taskfile for enhanced vulnerability management 2025-12-30 23:45:17 -06:00
4c1c819e42 chore: remove pkg/ from .dockerignore to streamline Docker build context 2025-12-30 23:45:10 -06:00
078fa0f17d feat: update Dockerfile to add builder user and improve build environment 2025-12-30 23:45:05 -06:00
876476cff5 feat: update SBOM workflow to include Trivy installation and improve commit logic
Some checks failed
Bearer / scan (push) Successful in 46s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 47s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 46s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 45s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 48s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 50s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 53s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 46s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 49s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 47s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 43s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 45s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 1m18s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (pull_request) Failing after 1m18s
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (pull_request) Failing after 1m16s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Revive Lint / lint (push) Successful in 1m3s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m23s
Run Gosec / tests (push) Successful in 1m31s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 2m47s
2025-12-30 21:20:41 -06:00
899b08e92e feat: add Trivy installation and scanning tasks to Taskfile for vulnerability management 2025-12-30 21:20:34 -06:00
eec73d2d93 feat: add graceful shutdown support to Transport with done channel and stopOnce synchronization
Some checks failed
Bearer / scan (push) Successful in 47s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 46s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 44s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 42s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 54s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 58s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 56s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (pull_request) Failing after 52s
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (pull_request) Failing after 58s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m6s
Go Revive Lint / lint (push) Successful in 58s
Run Gosec / tests (push) Successful in 1m54s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 2m27s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 9m27s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 9m29s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 9m29s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 9m27s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m27s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m29s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-30 21:15:16 -06:00
6888eccc62 refactor: replace hardcoded values with constants and standardize mutex naming in WebSocketInterface 2025-12-30 21:15:08 -06:00
6fa0187ae1 feat: implement native WebSocket interface for connection and message handling 2025-12-30 21:15:01 -06:00
2ba3f059a1 test: add comprehensive WebSocket interface tests for connection, key generation, and message handling 2025-12-30 21:14:57 -06:00
63454b3bbb refactor: enhance UDPInterface with improved concurrency handling and consistent mutex naming 2025-12-30 21:14:42 -06:00
9f36e37f94 refactor: remove commented-out code in udp_test.go to improve code clarity 2025-12-30 21:14:36 -06:00
73b982c6e0 refactor: enhance TCPClientInterface and TCPServerInterface with improved concurrency handling and added connection timeout 2025-12-30 21:14:30 -06:00
22c54f2252 refactor: replace hardcoded TCP options with constants for improved readability in TCPClientInterface 2025-12-30 21:14:24 -06:00
d68a6cfb9c refactor: replace hardcoded SO_KEEPALIVE value with constant for improved readability in TCPClientInterface 2025-12-30 21:14:18 -06:00
8267123fb5 refactor: standardize mutex naming to improve code consistency in BaseInterface 2025-12-30 21:14:13 -06:00
a540b64331 refactor: remove commented-out code in mockInterface to clean up interface_test.go 2025-12-30 21:14:04 -06:00
b0d2d4778f refactor: enhance AutoInterface with done channel and improved locking for concurrency 2025-12-30 21:13:56 -06:00
6cb90d3c4b refactor: replace mutex with Mutex for improved consistency in peer management tests 2025-12-30 21:13:46 -06:00
009755c981 refactor: implement read-write locking for knownDestinations to improve concurrency 2025-12-30 21:13:42 -06:00
595430c808 refactor: remove commented-out code for default interfaces in InitConfig function 2025-12-30 21:13:38 -06:00
e9b647d5a7 refactor: remove unused timestamp and data comments in SendLinkPacket function 2025-12-30 21:13:34 -06:00
7d57888696 sast: bearer:disable javascript_lang_logger_leak 2025-12-30 21:13:26 -06:00
cbe2df02ad refactor: remove commented-out code for interface configuration in CreateDefaultConfig function 2025-12-30 21:12:51 -06:00
ff893945e9 feat: add WebSocketInterface support and configure Quad4 WebSocket in main.go 2025-12-30 21:12:44 -06:00
b705427bc9 chore: update check task to include test-short and scan dependencies 2025-12-30 21:12:36 -06:00
88083be84e fix: update test-wasm task to dynamically set PATH for WebAssembly tests
Some checks failed
Bearer / scan (push) Failing after 44s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 52s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 40s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 36s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 34s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 43s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 41s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 39s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 38s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 38s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 38s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 36s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 35s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m15s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (pull_request) Failing after 1m22s
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (pull_request) Failing after 1m20s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 2m6s
Go Revive Lint / lint (push) Successful in 1m6s
Run Gosec / tests (push) Successful in 1m35s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-30 19:28:01 -06:00
4ea1fd1f28 feat: add wasm_exec_node.js for Node.js support in WebAssembly execution
Some checks failed
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 36s
Bearer / scan (push) Failing after 38s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 32s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 34s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 52s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 48s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 50s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (pull_request) Failing after 46s
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (pull_request) Failing after 59s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 59s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Failing after 1m33s
Run Gosec / tests (push) Successful in 1m45s
Go Revive Lint / lint (push) Successful in 52s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 9m23s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 9m25s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 9m23s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 9m25s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m23s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m25s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-30 19:27:24 -06:00
1ad1f3cfd2 feat: enhance CI workflow by adding Node.js setup and WebAssembly test/build steps for Linux AMD64
Some checks failed
Bearer / scan (push) Successful in 42s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 40s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 39s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 37s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 49s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 52s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 53s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (pull_request) Failing after 54s
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (pull_request) Failing after 1m0s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 57s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Failing after 1m45s
Go Revive Lint / lint (push) Successful in 49s
Run Gosec / tests (push) Successful in 1m44s
Go Build Multi-Platform / Create Release (push) Has been cancelled
Go Build Multi-Platform / build (amd64, darwin) (push) Has been cancelled
Go Build Multi-Platform / build (arm64, freebsd) (push) Has been cancelled
Go Build Multi-Platform / build (arm64, darwin) (push) Has been cancelled
Go Build Multi-Platform / build (arm, windows) (push) Has been cancelled
Go Build Multi-Platform / build (amd64, linux) (push) Has been cancelled
Go Build Multi-Platform / build (arm, freebsd) (push) Has been cancelled
2025-12-30 19:18:53 -06:00
1281731a81 feat: add test-wasm task for running WebAssembly tests and simplify build-wasm command 2025-12-30 19:18:47 -06:00
8d97c29b19 docs: add test-wasm command to README for running WebAssembly tests 2025-12-30 19:18:42 -06:00
f8712b35b8 refactor: extract run function 2025-12-30 19:18:37 -06:00
0051405033 test: add initial tests for WebAssembly 2025-12-30 19:18:26 -06:00
3a14394640 feat: add go_js_wasm_exec script to increase V8 stack size for WebAssembly tests 2025-12-30 19:17:57 -06:00
2faf1fb5a2 feat: add wasm_exec.js for WebAssembly support, implementing file system, process, and crypto APIs 2025-12-30 19:17:49 -06:00
e51baa8673 test: add comprehensive tests for WASM functions including registration, stats retrieval, connection status, and message handling 2025-12-30 19:17:43 -06:00
21a0dafae6 feat: enhance chat message handling in WASM by including sender information and user-specific app data 2025-12-30 19:17:37 -06:00
156d642fe6 docs: add Go Build badge to README for CI visibility
Some checks failed
Bearer / scan (push) Successful in 36s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m2s
Go Revive Lint / lint (push) Successful in 47s
Run Gosec / tests (push) Successful in 1m19s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 2m12s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (pull_request) Failing after 1m12s
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (pull_request) Failing after 56s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 58s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 54s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 40s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 56s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 39s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 37s
Generate SBOM / generate-sbom (push) Successful in 36s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 9m25s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 9m23s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 9m25s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 9m23s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m25s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 9m25s
Go Build Multi-Platform / Create Release (push) Successful in 11s
2025-12-30 12:41:36 -06:00
20a1da6a56 docs: update README
All checks were successful
Bearer / scan (push) Successful in 29s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 47s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 42s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 35s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 33s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 37s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 36s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 32s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 34s
Go Revive Lint / lint (push) Successful in 44s
Run Gosec / tests (push) Successful in 49s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 9m25s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 9m22s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 9m25s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 9m25s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 18m50s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 19m4s
2025-12-30 12:35:44 -06:00
0a3d0c9440 refactor: update bandwidth checks in announce forwarding logic to prevent overload 2025-12-30 12:35:32 -06:00
04bd23e753 refactor: adjust rate limiter configuration in AnnounceManager for improved performance 2025-12-30 12:35:21 -06:00
a01b253473 refactor: update Limiter implementation to use capacity instead of interval and adjust related tests 2025-12-30 12:35:15 -06:00
53b981cb21 refactor: remove unused hops parameter from ReceivedAnnounce method in WASM handler
All checks were successful
Bearer / scan (push) Successful in 28s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 35s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 33s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 32s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 32s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 35s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 34s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 32s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 35s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 34s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 31s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 33s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 39s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m6s
Go Revive Lint / lint (push) Successful in 53s
Run Gosec / tests (push) Successful in 1m22s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m33s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-30 12:32:55 -06:00
6cc82159cf refactor: update WASM build commands in Taskfile to specify the correct package path for Reticulum
Some checks failed
Bearer / scan (push) Successful in 36s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 34s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 31s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 33s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 30s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 33s
Go Build Multi-Platform / build (wasm, js) (push) Failing after 33s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 58s
Go Revive Lint / lint (push) Successful in 1m0s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m36s
Run Gosec / tests (push) Successful in 1m10s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 9m23s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 9m25s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 9m23s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m23s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 9m25s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m25s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-30 12:28:09 -06:00
5a9bfd74d3 feat: add WASM package with JavaScript API for Reticulum integration 2025-12-30 12:27:59 -06:00
1c3de51913 feat: implement main entry point for WebAssembly (WASM) application with JS integration 2025-12-30 12:27:55 -06:00
25e0719954 fix: handle error when generating random tag in RequestPath method
All checks were successful
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 31s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 32s
Bearer / scan (push) Successful in 7s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 32s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 32s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 44s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 47s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 51s
Go Revive Lint / lint (push) Successful in 1m1s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m44s
Run Gosec / tests (push) Successful in 1m7s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 9m23s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 9m25s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 9m23s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m23s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m25s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 9m25s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-30 12:13:49 -06:00
cbff6e4bc4 refactor: optimize WASM build command in Taskfile by adding ldflags for smaller output 2025-12-30 12:09:35 -06:00
4d6eda36c0 feat: add support for building WebAssembly (WASM) targets in CI workflow 2025-12-30 12:09:29 -06:00
18200213f0 feat: add FromBytes function to create Identity from a 64-byte private key representation
Some checks failed
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 32s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 33s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 31s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 43s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 46s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 50s
Bearer / scan (push) Successful in 33s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m42s
Go Revive Lint / lint (push) Successful in 1m0s
Run Gosec / tests (push) Failing after 1m7s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 9m23s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 9m26s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 9m24s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 9m26s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m24s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m26s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-30 11:42:48 -06:00
3cb375e7f2 refactor: update timeout check in PacketReceipt to use time.Since to fix deadlock
Some checks failed
Bearer / scan (push) Successful in 7s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 32s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 34s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 33s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 34s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 36s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m2s
Go Revive Lint / lint (push) Successful in 51s
Run Gosec / tests (push) Failing after 1m19s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m38s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 9m24s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 9m26s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 9m24s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 9m22s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m26s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m25s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-30 11:31:51 -06:00
53d418d753 refactor: update Taskfile to change output paths for WASM build and add example tasks for various functionalities
Some checks failed
Bearer / scan (push) Successful in 28s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 28s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 31s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 33s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 47s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 45s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 48s
Go Revive Lint / lint (push) Successful in 53s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m38s
Run Gosec / tests (push) Failing after 1m6s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 9m23s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 9m23s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 9m25s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m23s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 9m25s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m25s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-30 11:14:37 -06:00
ac0407c954 refactor: update path request handling to use transportIdentity and improve logging with packet length 2025-12-30 01:30:35 -06:00
83abefcf43 refactor: update TestLinkProofValidation to use new ValidateLinkProof signature with network interface parameter 2025-12-30 01:27:58 -06:00
3d4b77bee3 refactor: add SIZE_48 and TOKEN_OVERHEAD constants to common size constants in constants.go 2025-12-30 01:27:50 -06:00
71b6032ea4 refactor: update ValidateLinkProof method to include network interface parameter for enhanced validation 2025-12-30 01:27:45 -06:00
3675d63418 refactor: modify ValidateLinkProof method to accept a network interface parameter for enhanced validation 2025-12-30 01:27:41 -06:00
37652b2d33 refactor: update ValidateLinkProof method to include network interface parameter for improved link validation 2025-12-30 01:27:36 -06:00
5c19b76367 fix: improve error handling in DeriveKey function to return errors for empty secret and zero length key requests 2025-12-30 01:27:30 -06:00
f96af89269 refactor: improve path request handling and link packet processing with enhanced logging and error management 2025-12-30 01:27:21 -06:00
6704c620c8 refactor: enhance link handling by adding detailed logging for request processing, establishing links, and improving error handling during encryption and decryption 2025-12-30 01:27:16 -06:00
738aa9528a refactor: encryption and decryption methods by deriving HMAC and encryption keys based on key length, and update HMAC validation logic
All checks were successful
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 38s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 32s
Bearer / scan (push) Successful in 7s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 34s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 37s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 31s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 59s
Go Revive Lint / lint (push) Successful in 48s
Run Gosec / tests (push) Successful in 1m18s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m32s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 9m26s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 9m24s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 9m26s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m24s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 9m24s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m26s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-29 23:59:58 -06:00
ff1589216b fix: simplify dependency installation step in CI workflow by renaming task and removing unnecessary setup step 2025-12-29 23:59:05 -06:00
3af2be27d8 cleanup: remove extra comments
All checks were successful
Bearer / scan (push) Successful in 31s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 32s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 31s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 31s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 44s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 47s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 54s
Go Revive Lint / lint (push) Successful in 1m5s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m50s
Run Gosec / tests (push) Successful in 1m10s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 9m23s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 9m23s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 9m25s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 9m25s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m23s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m25s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-29 23:50:03 -06:00
8e777bef03 refactor: update encryption and decryption processes by deriving key material for HMAC and encryption separately, and improve HMAC validation logic 2025-12-29 23:46:33 -06:00
220912989e fix: improve error handling for incoming link requests by logging errors when handling fails 2025-12-29 23:45:36 -06:00
177dc3a099 test: add comprehensive tests for link request handling and response processing, including mock transport and interface implementations
Some checks failed
Bearer / scan (push) Successful in 27s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 34s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 31s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 33s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 43s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 38s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 47s
Go Revive Lint / lint (push) Successful in 1m4s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m41s
Run Gosec / tests (push) Failing after 1m10s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 9m24s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 9m26s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 9m24s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 9m26s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m24s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m26s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-29 23:29:04 -06:00
df3497b725 refactor: streamline request handling logic by consolidating type checks for requested_at and request_payload, and rename max function for clarity 2025-12-29 23:28:56 -06:00
e3d65525b8 feat: add TODOs for path table persistence and improve packet handling logic in transport 2025-12-29 23:27:32 -06:00
474bb2ff33 feat: enhance destination handling by auto-registering with transport for incoming directions and adding packet reception logic 2025-12-29 23:27:23 -06:00
5707c230e2 fix: update request handling to support multiple data types for requested_at, path_hash, and request_payload, and improve logging for request processing 2025-12-29 23:27:16 -06:00
4ae59c9716 feat: implement WebSocketInterface for handling WebSocket connections in WASM environment
All checks were successful
Bearer / scan (push) Successful in 24s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 30s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 30s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 33s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 40s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 38s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 48s
Go Revive Lint / lint (push) Successful in 1m3s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m37s
Run Gosec / tests (push) Successful in 1m11s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 9m22s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 9m24s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 9m22s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m22s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 9m24s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m24s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-29 23:00:53 -06:00
59060d6002 feat: add TCPClientInterface methods for setting timeouts on Linux and OSX in new tcp_wasm.go file 2025-12-29 23:00:48 -06:00
ed413b62a1 fix: update wasm_exec.js copy logic to check both lib and misc directories for improved compatibility 2025-12-29 23:00:43 -06:00
b60d91aa17 refactor: buffer, channel, rate, and resolver packages to introduce constants for magic numbers 2025-12-29 22:38:39 -06:00
d465f103ec Add 'bearer:disable go_gosec_unsafe_unsafe' comments in tcp_linux.go to suppress specific security scanner warnings related to unsafe operations.
All checks were successful
Bearer / scan (push) Successful in 7s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 31s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 33s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 31s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 33s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 34s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m0s
Go Revive Lint / lint (push) Successful in 48s
Run Gosec / tests (push) Successful in 1m17s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m36s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 9m23s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 9m25s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 9m23s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m25s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m23s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 9m25s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-29 22:33:17 -06:00
fcfd04c0c2 Add 'bearer:disable go_gosec_filesystem_filereadtaint' comments to file loading functions across config and identity modules to suppress specific security scanner warnings 2025-12-29 22:33:07 -06:00
b25f2c2bdc Refactor Channel message handler management to use structured entries with IDs for easier identification. Update AddMessageHandler and RemoveMessageHandler methods accordingly, and adjust tests to validate new functionality. 2025-12-29 22:32:57 -06:00
7bb127526c Refactor RawChannelReader to use a map for callbacks instead of a slice, enabling callback identification by ID. Update AddReadyCallback and RemoveReadyCallback methods accordingly. Adjust tests to reflect these changes. 2025-12-29 22:32:49 -06:00
48f8288577 Update revive installation in CI workflow to use the latest version from GitHub
Some checks failed
Bearer / scan (push) Failing after 35s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 37s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 37s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 33s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 31s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 30s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 32s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 33s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 32s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 30s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 47s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 39s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 48s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Revive Lint / lint (push) Successful in 1m5s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m42s
Run Gosec / tests (push) Successful in 1m12s
2025-12-29 22:22:01 -06:00
7086926839 Add 'bearer:disable' comments to configuration and identity file loading functions to suppress security scanner warnings
Some checks failed
Bearer / scan (push) Failing after 32s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 32s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 29s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 32s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 42s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 40s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 49s
Go Revive Lint / lint (push) Failing after 24s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m43s
Run Gosec / tests (push) Successful in 1m5s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 9m23s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 9m25s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 9m23s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 9m25s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m25s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m23s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-29 22:20:48 -06:00
6c3bdaa743 Add revive installation step in CI workflow
Some checks failed
Bearer / scan (push) Failing after 35s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 39s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 41s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 28s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 28s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 28s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 29s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 31s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 32s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 34s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 33s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 28s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Revive Lint / lint (push) Failing after 24s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 56s
Run Gosec / tests (push) Successful in 1m5s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m31s
2025-12-29 22:17:32 -06:00
d8d38fdfe4 Add Bearer security scanner workflow and update README with new badge
Some checks failed
Bearer / scan (push) Failing after 25s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 31s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 31s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 29s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 38s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 31s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 30s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 28s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 35s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 31s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 34s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 30s
Go Revive Lint / lint (push) Failing after 20s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 53s
Run Gosec / tests (push) Successful in 1m3s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m31s
2025-12-29 22:14:39 -06:00
82dad74ba8 Update CI workflow to switch from 'master' to 'main' branch for SBOM updates
Some checks failed
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 30s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 29s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 33s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 32s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 35s
Go Revive Lint / lint (push) Failing after 15s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 54s
Run Gosec / tests (push) Successful in 1m2s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m34s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 9m26s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 9m24s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 9m26s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 9m24s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 9m24s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m26s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-29 22:10:09 -06:00
b630122d78 Refactor CI workflow to replace revive installation with Task setup and update linting command. 2025-12-29 22:10:04 -06:00
99e4d92e8e Update README 2025-12-29 22:09:55 -06:00
9fa712c0b1 Refactor CI workflows to utilize Task for build and test steps, add SBOM generation workflow, and remove deprecated steps.
All checks were successful
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 26s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 33s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 30s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 32s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 33s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 31s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 31s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 29s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 41s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 33s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 34s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m8s
Go Revive Lint / lint (push) Successful in 1m9s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m44s
Run Gosec / tests (push) Successful in 1m14s
2025-12-29 22:04:58 -06:00
87fc514f32 Add tasks for running tests with race detector and generating SHA256 checksums in Taskfile.yml 2025-12-29 22:04:52 -06:00
1f6f8580a8 Update README
All checks were successful
Run Gosec / tests (push) Successful in 1m6s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 51s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 50s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 54s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 52s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 57s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 47s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 43s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 48s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 46s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 51s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 49s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 46s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m42s
Go Revive Lint / lint (push) Successful in 54s
2025-12-29 21:58:48 -06:00
5a3fed8f8b Add tasks for gosec security scanning and WebAssembly builds in Taskfile.yml 2025-12-29 21:58:33 -06:00
5e4b21a431 Add gosec to flake.nix 2025-12-29 21:58:14 -06:00
aea98d4cae Add unit tests for RawChannelReader and RawChannelWriter, including methods for reading, writing, and handling messages.
Some checks failed
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 47s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 45s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 51s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 50s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 52s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 54s
Run Gosec / tests (push) Successful in 1m35s
Go Revive Lint / lint (push) Successful in 56s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m49s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 9m28s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 9m34s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 9m32s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m32s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 9m30s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 9m34s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-29 21:57:20 -06:00
8aa30a39e4 Update CONTRIBUTING.md 2025-12-29 21:40:20 -06:00
c3893eb33d refactor: replace magic numbers and string literals with constants
Some checks failed
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m34s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 49s
Go Revive Lint / lint (push) Successful in 46s
Run Gosec / tests (push) Successful in 1m21s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 49s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 1m4s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 58s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 1m1s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 41s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 4m51s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 4m53s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 4m49s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 4m57s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 4m55s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 4m59s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-29 00:25:28 -06:00
53e98c73af Add unit tests for Reticulum-Go packages including reticulum, storage, announce, channel, destination, identity, resource, and transport, ensuring comprehensive coverage of functionality.
Some checks failed
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 51s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 49s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 58s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 57s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 49s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 53s
Run Gosec / tests (push) Successful in 1m25s
Go Revive Lint / lint (push) Successful in 48s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m39s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 4m59s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 9m39s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m33s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 9m35s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 9m37s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 9m42s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-29 00:15:08 -06:00
2fc9446687 Add .dockerignore and update .gitignore to include build artifacts, test binaries, and editor configuration files. 2025-12-28 22:55:38 -06:00
49aee4818b Add unit tests for various packages including config, buffer, debug, pathfinder, rate, and resolver. 2025-12-28 22:55:28 -06:00
430290deaf Fix Gitea build workflow to verify and upload SPDX SBOM files, ensuring proper error handling and file existence checks.
All checks were successful
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 49s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 47s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 53s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 56s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 1m2s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 1m4s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 53s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 51s
Run Gosec / tests (push) Successful in 17s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 43s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 49s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 56s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 54s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Revive Lint / lint (push) Successful in 9m31s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 18m56s
2025-12-28 22:41:51 -06:00
2ae1143b19 Add flake.lock
All checks were successful
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 53s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 58s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 53s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 57s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 53s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 55s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 51s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 54s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 56s
Go Revive Lint / lint (push) Successful in 52s
Run Gosec / tests (push) Successful in 1m12s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m26s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 9m54s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 9m52s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 9m29s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-28 22:28:46 -06:00
a34c211872 refactor: format code and add more constants
Some checks failed
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 12s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 51s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 49s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 49s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 57s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 19s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 21s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 44s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 48s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 47s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 46s
Run Gosec / tests (push) Successful in 45s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Revive Lint / lint (push) Successful in 9m48s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 19m13s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 19m19s
2025-12-28 22:27:16 -06:00
fda77ba10d Revise CONTRIBUTING.md 2025-12-28 22:26:44 -06:00
9a93a08b85 Add flake.nix for Reticulum-Go development environment setup 2025-12-28 22:26:35 -06:00
25fa49ffe2 Update README 2025-12-28 22:20:15 -06:00
96c68f2aff Update TODO.md 2025-12-28 22:18:38 -06:00
30210d0714 Add Taskfile for build and development tasks. 2025-12-28 22:17:49 -06:00
e8fc291a01 Update README.md 2025-12-28 21:55:03 -06:00
ea0797de68 Update License to 0BSD 2025-12-28 21:54:55 -06:00
3cacfadf27 Update Trivy installation URL in Gitea build workflow to use internal asset repository 2025-12-28 21:44:59 -06:00
45878f0666 Add Trivy installation step to Gitea build workflow and update upload-artifact action to v3.2.1
All checks were successful
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 59s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 1m8s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 1m12s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 1m15s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 1m1s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 52s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m42s
Go Revive Lint / lint (push) Successful in 53s
Run Gosec / tests (push) Successful in 1m33s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 9m33s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 9m31s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 9m33s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m31s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m29s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 9m31s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-28 21:34:55 -06:00
483234eee0 Update Gitea build workflow to use the latest version of the upload-artifact action (v3.2.0-node20)
Some checks failed
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 1m4s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 1m3s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 1m0s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 1m1s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 48s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 53s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m41s
Go Revive Lint / lint (push) Successful in 55s
Run Gosec / tests (push) Successful in 1m39s
Go Build Multi-Platform / build (arm, freebsd) (push) Has been cancelled
Go Build Multi-Platform / Create Release (push) Has been cancelled
Go Build Multi-Platform / build (arm64, freebsd) (push) Has been cancelled
Go Build Multi-Platform / build (arm64, darwin) (push) Has been cancelled
Go Build Multi-Platform / build (arm, windows) (push) Has been cancelled
Go Build Multi-Platform / build (arm, linux) (push) Has been cancelled
Go Build Multi-Platform / build (arm64, linux) (push) Has been cancelled
2025-12-28 21:30:41 -06:00
82523cc7df Update SECURITY.md to include Gitea instance for actions, specify CycloneDX for BOM generation, and clarify vulnerability reporting process. 2025-12-28 21:21:34 -06:00
634ff693de Remove .slsa-goreleaser.yml configuration file and update Gitea build workflow to include SPDX SBOM generation and additional artifact uploads.
Some checks failed
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 1m28s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 1m30s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m33s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 53s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 57s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 1m30s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 40s
Go Revive Lint / lint (push) Successful in 1m12s
Run Gosec / tests (push) Successful in 1m29s
Go Build Multi-Platform / Create Release (push) Has been cancelled
Go Build Multi-Platform / build (amd64, darwin) (push) Has been cancelled
Go Build Multi-Platform / build (arm64, freebsd) (push) Has been cancelled
Go Build Multi-Platform / build (amd64, linux) (push) Has been cancelled
Go Build Multi-Platform / build (arm, freebsd) (push) Has been cancelled
Go Build Multi-Platform / build (arm, windows) (push) Has been cancelled
Go Build Multi-Platform / build (arm64, linux) (push) Has been cancelled
2025-12-28 21:21:20 -06:00
ea36ba7a65 Update Gitea workflows to use new action paths and add SBOM generation step
All checks were successful
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 1m18s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 1m14s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 1m12s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 1m16s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 30s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m58s
Go Revive Lint / lint (push) Successful in 1m31s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 2m1s
Run Gosec / tests (push) Successful in 2m31s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 9m48s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m43s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 9m44s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 9m46s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m41s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 9m39s
Go Build Multi-Platform / Create Release (push) Has been skipped
2025-12-28 21:07:26 -06:00
8e243a7c8b Update import paths to use the new Gitea paths
All checks were successful
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 34s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 38s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 37s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 32s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 35s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 34s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 37s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 42s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m29s
Go Revive Lint / lint (push) Successful in 47s
Run Gosec / tests (push) Successful in 1m3s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 27s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 44s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 42s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m5s
2025-12-20 20:02:49 -06:00
dd4383a526 Update module path and upgrade golang.org/x/crypto to v0.46.0 2025-12-20 20:02:28 -06:00
75ddddd537 Remove windows/mac due to gitea runner limitation
All checks were successful
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 24s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 35s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 37s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 41s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 53s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 40s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 35s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 33s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 42s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 41s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 39s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 58s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m34s
Go Revive Lint / lint (push) Successful in 54s
Go Build Multi-Platform / Create Release (push) Has been skipped
Run Gosec / tests (push) Successful in 1m0s
2025-12-20 19:50:17 -06:00
6b3fae179f Remove codeql
Some checks failed
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 39s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 37s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 52s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 54s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 37s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 36s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 34s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 32s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 34s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 38s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 39s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 58s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m26s
Go Revive Lint / lint (push) Successful in 49s
Go Build Multi-Platform / Create Release (push) Has been skipped
Run Gosec / tests (push) Successful in 1m3s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-12-20 19:27:28 -06:00
a1834f35f7 Rename to .gitea instaed of .github
Some checks failed
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 41s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 40s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 42s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 37s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 46s
Go Revive Lint / lint (push) Successful in 54s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 41s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 42s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 35s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 43s
CodeQL Advanced / Analyze (go) (autobuild, go) (push) Failing after 34s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 39s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 37s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 58s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m17s
Run Gosec / tests (push) Successful in 1m14s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-12-20 19:24:08 -06:00
804e8ed997 Update README 2025-12-20 19:23:48 -06:00
c46d15fc90 Remove 2025-12-20 19:23:38 -06:00
06ac57c677 Update GitHub Actions workflows to use updated artifact upload and download actions
Some checks failed
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 1m5s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 50s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 1m7s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 40s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 38s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 42s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 36s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 40s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 34s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 37s
CodeQL Advanced / Analyze (go) (autobuild, go) (push) Failing after 35s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 37s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 57s
Go Revive Lint / lint (push) Successful in 54s
Run Gosec / tests (push) Successful in 59s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m22s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-12-20 19:22:38 -06:00
ea5c9147da Update README
Some checks failed
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 48s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 51s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 47s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 53s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 24s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 36s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 36s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 35s
CodeQL Advanced / Analyze (go) (autobuild, go) (push) Failing after 39s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 41s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 57s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 44s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 57s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Revive Lint / lint (push) Successful in 57s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m18s
Run Gosec / tests (push) Successful in 1m24s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-12-20 19:19:45 -06:00
49bc7fe7f6 Remove deepsource 2025-12-20 19:19:39 -06:00
9b1f45ff77 Update module path to remove the GitHub 2025-12-20 19:19:30 -06:00
0efd83f0e8 Update TODO
Some checks failed
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 1s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 1s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 1s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 1s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 1s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 1s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Failing after 1s
Run Gosec / tests (push) Failing after 1s
Go Revive Lint / lint (push) Failing after 1s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 1s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 1s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 1s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 1s
CodeQL Advanced / Analyze (go) (autobuild, go) (push) Failing after 1s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Failing after 1s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 3m42s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-12-06 13:11:22 -06:00
7dc9f94771 Update SECURITY.md 2025-12-06 13:11:17 -06:00
ae729c2ca7 Configure SLSA Go releaser to set CGO_ENABLED=0 and update Go version handling in workflow
Some checks failed
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 2s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 2s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 3s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 1s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 4s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 4s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 29s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 4s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 35s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 34s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Failing after 2s
Run Gosec / tests (push) Failing after 1s
Go Revive Lint / lint (push) Failing after 1s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 29s
Go Build Multi-Platform / Create Release (push) Has been skipped
CodeQL Advanced / Analyze (go) (autobuild, go) (push) Failing after 5m59s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 6m31s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-12-06 13:08:27 -06:00
3b3389118f Update to use version instead of hash
Some checks failed
Run Gosec / tests (push) Successful in 40s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 2s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 1s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 1s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 2s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 2s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 1s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 4s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 3s
CodeQL Advanced / Analyze (go) (autobuild, go) (push) Failing after 4s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 30s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 32s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 29s
Go Revive Lint / lint (push) Failing after 4s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 41s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m4s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-12-06 13:01:18 -06:00
66b11b4f39 update slsa releaser action hash
Some checks failed
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 3s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 1s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 1s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 2s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 1s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 1s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 2s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 4s
CodeQL Advanced / Analyze (go) (autobuild, go) (push) Failing after 7s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Failing after 4s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 37s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 35s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 33s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 42s
Go Revive Lint / lint (push) Successful in 40s
Run Gosec / tests (push) Successful in 42s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-12-06 12:59:04 -06:00
afc0779edf Add CodeQL workflow for Go analysis with scheduled runs and pull request triggers
Some checks failed
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 2s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 2s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 2s
CodeQL Advanced / Analyze (go) (autobuild, go) (push) Failing after 4s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Failing after 3s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 3s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 2s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 2s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 2s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 5s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 34s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 36s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 32s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 43s
Go Revive Lint / lint (push) Successful in 40s
Run Gosec / tests (push) Successful in 42s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-12-06 12:46:45 -06:00
a6546d3566 Update SLSA Go releaser workflow to use specific commit hash for builder configuration 2025-12-06 12:46:39 -06:00
e59fee8e60 Add SLSA Go releaser configuration and workflow files for automated builds and releases
Some checks failed
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 5s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 5s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 29s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 32s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 31s
Run Gosec / tests (push) Failing after 2s
Go Revive Lint / lint (push) Failing after 1s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 45s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 3s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 3s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 1s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 1s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 2s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 37s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m9s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-12-06 12:42:07 -06:00
395b180757 Minor correction
Some checks failed
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 3s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 2s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 2s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 2s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 2s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 4s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Failing after 4s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 34s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 33s
Go Build Multi-Platform / Create Release (push) Has been skipped
Run Gosec / tests (push) Successful in 41s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 2s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 4s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 36s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 42s
Go Revive Lint / lint (push) Successful in 37s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-12-06 12:38:28 -06:00
b743f57690 Improve error handling in processPathRequest by logging failures during local destination announcement and path request sending
Some checks failed
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 2s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 4s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 5s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Failing after 4s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 36s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 38s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Revive Lint / lint (push) Successful in 1m18s
Run Gosec / tests (push) Successful in 1m33s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 2s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 2s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 2s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 2s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 3s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 34s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m19s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-12-06 12:19:46 -06:00
bd0c94109e Improve error handling in ValidateLinkProof by logging failures during RTT packet transmission 2025-12-06 12:19:37 -06:00
6896672562 Update AES256 CBC decryption test 2025-12-06 12:19:32 -06:00
fb69af9bf4 Update Reticulum initialization to incorporate storage management and identity handling
Some checks failed
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 2s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 1s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Failing after 1s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Failing after 1s
Go Revive Lint / lint (push) Failing after 1s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 1s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 1s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 1s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 1s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 1s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 1s
Run Gosec / tests (push) Failing after 1s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 46s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 1m8s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 1m6s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-12-06 12:14:20 -06:00
dd87da4a51 Fix packet handling in Transport to clarify link data processing
Some checks failed
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 22s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 30s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 29s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 27s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 33s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 33s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 31s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Failing after 32s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Failing after 37s
Go Revive Lint / lint (push) Successful in 34s
Run Gosec / tests (push) Failing after 41s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 31s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 30s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 31s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 32s
2025-12-01 20:52:04 -06:00
c8d231556c Add channel handling to Link struct; implement methods for managing incoming channel packets, resource advertisements, and packet delivery. Enhance initialization with IncomingLinkHandler registration and improve error handling for resource requests.
Some checks failed
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 17s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 25s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 31s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 33s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 31s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 23s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 40s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 38s
Go Revive Lint / lint (push) Successful in 30s
Run Gosec / tests (push) Failing after 57s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 29s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 27s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 33s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Failing after 35s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Failing after 36s
2025-12-01 20:30:59 -06:00
b489135c5b Enhance Transport struct with path request handling and cleanup mechanisms; introduce DiscoveryPathRequest and PathAnnounceEntry types for improved path management, and implement maintenance jobs for expired paths, discovery requests, and announces. 2025-12-01 20:30:53 -06:00
1d83e7f539 Add request handling and metadata management to Resource struct; introduce methods for request ID, response status, and hashmap access, while removing deprecated IsCompressed method for cleaner code. 2025-12-01 20:30:42 -06:00
565b13a0eb Add ResourceAdvertisement struct and related methods for managing resource advertisements; implement packing and unpacking functionality with msgpack for efficient data handling. 2025-12-01 20:30:37 -06:00
52f8e21da0 Refactor Destination struct to enhance request handling and link management; introduce IncomingLinkHandler for improved link processing and update transport interface usage. 2025-12-01 20:30:32 -06:00
ad5b6ed83a Add LoadOrCreateTransportIdentity function to manage transport identity loading and creation; implement storage path handling and error logging for improved identity management. 2025-12-01 20:30:27 -06:00
cced3f5092 Add HandleInbound method for processing incoming channel packets; introduce GenericMessage struct for message handling and packing/unpacking functionality. 2025-12-01 20:30:22 -06:00
3b2a8591a7 Update link management by adding RegisterLink and UnregisterLink methods; improve handleLinkPacket and handleProofPacket for better link validation and error logging.
Some checks failed
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 39s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 43s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 41s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 45s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 14s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 13s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 20s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 21s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 20s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 21s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 20s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Failing after 1m15s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Failing after 1m17s
Go Revive Lint / lint (push) Successful in 1m11s
Run Gosec / tests (push) Failing after 1m30s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-12-01 20:02:02 -06:00
f513c6abcd Add RESPONSE_MAX_GRACE_TIME constant to resource.go for enhanced timeout management 2025-12-01 20:01:54 -06:00
e83bc31ccc Update Link management with improved request handling, timeout mechanisms, and resource management; refactor packet handling for better clarity and efficiency. 2025-12-01 20:01:48 -06:00
1e67bc56b6 Improve Destination struct with better request handling and link management 2025-12-01 20:01:31 -06:00
b0e6ce93f9 Update README.md
Some checks failed
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 21s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 30s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 33s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 36s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 40s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 34s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 52s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 32s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 31s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 36s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 38s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 49s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m20s
Go Revive Lint / lint (push) Successful in 45s
Run Gosec / tests (push) Successful in 56s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-11-21 15:19:27 -06:00
0d2239be83 suppress errors on file removal in SaveRatchet and LoadRatchets methods for improved error handling
Some checks failed
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 35s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 33s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 33s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 34s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 36s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 30s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 27s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 26s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 33s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 32s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 54s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 45s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m37s
Go Revive Lint / lint (push) Successful in 47s
Run Gosec / tests (push) Successful in 1m22s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-11-21 12:45:40 -06:00
cfcdb62168 Add HandleIncomingLinkRequest and GetLinkID methods for improved link handling and status retrieval; enhance logging for link proof generation and sending process.
Some checks failed
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 26s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 34s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 32s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 46s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 35s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Has been cancelled
Run Gosec / tests (push) Has been cancelled
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Has been cancelled
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 38s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 44s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 42s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 41s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 27s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 43s
Go Build Multi-Platform / Create Release (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
Go Revive Lint / lint (push) Has been cancelled
2025-11-21 12:43:22 -06:00
d14692b19b Add FromFile and loadPrivateKey methods to Identity for loading identity from file and initializing keys 2025-11-21 12:43:16 -06:00
ca3bef0635 implement storage manager for ratchet key management with directory initialization and ratchet data handling 2025-11-21 12:43:10 -06:00
59486330ec update HandleIncomingLinkRequest to accept packet instead of linkID and add HandleRequest method for improved request handling 2025-11-21 12:43:00 -06:00
1133e9755d Add better logging in HandlePacket and handleLinkPacket for better error tracking and clarity 2025-11-21 12:42:18 -06:00
b450aa8569 refactor: update delivery callback in TestPacketReceiptCallbacks to use channel for improved synchronization
Some checks failed
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 33s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 20s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 30s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 38s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 39s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 36s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Build Multi-Platform / build (arm, windows) (push) Failing after 34s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 30s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m25s
Go Revive Lint / lint (push) Successful in 44s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 29s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 31s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 44s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 43s
Go Build Multi-Platform / Create Release (push) Has been skipped
Run Gosec / tests (push) Successful in 59s
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-11-20 21:49:50 -06:00
6aef9e9337 update revive linter rules
Some checks failed
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 31s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 45s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Failing after 47s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Revive Lint / lint (push) Successful in 33s
Run Gosec / tests (push) Successful in 54s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 22s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 24s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 29s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 46s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 45s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 33s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 34s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 35s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 33s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 33s
2025-11-20 21:46:15 -06:00
678ab32ee6 refactor: simplify index increment for IFAC code handling in handleAnnouncePacket for improved readability 2025-11-20 21:45:49 -06:00
4e37112aee refactor: replace log statements with debug logging in packet.go for improved clarity and consistency 2025-11-20 21:45:44 -06:00
af046370db refactor: replace log statements with debug logging in link.go for improved clarity and consistency 2025-11-20 21:45:39 -06:00
eb40fd6451 refactor: remove flaky address resolution checks in TestNewUDPInterface for improved test reliability 2025-11-20 21:45:35 -06:00
763fb4a962 refactor: improve variable initialization in GetRTT method for better clarity 2025-11-20 21:45:30 -06:00
673f19b1ff feat: add OpenBSD support for TCP keepalive configuration in TCPClientInterface 2025-11-20 21:45:25 -06:00
4a20551e9a feat: add NetBSD support for TCP keepalive configuration in TCPClientInterface 2025-11-20 21:45:20 -06:00
1ac5696c80 refactor: improve variable initialization and return types in Identity methods for clarity 2025-11-20 21:45:15 -06:00
5c2ea259b8 refactor: replace log statements with debug logging for packet resend errors in channel.go 2025-11-20 21:45:10 -06:00
7573e942f1 refactor: streamline Read and Close methods in buffer.go for improved clarity and efficiency 2025-11-20 21:45:06 -06:00
d3cf775394 refactor: simplify RequestPath method by removing unnecessary error handling 2025-11-20 21:45:01 -06:00
5e19f6f802 fix: update TargetHost for Quad4 TCP interface from rns.quad4.io to rns2.quad4.io 2025-11-20 21:44:51 -06:00
72d70b2141 feat: add receipt management for packet handling, including registration, unregistration, and proof validation 2025-11-20 21:31:36 -06:00
06da42a148 feat: implement PacketReceipt struct with methods for receipt creation, validation, and timeout handling 2025-11-20 21:31:31 -06:00
ec8b843cd4 feat: enhance Packet struct with new fields and hash methods for improved functionality 2025-11-20 21:31:24 -06:00
9e7e9a71ca feat: add link establishment tests and implement key generation, handshake, and proof validation in the Link module 2025-11-20 21:31:18 -06:00
b0669954a4 code cleanup 2025-11-20 21:25:50 -06:00
ded5853026 move platformGetRTT implementation to tcp_common.go for non-Linux platforms 2025-11-20 21:24:58 -06:00
426422413c Update TODO 2025-11-20 21:24:36 -06:00
50c8546344 chore: update golang.org/x/crypto dependency to v0.45.0 in go.mod and go.sum
Some checks failed
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 33s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 37s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 37s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 39s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 37s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 36s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 33s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 33s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 52s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 34s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 34s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 30s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m20s
Go Revive Lint / lint (push) Successful in 48s
Run Gosec / tests (push) Successful in 50s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-11-20 18:22:08 -06:00
b0fad14504 fix: add security comment for TCP_INFO syscall in platformGetRTT function
Some checks failed
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 23s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 24s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 39s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 41s
Go Build Multi-Platform / build (arm64, freebsd) (push) Has been cancelled
Go Build Multi-Platform / build (arm64, linux) (push) Has been cancelled
Go Build Multi-Platform / build (arm64, windows) (push) Has been cancelled
Go Build Multi-Platform / Create Release (push) Has been cancelled
Go Build Multi-Platform / build (arm, linux) (push) Has been cancelled
Go Build Multi-Platform / build (arm, windows) (push) Has been cancelled
Go Revive Lint / lint (push) Has been cancelled
Go Build Multi-Platform / build (arm, freebsd) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Has been cancelled
Go Build Multi-Platform / build (arm64, darwin) (push) Has been cancelled
Run Gosec / tests (push) Has been cancelled
2025-11-20 18:21:14 -06:00
24aa4fa88b remove default RTT implementation for non-Linux platforms and enhance keepalive period configuration for FreeBSD and Windows
Some checks failed
Go Build Multi-Platform / build (arm, linux) (push) Failing after 38s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 33s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 41s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 31s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 22s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 29s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 37s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 31s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 30s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 34s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 33s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m0s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Revive Lint / lint (push) Successful in 48s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m25s
Run Gosec / tests (push) Failing after 58s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-11-20 18:14:57 -06:00
189645940c fix: add security comments to clarify handling of non-critical errors in ratchet file operations
Some checks failed
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 35s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 39s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 38s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 30s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 29s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 33s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 32s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 35s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 36s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 36s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 34s
Run Gosec / tests (push) Failing after 58s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 46s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m28s
Go Revive Lint / lint (push) Successful in 55s
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-11-20 18:04:30 -06:00
4f37df2fc0 fix: add security comments to handle non-critical errors in ratchet persistence 2025-11-20 18:04:26 -06:00
4859f5513a fix: add security comment for Unix timestamp handling in CreatePacket function 2025-11-20 18:04:22 -06:00
2a2d6d6515 feat: add platform-specific TCP timeout configurations for Darwin, FreeBSD, Windows, and Linux 2025-11-20 18:04:11 -06:00
d0c111d2f5 Update README
Some checks failed
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 42s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 46s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 41s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 37s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 44s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 40s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 38s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 41s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 38s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Revive Lint / lint (push) Successful in 57s
Run Gosec / tests (push) Failing after 1m17s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 40s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 42s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m33s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 57s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-11-20 17:57:58 -06:00
3d943aaaef refactor: improve announce packet handling by clarifying address extraction and validation logic
Some checks failed
Go Build Multi-Platform / build (arm, freebsd) (push) Has been cancelled
Go Build Multi-Platform / build (arm, linux) (push) Has been cancelled
Go Build Multi-Platform / build (arm, windows) (push) Has been cancelled
Go Build Multi-Platform / build (arm64, darwin) (push) Has been cancelled
Go Build Multi-Platform / build (amd64, windows) (push) Has been cancelled
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Has been cancelled
Run Gosec / tests (push) Has been cancelled
Go Revive Lint / lint (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Build Multi-Platform / build (amd64, linux) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Has been cancelled
Go Build Multi-Platform / build (amd64, freebsd) (push) Has been cancelled
Go Build Multi-Platform / build (arm64, freebsd) (push) Has been cancelled
Go Build Multi-Platform / build (arm64, linux) (push) Has been cancelled
Go Build Multi-Platform / build (arm64, windows) (push) Has been cancelled
Go Build Multi-Platform / Create Release (push) Has been cancelled
Go Build Multi-Platform / build (amd64, darwin) (push) Has started running
2025-11-20 17:57:13 -06:00
d5d7fbdb79 refactor: simplify comments in getHashablePart function for clarity 2025-11-20 17:57:07 -06:00
fdfe895d2d feat: enhance TCP and UDP interfaces with improved timeout settings and MTU configuration 2025-11-20 17:57:00 -06:00
01e639133b feat: enhance AutoInterface with improved configuration and multicast handling 2025-11-20 17:56:54 -06:00
6c6953e664 refactor: update identity management to use raw byte storage and msgpack for ratchet persistence 2025-11-20 17:56:48 -06:00
b4039dc148 feat: implement ratchet management with persistence and rotation 2025-11-20 17:56:40 -06:00
ba18fba43e Fix announce packet and debug logging 2025-11-20 17:56:29 -06:00
f4a929ce3a Update README.md
Some checks failed
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 41s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 46s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 44s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 33s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 33s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 32s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 55s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 53s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 43s
Run Gosec / tests (push) Successful in 58s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 48s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 35s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 51s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m29s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Revive Lint / lint (push) Successful in 45s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-11-13 10:30:50 -06:00
fe66163ef7 remove 2025-11-13 10:30:41 -06:00
2cc34172c8 remove workflows 2025-11-13 10:30:32 -06:00
9331c4edbd add donation information and funding options
Some checks failed
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 1m15s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 31s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 33s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 1m19s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 1m17s
Benchmark GC Performance / benchmark (push) Successful in 1m42s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 29s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 26s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 26s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 32s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m8s
Run Gosec / tests (push) Successful in 1m0s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 30s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 31s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m0s
Go Revive Lint / lint (push) Successful in 25s
Performance Monitor / performance-monitor (push) Successful in 2m27s
2025-11-10 12:57:25 -06:00
f097bb3241 security: use net.JoinHostPort instead of fmt.Sprintf
Some checks failed
Benchmark GC Performance / benchmark (push) Successful in 1m19s
Run Gosec / tests (push) Successful in 48s
Go Revive Lint / lint (push) Successful in 34s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 31s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 34s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 29s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 27s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 48s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m28s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 27s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 27s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 27s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 27s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 27s
Performance Monitor / performance-monitor (push) Successful in 2m40s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 25s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 27s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-11-09 00:05:36 -06:00
22fc5093db perf: combine multiple append calls in transport 2025-11-09 00:05:31 -06:00
fc95e54b2e update: Makefile with debug build target and update help text
Some checks failed
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 55s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 58s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 54s
Benchmark GC Performance / benchmark (push) Successful in 1m28s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 48s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 46s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 29s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 28s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m32s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 50s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 36s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 38s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 40s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 46s
Run Gosec / tests (push) Successful in 51s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Revive Lint / lint (push) Successful in 36s
Performance Monitor / performance-monitor (push) Has been cancelled
2025-11-09 00:01:19 -06:00
636d400f1e refactor: migrate to structured debug logging 2025-11-09 00:00:55 -06:00
fd5eb65bc0 fix: use context.TODO() in logger.Enabled check for improved clarity 2025-11-07 12:51:14 -06:00
4e13fe523b fix: resolve deepsource linter issues
- Fixed the append usage warning (CRT-D0001)
- Fixed the nil context warning (SCC-SA1012)
2025-11-07 12:47:47 -06:00
dd2cc3e3d9 update logging function to improve argument handling 2025-11-07 12:44:40 -06:00
353e9c6d9b Update logging to use new debug package
- Removed the old debug logging functions and replaced them with calls to the new debug package.
- Updated logging statements throughout the main.go file to utilize adjustable logging levels.
- Enhanced the clarity and structure of log messages for better debugging and traceability.
2025-11-07 12:41:29 -06:00
088ba3337d Add debug package for logging with adjustable levels
- Introduced a new debug package that allows for logging at various levels (1-7).
- Implemented initialization, logger retrieval, and logging functions.
- Added functionality to set and get the current debug level.
2025-11-07 12:41:14 -06:00
4cd2338095 Update README.md 2025-11-06 15:12:16 -06:00
c6cc1d8ca8 Update 2025-10-31 07:39:51 -05:00
0afb0e9ade Update 2025-10-31 07:39:41 -05:00
feeaa72102 Update GitHub Actions workflows
- Pin to full -length commit hash
- Add master alongside main
2025-10-31 07:39:34 -05:00
bb964445f3 add examples to gitignore for now 2025-10-30 18:57:48 -05:00
5369037a74 Add TinyGo build targets to Makefile and create GitHub Actions workflow for TinyGo builds 2025-10-30 18:54:23 -05:00
bb98248830 Update Go module dependencies and version in go.mod and go.sum files 2025-10-30 18:49:23 -05:00
575657bbc5 Remove cyclomatic and cognitive complexity rules from revive.toml configuration 2025-10-26 11:49:48 -05:00
8da4a759f5 Add FromHash method to create destinations from known hashes and enhance link handling capabilities 2025-10-07 22:44:08 -05:00
dff1489ee5 Add link ID generation in Establish method for connection requests 2025-10-07 22:44:02 -05:00
30c97bc9dd Implement identity recall functionality to retrieve existing identities by hash 2025-10-07 22:43:55 -05:00
005e2566aa Add destination registration and link handling in Transport 2025-10-07 22:43:45 -05:00
cc10830df3 Fix CreatePacket method in Announce struct to dynamically set context flag based on the existence of ratchet data. 2025-10-07 21:53:39 -05:00
b548e5711e Add destination type constants for packet handling in types.go 2025-10-07 21:31:45 -05:00
cc89bfef6e Update destination hash calculation in handleAnnouncePacket to use SHA256. 2025-10-07 21:31:40 -05:00
45a3ac1e87 Fix destination hash calculation by incorporating SHA256 for name hashing. 2025-10-07 21:31:16 -05:00
e39936ac30 Fix Announce struct to include destination name and hash validation.
Also update New and NewAnnounce functions to require destinationHash and destinationName parameters.
2025-10-07 21:30:57 -05:00
b601ae1c51 Add read loop for UDP interface to handle incoming packets asynchronously. 2025-10-07 21:30:27 -05:00
7d7a022736 Refine CPU usage calculation in performance monitoring script by truncating decimal values before summation. 2025-09-27 05:56:31 -05:00
0ac2a8d200 Add binary execution tests for Linux, macOS, and Windows in CI workflow. Enhance ARM cross-compilation testing with specific environment variables. 2025-09-27 05:54:30 -05:00
f3808a73e1 Remove caching step for Go modules in performance monitoring workflow 2025-09-27 05:51:43 -05:00
cb908fb143 Increase monitoring duration 2025-09-27 05:50:29 -05:00
f53194be25 Fix announce packet handling to align with RNS specification. Enhance payload parsing, signature verification, and destination hash validation. Improve logging for better debugging of announce packet processing. 2025-09-27 05:48:33 -05:00
ad732d1465 mark interface as online 2025-09-27 05:47:28 -05:00
b70a7d03af Add autointerface for testing 2025-09-27 05:47:17 -05:00
911fe3ea8e Add support for 32-byte Ed25519 2025-09-27 05:46:51 -05:00
b59bb349dc add basic performance monitoring action 2025-09-27 05:44:47 -05:00
08cbacd69f Update README badges for clarity and consistency in multi-platform testing 2025-09-27 05:44:03 -05:00
9a70a92261 Add support for multi-platform testing 2025-09-27 05:43:54 -05:00
be34168a1b Refine comment in TCPClientInterface to clarify HDLC framing usage for TCP connections 2025-09-27 04:41:36 -05:00
cebab6b2f3 Add debug logging and missing packet data 2025-09-27 04:41:25 -05:00
fdcb371582 Fix announce packet creation and sending logic to use the announce package. Enhance error handling and logging for interface checks during packet transmission. 2025-09-27 04:40:47 -05:00
f01b1f8bac Update Decrypt method in Identity to validate token structure and HMAC. Update extraction logic for ephemeral public key, ciphertext, and MAC, ensuring proper error handling for token size and HMAC validation. 2025-09-27 04:40:35 -05:00
a0eca36884 Update logging in HandlePacket and handleAnnouncePacket. 2025-09-27 04:30:59 -05:00
972d00df92 Fix TCPClientInterface readLoop and handlePacket methods to streamline HDLC framing logic and improve packet handling. Remove KISS framing support and update logging for received packets. Ensure outgoing data uses HDLC framing consistently. 2025-09-27 04:30:28 -05:00
483b6e562b Update announce packet creation and sending logic to utilize transport methods 2025-09-27 04:30:13 -05:00
cbb5ffa970 Cleanup incorrect or outdated code 2025-09-27 04:29:59 -05:00
b7cc0c39b4 Fix announce data parsing to include ratchet field and update length checks 2025-09-27 04:26:22 -05:00
982c173760 Add GitHub Actions workflow for benchmarking GC performance 2025-09-25 13:13:39 -05:00
49ca73ab3a Update TODO 2025-09-25 13:11:56 -05:00
43b224b4d7 Update README 2025-09-25 13:11:46 -05:00
456a95d569 Add benchmarking tests for packet and transport operations 2025-09-25 12:42:44 -05:00
53b2d18a79 Add benchmarking targets to Makefile for standard and experimental GC 2025-09-25 12:42:06 -05:00
8d7f86e15a Update README 2025-09-25 12:28:24 -05:00
40213eeac9 Add experimental greenteagc support, release build (strip debug symbols) and lint 2025-09-25 12:24:14 -05:00
5cb8b12a0f Update Dockerfiles to use Go version 1.25 with ARG for flexibility 2025-09-25 03:31:57 -05:00
2f165186d1 Update README 2025-09-25 03:29:24 -05:00
6cd3b15d78 Update Go version to 1.25 in workflow files 2025-09-25 03:29:16 -05:00
98c8d35f1e Update Go version to 1.25 and upgrade golang.org/x/crypto dependency to v0.42.0 2025-09-25 03:29:09 -05:00
064b2b10b8 Update revive.toml rules 2025-09-25 03:27:49 -05:00
a8d78d2784 Add Dockerfiles 2025-09-21 02:35:05 -05:00
5a0c70190f Add full-length commit hashes for actions for improved supply chain security. 2025-09-21 02:20:58 -05:00
d5bf7dc720 Update 2025-09-07 02:40:33 -05:00
8b4bca7939 Update interface registration logging 2025-09-07 02:24:37 -05:00
c004ff1a97 Fix packet header handling in Pack and Unpack methods to correct order of DestinationHash and TransportID. 2025-09-07 02:24:07 -05:00
38323da57d Fix hash calculation in Destination to use TruncatedHash and improve logging for interface announcements 2025-09-07 02:23:30 -05:00
2ffd12b3e1 Add Send method to TCPClientInterface and better logging in UDPInterface 2025-09-07 02:21:48 -05:00
069d4163eb Update README 2025-09-07 01:58:53 -05:00
93e1317789 Update wording for usage of LLMs 2025-09-07 01:58:41 -05:00
3b270e05c4 Update Go version to 1.24.6 and upgrade golang.org/x/crypto to v0.41.0 2025-09-07 01:58:22 -05:00
a05818b3a7 Add matrix link 2025-08-18 03:08:40 -05:00
df2b0a0079 Remove outdated development guidelines from CONTRIBUTING.md 2025-08-16 19:34:23 -05:00
c507e9125b Update README to include Go installation instructions 2025-08-16 19:34:15 -05:00
767110f3d0 Merge pull request #2 from MikeColes/paramter-order
match order of parameters to called function
2025-08-10 17:09:49 -05:00
Mike Coles
8e5f193caf match order of parameters to called function 2025-08-07 11:12:09 -04:00
fed33aadff add badge 2025-07-15 14:08:22 -05:00
d0c83ec1a2 update revive workflow 2025-07-15 14:06:18 -05:00
aa94bee606 fix workflow permissions 2025-07-15 14:02:22 -05:00
745609423f update 2025-07-15 14:01:10 -05:00
16e1c7e4eb add revive workflow 2025-07-15 13:59:27 -05:00
aec3672228 update 2025-07-15 13:55:04 -05:00
aace3abd6d update build workflow 2025-07-15 13:53:26 -05:00
ca3fefaae8 Add workflow permissions 2025-07-15 13:51:32 -05:00
d4f89735f6 add bearer 2025-07-15 13:51:13 -05:00
b37d393286 Update SECURITY.md to simplify vulnerability reporting instructions. 2025-07-15 13:51:07 -05:00
5e0c829cf6 Fix: Address various static analysis warnings
- **pkg/announce/announce.go**: Added error handling for `rand.Read` to log potential issues when generating random hashes.
- **pkg/buffer/buffer.go**: Removed a redundant `#nosec G115` comment as the line no longer triggers the warning.
- **pkg/cryptography/aes.go**: Added `#nosec G407` to explicitly acknowledge the use of `cipher.NewCBCEncrypter` which is acceptable in this context.
- **pkg/transport/transport.go**: Removed redundant `#nosec G115` comments as the lines no longer trigger the warning.
2025-07-15 13:45:48 -05:00
a80f2bb2ac Add a GetConfig method to the Transport struct. 2025-07-15 13:40:28 -05:00
7de206447a Migrate all AES encryption to AES-256-CBC and implement persistent ratchet storage. 2025-07-15 13:40:20 -05:00
f740514e2b Fix Destination announcing to use a dedicated announce package and improve transport integration. 2025-07-15 13:40:11 -05:00
b907dd93f1 Announce packet creation to strictly follow Reticulum specification. 2025-07-15 13:39:49 -05:00
011a6303eb Use destination-based announcing and consolidate ratchet path handling. 2025-07-15 13:39:39 -05:00
12f487d937 use AES-256-CBC only 2025-07-15 13:31:19 -05:00
b9aebc8406 gosec fixes and added #nosec where necassary 2025-07-06 00:33:50 -05:00
ffb3c3d4f4 Update Go version and x/crypto dependency to latest stable versions. 2025-07-06 00:09:53 -05:00
f291ba74e9 update 2025-07-06 00:09:41 -05:00
6e87fc9bcd go fmt 2025-07-06 00:09:14 -05:00
cb402e2bb6 add badges 2025-07-06 00:07:19 -05:00
fe5101340a Update TODO with AES 256 completion 2025-07-06 00:05:34 -05:00
dfac66e8bc add workflows 2025-07-06 00:05:11 -05:00
bc05835dae Add AES 256 and update AES test 2025-07-05 23:59:59 -05:00
Ivan
26371cdb6a Code cleanup of unused functions/variables 2025-05-07 18:35:45 -05:00
Ivan
41db0500af update x/crypto v0.37.0 > v0.38.0 2025-05-07 18:28:29 -05:00
Ivan
8114c3bda4 Add unit tests for configuration, cryptography, interfaces, and packet handling. 2025-05-07 18:24:52 -05:00
Ivan
3f141bf93b update 2025-05-07 18:24:07 -05:00
Ivan
a9bf658b03 update with badge 2025-05-07 18:23:50 -05:00
Ivan
ae9a35e3bb update 2025-05-04 03:47:13 -05:00
Ivan
32d32380d8 update license 2025-05-04 03:47:09 -05:00
Ivan
5e40f0bfe8 fix 2025-04-18 22:54:43 -05:00
315b35fc81 update 2025-04-18 22:53:45 -05:00
54dec6aa89 add 2025-04-18 22:53:42 -05:00
92c8faec11 update 2025-04-18 22:53:38 -05:00
2aff4989e5 Updated 2025-04-18 22:42:21 -05:00
f1d2a31be6 Updated 2025-04-18 22:42:12 -05:00
f604d1a3c8 create TODO 2025-04-18 22:41:54 -05:00
26a54436f7 remove 2025-04-18 22:41:48 -05:00
2fd85a1034 update x/crypto 2025-04-15 12:48:44 -05:00
c8e81cd9f0 Enhance node announcement handling and packet structure. Introduce node-specific metadata in the Reticulum struct, update announce packet creation to support new formats, and improve validation checks for announce data. Adjust minimum packet size requirements and refactor related functions for clarity and consistency. 2025-03-29 18:12:47 -05:00
2f61ce9bf3 remove github actions for now 2025-03-16 21:20:58 -05:00
b647e7c6c2 update 2025-03-12 00:25:39 -05:00
6b3990d399 more platforms 2025-03-11 23:06:56 -05:00
041b439a66 fix RTT for specific platforms 2025-03-11 23:06:26 -05:00
534982b99d remove old code 2025-03-11 22:51:38 -05:00
7379d07aba add revive config 2025-03-11 22:51:31 -05:00
03345bc256 update 2025-03-11 22:51:25 -05:00
e486923e8f update 2025-03-11 22:32:36 -05:00
d7f41b785f update 2025-03-11 22:22:55 -05:00
15303a21dc update 2025-03-11 22:22:51 -05:00
4d4863aeeb format 2025-03-11 22:18:40 -05:00
76a4103a56 cleanup 2025-03-11 22:18:09 -05:00
96348ce349 add: gosec 2025-03-11 22:18:02 -05:00
Sudo-Ivan
322711ba20 update 2025-02-14 13:06:11 -06:00
Sudo-Ivan
772248b31f update Go to 1.24 2025-02-14 13:05:47 -06:00
Ivan
fa1c80169e Update README.md 2025-02-14 16:04:46 +00:00
Sudo-Ivan
cb1e4a1115 update 2025-01-25 13:55:19 -06:00
Sudo-Ivan
836e97b17d add 2025-01-25 13:47:32 -06:00
Sudo-Ivan
87d3b4a58b update interfaces 2025-01-05 16:24:58 -06:00
Sudo-Ivan
77729e07e1 packet interceptor 2025-01-04 18:37:53 -06:00
Sudo-Ivan
79e1caa815 0.3.7 - announce packet improvements, cryptgraphy pkg, cleanup 2025-01-04 18:20:36 -06:00
Sudo-Ivan
a5b905bbaf cleanup/sort 2025-01-04 18:17:33 -06:00
Sudo-Ivan
c870406244 move cryptography to its own folder/files 2025-01-04 18:17:13 -06:00
Sudo-Ivan
ea8daf6bb2 0.3.6 - announce packet creation 2025-01-02 14:10:31 -06:00
Sudo-Ivan
d79406e354 update 2025-01-02 13:51:21 -06:00
Sudo-Ivan
f9b8d29780 update 2025-01-02 13:45:57 -06:00
Sudo-Ivan
0cebfb2193 update 2025-01-01 19:14:36 -06:00
Sudo-Ivan
9e229287e8 update 2025-01-01 19:12:40 -06:00
Sudo-Ivan
9508e6e195 update packet creation 2025-01-01 19:12:32 -06:00
Sudo-Ivan
5acbef454f 0.3.5 2025-01-01 18:31:58 -06:00
Sudo-Ivan
0862830431 0.3.4 2025-01-01 17:00:11 -06:00
Sudo-Ivan
6cdc02346f update 2025-01-01 03:12:26 -06:00
Sudo-Ivan
3ffd5b72a1 update 2025-01-01 02:03:12 -06:00
Sudo-Ivan
73af84e24f 0.3.3 2025-01-01 01:41:16 -06:00
Sudo-Ivan
ae40d2879c update and format 2025-01-01 00:58:37 -06:00
Sudo-Ivan
a2499e4a15 update 2025-01-01 00:46:47 -06:00
Sudo-Ivan
30ea1dd0c7 0.3.2 2025-01-01 00:40:25 -06:00
Sudo-Ivan
785bc7d782 update clients 2024-12-31 19:26:30 -06:00
Sudo-Ivan
144f5bea6a update 2024-12-31 19:26:21 -06:00
Sudo-Ivan
a3c701e205 add to-do item 2024-12-31 19:25:31 -06:00
Sudo-Ivan
a8a7607eb6 update binary name 2024-12-31 19:25:23 -06:00
Sudo-Ivan
a2947a3adb update announce.go 2024-12-31 17:07:23 -06:00
Sudo-Ivan
2cb37102fb update 2024-12-31 17:02:57 -06:00
Sudo-Ivan
54c401e2a5 buffer, channel, more transport constants 2024-12-31 17:02:51 -06:00
Sudo-Ivan
8df4039b18 update 2024-12-31 15:30:39 -06:00
Sudo-Ivan
12156adae9 update 2024-12-31 15:22:31 -06:00
Sudo-Ivan
a34e3d274e fix PGP key 2024-12-31 15:20:14 -06:00
Sudo-Ivan
f3d22dfcd4 0.3.1 2024-12-31 15:15:06 -06:00
Sudo-Ivan
99d8e44182 update 2024-12-31 14:41:05 -06:00
Sudo-Ivan
083991c997 add Makefile 2024-12-31 14:34:04 -06:00
Sudo-Ivan
9ca24d96ab move 2024-12-31 14:33:58 -06:00
Sudo-Ivan
b478ca346e update 2024-12-31 14:30:38 -06:00
Sudo-Ivan
20b532e005 update 2024-12-31 14:19:49 -06:00
Sudo-Ivan
80eac50632 update 2024-12-31 14:12:55 -06:00
Sudo-Ivan
f15d8f6a84 move To-Do to README 2024-12-31 14:12:17 -06:00
Sudo-Ivan
c523d6f542 update 2024-12-31 14:11:44 -06:00
Sudo-Ivan
8a175e3051 update 2024-12-31 14:00:10 -06:00
Sudo-Ivan
28d46921d3 0.3.0 2024-12-31 13:49:05 -06:00
Sudo-Ivan
613ceddb0b update common packages 2024-12-31 13:48:22 -06:00
Sudo-Ivan
599dd91979 announce: ratchet support, trunacted hash fix 2024-12-31 13:48:22 -06:00
deepsource-io[bot]
e724886578 ci: add .deepsource.toml 2024-12-31 17:51:22 +00:00
Sudo-Ivan
3034c0b0b4 link physical stats 2024-12-31 11:30:30 -06:00
Sudo-Ivan
3ed2c67742 interface: add interface mods, types and more 2024-12-31 11:22:37 -06:00
Sudo-Ivan
f2c146b7c5 transport: update constants, functions 2024-12-31 11:21:55 -06:00
Sudo-Ivan
59cef5e56a ephermeral keypair, ratchets, shared secret and more 2024-12-31 10:39:31 -06:00
Sudo-Ivan
ef613cc873 0.2.9 - rachets and cryptography 2024-12-30 23:41:18 -06:00
Sudo-Ivan
7a7ce84778 0.2.8 2024-12-30 12:58:43 -06:00
Sudo-Ivan
7ef7e60a87 0.2.7 2024-12-30 11:35:11 -06:00
Sudo-Ivan
73349d4a28 add: AES-CBC 2024-12-30 11:25:45 -06:00
Sudo-Ivan
31128a6758 0.2.6 2024-12-30 11:09:25 -06:00
Sudo-Ivan
566ce5da96 update 2024-12-30 04:22:01 -06:00
Sudo-Ivan
139926be05 0.2.5 2024-12-30 04:00:52 -06:00
Sudo-Ivan
decbd8f29a 0.2.4 2024-12-30 03:50:52 -06:00
Sudo-Ivan
0f5f5cbb13 0.2.3 2024-12-30 02:54:49 -06:00
Sudo-Ivan
a2476c9551 0.2.2 2024-12-30 02:43:35 -06:00
Sudo-Ivan
bfc75a2290 0.2.1 2024-12-30 02:34:38 -06:00
Sudo-Ivan
2e01fa565d update 0.2.0 2024-12-30 02:26:51 -06:00
119 changed files with 21970 additions and 2041 deletions

36
.dockerignore Normal file
View File

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

@@ -0,0 +1,27 @@
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: ./

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

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

View File

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

View File

@@ -0,0 +1,27 @@
name: Run Gosec
on:
push:
branches:
- main
- master
pull_request:
branches:
- main
- master
permissions:
contents: read
jobs:
tests:
runs-on: ubuntu-latest
env:
GO111MODULE: on
steps:
- name: Checkout Source
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Run Gosec Security Scanner
uses: https://git.quad4.io/actions/gosec@c073629009897d89e03229bc81232c7375892086
with:
args: ./...

View File

@@ -0,0 +1,33 @@
name: Go Revive Lint
on:
push:
branches: [ "main", "master" ]
pull_request:
branches: [ "main", "master" ]
jobs:
lint:
permissions:
contents: read
pull-requests: read
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Go
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: '1.25'
- name: Setup Task
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
with:
version: '3.46.3'
- name: Install revive
run: go install github.com/mgechev/revive@latest
- name: Run lint
run: task lint

53
.gitea/workflows/sbom.yml Normal file
View File

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

View File

@@ -0,0 +1,64 @@
name: TinyGo Build
on:
push:
branches: [ "tinygo" ]
pull_request:
branches: [ "tinygo" ]
jobs:
tinygo-build:
permissions:
contents: read
strategy:
matrix:
include:
- name: tinygo-default
target: ""
output: reticulum-go-tinygo
make_target: tinygo-build
- name: tinygo-wasm
target: wasm
output: reticulum-go.wasm
make_target: tinygo-wasm
runs-on: ubuntu-latest
outputs:
build_complete: ${{ steps.build_step.outcome == 'success' }}
steps:
- name: Checkout code
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Go
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
with:
go-version: '1.24'
- name: Install TinyGo
run: |
wget https://github.com/tinygo-org/tinygo/releases/download/v0.37.0/tinygo_0.37.0_amd64.deb
sudo dpkg -i tinygo_0.37.0_amd64.deb
- name: Build with TinyGo
id: build_step
run: |
make ${{ matrix.make_target }}
output_name="${{ matrix.output }}"
if [ -f "bin/${output_name}" ]; then
sha256sum "bin/${output_name}" | cut -d' ' -f1 > "bin/${output_name}.sha256"
echo "Built: ${output_name}"
echo "Generated checksum: bin/${output_name}.sha256"
else
echo "Build output not found: bin/${output_name}"
ls -la bin/
exit 1
fi
- name: Upload Artifact
uses: https://git.quad4.io/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5
with:
name: ${{ matrix.name }}
path: bin/${{ matrix.output }}*

31
.gitignore vendored
View File

@@ -1,8 +1,31 @@
reticulum-client
reticulum-server
# Build artifacts
bin/
# Test coverage reports
coverage.out
# Log files
*.log
logs/
# Local environment variables
.env
.json
# 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

7
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,7 @@
# Contributing
Send issues, suggestions, `.patch` files, or any feedback to one of the preferred methods:
1. Reticulum LXMF: `7cc8d66b4f6a0e0e49d34af7f6077b5a` - Ivan (main developer)
2. XMPP: `ivan@chat.quad4.io` - Ivan (main developer)
3. Email: `team@quad4.io` - Quad4 Team

14
CONTRIBUTORS Normal file
View File

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

12
LICENSE Normal file
View File

@@ -0,0 +1,12 @@
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
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

213
README.md
View File

@@ -1,4 +1,215 @@
# Reticulum-Go
Reticulum Network Stack in 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)
[![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)
## Project Goals:
- **Full Protocol Compatibility**: Maintain complete interoperability with the Python reference implementation
- **Cross-Platform Support**: Support for legacy and modern platforms across multiple architectures
- **Performance**: Leverage Go's concurrency model and runtime for improved throughput and latency
- **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
### Building the Binary
```bash
task build
```
The compiled binary will be located in `bin/reticulum-go`.
### Running the Application
```bash
task run
```
### Running Tests
```bash
task test
```
## Development
### Code Quality
Format code:
```bash
task fmt
```
Run static analysis checks (formatting, vet, linting):
```bash
task check
```
### Testing
Run all tests:
```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.

24
SECURITY.md Normal file
View File

@@ -0,0 +1,24 @@
# Security Policy
## 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
- BOM generation using CycloneDX
## Cryptography Dependencies
- golang.org/x/crypto `v0.46.0` for core cryptographic primitives
- hkdf
- curve25519
- go/crypto
- ed25519
- sha256
- rand
- aes
- cipher
- hmac
## Reporting a Vulnerability
Refer to [https://quad4.io/security](https://quad4.io/security) for how to report vulnerabilities.

11
TODO.md Normal file
View File

@@ -0,0 +1,11 @@
Working on creating a project and issues to better track things. Check out https://git.quad4.io/Networks/Reticulum-Go/projects/2
## Todo
- Created dedicated constants.go for each section.
- Link Request/Response System (in-progress)
- Resource Transfer System (in-progress)
- Link Keep-Alive & Timeout (in-progress)
- Examples (in-progress)
- Tests
- Documentation

683
Taskfile.yml Normal file
View File

@@ -0,0 +1,683 @@
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 ./...'
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

102
To-Do
View File

@@ -1,102 +0,0 @@
To-Do List
Core Components
[âś“] Basic Configuration System
[âś“] Basic config structure
[âś“] Default settings
[âś“] Config file loading/saving
[âś“] Path management
[âś“] Constants Definition
[âś“] Packet constants
[âś“] MTU constants
[âś“] Header types
[âś“] Additional protocol constants
[âś“] Identity Management
[âś“] Identity creation
[âś“] Key pair generation
[âś“] Identity storage/recall
[âś“] Packet Handling
[âś“] Packet creation
[âś“] Packet validation
[âś“] Basic proof system
[âś“] Crypto Implementation
[âś“] Basic encryption
[âś“] Key exchange
[âś“] Hash functions
[âś“] Ratchet implementation
[âś“] Transport Layer
[âś“] Path management
[âś“] Basic packet routing
[âś“] Announce handling
[âś“] Link management
[âś“] Resource cleanup
[âś“] Network layer integration
[âś“] Destination System
[âś“] Destination creation
[âś“] Destination types (IN/OUT)
[âś“] Destination aspects
[âś“] Announce implementation
[âś“] Ratchet support
[âś“] Request handlers
[âś“] Link System
[âś“] Link establishment
[âś“] Link teardown
[âś“] Basic packet transfer
[âś“] Encryption/Decryption
[âś“] Identity verification
[âś“] Request/Response handling
[âś“] Resource System
[âś“] Resource creation
[âś“] Resource transfer
[âś“] Compression
[âś“] Progress tracking
[âś“] Segmentation
[âś“] Cleanup routines
Basic Features
[âś“] Network Interface
[âś“] Basic UDP transport
[âś“] TCP transport
[ ] Interface discovery
[ ] Connection management
[âś“] Packet framing
[âś“] Transport integration
[âś“] Announce System
[âś“] Announce creation
[âś“] Announce propagation
[âś“] Path requests
[âś“] Resource Management
[âś“] Resource tracking
[âś“] Memory management
[âś“] Cleanup routines
[âś“] Client Implementation
[âś“] Basic client structure
[âś“] Configuration handling
[âś“] Interactive mode
[âś“] Link establishment
[âś“] Message sending/receiving
Next Immediate Tasks:
1. [âś“] Fix import cycles by creating common package
2. [ ] Implement Interface discovery
3. [ ] Implement Connection management
4. [ ] Test network layer integration end-to-end
5. [ ] Add error handling for network failures
6. [ ] Implement interface auto-configuration
7. [ ] Complete NetworkInterface implementation
8. [ ] Add comprehensive interface tests
9. [ ] Implement connection retry logic
10. [ ] Add metrics collection for interfaces
11. [ ] Add client reconnection handling
12. [ ] Implement client-side path caching

View File

@@ -1,137 +0,0 @@
package main
import (
"bufio"
"flag"
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
"github.com/Sudo-Ivan/reticulum-go/internal/config"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
"github.com/Sudo-Ivan/reticulum-go/pkg/destination"
)
var (
configPath = flag.String("config", "", "Path to config file")
targetHash = flag.String("target", "", "Target destination hash")
)
func main() {
flag.Parse()
var cfg *common.ReticulumConfig
var err error
if *configPath == "" {
cfg, err = config.InitConfig()
if err != nil {
log.Fatalf("Failed to initialize config: %v", err)
}
} else {
cfg, err = config.LoadConfig(*configPath)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
}
// Enable transport by default for client
cfg.EnableTransport = true
// Initialize transport
transport, err := transport.NewTransport(cfg)
if err != nil {
log.Fatalf("Failed to initialize transport: %v", err)
}
defer transport.Close()
// If target specified, establish connection
if *targetHash != "" {
destHash, err := identity.HashFromHex(*targetHash)
if err != nil {
log.Fatalf("Invalid destination hash: %v", err)
}
// Request path if needed
if !transport.HasPath(destHash) {
fmt.Println("Requesting path to destination...")
if err := transport.RequestPath(destHash, "", nil, true); err != nil {
log.Fatalf("Failed to request path: %v", err)
}
}
// Get destination identity
destIdentity, err := identity.Recall(destHash)
if err != nil {
log.Fatalf("Failed to recall identity: %v", err)
}
// Create destination
dest, err := destination.New(
destIdentity,
destination.OUT,
destination.SINGLE,
"client",
"direct",
)
if err != nil {
log.Fatalf("Failed to create destination: %v", err)
}
// Enable and configure ratchets
dest.SetRetainedRatchets(destination.RATCHET_COUNT)
dest.SetRatchetInterval(destination.RATCHET_INTERVAL)
dest.EnforceRatchets()
// Create link
link := transport.NewLink(dest.Hash(), func() {
fmt.Println("Link established")
}, func() {
fmt.Println("Link closed")
})
defer link.Teardown()
// Set packet callback
link.SetPacketCallback(func(data []byte) {
fmt.Printf("Received: %s\n", string(data))
})
// Start interactive loop
go interactiveLoop(link)
} else {
fmt.Println("No target specified. Use -target <hash> to connect to a destination")
return
}
// Wait for interrupt
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
}
func interactiveLoop(link *transport.Link) {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("> ")
input, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("Error reading input: %v\n", err)
continue
}
input = strings.TrimSpace(input)
if input == "quit" || input == "exit" {
return
}
if err := link.Send([]byte(input)); err != nil {
fmt.Printf("Failed to send: %v\n", err)
}
}
}

642
cmd/reticulum-go/main.go Normal file
View File

@@ -0,0 +1,642 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package main
import (
"encoding/binary"
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"runtime"
"sync"
"syscall"
"time"
"git.quad4.io/Networks/Reticulum-Go/internal/config"
"git.quad4.io/Networks/Reticulum-Go/internal/storage"
"git.quad4.io/Networks/Reticulum-Go/pkg/buffer"
"git.quad4.io/Networks/Reticulum-Go/pkg/channel"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
"git.quad4.io/Networks/Reticulum-Go/pkg/destination"
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
"git.quad4.io/Networks/Reticulum-Go/pkg/interfaces"
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
"git.quad4.io/Networks/Reticulum-Go/pkg/transport"
)
var (
interceptPackets = flag.Bool("intercept-packets", false, "Enable packet interception")
interceptOutput = flag.String("intercept-output", "packets.log", "Output file for intercepted packets")
)
const (
ANNOUNCE_RATE_TARGET = 3600 // Default target time between announces (1 hour)
ANNOUNCE_RATE_GRACE = 3 // Number of grace announces before enforcing rate
ANNOUNCE_RATE_PENALTY = 7200 // Additional penalty time for rate violations
MAX_ANNOUNCE_HOPS = 128 // Maximum number of hops for announces
APP_NAME = "Reticulum-Go Test Node"
APP_ASPECT = "node" // Always use "node" for node announces
)
type Reticulum struct {
config *common.ReticulumConfig
transport *transport.Transport
interfaces []interfaces.Interface
channels map[string]*channel.Channel
buffers map[string]*buffer.Buffer
pathRequests map[string]*common.PathRequest
announceHistory map[string]announceRecord
announceHistoryMu sync.RWMutex
identity *identity.Identity
destination *destination.Destination
storage *storage.Manager
// Node-specific information
maxTransferSize int16 // Max transfer size in KB
nodeEnabled bool // Whether this node is enabled
nodeTimestamp int64 // Last node announcement timestamp
}
type announceRecord struct {
timestamp int64
appData []byte
}
func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
if cfg == nil {
cfg = config.DefaultConfig()
}
// Set default app name and aspect if not provided
if cfg.AppName == "" {
cfg.AppName = APP_NAME
}
if cfg.AppAspect == "" {
cfg.AppAspect = APP_ASPECT // Always use "node" for node announcements
}
if err := initializeDirectories(); err != nil {
return nil, fmt.Errorf("failed to initialize directories: %v", err)
}
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)
debug.Log(debug.DEBUG_INFO, "Transport initialized")
// Load or create identity
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 {
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()))
// 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
debug.Log(debug.DEBUG_INFO, "Creating destination...")
dest, err := destination.New(
ident,
destination.IN,
destination.SINGLE,
"nomadnetwork",
t,
"node",
)
if err != nil {
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()))
// Set node metadata
nodeTimestamp := time.Now().Unix()
r := &Reticulum{
config: cfg,
transport: t,
interfaces: make([]interfaces.Interface, 0),
channels: make(map[string]*channel.Channel),
buffers: make(map[string]*buffer.Buffer),
pathRequests: make(map[string]*common.PathRequest),
announceHistory: make(map[string]announceRecord),
identity: ident,
destination: dest,
storage: storageMgr,
// Node-specific information
maxTransferSize: common.NUM_500, // Default 500KB
nodeEnabled: true, // Enabled by default
nodeTimestamp: nodeTimestamp,
}
// Enable destination features
dest.AcceptsLinks(true)
// Enable ratchets and point to a file for persistence.
// The actual path should probably be configurable.
ratchetPath := ".git.quad4.io/Networks/Reticulum-Go/storage/ratchets/" + r.identity.GetHexHash()
dest.EnableRatchets(ratchetPath)
dest.SetProofStrategy(destination.PROVE_APP)
debug.Log(debug.DEBUG_VERBOSE, "Configured destination features")
// Initialize interfaces from config
for name, ifaceConfig := range cfg.Interfaces {
if !ifaceConfig.Enabled {
continue
}
var iface interfaces.Interface
var err error
switch ifaceConfig.Type {
case common.STR_TCP_CLIENT:
iface, err = interfaces.NewTCPClientInterface(
name,
ifaceConfig.TargetHost,
ifaceConfig.TargetPort,
ifaceConfig.KISSFraming,
ifaceConfig.I2PTunneled,
ifaceConfig.Enabled,
)
case "UDPInterface":
iface, err = interfaces.NewUDPInterface(
name,
ifaceConfig.Address,
ifaceConfig.TargetHost,
ifaceConfig.Enabled,
)
case "AutoInterface":
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:
debug.Log(debug.DEBUG_CRITICAL, "Unknown interface type", common.STR_TYPE, ifaceConfig.Type)
continue
}
if err != nil {
if cfg.PanicOnInterfaceErr {
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)
continue
}
// Set packet callback
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))
if r.transport != nil {
r.transport.HandlePacket(data, ni)
} else {
debug.Log(debug.DEBUG_CRITICAL, "Transport is nil in packet callback")
}
})
debug.Log(debug.DEBUG_ERROR, "Configuring interface", common.STR_NAME, name, common.STR_TYPE, ifaceConfig.Type)
r.interfaces = append(r.interfaces, iface)
debug.Log(debug.DEBUG_INFO, "Interface started successfully", common.STR_NAME, name)
}
return r, nil
}
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))
ch := channel.NewChannel(&transportWrapper{r.transport})
r.channels[iface.GetName()] = ch
rw := buffer.CreateBidirectionalBuffer(
1,
2,
ch,
func(size int) {
data := make([]byte, size)
debug.Log(debug.DEBUG_PACKETS, "Interface reading bytes from buffer", common.STR_NAME, iface.GetName(), "size", size)
iface.ProcessIncoming(data)
if len(data) > common.ZERO {
debug.Log(debug.DEBUG_TRACE, "Interface received packet type", common.STR_NAME, iface.GetName(), common.STR_TYPE, fmt.Sprintf("0x%02x", data[0]))
r.transport.HandlePacket(data, iface)
}
},
)
r.buffers[iface.GetName()] = &buffer.Buffer{
ReadWriter: rw,
}
}
func (r *Reticulum) monitorInterfaces() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
for _, iface := range r.interfaces {
if tcpClient, ok := iface.(*interfaces.TCPClientInterface); ok {
stats := fmt.Sprintf("Interface %s status - Connected: %v, TX: %d bytes (%.2f Kbps), RX: %d bytes (%.2f Kbps)",
iface.GetName(),
tcpClient.IsConnected(),
tcpClient.GetTxBytes(),
float64(tcpClient.GetTxBytes()*8)/(5*1024),
tcpClient.GetRxBytes(),
float64(tcpClient.GetRxBytes()*8)/(5*1024),
)
if runtime.GOOS != "windows" {
stats = fmt.Sprintf("%s, RTT: %v", stats, tcpClient.GetRTT())
}
debug.Log(debug.DEBUG_VERBOSE, "Interface status", "stats", stats)
}
}
}
}
func main() {
flag.Parse()
debug.Init()
debug.Log(debug.DEBUG_CRITICAL, "Initializing Reticulum", "debug_level", debug.GetDebugLevel())
cfg, err := config.InitConfig()
if err != nil {
debug.GetLogger().Error("Failed to initialize config", common.STR_ERROR, err)
os.Exit(1)
}
debug.Log(debug.DEBUG_ERROR, "Configuration loaded", "path", cfg.ConfigPath)
r, err := NewReticulum(cfg)
if err != nil {
debug.GetLogger().Error("Failed to create Reticulum instance", common.STR_ERROR, err)
os.Exit(1)
}
// Start monitoring interfaces
go r.monitorInterfaces()
// Register announce handler
handler := NewAnnounceHandler(r, []string{"*"})
r.transport.RegisterAnnounceHandler(handler)
// Start Reticulum
if err := r.Start(); err != nil {
debug.GetLogger().Error("Failed to start Reticulum", common.STR_ERROR, err)
os.Exit(1)
}
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
debug.Log(debug.DEBUG_CRITICAL, "Shutting down...")
if err := r.Stop(); err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Error during shutdown", common.STR_ERROR, err)
}
debug.Log(debug.DEBUG_CRITICAL, "Goodbye!")
}
type transportWrapper struct {
*transport.Transport
}
func (tw *transportWrapper) GetRTT() float64 {
return 0.1
}
func (tw *transportWrapper) RTT() float64 {
return tw.GetRTT()
}
func (tw *transportWrapper) GetStatus() byte {
return transport.STATUS_ACTIVE
}
func (tw *transportWrapper) Send(data []byte) interface{} {
p := &packet.Packet{
PacketType: packet.PacketTypeData,
Hops: 0,
Data: data,
HeaderType: packet.HeaderType1,
}
err := tw.Transport.SendPacket(p)
if err != nil {
return nil
}
return p
}
func (tw *transportWrapper) Resend(p interface{}) error {
if pkt, ok := p.(*packet.Packet); ok {
return tw.Transport.SendPacket(pkt)
}
return fmt.Errorf("invalid packet type")
}
func (tw *transportWrapper) SetPacketTimeout(packet interface{}, callback func(interface{}), timeout time.Duration) {
time.AfterFunc(timeout, func() {
callback(packet)
})
}
func (tw *transportWrapper) SetPacketDelivered(packet interface{}, callback func(interface{})) {
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 {
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{
basePath,
filepath.Join(basePath, common.STR_STORAGE),
filepath.Join(basePath, common.STR_STORAGE, "destinations"),
filepath.Join(basePath, common.STR_STORAGE, "identities"),
filepath.Join(basePath, common.STR_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 {
if err := os.MkdirAll(dir, common.NUM_0700); err != nil { // #nosec G301
return fmt.Errorf("failed to create directory %s: %v", dir, err)
}
}
return nil
}
func (r *Reticulum) Start() error {
debug.Log(debug.DEBUG_ERROR, "Starting Reticulum...")
if err := r.transport.Start(); err != nil {
return fmt.Errorf("failed to start transport: %v", err)
}
debug.Log(debug.DEBUG_INFO, "Transport started successfully")
// Start interfaces
for _, iface := range r.interfaces {
debug.Log(debug.DEBUG_ERROR, "Starting interface", "name", iface.GetName())
if err := iface.Start(); err != nil {
if r.config.PanicOnInterfaceErr {
return fmt.Errorf("failed to start interface %s: %v", iface.GetName(), err)
}
debug.Log(debug.DEBUG_CRITICAL, "Error starting interface", "name", iface.GetName(), "error", err)
continue
}
if netIface, ok := iface.(common.NetworkInterface); ok {
// Register interface with transport
if err := r.transport.RegisterInterface(iface.GetName(), netIface); err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Failed to register interface with transport", "name", iface.GetName(), "error", err)
} else {
debug.Log(debug.DEBUG_INFO, "Registered interface with transport", "name", iface.GetName())
}
r.handleInterface(netIface)
}
debug.Log(debug.DEBUG_INFO, "Interface started successfully", "name", iface.GetName())
}
// Wait for interfaces to initialize
time.Sleep(2 * time.Second)
// Send initial announce
debug.Log(debug.DEBUG_ERROR, "Sending initial announce")
nodeName := "Reticulum-Go Test Node"
r.destination.SetDefaultAppData([]byte(nodeName))
if err := r.destination.Announce(false, nil, nil); err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Failed to send initial announce", "error", err)
}
// Start periodic announce goroutine
go func() {
// Wait a bit before the first announce
time.Sleep(5 * time.Second)
for {
debug.Log(debug.DEBUG_INFO, "Announcing destination...")
err := r.destination.Announce(false, nil, nil)
if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Could not send announce", "error", err)
}
time.Sleep(60 * time.Second)
}
}()
go r.monitorInterfaces()
debug.Log(debug.DEBUG_ERROR, "Reticulum started successfully")
return nil
}
func (r *Reticulum) Stop() error {
debug.Log(debug.DEBUG_ERROR, "Stopping Reticulum...")
for _, buf := range r.buffers {
if err := buf.Close(); err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Error closing buffer", "error", err)
}
}
for _, ch := range r.channels {
if err := ch.Close(); err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Error closing channel", "error", err)
}
}
for _, iface := range r.interfaces {
if err := iface.Stop(); err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Error stopping interface", "name", iface.GetName(), "error", err)
}
}
if err := r.transport.Close(); err != nil {
return fmt.Errorf("failed to close transport: %v", err)
}
debug.Log(debug.DEBUG_ERROR, "Reticulum stopped successfully")
return nil
}
type AnnounceHandler struct {
aspectFilter []string
reticulum *Reticulum
}
func NewAnnounceHandler(r *Reticulum, aspectFilter []string) *AnnounceHandler {
return &AnnounceHandler{
aspectFilter: aspectFilter,
reticulum: r,
}
}
func (h *AnnounceHandler) AspectFilter() []string {
return h.aspectFilter
}
func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appData []byte, hops uint8) error {
debug.Log(debug.DEBUG_INFO, "Received announce", "hash", fmt.Sprintf("%x", destHash), "hops", hops)
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)
var isNode bool
var nodeEnabled bool
var nodeTimestamp int64
var nodeMaxSize int16
// Parse msgpack appData from transport announce format
if len(appData) > common.ZERO {
// appData is msgpack array [name, customData]
if appData[0] == common.HEX_0x92 { // array of 2 elements
// Skip array header and first element (name)
pos := common.ONE
if pos < len(appData) && appData[pos] == common.HEX_0xC4 { // bin 8
nameLen := int(appData[pos+1])
pos += common.TWO + nameLen
if pos < len(appData) && appData[pos] == common.HEX_0xC4 { // bin 8
dataLen := int(appData[pos+1])
if pos+2+dataLen <= len(appData) {
customData := appData[pos+2 : pos+2+dataLen]
nodeName := string(customData)
debug.Log(debug.DEBUG_INFO, "Parsed node name", "name", nodeName)
debug.Log(debug.DEBUG_INFO, "Announced node", "name", nodeName)
}
}
}
} else {
// Fallback: treat as raw node name
nodeName := string(appData)
debug.Log(debug.DEBUG_INFO, "Raw node name", "name", nodeName)
debug.Log(debug.DEBUG_INFO, "Announced node", "name", nodeName)
}
} else {
debug.Log(debug.DEBUG_INFO, "No appData (empty announce)")
}
// Type assert and log identity details
if identity, ok := id.(*identity.Identity); ok {
debug.Log(debug.DEBUG_ALL, "Identity details")
debug.Log(debug.DEBUG_ALL, "Identity hash", "hash", identity.GetHexHash())
debug.Log(debug.DEBUG_ALL, "Identity public key", "key", fmt.Sprintf("%x", identity.GetPublicKey()))
ratchets := identity.GetRatchets()
debug.Log(debug.DEBUG_ALL, "Active ratchets", "count", len(ratchets))
if len(ratchets) > 0 {
ratchetKey := identity.GetCurrentRatchetKey()
if ratchetKey != nil {
ratchetID := identity.GetRatchetID(ratchetKey)
debug.Log(debug.DEBUG_ALL, "Current ratchet ID", "id", fmt.Sprintf("%x", ratchetID))
}
}
// Create a better record with more info
recordType := "peer"
if isNode {
recordType = "node"
debug.Log(debug.DEBUG_INFO, "Storing node in announce history", "enabled", nodeEnabled, "timestamp", nodeTimestamp, "maxsize", fmt.Sprintf("%dKB", nodeMaxSize))
}
h.reticulum.announceHistoryMu.Lock()
h.reticulum.announceHistory[identity.GetHexHash()] = announceRecord{
timestamp: time.Now().Unix(),
appData: appData,
}
h.reticulum.announceHistoryMu.Unlock()
debug.Log(debug.DEBUG_VERBOSE, "Stored announce in history", "type", recordType, "identity", identity.GetHexHash())
}
return nil
}
func (h *AnnounceHandler) ReceivePathResponses() bool {
return true
}
func (r *Reticulum) GetDestination() *destination.Destination {
return r.destination
}
func (r *Reticulum) createNodeAppData() []byte {
// Create a msgpack array with 3 elements
// [Bool, Int32, Int16] for [enable, timestamp, max_transfer_size]
appData := []byte{common.HEX_0x93} // Array with 3 elements
// Element 0: Boolean for enable/disable peer
if r.nodeEnabled {
appData = append(appData, common.HEX_0xC3) // true
} else {
appData = append(appData, common.HEX_0xC2) // false
}
// Element 1: Int32 timestamp (current time)
r.nodeTimestamp = time.Now().Unix()
appData = append(appData, common.HEX_0xD2) // int32 format
timeBytes := make([]byte, common.FOUR)
binary.BigEndian.PutUint32(timeBytes, uint32(r.nodeTimestamp)) // #nosec G115
appData = append(appData, timeBytes...)
// Element 2: Int16 max transfer size in KB
appData = append(appData, common.HEX_0xD1) // int16 format
sizeBytes := make([]byte, common.TWO)
binary.BigEndian.PutUint16(sizeBytes, uint16(r.maxTransferSize)) // #nosec G115
appData = append(appData, sizeBytes...)
debug.Log(debug.DEBUG_ALL, "Created node appData", "enable", r.nodeEnabled, "timestamp", r.nodeTimestamp, "maxsize", r.maxTransferSize, "data", fmt.Sprintf("%x", appData))
return appData
}

View File

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

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

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

@@ -1,117 +0,0 @@
package main
import (
"log"
"os"
"os/signal"
"syscall"
"github.com/Sudo-Ivan/reticulum-go/internal/config"
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
"github.com/Sudo-Ivan/reticulum-go/pkg/interfaces"
)
type Reticulum struct {
config *config.ReticulumConfig
transport *transport.Transport
}
func NewReticulum(cfg *config.ReticulumConfig) (*Reticulum, error) {
if cfg == nil {
cfg = config.DefaultConfig()
}
// Initialize transport
t, err := transport.NewTransport(cfg)
if err != nil {
return nil, err
}
return &Reticulum{
config: cfg,
transport: t,
}, nil
}
func (r *Reticulum) Start() error {
// Initialize interfaces based on config
for _, ifaceConfig := range r.config.Interfaces {
var iface interfaces.Interface
switch ifaceConfig.Type {
case "tcp":
client, err := interfaces.NewTCPClient(
ifaceConfig.Name,
ifaceConfig.Address,
ifaceConfig.Port,
ifaceConfig.KISSFraming,
ifaceConfig.I2PTunneled,
)
if err != nil {
log.Printf("Failed to create TCP interface %s: %v", ifaceConfig.Name, err)
continue
}
iface = client
case "tcpserver":
server, err := interfaces.NewTCPServer(
ifaceConfig.Name,
ifaceConfig.Address,
ifaceConfig.Port,
ifaceConfig.PreferIPv6,
ifaceConfig.I2PTunneled,
)
if err != nil {
log.Printf("Failed to create TCP server interface %s: %v", ifaceConfig.Name, err)
continue
}
iface = server
default:
log.Printf("Unknown interface type: %s", ifaceConfig.Type)
continue
}
// Set packet callback to transport
iface.SetPacketCallback(r.transport.HandlePacket)
}
log.Printf("Reticulum initialized with config at: %s", r.config.ConfigPath)
return nil
}
func (r *Reticulum) Stop() error {
if err := r.transport.Close(); err != nil {
return err
}
return nil
}
func main() {
// Initialize configuration
cfg, err := config.InitConfig()
if err != nil {
log.Fatalf("Failed to initialize config: %v", err)
}
// Create new reticulum instance
r, err := NewReticulum(cfg)
if err != nil {
log.Fatalf("Failed to create Reticulum instance: %v", err)
}
// Start reticulum
if err := r.Start(); err != nil {
log.Fatalf("Failed to start Reticulum: %v", err)
}
// Wait for interrupt signal
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
// Clean shutdown
if err := r.Stop(); err != nil {
log.Printf("Error during shutdown: %v", err)
}
}

View File

@@ -1,18 +0,0 @@
enable_transport = true
share_instance = true
shared_instance_port = 37428
instance_control_port = 37429
panic_on_interface_error = false
loglevel = 4
[interfaces]
[interfaces."Local TCP"]
type = "TCPClientInterface"
enabled = true
target_host = "127.0.0.1"
target_port = 4242
[interfaces."Local UDP"]
type = "UDPInterface"
enabled = true
interface = "lo"

View File

@@ -1,18 +0,0 @@
enable_transport = true
share_instance = true
shared_instance_port = 37430
instance_control_port = 37431
panic_on_interface_error = false
loglevel = 4
[interfaces]
[interfaces."Local TCP"]
type = "TCPClientInterface"
enabled = true
target_host = "127.0.0.1"
target_port = 4243
[interfaces."Local UDP"]
type = "UDPInterface"
enabled = true
interface = "lo"

39
docker/Dockerfile Normal file
View File

@@ -0,0 +1,39 @@
ARG GO_VERSION=1.25
FROM golang:${GO_VERSION}-alpine AS builder
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
RUN apk add --no-cache git
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY cmd/ cmd/
COPY internal/ internal/
COPY pkg/ pkg/
RUN go build \
-ldflags='-w -s -extldflags "-static"' \
-a -installsuffix cgo \
-o reticulum-go \
./cmd/reticulum-go
FROM busybox:1.37.0@sha256:870e815c3a50dd0f6b40efddb319c72c32c3ee340b5a3e8945904232ccd12f44
RUN adduser -D -s /bin/sh app
COPY --from=builder /build/reticulum-go /usr/local/bin/reticulum-go
RUN chmod +x /usr/local/bin/reticulum-go
RUN mkdir -p /app && chown app:app /app
USER app
WORKDIR /app
EXPOSE 4242
ENTRYPOINT ["/usr/local/bin/reticulum-go"]

30
docker/Dockerfile.build Normal file
View File

@@ -0,0 +1,30 @@
ARG GO_VERSION=1.25
FROM golang:${GO_VERSION}-alpine
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
RUN apk add --no-cache git && \
adduser -D -s /bin/sh builder
WORKDIR /build
USER builder
COPY go.mod go.sum ./
RUN go mod download
COPY cmd/ cmd/
COPY internal/ internal/
COPY pkg/ pkg/
ARG BINARY_NAME=reticulum-go
ARG BUILD_PATH=./cmd/reticulum-go
RUN mkdir -p /dist && \
go build \
-ldflags='-w -s -extldflags "-static"' \
-a -installsuffix cgo \
-o /dist/${BINARY_NAME} \
${BUILD_PATH}

16
examples/wasm/go.mod Normal file
View File

@@ -0,0 +1,16 @@
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 => ../../

16
examples/wasm/go.sum Normal file
View File

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

80
examples/wasm/main.go Normal file
View File

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

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

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

@@ -0,0 +1,575 @@
// 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 Normal file
View File

@@ -0,0 +1,61 @@
{
"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
}

50
flake.nix Normal file
View File

@@ -0,0 +1,50 @@
{
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";
};
});
}

11
go.mod
View File

@@ -1,9 +1,10 @@
module github.com/Sudo-Ivan/reticulum-go
module git.quad4.io/Networks/Reticulum-Go
go 1.23.4
go 1.24.0
require (
github.com/pelletier/go-toml v1.9.5
golang.org/x/crypto v0.31.0
gopkg.in/yaml.v3 v3.0.1
github.com/vmihailenco/msgpack/v5 v5.4.1
golang.org/x/crypto v0.46.0
)
require github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect

22
go.sum
View File

@@ -1,8 +1,14 @@
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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,28 +1,33 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package config
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/pelletier/go-toml"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
)
const (
DefaultSharedInstancePort = 37428
DefaultInstanceControlPort = 37429
DefaultLogLevel = 4
DefaultLogLevel = 4
)
func DefaultConfig() *common.ReticulumConfig {
return &common.ReticulumConfig{
EnableTransport: false,
ShareInstance: true,
SharedInstancePort: DefaultSharedInstancePort,
InstanceControlPort: DefaultInstanceControlPort,
EnableTransport: true,
ShareInstance: true,
SharedInstancePort: DefaultSharedInstancePort,
InstanceControlPort: DefaultInstanceControlPort,
PanicOnInterfaceErr: false,
LogLevel: DefaultLogLevel,
Interfaces: make(map[string]common.InterfaceConfig),
Interfaces: make(map[string]*common.InterfaceConfig),
}
}
@@ -31,7 +36,7 @@ func GetConfigPath() (string, error) {
if err != nil {
return "", err
}
return filepath.Join(homeDir, ".reticulum", "config"), nil
return filepath.Join(homeDir, ".reticulum-go", "config"), nil
}
func EnsureConfigDir() error {
@@ -40,65 +45,211 @@ func EnsureConfigDir() error {
return err
}
configDir := filepath.Join(homeDir, ".reticulum")
return os.MkdirAll(configDir, 0755)
configDir := filepath.Join(homeDir, ".reticulum-go")
return os.MkdirAll(configDir, 0700) // #nosec G301
}
// parseValue parses string values into appropriate types
func parseValue(value string) interface{} {
value = strings.TrimSpace(value)
// Try bool
if value == "true" {
return true
}
if value == "false" {
return false
}
// Try int
if i, err := strconv.Atoi(value); err == nil {
return i
}
// Return as string
return value
}
// LoadConfig loads the configuration from the specified path
func LoadConfig(path string) (*common.ReticulumConfig, error) {
data, err := os.ReadFile(path)
// bearer:disable go_gosec_filesystem_filereadtaint
file, err := os.Open(path) // #nosec G304
if err != nil {
return nil, err
}
defer file.Close()
cfg := DefaultConfig()
if err := toml.Unmarshal(data, cfg); err != nil {
return nil, err
cfg.ConfigPath = path
scanner := bufio.NewScanner(file)
var currentInterface *common.InterfaceConfig
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// Skip comments and empty lines
if line == "" || strings.HasPrefix(line, "#") {
continue
}
// Handle interface sections
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
name := strings.Trim(line, "[]")
currentInterface = &common.InterfaceConfig{Name: name}
cfg.Interfaces[name] = currentInterface
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
if currentInterface != nil {
// Parse interface config
switch key {
case "type":
currentInterface.Type = value
case "enabled":
currentInterface.Enabled = value == "true"
case "address":
currentInterface.Address = value
case "port":
currentInterface.Port, _ = strconv.Atoi(value)
case "target_host":
currentInterface.TargetHost = value
case "target_port":
currentInterface.TargetPort, _ = strconv.Atoi(value)
case "discovery_port":
currentInterface.DiscoveryPort, _ = strconv.Atoi(value)
case "data_port":
currentInterface.DataPort, _ = strconv.Atoi(value)
case "discovery_scope":
currentInterface.DiscoveryScope = value
case "group_id":
currentInterface.GroupID = value
}
} else {
// Parse global config
switch key {
case "enable_transport":
cfg.EnableTransport = value == "true"
case "share_instance":
cfg.ShareInstance = value == "true"
case "shared_instance_port":
cfg.SharedInstancePort, _ = strconv.Atoi(value)
case "instance_control_port":
cfg.InstanceControlPort, _ = strconv.Atoi(value)
case "panic_on_interface_error":
cfg.PanicOnInterfaceErr = value == "true"
case "loglevel":
cfg.LogLevel, _ = strconv.Atoi(value)
}
}
}
cfg.ConfigPath = path
return cfg, nil
}
// SaveConfig saves the configuration to the specified path
func SaveConfig(cfg *common.ReticulumConfig) error {
data, err := toml.Marshal(cfg)
if err != nil {
return err
if cfg.ConfigPath == "" {
return fmt.Errorf("config path not set")
}
return os.WriteFile(cfg.ConfigPath, data, 0644)
var builder strings.Builder
// Write global config
builder.WriteString("# Reticulum Configuration\n")
builder.WriteString(fmt.Sprintf("enable_transport = %v\n", cfg.EnableTransport))
builder.WriteString(fmt.Sprintf("share_instance = %v\n", cfg.ShareInstance))
builder.WriteString(fmt.Sprintf("shared_instance_port = %d\n", cfg.SharedInstancePort))
builder.WriteString(fmt.Sprintf("instance_control_port = %d\n", cfg.InstanceControlPort))
builder.WriteString(fmt.Sprintf("panic_on_interface_error = %v\n", cfg.PanicOnInterfaceErr))
builder.WriteString(fmt.Sprintf("loglevel = %d\n\n", cfg.LogLevel))
// Write interface configs
for name, iface := range cfg.Interfaces {
builder.WriteString(fmt.Sprintf("[%s]\n", name))
builder.WriteString(fmt.Sprintf("type = %s\n", iface.Type))
builder.WriteString(fmt.Sprintf("enabled = %v\n", iface.Enabled))
if iface.Address != "" {
builder.WriteString(fmt.Sprintf("address = %s\n", iface.Address))
}
if iface.Port != 0 {
builder.WriteString(fmt.Sprintf("port = %d\n", iface.Port))
}
if iface.TargetHost != "" {
builder.WriteString(fmt.Sprintf("target_host = %s\n", iface.TargetHost))
}
if iface.TargetPort != 0 {
builder.WriteString(fmt.Sprintf("target_port = %d\n", iface.TargetPort))
}
if iface.DiscoveryPort != 0 {
builder.WriteString(fmt.Sprintf("discovery_port = %d\n", iface.DiscoveryPort))
}
if iface.DataPort != 0 {
builder.WriteString(fmt.Sprintf("data_port = %d\n", iface.DataPort))
}
if iface.DiscoveryScope != "" {
builder.WriteString(fmt.Sprintf("discovery_scope = %s\n", iface.DiscoveryScope))
}
if iface.GroupID != "" {
builder.WriteString(fmt.Sprintf("group_id = %s\n", iface.GroupID))
}
builder.WriteString("\n")
}
return os.WriteFile(cfg.ConfigPath, []byte(builder.String()), 0600) // #nosec G306
}
// CreateDefaultConfig creates a default configuration file
func CreateDefaultConfig(path string) error {
cfg := DefaultConfig()
cfg.ConfigPath = path
// Add default interface
cfg.Interfaces["Default Interface"] = common.InterfaceConfig{
Type: "AutoInterface",
Enabled: false,
cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{
Type: "AutoInterface",
Enabled: true,
GroupID: "reticulum",
DiscoveryScope: "link",
DiscoveryPort: 29716,
DataPort: 42671,
}
// Add default quad4net interface
cfg.Interfaces["quad4net tcp"] = common.InterfaceConfig{
cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
Type: "TCPClientInterface",
Enabled: true,
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",
}
data, err := toml.Marshal(cfg)
if err != nil {
cfg.Interfaces["Local UDP"] = &common.InterfaceConfig{
Type: "UDPInterface",
Enabled: false,
Address: "0.0.0.0",
Port: 37696,
}
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { // #nosec G301
return err
}
// Create config directory if it doesn't exist
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
return os.WriteFile(path, data, 0644)
return SaveConfig(cfg)
}
// InitConfig initializes the configuration system
@@ -118,4 +269,4 @@ func InitConfig() (*common.ReticulumConfig, error) {
// Load config
return LoadConfig(configPath)
}
}

View File

@@ -0,0 +1,136 @@
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)
}
}

191
internal/storage/storage.go Normal file
View File

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

@@ -0,0 +1,117 @@
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")
}
}

17
misc/wasm/go_js_wasm_exec Executable file
View File

@@ -0,0 +1,17 @@
#!/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" "$@"

575
misc/wasm/wasm_exec.js Normal file
View File

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

@@ -0,0 +1,41 @@
// 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,109 +1,164 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package announce
import (
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"sync"
"time"
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
"golang.org/x/crypto/curve25519"
)
const (
PACKET_TYPE_DATA = 0x00
PACKET_TYPE_ANNOUNCE = 0x01
PACKET_TYPE_LINK = 0x02
PACKET_TYPE_PROOF = 0x03
// Announce Types
ANNOUNCE_NONE = 0x00
ANNOUNCE_PATH = 0x01
ANNOUNCE_IDENTITY = 0x02
// Header Types
HEADER_TYPE_1 = 0x00 // One address field
HEADER_TYPE_2 = 0x01 // Two address fields
// Propagation Types
PROP_TYPE_BROADCAST = 0x00
PROP_TYPE_TRANSPORT = 0x01
DEST_TYPE_SINGLE = 0x00
DEST_TYPE_GROUP = 0x01
DEST_TYPE_PLAIN = 0x02
DEST_TYPE_LINK = 0x03
// IFAC Flag
IFAC_NONE = 0x00
IFAC_AUTH = 0x80
MAX_HOPS = 128
PROPAGATION_RATE = 0.02 // 2% of interface bandwidth
RETRY_INTERVAL = 300 // 5 minutes
MAX_RETRIES = 3
)
type AnnounceHandler interface {
AspectFilter() []string
ReceivedAnnounce(destinationHash []byte, announcedIdentity *identity.Identity, appData []byte) error
ReceivePathResponses() bool
}
type Announce struct {
mutex sync.RWMutex
mutex *sync.RWMutex
destinationHash []byte
identity *identity.Identity
appData []byte
hops uint8
timestamp int64
signature []byte
pathResponse bool
retries int
handlers []AnnounceHandler
destinationName string
identity *identity.Identity
appData []byte
config *common.ReticulumConfig
hops uint8
timestamp int64
signature []byte
pathResponse bool
retries int
handlers []Handler
ratchetID []byte
packet []byte
hash []byte
}
func New(dest *identity.Identity, appData []byte, pathResponse bool) (*Announce, error) {
a := &Announce{
identity: dest,
appData: appData,
hops: 0,
timestamp: time.Now().Unix(),
pathResponse: pathResponse,
retries: 0,
handlers: make([]AnnounceHandler, 0),
func New(dest *identity.Identity, destinationHash []byte, destinationName string, appData []byte, pathResponse bool, config *common.ReticulumConfig) (*Announce, error) {
if dest == nil {
return nil, errors.New("destination identity required")
}
// Generate destination hash
hash := sha256.New()
hash.Write(dest.GetPublicKey())
a.destinationHash = hash.Sum(nil)[:16] // Truncated hash
if len(destinationHash) == 0 {
return nil, errors.New("destination hash required")
}
// Sign the announce
if destinationName == "" {
return nil, errors.New("destination name required")
}
a := &Announce{
mutex: &sync.RWMutex{},
identity: dest,
destinationHash: destinationHash,
destinationName: destinationName,
appData: appData,
config: config,
hops: 0,
timestamp: time.Now().Unix(),
pathResponse: pathResponse,
retries: 0,
handlers: make([]Handler, 0),
}
// Get current ratchet ID if enabled
currentRatchet := dest.GetCurrentRatchetKey()
if currentRatchet != nil {
ratchetPub, err := curve25519.X25519(currentRatchet, curve25519.Basepoint)
if err == nil {
a.ratchetID = dest.GetRatchetID(ratchetPub)
}
}
// Sign announce data
signData := append(a.destinationHash, a.appData...)
if a.ratchetID != nil {
signData = append(signData, a.ratchetID...)
}
a.signature = dest.Sign(signData)
return a, nil
}
func (a *Announce) Propagate(interfaces []transport.Interface) error {
a.mutex.Lock()
defer a.mutex.Unlock()
func (a *Announce) Propagate(interfaces []common.NetworkInterface) error {
a.mutex.RLock()
defer a.mutex.RUnlock()
if a.hops >= MAX_HOPS {
return errors.New("maximum hop count reached")
debug.Log(debug.DEBUG_TRACE, "Propagating announce across interfaces", "count", len(interfaces))
var packet []byte
if a.packet != nil {
debug.Log(debug.DEBUG_TRACE, "Using cached packet", "bytes", len(a.packet))
packet = a.packet
} else {
debug.Log(debug.DEBUG_TRACE, "Creating new packet")
packet = a.CreatePacket()
a.packet = packet
}
// Increment hop count
a.hops++
// Create announce packet
packet := make([]byte, 0)
packet = append(packet, a.destinationHash...)
packet = append(packet, a.identity.GetPublicKey()...)
packet = append(packet, byte(a.hops))
if a.appData != nil {
packet = append(packet, a.appData...)
}
packet = append(packet, a.signature...)
// Propagate to all interfaces
for _, iface := range interfaces {
if err := iface.SendAnnounce(packet, a.pathResponse); err != nil {
return err
if !iface.IsEnabled() {
debug.Log(debug.DEBUG_TRACE, "Skipping disabled interface", "name", iface.GetName())
continue
}
if !iface.GetBandwidthAvailable() {
debug.Log(debug.DEBUG_TRACE, "Skipping interface with insufficient bandwidth", "name", iface.GetName())
continue
}
debug.Log(debug.DEBUG_TRACE, "Sending announce on interface", "name", iface.GetName())
if err := iface.Send(packet, ""); err != nil {
debug.Log(debug.DEBUG_TRACE, "Failed to send on interface", "name", iface.GetName(), "error", 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())
}
return nil
}
func (a *Announce) RegisterHandler(handler AnnounceHandler) {
func (a *Announce) RegisterHandler(handler Handler) {
a.mutex.Lock()
defer a.mutex.Unlock()
a.handlers = append(a.handlers, handler)
}
func (a *Announce) DeregisterHandler(handler AnnounceHandler) {
func (a *Announce) DeregisterHandler(handler Handler) {
a.mutex.Lock()
defer a.mutex.Unlock()
for i, h := range a.handlers {
@@ -118,28 +173,113 @@ func (a *Announce) HandleAnnounce(data []byte) error {
a.mutex.Lock()
defer a.mutex.Unlock()
// Validate announce data
if len(data) < 16+32+1 { // Min size: hash + pubkey + hops
return errors.New("invalid announce data")
debug.Log(debug.DEBUG_TRACE, "Handling announce packet", "bytes", len(data))
// Minimum packet size validation
// header(2) + desthash(16) + context(1) + enckey(32) + signkey(32) + namehash(10) +
// randomhash(10) + signature(64) + min app data(3)
if len(data) < 170 {
debug.Log(debug.DEBUG_TRACE, "Invalid announce data length", "bytes", len(data), "minimum", 170)
return errors.New("invalid announce data length")
}
// Extract fields
destHash := data[:16]
pubKey := data[16:48]
hops := data[48]
appData := data[49 : len(data)-64]
signature := data[len(data)-64:]
// Extract header and check packet type
header := data[:2]
if header[0]&0x03 != PACKET_TYPE_ANNOUNCE {
return errors.New("not an announce packet")
}
// Get hop count
hopCount := header[1]
if hopCount > MAX_HOPS {
debug.Log(debug.DEBUG_TRACE, "Announce exceeded max hops", "hops", hopCount)
return errors.New("announce exceeded maximum hop count")
}
// Parse the packet based on header type
headerType := (header[0] & 0b01000000) >> 6
var contextByte byte
var packetData []byte
if headerType == HEADER_TYPE_2 {
// Header type 2 format: header(2) + desthash(16) + transportid(16) + context(1) + data
if len(data) < 35 {
return errors.New("header type 2 packet too short")
}
destHash := data[2:18]
transportID := data[18:34]
contextByte = data[34]
packetData = data[35:]
debug.Log(debug.DEBUG_TRACE, "Header type 2 announce", "destHash", fmt.Sprintf("%x", destHash), "transportID", fmt.Sprintf("%x", transportID), "context", contextByte)
} else {
// Header type 1 format: header(2) + desthash(16) + context(1) + data
if len(data) < 19 {
return errors.New("header type 1 packet too short")
}
destHash := data[2:18]
contextByte = data[18]
packetData = data[19:]
debug.Log(debug.DEBUG_TRACE, "Header type 1 announce", "destHash", fmt.Sprintf("%x", destHash), "context", contextByte)
}
// Now parse the data portion according to the spec
// Public Key (32) + Signing Key (32) + Name Hash (10) + Random Hash (10) + Ratchet (32) + Signature (64) + App Data
if len(packetData) < 180 { // 32 + 32 + 10 + 10 + 32 + 64
return errors.New("announce data too short")
}
// Extract the components
encKey := packetData[:32]
signKey := packetData[32:64]
nameHash := packetData[64:74]
randomHash := packetData[74:84]
ratchetData := packetData[84:116]
signature := packetData[116:180]
appData := packetData[180:]
debug.Log(debug.DEBUG_TRACE, "Announce fields", "encKey", fmt.Sprintf("%x", encKey), "signKey", fmt.Sprintf("%x", signKey))
debug.Log(debug.DEBUG_TRACE, "Name and random hash", "nameHash", fmt.Sprintf("%x", nameHash), "randomHash", fmt.Sprintf("%x", randomHash))
debug.Log(debug.DEBUG_TRACE, "Ratchet data", "ratchet", fmt.Sprintf("%x", ratchetData[:8]))
debug.Log(debug.DEBUG_TRACE, "Signature and app data", "signature", fmt.Sprintf("%x", signature[:8]), "appDataLen", len(appData))
// Get the destination hash from header
var destHash []byte
if headerType == HEADER_TYPE_2 {
destHash = data[2:18]
} else {
destHash = data[2:18]
}
// Combine public keys
pubKey := append(encKey, signKey...)
// Create announced identity from public keys
announcedIdentity := identity.FromPublicKey(pubKey)
if announcedIdentity == nil {
return errors.New("invalid identity public key")
}
// Verify signature
signData := append(destHash, appData...)
if !a.identity.Verify(signData, signature) {
signedData := make([]byte, 0)
signedData = append(signedData, destHash...)
signedData = append(signedData, encKey...)
signedData = append(signedData, signKey...)
signedData = append(signedData, nameHash...)
signedData = append(signedData, randomHash...)
signedData = append(signedData, ratchetData...)
signedData = append(signedData, appData...)
if !announcedIdentity.Verify(signedData, signature) {
return errors.New("invalid announce signature")
}
// Process announce with registered handlers
// Process with handlers
for _, handler := range a.handlers {
if handler.ReceivePathResponses() || !a.pathResponse {
if err := handler.ReceivedAnnounce(destHash, a.identity, appData); err != nil {
if err := handler.ReceivedAnnounce(destHash, announcedIdentity, appData, hopCount); err != nil {
return err
}
}
@@ -148,7 +288,7 @@ func (a *Announce) HandleAnnounce(data []byte) error {
return nil
}
func (a *Announce) RequestPath(destHash []byte, onInterface transport.Interface) error {
func (a *Announce) RequestPath(destHash []byte, onInterface common.NetworkInterface) error {
a.mutex.Lock()
defer a.mutex.Unlock()
@@ -158,9 +298,225 @@ func (a *Announce) RequestPath(destHash []byte, onInterface transport.Interface)
packet = append(packet, byte(0)) // Initial hop count
// Send path request
if err := onInterface.SendPathRequest(packet); err != nil {
return err
return onInterface.Send(packet, "")
}
// CreateHeader creates a Reticulum packet header according to spec
func CreateHeader(ifacFlag byte, headerType byte, contextFlag byte, propType byte, destType byte, packetType byte, hops byte) []byte {
header := make([]byte, 2)
// First byte: [IFAC Flag], [Header Type], [Context Flag], [Propagation Type], [Destination Type] and [Packet Type]
header[0] = ifacFlag | (headerType << 6) | (contextFlag << 5) |
(propType << 4) | (destType << 2) | packetType
// Second byte: Number of hops
header[1] = hops
return header
}
func (a *Announce) CreatePacket() []byte {
// This function creates the complete announce packet according to the Reticulum specification.
// Announce Packet Structure:
// [Header (2 bytes)][Dest Hash (16 bytes)][Context (1 byte)][Announce Data]
// Announce Data Structure:
// [Public Key (64 bytes)][Name Hash (10 bytes)][Random Hash (10 bytes)][Ratchet (32 bytes optional)][Signature (64 bytes)][App Data]
// 2. Destination Hash
destHash := a.destinationHash
if len(destHash) > 16 {
destHash = destHash[:16]
}
return nil
}
// 3. Announce Data
// 3.1 Public Key (full 64 bytes - not split into enc/sign keys in packet)
pubKey := a.identity.GetPublicKey()
if len(pubKey) != 64 {
debug.Log(debug.DEBUG_TRACE, "Invalid public key length", "expected", 64, "got", len(pubKey))
}
// 3.2 Name Hash
nameHash := sha256.Sum256([]byte(a.destinationName))
nameHash10 := nameHash[:10]
// 3.3 Random Hash (5 bytes random + 5 bytes timestamp)
randomHash := make([]byte, 10)
_, err := rand.Read(randomHash[:5])
if err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to read random bytes for announce", "error", 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)
var ratchetData []byte
currentRatchetKey := a.identity.GetCurrentRatchetKey()
if currentRatchetKey != nil {
ratchetPub, err := curve25519.X25519(currentRatchetKey, curve25519.Basepoint)
if err == nil {
ratchetData = make([]byte, 32)
copy(ratchetData, ratchetPub)
}
}
// Determine context flag based on whether ratchet exists
contextFlag := byte(0)
if len(ratchetData) > 0 {
contextFlag = 1 // FLAG_SET
}
// 1. Create Header - Use HEADER_TYPE_1
header := CreateHeader(
IFAC_NONE,
HEADER_TYPE_1,
contextFlag,
PROP_TYPE_BROADCAST,
DEST_TYPE_SINGLE,
PACKET_TYPE_ANNOUNCE,
a.hops,
)
// 4. Context Byte
contextByte := byte(0)
if a.pathResponse {
contextByte = 0x0B // PATH_RESPONSE context
}
// 3.5 Signature
// The signature is calculated over: Dest Hash + Public Key (64 bytes) + Name Hash + Random Hash + Ratchet (if exists) + App Data
validationData := make([]byte, 0)
validationData = append(validationData, destHash...)
validationData = append(validationData, pubKey...)
validationData = append(validationData, nameHash10...)
validationData = append(validationData, randomHash...)
if len(ratchetData) > 0 {
validationData = append(validationData, ratchetData...)
}
validationData = append(validationData, a.appData...)
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))
// 5. Assemble the packet (HEADER_TYPE_1 format)
packet := make([]byte, 0)
packet = append(packet, header...)
packet = append(packet, destHash...)
packet = append(packet, contextByte)
packet = append(packet, pubKey...)
packet = append(packet, nameHash10...)
packet = append(packet, randomHash...)
if len(ratchetData) > 0 {
packet = append(packet, ratchetData...)
}
packet = append(packet, signature...)
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
}
type AnnouncePacket struct {
Data []byte
}
func NewAnnouncePacket(pubKey []byte, appData []byte, announceID []byte) *AnnouncePacket {
packet := &AnnouncePacket{}
// Build packet data
packet.Data = make([]byte, 0, len(pubKey)+len(appData)+len(announceID)+4)
// Add header
packet.Data = append(packet.Data, PACKET_TYPE_ANNOUNCE)
packet.Data = append(packet.Data, ANNOUNCE_IDENTITY)
// Add public key
packet.Data = append(packet.Data, pubKey...)
// Add app data length and content
appDataLen := make([]byte, 2)
binary.BigEndian.PutUint16(appDataLen, uint16(len(appData))) // #nosec G115
packet.Data = append(packet.Data, appDataLen...)
packet.Data = append(packet.Data, appData...)
// Add announce ID
packet.Data = append(packet.Data, announceID...)
return packet
}
// 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) {
debug.Log(debug.DEBUG_TRACE, "Creating new announce", "destHash", fmt.Sprintf("%x", destinationHash), "appDataLen", len(appData), "hasRatchet", ratchetID != nil, "pathResponse", pathResponse)
if identity == nil {
debug.Log(debug.DEBUG_ERROR, "Nil identity provided")
return nil, errors.New("identity cannot be nil")
}
if config == nil {
return nil, errors.New("config cannot be nil")
}
if len(destinationHash) == 0 {
return nil, errors.New("destination hash cannot be empty")
}
destHash := destinationHash
debug.Log(debug.DEBUG_TRACE, "Using provided destination hash", "destHash", fmt.Sprintf("%x", destHash))
a := &Announce{
identity: identity,
appData: appData,
ratchetID: ratchetID,
pathResponse: pathResponse,
destinationHash: destHash,
hops: 0,
mutex: &sync.RWMutex{},
handlers: make([]Handler, 0),
config: config,
}
debug.Log(debug.DEBUG_TRACE, "Created announce object", "destHash", fmt.Sprintf("%x", a.destinationHash), "hops", a.hops)
// Create initial packet
packet := a.CreatePacket()
a.packet = packet
// Generate hash
hash := a.Hash()
debug.Log(debug.DEBUG_TRACE, "Generated announce hash", "hash", fmt.Sprintf("%x", hash))
return a, nil
}
func (a *Announce) Hash() []byte {
if a.hash == nil {
// Generate hash from announce data
h := sha256.New()
h.Write(a.destinationHash)
h.Write(a.identity.GetPublicKey())
h.Write([]byte{a.hops})
h.Write(a.appData)
if a.ratchetID != nil {
h.Write(a.ratchetID)
}
a.hash = h.Sum(nil)
}
return a.hash
}
func (a *Announce) GetPacket() []byte {
a.mutex.Lock()
defer a.mutex.Unlock()
if a.packet == nil {
// Use CreatePacket to generate the packet
a.packet = a.CreatePacket()
}
return a.packet
}

View File

@@ -0,0 +1,123 @@
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))
}
}

9
pkg/announce/handler.go Normal file
View File

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

266
pkg/buffer/buffer.go Normal file
View File

@@ -0,0 +1,266 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package buffer
import (
"bufio"
"bytes"
"compress/bzip2"
"encoding/binary"
"io"
"sync"
"git.quad4.io/Networks/Reticulum-Go/pkg/channel"
)
const (
StreamIDMax = 0x3fff // 16383
MaxChunkLen = 16 * 1024
MaxDataLen = 457 // MDU - 2 - 6 (2 for stream header, 6 for channel envelope)
CompressTries = 4
// Stream header flags
StreamHeaderEOF = 0x8000
StreamHeaderCompressed = 0x4000
// Message type
StreamDataMessageType = 0x01
// Header size
StreamHeaderSize = 2
// Compression threshold
CompressThreshold = 32
)
type StreamDataMessage struct {
StreamID uint16
Data []byte
EOF bool
Compressed bool
}
func (m *StreamDataMessage) Pack() ([]byte, error) {
headerVal := uint16(m.StreamID & StreamIDMax)
if m.EOF {
headerVal |= StreamHeaderEOF
}
if m.Compressed {
headerVal |= StreamHeaderCompressed
}
buf := new(bytes.Buffer)
if err := binary.Write(buf, binary.BigEndian, headerVal); err != nil { // #nosec G104
return nil, err // Or handle the error appropriately
}
buf.Write(m.Data)
return buf.Bytes(), nil
}
func (m *StreamDataMessage) GetType() uint16 {
return StreamDataMessageType
}
func (m *StreamDataMessage) Unpack(data []byte) error {
if len(data) < StreamHeaderSize {
return io.ErrShortBuffer
}
header := binary.BigEndian.Uint16(data[:StreamHeaderSize])
m.StreamID = header & StreamIDMax
m.EOF = (header & StreamHeaderEOF) != 0
m.Compressed = (header & StreamHeaderCompressed) != 0
m.Data = data[StreamHeaderSize:]
return nil
}
type RawChannelReader struct {
streamID int
channel *channel.Channel
buffer *bytes.Buffer
eof bool
callbacks map[int]func(int)
nextCallbackID int
messageHandlerID int
mutex sync.RWMutex
}
func NewRawChannelReader(streamID int, ch *channel.Channel) *RawChannelReader {
reader := &RawChannelReader{
streamID: streamID,
channel: ch,
buffer: bytes.NewBuffer(nil),
callbacks: make(map[int]func(int)),
}
reader.messageHandlerID = ch.AddMessageHandler(reader.HandleMessage)
return reader
}
func (r *RawChannelReader) AddReadyCallback(cb func(int)) int {
r.mutex.Lock()
defer r.mutex.Unlock()
id := r.nextCallbackID
r.nextCallbackID++
r.callbacks[id] = cb
return id
}
func (r *RawChannelReader) RemoveReadyCallback(id int) {
r.mutex.Lock()
defer r.mutex.Unlock()
delete(r.callbacks, id)
}
func (r *RawChannelReader) Read(p []byte) (n int, err error) {
r.mutex.Lock()
defer r.mutex.Unlock()
if r.buffer.Len() == 0 && r.eof {
return 0, io.EOF
}
n, err = r.buffer.Read(p)
if err == io.EOF && !r.eof {
err = nil
}
return n, err
}
func (r *RawChannelReader) HandleMessage(msg channel.MessageBase) bool { // #nosec G115
if streamMsg, ok := msg.(*StreamDataMessage); ok && streamMsg.StreamID == uint16(r.streamID) {
r.mutex.Lock()
defer r.mutex.Unlock()
if streamMsg.Compressed {
decompressed := decompressData(streamMsg.Data)
r.buffer.Write(decompressed)
} else {
r.buffer.Write(streamMsg.Data)
}
if streamMsg.EOF {
r.eof = true
}
// Notify callbacks
for _, cb := range r.callbacks {
cb(r.buffer.Len())
}
return true
}
return false
}
type RawChannelWriter struct {
streamID int
channel *channel.Channel
eof bool
}
func NewRawChannelWriter(streamID int, ch *channel.Channel) *RawChannelWriter {
return &RawChannelWriter{
streamID: streamID,
channel: ch,
}
}
func (w *RawChannelWriter) Write(p []byte) (n int, err error) {
if len(p) > MaxChunkLen {
p = p[:MaxChunkLen]
}
msg := &StreamDataMessage{
StreamID: uint16(w.streamID), // #nosec G115
Data: p,
EOF: w.eof,
}
if len(p) > CompressThreshold {
for try := 1; try < CompressTries; try++ {
chunkLen := len(p) / try
compressed := compressData(p[:chunkLen])
if len(compressed) < MaxDataLen && len(compressed) < chunkLen {
msg.Data = compressed
msg.Compressed = true
break
}
}
}
if err := w.channel.Send(msg); err != nil {
return 0, err
}
return len(p), nil
}
func (w *RawChannelWriter) Close() error {
w.eof = true
_, err := w.Write(nil)
return err
}
type Buffer struct {
ReadWriter *bufio.ReadWriter
}
func (b *Buffer) Write(p []byte) (n int, err error) {
return b.ReadWriter.Write(p)
}
func (b *Buffer) Read(p []byte) (n int, err error) {
return b.ReadWriter.Read(p)
}
func (b *Buffer) Close() error {
return b.ReadWriter.Writer.Flush()
}
func CreateReader(streamID int, ch *channel.Channel, readyCallback func(int)) *bufio.Reader {
raw := NewRawChannelReader(streamID, ch)
if readyCallback != nil {
raw.AddReadyCallback(readyCallback)
}
return bufio.NewReader(raw)
}
func CreateWriter(streamID int, ch *channel.Channel) *bufio.Writer {
raw := NewRawChannelWriter(streamID, ch)
return bufio.NewWriter(raw)
}
func CreateBidirectionalBuffer(receiveStreamID, sendStreamID int, ch *channel.Channel, readyCallback func(int)) *bufio.ReadWriter {
reader := CreateReader(receiveStreamID, ch, readyCallback)
writer := CreateWriter(sendStreamID, ch)
return bufio.NewReadWriter(reader, writer)
}
func compressData(data []byte) []byte {
var compressed bytes.Buffer
w := bytes.NewBuffer(data)
r := bzip2.NewReader(w)
// bearer:disable go_gosec_filesystem_decompression_bomb
_, err := io.Copy(&compressed, r) // #nosec G104 #nosec G110
if err != nil {
// Handle error, e.g., log it or return an error
return nil
}
return compressed.Bytes()
}
func decompressData(data []byte) []byte {
reader := bzip2.NewReader(bytes.NewReader(data))
var decompressed bytes.Buffer
// Limit the amount of data read to prevent decompression bombs
limitedReader := io.LimitReader(reader, MaxChunkLen) // #nosec G110
// bearer:disable go_gosec_filesystem_decompression_bomb
_, err := io.Copy(&decompressed, limitedReader)
if err != nil {
// Handle error, e.g., log it or return an error
return nil
}
return decompressed.Bytes()
}

449
pkg/buffer/buffer_test.go Normal file
View File

@@ -0,0 +1,449 @@
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")
}
}

304
pkg/channel/channel.go Normal file
View File

@@ -0,0 +1,304 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package channel
import (
"errors"
"math"
"sync"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
"git.quad4.io/Networks/Reticulum-Go/pkg/transport"
)
const (
// Window sizes and thresholds
WindowInitial = 2
WindowMin = 2
WindowMinSlow = 2
WindowMinMedium = 5
WindowMinFast = 16
WindowMaxSlow = 5
WindowMaxMedium = 12
WindowMaxFast = 48
WindowMax = WindowMaxFast
WindowFlexibility = 4
// RTT thresholds
RTTFast = 0.18
RTTMedium = 0.75
RTTSlow = 1.45
// Sequence numbers
SeqMax uint16 = 0xFFFF
SeqModulus uint16 = SeqMax
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
type MessageState int
const (
MsgStateNew MessageState = iota
MsgStateSent
MsgStateDelivered
MsgStateFailed
)
// MessageBase defines the interface for messages that can be sent over a channel
type MessageBase interface {
Pack() ([]byte, error)
Unpack([]byte) error
GetType() uint16
}
// Channel manages reliable message delivery over a transport link
type Channel struct {
link transport.LinkInterface
mutex sync.RWMutex
txRing []*Envelope
rxRing []*Envelope
window int
windowMax int
windowMin int
windowFlex int
nextSequence uint16
nextRxSequence uint16
maxTries int
fastRateRounds int
medRateRounds int
messageHandlers []messageHandlerEntry
nextHandlerID int
}
type messageHandlerEntry struct {
id int
handler func(MessageBase) bool
}
// Envelope wraps a message with metadata for transmission
type Envelope struct {
Sequence uint16
Message MessageBase
Raw []byte
Packet interface{}
Tries int
Timestamp time.Time
}
// NewChannel creates a new Channel instance
func NewChannel(link transport.LinkInterface) *Channel {
return &Channel{
link: link,
messageHandlers: make([]messageHandlerEntry, 0),
mutex: sync.RWMutex{},
windowMax: WindowMaxSlow,
windowMin: WindowMinSlow,
window: WindowInitial,
maxTries: DefaultMaxTries,
}
}
// Send transmits a message over the channel
func (c *Channel) Send(msg MessageBase) error {
if c.link.GetStatus() != transport.STATUS_ACTIVE {
return errors.New("link not ready")
}
env := &Envelope{
Sequence: c.nextSequence,
Message: msg,
Timestamp: time.Now(),
}
c.mutex.Lock()
c.nextSequence = (c.nextSequence + common.ONE) % SeqModulus
c.txRing = append(c.txRing, env)
c.mutex.Unlock()
data, err := msg.Pack()
if err != nil {
return err
}
env.Raw = data
packet := c.link.Send(data)
env.Packet = packet
env.Tries++
timeout := c.getPacketTimeout(env.Tries)
c.link.SetPacketTimeout(packet, c.handleTimeout, timeout)
c.link.SetPacketDelivered(packet, c.handleDelivered)
return nil
}
// handleTimeout handles packet timeout events
func (c *Channel) handleTimeout(packet interface{}) {
c.mutex.Lock()
defer c.mutex.Unlock()
for _, env := range c.txRing {
if env.Packet == packet {
if env.Tries >= c.maxTries {
// Remove from ring and notify failure
return
}
env.Tries++
if err := c.link.Resend(packet); err != nil { // #nosec G104
// Handle resend error, e.g., log it or mark envelope as failed
debug.Log(debug.DEBUG_INFO, "Failed to resend packet", "error", err)
// Optionally, mark the envelope as failed or remove it from txRing
// env.State = MsgStateFailed
// c.txRing = append(c.txRing[:i], c.txRing[i+1:]...)
return
}
timeout := c.getPacketTimeout(env.Tries)
c.link.SetPacketTimeout(packet, c.handleTimeout, timeout)
break
}
}
}
// handleDelivered handles packet delivery confirmations
func (c *Channel) handleDelivered(packet interface{}) {
c.mutex.Lock()
defer c.mutex.Unlock()
for i, env := range c.txRing {
if env.Packet == packet {
c.txRing = append(c.txRing[:i], c.txRing[i+1:]...)
break
}
}
}
func (c *Channel) getPacketTimeout(tries int) time.Duration {
rtt := c.link.GetRTT()
if rtt < RTTMinThreshold {
rtt = RTTMinThreshold
}
timeout := math.Pow(TimeoutBaseMultiplier, float64(tries-common.ONE)) * rtt * TimeoutRingMultiplier * float64(len(c.txRing)+TimeoutRingOffset)
return time.Duration(timeout * float64(time.Second))
}
func (c *Channel) AddMessageHandler(handler func(MessageBase) bool) int {
c.mutex.Lock()
defer c.mutex.Unlock()
id := c.nextHandlerID
c.nextHandlerID++
c.messageHandlers = append(c.messageHandlers, messageHandlerEntry{id: id, handler: handler})
return id
}
func (c *Channel) RemoveMessageHandler(id int) {
c.mutex.Lock()
defer c.mutex.Unlock()
for i, entry := range c.messageHandlers {
if entry.id == id {
c.messageHandlers = append(c.messageHandlers[:i], c.messageHandlers[i+1:]...)
break
}
}
}
func (c *Channel) updateRateThresholds() {
rtt := c.link.RTT()
if rtt > RTTFast {
c.fastRateRounds = common.ZERO
if rtt > RTTMedium {
c.medRateRounds = common.ZERO
} else {
c.medRateRounds++
if c.windowMax < WindowMaxMedium && c.medRateRounds == FastRateThreshold {
c.windowMax = WindowMaxMedium
c.windowMin = WindowMinMedium
}
}
} else {
c.fastRateRounds++
if c.windowMax < WindowMaxFast && c.fastRateRounds == FastRateThreshold {
c.windowMax = WindowMaxFast
c.windowMin = WindowMinFast
}
}
}
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 {
c.mutex.Lock()
defer c.mutex.Unlock()
// Cleanup resources
return nil
}

130
pkg/channel/channel_test.go Normal file
View File

@@ -0,0 +1,130 @@
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,29 +1,95 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package common
import (
"fmt"
)
const (
DEFAULT_SHARED_INSTANCE_PORT = 37428
DEFAULT_INSTANCE_CONTROL_PORT = 37429
DEFAULT_LOG_LEVEL = 20
)
// ConfigProvider interface for accessing configuration
type ConfigProvider interface {
GetConfigPath() string
GetLogLevel() int
GetInterfaces() map[string]InterfaceConfig
GetConfigPath() string
GetLogLevel() int
GetInterfaces() map[string]InterfaceConfig
}
// InterfaceConfig represents interface configuration
type InterfaceConfig struct {
Type string `toml:"type"`
Enabled bool `toml:"enabled"`
TargetHost string `toml:"target_host,omitempty"`
TargetPort int `toml:"target_port,omitempty"`
Interface string `toml:"interface,omitempty"`
Name string
Type string
Enabled bool
Address string
Port int
TargetHost string
TargetPort int
TargetAddress string
Interface string
KISSFraming bool
I2PTunneled bool
PreferIPv6 bool
MaxReconnTries int
Bitrate int64
MTU int
GroupID string
DiscoveryScope string
DiscoveryPort int
DataPort int
}
// ReticulumConfig represents the main configuration structure
type ReticulumConfig struct {
EnableTransport bool `toml:"enable_transport"`
ShareInstance bool `toml:"share_instance"`
SharedInstancePort int `toml:"shared_instance_port"`
InstanceControlPort int `toml:"instance_control_port"`
PanicOnInterfaceErr bool `toml:"panic_on_interface_error"`
LogLevel int `toml:"loglevel"`
ConfigPath string `toml:"-"`
Interfaces map[string]InterfaceConfig
}
ConfigPath string
EnableTransport bool
ShareInstance bool
SharedInstancePort int
InstanceControlPort int
PanicOnInterfaceErr bool
LogLevel int
Interfaces map[string]*InterfaceConfig
AppName string
AppAspect string
}
// NewReticulumConfig creates a new ReticulumConfig with default values
func NewReticulumConfig() *ReticulumConfig {
return &ReticulumConfig{
EnableTransport: true,
ShareInstance: false,
SharedInstancePort: DEFAULT_SHARED_INSTANCE_PORT,
InstanceControlPort: DEFAULT_INSTANCE_CONTROL_PORT,
PanicOnInterfaceErr: false,
LogLevel: DEFAULT_LOG_LEVEL,
Interfaces: make(map[string]*InterfaceConfig),
}
}
// Validate checks if the configuration is valid
func (c *ReticulumConfig) Validate() error {
if c.SharedInstancePort < 1 || c.SharedInstancePort > 65535 {
return fmt.Errorf("invalid shared instance port: %d", c.SharedInstancePort)
}
if c.InstanceControlPort < 1 || c.InstanceControlPort > 65535 {
return fmt.Errorf("invalid instance control port: %d", c.InstanceControlPort)
}
return nil
}
func DefaultConfig() *ReticulumConfig {
return &ReticulumConfig{
EnableTransport: true,
ShareInstance: false,
SharedInstancePort: DEFAULT_SHARED_INSTANCE_PORT,
InstanceControlPort: DEFAULT_INSTANCE_CONTROL_PORT,
PanicOnInterfaceErr: false,
LogLevel: DEFAULT_LOG_LEVEL,
Interfaces: make(map[string]*InterfaceConfig),
AppName: "Go Client",
AppAspect: "node",
}
}

94
pkg/common/config_test.go Normal file
View File

@@ -0,0 +1,94 @@
package common
import (
"testing"
)
func TestNewReticulumConfig(t *testing.T) {
cfg := NewReticulumConfig()
if !cfg.EnableTransport {
t.Errorf("NewReticulumConfig() EnableTransport = %v; want true", cfg.EnableTransport)
}
if cfg.ShareInstance {
t.Errorf("NewReticulumConfig() ShareInstance = %v; want false", cfg.ShareInstance)
}
if cfg.SharedInstancePort != DEFAULT_SHARED_INSTANCE_PORT {
t.Errorf("NewReticulumConfig() SharedInstancePort = %d; want %d", cfg.SharedInstancePort, DEFAULT_SHARED_INSTANCE_PORT)
}
if cfg.InstanceControlPort != DEFAULT_INSTANCE_CONTROL_PORT {
t.Errorf("NewReticulumConfig() InstanceControlPort = %d; want %d", cfg.InstanceControlPort, DEFAULT_INSTANCE_CONTROL_PORT)
}
if cfg.PanicOnInterfaceErr {
t.Errorf("NewReticulumConfig() PanicOnInterfaceErr = %v; want false", cfg.PanicOnInterfaceErr)
}
if cfg.LogLevel != DEFAULT_LOG_LEVEL {
t.Errorf("NewReticulumConfig() LogLevel = %d; want %d", cfg.LogLevel, DEFAULT_LOG_LEVEL)
}
if len(cfg.Interfaces) != 0 {
t.Errorf("NewReticulumConfig() Interfaces length = %d; want 0", len(cfg.Interfaces))
}
}
func TestDefaultConfig(t *testing.T) {
cfg := DefaultConfig()
if !cfg.EnableTransport {
t.Errorf("DefaultConfig() EnableTransport = %v; want true", cfg.EnableTransport)
}
if cfg.ShareInstance {
t.Errorf("DefaultConfig() ShareInstance = %v; want false", cfg.ShareInstance)
}
if cfg.SharedInstancePort != DEFAULT_SHARED_INSTANCE_PORT {
t.Errorf("DefaultConfig() SharedInstancePort = %d; want %d", cfg.SharedInstancePort, DEFAULT_SHARED_INSTANCE_PORT)
}
if cfg.InstanceControlPort != DEFAULT_INSTANCE_CONTROL_PORT {
t.Errorf("DefaultConfig() InstanceControlPort = %d; want %d", cfg.InstanceControlPort, DEFAULT_INSTANCE_CONTROL_PORT)
}
if cfg.PanicOnInterfaceErr {
t.Errorf("DefaultConfig() PanicOnInterfaceErr = %v; want false", cfg.PanicOnInterfaceErr)
}
if cfg.LogLevel != DEFAULT_LOG_LEVEL {
t.Errorf("DefaultConfig() LogLevel = %d; want %d", cfg.LogLevel, DEFAULT_LOG_LEVEL)
}
if len(cfg.Interfaces) != 0 {
t.Errorf("DefaultConfig() Interfaces length = %d; want 0", len(cfg.Interfaces))
}
if cfg.AppName != "Go Client" {
t.Errorf("DefaultConfig() AppName = %q; want %q", cfg.AppName, "Go Client")
}
if cfg.AppAspect != "node" {
t.Errorf("DefaultConfig() AppAspect = %q; want %q", cfg.AppAspect, "node")
}
}
func TestReticulumConfig_Validate(t *testing.T) {
validConfig := DefaultConfig()
if err := validConfig.Validate(); err != nil {
t.Errorf("Validate() on default config failed: %v", err)
}
invalidPortConfig1 := DefaultConfig()
invalidPortConfig1.SharedInstancePort = 0
if err := invalidPortConfig1.Validate(); err == nil {
t.Errorf("Validate() did not return error for invalid SharedInstancePort 0")
}
invalidPortConfig2 := DefaultConfig()
invalidPortConfig2.SharedInstancePort = 65536
if err := invalidPortConfig2.Validate(); err == nil {
t.Errorf("Validate() did not return error for invalid SharedInstancePort 65536")
}
invalidPortConfig3 := DefaultConfig()
invalidPortConfig3.InstanceControlPort = 0
if err := invalidPortConfig3.Validate(); err == nil {
t.Errorf("Validate() did not return error for invalid InstanceControlPort 0")
}
invalidPortConfig4 := DefaultConfig()
invalidPortConfig4.InstanceControlPort = 65536
if err := invalidPortConfig4.Validate(); err == nil {
t.Errorf("Validate() did not return error for invalid InstanceControlPort 65536")
}
}

View File

@@ -1,28 +1,146 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package common
const (
// Interface Types
IF_TYPE_UDP InterfaceType = iota
IF_TYPE_TCP
IF_TYPE_UNIX
// Interface Types
IF_TYPE_NONE InterfaceType = iota
IF_TYPE_UDP
IF_TYPE_TCP
IF_TYPE_UNIX
IF_TYPE_I2P
IF_TYPE_BLUETOOTH
IF_TYPE_SERIAL
IF_TYPE_AUTO
// Interface Modes
IF_MODE_FULL InterfaceMode = iota
IF_MODE_POINT
IF_MODE_GATEWAY
// Interface Modes
IF_MODE_FULL InterfaceMode = iota
IF_MODE_POINT
IF_MODE_GATEWAY
IF_MODE_ACCESS_POINT
IF_MODE_ROAMING
IF_MODE_BOUNDARY
// Transport Modes
TRANSPORT_MODE_DIRECT TransportMode = iota
TRANSPORT_MODE_RELAY
TRANSPORT_MODE_GATEWAY
// Transport Modes
TRANSPORT_MODE_DIRECT TransportMode = iota
TRANSPORT_MODE_RELAY
TRANSPORT_MODE_GATEWAY
// Path Status
PATH_STATUS_UNKNOWN PathStatus = iota
PATH_STATUS_DIRECT
PATH_STATUS_RELAY
PATH_STATUS_FAILED
// Path Status
PATH_STATUS_UNKNOWN PathStatus = iota
PATH_STATUS_DIRECT
PATH_STATUS_RELAY
PATH_STATUS_FAILED
// Common Constants
DEFAULT_MTU = 1500
MAX_PACKET_SIZE = 65535
)
// Resource Status
RESOURCE_STATUS_PENDING = 0x00
RESOURCE_STATUS_ACTIVE = 0x01
RESOURCE_STATUS_COMPLETE = 0x02
RESOURCE_STATUS_FAILED = 0x03
RESOURCE_STATUS_CANCELLED = 0x04
// Link Status
LINK_STATUS_PENDING = 0x00
LINK_STATUS_ACTIVE = 0x01
LINK_STATUS_CLOSED = 0x02
LINK_STATUS_FAILED = 0x03
// Direction Constants
IN = 0x01
OUT = 0x02
// Common Constants
DEFAULT_MTU = 1500
MAX_PACKET_SIZE = 65535
BITRATE_MINIMUM = 5
// Timeouts and Intervals
ESTABLISH_TIMEOUT = 6
KEEPALIVE_INTERVAL = 360
STALE_TIME = 720
PATH_REQUEST_TTL = 300
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,57 +1,211 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package common
import (
"net"
"sync"
"time"
"encoding/binary"
"net"
"sync"
"time"
)
// NetworkInterface combines both low-level and high-level interface requirements
// NetworkInterface defines the interface for all network communication methods
type NetworkInterface interface {
// Low-level network operations
Start() error
Stop() error
Send(data []byte, address string) error
Receive() ([]byte, string, error)
GetType() InterfaceType
GetMode() InterfaceMode
GetMTU() int
// High-level packet operations
ProcessIncoming([]byte)
ProcessOutgoing([]byte) error
SendPathRequest([]byte) error
SendLinkPacket([]byte, []byte, time.Time) error
Detach()
SetPacketCallback(PacketCallback)
// Additional required fields
GetName() string
GetConn() net.Conn
IsEnabled() bool
// Core interface operations
Start() error
Stop() error
Enable()
Disable()
Detach()
// Network operations
Send(data []byte, address string) error
GetConn() net.Conn
GetMTU() int
GetName() string
// Interface properties
GetType() InterfaceType
GetMode() InterfaceMode
IsEnabled() bool
IsOnline() bool
IsDetached() bool
GetBandwidthAvailable() bool
// Packet handling
ProcessIncoming([]byte)
ProcessOutgoing([]byte) error
SendPathRequest([]byte) error
SendLinkPacket([]byte, []byte, time.Time) error
SetPacketCallback(PacketCallback)
GetPacketCallback() PacketCallback
}
type PacketCallback func([]byte, interface{})
// BaseInterface provides common implementation
// BaseInterface provides common implementation for network interfaces
type BaseInterface struct {
Name string
Mode InterfaceMode
Type InterfaceType
Online bool
Detached bool
IN bool
OUT bool
MTU int
Bitrate int64
TxBytes uint64
RxBytes uint64
mutex sync.RWMutex
owner interface{}
packetCallback PacketCallback
}
Name string
Mode InterfaceMode
Type InterfaceType
Online bool
Enabled bool
Detached bool
IN bool
OUT bool
MTU int
Bitrate int64
TxBytes uint64
RxBytes uint64
lastTx time.Time
Mutex sync.RWMutex
Owner interface{}
PacketCallback PacketCallback
}
// NewBaseInterface creates a new BaseInterface instance
func NewBaseInterface(name string, ifaceType InterfaceType, enabled bool) BaseInterface {
return BaseInterface{
Name: name,
Type: ifaceType,
Mode: IF_MODE_FULL,
Enabled: enabled,
MTU: DEFAULT_MTU,
Bitrate: BITRATE_MINIMUM,
lastTx: time.Now(),
}
}
// Default implementations for BaseInterface
func (i *BaseInterface) GetType() InterfaceType {
return i.Type
}
func (i *BaseInterface) GetMode() InterfaceMode {
return i.Mode
}
func (i *BaseInterface) GetMTU() int {
return i.MTU
}
func (i *BaseInterface) GetName() string {
return i.Name
}
func (i *BaseInterface) IsEnabled() bool {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.Enabled && i.Online && !i.Detached
}
func (i *BaseInterface) IsOnline() bool {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.Online
}
func (i *BaseInterface) IsDetached() bool {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.Detached
}
func (i *BaseInterface) SetPacketCallback(callback PacketCallback) {
i.Mutex.Lock()
defer i.Mutex.Unlock()
i.PacketCallback = callback
}
func (i *BaseInterface) GetPacketCallback() PacketCallback {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.PacketCallback
}
func (i *BaseInterface) Detach() {
i.Mutex.Lock()
defer i.Mutex.Unlock()
i.Detached = true
i.Online = false
}
func (i *BaseInterface) Enable() {
i.Mutex.Lock()
defer i.Mutex.Unlock()
i.Enabled = true
i.Online = true
}
func (i *BaseInterface) Disable() {
i.Mutex.Lock()
defer i.Mutex.Unlock()
i.Enabled = false
i.Online = false
}
// Default implementations that should be overridden by specific interfaces
func (i *BaseInterface) Start() error {
return nil
}
func (i *BaseInterface) Stop() error {
return nil
}
func (i *BaseInterface) GetConn() net.Conn {
return nil
}
func (i *BaseInterface) Send(data []byte, address string) error {
return i.ProcessOutgoing(data)
}
func (i *BaseInterface) ProcessIncoming(data []byte) {
if i.PacketCallback != nil {
i.PacketCallback(data, i)
}
}
func (i *BaseInterface) ProcessOutgoing(data []byte) error {
return nil
}
func (i *BaseInterface) SendPathRequest(data []byte) error {
return i.Send(data, "")
}
func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.Time) error {
// Create link packet
packet := make([]byte, 0, len(dest)+len(data)+9) // 1 byte type + dest + 8 byte timestamp
packet = append(packet, 0x02) // Link packet type
packet = append(packet, dest...)
ts := make([]byte, 8)
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) // #nosec G115
packet = append(packet, ts...)
packet = append(packet, data...)
return i.Send(packet, "")
}
func (i *BaseInterface) GetBandwidthAvailable() bool {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
// If no transmission in last second, bandwidth is available
if time.Since(i.lastTx) > time.Second {
return true
}
// Calculate current bandwidth usage
bytesPerSec := float64(i.TxBytes) / time.Since(i.lastTx).Seconds()
currentUsage := bytesPerSec * 8 // Convert to bits/sec
// Check if usage is below threshold (2% of total bitrate)
maxUsage := float64(i.Bitrate) * 0.02 // 2% propagation rate
return currentUsage < maxUsage
}

View File

@@ -0,0 +1,288 @@
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,36 +1,84 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package common
import (
"time"
)
// Interface related types
type InterfaceMode byte
type InterfaceType byte
// Destination type constants
const (
DESTINATION_SINGLE = 0x00
DESTINATION_GROUP = 0x01
DESTINATION_PLAIN = 0x02
)
// Transport related types
type TransportMode byte
type PathStatus byte
// Common structs
// Path represents routing information for a destination
type Path struct {
Interface NetworkInterface
Address string
Status PathStatus
LastSeen time.Time
NextHop []byte
Hops uint8
LastUpdated time.Time
Interface NetworkInterface
LastSeen time.Time
NextHop []byte
Hops uint8
LastUpdated time.Time
HopCount uint8
}
// Common callbacks
type ProofRequestedCallback func(interface{}) bool
type ProofRequestedCallback func([]byte, []byte)
type LinkEstablishedCallback func(interface{})
type PacketCallback func([]byte, NetworkInterface)
// Request handler
// RequestHandler manages path requests and responses
type RequestHandler struct {
Path string
ResponseGenerator func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity interface{}, requestedAt int64) []byte
AllowMode byte
AllowedList [][]byte
}
}
// Interface types
type InterfaceMode byte
type InterfaceType byte
// RatchetIDReceiver holds ratchet ID information
type RatchetIDReceiver struct {
LatestRatchetID []byte
}
// NetworkStats holds interface statistics
type NetworkStats struct {
BytesSent uint64
BytesReceived uint64
PacketsSent uint64
PacketsReceived uint64
LastUpdated time.Time
}
// LinkStatus represents the current state of a link
type LinkStatus struct {
Established bool
LastSeen time.Time
RTT time.Duration
Quality float64
Hops uint8
}
// PathRequest represents a path discovery request
type PathRequest struct {
DestinationHash []byte
Tag []byte
TTL int
Recursive bool
}
// PathResponse represents a path discovery response
type PathResponse struct {
DestinationHash []byte
NextHop []byte
Hops uint8
Tag []byte
}

View File

@@ -1,49 +1,272 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package config
import (
"io/ioutil"
"gopkg.in/yaml.v3"
"bufio"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
)
type Config struct {
Identity struct {
Name string `yaml:"name"`
StoragePath string `yaml:"storage_path"`
} `yaml:"identity"`
Identity struct {
Name string
StoragePath string
}
Interfaces []struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
Enabled bool `yaml:"enabled"`
ListenPort int `yaml:"listen_port"`
ListenIP string `yaml:"listen_ip"`
KissFraming bool `yaml:"kiss_framing"`
I2PTunneled bool `yaml:"i2p_tunneled"`
} `yaml:"interfaces"`
Interfaces []struct {
Name string
Type string
Enabled bool
ListenPort int
ListenIP string
KissFraming bool
I2PTunneled bool
}
Transport struct {
AnnounceInterval int `yaml:"announce_interval"`
PathRequestTimeout int `yaml:"path_request_timeout"`
MaxHops int `yaml:"max_hops"`
BitrateLimit int64 `yaml:"bitrate_limit"`
} `yaml:"transport"`
Transport struct {
AnnounceInterval int
PathRequestTimeout int
MaxHops int
BitrateLimit int64
}
Logging struct {
Level string `yaml:"level"`
File string `yaml:"file"`
} `yaml:"logging"`
Logging struct {
Level string
File string
}
}
func LoadConfig(path string) (*Config, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
// bearer:disable go_gosec_filesystem_filereadtaint
file, err := os.Open(path) // #nosec G304
if err != nil {
return nil, err
}
defer file.Close()
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
cfg := &Config{}
scanner := bufio.NewScanner(file)
var currentSection string
return &cfg, nil
}
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// Skip comments and empty lines
if line == "" || strings.HasPrefix(line, "#") {
continue
}
// Handle sections
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
currentSection = strings.Trim(line, "[]")
// If this is an interface section, append new interface
if strings.HasPrefix(currentSection, "interface ") {
cfg.Interfaces = append(cfg.Interfaces, struct {
Name string
Type string
Enabled bool
ListenPort int
ListenIP string
KissFraming bool
I2PTunneled bool
}{})
}
continue
}
// Parse key-value pairs
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
switch currentSection {
case "identity":
switch key {
case "name":
cfg.Identity.Name = value
case "storage_path":
cfg.Identity.StoragePath = value
}
case "transport":
switch key {
case "announce_interval":
cfg.Transport.AnnounceInterval, _ = strconv.Atoi(value)
case "path_request_timeout":
cfg.Transport.PathRequestTimeout, _ = strconv.Atoi(value)
case "max_hops":
cfg.Transport.MaxHops, _ = strconv.Atoi(value)
case "bitrate_limit":
cfg.Transport.BitrateLimit, _ = strconv.ParseInt(value, 10, 64)
}
case "logging":
switch key {
case "level":
cfg.Logging.Level = value
case "file":
cfg.Logging.File = value
}
default:
// Handle interface sections
if strings.HasPrefix(currentSection, "interface ") && len(cfg.Interfaces) > 0 {
iface := &cfg.Interfaces[len(cfg.Interfaces)-1]
switch key {
case "name":
iface.Name = value
case "type":
iface.Type = value
case "enabled":
iface.Enabled = value == "true"
case "listen_port":
iface.ListenPort, _ = strconv.Atoi(value)
case "listen_ip":
iface.ListenIP = value
case "kiss_framing":
iface.KissFraming = value == "true"
case "i2p_tunneled":
iface.I2PTunneled = value == "true"
}
}
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return cfg, nil
}
func SaveConfig(cfg *Config, path string) error {
var builder strings.Builder
// Write Identity section
builder.WriteString("[identity]\n")
builder.WriteString(fmt.Sprintf("name = %s\n", cfg.Identity.Name))
builder.WriteString(fmt.Sprintf("storage_path = %s\n\n", cfg.Identity.StoragePath))
// Write Transport section
builder.WriteString("[transport]\n")
builder.WriteString(fmt.Sprintf("announce_interval = %d\n", cfg.Transport.AnnounceInterval))
builder.WriteString(fmt.Sprintf("path_request_timeout = %d\n", cfg.Transport.PathRequestTimeout))
builder.WriteString(fmt.Sprintf("max_hops = %d\n", cfg.Transport.MaxHops))
builder.WriteString(fmt.Sprintf("bitrate_limit = %d\n\n", cfg.Transport.BitrateLimit))
// Write Logging section
builder.WriteString("[logging]\n")
builder.WriteString(fmt.Sprintf("level = %s\n", cfg.Logging.Level))
builder.WriteString(fmt.Sprintf("file = %s\n\n", cfg.Logging.File))
// Write Interface sections
for _, iface := range cfg.Interfaces {
builder.WriteString(fmt.Sprintf("[interface %s]\n", iface.Name))
builder.WriteString(fmt.Sprintf("type = %s\n", iface.Type))
builder.WriteString(fmt.Sprintf("enabled = %v\n", iface.Enabled))
builder.WriteString(fmt.Sprintf("listen_port = %d\n", iface.ListenPort))
builder.WriteString(fmt.Sprintf("listen_ip = %s\n", iface.ListenIP))
builder.WriteString(fmt.Sprintf("kiss_framing = %v\n", iface.KissFraming))
builder.WriteString(fmt.Sprintf("i2p_tunneled = %v\n\n", iface.I2PTunneled))
}
return os.WriteFile(path, []byte(builder.String()), 0600) // #nosec G306
}
func GetConfigDir() string {
homeDir, err := os.UserHomeDir()
if err != nil {
// Fallback to current directory if home directory cannot be determined
return ".reticulum-go"
}
return filepath.Join(homeDir, ".reticulum-go")
}
func GetDefaultConfigPath() string {
return filepath.Join(GetConfigDir(), "config")
}
func EnsureConfigDir() error {
configDir := GetConfigDir()
return os.MkdirAll(configDir, 0700) // #nosec G301
}
func InitConfig() (*Config, error) {
// Ensure config directory exists
if err := EnsureConfigDir(); err != nil {
return nil, fmt.Errorf("failed to create config directory: %v", err)
}
configPath := GetDefaultConfigPath()
// Check if config file exists
if _, err := os.Stat(configPath); os.IsNotExist(err) {
// Create default config
cfg := &Config{}
// Set default values
cfg.Identity.Name = "reticulum-node"
cfg.Identity.StoragePath = filepath.Join(GetConfigDir(), "storage")
cfg.Transport.AnnounceInterval = 300
cfg.Transport.PathRequestTimeout = 15
cfg.Transport.MaxHops = 8
cfg.Transport.BitrateLimit = 1000000
cfg.Logging.Level = "info"
cfg.Logging.File = filepath.Join(GetConfigDir(), "reticulum.log")
cfg.Interfaces = append(cfg.Interfaces, struct {
Name string
Type string
Enabled bool
ListenPort int
ListenIP string
KissFraming bool
I2PTunneled bool
}{
Name: "Local UDP",
Type: "UDPInterface",
Enabled: true,
ListenPort: 37697,
ListenIP: "0.0.0.0",
})
cfg.Interfaces = append(cfg.Interfaces, struct {
Name string
Type string
Enabled bool
ListenPort int
ListenIP string
KissFraming bool
I2PTunneled bool
}{
Name: "Auto Discovery",
Type: "AutoInterface",
Enabled: true,
ListenPort: 29717,
})
// Save default config
if err := SaveConfig(cfg, configPath); err != nil {
return nil, fmt.Errorf("failed to save default config: %v", err)
}
}
// Load config
cfg, err := LoadConfig(configPath)
if err != nil {
return nil, fmt.Errorf("failed to load config: %v", err)
}
return cfg, nil
}

192
pkg/config/config_test.go Normal file
View File

@@ -0,0 +1,192 @@
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")
}
}

112
pkg/cryptography/aes.go Normal file
View File

@@ -0,0 +1,112 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package cryptography
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"errors"
"io"
)
const (
// AES256KeySize is the size of an AES-256 key in bytes.
AES256KeySize = 32 // 256 bits
)
// GenerateAES256Key generates a random AES-256 key.
func GenerateAES256Key() ([]byte, error) {
key := make([]byte, AES256KeySize)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
return nil, err
}
return key, nil
}
// EncryptAES256CBC encrypts data using AES-256 in CBC mode.
// The IV is prepended to the ciphertext.
func EncryptAES256CBC(key, plaintext []byte) ([]byte, error) {
if len(key) != AES256KeySize {
return nil, errors.New("invalid key size: must be 32 bytes for AES-256")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// Generate a random IV.
iv := make([]byte, aes.BlockSize)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
// Add PKCS7 padding.
padding := aes.BlockSize - len(plaintext)%aes.BlockSize
padtext := make([]byte, len(plaintext)+padding)
copy(padtext, plaintext)
for i := len(plaintext); i < len(padtext); i++ {
padtext[i] = byte(padding)
}
// Encrypt the data.
mode := cipher.NewCBCEncrypter(block, iv) // #nosec G407
ciphertext := make([]byte, len(padtext))
mode.CryptBlocks(ciphertext, padtext)
// Prepend the IV to the ciphertext.
return append(iv, ciphertext...), nil
}
// DecryptAES256CBC decrypts data using AES-256 in CBC mode.
// It assumes the IV is prepended to the ciphertext.
func DecryptAES256CBC(key, ciphertext []byte) ([]byte, error) {
if len(key) != AES256KeySize {
return nil, errors.New("invalid key size: must be 32 bytes for AES-256")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(ciphertext) < aes.BlockSize {
return nil, errors.New("ciphertext is too short")
}
// Extract the IV from the beginning of the ciphertext.
iv := ciphertext[:aes.BlockSize]
ciphertext = ciphertext[aes.BlockSize:]
if len(ciphertext)%aes.BlockSize != 0 {
return nil, errors.New("ciphertext is not a multiple of the block size")
}
// Decrypt the data.
mode := cipher.NewCBCDecrypter(block, iv)
plaintext := make([]byte, len(ciphertext))
mode.CryptBlocks(plaintext, ciphertext)
// Remove PKCS7 padding.
if len(plaintext) == 0 {
return nil, errors.New("invalid padding: plaintext is empty")
}
padding := int(plaintext[len(plaintext)-1])
if padding > aes.BlockSize || padding == 0 {
return nil, errors.New("invalid padding size")
}
if len(plaintext) < padding {
return nil, errors.New("invalid padding: padding size is larger than plaintext")
}
// Verify the padding bytes.
for i := len(plaintext) - padding; i < len(plaintext); i++ {
if plaintext[i] != byte(padding) {
return nil, errors.New("invalid padding bytes")
}
}
return plaintext[:len(plaintext)-padding], nil
}

View File

@@ -0,0 +1,199 @@
package cryptography
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"fmt"
"testing"
)
func TestGenerateAES256Key(t *testing.T) {
key, err := GenerateAES256Key()
if err != nil {
t.Fatalf("GenerateAES256Key failed: %v", err)
}
if len(key) != AES256KeySize {
t.Errorf("Expected key size %d, got %d", AES256KeySize, len(key))
}
}
func TestAES256CBCEncryptionDecryption(t *testing.T) {
key, err := GenerateAES256Key()
if err != nil {
t.Fatalf("Failed to generate AES-256 key: %v", err)
}
testCases := []struct {
name string
plaintext []byte
}{
{"ShortMessage", []byte("Hello")},
{"BlockSizeMessage", []byte("This is 16 bytes")},
{"LongMessage", []byte("This is a longer message that spans multiple AES blocks and tests the padding.")},
{"EmptyMessage", []byte("")},
{"SingleByte", []byte("A")},
{"ExactlyTwoBlocks", []byte("This is exactly 32 bytes long!!!")},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ciphertext, err := EncryptAES256CBC(key, tc.plaintext)
if err != nil {
t.Fatalf("EncryptAES256CBC failed: %v", err)
}
decrypted, err := DecryptAES256CBC(key, ciphertext)
if err != nil {
t.Fatalf("DecryptAES256CBC failed: %v", err)
}
if !bytes.Equal(tc.plaintext, decrypted) {
t.Errorf("Decrypted text does not match original plaintext.\nGot: %q (%x)\nWant: %q (%x)",
decrypted, decrypted, tc.plaintext, tc.plaintext)
}
})
}
}
func TestAES256CBC_InvalidKeySize(t *testing.T) {
plaintext := []byte("test message")
invalidKeys := [][]byte{
make([]byte, 16), // AES-128
make([]byte, 24), // AES-192
make([]byte, 15), // Too short
make([]byte, 33), // Too long
nil, // Nil key
}
for i, key := range invalidKeys {
t.Run(fmt.Sprintf("InvalidKey_%d", i), func(t *testing.T) {
_, err := EncryptAES256CBC(key, plaintext)
if err == nil {
t.Error("EncryptAES256CBC should have failed with invalid key size")
}
// Test with some dummy ciphertext
dummyCiphertext := make([]byte, 32) // Just enough for IV + one block
rand.Read(dummyCiphertext)
_, err = DecryptAES256CBC(key, dummyCiphertext)
if err == nil {
t.Error("DecryptAES256CBC should have failed with invalid key size")
}
})
}
}
func TestDecryptAES256CBCErrorCases(t *testing.T) {
key, err := GenerateAES256Key()
if err != nil {
t.Fatalf("Failed to generate key: %v", err)
}
t.Run("CiphertextTooShort", func(t *testing.T) {
shortCiphertext := []byte{0x01, 0x02, 0x03} // Less than AES block size
_, err := DecryptAES256CBC(key, shortCiphertext)
if err == nil {
t.Error("DecryptAES256CBC should have failed for ciphertext shorter than block size")
}
})
t.Run("CiphertextNotMultipleOfBlockSize", func(t *testing.T) {
iv := make([]byte, aes.BlockSize)
rand.Read(iv)
invalidCiphertext := append(iv, []byte{0x01, 0x02, 0x03}...) // IV + data not multiple of block size
_, err := DecryptAES256CBC(key, invalidCiphertext)
if err == nil {
t.Error("DecryptAES256CBC should have failed for ciphertext not multiple of block size")
}
})
t.Run("InvalidPadding", func(t *testing.T) {
// Create a valid ciphertext first
plaintext := []byte("valid data")
ciphertext, err := EncryptAES256CBC(key, plaintext)
if err != nil {
t.Fatalf("Failed to create test ciphertext: %v", err)
}
// Corrupt the byte that XORs with the last padding byte.
// 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))
copy(corruptedCiphertext, ciphertext)
corruptedCiphertext[len(ciphertext)-aes.BlockSize-1] ^= 0xFF
_, err = DecryptAES256CBC(key, corruptedCiphertext)
if err == nil {
t.Error("DecryptAES256CBC should have failed for corrupted padding")
}
})
t.Run("EmptyPlaintextAfterDecryption", func(t *testing.T) {
// This creates a ciphertext that decrypts to just padding
key, _ := GenerateAES256Key()
iv := make([]byte, aes.BlockSize)
// A block of padding bytes
paddedBlock := bytes.Repeat([]byte{byte(aes.BlockSize)}, aes.BlockSize)
block, _ := aes.NewCipher(key)
mode := cipher.NewCBCEncrypter(block, iv)
ciphertext := make([]byte, len(paddedBlock))
mode.CryptBlocks(ciphertext, paddedBlock)
// Prepend IV
fullCiphertext := append(iv, ciphertext...)
// This should decrypt to an empty slice, which is valid
decrypted, err := DecryptAES256CBC(key, fullCiphertext)
if err != nil {
t.Errorf("DecryptAES256CBC failed for empty plaintext case: %v", err)
}
if len(decrypted) != 0 {
t.Errorf("Expected empty plaintext, got %q", decrypted)
}
})
}
func TestConstants(t *testing.T) {
if AES256KeySize != 32 {
t.Errorf("AES256KeySize should be 32, got %d", AES256KeySize)
}
}
func BenchmarkAES256CBC(b *testing.B) {
key, err := GenerateAES256Key()
if err != nil {
b.Fatalf("Failed to generate key: %v", err)
}
data := make([]byte, 1024) // 1KB of data
rand.Read(data)
b.Run("Encrypt", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := EncryptAES256CBC(key, data)
if err != nil {
b.Fatal(err)
}
}
})
ciphertext, _ := EncryptAES256CBC(key, data)
b.Run("Decrypt", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := DecryptAES256CBC(key, ciphertext)
if err != nil {
b.Fatal(err)
}
}
})
}

View File

@@ -0,0 +1,24 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package cryptography
import (
"crypto/sha256"
"golang.org/x/crypto/curve25519"
)
const (
SHA256Size = 32
)
// GetBasepoint returns the standard Curve25519 basepoint
func GetBasepoint() []byte {
return curve25519.Basepoint
}
func Hash(data []byte) []byte {
h := sha256.New()
h.Write(data)
return h.Sum(nil)
}

View File

@@ -0,0 +1,27 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package cryptography
import (
"crypto/rand"
"golang.org/x/crypto/curve25519"
)
func GenerateKeyPair() (privateKey, publicKey []byte, err error) {
privateKey = make([]byte, curve25519.ScalarSize)
if _, err := rand.Read(privateKey); err != nil {
return nil, nil, err
}
publicKey, err = curve25519.X25519(privateKey, curve25519.Basepoint)
if err != nil {
return nil, nil, err
}
return privateKey, publicKey, nil
}
func DeriveSharedSecret(privateKey, peerPublicKey []byte) ([]byte, error) {
return curve25519.X25519(privateKey, peerPublicKey)
}

View File

@@ -0,0 +1,63 @@
package cryptography
import (
"bytes"
"testing"
"golang.org/x/crypto/curve25519"
)
func TestGenerateKeyPair(t *testing.T) {
priv1, pub1, err := GenerateKeyPair()
if err != nil {
t.Fatalf("GenerateKeyPair failed: %v", err)
}
if len(priv1) != curve25519.ScalarSize {
t.Errorf("Private key length is %d, want %d", len(priv1), curve25519.ScalarSize)
}
if len(pub1) != curve25519.PointSize {
t.Errorf("Public key length is %d, want %d", len(pub1), curve25519.PointSize)
}
// Generate another pair, should be different
priv2, pub2, err := GenerateKeyPair()
if err != nil {
t.Fatalf("Second GenerateKeyPair failed: %v", err)
}
if bytes.Equal(priv1, priv2) {
t.Error("Generated private keys are identical")
}
if bytes.Equal(pub1, pub2) {
t.Error("Generated public keys are identical")
}
}
func TestDeriveSharedSecret(t *testing.T) {
privA, pubA, err := GenerateKeyPair()
if err != nil {
t.Fatalf("GenerateKeyPair A failed: %v", err)
}
privB, pubB, err := GenerateKeyPair()
if err != nil {
t.Fatalf("GenerateKeyPair B failed: %v", err)
}
secretA, err := DeriveSharedSecret(privA, pubB)
if err != nil {
t.Fatalf("DeriveSharedSecret (A perspective) failed: %v", err)
}
secretB, err := DeriveSharedSecret(privB, pubA)
if err != nil {
t.Fatalf("DeriveSharedSecret (B perspective) failed: %v", err)
}
if !bytes.Equal(secretA, secretB) {
t.Errorf("Derived shared secrets do not match:\nSecret A: %x\nSecret B: %x", secretA, secretB)
}
if len(secretA) != curve25519.PointSize { // Shared secret length
t.Errorf("Shared secret length is %d, want %d", len(secretA), curve25519.PointSize)
}
}

View File

@@ -0,0 +1,20 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package cryptography
import (
"crypto/ed25519"
"crypto/rand"
)
func GenerateSigningKeyPair() (ed25519.PublicKey, ed25519.PrivateKey, error) {
return ed25519.GenerateKey(rand.Reader)
}
func Sign(privateKey ed25519.PrivateKey, message []byte) []byte {
return ed25519.Sign(privateKey, message)
}
func Verify(publicKey ed25519.PublicKey, message, signature []byte) bool {
return ed25519.Verify(publicKey, message, signature)
}

View File

@@ -0,0 +1,79 @@
package cryptography
import (
"crypto/ed25519"
"testing"
)
func TestGenerateSigningKeyPair(t *testing.T) {
pub1, priv1, err := GenerateSigningKeyPair()
if err != nil {
t.Fatalf("GenerateSigningKeyPair failed: %v", err)
}
if len(pub1) != ed25519.PublicKeySize {
t.Errorf("Public key length is %d, want %d", len(pub1), ed25519.PublicKeySize)
}
if len(priv1) != ed25519.PrivateKeySize {
t.Errorf("Private key length is %d, want %d", len(priv1), ed25519.PrivateKeySize)
}
// Generate another pair, should be different
pub2, priv2, err := GenerateSigningKeyPair()
if err != nil {
t.Fatalf("Second GenerateSigningKeyPair failed: %v", err)
}
if pub1.Equal(pub2) {
t.Error("Generated public keys are identical")
}
if priv1.Equal(priv2) {
t.Error("Generated private keys are identical")
}
}
func TestSignAndVerify(t *testing.T) {
pub, priv, err := GenerateSigningKeyPair()
if err != nil {
t.Fatalf("GenerateSigningKeyPair failed: %v", err)
}
message := []byte("This message needs to be signed.")
signature := Sign(priv, message)
if len(signature) != ed25519.SignatureSize {
t.Errorf("Signature length is %d, want %d", len(signature), ed25519.SignatureSize)
}
// Verify correct signature
if !Verify(pub, message, signature) {
t.Errorf("Verify failed for a valid signature")
}
// Verify with tampered message
tamperedMessage := append(message, '!')
if Verify(pub, tamperedMessage, signature) {
t.Errorf("Verify succeeded for a tampered message")
}
// Verify with tampered signature
tamperedSignature := append(signature[:len(signature)-1], ^signature[len(signature)-1])
if Verify(pub, message, tamperedSignature) {
t.Errorf("Verify succeeded for a tampered signature")
}
// Verify with wrong public key
wrongPub, _, _ := GenerateSigningKeyPair()
if Verify(wrongPub, message, signature) {
t.Errorf("Verify succeeded with the wrong public key")
}
// Verify empty message
emptyMessage := []byte("")
emptySig := Sign(priv, emptyMessage)
if !Verify(pub, emptyMessage, emptySig) {
t.Errorf("Verify failed for an empty message")
}
if Verify(pub, message, emptySig) {
t.Errorf("Verify succeeded comparing non-empty message with empty signature")
}
}

50
pkg/cryptography/hkdf.go Normal file
View File

@@ -0,0 +1,50 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package cryptography
import (
"crypto/hmac"
"crypto/sha256"
"errors"
"math"
)
func DeriveKey(secret, salt, info []byte, length int) ([]byte, error) {
hashLen := 32
if length < 1 {
return nil, errors.New("invalid output key length")
}
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

@@ -0,0 +1,105 @@
package cryptography
import (
"bytes"
"testing"
)
func TestDeriveKey(t *testing.T) {
secret := []byte("test-secret")
salt := []byte("test-salt")
info := []byte("test-info")
length := 32 // Desired key length
key1, err := DeriveKey(secret, salt, info, length)
if err != nil {
t.Fatalf("DeriveKey failed: %v", err)
}
if len(key1) != length {
t.Errorf("DeriveKey returned key of length %d; want %d", len(key1), length)
}
// Derive another key with the same parameters, should be identical
key2, err := DeriveKey(secret, salt, info, length)
if err != nil {
t.Fatalf("Second DeriveKey failed: %v", err)
}
if !bytes.Equal(key1, key2) {
t.Errorf("DeriveKey is not deterministic. Got %x and %x for the same inputs", key1, key2)
}
// Derive a key with different info, should be different
differentInfo := []byte("different-info")
key3, err := DeriveKey(secret, salt, differentInfo, length)
if err != nil {
t.Fatalf("DeriveKey with different info failed: %v", err)
}
if bytes.Equal(key1, key3) {
t.Errorf("DeriveKey produced the same key for different info strings")
}
// Derive a key with different salt, should be different
differentSalt := []byte("different-salt")
key4, err := DeriveKey(secret, differentSalt, info, length)
if err != nil {
t.Fatalf("DeriveKey with different salt failed: %v", err)
}
if bytes.Equal(key1, key4) {
t.Errorf("DeriveKey produced the same key for different salts")
}
// Derive a key with different secret, should be different
differentSecret := []byte("different-secret")
key5, err := DeriveKey(differentSecret, salt, info, length)
if err != nil {
t.Fatalf("DeriveKey with different secret failed: %v", err)
}
if bytes.Equal(key1, key5) {
t.Errorf("DeriveKey produced the same key for different secrets")
}
// Derive a key with different length
differentLength := 64
key6, err := DeriveKey(secret, salt, info, differentLength)
if err != nil {
t.Fatalf("DeriveKey with different length failed: %v", err)
}
if len(key6) != differentLength {
t.Errorf("DeriveKey returned key of length %d; want %d", len(key6), differentLength)
}
}
func TestDeriveKeyEdgeCases(t *testing.T) {
secret := []byte("test-secret")
salt := []byte("test-salt")
info := []byte("test-info")
t.Run("EmptySecret", func(t *testing.T) {
_, err := DeriveKey([]byte{}, salt, info, 32)
if err == nil {
t.Errorf("DeriveKey should fail with empty secret")
}
})
t.Run("EmptySalt", func(t *testing.T) {
_, err := DeriveKey(secret, []byte{}, info, 32)
if err != nil {
t.Errorf("DeriveKey failed with empty salt: %v", err)
}
})
t.Run("EmptyInfo", func(t *testing.T) {
_, err := DeriveKey(secret, salt, []byte{}, 32)
if err != nil {
t.Errorf("DeriveKey failed with empty info: %v", err)
}
})
t.Run("ZeroLength", func(t *testing.T) {
_, err := DeriveKey(secret, salt, info, 0)
if err == nil {
t.Errorf("DeriveKey should fail with zero length")
}
})
}

28
pkg/cryptography/hmac.go Normal file
View File

@@ -0,0 +1,28 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package cryptography
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
)
func GenerateHMACKey(size int) ([]byte, error) {
key := make([]byte, size)
if _, err := rand.Read(key); err != nil {
return nil, err
}
return key, nil
}
func ComputeHMAC(key, message []byte) []byte {
h := hmac.New(sha256.New, key)
h.Write(message)
return h.Sum(nil)
}
func ValidateHMAC(key, message, messageHMAC []byte) bool {
expectedHMAC := ComputeHMAC(key, message)
return hmac.Equal(messageHMAC, expectedHMAC)
}

View File

@@ -0,0 +1,80 @@
package cryptography
import (
"testing"
)
func TestGenerateHMACKey(t *testing.T) {
testSizes := []int{16, 32, 64}
for _, size := range testSizes {
t.Run("Size"+string(rune(size)), func(t *testing.T) { // Simple name conversion
key, err := GenerateHMACKey(size)
if err != nil {
t.Fatalf("GenerateHMACKey(%d) failed: %v", size, err)
}
if len(key) != size {
t.Errorf("GenerateHMACKey(%d) returned key of length %d; want %d", size, len(key), size)
}
// Check if key is not all zeros (basic check for randomness)
isZero := true
for _, b := range key {
if b != 0 {
isZero = false
break
}
}
if isZero {
t.Errorf("GenerateHMACKey(%d) returned an all-zero key", size)
}
})
}
}
func TestComputeAndValidateHMAC(t *testing.T) {
key, err := GenerateHMACKey(32) // Use SHA256 key size
if err != nil {
t.Fatalf("Failed to generate HMAC key: %v", err)
}
message := []byte("This is a test message.")
// Compute HMAC
computedHMAC := ComputeHMAC(key, message)
if len(computedHMAC) != 32 { // SHA256 output size
t.Errorf("ComputeHMAC returned HMAC of length %d; want 32", len(computedHMAC))
}
// Validate correct HMAC
if !ValidateHMAC(key, message, computedHMAC) {
t.Errorf("ValidateHMAC failed for correctly computed HMAC")
}
// Validate incorrect HMAC (tampered message)
tamperedMessage := append(message, byte('!'))
if ValidateHMAC(key, tamperedMessage, computedHMAC) {
t.Errorf("ValidateHMAC succeeded for tampered message")
}
// Validate incorrect HMAC (tampered key)
wrongKey, _ := GenerateHMACKey(32)
if ValidateHMAC(wrongKey, message, computedHMAC) {
t.Errorf("ValidateHMAC succeeded for incorrect key")
}
// Validate incorrect HMAC (tampered HMAC)
tamperedHMAC := append(computedHMAC[:len(computedHMAC)-1], ^computedHMAC[len(computedHMAC)-1])
if ValidateHMAC(key, message, tamperedHMAC) {
t.Errorf("ValidateHMAC succeeded for tampered HMAC")
}
// Validate empty message
emptyMessage := []byte("")
emptyHMAC := ComputeHMAC(key, emptyMessage)
if !ValidateHMAC(key, emptyMessage, emptyHMAC) {
t.Errorf("ValidateHMAC failed for empty message")
}
if ValidateHMAC(key, message, emptyHMAC) {
t.Errorf("ValidateHMAC succeeded comparing non-empty message with empty HMAC")
}
}

117
pkg/debug/debug.go Normal file
View File

@@ -0,0 +1,117 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package debug
import (
"context"
"flag"
"log/slog"
"os"
)
const (
DEBUG_CRITICAL = 1
DEBUG_ERROR = 2
DEBUG_INFO = 3
DEBUG_VERBOSE = 4
DEBUG_TRACE = 5
DEBUG_PACKETS = 6
DEBUG_ALL = 7
)
var (
debugLevel = flag.Int("debug", 3, "debug level (1-7)")
logger *slog.Logger
initialized bool
)
func Init() {
if initialized {
return
}
initialized = true
var level slog.Level
switch {
case *debugLevel >= DEBUG_ALL:
level = slog.LevelDebug
case *debugLevel >= DEBUG_PACKETS:
level = slog.LevelDebug
case *debugLevel >= DEBUG_TRACE:
level = slog.LevelDebug
case *debugLevel >= DEBUG_VERBOSE:
level = slog.LevelDebug
case *debugLevel >= DEBUG_INFO:
level = slog.LevelInfo
case *debugLevel >= DEBUG_ERROR:
level = slog.LevelWarn
case *debugLevel >= DEBUG_CRITICAL:
level = slog.LevelError
default:
level = slog.LevelError
}
opts := &slog.HandlerOptions{
Level: level,
}
logger = slog.New(slog.NewTextHandler(os.Stderr, opts))
slog.SetDefault(logger)
}
func GetLogger() *slog.Logger {
if !initialized {
Init()
}
return logger
}
func Log(level int, msg string, args ...interface{}) {
if !initialized {
Init()
}
if *debugLevel < level {
return
}
var slogLevel slog.Level
switch {
case level >= DEBUG_ALL:
slogLevel = slog.LevelDebug
case level >= DEBUG_PACKETS:
slogLevel = slog.LevelDebug
case level >= DEBUG_TRACE:
slogLevel = slog.LevelDebug
case level >= DEBUG_VERBOSE:
slogLevel = slog.LevelDebug
case level >= DEBUG_INFO:
slogLevel = slog.LevelInfo
case level >= DEBUG_ERROR:
slogLevel = slog.LevelWarn
case level >= DEBUG_CRITICAL:
slogLevel = slog.LevelError
default:
slogLevel = slog.LevelError
}
if !logger.Enabled(context.TODO(), slogLevel) {
return
}
allArgs := make([]interface{}, len(args)+2)
copy(allArgs, args)
allArgs[len(args)] = "debug_level"
allArgs[len(args)+1] = level
logger.Log(context.TODO(), slogLevel, msg, allArgs...)
}
func SetDebugLevel(level int) {
*debugLevel = level
if initialized {
Init()
}
}
func GetDebugLevel() int {
return *debugLevel
}

185
pkg/debug/debug_test.go Normal file
View File

@@ -0,0 +1,185 @@
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,21 +1,35 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package destination
import (
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"sync"
"time"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
"git.quad4.io/Networks/Reticulum-Go/pkg/announce"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
"github.com/vmihailenco/msgpack/v5"
"golang.org/x/crypto/curve25519"
)
const (
// Destination direction types
// The IN bit specifies that the destination can receive traffic.
// The OUT bit specifies that the destination can send traffic.
// A destination can be both IN and OUT.
IN = 0x01
OUT = 0x02
// Destination types
SINGLE = 0x00
GROUP = 0x01
PLAIN = 0x02
@@ -41,42 +55,60 @@ type RequestHandler struct {
ResponseGenerator func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity *identity.Identity, requestedAt int64) []byte
AllowMode 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 {
identity *identity.Identity
direction byte
destType byte
appName string
aspects []string
hash []byte
acceptsLinks bool
proofStrategy byte
packetCallback PacketCallback
proofCallback ProofRequestedCallback
linkCallback LinkEstablishedCallback
ratchetsEnabled bool
ratchetPath string
ratchetCount int
ratchetInterval int
enforceRatchets bool
defaultAppData []byte
identity *identity.Identity
direction byte
destType byte
appName string
aspects []string
hashValue []byte
transport Transport
acceptsLinks bool
proofStrategy byte
packetCallback PacketCallback
proofCallback ProofRequestedCallback
linkCallback LinkEstablishedCallback
ratchetsEnabled bool
ratchetPath string
ratchetCount int
ratchetInterval int
enforceRatchets bool
latestRatchetTime time.Time
latestRatchetID []byte
ratchets [][]byte
ratchetFileLock sync.Mutex
defaultAppData []byte
mutex sync.RWMutex
requestHandlers map[string]*RequestHandler
callbacks struct {
packetReceived common.PacketCallback
proofRequested common.ProofRequestedCallback
linkEstablished common.LinkEstablishedCallback
}
}
func New(id *identity.Identity, direction byte, destType byte, appName string, aspects ...string) (*Destination, error) {
func New(id *identity.Identity, direction byte, destType byte, appName string, transport Transport, aspects ...string) (*Destination, error) {
debug.Log(debug.DEBUG_INFO, "Creating new destination", "app", appName, "type", destType, "direction", direction)
if id == nil {
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil")
return nil, errors.New("identity cannot be nil")
}
@@ -86,6 +118,7 @@ func New(id *identity.Identity, direction byte, destType byte, appName string, a
destType: destType,
appName: appName,
aspects: aspects,
transport: transport,
acceptsLinks: false,
proofStrategy: PROVE_NONE,
ratchetCount: RATCHET_COUNT,
@@ -94,19 +127,69 @@ func New(id *identity.Identity, direction byte, destType byte, appName string, a
}
// Generate destination hash
d.hash = d.Hash()
d.hashValue = d.calculateHash()
debug.Log(debug.DEBUG_VERBOSE, "Created destination with hash", "hash", fmt.Sprintf("%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
}
func (d *Destination) Hash() []byte {
nameHash := sha256.Sum256([]byte(d.ExpandName()))
identityHash := sha256.Sum256(d.identity.GetPublicKey())
combined := append(nameHash[:], identityHash[:]...)
finalHash := sha256.Sum256(combined)
return finalHash[:16] // Truncated to 128 bits
// 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.
func FromHash(hash []byte, id *identity.Identity, destType byte, transport Transport) (*Destination, error) {
debug.Log(debug.DEBUG_INFO, "Creating destination from hash", "hash", fmt.Sprintf("%x", hash))
if id == nil {
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil")
return nil, errors.New("identity cannot be nil")
}
d := &Destination{
identity: id,
direction: OUT,
destType: destType,
hashValue: hash,
transport: transport,
acceptsLinks: false,
proofStrategy: PROVE_NONE,
ratchetCount: RATCHET_COUNT,
ratchetInterval: RATCHET_INTERVAL,
requestHandlers: make(map[string]*RequestHandler),
}
debug.Log(debug.DEBUG_VERBOSE, "Created destination from hash", "hash", fmt.Sprintf("%x", hash))
return d, nil
}
func (d *Destination) calculateHash() []byte {
debug.Log(debug.DEBUG_TRACE, "Calculating hash for destination", "name", d.ExpandName())
// destination_hash = SHA256(name_hash_10bytes + identity_hash_16bytes)[:16]
// Identity hash is the truncated hash of the public key (16 bytes)
identityHash := identity.TruncatedHash(d.identity.GetPublicKey())
// Name hash is the FULL 32-byte SHA256, then we take first 10 bytes for concatenation
nameHashFull := sha256.Sum256([]byte(d.ExpandName()))
nameHash10 := nameHashFull[:10] // Only use 10 bytes
debug.Log(debug.DEBUG_ALL, "Identity hash", "hash", fmt.Sprintf("%x", identityHash))
debug.Log(debug.DEBUG_ALL, "Name hash (10 bytes)", "hash", fmt.Sprintf("%x", nameHash10))
// Concatenate name_hash (10 bytes) + identity_hash (16 bytes) = 26 bytes
combined := append(nameHash10, identityHash...)
// Then hash again and truncate to 16 bytes
finalHashFull := sha256.Sum256(combined)
finalHash := finalHashFull[:16]
debug.Log(debug.DEBUG_VERBOSE, "Calculated destination hash", "hash", fmt.Sprintf("%x", finalHash))
return finalHash
}
func (d *Destination) ExpandName() string {
@@ -117,80 +200,68 @@ func (d *Destination) ExpandName() string {
return name
}
func (d *Destination) Announce(appData []byte) error {
func (d *Destination) Announce(pathResponse bool, tag []byte, attachedInterface common.NetworkInterface) error {
d.mutex.Lock()
defer d.mutex.Unlock()
// If no specific appData provided, use default
if appData == nil {
appData = d.defaultAppData
}
debug.Log(debug.DEBUG_VERBOSE, "Announcing destination", "name", d.ExpandName(), "path_response", pathResponse)
// Create announce packet
packet := make([]byte, 0)
appData := d.defaultAppData
// Add destination hash
packet = append(packet, d.hash...)
// Add identity public key
packet = append(packet, d.identity.GetPublicKey()...)
// Add flags byte
flags := byte(0)
if d.acceptsLinks {
flags |= 0x01
}
if d.ratchetsEnabled {
flags |= 0x02
}
packet = append(packet, flags)
// Add proof strategy
packet = append(packet, d.proofStrategy)
// Add app data length and data if present
if appData != nil {
appDataLen := uint16(len(appData))
lenBytes := make([]byte, 2)
binary.BigEndian.PutUint16(lenBytes, appDataLen)
packet = append(packet, lenBytes...)
packet = append(packet, appData...)
} else {
// No app data
packet = append(packet, 0x00, 0x00)
}
// Add ratchet data if enabled
if d.ratchetsEnabled {
// Add ratchet interval
intervalBytes := make([]byte, 4)
binary.BigEndian.PutUint32(intervalBytes, uint32(d.ratchetInterval))
packet = append(packet, intervalBytes...)
// Add current ratchet key
ratchetKey := d.identity.GetCurrentRatchetKey()
if ratchetKey == nil {
return errors.New("failed to get current ratchet key")
}
packet = append(packet, ratchetKey...)
}
// Sign the announce packet
signature, err := d.Sign(packet)
// Create announce packet using announce package
announceObj, err := announce.New(d.identity, d.hashValue, d.ExpandName(), appData, pathResponse, d.transport.GetConfig())
if err != nil {
return fmt.Errorf("failed to sign announce packet: %w", err)
return fmt.Errorf("failed to create announce: %w", err)
}
packet = append(packet, signature...)
// Send announce packet through transport layer
// This will need to be implemented in the transport package
return transport.SendAnnounce(packet)
packet := announceObj.GetPacket()
if packet == nil {
return errors.New("failed to create announce packet")
}
if pathResponse && tag != nil {
debug.Log(debug.DEBUG_INFO, "Sending path response announce", "tag", fmt.Sprintf("%x", tag))
}
if d.transport == nil {
return errors.New("transport not initialized")
}
var lastErr error
if attachedInterface != nil {
if attachedInterface.IsEnabled() && attachedInterface.IsOnline() {
debug.Log(debug.DEBUG_VERBOSE, "Sending announce to attached interface", "name", attachedInterface.GetName())
if err := attachedInterface.Send(packet, ""); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to send announce on attached interface", "error", err)
lastErr = err
}
}
} else {
interfaces := d.transport.GetInterfaces()
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
}
}
}
}
return lastErr
}
func (d *Destination) AcceptsLinks(accepts bool) {
d.mutex.Lock()
defer d.mutex.Unlock()
d.acceptsLinks = accepts
// Register with transport if accepting links
if accepts && d.transport != nil {
d.transport.RegisterDestination(d.hashValue, d)
debug.Log(debug.DEBUG_VERBOSE, "Destination registered with transport for link requests", "hash", fmt.Sprintf("%x", d.hashValue))
}
}
func (d *Destination) SetLinkEstablishedCallback(callback common.LinkEstablishedCallback) {
@@ -199,12 +270,72 @@ func (d *Destination) SetLinkEstablishedCallback(callback common.LinkEstablished
d.linkCallback = callback
}
func (d *Destination) GetLinkCallback() common.LinkEstablishedCallback {
d.mutex.RLock()
defer d.mutex.RUnlock()
return d.linkCallback
}
func (d *Destination) HandleIncomingLinkRequest(pkt interface{}, transport interface{}, networkIface common.NetworkInterface) error {
debug.Log(debug.DEBUG_INFO, "Handling incoming link request for destination", "hash", fmt.Sprintf("%x", d.GetHash()))
pktObj, ok := pkt.(*packet.Packet)
if !ok {
return errors.New("invalid packet type")
}
if incomingLinkHandler == nil {
return errors.New("no incoming link handler registered")
}
linkIface, err := incomingLinkHandler(pktObj, d, transport, networkIface)
if err != nil {
return fmt.Errorf("failed to handle link request: %w", err)
}
if d.linkCallback != nil && linkIface != nil {
debug.Log(debug.DEBUG_INFO, "Calling link established callback")
d.linkCallback(linkIface)
}
return nil
}
func (d *Destination) SetPacketCallback(callback common.PacketCallback) {
d.mutex.Lock()
defer d.mutex.Unlock()
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) {
d.mutex.Lock()
defer d.mutex.Unlock()
@@ -220,9 +351,28 @@ func (d *Destination) SetProofStrategy(strategy byte) {
func (d *Destination) EnableRatchets(path string) bool {
d.mutex.Lock()
defer d.mutex.Unlock()
if path == "" {
debug.Log(debug.DEBUG_ERROR, "No ratchet file path specified")
return false
}
d.ratchetsEnabled = true
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
}
@@ -236,7 +386,7 @@ func (d *Destination) SetRetainedRatchets(count int) bool {
if count < 1 {
return false
}
d.mutex.Lock()
defer d.mutex.Unlock()
d.ratchetCount = count
@@ -247,7 +397,7 @@ func (d *Destination) SetRatchetInterval(interval int) bool {
if interval < 1 {
return false
}
d.mutex.Lock()
defer d.mutex.Unlock()
d.ratchetInterval = interval
@@ -275,7 +425,7 @@ func (d *Destination) RegisterRequestHandler(path string, responseGen func(strin
return errors.New("invalid allow mode")
}
if allow == ALLOW_LIST && (allowedList == nil || len(allowedList) == 0) {
if allow == ALLOW_LIST && len(allowedList) == 0 {
return errors.New("allowed list required for ALLOW_LIST mode")
}
@@ -303,21 +453,88 @@ func (d *Destination) DeregisterRequestHandler(path string) bool {
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) {
if d.destType == PLAIN {
debug.Log(debug.DEBUG_VERBOSE, "Using plaintext transmission for PLAIN destination")
return plaintext, nil
}
if d.identity == nil {
debug.Log(debug.DEBUG_INFO, "Cannot encrypt: no identity available")
return nil, errors.New("no identity available for encryption")
}
debug.Log(debug.DEBUG_VERBOSE, "Encrypting bytes for destination", "bytes", len(plaintext), "destType", d.destType)
switch d.destType {
case SINGLE:
return d.identity.Encrypt(plaintext, nil)
recipientKey := d.identity.GetEncryptionKey()
debug.Log(debug.DEBUG_VERBOSE, "Encrypting for single recipient", "key", fmt.Sprintf("%x", recipientKey[:8]))
return d.identity.Encrypt(plaintext, recipientKey)
case GROUP:
return d.identity.EncryptSymmetric(plaintext)
key := d.identity.GetCurrentRatchetKey()
if key == nil {
debug.Log(debug.DEBUG_INFO, "Cannot encrypt: 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]))
return d.identity.EncryptWithHMAC(plaintext, key)
default:
debug.Log(debug.DEBUG_INFO, "Unsupported destination type for encryption", "destType", d.destType)
return nil, errors.New("unsupported destination type for encryption")
}
}
@@ -331,14 +548,15 @@ func (d *Destination) Decrypt(ciphertext []byte) ([]byte, error) {
return nil, errors.New("no identity available for decryption")
}
switch d.destType {
case SINGLE:
return d.identity.Decrypt(ciphertext, nil)
case GROUP:
return d.identity.DecryptSymmetric(ciphertext)
default:
return nil, errors.New("unsupported destination type for decryption")
}
// Create empty ratchet receiver to get latest ratchet ID if available
ratchetReceiver := &common.RatchetIDReceiver{}
// Call Decrypt with full parameter list:
// - ciphertext: the encrypted data
// - ratchets: nil since we're not providing specific ratchets
// - enforceRatchets: false to allow fallback to normal decryption
// - ratchetIDReceiver: to receive the latest ratchet ID used
return d.identity.Decrypt(ciphertext, nil, false, ratchetReceiver)
}
func (d *Destination) Sign(data []byte) ([]byte, error) {
@@ -347,4 +565,216 @@ func (d *Destination) Sign(data []byte) ([]byte, error) {
}
signature := d.identity.Sign(data)
return signature, nil
}
}
func (d *Destination) GetPublicKey() []byte {
if d.identity == nil {
return nil
}
return d.identity.GetPublicKey()
}
func (d *Destination) GetIdentity() *identity.Identity {
return d.identity
}
func (d *Destination) GetType() byte {
return d.destType
}
func (d *Destination) GetHash() []byte {
d.mutex.RLock()
defer d.mutex.RUnlock()
if d.hashValue == nil {
d.mutex.RUnlock()
d.mutex.Lock()
defer d.mutex.Unlock()
if d.hashValue == nil {
d.hashValue = d.calculateHash()
}
}
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

@@ -0,0 +1,152 @@
package destination
import (
"bytes"
"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")
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,148 @@
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))
}
}

605
pkg/interfaces/auto.go Normal file
View File

@@ -0,0 +1,605 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package interfaces
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"net"
"sync"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
)
const (
HW_MTU = 1196
DEFAULT_DISCOVERY_PORT = 29716
DEFAULT_DATA_PORT = 42671
DEFAULT_GROUP_ID = "reticulum"
BITRATE_GUESS = 10 * 1000 * 1000
PEERING_TIMEOUT = 22 * time.Second
ANNOUNCE_INTERVAL = 1600 * time.Millisecond
PEER_JOB_INTERVAL = 4 * time.Second
MCAST_ECHO_TIMEOUT = 6500 * time.Millisecond
SCOPE_LINK = "2"
SCOPE_ADMIN = "4"
SCOPE_SITE = "5"
SCOPE_ORGANISATION = "8"
SCOPE_GLOBAL = "e"
MCAST_ADDR_TYPE_PERMANENT = "0"
MCAST_ADDR_TYPE_TEMPORARY = "1"
)
type AutoInterface struct {
BaseInterface
groupID []byte
groupHash []byte
discoveryPort int
dataPort int
discoveryScope string
multicastAddrType string
mcastDiscoveryAddr string
ifacNetname string
peers map[string]*Peer
linkLocalAddrs []string
adoptedInterfaces map[string]*AdoptedInterface
interfaceServers map[string]*net.UDPConn
discoveryServers map[string]*net.UDPConn
multicastEchoes map[string]time.Time
timedOutInterfaces map[string]time.Time
allowedInterfaces []string
ignoredInterfaces []string
outboundConn *net.UDPConn
announceInterval time.Duration
peerJobInterval time.Duration
peeringTimeout time.Duration
mcastEchoTimeout time.Duration
done chan struct{}
stopOnce sync.Once
}
type AdoptedInterface struct {
name string
linkLocalAddr string
index int
}
type Peer struct {
ifaceName string
lastHeard time.Time
addr *net.UDPAddr
}
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
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))
ifacNetname := hex.EncodeToString(groupHash[:])[:16]
mcastAddr := fmt.Sprintf("ff%s%s::%s", discoveryScope, multicastAddrType, ifacNetname)
ai := &AutoInterface{
BaseInterface: BaseInterface{
Name: name,
Mode: common.IF_MODE_FULL,
Type: common.IF_TYPE_AUTO,
Online: false,
Enabled: config.Enabled,
Detached: false,
IN: true,
OUT: false,
MTU: HW_MTU,
Bitrate: BITRATE_GUESS,
},
groupID: []byte(groupID),
groupHash: groupHash[:],
discoveryPort: discoveryPort,
dataPort: dataPort,
discoveryScope: discoveryScope,
multicastAddrType: multicastAddrType,
mcastDiscoveryAddr: mcastAddr,
ifacNetname: ifacNetname,
peers: make(map[string]*Peer),
linkLocalAddrs: make([]string, 0),
adoptedInterfaces: make(map[string]*AdoptedInterface),
interfaceServers: make(map[string]*net.UDPConn),
discoveryServers: make(map[string]*net.UDPConn),
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,
done: make(chan struct{}),
}
debug.Log(debug.DEBUG_INFO, "AutoInterface configured", "name", name, "group", groupID, "mcast_addr", mcastAddr)
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 {
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()
if err != nil {
return fmt.Errorf("failed to list interfaces: %v", err)
}
for _, iface := range interfaces {
if ai.shouldIgnoreInterface(iface.Name) {
debug.Log(debug.DEBUG_TRACE, "Ignoring interface", "name", iface.Name)
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
}
}
if len(ai.adoptedInterfaces) == 0 {
return fmt.Errorf("no suitable interfaces found")
}
ai.Online = true
ai.IN = true
ai.OUT = true
go ai.peerJobs()
go ai.announceLoop()
debug.Log(debug.DEBUG_INFO, "AutoInterface started", "adopted", len(ai.adoptedInterfaces))
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 {
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()
if err != nil {
return err
}
var linkLocalAddr string
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok {
if ipnet.IP.To4() == nil && ipnet.IP.IsLinkLocalUnicast() {
linkLocalAddr = 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.Mutex.Unlock()
if err := ai.startDiscoveryListener(iface); err != nil {
return fmt.Errorf("failed to start discovery listener: %v", err)
}
if err := ai.startDataListener(iface); err != nil {
return fmt.Errorf("failed to start data listener: %v", err)
}
debug.Log(debug.DEBUG_INFO, "Configured interface", "name", iface.Name, "addr", linkLocalAddr)
return nil
}
func (ai *AutoInterface) startDiscoveryListener(iface *net.Interface) error {
addr := &net.UDPAddr{
IP: net.ParseIP(ai.mcastDiscoveryAddr),
Port: ai.discoveryPort,
Zone: iface.Name,
}
conn, err := net.ListenMulticastUDP("udp6", iface, addr)
if err != nil {
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)
debug.Log(debug.DEBUG_VERBOSE, "Discovery listener started", "interface", iface.Name, "addr", ai.mcastDiscoveryAddr)
return nil
}
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{
IP: net.ParseIP(adoptedIface.linkLocalAddr),
Port: ai.dataPort,
Zone: iface.Name,
}
conn, err := net.ListenUDP("udp6", addr)
if err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to listen on data port", "addr", addr, "error", 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.Mutex.Unlock()
go ai.handleData(conn, iface.Name)
debug.Log(debug.DEBUG_VERBOSE, "Data listener started", "interface", iface.Name, "addr", addr)
return nil
}
func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) {
buf := make([]byte, common.NUM_1024)
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, "Discovery read error", "interface", ifaceName, "error", err)
}
return
}
if n >= len(ai.groupHash) {
receivedHash := buf[:len(ai.groupHash)]
if bytes.Equal(receivedHash, ai.groupHash) {
ai.handlePeerAnnounce(remoteAddr, ifaceName)
} else {
debug.Log(debug.DEBUG_TRACE, "Received discovery with mismatched group hash", "interface", ifaceName)
}
}
}
}
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, _, err := conn.ReadFromUDP(buf)
if err != nil {
if ai.IsOnline() {
debug.Log(debug.DEBUG_ERROR, "Data read error", "interface", ifaceName, "error", err)
}
return
}
if callback := ai.GetPacketCallback(); callback != nil {
callback(buf[:n], ai)
}
}
}
func (ai *AutoInterface) handlePeerAnnounce(addr *net.UDPAddr, ifaceName string) {
ai.Mutex.Lock()
defer ai.Mutex.Unlock()
peerIP := addr.IP.String()
for _, localAddr := range ai.linkLocalAddrs {
if peerIP == localAddr {
ai.multicastEchoes[ifaceName] = time.Now()
debug.Log(debug.DEBUG_TRACE, "Received own multicast echo", "interface", ifaceName)
return
}
}
peerKey := peerIP + "%" + ifaceName
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,
lastHeard: time.Now(),
addr: addr,
}
debug.Log(debug.DEBUG_INFO, "Discovered new peer", "peer", peerIP, "interface", 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
}
}
if _, err := ai.outboundConn.WriteToUDP(ai.groupHash, mcastAddr); err != nil {
debug.Log(debug.DEBUG_VERBOSE, "Failed to send peer announce", "interface", ifaceName, "error", err)
} else {
debug.Log(debug.DEBUG_TRACE, "Sent peer announce", "interface", adoptedIface.name)
}
}
}
func (ai *AutoInterface) peerJobs() {
ticker := time.NewTicker(ai.peerJobInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if !ai.IsOnline() {
return
}
ai.Mutex.Lock()
now := time.Now()
for peerKey, peer := range ai.peers {
if now.Sub(peer.lastHeard) > ai.peeringTimeout {
delete(ai.peers, peerKey)
debug.Log(debug.DEBUG_VERBOSE, "Removed timed out peer", "peer", peerKey)
}
}
for ifaceName, echoTime := range ai.multicastEchoes {
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 {
if !ai.IsOnline() {
return fmt.Errorf("interface offline")
}
ai.Mutex.RLock()
defer ai.Mutex.RUnlock()
if len(ai.peers) == 0 {
debug.Log(debug.DEBUG_TRACE, "No peers available for sending")
return nil
}
if ai.outboundConn == nil {
var err error
ai.outboundConn, err = net.ListenUDP("udp6", &net.UDPAddr{Port: 0})
if err != nil {
return fmt.Errorf("failed to create outbound socket: %v", err)
}
}
sentCount := 0
for _, peer := range ai.peers {
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
}
sentCount++
}
if sentCount > 0 {
debug.Log(debug.DEBUG_TRACE, "Sent data to peers", "count", sentCount, "bytes", len(data))
}
return nil
}
func (ai *AutoInterface) Stop() error {
ai.Mutex.Lock()
ai.Online = false
ai.IN = false
ai.OUT = false
for _, server := range ai.interfaceServers {
server.Close() // #nosec G104
}
for _, server := range ai.discoveryServers {
server.Close() // #nosec G104
}
if ai.outboundConn != nil {
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
}

295
pkg/interfaces/auto_test.go Normal file
View File

@@ -0,0 +1,295 @@
package interfaces
import (
"net"
"testing"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
)
func TestNewAutoInterface(t *testing.T) {
t.Run("DefaultConfig", func(t *testing.T) {
config := &common.InterfaceConfig{Enabled: true}
ai, err := NewAutoInterface("autoDefault", config)
if err != nil {
t.Fatalf("NewAutoInterface failed with default config: %v", err)
}
if ai == nil {
t.Fatal("NewAutoInterface returned nil with default config")
}
if ai.GetName() != "autoDefault" {
t.Errorf("GetName() = %s; want autoDefault", ai.GetName())
}
if ai.GetType() != common.IF_TYPE_AUTO {
t.Errorf("GetType() = %v; want %v", ai.GetType(), common.IF_TYPE_AUTO)
}
if ai.discoveryPort != DEFAULT_DISCOVERY_PORT {
t.Errorf("discoveryPort = %d; want %d", ai.discoveryPort, DEFAULT_DISCOVERY_PORT)
}
if ai.dataPort != DEFAULT_DATA_PORT {
t.Errorf("dataPort = %d; want %d", ai.dataPort, DEFAULT_DATA_PORT)
}
if string(ai.groupID) != "reticulum" {
t.Errorf("groupID = %s; want reticulum", string(ai.groupID))
}
if ai.discoveryScope != SCOPE_LINK {
t.Errorf("discoveryScope = %s; want %s", ai.discoveryScope, SCOPE_LINK)
}
if len(ai.peers) != 0 {
t.Errorf("peers map not empty initially")
}
})
t.Run("CustomConfig", func(t *testing.T) {
config := &common.InterfaceConfig{
Enabled: true,
DiscoveryPort: 12345,
DataPort: 54321,
GroupID: "customGroup",
}
ai, err := NewAutoInterface("autoCustom", config)
if err != nil {
t.Fatalf("NewAutoInterface failed with custom config: %v", err)
}
if ai == nil {
t.Fatal("NewAutoInterface returned nil with custom config")
}
if ai.discoveryPort != 12345 {
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" {
t.Errorf("groupID = %s; want customGroup", string(ai.groupID))
}
})
}
// mockAutoInterface embeds AutoInterface but overrides methods that start goroutines
type mockAutoInterface struct {
*AutoInterface
}
func newMockAutoInterface(name string, config *common.InterfaceConfig) (*mockAutoInterface, error) {
ai, err := NewAutoInterface(name, config)
if err != nil {
return nil, err
}
// Initialize maps that would normally be initialized in Start()
ai.peers = make(map[string]*Peer)
ai.linkLocalAddrs = make([]string, 0)
ai.adoptedInterfaces = make(map[string]*AdoptedInterface)
ai.interfaceServers = make(map[string]*net.UDPConn)
ai.discoveryServers = make(map[string]*net.UDPConn)
ai.multicastEchoes = make(map[string]time.Time)
ai.timedOutInterfaces = make(map[string]time.Time)
return &mockAutoInterface{AutoInterface: ai}, nil
}
func (m *mockAutoInterface) Start() error {
// Don't start any goroutines
return nil
}
func (m *mockAutoInterface) Stop() error {
// Don't try to close connections that were never opened
return nil
}
// mockHandlePeerAnnounce is a test-only method that doesn't handle its own locking
func (m *mockAutoInterface) mockHandlePeerAnnounce(addr *net.UDPAddr, ifaceName string) {
peerAddr := addr.IP.String() + "%" + addr.Zone
for _, localAddr := range m.linkLocalAddrs {
if peerAddr == localAddr {
m.multicastEchoes[ifaceName] = time.Now()
return
}
}
if _, exists := m.peers[peerAddr]; !exists {
m.peers[peerAddr] = &Peer{
ifaceName: ifaceName,
lastHeard: time.Now(),
}
} else {
m.peers[peerAddr].lastHeard = time.Now()
}
}
func TestAutoInterfacePeerManagement(t *testing.T) {
// Use a shorter timeout for testing
testTimeout := 100 * time.Millisecond
config := &common.InterfaceConfig{Enabled: true}
ai, err := newMockAutoInterface("autoPeerTest", config)
if err != nil {
t.Fatalf("Failed to create mock interface: %v", err)
}
// Create a done channel to signal goroutine cleanup
done := make(chan struct{})
// Start peer management with done channel
go func() {
ticker := time.NewTicker(testTimeout)
defer ticker.Stop()
for {
select {
case <-ticker.C:
ai.Mutex.Lock()
now := time.Now()
for addr, peer := range ai.peers {
if now.Sub(peer.lastHeard) > testTimeout {
delete(ai.peers, addr)
}
}
ai.Mutex.Unlock()
case <-done:
return
}
}
}()
// Ensure cleanup
defer func() {
close(done)
ai.Stop()
}()
// Simulate receiving peer announces
peer1AddrStr := "fe80::1%eth0"
peer2AddrStr := "fe80::2%eth0"
localAddrStr := "fe80::aaaa%eth0" // Simulate a local address
peer1Addr := &net.UDPAddr{IP: net.ParseIP("fe80::1"), Zone: "eth0"}
peer2Addr := &net.UDPAddr{IP: net.ParseIP("fe80::2"), Zone: "eth0"}
localAddr := &net.UDPAddr{IP: net.ParseIP("fe80::aaaa"), Zone: "eth0"}
ai.Mutex.Lock()
ai.linkLocalAddrs = append(ai.linkLocalAddrs, localAddrStr)
ai.Mutex.Unlock()
t.Run("AddPeer1", func(t *testing.T) {
ai.Mutex.Lock()
ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
ai.Mutex.Unlock()
// Give a small amount of time for the peer to be processed
time.Sleep(10 * time.Millisecond)
ai.Mutex.RLock()
count := len(ai.peers)
peer, exists := ai.peers[peer1AddrStr]
var ifaceName string
if exists {
ifaceName = peer.ifaceName
}
ai.Mutex.RUnlock()
if count != 1 {
t.Fatalf("Expected 1 peer, got %d", count)
}
if !exists {
t.Fatalf("Peer %s not found in map", peer1AddrStr)
}
if ifaceName != "eth0" {
t.Errorf("Peer %s interface name = %s; want eth0", peer1AddrStr, ifaceName)
}
})
t.Run("AddPeer2", func(t *testing.T) {
ai.Mutex.Lock()
ai.mockHandlePeerAnnounce(peer2Addr, "eth0")
ai.Mutex.Unlock()
// Give a small amount of time for the peer to be processed
time.Sleep(10 * time.Millisecond)
ai.Mutex.RLock()
count := len(ai.peers)
_, exists := ai.peers[peer2AddrStr]
ai.Mutex.RUnlock()
if count != 2 {
t.Fatalf("Expected 2 peers, got %d", count)
}
if !exists {
t.Fatalf("Peer %s not found in map", peer2AddrStr)
}
})
t.Run("IgnoreLocalAnnounce", func(t *testing.T) {
ai.Mutex.Lock()
ai.mockHandlePeerAnnounce(localAddr, "eth0")
ai.Mutex.Unlock()
// Give a small amount of time for the peer to be processed
time.Sleep(10 * time.Millisecond)
ai.Mutex.RLock()
count := len(ai.peers)
ai.Mutex.RUnlock()
if count != 2 {
t.Fatalf("Expected 2 peers after local announce, got %d", count)
}
})
t.Run("UpdatePeerTimestamp", func(t *testing.T) {
ai.Mutex.RLock()
peer, exists := ai.peers[peer1AddrStr]
var initialTime time.Time
if exists {
initialTime = peer.lastHeard
}
ai.Mutex.RUnlock()
if !exists {
t.Fatalf("Peer %s not found before timestamp update", peer1AddrStr)
}
ai.Mutex.Lock()
ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
ai.Mutex.Unlock()
// Give a small amount of time for the peer to be processed
time.Sleep(10 * time.Millisecond)
ai.Mutex.RLock()
peer, exists = ai.peers[peer1AddrStr]
var updatedTime time.Time
if exists {
updatedTime = peer.lastHeard
}
ai.Mutex.RUnlock()
if !exists {
t.Fatalf("Peer %s not found after timestamp update", peer1AddrStr)
}
if !updatedTime.After(initialTime) {
t.Errorf("Peer timestamp was not updated after receiving another announce")
}
})
t.Run("PeerTimeout", func(t *testing.T) {
// Wait for peer timeout
time.Sleep(testTimeout * 2)
ai.Mutex.RLock()
count := len(ai.peers)
ai.Mutex.RUnlock()
if count != 0 {
t.Errorf("Expected all peers to timeout, got %d peers", count)
}
})
}

View File

@@ -1,51 +1,135 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package interfaces
import (
"encoding/binary"
"fmt"
"net"
"sync"
"time"
"encoding/binary"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
)
const (
BITRATE_MINIMUM = 5 // Minimum required bitrate in bits/sec
BITRATE_MINIMUM = 1200 // Minimum bitrate in bits/second
MODE_FULL = 0x01
// Interface modes
MODE_GATEWAY = 0x02
MODE_ACCESS_POINT = 0x03
MODE_ROAMING = 0x04
MODE_BOUNDARY = 0x05
// Interface types
TYPE_UDP = 0x01
TYPE_TCP = 0x02
PROPAGATION_RATE = 0.02 // 2% of interface bandwidth
)
// BaseInterface embeds common.BaseInterface and implements common.Interface
type Interface interface {
GetName() string
GetType() common.InterfaceType
GetMode() common.InterfaceMode
IsOnline() bool
IsDetached() bool
IsEnabled() bool
Detach()
Enable()
Disable()
Send(data []byte, addr string) error
SetPacketCallback(common.PacketCallback)
GetPacketCallback() common.PacketCallback
ProcessIncoming([]byte)
ProcessOutgoing([]byte) error
SendPathRequest([]byte) error
SendLinkPacket([]byte, []byte, time.Time) error
Start() error
Stop() error
GetMTU() int
GetConn() net.Conn
GetBandwidthAvailable() bool
common.NetworkInterface
}
type BaseInterface struct {
common.BaseInterface
Name string
Mode common.InterfaceMode
Type common.InterfaceType
Online bool
Enabled bool
Detached bool
IN bool
OUT bool
MTU int
Bitrate int64
TxBytes uint64
RxBytes uint64
lastTx time.Time
lastRx time.Time
Mutex sync.RWMutex
packetCallback common.PacketCallback
}
func NewBaseInterface(name string, ifType common.InterfaceType, enabled bool) BaseInterface {
return BaseInterface{
Name: name,
Mode: common.IF_MODE_FULL,
Type: ifType,
Online: false,
Enabled: enabled,
Detached: false,
IN: false,
OUT: false,
MTU: common.DEFAULT_MTU,
Bitrate: BITRATE_MINIMUM,
lastTx: time.Now(),
lastRx: time.Now(),
}
}
func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {
i.mutex.Lock()
defer i.mutex.Unlock()
i.Mutex.Lock()
defer i.Mutex.Unlock()
i.packetCallback = callback
}
func (i *BaseInterface) GetPacketCallback() common.PacketCallback {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.packetCallback
}
func (i *BaseInterface) ProcessIncoming(data []byte) {
i.mutex.RLock()
i.Mutex.Lock()
i.RxBytes += uint64(len(data))
i.Mutex.Unlock()
i.Mutex.RLock()
callback := i.packetCallback
i.mutex.RUnlock()
i.Mutex.RUnlock()
if callback != nil {
callback(data, i)
}
i.RxBytes += uint64(len(data))
}
func (i *BaseInterface) ProcessOutgoing(data []byte) error {
i.TxBytes += uint64(len(data))
return nil
}
if !i.Online || i.Detached {
debug.Log(debug.DEBUG_CRITICAL, "Interface cannot process outgoing packet - interface offline or detached", "name", i.Name)
return fmt.Errorf("interface offline or detached")
}
func (i *BaseInterface) Detach() {
i.mutex.Lock()
defer i.mutex.Unlock()
i.Detached = true
i.Online = false
i.Mutex.Lock()
i.TxBytes += uint64(len(data))
i.Mutex.Unlock()
debug.Log(debug.DEBUG_VERBOSE, "Interface processed outgoing packet", "name", i.Name, "bytes", len(data), "total_tx", i.TxBytes)
return nil
}
func (i *BaseInterface) SendPathRequest(packet []byte) error {
@@ -53,8 +137,8 @@ func (i *BaseInterface) SendPathRequest(packet []byte) error {
return fmt.Errorf("interface offline or detached")
}
frame := make([]byte, 0, len(packet)+2)
frame = append(frame, 0x01)
frame := make([]byte, 0, len(packet)+1)
frame = append(frame, common.HEX_0x01)
frame = append(frame, packet...)
return i.ProcessOutgoing(frame)
@@ -66,14 +150,158 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
}
frame := make([]byte, 0, len(dest)+len(data)+9)
frame = append(frame, 0x02)
frame = append(frame, common.HEX_0x02)
frame = append(frame, dest...)
ts := make([]byte, 8)
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix()))
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) // #nosec G115
frame = append(frame, ts...)
frame = append(frame, data...)
return i.ProcessOutgoing(frame)
}
}
func (i *BaseInterface) Detach() {
i.Mutex.Lock()
defer i.Mutex.Unlock()
i.Detached = true
i.Online = false
}
func (i *BaseInterface) IsEnabled() bool {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.Enabled && i.Online && !i.Detached
}
func (i *BaseInterface) Enable() {
i.Mutex.Lock()
defer i.Mutex.Unlock()
prevState := i.Enabled
i.Enabled = 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)
}
func (i *BaseInterface) Disable() {
i.Mutex.Lock()
defer i.Mutex.Unlock()
i.Enabled = false
i.Online = false
debug.Log(debug.DEBUG_ERROR, "Interface disabled and offline", "name", i.Name)
}
func (i *BaseInterface) GetName() string {
return i.Name
}
func (i *BaseInterface) GetType() common.InterfaceType {
return i.Type
}
func (i *BaseInterface) GetMode() common.InterfaceMode {
return i.Mode
}
func (i *BaseInterface) GetMTU() int {
return i.MTU
}
func (i *BaseInterface) IsOnline() bool {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.Online
}
func (i *BaseInterface) IsDetached() bool {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.Detached
}
func (i *BaseInterface) Start() error {
return nil
}
func (i *BaseInterface) Stop() error {
return nil
}
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)
err := i.ProcessOutgoing(data)
if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Interface failed to send data", "name", i.Name, "error", err)
return err
}
i.updateBandwidthStats(uint64(len(data)))
return nil
}
func (i *BaseInterface) GetConn() net.Conn {
return nil
}
func (i *BaseInterface) GetBandwidthAvailable() bool {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
now := time.Now()
timeSinceLastTx := now.Sub(i.lastTx)
if timeSinceLastTx > time.Second {
debug.Log(debug.DEBUG_VERBOSE, "Interface bandwidth available", "name", i.Name, "idle_seconds", timeSinceLastTx.Seconds())
return true
}
bytesPerSec := float64(i.TxBytes) / timeSinceLastTx.Seconds()
currentUsage := bytesPerSec * 8
maxUsage := float64(i.Bitrate) * PROPAGATION_RATE
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)
return available
}
func (i *BaseInterface) updateBandwidthStats(bytes uint64) {
i.Mutex.Lock()
defer i.Mutex.Unlock()
i.TxBytes += bytes
i.lastTx = time.Now()
debug.Log(debug.DEBUG_VERBOSE, "Interface updated bandwidth stats", "name", i.Name, "tx_bytes", i.TxBytes, "last_tx", i.lastTx)
}
type InterceptedInterface struct {
Interface
interceptor func([]byte, common.NetworkInterface) error
originalSend func([]byte, string) error
}
// Create constructor for intercepted interface
func NewInterceptedInterface(base Interface, interceptor func([]byte, common.NetworkInterface) error) *InterceptedInterface {
return &InterceptedInterface{
Interface: base,
interceptor: interceptor,
originalSend: base.Send,
}
}
// Implement Send method for intercepted interface
func (i *InterceptedInterface) Send(data []byte, addr string) error {
// Call interceptor if provided
if i.interceptor != nil && len(data) > 0 {
if err := i.interceptor(data, i); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to intercept outgoing packet", "error", err)
}
}
// Call original send
return i.originalSend(data, addr)
}

View File

@@ -0,0 +1,229 @@
package interfaces
import (
"bytes"
"net"
"sync"
"testing"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
)
func TestBaseInterfaceStateChanges(t *testing.T) {
bi := NewBaseInterface("test", common.IF_TYPE_TCP, false) // Start disabled
if bi.IsEnabled() {
t.Error("Newly created disabled interface reports IsEnabled() == true")
}
if bi.IsOnline() {
t.Error("Newly created disabled interface reports IsOnline() == true")
}
if bi.IsDetached() {
t.Error("Newly created interface reports IsDetached() == true")
}
bi.Enable()
if !bi.IsEnabled() {
t.Error("After Enable(), IsEnabled() == false")
}
if !bi.IsOnline() {
t.Error("After Enable(), IsOnline() == false")
}
if bi.IsDetached() {
t.Error("After Enable(), IsDetached() == true")
}
bi.Detach()
if bi.IsEnabled() {
t.Error("After Detach(), IsEnabled() == true")
}
if bi.IsOnline() {
t.Error("After Detach(), IsOnline() == true")
}
if !bi.IsDetached() {
t.Error("After Detach(), IsDetached() == false")
}
// Reset for Disable test
bi = NewBaseInterface("test2", common.IF_TYPE_UDP, true) // Start enabled
if !bi.Enabled { // Check the Enabled field directly first
t.Error("Newly created enabled interface reports Enabled == false")
}
if bi.IsEnabled() { // IsEnabled should still be false because Online is false
t.Error("Newly created enabled interface reports IsEnabled() == true before Enable() is called")
}
bi.Enable() // Explicitly enable to set Online = true
if !bi.IsEnabled() { // Now IsEnabled should be true
t.Error("After Enable() on initially enabled interface, IsEnabled() == false")
}
bi.Disable()
if bi.Enabled { // Check Enabled field after Disable()
t.Error("After Disable(), Enabled == true")
}
if bi.IsOnline() {
t.Error("After Disable(), IsOnline() == true")
}
if bi.IsDetached() { // Disable doesn't detach
t.Error("After Disable(), IsDetached() == true")
}
}
func TestBaseInterfaceGetters(t *testing.T) {
bi := NewBaseInterface("getterTest", common.IF_TYPE_AUTO, true)
if bi.GetName() != "getterTest" {
t.Errorf("GetName() = %s; want getterTest", bi.GetName())
}
if bi.GetType() != common.IF_TYPE_AUTO {
t.Errorf("GetType() = %v; want %v", bi.GetType(), common.IF_TYPE_AUTO)
}
if bi.GetMode() != common.IF_MODE_FULL {
t.Errorf("GetMode() = %v; want %v", bi.GetMode(), common.IF_MODE_FULL)
}
if bi.GetMTU() != common.DEFAULT_MTU { // Assuming default MTU
t.Errorf("GetMTU() = %d; want %d", bi.GetMTU(), common.DEFAULT_MTU)
}
}
func TestBaseInterfaceCallbacks(t *testing.T) {
bi := NewBaseInterface("callbackTest", common.IF_TYPE_TCP, true)
var wg sync.WaitGroup
var callbackCalled bool
callback := func(data []byte, iface common.NetworkInterface) {
if len(data) != 5 {
t.Errorf("Callback received data length %d; want 5", len(data))
}
if iface.GetName() != "callbackTest" {
t.Errorf("Callback received interface name %s; want callbackTest", iface.GetName())
}
callbackCalled = true
wg.Done()
}
bi.SetPacketCallback(callback)
if bi.GetPacketCallback() == nil { // Cannot directly compare functions
t.Error("GetPacketCallback() returned nil after SetPacketCallback()")
}
wg.Add(1)
go bi.ProcessIncoming([]byte{1, 2, 3, 4, 5}) // Run in goroutine as callback might block
// Wait for callback or timeout
waitTimeout(&wg, 1*time.Second, t)
if !callbackCalled {
t.Error("Packet callback was not called after ProcessIncoming")
}
}
func TestBaseInterfaceStats(t *testing.T) {
bi := NewBaseInterface("statsTest", common.IF_TYPE_UDP, true)
bi.Enable() // Need to be Online for ProcessOutgoing
data1 := []byte{1, 2, 3}
data2 := []byte{4, 5, 6, 7, 8}
bi.ProcessIncoming(data1)
if bi.RxBytes != uint64(len(data1)) {
t.Errorf("RxBytes = %d; want %d after first ProcessIncoming", bi.RxBytes, len(data1))
}
bi.ProcessIncoming(data2)
if bi.RxBytes != uint64(len(data1)+len(data2)) {
t.Errorf("RxBytes = %d; want %d after second ProcessIncoming", bi.RxBytes, len(data1)+len(data2))
}
// ProcessOutgoing only updates TxBytes in BaseInterface
err := bi.ProcessOutgoing(data1)
if err != nil {
t.Fatalf("ProcessOutgoing failed: %v", err)
}
if bi.TxBytes != uint64(len(data1)) {
t.Errorf("TxBytes = %d; want %d after first ProcessOutgoing", bi.TxBytes, len(data1))
}
err = bi.ProcessOutgoing(data2)
if err != nil {
t.Fatalf("ProcessOutgoing failed: %v", err)
}
if bi.TxBytes != uint64(len(data1)+len(data2)) {
t.Errorf("TxBytes = %d; want %d after second ProcessOutgoing", bi.TxBytes, len(data1)+len(data2))
}
}
// Helper function to wait for a WaitGroup with a timeout
func waitTimeout(wg *sync.WaitGroup, timeout time.Duration, t *testing.T) {
c := make(chan struct{})
go func() {
defer close(c)
wg.Wait()
}()
select {
case <-c:
// Completed normally
case <-time.After(timeout):
t.Fatal("Timed out waiting for WaitGroup")
}
}
// Minimal mock interface for InterceptedInterface test
type mockInterface struct {
BaseInterface
sendCalled bool
sendData []byte
}
func (m *mockInterface) Send(data []byte, addr string) error {
m.sendCalled = true
m.sendData = data
return nil
}
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) ProcessIncoming(data []byte) {}
func (m *mockInterface) ProcessOutgoing(data []byte) error { return nil }
func (m *mockInterface) SendPathRequest([]byte) error { return nil }
func (m *mockInterface) SendLinkPacket([]byte, []byte, time.Time) error { return nil }
func (m *mockInterface) Start() error { return nil }
func (m *mockInterface) Stop() error { return nil }
func (m *mockInterface) GetConn() net.Conn { return nil }
func (m *mockInterface) GetBandwidthAvailable() bool { return true }
func TestInterceptedInterface(t *testing.T) {
mockBase := &mockInterface{}
var interceptorCalled bool
var interceptedData []byte
interceptor := func(data []byte, iface common.NetworkInterface) error {
interceptorCalled = true
interceptedData = data
return nil
}
intercepted := NewInterceptedInterface(mockBase, interceptor)
testData := []byte("intercept me")
err := intercepted.Send(testData, "dummy_addr")
if err != nil {
t.Fatalf("Intercepted Send failed: %v", err)
}
if !interceptorCalled {
t.Error("Interceptor function was not called")
}
if !bytes.Equal(interceptedData, testData) {
t.Errorf("Interceptor received data %x; want %x", interceptedData, testData)
}
if !mockBase.sendCalled {
t.Error("Original Send function was not called")
}
if !bytes.Equal(mockBase.sendData, testData) {
t.Errorf("Original Send received data %x; want %x", mockBase.sendData, testData)
}
}

View File

@@ -1,10 +1,16 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package interfaces
import (
"fmt"
"net"
"runtime"
"sync"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
)
const (
@@ -17,103 +23,148 @@ const (
KISS_TFEND = 0xDC
KISS_TFESC = 0xDD
TCP_USER_TIMEOUT = 24
TCP_PROBE_AFTER = 5
TCP_PROBE_INTERVAL = 2
TCP_PROBES = 12
RECONNECT_WAIT = 5
INITIAL_TIMEOUT = 5
DEFAULT_MTU = 1064
BITRATE_GUESS_VAL = 10 * 1000 * 1000
RECONNECT_WAIT = 5
INITIAL_TIMEOUT = 5
INITIAL_BACKOFF = time.Second
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 {
Interface
conn net.Conn
targetAddr string
targetPort int
kissFraming bool
i2pTunneled bool
initiator bool
reconnecting bool
neverConnected bool
writing bool
BaseInterface
conn net.Conn
targetAddr string
targetPort int
kissFraming bool
i2pTunneled bool
initiator bool
reconnecting bool
neverConnected bool
writing bool
maxReconnectTries int
packetBuffer []byte
packetType byte
packetBuffer []byte
packetType byte
done chan struct{}
stopOnce sync.Once
}
func NewTCPClient(name string, targetAddr string, targetPort int, kissFraming bool, i2pTunneled bool) (*TCPClientInterface, error) {
func NewTCPClientInterface(name string, targetHost string, targetPort int, kissFraming bool, i2pTunneled bool, enabled bool) (*TCPClientInterface, error) {
tc := &TCPClientInterface{
Interface: Interface{
Name: name,
Mode: MODE_FULL,
MTU: 1064,
Bitrate: 10000000, // 10Mbps estimate
},
targetAddr: targetAddr,
targetPort: targetPort,
kissFraming: kissFraming,
i2pTunneled: i2pTunneled,
initiator: true,
BaseInterface: NewBaseInterface(name, common.IF_TYPE_TCP, enabled),
targetAddr: targetHost,
targetPort: targetPort,
kissFraming: kissFraming,
i2pTunneled: i2pTunneled,
initiator: true,
maxReconnectTries: RECONNECT_WAIT * TCP_PROBES_COUNT,
packetBuffer: make([]byte, 0),
neverConnected: true,
done: make(chan struct{}),
}
if err := tc.connect(true); err != nil {
go tc.reconnect()
} else {
if enabled {
addr := net.JoinHostPort(targetHost, fmt.Sprintf("%d", targetPort))
conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
tc.conn = conn
tc.Online = true
go tc.readLoop()
}
return tc, nil
}
func (tc *TCPClientInterface) connect(initial bool) error {
addr := fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort)
conn, err := net.DialTimeout("tcp", addr, time.Second*INITIAL_TIMEOUT)
if err != nil {
if initial {
return fmt.Errorf("initial connection failed: %v", err)
func (tc *TCPClientInterface) Start() error {
tc.Mutex.Lock()
if !tc.Enabled || tc.Detached {
tc.Mutex.Unlock()
return fmt.Errorf("interface not enabled or detached")
}
if tc.conn != nil {
tc.Online = true
go tc.readLoop()
tc.Mutex.Unlock()
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))
conn, err := net.DialTimeout("tcp", addr, TCP_CONNECT_TIMEOUT)
if err != nil {
return err
}
tc.Mutex.Lock()
tc.conn = conn
tc.Online = true
tc.writing = false
tc.neverConnected = false
tc.Mutex.Unlock()
// Set TCP options
if tcpConn, ok := conn.(*net.TCPConn); ok {
tcpConn.SetNoDelay(true)
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(time.Second * TCP_PROBE_INTERVAL)
// Set platform-specific timeouts
switch runtime.GOOS {
case "linux":
if err := tc.setTimeoutsLinux(); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to set Linux TCP timeouts", "error", err)
}
case "darwin":
if err := tc.setTimeoutsOSX(); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to set OSX TCP timeouts", "error", err)
}
}
tc.Mutex.Lock()
tc.Online = true
tc.Mutex.Unlock()
go tc.readLoop()
return nil
}
func (tc *TCPClientInterface) reconnect() {
if tc.initiator && !tc.reconnecting {
tc.reconnecting = true
attempts := 0
for !tc.Online {
time.Sleep(time.Second * RECONNECT_WAIT)
attempts++
if tc.maxReconnectTries > 0 && attempts > tc.maxReconnectTries {
tc.teardown()
break
}
if err := tc.connect(false); err != nil {
continue
}
go tc.readLoop()
break
}
tc.reconnecting = false
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) readLoop() {
@@ -123,10 +174,30 @@ func (tc *TCPClientInterface) readLoop() {
dataBuffer := make([]byte, 0)
for {
n, err := tc.conn.Read(buffer)
tc.Mutex.RLock()
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 {
tc.Mutex.Lock()
tc.Online = false
if tc.initiator && !tc.Detached {
detached := tc.Detached
initiator := tc.initiator
tc.Mutex.Unlock()
if initiator && !detached {
go tc.reconnect()
} else {
tc.teardown()
@@ -134,51 +205,29 @@ func (tc *TCPClientInterface) readLoop() {
return
}
tc.UpdateStats(uint64(n), true) // #nosec G115
for i := 0; i < n; i++ {
b := buffer[i]
if tc.kissFraming {
// KISS framing logic
if inFrame && b == KISS_FEND {
inFrame = false
if b == HDLC_FLAG {
if inFrame && len(dataBuffer) > 0 {
tc.handlePacket(dataBuffer)
dataBuffer = dataBuffer[:0]
} else if b == KISS_FEND {
inFrame = true
} else if inFrame {
if b == KISS_FESC {
escape = true
} else {
if escape {
if b == KISS_TFEND {
b = KISS_FEND
}
if b == KISS_TFESC {
b = KISS_FESC
}
escape = false
}
dataBuffer = append(dataBuffer, b)
}
}
} else {
// HDLC framing logic
if inFrame && b == HDLC_FLAG {
inFrame = false
tc.handlePacket(dataBuffer)
dataBuffer = dataBuffer[:0]
} else if b == HDLC_FLAG {
inFrame = true
} else if inFrame {
if b == HDLC_ESC {
escape = true
} else {
if escape {
b ^= HDLC_ESC_MASK
escape = false
}
dataBuffer = append(dataBuffer, b)
inFrame = !inFrame
continue
}
if inFrame {
if b == HDLC_ESC {
escape = true
} else {
if escape {
b ^= HDLC_ESC_MASK
escape = false
}
dataBuffer = append(dataBuffer, b)
}
}
}
@@ -187,50 +236,75 @@ func (tc *TCPClientInterface) readLoop() {
func (tc *TCPClientInterface) handlePacket(data []byte) {
if len(data) < 1 {
debug.Log(debug.DEBUG_ALL, "Received invalid packet: empty")
return
}
packetType := data[0]
payload := data[1:]
tc.Mutex.Lock()
tc.RxBytes += uint64(len(data))
lastRx := time.Now()
tc.lastRx = lastRx
callback := tc.packetCallback
tc.Mutex.Unlock()
switch packetType {
case 0x01: // Path request
tc.Interface.ProcessIncoming(payload)
case 0x02: // Link packet
if len(payload) < 40 { // minimum size for link packet
return
}
tc.Interface.ProcessIncoming(payload)
default:
// Unknown packet type
return
debug.Log(debug.DEBUG_ALL, "Received packet", "type", fmt.Sprintf("0x%02x", data[0]), "size", len(data))
// For RNS packets, call the packet callback directly
if callback != nil {
debug.Log(debug.DEBUG_ALL, "Calling packet callback for RNS packet")
callback(data, tc)
} else {
debug.Log(debug.DEBUG_ALL, "No packet callback set for TCP interface")
}
}
// Send implements the interface Send method for TCP interface
func (tc *TCPClientInterface) Send(data []byte, address string) error {
debug.Log(debug.DEBUG_ALL, "TCP interface sending bytes", "name", tc.Name, "bytes", len(data))
if !tc.IsEnabled() || !tc.IsOnline() {
return fmt.Errorf("TCP interface %s is not online", tc.Name)
}
// Send data directly - packet type is already in the first byte of data
// TCP interface uses HDLC framing around the raw packet
return tc.ProcessOutgoing(data)
}
func (tc *TCPClientInterface) ProcessOutgoing(data []byte) error {
if !tc.Online {
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
if tc.kissFraming {
frame = append([]byte{KISS_FEND}, escapeKISS(data)...)
frame = append(frame, KISS_FEND)
} else {
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
frame = append(frame, HDLC_FLAG)
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
frame = append(frame, HDLC_FLAG)
tc.UpdateStats(uint64(len(frame)), false) // #nosec G115
debug.Log(debug.DEBUG_ALL, "TCP interface writing to network", "name", tc.Name, "bytes", len(frame))
tc.Mutex.RLock()
conn := tc.conn
tc.Mutex.RUnlock()
if conn == nil {
return fmt.Errorf("connection closed")
}
if _, err := tc.conn.Write(frame); err != nil {
tc.teardown()
return fmt.Errorf("write failed: %v", err)
_, err := conn.Write(frame)
if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "TCP interface write failed", "name", tc.Name, "error", err)
}
tc.Interface.ProcessOutgoing(data)
return nil
return err
}
func (tc *TCPClientInterface) teardown() {
@@ -238,7 +312,7 @@ func (tc *TCPClientInterface) teardown() {
tc.IN = false
tc.OUT = false
if tc.conn != nil {
tc.conn.Close()
_ = tc.conn.Close()
}
}
@@ -269,130 +343,203 @@ func escapeKISS(data []byte) []byte {
return escaped
}
type TCPServerInterface struct {
Interface
server net.Listener
bindAddr string
bindPort int
i2pTunneled bool
preferIPv6 bool
spawned []*TCPClientInterface
spawnedMutex sync.RWMutex
func (tc *TCPClientInterface) SetPacketCallback(cb common.PacketCallback) {
tc.packetCallback = cb
}
func NewTCPServer(name string, bindAddr string, bindPort int, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) {
ts := &TCPServerInterface{
Interface: Interface{
Name: name,
Mode: MODE_FULL,
MTU: 1064,
Bitrate: 10000000, // 10Mbps estimate
},
bindAddr: bindAddr,
bindPort: bindPort,
i2pTunneled: i2pTunneled,
preferIPv6: preferIPv6,
spawned: make([]*TCPClientInterface, 0),
}
// Resolve bind address
var addr string
if ts.bindAddr == "" {
if ts.preferIPv6 {
addr = fmt.Sprintf("[::0]:%d", ts.bindPort)
} else {
addr = fmt.Sprintf("0.0.0.0:%d", ts.bindPort)
}
} else {
addr = fmt.Sprintf("%s:%d", ts.bindAddr, ts.bindPort)
}
// Create listener
var err error
ts.server, err = net.Listen("tcp", addr)
if err != nil {
return nil, fmt.Errorf("failed to create TCP listener: %v", err)
}
ts.Online = true
ts.IN = true
// Start accept loop
go ts.acceptLoop()
return ts, nil
func (tc *TCPClientInterface) IsEnabled() bool {
tc.Mutex.RLock()
defer tc.Mutex.RUnlock()
return tc.Enabled && tc.Online && !tc.Detached
}
func (ts *TCPServerInterface) acceptLoop() {
for {
conn, err := ts.server.Accept()
if err != nil {
if !ts.Detached {
// Log error and continue accepting
continue
}
func (tc *TCPClientInterface) GetName() string {
return tc.Name
}
func (tc *TCPClientInterface) GetPacketCallback() common.PacketCallback {
tc.Mutex.RLock()
defer tc.Mutex.RUnlock()
return tc.packetCallback
}
func (tc *TCPClientInterface) IsDetached() bool {
tc.Mutex.RLock()
defer tc.Mutex.RUnlock()
return tc.Detached
}
func (tc *TCPClientInterface) IsOnline() bool {
tc.Mutex.RLock()
defer tc.Mutex.RUnlock()
return tc.Online
}
func (tc *TCPClientInterface) reconnect() {
tc.Mutex.Lock()
if tc.reconnecting {
tc.Mutex.Unlock()
return
}
tc.reconnecting = true
tc.Mutex.Unlock()
backoff := time.Second
maxBackoff := time.Minute * 5
retries := 0
for retries < tc.maxReconnectTries {
tc.teardown()
addr := net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort))
conn, err := net.Dial("tcp", addr)
if err == nil {
tc.Mutex.Lock()
tc.conn = conn
tc.Online = true
tc.neverConnected = false
tc.reconnecting = false
tc.Mutex.Unlock()
go tc.readLoop()
return
}
// Create new client interface for this connection
client := &TCPClientInterface{
Interface: Interface{
Name: fmt.Sprintf("Client-%s-%s", ts.Name, conn.RemoteAddr()),
Mode: ts.Mode,
MTU: ts.MTU,
},
conn: conn,
i2pTunneled: ts.i2pTunneled,
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)
// Wait with exponential backoff
time.Sleep(backoff)
// Increase backoff time exponentially
backoff *= 2
if backoff > maxBackoff {
backoff = maxBackoff
}
// Configure TCP options
if tcpConn, ok := conn.(*net.TCPConn); ok {
tcpConn.SetNoDelay(true)
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(time.Duration(TCP_PROBE_INTERVAL) * time.Second)
retries++
}
tc.Mutex.Lock()
tc.reconnecting = false
tc.Mutex.Unlock()
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)
}
func (tc *TCPClientInterface) Enable() {
tc.Mutex.Lock()
defer tc.Mutex.Unlock()
tc.Online = true
}
func (tc *TCPClientInterface) Disable() {
tc.Mutex.Lock()
defer tc.Mutex.Unlock()
tc.Online = false
}
func (tc *TCPClientInterface) IsConnected() bool {
tc.Mutex.RLock()
defer tc.Mutex.RUnlock()
return tc.conn != nil && tc.Online && !tc.reconnecting
}
func (tc *TCPClientInterface) GetRTT() time.Duration {
tc.Mutex.RLock()
defer tc.Mutex.RUnlock()
if !tc.IsConnected() {
return 0
}
if tcpConn, ok := tc.conn.(*net.TCPConn); ok {
var rtt time.Duration
if runtime.GOOS == "linux" {
if info, err := tcpConn.SyscallConn(); err == nil {
if err := info.Control(func(fd uintptr) { // #nosec G104
rtt = platformGetRTT(fd)
}); err != nil {
debug.Log(debug.DEBUG_ERROR, "Error in SyscallConn Control", "error", err)
}
}
}
return rtt
}
client.Online = true
client.IN = ts.IN
client.OUT = ts.OUT
return 0
}
// Add to spawned interfaces
ts.spawnedMutex.Lock()
ts.spawned = append(ts.spawned, client)
ts.spawnedMutex.Unlock()
func (tc *TCPClientInterface) GetTxBytes() uint64 {
tc.Mutex.RLock()
defer tc.Mutex.RUnlock()
return tc.TxBytes
}
// Start client read loop
go client.readLoop()
func (tc *TCPClientInterface) GetRxBytes() uint64 {
tc.Mutex.RLock()
defer tc.Mutex.RUnlock()
return tc.RxBytes
}
func (tc *TCPClientInterface) UpdateStats(bytes uint64, isRx bool) {
tc.Mutex.Lock()
defer tc.Mutex.Unlock()
now := time.Now()
if isRx {
tc.RxBytes += bytes
tc.lastRx = now
debug.Log(debug.DEBUG_TRACE, "Interface RX stats", "name", tc.Name, "bytes", bytes, "total", tc.RxBytes, "last", tc.lastRx)
} else {
tc.TxBytes += bytes
tc.lastTx = now
debug.Log(debug.DEBUG_TRACE, "Interface TX stats", "name", tc.Name, "bytes", bytes, "total", tc.TxBytes, "last", tc.lastTx)
}
}
func (ts *TCPServerInterface) Detach() {
ts.Interface.Detach()
if ts.server != nil {
ts.server.Close()
}
ts.spawnedMutex.Lock()
for _, client := range ts.spawned {
client.Detach()
}
ts.spawned = nil
ts.spawnedMutex.Unlock()
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 (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
ts.spawnedMutex.RLock()
defer ts.spawnedMutex.RUnlock()
type TCPServerInterface struct {
BaseInterface
connections map[string]net.Conn
listener net.Listener
bindAddr string
bindPort int
preferIPv6 bool
kissFraming bool
i2pTunneled bool
done chan struct{}
stopOnce sync.Once
}
var lastErr error
for _, client := range ts.spawned {
if err := client.ProcessOutgoing(data); err != nil {
lastErr = err
}
func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFraming bool, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) {
ts := &TCPServerInterface{
BaseInterface: BaseInterface{
Name: name,
Mode: common.IF_MODE_FULL,
Type: common.IF_TYPE_TCP,
Online: false,
MTU: common.DEFAULT_MTU,
Enabled: true,
Detached: false,
},
connections: make(map[string]net.Conn),
bindAddr: bindAddr,
bindPort: bindPort,
preferIPv6: preferIPv6,
kissFraming: kissFraming,
i2pTunneled: i2pTunneled,
done: make(chan struct{}),
}
return lastErr
return ts, nil
}
func (ts *TCPServerInterface) String() string {
@@ -401,8 +548,228 @@ func (ts *TCPServerInterface) String() string {
if ts.preferIPv6 {
addr = "[::0]"
} else {
addr = "0.0.0.0"
addr = "0.0.0.0"
}
}
return fmt.Sprintf("TCPServerInterface[%s/%s:%d]", ts.Name, addr, ts.bindPort)
}
}
func (ts *TCPServerInterface) SetPacketCallback(callback common.PacketCallback) {
ts.Mutex.Lock()
defer ts.Mutex.Unlock()
ts.packetCallback = callback
}
func (ts *TCPServerInterface) GetPacketCallback() common.PacketCallback {
ts.Mutex.RLock()
defer ts.Mutex.RUnlock()
return ts.packetCallback
}
func (ts *TCPServerInterface) IsEnabled() bool {
ts.Mutex.RLock()
defer ts.Mutex.RUnlock()
return ts.Enabled && ts.Online && !ts.Detached
}
func (ts *TCPServerInterface) GetName() string {
return ts.Name
}
func (ts *TCPServerInterface) IsDetached() bool {
ts.Mutex.RLock()
defer ts.Mutex.RUnlock()
return ts.Detached
}
func (ts *TCPServerInterface) IsOnline() bool {
ts.Mutex.RLock()
defer ts.Mutex.RUnlock()
return ts.Online
}
func (ts *TCPServerInterface) Enable() {
ts.Mutex.Lock()
defer ts.Mutex.Unlock()
ts.Online = true
}
func (ts *TCPServerInterface) Disable() {
ts.Mutex.Lock()
defer ts.Mutex.Unlock()
ts.Online = false
}
func (ts *TCPServerInterface) Start() error {
ts.Mutex.Lock()
if ts.listener != nil {
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))
listener, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("failed to start TCP server: %w", err)
}
ts.Mutex.Lock()
ts.listener = listener
ts.Online = true
ts.Mutex.Unlock()
// Accept connections in a goroutine
go func() {
for {
ts.Mutex.RLock()
done := ts.done
ts.Mutex.RUnlock()
select {
case <-done:
return
default:
}
conn, err := listener.Accept()
if err != nil {
ts.Mutex.RLock()
online := ts.Online
ts.Mutex.RUnlock()
if !online {
return // Normal shutdown
}
debug.Log(debug.DEBUG_ERROR, "Error accepting connection", "error", err)
continue
}
// Handle each connection in a separate goroutine
go ts.handleConnection(conn)
}
}()
return nil
}
func (ts *TCPServerInterface) Stop() error {
ts.Mutex.Lock()
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
}
func (ts *TCPServerInterface) GetTxBytes() uint64 {
ts.Mutex.RLock()
defer ts.Mutex.RUnlock()
return ts.TxBytes
}
func (ts *TCPServerInterface) GetRxBytes() uint64 {
ts.Mutex.RLock()
defer ts.Mutex.RUnlock()
return ts.RxBytes
}
func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
addr := conn.RemoteAddr().String()
ts.Mutex.Lock()
ts.connections[addr] = conn
ts.Mutex.Unlock()
defer func() {
ts.Mutex.Lock()
delete(ts.connections, addr)
ts.Mutex.Unlock()
_ = conn.Close()
}()
buffer := make([]byte, ts.MTU)
for {
ts.Mutex.RLock()
done := ts.done
ts.Mutex.RUnlock()
select {
case <-done:
return
default:
}
n, err := conn.Read(buffer)
if err != nil {
return
}
ts.Mutex.Lock()
ts.RxBytes += uint64(n) // #nosec G115
callback := ts.packetCallback
ts.Mutex.Unlock()
if callback != nil {
callback(buffer[:n], ts)
}
}
}
func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
ts.Mutex.RLock()
online := ts.Online
ts.Mutex.RUnlock()
if !online {
return fmt.Errorf("interface offline")
}
var frame []byte
if ts.kissFraming {
frame = append([]byte{KISS_FEND}, escapeKISS(data)...)
frame = append(frame, KISS_FEND)
} else {
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
frame = append(frame, HDLC_FLAG)
}
ts.Mutex.Lock()
ts.TxBytes += uint64(len(frame)) // #nosec G115
conns := make([]net.Conn, 0, len(ts.connections))
for _, conn := range ts.connections {
conns = append(conns, conn)
}
ts.Mutex.Unlock()
for _, conn := range conns {
if _, err := conn.Write(frame); err != nil {
debug.Log(debug.DEBUG_VERBOSE, "Error writing to connection", "address", conn.RemoteAddr(), "error", err)
}
}
return nil
}

View File

@@ -0,0 +1,16 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build !linux
// +build !linux
package interfaces
import (
"time"
)
// platformGetRTT is defined in OS-specific files
// Default implementation for non-Linux platforms
func platformGetRTT(fd uintptr) time.Duration {
return 0
}

View File

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

@@ -0,0 +1,41 @@
// 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()
}

111
pkg/interfaces/tcp_linux.go Normal file
View File

@@ -0,0 +1,111 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build linux
// +build linux
package interfaces
import (
"fmt"
"net"
"syscall"
"time"
"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 {
var info syscall.TCPInfo
// bearer:disable go_gosec_unsafe_unsafe
infoLen := uint32(unsafe.Sizeof(info))
const TCP_INFO = 11
// #nosec G103
_, _, errno := syscall.Syscall6(
syscall.SYS_GETSOCKOPT,
fd,
syscall.IPPROTO_TCP,
TCP_INFO,
// bearer:disable go_gosec_unsafe_unsafe
uintptr(unsafe.Pointer(&info)),
// bearer:disable go_gosec_unsafe_unsafe
uintptr(unsafe.Pointer(&infoLen)),
0,
)
if errno != 0 {
return 0
}
return time.Duration(info.Rtt) * time.Microsecond
}

View File

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

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

@@ -0,0 +1,52 @@
package interfaces
import (
"bytes"
"testing"
)
func TestEscapeHDLC(t *testing.T) {
testCases := []struct {
name string
input []byte
expected []byte
}{
{"NoEscape", []byte{0x01, 0x02, 0x03}, []byte{0x01, 0x02, 0x03}},
{"EscapeFlag", []byte{0x01, HDLC_FLAG, 0x03}, []byte{0x01, HDLC_ESC, HDLC_FLAG ^ HDLC_ESC_MASK, 0x03}},
{"EscapeEsc", []byte{0x01, HDLC_ESC, 0x03}, []byte{0x01, HDLC_ESC, HDLC_ESC ^ HDLC_ESC_MASK, 0x03}},
{"EscapeBoth", []byte{HDLC_FLAG, HDLC_ESC}, []byte{HDLC_ESC, HDLC_FLAG ^ HDLC_ESC_MASK, HDLC_ESC, HDLC_ESC ^ HDLC_ESC_MASK}},
{"Empty", []byte{}, []byte{}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := escapeHDLC(tc.input)
if !bytes.Equal(result, tc.expected) {
t.Errorf("escapeHDLC(%x) = %x; want %x", tc.input, result, tc.expected)
}
})
}
}
func TestEscapeKISS(t *testing.T) {
testCases := []struct {
name string
input []byte
expected []byte
}{
{"NoEscape", []byte{0x01, 0x02, 0x03}, []byte{0x01, 0x02, 0x03}},
{"EscapeFEND", []byte{0x01, KISS_FEND, 0x03}, []byte{0x01, KISS_FESC, KISS_TFEND, 0x03}},
{"EscapeFESC", []byte{0x01, KISS_FESC, 0x03}, []byte{0x01, KISS_FESC, KISS_TFESC, 0x03}},
{"EscapeBoth", []byte{KISS_FEND, KISS_FESC}, []byte{KISS_FESC, KISS_TFEND, KISS_FESC, KISS_TFESC}},
{"Empty", []byte{}, []byte{}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := escapeKISS(tc.input)
if !bytes.Equal(result, tc.expected) {
t.Errorf("escapeKISS(%x) = %x; want %x", tc.input, result, tc.expected)
}
})
}
}

View File

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

@@ -0,0 +1,42 @@
// 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,88 +1,141 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package interfaces
import (
"fmt"
"net"
"sync"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
)
type UDPInterface struct {
Interface
conn *net.UDPConn
listenAddr *net.UDPAddr
BaseInterface
conn *net.UDPConn
addr *net.UDPAddr
targetAddr *net.UDPAddr
readBuffer []byte
done chan struct{}
stopOnce sync.Once
}
func NewUDPInterface(name string, listenAddr string, targetAddr string) (*UDPInterface, error) {
ui := &UDPInterface{
Interface: Interface{
Name: name,
Mode: MODE_FULL,
MTU: 1500,
Bitrate: 100000000, // 100Mbps estimate for UDP
},
readBuffer: make([]byte, 65535),
}
// Parse listen address
laddr, err := net.ResolveUDPAddr("udp", listenAddr)
func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) {
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, fmt.Errorf("invalid listen address: %v", err)
return nil, err
}
ui.listenAddr = laddr
// Parse target address if provided
if targetAddr != "" {
taddr, err := net.ResolveUDPAddr("udp", targetAddr)
var targetAddr *net.UDPAddr
if target != "" {
targetAddr, err = net.ResolveUDPAddr("udp", target)
if err != nil {
return nil, fmt.Errorf("invalid target address: %v", err)
return nil, err
}
ui.targetAddr = taddr
ui.OUT = true
}
// Create UDP connection
conn, err := net.ListenUDP("udp", ui.listenAddr)
if err != nil {
return nil, fmt.Errorf("failed to listen on UDP: %v", err)
ui := &UDPInterface{
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
addr: udpAddr,
targetAddr: targetAddr,
readBuffer: make([]byte, common.NUM_1064),
done: make(chan struct{}),
}
ui.conn = conn
ui.IN = true
ui.Online = true
// Start read loop
go ui.readLoop()
ui.MTU = common.NUM_1064
return ui, nil
}
func (ui *UDPInterface) readLoop() {
for {
if !ui.Online {
return
func (ui *UDPInterface) GetName() string {
return ui.Name
}
func (ui *UDPInterface) GetType() common.InterfaceType {
return ui.Type
}
func (ui *UDPInterface) GetMode() common.InterfaceMode {
return ui.Mode
}
func (ui *UDPInterface) IsOnline() bool {
ui.Mutex.RLock()
defer ui.Mutex.RUnlock()
return ui.Online
}
func (ui *UDPInterface) IsDetached() bool {
ui.Mutex.RLock()
defer ui.Mutex.RUnlock()
return ui.Detached
}
func (ui *UDPInterface) Detach() {
ui.Mutex.Lock()
defer ui.Mutex.Unlock()
ui.Detached = true
ui.Online = false
if ui.conn != nil {
ui.conn.Close() // #nosec G104
}
ui.stopOnce.Do(func() {
if ui.done != nil {
close(ui.done)
}
})
}
n, addr, err := ui.conn.ReadFromUDP(ui.readBuffer)
if err != nil {
if !ui.Detached {
// Log error
}
continue
}
func (ui *UDPInterface) Send(data []byte, addr string) error {
debug.Log(debug.DEBUG_ALL, "UDP interface sending bytes", "name", ui.Name, "bytes", len(data))
// Copy received data
data := make([]byte, n)
copy(data, ui.readBuffer[:n])
if !ui.IsEnabled() {
return fmt.Errorf("interface not enabled")
}
// Process packet
ui.ProcessIncoming(data)
if ui.targetAddr == nil {
return fmt.Errorf("no target address configured")
}
ui.Mutex.Lock()
ui.TxBytes += uint64(len(data))
ui.Mutex.Unlock()
_, err := ui.conn.WriteTo(data, ui.targetAddr)
if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "UDP interface write failed", "name", ui.Name, "error", err)
} else {
debug.Log(debug.DEBUG_ALL, "UDP interface sent bytes successfully", "name", ui.Name, "bytes", len(data))
}
return err
}
func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) {
ui.Mutex.Lock()
defer ui.Mutex.Unlock()
ui.packetCallback = callback
}
func (ui *UDPInterface) GetPacketCallback() common.PacketCallback {
ui.Mutex.RLock()
defer ui.Mutex.RUnlock()
return ui.packetCallback
}
func (ui *UDPInterface) ProcessIncoming(data []byte) {
if callback := ui.GetPacketCallback(); callback != nil {
callback(data, ui)
}
}
func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
if !ui.Online || ui.targetAddr == nil {
return fmt.Errorf("interface offline or no target address configured")
if !ui.IsOnline() {
return fmt.Errorf("interface offline")
}
if ui.targetAddr == nil {
return fmt.Errorf("no target address configured")
}
_, err := ui.conn.WriteToUDP(data, ui.targetAddr)
@@ -90,13 +143,151 @@ func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
return fmt.Errorf("UDP write failed: %v", err)
}
ui.Interface.ProcessOutgoing(data)
ui.Mutex.Lock()
ui.TxBytes += uint64(len(data))
ui.Mutex.Unlock()
return nil
}
func (ui *UDPInterface) Detach() {
ui.Interface.Detach()
func (ui *UDPInterface) GetConn() net.Conn {
return ui.conn
}
func (ui *UDPInterface) GetTxBytes() uint64 {
ui.Mutex.RLock()
defer ui.Mutex.RUnlock()
return ui.TxBytes
}
func (ui *UDPInterface) GetRxBytes() uint64 {
ui.Mutex.RLock()
defer ui.Mutex.RUnlock()
return ui.RxBytes
}
func (ui *UDPInterface) GetMTU() int {
return ui.MTU
}
func (ui *UDPInterface) GetBitrate() int {
return int(ui.Bitrate)
}
func (ui *UDPInterface) Enable() {
ui.Mutex.Lock()
defer ui.Mutex.Unlock()
ui.Online = true
}
func (ui *UDPInterface) Disable() {
ui.Mutex.Lock()
defer ui.Mutex.Unlock()
ui.Online = false
}
func (ui *UDPInterface) Start() error {
ui.Mutex.Lock()
if ui.conn != nil {
ui.conn.Close()
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)
if err != nil {
return err
}
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.Mutex.Unlock()
// Start the read loop in a goroutine
go ui.readLoop()
return nil
}
func (ui *UDPInterface) Stop() error {
ui.Detach()
return nil
}
func (ui *UDPInterface) readLoop() {
buffer := make([]byte, common.NUM_1064)
for {
ui.Mutex.RLock()
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 {
ui.Mutex.RLock()
stillOnline := ui.Online
ui.Mutex.RUnlock()
if stillOnline {
debug.Log(debug.DEBUG_ERROR, "Error reading from UDP interface", "name", ui.Name, "error", err)
}
return
}
ui.Mutex.Lock()
// #nosec G115 - Network read sizes are always positive and within safe range
ui.RxBytes += uint64(n)
// Auto-discover target address from first packet if not set
if ui.targetAddr == nil {
debug.Log(debug.DEBUG_ALL, "UDP interface discovered peer", "name", ui.Name, "peer", remoteAddr.String())
ui.targetAddr = remoteAddr
}
callback := ui.packetCallback
ui.Mutex.Unlock()
if callback != nil {
callback(buffer[:n], ui)
}
}
}
func (ui *UDPInterface) IsEnabled() bool {
ui.Mutex.RLock()
defer ui.Mutex.RUnlock()
return ui.Enabled && ui.Online && !ui.Detached
}

View File

@@ -0,0 +1,87 @@
package interfaces
import (
"testing"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
)
func TestNewUDPInterface(t *testing.T) {
validAddr := "127.0.0.1:0" // Use port 0 for OS to assign a free port
validTarget := "127.0.0.1:8080"
invalidAddr := "invalid-address"
t.Run("ValidConfig", func(t *testing.T) {
ui, err := NewUDPInterface("udpValid", validAddr, validTarget, true)
if err != nil {
t.Fatalf("NewUDPInterface failed with valid config: %v", err)
}
if ui == nil {
t.Fatal("NewUDPInterface returned nil interface with valid config")
}
if ui.GetName() != "udpValid" {
t.Errorf("GetName() = %s; want udpValid", ui.GetName())
}
if ui.GetType() != common.IF_TYPE_UDP {
t.Errorf("GetType() = %v; want %v", ui.GetType(), common.IF_TYPE_UDP)
}
if ui.targetAddr.String() != validTarget {
t.Errorf("Resolved targetAddr = %s; want %s", ui.targetAddr.String(), validTarget)
}
if !ui.Enabled { // BaseInterface field
t.Error("Interface not enabled by default when requested")
}
if ui.IsOnline() { // Should be offline initially
t.Error("Interface online initially")
}
})
t.Run("ValidConfigNoTarget", func(t *testing.T) {
ui, err := NewUDPInterface("udpNoTarget", validAddr, "", true)
if err != nil {
t.Fatalf("NewUDPInterface failed with valid config (no target): %v", err)
}
if ui == nil {
t.Fatal("NewUDPInterface returned nil interface with valid config (no target)")
}
if ui.targetAddr != nil {
t.Errorf("targetAddr = %v; want nil", ui.targetAddr)
}
})
t.Run("InvalidAddress", func(t *testing.T) {
_, err := NewUDPInterface("udpInvalidAddr", invalidAddr, validTarget, true)
if err == nil {
t.Error("NewUDPInterface succeeded with invalid address")
}
})
t.Run("InvalidTarget", func(t *testing.T) {
_, err := NewUDPInterface("udpInvalidTarget", validAddr, invalidAddr, true)
if err == nil {
t.Error("NewUDPInterface succeeded with invalid target address")
}
})
}
func TestUDPInterfaceState(t *testing.T) {
// Basic state tests are covered by BaseInterface tests
addr := "127.0.0.1:0"
ui, _ := NewUDPInterface("udpState", addr, "", true)
if ui.conn != nil {
t.Error("conn field is not nil before Start()")
}
// We don't call Start() here because it requires actual network binding
// Testing Send requires Start() and a listener, which is too complex for unit tests here
// Test Detach
ui.Detach()
if !ui.IsDetached() {
t.Error("IsDetached() is false after Detach()")
}
// Further tests on Send/ProcessOutgoing/readLoop would require mocking net.UDPConn
// or setting up a local listener.
}

View File

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

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

@@ -0,0 +1,253 @@
// 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() {
return fmt.Errorf("WebSocket already started")
}
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")
var packet []byte
if data.Type() == js.TypeString {
packet = []byte(data.String())
} else if data.Type() == js.TypeObject {
array := js.Global().Get("Uint8Array").New(data)
length := array.Get("length").Int()
packet = make([]byte, length)
js.CopyBytesToGo(packet, array)
} else {
debug.Log(debug.DEBUG_ERROR, "Unknown WebSocket message type", "type", data.Type().String())
return nil
}
if len(packet) < 1 {
debug.Log(debug.DEBUG_ERROR, "WebSocket message empty")
return nil
}
wsi.Mutex.Lock()
wsi.RxBytes += uint64(len(packet))
wsi.Mutex.Unlock()
wsi.ProcessIncoming(packet)
return nil
}))
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 {
time.Sleep(WS_RECONNECT_DELAY)
go 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) Send(data []byte, addr string) error {
if !wsi.IsEnabled() {
return fmt.Errorf("interface not enabled")
}
wsi.Mutex.Lock()
wsi.TxBytes += uint64(len(data))
wsi.Mutex.Unlock()
if !wsi.connected {
wsi.Mutex.Lock()
wsi.messageQueue = append(wsi.messageQueue, data)
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) ProcessOutgoing(data []byte) error {
return wsi.Send(data, "")
}
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

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

218
pkg/link/link_test.go Normal file
View File

@@ -0,0 +1,218 @@
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,13 +1,11 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package packet
const (
// MTU constants
EncryptedMDU = 383 // Maximum size of payload data in encrypted packet
PlainMDU = 464 // Maximum size of payload data in unencrypted packet
// Header Types
HeaderType1 = 0 // Two byte header, one 16 byte address field
HeaderType2 = 1 // Two byte header, two 16 byte address fields
PlainMDU = 464 // Maximum size of payload data in unencrypted packet
// Propagation Types
PropagationBroadcast = 0
@@ -19,9 +17,7 @@ const (
DestinationPlain = 2
DestinationLink = 3
// Packet Types
PacketData = 0
PacketAnnounce = 1
PacketLinkRequest = 2
PacketProof = 3
)
// Minimum packet sizes
MinAnnounceSize = 170 // header(2) + desthash(16) + context(1) + enckey(32) + signkey(32) +
// namehash(10) + randomhash(10) + signature(64) + min appdata(3)
)

View File

@@ -1,171 +1,356 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package packet
import (
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
)
const (
HeaderSize = 2
AddressSize = 16
ContextSize = 1
MaxDataSize = 465 // Maximum size of payload data
)
// Packet Types
PacketTypeData = 0x00
PacketTypeAnnounce = 0x01
PacketTypeLinkReq = 0x02
PacketTypeProof = 0x03
// Header flags and types
const (
// First byte flags
IFACFlag = 0x80 // Interface authentication code flag
HeaderTypeFlag = 0x40 // Header type flag
ContextFlag = 0x20 // Context flag
PropagationFlags = 0x18 // Propagation type flags (bits 3-4)
DestinationFlags = 0x06 // Destination type flags (bits 1-2)
PacketTypeFlags = 0x01 // Packet type flags (bit 0)
// Header Types
HeaderType1 = 0x00
HeaderType2 = 0x01
// Second byte
HopsField = 0xFF // Number of hops (entire byte)
// Context Types
ContextNone = 0x00
ContextResource = 0x01
ContextResourceAdv = 0x02
ContextResourceReq = 0x03
ContextResourceHMU = 0x04
ContextResourcePRF = 0x05
ContextResourceICL = 0x06
ContextResourceRCL = 0x07
ContextCacheReq = 0x08
ContextRequest = 0x09
ContextResponse = 0x0A
ContextPathResponse = 0x0B
ContextCommand = 0x0C
ContextCmdStatus = 0x0D
ContextChannel = 0x0E
ContextKeepalive = 0xFA
ContextLinkIdentify = 0xFB
ContextLinkClose = 0xFC
ContextLinkProof = 0xFD
ContextLRRTT = 0xFE
ContextLRProof = 0xFF
// Flag Values
FlagSet = 0x01
FlagUnset = 0x00
// Header sizes
HeaderMaxSize = 64
MTU = 500
AddressSize = 32 // Size of address/hash fields in bytes
)
type Packet struct {
Header [2]byte
Addresses []byte // Either 16 or 32 bytes depending on header type
Context byte
Data []byte
AccessCode []byte // Optional: Only present if IFAC flag is set
HeaderType byte
PacketType byte
TransportType byte
Context byte
ContextFlag byte
Hops byte
DestinationType byte
DestinationHash []byte
Destination interface{}
TransportID []byte
Data []byte
Raw []byte
Packed bool
Sent bool
CreateReceipt bool
FromPacked bool
SentAt time.Time
PacketHash []byte
RatchetID []byte
RSSI *float64
SNR *float64
Q *float64
Addresses []byte
Link interface{}
receipt *PacketReceipt
}
func NewPacket(headerType, propagationType, destinationType, packetType byte, hops byte) *Packet {
p := &Packet{
Header: [2]byte{0, hops},
Addresses: make([]byte, 0),
Data: make([]byte, 0),
type PacketConfig struct {
DestType byte
Data []byte
PacketType byte
Context byte
TransportType byte
HeaderType byte
TransportID []byte
CreateReceipt bool
ContextFlag byte
}
func NewPacket(destType byte, data []byte, packetType byte, context byte,
transportType byte, headerType byte, transportID []byte, createReceipt bool,
contextFlag byte) *Packet {
return &Packet{
HeaderType: headerType,
PacketType: packetType,
TransportType: transportType,
Context: context,
ContextFlag: contextFlag,
Hops: 0,
DestinationType: destType,
Data: data,
TransportID: transportID,
CreateReceipt: createReceipt,
Packed: false,
Sent: false,
FromPacked: false,
}
}
func (p *Packet) Pack() error {
if p.Packed {
return nil
}
// Set header type
if headerType == HeaderType2 {
p.Header[0] |= HeaderTypeFlag
p.Addresses = make([]byte, 2*AddressSize) // Two address fields
debug.Log(debug.DEBUG_PACKETS, "Packing packet", "type", p.PacketType, "header", p.HeaderType)
// Create header byte (Corrected order)
flags := byte(0)
flags |= (p.HeaderType << 6) & 0b01000000
flags |= (p.ContextFlag << 5) & 0b00100000
flags |= (p.TransportType << 4) & 0b00010000
flags |= (p.DestinationType << 2) & 0b00001100
flags |= p.PacketType & 0b00000011
header := []byte{flags, p.Hops}
debug.Log(debug.DEBUG_TRACE, "Created packet header", "flags", fmt.Sprintf("%08b", flags), "hops", p.Hops)
header = append(header, p.DestinationHash...)
if p.HeaderType == HeaderType2 {
if p.TransportID == nil {
return errors.New("transport ID required for header type 2")
}
header = append(header, p.TransportID...)
debug.Log(debug.DEBUG_ALL, "Added transport ID to header", "transport_id", fmt.Sprintf("%x", p.TransportID))
}
header = append(header, p.Context)
debug.Log(debug.DEBUG_PACKETS, "Final header length", "bytes", len(header))
p.Raw = append(header, p.Data...)
debug.Log(debug.DEBUG_TRACE, "Final packet size", "bytes", len(p.Raw))
if len(p.Raw) > MTU {
return errors.New("packet size exceeds MTU")
}
p.Packed = true
p.updateHash()
debug.Log(debug.DEBUG_ALL, "Packet hash", "hash", fmt.Sprintf("%x", p.PacketHash))
return nil
}
func (p *Packet) Unpack() error {
if len(p.Raw) < 3 {
return errors.New("packet too short")
}
flags := p.Raw[0]
p.Hops = p.Raw[1]
p.HeaderType = (flags & 0b01000000) >> 6
p.ContextFlag = (flags & 0b00100000) >> 5
p.TransportType = (flags & 0b00010000) >> 4
p.DestinationType = (flags & 0b00001100) >> 2
p.PacketType = flags & 0b00000011
dstLen := 16 // Truncated hash length
if p.HeaderType == HeaderType2 {
// Header Type 2: Header(2) + DestHash(16) + TransportID(16) + Context(1) + Data
if len(p.Raw) < 2*dstLen+3 {
return errors.New("packet too short for header type 2")
}
p.DestinationHash = p.Raw[2 : dstLen+2] // Destination hash first
p.TransportID = p.Raw[dstLen+2 : 2*dstLen+2] // Transport ID second
p.Context = p.Raw[2*dstLen+2]
p.Data = p.Raw[2*dstLen+3:]
} else {
p.Addresses = make([]byte, AddressSize) // One address field
// Header Type 1: Header(2) + DestHash(16) + Context(1) + Data
if len(p.Raw) < dstLen+3 {
return errors.New("packet too short for header type 1")
}
p.TransportID = nil
p.DestinationHash = p.Raw[2 : dstLen+2]
p.Context = p.Raw[dstLen+2]
p.Data = p.Raw[dstLen+3:]
}
// Set propagation type
p.Header[0] |= (propagationType << 3) & PropagationFlags
// Set destination type
p.Header[0] |= (destinationType << 1) & DestinationFlags
// Set packet type
p.Header[0] |= packetType & PacketTypeFlags
return p
}
func (p *Packet) SetAccessCode(code []byte) {
p.AccessCode = code
p.Header[0] |= IFACFlag
}
func (p *Packet) SetContext(context byte) {
p.Context = context
p.Header[0] |= ContextFlag
}
func (p *Packet) SetData(data []byte) error {
if len(data) > MaxDataSize {
return errors.New("data exceeds maximum allowed size")
}
p.Data = data
p.Packed = false
p.updateHash()
return nil
}
func (p *Packet) SetAddress(index int, address []byte) error {
if len(address) != AddressSize {
return errors.New("invalid address size")
func (p *Packet) GetHash() []byte {
hashable := p.getHashablePart()
hash := sha256.Sum256(hashable)
return hash[:]
}
func (p *Packet) getHashablePart() []byte {
hashable := []byte{p.Raw[0] & 0b00001111} // Lower 4 bits of flags
if p.HeaderType == HeaderType2 {
// Start hash from DestHash (index 18), skipping TransportID
dstLen := 16 // RNS.Identity.TRUNCATED_HASHLENGTH / 8
startIndex := dstLen + 2
if len(p.Raw) > startIndex {
hashable = append(hashable, p.Raw[startIndex:]...)
}
} else {
// Start hash from DestHash (index 2)
if len(p.Raw) > 2 {
hashable = append(hashable, p.Raw[2:]...)
}
}
offset := index * AddressSize
if offset+AddressSize > len(p.Addresses) {
return errors.New("address index out of range")
return hashable
}
func (p *Packet) updateHash() {
p.PacketHash = p.GetHash()
}
func (p *Packet) Hash() []byte {
return p.GetHash()
}
func (p *Packet) TruncatedHash() []byte {
hash := p.GetHash()
if len(hash) >= 16 {
return hash[:16]
}
copy(p.Addresses[offset:], address)
return nil
return hash
}
func (p *Packet) Serialize() ([]byte, error) {
totalSize := HeaderSize + len(p.Addresses) + ContextSize + len(p.Data)
if p.AccessCode != nil {
totalSize += len(p.AccessCode)
if !p.Packed {
if err := p.Pack(); err != nil {
return nil, fmt.Errorf("failed to pack packet: %w", err)
}
}
buffer := make([]byte, totalSize)
offset := 0
p.Addresses = p.DestinationHash
// Write header
copy(buffer[offset:], p.Header[:])
offset += HeaderSize
// Write access code if present
if p.AccessCode != nil {
copy(buffer[offset:], p.AccessCode)
offset += len(p.AccessCode)
}
// Write addresses
copy(buffer[offset:], p.Addresses)
offset += len(p.Addresses)
// Write context
buffer[offset] = p.Context
offset += ContextSize
// Write data
copy(buffer[offset:], p.Data)
return buffer, nil
return p.Raw, nil
}
func ParsePacket(data []byte) (*Packet, error) {
if len(data) < HeaderSize {
return nil, errors.New("packet data too short")
func NewAnnouncePacket(destHash []byte, identity *identity.Identity, appData []byte, transportID []byte) (*Packet, error) {
debug.Log(debug.DEBUG_ALL, "Creating new announce packet", "dest_hash", fmt.Sprintf("%x", destHash), "app_data", fmt.Sprintf("%x", appData))
// Get public key separated into encryption and signing keys
pubKey := identity.GetPublicKey()
encKey := pubKey[:32]
signKey := pubKey[32:]
debug.Log(debug.DEBUG_PACKETS, "Using public keys", "enc_key", fmt.Sprintf("%x", encKey), "sign_key", fmt.Sprintf("%x", signKey))
// Parse app name from first msgpack element if possible
// For nodes, we'll use "reticulum.node" as the name hash
var appName string
if len(appData) > 2 && appData[0] == 0x93 {
// This is a node announce, use standard node name
appName = "reticulum.node"
} else if len(appData) > 3 && appData[0] == 0x92 && appData[1] == 0xc4 {
// Try to extract name from peer announce appData
nameLen := int(appData[2])
if 3+nameLen <= len(appData) {
appName = string(appData[3 : 3+nameLen])
} else {
// Default fallback
appName = "reticulum-go.node"
}
} else {
// Default fallback
appName = "reticulum-go.node"
}
// Create name hash (10 bytes)
nameHash := sha256.Sum256([]byte(appName))
nameHash10 := nameHash[:10]
debug.Log(debug.DEBUG_PACKETS, "Using name hash", "name", appName, "hash", fmt.Sprintf("%x", nameHash10))
// Create random hash (10 bytes) - 5 bytes random + 5 bytes time
randomHash := make([]byte, 10)
_, err := rand.Read(randomHash[:5]) // #nosec G104
if err != nil {
debug.Log(debug.DEBUG_PACKETS, "Failed to read random bytes for hash", "error", err)
return nil, err // Or handle the error appropriately
}
timeBytes := make([]byte, 8)
binary.BigEndian.PutUint64(timeBytes, uint64(time.Now().Unix())) // #nosec G115
copy(randomHash[5:], timeBytes[:5])
debug.Log(debug.DEBUG_PACKETS, "Generated random hash", "hash", fmt.Sprintf("%x", randomHash))
// Prepare ratchet ID if available (not yet implemented)
var ratchetID []byte
// Prepare data for signature
// Signature consists of destination hash, public keys, name hash, random hash, and app data
signedData := make([]byte, 0, len(destHash)+len(encKey)+len(signKey)+len(nameHash10)+len(randomHash)+len(appData))
signedData = append(signedData, destHash...)
signedData = append(signedData, encKey...)
signedData = append(signedData, signKey...)
signedData = append(signedData, nameHash10...)
signedData = append(signedData, randomHash...)
signedData = append(signedData, appData...)
debug.Log(debug.DEBUG_TRACE, "Created signed data", "bytes", len(signedData))
// Sign the data
signature := identity.Sign(signedData)
debug.Log(debug.DEBUG_PACKETS, "Generated signature", "signature", fmt.Sprintf("%x", signature))
// Combine all fields according to spec
// Data structure: Public Key (32) + Signing Key (32) + Name Hash (10) + Random Hash (10) + Ratchet (optional) + Signature (64) + App Data
data := make([]byte, 0, 32+32+10+10+64+len(appData))
data = append(data, encKey...) // Encryption key (32 bytes)
data = append(data, signKey...) // Signing key (32 bytes)
data = append(data, nameHash10...) // Name hash (10 bytes)
data = append(data, randomHash...) // Random hash (10 bytes)
if ratchetID != nil {
data = append(data, ratchetID...) // Ratchet ID (32 bytes if present)
}
data = append(data, signature...) // Signature (64 bytes)
data = append(data, appData...) // Application data (variable)
debug.Log(debug.DEBUG_TRACE, "Combined packet data", "bytes", len(data))
// Create the packet with header type 2 (two address fields)
p := &Packet{
Header: [2]byte{data[0], data[1]},
HeaderType: HeaderType2,
PacketType: PacketTypeAnnounce,
TransportID: transportID,
DestinationHash: destHash,
Data: data,
}
offset := HeaderSize
// Handle access code if present
if p.Header[0]&IFACFlag != 0 {
// Access code handling would go here
// For now, we'll assume no access code
return nil, errors.New("access code handling not implemented")
}
// Determine address size based on header type
addrLen := AddressSize
if p.Header[0]&HeaderTypeFlag != 0 {
addrLen = 2 * AddressSize
}
if len(data[offset:]) < addrLen+ContextSize {
return nil, errors.New("packet data too short for addresses and context")
}
// Copy addresses
p.Addresses = make([]byte, addrLen)
copy(p.Addresses, data[offset:offset+addrLen])
offset += addrLen
// Copy context
p.Context = data[offset]
offset++
// Copy remaining data
p.Data = make([]byte, len(data)-offset)
copy(p.Data, data[offset:])
debug.Log(debug.DEBUG_VERBOSE, "Created announce packet", "type", p.PacketType, "header", p.HeaderType)
return p, nil
}
}

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