88 Commits

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

View File

@@ -6,7 +6,6 @@ bin/
*.dylib
# Go modules' cache
/pkg/
vendor/
# Local test/coverage/log artifacts

View File

@@ -52,6 +52,12 @@ jobs:
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
@@ -59,11 +65,21 @@ jobs:
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 }})..."

View File

@@ -8,13 +8,14 @@ on:
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
ref: ${{ github.ref }}
- name: Setup Go
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
@@ -29,26 +30,24 @@ jobs:
- name: Install dependencies
run: task deps
- name: Download Trivy
run: |
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
- name: Install Trivy
run: task trivy:install
- name: Generate SBOM
run: |
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 .
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 checkout main
git fetch origin main || git fetch origin master
git checkout main || git checkout master
git add sbom/
git diff --quiet && git diff --staged --quiet || (git commit -m "Auto-update SBOM [skip ci]" && git push origin main)
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

@@ -5,29 +5,15 @@ on:
branches: [ "tinygo" ]
pull_request:
branches: [ "tinygo" ]
workflow_dispatch:
jobs:
tinygo-build:
tinygo-build-all:
permissions:
contents: read
strategy:
matrix:
include:
- name: tinygo-default
target: ""
output: reticulum-go-tinygo
make_target: tinygo-build
- name: tinygo-wasm
target: wasm
output: reticulum-go.wasm
make_target: tinygo-wasm
runs-on: ubuntu-latest
outputs:
build_complete: ${{ steps.build_step.outcome == 'success' }}
steps:
- name: Checkout code
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -37,28 +23,64 @@ jobs:
with:
go-version: '1.24'
- name: Setup Task
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
with:
version: '3.46.3'
- name: Install TinyGo
run: |
wget https://github.com/tinygo-org/tinygo/releases/download/v0.37.0/tinygo_0.37.0_amd64.deb
sudo dpkg -i tinygo_0.37.0_amd64.deb
- name: Build with TinyGo
- name: Build for all TinyGo targets
id: build_step
run: |
make ${{ matrix.make_target }}
output_name="${{ matrix.output }}"
if [ -f "bin/${output_name}" ]; then
sha256sum "bin/${output_name}" | cut -d' ' -f1 > "bin/${output_name}.sha256"
echo "Built: ${output_name}"
echo "Generated checksum: bin/${output_name}.sha256"
else
echo "Build output not found: bin/${output_name}"
ls -la bin/
exit 1
fi
task tinygo-build-all || true
echo "Build process completed (some targets may have failed)"
- name: Upload Artifact
- name: Collect build results
run: |
mkdir -p artifacts
unsupported_file="artifacts/unsupported-microcontrollers.txt"
echo "# Unsupported Microcontrollers" > "$unsupported_file"
echo "# Generated: $(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> "$unsupported_file"
echo "" >> "$unsupported_file"
failed_count=0
for log_file in bin/build-*.log; do
if [ -f "$log_file" ]; then
target=$(basename "$log_file" | sed 's/build-\(.*\)\.log/\1/')
binary_file="bin/reticulum-go-${target}"
if [ ! -f "$binary_file" ] || grep -qi "error\|Error\|ERROR\|failed\|Failed\|FAILED" "$log_file"; then
failed_count=$((failed_count + 1))
echo "## $target" >> "$unsupported_file"
echo "" >> "$unsupported_file"
if grep -qi "program too large\|overflowed\|too big\|LLVM ERROR\|Error while" "$log_file"; then
grep -i "program too large\|overflowed\|too big\|LLVM ERROR\|Error while" "$log_file" | head -5 >> "$unsupported_file"
else
tail -15 "$log_file" >> "$unsupported_file"
fi
echo "" >> "$unsupported_file"
echo "\`\`\`" >> "$unsupported_file"
tail -30 "$log_file" >> "$unsupported_file"
echo "\`\`\`" >> "$unsupported_file"
echo "" >> "$unsupported_file"
fi
fi
done
echo "Total failed builds: $failed_count" >> "$unsupported_file"
echo "Generated unsupported-microcontrollers.txt with $failed_count failed targets"
- name: Upload build artifacts
uses: https://git.quad4.io/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5
with:
name: ${{ matrix.name }}
path: bin/${{ matrix.output }}*
name: tinygo-builds
path: |
bin/reticulum-go-*
artifacts/unsupported-microcontrollers.txt
if-no-files-found: warn

3
.gitignore vendored
View File

@@ -15,7 +15,8 @@ logs/
*.json
# Example files, not adding them just yet.
examples/
/examples/*
!/examples/wasm/
# OS / Editor files
.DS_Store # macOS Finder metadata

View File

@@ -1,4 +1,4 @@
Copyright (c) 2024-2025 Sudo-Ivan / Quad4.io
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.

156
Makefile
View File

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

View File

@@ -145,6 +145,7 @@ The project uses [Task](https://taskfile.dev/) for all development and build ope
| 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 |
@@ -179,6 +180,12 @@ Build WebAssembly binary with standard Go compiler:
task build-wasm
```
Run WebAssembly unit tests (requires Node.js):
```bash
task test-wasm
```
Build with TinyGo:
```bash

View File

@@ -1,4 +1,6 @@
version: '3'
env:
GOPRIVATE: git.quad4.io
vars:
GOCMD: go
@@ -75,8 +77,32 @@ tasks:
- gosec ./...
check:
desc: Run fmt-check, vet, and lint
deps: [fmt-check, vet, lint]
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
@@ -150,9 +176,63 @@ tasks:
- '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 Linux architectures
deps: [build-linux]
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
@@ -163,7 +243,37 @@ tasks:
desc: Build binary with TinyGo compiler
cmds:
- mkdir -p {{.BUILD_DIR}}
- tinygo build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-tinygo -size short {{.MAIN_PACKAGE}}
- tinygo build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-tinygo -size short -opt=z -gc=leaking -panic=trap {{.MAIN_PACKAGE}}
tinygo-build-debug:
desc: Build binary optimized for debugging with TinyGo compiler
cmds:
- mkdir -p {{.BUILD_DIR}}
- tinygo build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-tinygo-debug -size short -opt=1 {{.MAIN_PACKAGE}}
tinygo-build-all:
desc: Build for all available TinyGo targets in parallel
cmds:
- mkdir -p {{.BUILD_DIR}}
- echo "Building for all TinyGo targets in parallel..."
- |
targets=$(tinygo targets)
failed=0
# Use xargs to build in parallel, limited to number of CPU cores
echo "$targets" | xargs -P $(nproc) -I {} sh -c '
target="{}";
if [ -n "$target" ]; then
echo "Building for target: $target";
if ! tinygo build -target "$target" -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-"$target" -size short -opt=z -gc=leaking -panic=trap {{.MAIN_PACKAGE}} 2>&1 | tee {{.BUILD_DIR}}/build-"$target".log; then
echo "Failed to build for $target" >> {{.BUILD_DIR}}/build-"$target".log;
exit 1;
fi;
fi' || failed=1
echo "Build complete. Check {{.BUILD_DIR}}/ for outputs and logs."
if [ $failed -ne 0 ]; then
echo "Some target(s) failed to build. See logs in {{.BUILD_DIR}}/build-*.log"
fi
tinygo-wasm:
desc: Build WebAssembly binary with TinyGo compiler
@@ -171,20 +281,37 @@ tasks:
- 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}}
- 'GOOS=js GOARCH=wasm {{.GOCMD}} build -ldflags="-s -w" -o {{.BUILD_DIR}}/{{.BINARY_NAME}}.wasm ./cmd/reticulum-wasm'
- '{{.GOCMD}} build -ldflags="-s -w" -o {{.BUILD_DIR}}/{{.BINARY_NAME}}.wasm ./cmd/reticulum-wasm'
build-wasm-example:
example:wasm:build:
desc: Build WebAssembly example
env:
CGO_ENABLED: '0'
cmds:
- mkdir -p examples/wasm/public/static
- 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)
@@ -199,6 +326,19 @@ tasks:
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:
@@ -285,3 +425,289 @@ tasks:
exit 1
fi
cd examples/filetransfer && {{.GOCMD}} run . --destination="${DESTINATION}"
trivy:install:
desc: Install Trivy scanner
cmds:
- |
if ! command -v trivy &> /dev/null; then
curl -L -o /tmp/trivy.deb https://git.quad4.io/Quad4-Extra/assets/raw/commit/90fdcea1bb71d91df2de6ff2e3897f278413f300/bin/trivy_0.68.2_Linux-64bit.deb
sudo dpkg -i /tmp/trivy.deb || sudo apt-get install -f -y
else
echo "Trivy is already installed: $(trivy --version)"
fi
trivy:scan:
desc: Run Trivy vulnerability scan
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
trivy fs --scanners vuln --severity HIGH,CRITICAL --timeout 90m .
trivy:scan-all:
desc: Run Trivy full scan (vulnerabilities, secrets, misconfig)
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
trivy fs --scanners vuln,secret,misconfig .
sbom:
desc: Generate SBOM files (SPDX and CycloneDX formats)
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
mkdir -p sbom
trivy fs --format spdx-json --include-dev-deps --output sbom/sbom.spdx.json .
trivy fs --format cyclonedx --include-dev-deps --output sbom/sbom.cyclonedx.json .
echo "SBOM files generated in sbom/ directory"
sbom:spdx:
desc: Generate SPDX JSON SBOM
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
mkdir -p sbom
trivy fs --format spdx-json --include-dev-deps --output sbom/sbom.spdx.json .
echo "SPDX SBOM generated: sbom/sbom.spdx.json"
sbom:cyclonedx:
desc: Generate CycloneDX SBOM
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
mkdir -p sbom
trivy fs --format cyclonedx --include-dev-deps --output sbom/sbom.cyclonedx.json .
echo "CycloneDX SBOM generated: sbom/sbom.cyclonedx.json"
trivy:scan:json:
desc: Run Trivy vulnerability scan with JSON output
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
mkdir -p reports
trivy fs --scanners vuln --format json --output reports/trivy-vuln.json --timeout 90m .
trivy:scan:sarif:
desc: Run Trivy scan with SARIF output (for GitHub/GitLab integration)
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
mkdir -p reports
trivy fs --scanners vuln,secret --format sarif --output reports/trivy.sarif --timeout 90m .
trivy:scan:secrets:
desc: Scan for hardcoded secrets
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
trivy fs --scanners secret .
trivy:scan:licenses:
desc: Scan for licenses in dependencies
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
trivy fs --scanners license .
trivy:scan:misconfig:
desc: Scan for misconfigurations in config files
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
trivy fs --scanners misconfig .
trivy:db-update:
desc: Update Trivy vulnerability database
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
trivy image --download-db-only
trivy:cache-clean:
desc: Clean Trivy cache
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
trivy clean --cache
trivy:compliance:
desc: "Generate compliance report (specify COMPLIANCE env var: docker-bench-cis, k8s-nsa, etc.)"
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
if [ -z "${COMPLIANCE}" ]; then
echo "Error: COMPLIANCE environment variable required"
echo "Example: COMPLIANCE=docker-bench-cis task trivy:compliance"
exit 1
fi
mkdir -p reports
trivy fs --compliance "${COMPLIANCE}" --format json --output "reports/compliance-${COMPLIANCE}.json" .
trivy:ci:
desc: Run Trivy scan for CI (exits with non-zero code on findings)
cmds:
- |
if ! command -v trivy &> /dev/null; then
echo "Error: Trivy not found. Run 'task trivy:install' first."
exit 1
fi
trivy fs --scanners vuln --severity HIGH,CRITICAL --exit-code 1 --timeout 90m .
docker:build:
desc: Build Docker image (runtime image)
vars:
IMAGE_NAME: reticulum-go
IMAGE_TAG: latest
cmds:
- docker build -f docker/Dockerfile -t {{.IMAGE_NAME}}:{{.IMAGE_TAG}} .
docker:build:tag:
desc: Build Docker image with custom tag (use IMAGE_TAG env var)
vars:
IMAGE_NAME: reticulum-go
IMAGE_TAG: ${IMAGE_TAG:-latest}
cmds:
- docker build -f docker/Dockerfile -t {{.IMAGE_NAME}}:{{.IMAGE_TAG}} .
docker:build:build:
desc: Build Docker image for building binaries only
vars:
IMAGE_NAME: reticulum-go-build
IMAGE_TAG: latest
cmds:
- docker build -f docker/Dockerfile.build -t {{.IMAGE_NAME}}:{{.IMAGE_TAG}} .
docker:run:
desc: Run Docker container (runtime image)
vars:
IMAGE_NAME: reticulum-go
IMAGE_TAG: latest
CONTAINER_NAME: reticulum-go
cmds:
- |
docker run --rm -it \
--name {{.CONTAINER_NAME}} \
-p 4242:4242 \
{{.IMAGE_NAME}}:{{.IMAGE_TAG}}
docker:run:detached:
desc: Run Docker container in detached mode
vars:
IMAGE_NAME: reticulum-go
IMAGE_TAG: latest
CONTAINER_NAME: reticulum-go
cmds:
- |
docker run -d \
--name {{.CONTAINER_NAME}} \
-p 4242:4242 \
{{.IMAGE_NAME}}:{{.IMAGE_TAG}}
docker:stop:
desc: Stop running Docker container
vars:
CONTAINER_NAME: reticulum-go
cmds:
- docker stop {{.CONTAINER_NAME}} || true
- docker rm {{.CONTAINER_NAME}} || true
docker:extract:
desc: Extract binary from build container
vars:
IMAGE_NAME: reticulum-go-build
IMAGE_TAG: latest
BINARY_NAME: reticulum-go
cmds:
- |
CONTAINER_ID=$(docker create {{.IMAGE_NAME}}:{{.IMAGE_TAG}})
docker cp $CONTAINER_ID:/dist/{{.BINARY_NAME}} {{.BUILD_DIR}}/{{.BINARY_NAME}}
docker rm $CONTAINER_ID
echo "Binary extracted to {{.BUILD_DIR}}/{{.BINARY_NAME}}"
docker:buildx:setup:
desc: Setup Docker buildx for multi-platform builds
cmds:
- docker buildx create --name reticulum-builder --use || docker buildx use reticulum-builder
- docker buildx inspect --bootstrap
docker:buildx:build:
desc: Build multi-platform Docker image
vars:
IMAGE_NAME: reticulum-go
IMAGE_TAG: latest
PLATFORMS: linux/amd64,linux/arm64,linux/arm/v7
cmds:
- |
docker buildx build \
--platform {{.PLATFORMS}} \
-f docker/Dockerfile \
-t {{.IMAGE_NAME}}:{{.IMAGE_TAG}} \
--load \
.
docker:buildx:build:push:
desc: Build and push multi-platform Docker image
vars:
IMAGE_NAME: reticulum-go
IMAGE_TAG: latest
PLATFORMS: linux/amd64,linux/arm64,linux/arm/v7
cmds:
- |
if [ -z "${DOCKER_REGISTRY}" ]; then
echo "Error: DOCKER_REGISTRY environment variable required"
echo "Example: DOCKER_REGISTRY=registry.example.com task docker:buildx:build:push"
exit 1
fi
docker buildx build \
--platform {{.PLATFORMS}} \
-f docker/Dockerfile \
-t ${DOCKER_REGISTRY}/{{.IMAGE_NAME}}:{{.IMAGE_TAG}} \
--push \
.
docker:clean:
desc: Clean Docker images and containers
cmds:
- docker stop reticulum-go || true
- docker rm reticulum-go || true
- docker rmi reticulum-go:latest || true
- docker rmi reticulum-go-build:latest || true

View File

@@ -1,3 +1,5 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package main
import (
@@ -193,6 +195,46 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
)
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)
}
case "SerialInterface":
iface, err = interfaces.NewSerialInterface(
name,
ifaceConfig.Interface,
uint32(ifaceConfig.Bitrate), // #nosec G115
ifaceConfig.Enabled,
)
case "RNodeInterface":
// RNode usually runs over Serial
serial, sErr := interfaces.NewSerialInterface(
name+"_serial",
ifaceConfig.Interface,
uint32(ifaceConfig.Bitrate), // #nosec G115
ifaceConfig.Enabled,
)
if sErr != nil {
err = sErr
} else {
iface, err = interfaces.NewRNodeInterface(
name,
serial,
ifaceConfig.Frequency,
ifaceConfig.Bandwidth,
ifaceConfig.SF,
ifaceConfig.CR,
ifaceConfig.TXPower,
)
}
default:
debug.Log(debug.DEBUG_CRITICAL, "Unknown interface type", common.STR_TYPE, ifaceConfig.Type)
continue
@@ -289,34 +331,6 @@ func main() {
}
debug.Log(debug.DEBUG_ERROR, "Configuration loaded", "path", cfg.ConfigPath)
if len(cfg.Interfaces) == 0 {
debug.Log(debug.DEBUG_ERROR, "No interfaces configured, adding default interfaces")
cfg.Interfaces = make(map[string]*common.InterfaceConfig)
// Auto interface for local discovery
cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{
Type: "AutoInterface",
Enabled: true,
Name: "Auto Discovery",
}
cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
Type: common.STR_TCP_CLIENT,
Enabled: false,
TargetHost: "127.0.0.1",
TargetPort: common.NUM_4242,
Name: "Go-RNS-Testnet",
}
cfg.Interfaces["Quad4 TCP"] = &common.InterfaceConfig{
Type: common.STR_TCP_CLIENT,
Enabled: true,
TargetHost: "rns2.quad4.io",
TargetPort: common.NUM_4242,
Name: "Quad4 TCP",
}
}
r, err := NewReticulum(cfg)
if err != nil {
debug.GetLogger().Error("Failed to create Reticulum instance", common.STR_ERROR, err)
@@ -542,10 +556,10 @@ func (h *AnnounceHandler) AspectFilter() []string {
return h.aspectFilter
}
func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appData []byte) error {
debug.Log(debug.DEBUG_INFO, "Received announce", "hash", fmt.Sprintf("%x", destHash))
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))
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
@@ -639,7 +653,6 @@ func (r *Reticulum) createNodeAppData() []byte {
}
// Element 1: Int32 timestamp (current time)
// Update the timestamp when creating new announcements
r.nodeTimestamp = time.Now().Unix()
appData = append(appData, common.HEX_0xD2) // int32 format
timeBytes := make([]byte, common.FOUR)

View File

@@ -1,3 +1,5 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build js && wasm
// +build js,wasm
@@ -11,6 +13,12 @@ import (
)
func main() {
run()
// Keep the Go program running
select {}
}
func run() {
debug.Init()
debug.SetDebugLevel(debug.DEBUG_INFO)
@@ -18,8 +26,5 @@ func main() {
// Notify JS that reticulum is ready
js.Global().Call("reticulumReady")
// Keep the Go program running
select {}
}

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

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

View File

@@ -5,10 +5,13 @@ ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
RUN apk add --no-cache git
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

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;
};
}
}
})();

View File

Binary file not shown.

View File

@@ -13,7 +13,7 @@
inherit system;
};
go = pkgs.go_1_24;
go = pkgs.go_1_25;
in
{
devShells.default = pkgs.mkShell {
@@ -23,6 +23,7 @@
revive
gosec
gnumake
tinygo
];
shellHook = ''
@@ -31,6 +32,7 @@
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')"
'';
};

4
go.mod
View File

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

14
go.sum
View File

@@ -1,14 +1,4 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/shamaton/msgpack/v2 v2.4.0 h1:O5Z08MRmbo0lA9o2xnQ4TXx6teJbPqEurqcCOQ8Oi/4=
github.com/shamaton/msgpack/v2 v2.4.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,3 +1,5 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package config
import (
@@ -211,7 +213,6 @@ func CreateDefaultConfig(path string) error {
cfg := DefaultConfig()
cfg.ConfigPath = path
// Add Auto Interface
cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{
Type: "AutoInterface",
Enabled: true,
@@ -221,7 +222,6 @@ func CreateDefaultConfig(path string) error {
DataPort: 42671,
}
// Add default interfaces
cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
Type: "TCPClientInterface",
Enabled: true,

View File

@@ -1,3 +1,5 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package storage
import (
@@ -8,8 +10,8 @@ import (
"sync"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
"github.com/vmihailenco/msgpack/v5"
)
type Manager struct {
@@ -86,7 +88,7 @@ func (m *Manager) SaveRatchet(identityHash []byte, ratchetKey []byte) error {
Received: time.Now().Unix(),
}
data, err := msgpack.Marshal(ratchetData)
data, err := common.MsgpackMarshal(ratchetData)
if err != nil {
return fmt.Errorf("failed to marshal ratchet data: %w", err)
}
@@ -144,7 +146,7 @@ func (m *Manager) LoadRatchets(identityHash []byte) (map[string][]byte, error) {
}
var ratchetData RatchetData
if err := msgpack.Unmarshal(data, &ratchetData); err != nil {
if err := common.MsgpackUnmarshal(data, &ratchetData); err != nil {
debug.Log(debug.DEBUG_ERROR, "Corrupted ratchet data", "file", entry.Name(), "error", err)
_ = os.Remove(filePath)
continue

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,3 +1,5 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package announce
import (
@@ -49,12 +51,6 @@ const (
MAX_RETRIES = 3
)
type AnnounceHandler interface {
AspectFilter() []string
ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte) error
ReceivePathResponses() bool
}
type Announce struct {
mutex *sync.RWMutex
destinationHash []byte
@@ -67,7 +63,7 @@ type Announce struct {
signature []byte
pathResponse bool
retries int
handlers []AnnounceHandler
handlers []Handler
ratchetID []byte
packet []byte
hash []byte
@@ -97,7 +93,7 @@ func New(dest *identity.Identity, destinationHash []byte, destinationName string
timestamp: time.Now().Unix(),
pathResponse: pathResponse,
retries: 0,
handlers: make([]AnnounceHandler, 0),
handlers: make([]Handler, 0),
}
// Get current ratchet ID if enabled
@@ -156,13 +152,13 @@ func (a *Announce) Propagate(interfaces []common.NetworkInterface) error {
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 {
@@ -283,7 +279,7 @@ func (a *Announce) HandleAnnounce(data []byte) error {
// Process with handlers
for _, handler := range a.handlers {
if handler.ReceivePathResponses() || !a.pathResponse {
if err := handler.ReceivedAnnounce(destHash, announcedIdentity, appData); err != nil {
if err := handler.ReceivedAnnounce(destHash, announcedIdentity, appData, hopCount); err != nil {
return err
}
}
@@ -480,7 +476,7 @@ func NewAnnounce(identity *identity.Identity, destinationHash []byte, appData []
destinationHash: destHash,
hops: 0,
mutex: &sync.RWMutex{},
handlers: make([]AnnounceHandler, 0),
handlers: make([]Handler, 0),
config: config,
}

View File

@@ -17,7 +17,7 @@ func (m *mockAnnounceHandler) AspectFilter() []string {
return nil
}
func (m *mockAnnounceHandler) ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte) error {
func (m *mockAnnounceHandler) ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte, hops uint8) error {
m.received = true
return nil
}

View File

@@ -1,7 +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) error
ReceivedAnnounce(destHash []byte, identity interface{}, appData []byte, hops uint8) error
ReceivePathResponses() bool
}

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package common
import (
@@ -38,6 +40,11 @@ type InterfaceConfig struct {
DiscoveryScope string
DiscoveryPort int
DataPort int
Frequency uint32
Bandwidth uint32
SF uint8
CR uint8
TXPower uint8
}
// ReticulumConfig represents the main configuration structure

View File

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

View File

@@ -1,3 +1,5 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package common
import (
@@ -181,12 +183,10 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
packet = append(packet, 0x02) // Link packet type
packet = append(packet, dest...)
// Add timestamp
ts := make([]byte, 8)
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) // #nosec G115
packet = append(packet, ts...)
// Add data
packet = append(packet, data...)
return i.Send(packet, "")

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

@@ -0,0 +1,17 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package common
import (
"github.com/shamaton/msgpack/v2"
)
// Marshal returns the MessagePack encoding of v.
func MsgpackMarshal(v interface{}) ([]byte, error) {
return msgpack.Marshal(v)
}
// Unmarshal parses the MessagePack-encoded data and stores the result in the value pointed to by v.
func MsgpackUnmarshal(data []byte, v interface{}) error {
return msgpack.Unmarshal(data, v)
}

View File

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

View File

@@ -1,3 +1,5 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package config
import (
@@ -223,7 +225,6 @@ func InitConfig() (*Config, error) {
cfg.Logging.Level = "info"
cfg.Logging.File = filepath.Join(GetConfigDir(), "reticulum.log")
// Add default interfaces
cfg.Interfaces = append(cfg.Interfaces, struct {
Name string
Type string

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package destination
import (
@@ -15,7 +17,6 @@ import (
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
"github.com/vmihailenco/msgpack/v5"
"golang.org/x/crypto/curve25519"
)
@@ -605,7 +606,7 @@ func (d *Destination) persistRatchets() error {
debug.Log(debug.DEBUG_PACKETS, "Persisting ratchets", "count", len(d.ratchets), "path", d.ratchetPath)
// Pack ratchets using msgpack
packedRatchets, err := msgpack.Marshal(d.ratchets)
packedRatchets, err := common.MsgpackMarshal(d.ratchets)
if err != nil {
return fmt.Errorf("failed to pack ratchets: %w", err)
}
@@ -623,7 +624,7 @@ func (d *Destination) persistRatchets() error {
}
// Pack the entire structure
finalData, err := msgpack.Marshal(persistedData)
finalData, err := common.MsgpackMarshal(persistedData)
if err != nil {
return fmt.Errorf("failed to pack ratchet data: %w", err)
}
@@ -686,7 +687,7 @@ func (d *Destination) reloadRatchets() error {
// Unpack outer structure
var persistedData map[string][]byte
if err := msgpack.Unmarshal(fileData, &persistedData); err != nil {
if err := common.MsgpackUnmarshal(fileData, &persistedData); err != nil {
return fmt.Errorf("failed to unpack ratchet data: %w", err)
}
@@ -703,7 +704,7 @@ func (d *Destination) reloadRatchets() error {
}
// Unpack ratchet list
if err := msgpack.Unmarshal(packedRatchets, &d.ratchets); err != nil {
if err := common.MsgpackUnmarshal(packedRatchets, &d.ratchets); err != nil {
return fmt.Errorf("failed to unpack ratchet list: %w", err)
}

View File

@@ -1,3 +1,5 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package identity
import (
@@ -18,7 +20,6 @@ import (
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/cryptography"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
"github.com/vmihailenco/msgpack/v5"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf"
)
@@ -56,9 +57,10 @@ type Identity struct {
}
var (
knownDestinations = make(map[string][]interface{})
knownRatchets = make(map[string][]byte)
ratchetPersistLock sync.Mutex
knownDestinations = make(map[string][]interface{})
knownDestinationsLock sync.RWMutex
knownRatchets = make(map[string][]byte)
ratchetPersistLock sync.Mutex
)
func New() (*Identity, error) {
@@ -189,12 +191,14 @@ func Remember(packet []byte, destHash []byte, publicKey []byte, appData []byte)
// Store destination data as [packet, destHash, identity, appData]
id := FromPublicKey(publicKey)
knownDestinationsLock.Lock()
knownDestinations[hashStr] = []interface{}{
packet,
destHash,
id,
appData,
}
knownDestinationsLock.Unlock()
}
func ValidateAnnounce(packet []byte, destHash []byte, publicKey []byte, signature []byte, appData []byte) bool {
@@ -251,7 +255,11 @@ func (i *Identity) String() string {
func Recall(hash []byte) (*Identity, error) {
hashStr := hex.EncodeToString(hash)
if data, exists := knownDestinations[hashStr]; exists {
knownDestinationsLock.RLock()
data, exists := knownDestinations[hashStr]
knownDestinationsLock.RUnlock()
if exists {
// data is [packet, destHash, identity, appData]
if len(data) >= 3 {
if id, ok := data[2].(*Identity); ok {
@@ -636,7 +644,6 @@ func (i *Identity) loadPrivateKey(privateKey, signingSeed []byte) error {
signingKey := ed25519.NewKeyFromSeed(i.signingSeed)
i.verificationKey = signingKey.Public().(ed25519.PublicKey)
// Update hash
publicKeyBytes := make([]byte, 0, len(i.publicKey)+len(i.verificationKey))
publicKeyBytes = append(publicKeyBytes, i.publicKey...)
publicKeyBytes = append(publicKeyBytes, i.verificationKey...)
@@ -664,7 +671,7 @@ func (i *Identity) saveRatchets(path string) error {
}
// Pack ratchets using msgpack
packedRatchets, err := msgpack.Marshal(ratchetList)
packedRatchets, err := common.MsgpackMarshal(ratchetList)
if err != nil {
return fmt.Errorf("failed to pack ratchets: %w", err)
}
@@ -679,7 +686,7 @@ func (i *Identity) saveRatchets(path string) error {
}
// Pack the entire structure
finalData, err := msgpack.Marshal(persistedData)
finalData, err := common.MsgpackMarshal(persistedData)
if err != nil {
return fmt.Errorf("failed to pack ratchet data: %w", err)
}
@@ -792,7 +799,7 @@ func (i *Identity) loadRatchets(path string) error {
// Unpack outer structure: {"signature": ..., "ratchets": ...}
var persistedData map[string][]byte
if err := msgpack.Unmarshal(fileData, &persistedData); err != nil {
if err := common.MsgpackUnmarshal(fileData, &persistedData); err != nil {
return fmt.Errorf("failed to unpack ratchet data: %w", err)
}
@@ -810,7 +817,7 @@ func (i *Identity) loadRatchets(path string) error {
// Unpack ratchet list
var ratchetList [][]byte
if err := msgpack.Unmarshal(packedRatchets, &ratchetList); err != nil {
if err := common.MsgpackUnmarshal(packedRatchets, &ratchetList); err != nil {
return fmt.Errorf("failed to unpack ratchet list: %w", err)
}
@@ -854,7 +861,10 @@ func (i *Identity) GetRatchetID(ratchetPubBytes []byte) []byte {
}
func GetKnownDestination(hash string) ([]interface{}, bool) {
if data, exists := knownDestinations[hash]; exists {
knownDestinationsLock.RLock()
data, exists := knownDestinations[hash]
knownDestinationsLock.RUnlock()
if exists {
return data, true
}
return nil, false

View File

@@ -1,3 +1,8 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build !tinygo
// +build !tinygo
package interfaces
import (
@@ -53,12 +58,13 @@ type AutoInterface struct {
timedOutInterfaces map[string]time.Time
allowedInterfaces []string
ignoredInterfaces []string
mutex sync.RWMutex
outboundConn *net.UDPConn
announceInterval time.Duration
peerJobInterval time.Duration
peeringTimeout time.Duration
mcastEchoTimeout time.Duration
done chan struct{}
stopOnce sync.Once
}
type AdoptedInterface struct {
@@ -135,6 +141,7 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
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)
@@ -170,6 +177,20 @@ func normalizeMulticastType(mtype string) string {
}
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)
@@ -186,7 +207,9 @@ func (ai *AutoInterface) Start() error {
continue
}
if err := ai.configureInterface(&iface); err != nil {
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
}
@@ -262,7 +285,7 @@ func (ai *AutoInterface) configureInterface(iface *net.Interface) error {
return fmt.Errorf("no link-local IPv6 address found")
}
ai.mutex.Lock()
ai.Mutex.Lock()
ai.adoptedInterfaces[iface.Name] = &AdoptedInterface{
name: iface.Name,
linkLocalAddr: linkLocalAddr,
@@ -270,7 +293,7 @@ func (ai *AutoInterface) configureInterface(iface *net.Interface) error {
}
ai.linkLocalAddrs = append(ai.linkLocalAddrs, linkLocalAddr)
ai.multicastEchoes[iface.Name] = time.Now()
ai.mutex.Unlock()
ai.Mutex.Unlock()
if err := ai.startDiscoveryListener(iface); err != nil {
return fmt.Errorf("failed to start discovery listener: %v", err)
@@ -296,13 +319,13 @@ func (ai *AutoInterface) startDiscoveryListener(iface *net.Interface) error {
return err
}
if err := conn.SetReadBuffer(1024); err != nil {
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.Mutex.Lock()
ai.discoveryServers[iface.Name] = conn
ai.mutex.Unlock()
ai.Mutex.Unlock()
go ai.handleDiscovery(conn, iface.Name)
debug.Log(debug.DEBUG_VERBOSE, "Discovery listener started", "interface", iface.Name, "addr", ai.mcastDiscoveryAddr)
@@ -331,9 +354,9 @@ func (ai *AutoInterface) startDataListener(iface *net.Interface) error {
debug.Log(debug.DEBUG_ERROR, "Failed to set data read buffer", "error", err)
}
ai.mutex.Lock()
ai.Mutex.Lock()
ai.interfaceServers[iface.Name] = conn
ai.mutex.Unlock()
ai.Mutex.Unlock()
go ai.handleData(conn, iface.Name)
debug.Log(debug.DEBUG_VERBOSE, "Data listener started", "interface", iface.Name, "addr", addr)
@@ -341,8 +364,18 @@ func (ai *AutoInterface) startDataListener(iface *net.Interface) error {
}
func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) {
buf := make([]byte, 1024)
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() {
@@ -365,6 +398,16 @@ func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) {
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() {
@@ -380,8 +423,8 @@ func (ai *AutoInterface) handleData(conn *net.UDPConn, ifaceName string) {
}
func (ai *AutoInterface) handlePeerAnnounce(addr *net.UDPAddr, ifaceName string) {
ai.mutex.Lock()
defer ai.mutex.Unlock()
ai.Mutex.Lock()
defer ai.Mutex.Unlock()
peerIP := addr.IP.String()
@@ -412,17 +455,22 @@ func (ai *AutoInterface) announceLoop() {
ticker := time.NewTicker(ai.announceInterval)
defer ticker.Stop()
for range ticker.C {
if !ai.IsOnline() {
for {
select {
case <-ticker.C:
if !ai.IsOnline() {
return
}
ai.sendPeerAnnounce()
case <-ai.done:
return
}
ai.sendPeerAnnounce()
}
}
func (ai *AutoInterface) sendPeerAnnounce() {
ai.mutex.RLock()
defer ai.mutex.RUnlock()
ai.Mutex.RLock()
defer ai.Mutex.RUnlock()
for ifaceName, adoptedIface := range ai.adoptedInterfaces {
mcastAddr := &net.UDPAddr{
@@ -452,33 +500,38 @@ func (ai *AutoInterface) peerJobs() {
ticker := time.NewTicker(ai.peerJobInterval)
defer ticker.Stop()
for range ticker.C {
if !ai.IsOnline() {
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
}
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()
}
}
@@ -487,8 +540,8 @@ func (ai *AutoInterface) Send(data []byte, address string) error {
return fmt.Errorf("interface offline")
}
ai.mutex.RLock()
defer ai.mutex.RUnlock()
ai.Mutex.RLock()
defer ai.Mutex.RUnlock()
if len(ai.peers) == 0 {
debug.Log(debug.DEBUG_TRACE, "No peers available for sending")
@@ -526,9 +579,7 @@ func (ai *AutoInterface) Send(data []byte, address string) error {
}
func (ai *AutoInterface) Stop() error {
ai.mutex.Lock()
defer ai.mutex.Unlock()
ai.Mutex.Lock()
ai.Online = false
ai.IN = false
ai.OUT = false
@@ -544,6 +595,13 @@ func (ai *AutoInterface) Stop() error {
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

View File

@@ -144,14 +144,14 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
for {
select {
case <-ticker.C:
ai.mutex.Lock()
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()
ai.Mutex.Unlock()
case <-done:
return
}
@@ -173,27 +173,26 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
peer2Addr := &net.UDPAddr{IP: net.ParseIP("fe80::2"), Zone: "eth0"}
localAddr := &net.UDPAddr{IP: net.ParseIP("fe80::aaaa"), Zone: "eth0"}
// Add a simulated local address to avoid adding it as a peer
ai.mutex.Lock()
ai.Mutex.Lock()
ai.linkLocalAddrs = append(ai.linkLocalAddrs, localAddrStr)
ai.mutex.Unlock()
ai.Mutex.Unlock()
t.Run("AddPeer1", func(t *testing.T) {
ai.mutex.Lock()
ai.Mutex.Lock()
ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
ai.mutex.Unlock()
ai.Mutex.Unlock()
// Give a small amount of time for the peer to be processed
time.Sleep(10 * time.Millisecond)
ai.mutex.RLock()
ai.Mutex.RLock()
count := len(ai.peers)
peer, exists := ai.peers[peer1AddrStr]
var ifaceName string
if exists {
ifaceName = peer.ifaceName
}
ai.mutex.RUnlock()
ai.Mutex.RUnlock()
if count != 1 {
t.Fatalf("Expected 1 peer, got %d", count)
@@ -207,17 +206,17 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
})
t.Run("AddPeer2", func(t *testing.T) {
ai.mutex.Lock()
ai.Mutex.Lock()
ai.mockHandlePeerAnnounce(peer2Addr, "eth0")
ai.mutex.Unlock()
ai.Mutex.Unlock()
// Give a small amount of time for the peer to be processed
time.Sleep(10 * time.Millisecond)
ai.mutex.RLock()
ai.Mutex.RLock()
count := len(ai.peers)
_, exists := ai.peers[peer2AddrStr]
ai.mutex.RUnlock()
ai.Mutex.RUnlock()
if count != 2 {
t.Fatalf("Expected 2 peers, got %d", count)
@@ -228,16 +227,16 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
})
t.Run("IgnoreLocalAnnounce", func(t *testing.T) {
ai.mutex.Lock()
ai.Mutex.Lock()
ai.mockHandlePeerAnnounce(localAddr, "eth0")
ai.mutex.Unlock()
ai.Mutex.Unlock()
// Give a small amount of time for the peer to be processed
time.Sleep(10 * time.Millisecond)
ai.mutex.RLock()
ai.Mutex.RLock()
count := len(ai.peers)
ai.mutex.RUnlock()
ai.Mutex.RUnlock()
if count != 2 {
t.Fatalf("Expected 2 peers after local announce, got %d", count)
@@ -245,32 +244,32 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
})
t.Run("UpdatePeerTimestamp", func(t *testing.T) {
ai.mutex.RLock()
ai.Mutex.RLock()
peer, exists := ai.peers[peer1AddrStr]
var initialTime time.Time
if exists {
initialTime = peer.lastHeard
}
ai.mutex.RUnlock()
ai.Mutex.RUnlock()
if !exists {
t.Fatalf("Peer %s not found before timestamp update", peer1AddrStr)
}
ai.mutex.Lock()
ai.Mutex.Lock()
ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
ai.mutex.Unlock()
ai.Mutex.Unlock()
// Give a small amount of time for the peer to be processed
time.Sleep(10 * time.Millisecond)
ai.mutex.RLock()
ai.Mutex.RLock()
peer, exists = ai.peers[peer1AddrStr]
var updatedTime time.Time
if exists {
updatedTime = peer.lastHeard
}
ai.mutex.RUnlock()
ai.Mutex.RUnlock()
if !exists {
t.Fatalf("Peer %s not found after timestamp update", peer1AddrStr)
@@ -285,9 +284,9 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
// Wait for peer timeout
time.Sleep(testTimeout * 2)
ai.mutex.RLock()
ai.Mutex.RLock()
count := len(ai.peers)
ai.mutex.RUnlock()
ai.Mutex.RUnlock()
if count != 0 {
t.Errorf("Expected all peers to timeout, got %d peers", count)

View File

@@ -0,0 +1,96 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build tinygo
// +build tinygo
package interfaces
import (
"fmt"
"net"
"sync"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
)
const (
HW_MTU = 1196
DEFAULT_DISCOVERY_PORT = 29716
DEFAULT_DATA_PORT = 42671
DEFAULT_GROUP_ID = "reticulum"
BITRATE_GUESS = 10 * 1000 * 1000
)
type AutoInterface struct {
BaseInterface
groupID []byte
discoveryPort int
dataPort int
discoveryScope string
peers map[string]*Peer
linkLocalAddrs []string
adoptedInterfaces map[string]string
interfaceServers map[string]net.Conn
multicastEchoes map[string]time.Time
mutex sync.RWMutex
outboundConn net.Conn
}
type Peer struct {
ifaceName string
lastHeard time.Time
conn net.PacketConn
}
func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterface, error) {
ai := &AutoInterface{
BaseInterface: BaseInterface{
Name: name,
Mode: common.IF_MODE_FULL,
Type: common.IF_TYPE_AUTO,
Online: false,
Enabled: config.Enabled,
Detached: false,
IN: true,
OUT: false,
MTU: HW_MTU,
Bitrate: BITRATE_GUESS,
},
discoveryPort: DEFAULT_DISCOVERY_PORT,
dataPort: DEFAULT_DATA_PORT,
peers: make(map[string]*Peer),
linkLocalAddrs: make([]string, 0),
adoptedInterfaces: make(map[string]string),
interfaceServers: make(map[string]net.Conn),
multicastEchoes: make(map[string]time.Time),
}
if config.Port != 0 {
ai.discoveryPort = config.Port
}
if config.GroupID != "" {
ai.groupID = []byte(config.GroupID)
} else {
ai.groupID = []byte("reticulum")
}
return ai, nil
}
func (ai *AutoInterface) Start() error {
// TinyGo doesn't support net.Interfaces() or multicast UDP
return fmt.Errorf("AutoInterface not supported in TinyGo - requires interface enumeration and multicast UDP")
}
func (ai *AutoInterface) Send(data []byte, address string) error {
return fmt.Errorf("Send not supported in TinyGo - requires UDP client connections")
}
func (ai *AutoInterface) Stop() error {
ai.Mutex.Lock()
defer ai.Mutex.Unlock()
ai.Online = false
return nil
}

View File

@@ -1,3 +1,5 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package interfaces
import (
@@ -67,8 +69,9 @@ type BaseInterface struct {
TxBytes uint64
RxBytes uint64
lastTx time.Time
lastRx time.Time
mutex sync.RWMutex
Mutex sync.RWMutex
packetCallback common.PacketCallback
}
@@ -85,29 +88,30 @@ func NewBaseInterface(name string, ifType common.InterfaceType, enabled bool) Ba
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()
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.packetCallback
}
func (i *BaseInterface) ProcessIncoming(data []byte) {
i.mutex.Lock()
i.Mutex.Lock()
i.RxBytes += uint64(len(data))
i.mutex.Unlock()
i.Mutex.Unlock()
i.mutex.RLock()
i.Mutex.RLock()
callback := i.packetCallback
i.mutex.RUnlock()
i.Mutex.RUnlock()
if callback != nil {
callback(data, i)
@@ -120,9 +124,9 @@ func (i *BaseInterface) ProcessOutgoing(data []byte) error {
return fmt.Errorf("interface offline or detached")
}
i.mutex.Lock()
i.Mutex.Lock()
i.TxBytes += uint64(len(data))
i.mutex.Unlock()
i.Mutex.Unlock()
debug.Log(debug.DEBUG_VERBOSE, "Interface processed outgoing packet", "name", i.Name, "bytes", len(data), "total_tx", i.TxBytes)
return nil
@@ -134,7 +138,7 @@ func (i *BaseInterface) SendPathRequest(packet []byte) error {
}
frame := make([]byte, 0, len(packet)+1)
frame = append(frame, 0x01)
frame = append(frame, common.HEX_0x01)
frame = append(frame, packet...)
return i.ProcessOutgoing(frame)
@@ -146,7 +150,7 @@ 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)
@@ -158,21 +162,21 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
}
func (i *BaseInterface) Detach() {
i.mutex.Lock()
defer i.mutex.Unlock()
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()
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()
i.Mutex.Lock()
defer i.Mutex.Unlock()
prevState := i.Enabled
i.Enabled = true
@@ -182,8 +186,8 @@ func (i *BaseInterface) Enable() {
}
func (i *BaseInterface) Disable() {
i.mutex.Lock()
defer i.mutex.Unlock()
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)
@@ -206,14 +210,14 @@ func (i *BaseInterface) GetMTU() int {
}
func (i *BaseInterface) IsOnline() bool {
i.mutex.RLock()
defer i.mutex.RUnlock()
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.Online
}
func (i *BaseInterface) IsDetached() bool {
i.mutex.RLock()
defer i.mutex.RUnlock()
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.Detached
}
@@ -243,8 +247,8 @@ func (i *BaseInterface) GetConn() net.Conn {
}
func (i *BaseInterface) GetBandwidthAvailable() bool {
i.mutex.RLock()
defer i.mutex.RUnlock()
i.Mutex.RLock()
defer i.Mutex.RUnlock()
now := time.Now()
timeSinceLastTx := now.Sub(i.lastTx)
@@ -265,8 +269,8 @@ func (i *BaseInterface) GetBandwidthAvailable() bool {
}
func (i *BaseInterface) updateBandwidthStats(bytes uint64) {
i.mutex.Lock()
defer i.mutex.Unlock()
i.Mutex.Lock()
defer i.Mutex.Unlock()
i.TxBytes += bytes
i.lastTx = time.Now()

View File

@@ -183,7 +183,6 @@ func (m *mockInterface) Send(data []byte, addr string) error {
return nil
}
// Add other methods to satisfy the Interface interface (can be minimal/panic)
func (m *mockInterface) GetType() common.InterfaceType { return common.IF_TYPE_NONE }
func (m *mockInterface) GetMode() common.InterfaceMode { return common.IF_MODE_FULL }
func (m *mockInterface) ProcessIncoming(data []byte) {}

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

@@ -0,0 +1,24 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package interfaces
const (
KISS_FEND = 0xC0
KISS_FESC = 0xDB
KISS_TFEND = 0xDC
KISS_TFESC = 0xDD
)
func escapeKISS(data []byte) []byte {
escaped := make([]byte, 0, len(data)*2)
for _, b := range data {
if b == KISS_FEND {
escaped = append(escaped, KISS_FESC, KISS_TFEND)
} else if b == KISS_FESC {
escaped = append(escaped, KISS_FESC, KISS_TFESC)
} else {
escaped = append(escaped, b)
}
}
return escaped
}

View File

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build !tinygo
package interfaces
import (
"fmt"
)
type LoRaInterface struct {
BaseInterface
}
func NewLoRaInterface(name string, spi interface{}, cs, reset, dio0 interface{}, freq uint32, bw uint32, sf uint8, cr uint8, enabled bool) (*LoRaInterface, error) {
return nil, fmt.Errorf("LoRaInterface is only supported on TinyGo targets currently")
}
func (li *LoRaInterface) Start() error {
return fmt.Errorf("LoRaInterface is only supported on TinyGo targets currently")
}
func (li *LoRaInterface) Stop() error {
return nil
}
func (li *LoRaInterface) Send(data []byte, address string) error {
return fmt.Errorf("LoRaInterface is only supported on TinyGo targets currently")
}

View File

@@ -0,0 +1,292 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build tinygo
package interfaces
import (
"fmt"
"machine"
"sync"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
)
const (
REG_FIFO = 0x00
REG_OP_MODE = 0x01
REG_FRF_MSB = 0x06
REG_FRF_MID = 0x07
REG_FRF_LSB = 0x08
REG_PA_CONFIG = 0x09
REG_FIFO_ADDR_PTR = 0x0D
REG_FIFO_TX_BASE_ADDR = 0x0E
REG_FIFO_RX_BASE_ADDR = 0x0F
REG_FIFO_RX_CURRENT_ADDR = 0x10
REG_IRQ_FLAGS = 0x12
REG_RX_NB_BYTES = 0x13
REG_MODEM_CONFIG_1 = 0x1D
REG_MODEM_CONFIG_2 = 0x1E
REG_PREAMBLE_MSB = 0x20
REG_PREAMBLE_LSB = 0x21
REG_PAYLOAD_LENGTH = 0x22
REG_MODEM_CONFIG_3 = 0x26
REG_RSSI_WIDEBAND = 0x2C
REG_DETECTION_OPTIMIZE = 0x31
REG_DETECTION_THRESHOLD = 0x37
REG_SYNC_WORD = 0x39
REG_DIO_MAPPING_1 = 0x40
REG_VERSION = 0x42
MODE_LONG_RANGE_MODE = 0x80
MODE_SLEEP = 0x00
MODE_STDBY = 0x01
MODE_TX = 0x03
MODE_RX_CONTINUOUS = 0x05
IRQ_RX_DONE_MASK = 0x40
IRQ_PAYLOAD_CRC_ERROR_MASK = 0x20
IRQ_TX_DONE_MASK = 0x08
MAX_PKT_LENGTH = 255
)
// LoRaInterface provides a TinyGo SPI-based LoRa interface for SX127x.
type LoRaInterface struct {
BaseInterface
spi machine.SPI
cs machine.Pin
reset machine.Pin
dio0 machine.Pin
freq uint32
bw uint32
sf uint8
cr uint8
txPower uint8
done chan struct{}
stopOnce sync.Once
}
// NewLoRaInterface initializes a new LoRaInterface.
func NewLoRaInterface(name string, spi machine.SPI, cs, reset, dio0 machine.Pin, freq uint32, bw uint32, sf uint8, cr uint8, enabled bool) (*LoRaInterface, error) {
li := &LoRaInterface{
BaseInterface: NewBaseInterface(name, common.IF_TYPE_SERIAL, enabled),
spi: spi,
cs: cs,
reset: reset,
dio0: dio0,
freq: freq,
bw: bw,
sf: sf,
cr: cr,
txPower: 17,
done: make(chan struct{}),
}
li.MTU = MAX_PKT_LENGTH
li.Bitrate = int64(bw * uint32(sf) / (1 << (sf - 1)))
if enabled {
err := li.Start()
if err != nil {
return nil, err
}
}
return li, nil
}
// Start configures and brings the LoRaInterface online.
func (li *LoRaInterface) Start() error {
li.Mutex.Lock()
defer li.Mutex.Unlock()
if li.Online {
return nil
}
li.cs.Configure(machine.PinConfig{Mode: machine.PinOutput})
li.cs.High()
li.reset.Configure(machine.PinConfig{Mode: machine.PinOutput})
li.dio0.Configure(machine.PinConfig{Mode: machine.PinInput})
li.reset.Low()
time.Sleep(10 * time.Millisecond)
li.reset.High()
time.Sleep(10 * time.Millisecond)
version := li.readReg(REG_VERSION)
if version != 0x12 {
return fmt.Errorf("LoRa chip not found, version: 0x%02x", version)
}
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_SLEEP)
time.Sleep(10 * time.Millisecond)
frf := uint64(li.freq) << 19 / 32000000
li.writeReg(REG_FRF_MSB, uint8(frf>>16))
li.writeReg(REG_FRF_MID, uint8(frf>>8))
li.writeReg(REG_FRF_LSB, uint8(frf))
li.writeReg(REG_FIFO_TX_BASE_ADDR, 0)
li.writeReg(REG_FIFO_RX_BASE_ADDR, 0)
li.writeReg(0x0C, 0x23)
li.writeReg(REG_MODEM_CONFIG_3, 0x04)
li.writeReg(REG_PA_CONFIG, 0x80|(li.txPower-2))
var bwVal uint8
switch li.bw {
case 125000:
bwVal = 7
case 250000:
bwVal = 8
case 500000:
bwVal = 9
default:
bwVal = 7
}
li.writeReg(REG_MODEM_CONFIG_1, (bwVal<<4)|(li.cr-4)<<1|0x00)
li.writeReg(REG_MODEM_CONFIG_2, (li.sf<<4)|0x04)
li.writeReg(REG_SYNC_WORD, 0x12)
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_STDBY)
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_RX_CONTINUOUS)
li.Online = true
li.Enabled = true
go li.readLoop()
return nil
}
// readReg reads a byte from the given register.
func (li *LoRaInterface) readReg(reg uint8) uint8 {
li.cs.Low()
li.spi.Transfer(reg & 0x7F)
val, _ := li.spi.Transfer(0)
li.cs.High()
return val
}
// writeReg writes a byte to the given register.
func (li *LoRaInterface) writeReg(reg uint8, val uint8) {
li.cs.Low()
li.spi.Transfer(reg | 0x80)
li.spi.Transfer(val)
li.cs.High()
}
// readLoop polls the radio for received packets and dispatches them.
func (li *LoRaInterface) readLoop() {
for {
li.Mutex.RLock()
online := li.Online
done := li.done
li.Mutex.RUnlock()
if !online {
return
}
select {
case <-done:
return
default:
}
irq := li.readReg(REG_IRQ_FLAGS)
if irq&IRQ_RX_DONE_MASK != 0 {
li.writeReg(REG_IRQ_FLAGS, IRQ_RX_DONE_MASK)
if irq&IRQ_PAYLOAD_CRC_ERROR_MASK == 0 {
currentAddr := li.readReg(REG_FIFO_RX_CURRENT_ADDR)
li.writeReg(REG_FIFO_ADDR_PTR, currentAddr)
count := li.readReg(REG_RX_NB_BYTES)
packet := make([]byte, count)
li.cs.Low()
li.spi.Transfer(REG_FIFO)
for i := uint8(0); i < count; i++ {
packet[i], _ = li.spi.Transfer(0)
}
li.cs.High()
li.ProcessIncoming(packet)
}
}
time.Sleep(10 * time.Millisecond)
}
}
// Send transmits a packet over LoRa.
func (li *LoRaInterface) Send(data []byte, address string) error {
return li.ProcessOutgoing(data)
}
// ProcessOutgoing encodes and sends a packet.
func (li *LoRaInterface) ProcessOutgoing(data []byte) error {
li.Mutex.Lock()
defer li.Mutex.Unlock()
if !li.Online {
return fmt.Errorf("interface offline")
}
if len(data) > MAX_PKT_LENGTH {
return fmt.Errorf("packet too long for LoRa: %d", len(data))
}
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_STDBY)
li.writeReg(REG_FIFO_ADDR_PTR, 0)
li.cs.Low()
li.spi.Transfer(REG_FIFO | 0x80)
for _, b := range data {
li.spi.Transfer(b)
}
li.cs.High()
li.writeReg(REG_PAYLOAD_LENGTH, uint8(len(data)))
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_TX)
start := time.Now()
for {
if li.readReg(REG_IRQ_FLAGS)&IRQ_TX_DONE_MASK != 0 {
li.writeReg(REG_IRQ_FLAGS, IRQ_TX_DONE_MASK)
break
}
if time.Since(start) > 2*time.Second {
debug.Log(debug.DEBUG_ERROR, "LoRa TX timeout")
break
}
time.Sleep(1 * time.Millisecond)
}
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_RX_CONTINUOUS)
li.TxBytes += uint64(len(data))
li.lastTx = time.Now()
return nil
}
// Stop disables the LoRaInterface.
func (li *LoRaInterface) Stop() error {
li.Mutex.Lock()
li.Online = false
li.Enabled = false
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_SLEEP)
li.Mutex.Unlock()
li.stopOnce.Do(func() {
if li.done != nil {
close(li.done)
}
})
return nil
}

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

@@ -0,0 +1,262 @@
// SPDX-License-Identifier: 0BSD
package interfaces
import (
"encoding/binary"
"fmt"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
)
const (
RNODE_CMD_DATA = 0x00
RNODE_CMD_FREQUENCY = 0x01
RNODE_CMD_BANDWIDTH = 0x02
RNODE_CMD_TXPOWER = 0x03
RNODE_CMD_SF = 0x04
RNODE_CMD_CR = 0x05
RNODE_CMD_RADIO_STATE = 0x06
RNODE_CMD_RADIO_LOCK = 0x07
RNODE_CMD_DETECT = 0x08
RNODE_CMD_LEAVE = 0x0A
RNODE_CMD_ST_ALOCK = 0x0B
RNODE_CMD_LT_ALOCK = 0x0C
RNODE_CMD_READY = 0x0F
RNODE_CMD_STAT_RX = 0x21
RNODE_CMD_STAT_TX = 0x22
RNODE_CMD_STAT_RSSI = 0x23
RNODE_CMD_STAT_SNR = 0x24
RNODE_CMD_FW_VERSION = 0x50
RNODE_CMD_PLATFORM = 0x48
RNODE_CMD_MCU = 0x49
RNODE_DETECT_REQ = 0x73
RNODE_DETECT_RESP = 0x46
RNODE_RSSI_OFFSET = 157
)
// RNodeInterface represents a Reticulum node interface.
type RNodeInterface struct {
Interface
frequency uint32
bandwidth uint32
sf uint8
cr uint8
txPower uint8
callback common.PacketCallback
rFrequency uint32
rBandwidth uint32
rTXPower uint8
rSF uint8
rCR uint8
rState uint8
rDetected bool
rMajVer uint8
rMinVer uint8
interfaceReady bool
packetQueue [][]byte
}
// NewRNodeInterface creates a new RNodeInterface.
func NewRNodeInterface(name string, underlying Interface, freq uint32, bw uint32, sf uint8, cr uint8, txPower uint8) (*RNodeInterface, error) {
ri := &RNodeInterface{
Interface: underlying,
frequency: freq,
bandwidth: bw,
sf: sf,
cr: cr,
txPower: txPower,
}
underlying.SetPacketCallback(ri.handleIncoming)
return ri, nil
}
// SetPacketCallback sets the packet callback for the RNodeInterface.
func (ri *RNodeInterface) SetPacketCallback(cb common.PacketCallback) {
ri.callback = cb
}
func (ri *RNodeInterface) handleIncoming(data []byte, ni common.NetworkInterface) {
if len(data) < 1 {
return
}
cmd := data[0]
payload := data[1:]
switch cmd {
case RNODE_CMD_DATA:
if ri.callback != nil {
ri.callback(payload, ri)
}
case RNODE_CMD_READY:
ri.processQueue()
case RNODE_CMD_DETECT:
if len(payload) >= 1 && payload[0] == RNODE_DETECT_RESP {
ri.rDetected = true
}
case RNODE_CMD_FW_VERSION:
if len(payload) >= 2 {
ri.rMajVer = payload[0]
ri.rMinVer = payload[1]
debug.Log(debug.DEBUG_INFO, "RNode firmware version", "name", ri.GetName(), "version", fmt.Sprintf("%d.%d", ri.rMajVer, ri.rMinVer))
}
case RNODE_CMD_FREQUENCY:
if len(payload) >= 4 {
ri.rFrequency = binary.BigEndian.Uint32(payload)
}
case RNODE_CMD_BANDWIDTH:
if len(payload) >= 4 {
ri.rBandwidth = binary.BigEndian.Uint32(payload)
}
case RNODE_CMD_TXPOWER:
if len(payload) >= 1 {
ri.rTXPower = payload[0]
}
case RNODE_CMD_SF:
if len(payload) >= 1 {
ri.rSF = payload[0]
}
case RNODE_CMD_CR:
if len(payload) >= 1 {
ri.rCR = payload[0]
}
case RNODE_CMD_RADIO_STATE:
if len(payload) >= 1 {
ri.rState = payload[0]
}
case RNODE_CMD_STAT_RSSI:
if len(payload) >= 1 {
rssi := int(payload[0]) - RNODE_RSSI_OFFSET
debug.Log(debug.DEBUG_VERBOSE, "RNode RSSI", "name", ri.GetName(), "rssi", rssi)
}
case RNODE_CMD_STAT_SNR:
if len(payload) >= 1 {
snr := float32(int8(payload[0])) * 0.25
debug.Log(debug.DEBUG_VERBOSE, "RNode SNR", "name", ri.GetName(), "snr", snr)
}
default:
debug.Log(debug.DEBUG_ALL, "RNode received command", "cmd", fmt.Sprintf("0x%02x", cmd), "len", len(payload))
}
}
func (ri *RNodeInterface) processQueue() {
ri.interfaceReady = true
if len(ri.packetQueue) > 0 {
packet := ri.packetQueue[0]
ri.packetQueue = ri.packetQueue[1:]
_ = ri.Send(packet, "")
}
}
// Start initializes the RNodeInterface and configures device parameters.
func (ri *RNodeInterface) Start() error {
err := ri.Interface.Start()
if err != nil {
return err
}
time.Sleep(2 * time.Second)
if err := ri.detect(); err != nil {
return err
}
debug.Log(debug.DEBUG_INFO, "Initializing RNode...", "name", ri.GetName())
if ri.frequency != 0 {
freqBytes := make([]byte, 4)
binary.BigEndian.PutUint32(freqBytes, ri.frequency)
if err := ri.sendRNodeCommand(RNODE_CMD_FREQUENCY, freqBytes); err != nil {
return err
}
}
if ri.bandwidth != 0 {
bwBytes := make([]byte, 4)
binary.BigEndian.PutUint32(bwBytes, ri.bandwidth)
if err := ri.sendRNodeCommand(RNODE_CMD_BANDWIDTH, bwBytes); err != nil {
return err
}
}
if ri.sf != 0 {
if err := ri.sendRNodeCommand(RNODE_CMD_SF, []byte{ri.sf}); err != nil {
return err
}
}
if ri.cr != 0 {
if err := ri.sendRNodeCommand(RNODE_CMD_CR, []byte{ri.cr}); err != nil {
return err
}
}
if ri.txPower != 0 {
if err := ri.sendRNodeCommand(RNODE_CMD_TXPOWER, []byte{ri.txPower}); err != nil {
return err
}
}
if err := ri.sendRNodeCommand(RNODE_CMD_RADIO_STATE, []byte{0x01}); err != nil {
return err
}
ri.interfaceReady = true
debug.Log(debug.DEBUG_INFO, "RNode initialized", "name", ri.GetName())
return nil
}
// detect attempts to detect the RNode device and obtain firmware version.
func (ri *RNodeInterface) detect() error {
detectCmd := []byte{RNODE_DETECT_REQ}
if err := ri.sendRNodeCommand(RNODE_CMD_DETECT, detectCmd); err != nil {
return err
}
start := time.Now()
for !ri.rDetected {
if time.Since(start) > 2*time.Second {
debug.Log(debug.DEBUG_ERROR, "RNode detection timed out", "name", ri.GetName())
break
}
time.Sleep(100 * time.Millisecond)
}
if err := ri.sendRNodeCommand(RNODE_CMD_FW_VERSION, []byte{0x00}); err != nil {
return err
}
return nil
}
// sendRNodeCommand sends a command to the RNode device.
func (ri *RNodeInterface) sendRNodeCommand(cmd byte, data []byte) error {
if kissInterface, ok := ri.Interface.(interface {
SendKISS(byte, []byte) error
}); ok {
return kissInterface.SendKISS(cmd, data)
}
frame := make([]byte, 0, len(data)+1)
frame = append(frame, cmd)
frame = append(frame, data...)
return ri.Interface.Send(frame, "")
}
// Send transmits data using the underlying interface.
func (ri *RNodeInterface) Send(data []byte, addr string) error {
if !ri.interfaceReady {
ri.packetQueue = append(ri.packetQueue, data)
return nil
}
return ri.Interface.Send(data, addr)
}

View File

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build !tinygo
package interfaces
import (
"fmt"
)
type SerialInterface struct {
BaseInterface
}
func NewSerialInterface(name string, portName string, baud uint32, enabled bool) (*SerialInterface, error) {
return nil, fmt.Errorf("SerialInterface is only supported on TinyGo targets currently")
}
func (si *SerialInterface) Start() error {
return fmt.Errorf("SerialInterface is only supported on TinyGo targets currently")
}
func (si *SerialInterface) Stop() error {
return nil
}
func (si *SerialInterface) Send(data []byte, address string) error {
return fmt.Errorf("SerialInterface is only supported on TinyGo targets currently")
}

View File

@@ -0,0 +1,221 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build tinygo
package interfaces
import (
"fmt"
"machine"
"sync"
"time"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
)
const (
SERIAL_DEFAULT_BAUD = 115200
SERIAL_MTU = 1500
)
// SerialInterface implements a serial interface using TinyGo UART.
type SerialInterface struct {
BaseInterface
uart *machine.UART
baud uint32
done chan struct{}
stopOnce sync.Once
}
// NewSerialInterface creates and initializes a new SerialInterface.
func NewSerialInterface(name string, portName string, baud uint32, enabled bool) (*SerialInterface, error) {
if baud == 0 {
baud = SERIAL_DEFAULT_BAUD
}
uart, err := getUART(portName)
if err != nil {
return nil, err
}
si := &SerialInterface{
BaseInterface: NewBaseInterface(name, common.IF_TYPE_SERIAL, enabled),
uart: uart,
baud: baud,
done: make(chan struct{}),
}
si.MTU = SERIAL_MTU
si.Bitrate = int64(baud)
if enabled {
err := si.Start()
if err != nil {
return nil, err
}
}
return si, nil
}
// getUART returns a TinyGo UART handle by name or index.
func getUART(name string) (*machine.UART, error) {
switch name {
case "UART0", "0":
return machine.UART0, nil
case "UART1", "1":
return machine.UART1, nil
case "UART2", "2":
return machine.UART2, nil
default:
if name == "" {
return machine.UART0, nil
}
return nil, fmt.Errorf("unknown UART: %s", name)
}
}
// Start enables the serial interface and starts the read loop.
func (si *SerialInterface) Start() error {
si.Mutex.Lock()
defer si.Mutex.Unlock()
if si.Online {
return nil
}
err := si.uart.Configure(machine.UARTConfig{
BaudRate: si.baud,
})
if err != nil {
return fmt.Errorf("failed to configure UART: %w", err)
}
si.Online = true
si.Enabled = true
go si.readLoop()
return nil
}
// Stop disables the serial interface.
func (si *SerialInterface) Stop() error {
si.Mutex.Lock()
si.Online = false
si.Enabled = false
si.Mutex.Unlock()
si.stopOnce.Do(func() {
if si.done != nil {
close(si.done)
}
})
return nil
}
// readLoop reads and processes frames from the UART, handling KISS framing.
func (si *SerialInterface) readLoop() {
buffer := make([]byte, si.MTU)
dataBuffer := make([]byte, 0, si.MTU)
inFrame := false
escape := false
for {
si.Mutex.RLock()
online := si.Online
done := si.done
si.Mutex.RUnlock()
if !online {
return
}
select {
case <-done:
return
default:
}
if si.uart.Buffered() > 0 {
n, err := si.uart.Read(buffer)
if err != nil {
debug.Log(debug.DEBUG_ERROR, "Serial read error", "name", si.Name, "error", err)
time.Sleep(100 * time.Millisecond)
continue
}
if n > 0 {
for i := 0; i < n; i++ {
b := buffer[i]
if b == KISS_FEND {
if inFrame && len(dataBuffer) > 0 {
packet := make([]byte, len(dataBuffer))
copy(packet, dataBuffer)
si.ProcessIncoming(packet)
dataBuffer = dataBuffer[:0]
}
inFrame = true
escape = false
continue
}
if inFrame {
if b == KISS_FESC {
escape = true
} else {
if escape {
if b == KISS_TFEND {
b = KISS_FEND
} else if b == KISS_TFESC {
b = KISS_FESC
}
escape = false
}
dataBuffer = append(dataBuffer, b)
}
}
}
}
} else {
time.Sleep(10 * time.Millisecond)
}
}
}
// Send transmits data using KISS protocol with the default command 0x00.
func (si *SerialInterface) Send(data []byte, address string) error {
return si.SendKISS(0x00, data)
}
// SendKISS sends a KISS-encoded packet over the serial UART.
func (si *SerialInterface) SendKISS(command byte, data []byte) error {
si.Mutex.RLock()
online := si.Online
si.Mutex.RUnlock()
if !online {
return fmt.Errorf("interface offline")
}
frame := make([]byte, 0, len(data)*2+3)
frame = append(frame, KISS_FEND)
frame = append(frame, command)
frame = append(frame, escapeKISS(data)...)
frame = append(frame, KISS_FEND)
_, err := si.uart.Write(frame)
if err != nil {
return err
}
si.Mutex.Lock()
si.TxBytes += uint64(len(frame))
si.lastTx = time.Now()
si.Mutex.Unlock()
return nil
}

View File

@@ -1,3 +1,5 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package interfaces
import (
@@ -16,11 +18,6 @@ const (
HDLC_ESC = 0x7D
HDLC_ESC_MASK = 0x20
KISS_FEND = 0xC0
KISS_FESC = 0xDB
KISS_TFEND = 0xDC
KISS_TFESC = 0xDD
DEFAULT_MTU = 1064
BITRATE_GUESS_VAL = 10 * 1000 * 1000
RECONNECT_WAIT = 5
@@ -32,11 +29,15 @@ const (
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 {
@@ -53,12 +54,8 @@ type TCPClientInterface struct {
maxReconnectTries int
packetBuffer []byte
packetType byte
mutex sync.RWMutex
enabled bool
TxBytes uint64
RxBytes uint64
lastTx time.Time
lastRx time.Time
done chan struct{}
stopOnce sync.Once
}
func NewTCPClientInterface(name string, targetHost string, targetPort int, kissFraming bool, i2pTunneled bool, enabled bool) (*TCPClientInterface, error) {
@@ -69,10 +66,10 @@ func NewTCPClientInterface(name string, targetHost string, targetPort int, kissF
kissFraming: kissFraming,
i2pTunneled: i2pTunneled,
initiator: true,
enabled: enabled,
maxReconnectTries: RECONNECT_WAIT * TCP_PROBES_COUNT,
packetBuffer: make([]byte, 0),
neverConnected: true,
done: make(chan struct{}),
}
if enabled {
@@ -90,25 +87,41 @@ func NewTCPClientInterface(name string, targetHost string, targetPort int, kissF
}
func (tc *TCPClientInterface) Start() error {
tc.mutex.Lock()
defer tc.mutex.Unlock()
if !tc.Enabled {
return fmt.Errorf("interface not enabled")
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.Dial("tcp", addr)
conn, err := net.DialTimeout("tcp", addr, TCP_CONNECT_TIMEOUT)
if err != nil {
return err
}
tc.Mutex.Lock()
tc.conn = conn
tc.Mutex.Unlock()
// Set platform-specific timeouts
switch runtime.GOOS {
@@ -122,11 +135,33 @@ func (tc *TCPClientInterface) Start() error {
}
}
tc.Mutex.Lock()
tc.Online = true
tc.Mutex.Unlock()
go tc.readLoop()
return nil
}
func (tc *TCPClientInterface) Stop() error {
tc.Mutex.Lock()
tc.Enabled = false
tc.Online = false
if tc.conn != nil {
_ = tc.conn.Close()
tc.conn = nil
}
tc.Mutex.Unlock()
tc.stopOnce.Do(func() {
if tc.done != nil {
close(tc.done)
}
})
return nil
}
func (tc *TCPClientInterface) readLoop() {
buffer := make([]byte, tc.MTU)
inFrame := false
@@ -134,10 +169,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()
@@ -145,7 +200,6 @@ func (tc *TCPClientInterface) readLoop() {
return
}
// Update RX bytes for raw received data
tc.UpdateStats(uint64(n), true) // #nosec G115
for i := 0; i < n; i++ {
@@ -181,16 +235,17 @@ func (tc *TCPClientInterface) handlePacket(data []byte) {
return
}
tc.mutex.Lock()
tc.Mutex.Lock()
tc.RxBytes += uint64(len(data))
lastRx := time.Now()
tc.lastRx = lastRx
tc.mutex.Unlock()
callback := tc.packetCallback
tc.Mutex.Unlock()
debug.Log(debug.DEBUG_ALL, "Received packet", "type", fmt.Sprintf("0x%02x", data[0]), "size", len(data))
// For RNS packets, call the packet callback directly
if callback := tc.GetPacketCallback(); callback != nil {
if callback != nil {
debug.Log(debug.DEBUG_ALL, "Calling packet callback for RNS packet")
callback(data, tc)
} else {
@@ -212,7 +267,11 @@ func (tc *TCPClientInterface) Send(data []byte, address string) error {
}
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")
}
@@ -224,11 +283,19 @@ func (tc *TCPClientInterface) ProcessOutgoing(data []byte) error {
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
frame = append(frame, HDLC_FLAG)
// Update TX stats before sending
tc.UpdateStats(uint64(len(frame)), false)
tc.UpdateStats(uint64(len(frame)), false) // #nosec G115
debug.Log(debug.DEBUG_ALL, "TCP interface writing to network", "name", tc.Name, "bytes", len(frame))
_, err := tc.conn.Write(frame)
tc.Mutex.RLock()
conn := tc.conn
tc.Mutex.RUnlock()
if conn == nil {
return fmt.Errorf("connection closed")
}
_, err := conn.Write(frame)
if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "TCP interface write failed", "name", tc.Name, "error", err)
}
@@ -240,7 +307,7 @@ func (tc *TCPClientInterface) teardown() {
tc.IN = false
tc.OUT = false
if tc.conn != nil {
tc.conn.Close() // #nosec G104
_ = tc.conn.Close()
}
}
@@ -257,28 +324,14 @@ func escapeHDLC(data []byte) []byte {
return escaped
}
func escapeKISS(data []byte) []byte {
escaped := make([]byte, 0, len(data)*2)
for _, b := range data {
if b == KISS_FEND {
escaped = append(escaped, KISS_FESC, KISS_TFEND)
} else if b == KISS_FESC {
escaped = append(escaped, KISS_FESC, KISS_TFESC)
} else {
escaped = append(escaped, b)
}
}
return escaped
}
func (tc *TCPClientInterface) SetPacketCallback(cb common.PacketCallback) {
tc.packetCallback = cb
}
func (tc *TCPClientInterface) IsEnabled() bool {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
return tc.enabled && tc.Online && !tc.Detached
tc.Mutex.RLock()
defer tc.Mutex.RUnlock()
return tc.Enabled && tc.Online && !tc.Detached
}
func (tc *TCPClientInterface) GetName() string {
@@ -286,31 +339,31 @@ func (tc *TCPClientInterface) GetName() string {
}
func (tc *TCPClientInterface) GetPacketCallback() common.PacketCallback {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
tc.Mutex.RLock()
defer tc.Mutex.RUnlock()
return tc.packetCallback
}
func (tc *TCPClientInterface) IsDetached() bool {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
tc.Mutex.RLock()
defer tc.Mutex.RUnlock()
return tc.Detached
}
func (tc *TCPClientInterface) IsOnline() bool {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
tc.Mutex.RLock()
defer tc.Mutex.RUnlock()
return tc.Online
}
func (tc *TCPClientInterface) reconnect() {
tc.mutex.Lock()
tc.Mutex.Lock()
if tc.reconnecting {
tc.mutex.Unlock()
tc.Mutex.Unlock()
return
}
tc.reconnecting = true
tc.mutex.Unlock()
tc.Mutex.Unlock()
backoff := time.Second
maxBackoff := time.Minute * 5
@@ -323,13 +376,13 @@ func (tc *TCPClientInterface) reconnect() {
conn, err := net.Dial("tcp", addr)
if err == nil {
tc.mutex.Lock()
tc.Mutex.Lock()
tc.conn = conn
tc.Online = true
tc.neverConnected = false
tc.reconnecting = false
tc.mutex.Unlock()
tc.Mutex.Unlock()
go tc.readLoop()
return
@@ -349,35 +402,35 @@ func (tc *TCPClientInterface) reconnect() {
retries++
}
tc.mutex.Lock()
tc.Mutex.Lock()
tc.reconnecting = false
tc.mutex.Unlock()
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.Mutex.Lock()
defer tc.Mutex.Unlock()
tc.Online = true
}
func (tc *TCPClientInterface) Disable() {
tc.mutex.Lock()
defer tc.mutex.Unlock()
tc.Mutex.Lock()
defer tc.Mutex.Unlock()
tc.Online = false
}
func (tc *TCPClientInterface) IsConnected() bool {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
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()
tc.Mutex.RLock()
defer tc.Mutex.RUnlock()
if !tc.IsConnected() {
return 0
@@ -401,20 +454,20 @@ func (tc *TCPClientInterface) GetRTT() time.Duration {
}
func (tc *TCPClientInterface) GetTxBytes() uint64 {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
tc.Mutex.RLock()
defer tc.Mutex.RUnlock()
return tc.TxBytes
}
func (tc *TCPClientInterface) GetRxBytes() uint64 {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
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()
tc.Mutex.Lock()
defer tc.Mutex.Unlock()
now := time.Now()
if isRx {
@@ -429,23 +482,22 @@ func (tc *TCPClientInterface) UpdateStats(bytes uint64, isRx bool) {
}
func (tc *TCPClientInterface) GetStats() (tx uint64, rx uint64, lastTx time.Time, lastRx time.Time) {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
tc.Mutex.RLock()
defer tc.Mutex.RUnlock()
return tc.TxBytes, tc.RxBytes, tc.lastTx, tc.lastRx
}
type TCPServerInterface struct {
BaseInterface
connections map[string]net.Conn
mutex sync.RWMutex
bindAddr string
bindPort int
preferIPv6 bool
kissFraming bool
i2pTunneled bool
packetCallback common.PacketCallback
TxBytes uint64
RxBytes uint64
connections map[string]net.Conn
listener net.Listener
bindAddr string
bindPort int
preferIPv6 bool
kissFraming bool
i2pTunneled bool
done chan struct{}
stopOnce sync.Once
}
func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFraming bool, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) {
@@ -456,6 +508,7 @@ func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFrami
Type: common.IF_TYPE_TCP,
Online: false,
MTU: common.DEFAULT_MTU,
Enabled: true,
Detached: false,
},
connections: make(map[string]net.Conn),
@@ -464,6 +517,7 @@ func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFrami
preferIPv6: preferIPv6,
kissFraming: kissFraming,
i2pTunneled: i2pTunneled,
done: make(chan struct{}),
}
return ts, nil
@@ -482,21 +536,21 @@ func (ts *TCPServerInterface) String() string {
}
func (ts *TCPServerInterface) SetPacketCallback(callback common.PacketCallback) {
ts.mutex.Lock()
defer ts.mutex.Unlock()
ts.Mutex.Lock()
defer ts.Mutex.Unlock()
ts.packetCallback = callback
}
func (ts *TCPServerInterface) GetPacketCallback() common.PacketCallback {
ts.mutex.RLock()
defer ts.mutex.RUnlock()
ts.Mutex.RLock()
defer ts.Mutex.RUnlock()
return ts.packetCallback
}
func (ts *TCPServerInterface) IsEnabled() bool {
ts.mutex.RLock()
defer ts.mutex.RUnlock()
return ts.BaseInterface.Enabled && ts.BaseInterface.Online && !ts.BaseInterface.Detached
ts.Mutex.RLock()
defer ts.Mutex.RUnlock()
return ts.Enabled && ts.Online && !ts.Detached
}
func (ts *TCPServerInterface) GetName() string {
@@ -504,32 +558,47 @@ func (ts *TCPServerInterface) GetName() string {
}
func (ts *TCPServerInterface) IsDetached() bool {
ts.mutex.RLock()
defer ts.mutex.RUnlock()
return ts.BaseInterface.Detached
ts.Mutex.RLock()
defer ts.Mutex.RUnlock()
return ts.Detached
}
func (ts *TCPServerInterface) IsOnline() bool {
ts.mutex.RLock()
defer ts.mutex.RUnlock()
ts.Mutex.RLock()
defer ts.Mutex.RUnlock()
return ts.Online
}
func (ts *TCPServerInterface) Enable() {
ts.mutex.Lock()
defer ts.mutex.Unlock()
ts.Mutex.Lock()
defer ts.Mutex.Unlock()
ts.Online = true
}
func (ts *TCPServerInterface) Disable() {
ts.mutex.Lock()
defer ts.mutex.Unlock()
ts.Mutex.Lock()
defer ts.Mutex.Unlock()
ts.Online = false
}
func (ts *TCPServerInterface) Start() error {
ts.mutex.Lock()
defer ts.mutex.Unlock()
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)
@@ -537,14 +606,30 @@ func (ts *TCPServerInterface) Start() error {
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 {
if !ts.Online {
ts.Mutex.RLock()
online := ts.Online
ts.Mutex.RUnlock()
if !online {
return // Normal shutdown
}
debug.Log(debug.DEBUG_ERROR, "Error accepting connection", "error", err)
@@ -560,60 +645,87 @@ func (ts *TCPServerInterface) Start() error {
}
func (ts *TCPServerInterface) Stop() error {
ts.mutex.Lock()
defer ts.mutex.Unlock()
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()
ts.Mutex.RLock()
defer ts.Mutex.RUnlock()
return ts.TxBytes
}
func (ts *TCPServerInterface) GetRxBytes() uint64 {
ts.mutex.RLock()
defer ts.mutex.RUnlock()
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.Mutex.Lock()
ts.connections[addr] = conn
ts.mutex.Unlock()
ts.Mutex.Unlock()
defer func() {
ts.mutex.Lock()
ts.Mutex.Lock()
delete(ts.connections, addr)
ts.mutex.Unlock()
conn.Close() // #nosec G104
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.Mutex.Lock()
ts.RxBytes += uint64(n) // #nosec G115
ts.mutex.Unlock()
callback := ts.packetCallback
ts.Mutex.Unlock()
if ts.packetCallback != nil {
ts.packetCallback(buffer[:n], ts)
if callback != nil {
callback(buffer[:n], ts)
}
}
}
func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
ts.mutex.RLock()
defer ts.mutex.RUnlock()
ts.Mutex.RLock()
online := ts.Online
ts.Mutex.RUnlock()
if !ts.Online {
if !online {
return fmt.Errorf("interface offline")
}
@@ -626,9 +738,15 @@ func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
frame = append(frame, HDLC_FLAG)
}
ts.TxBytes += uint64(len(frame))
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)
}

View File

@@ -1,5 +1,7 @@
//go:build !linux
// +build !linux
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build !linux || tinygo
// +build !linux tinygo
package interfaces
@@ -12,3 +14,11 @@ import (
func platformGetRTT(fd uintptr) time.Duration {
return 0
}
func (tc *TCPClientInterface) setTimeoutsLinux() error {
return nil
}
func (tc *TCPClientInterface) setTimeoutsOSX() error {
return nil
}

View File

@@ -1,3 +1,5 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build darwin
// +build darwin
@@ -37,7 +39,7 @@ func (tc *TCPClientInterface) setTimeoutsOSX() error {
probeAfter = TCP_PROBE_AFTER_SEC
}
if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, 1); err != nil {
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
}

View File

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

View File

@@ -1,5 +1,7 @@
//go:build linux
// +build linux
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build linux && !tinygo
// +build linux,!tinygo
package interfaces
@@ -29,35 +31,40 @@ func (tc *TCPClientInterface) setTimeoutsLinux() error {
var userTimeout, probeAfter, probeInterval, probeCount int
if tc.i2pTunneled {
userTimeout = I2P_USER_TIMEOUT_SEC * 1000
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 * 1000
userTimeout = TCP_USER_TIMEOUT_SEC * TCP_MILLISECONDS
probeAfter = TCP_PROBE_AFTER_SEC
probeInterval = TCP_PROBE_INTERVAL_SEC
probeCount = TCP_PROBES_COUNT
}
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, 18, userTimeout); err != nil {
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, 1); err != nil {
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, 4, probeAfter); err != nil {
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, 5, probeInterval); err != nil {
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, 6, probeCount); err != nil {
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)
}
})
@@ -82,13 +89,13 @@ func platformGetRTT(fd uintptr) time.Duration {
// bearer:disable go_gosec_unsafe_unsafe
infoLen := uint32(unsafe.Sizeof(info))
// TCP_INFO is 11 on Linux
const TCP_INFO = 11
// #nosec G103
_, _, errno := syscall.Syscall6(
syscall.SYS_GETSOCKOPT,
fd,
syscall.IPPROTO_TCP,
11, // TCP_INFO
TCP_INFO,
// bearer:disable go_gosec_unsafe_unsafe
uintptr(unsafe.Pointer(&info)),
// bearer:disable go_gosec_unsafe_unsafe

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,8 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build !tinygo
// +build !tinygo
package interfaces
import (
@@ -14,8 +19,9 @@ type UDPInterface struct {
conn *net.UDPConn
addr *net.UDPAddr
targetAddr *net.UDPAddr
mutex sync.RWMutex
readBuffer []byte
done chan struct{}
stopOnce sync.Once
}
func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) {
@@ -36,45 +42,28 @@ func NewUDPInterface(name string, addr string, target string, enabled bool) (*UD
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
addr: udpAddr,
targetAddr: targetAddr,
readBuffer: make([]byte, 1064),
readBuffer: make([]byte, common.NUM_1064),
done: make(chan struct{}),
}
ui.MTU = 1064
ui.MTU = common.NUM_1064
return ui, nil
}
func (ui *UDPInterface) GetName() string {
return ui.Name
}
func (ui *UDPInterface) GetType() common.InterfaceType {
return ui.Type
}
func (ui *UDPInterface) GetMode() common.InterfaceMode {
return ui.Mode
}
func (ui *UDPInterface) IsOnline() bool {
ui.mutex.RLock()
defer ui.mutex.RUnlock()
return ui.Online
}
func (ui *UDPInterface) IsDetached() bool {
ui.mutex.RLock()
defer ui.mutex.RUnlock()
return ui.Detached
}
func (ui *UDPInterface) Detach() {
ui.mutex.Lock()
defer ui.mutex.Unlock()
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)
}
})
}
func (ui *UDPInterface) Send(data []byte, addr string) error {
@@ -88,10 +77,9 @@ func (ui *UDPInterface) Send(data []byte, addr string) error {
return fmt.Errorf("no target address configured")
}
// Update TX stats before sending
ui.mutex.Lock()
ui.Mutex.Lock()
ui.TxBytes += uint64(len(data))
ui.mutex.Unlock()
ui.Mutex.Unlock()
_, err := ui.conn.WriteTo(data, ui.targetAddr)
if err != nil {
@@ -102,41 +90,19 @@ func (ui *UDPInterface) Send(data []byte, addr string) error {
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.IsOnline() {
return fmt.Errorf("interface offline")
}
if ui.targetAddr == nil {
return fmt.Errorf("no target address configured")
}
_, err := ui.conn.WriteToUDP(data, ui.targetAddr)
_, err := ui.conn.Write(data)
if err != nil {
return fmt.Errorf("UDP write failed: %v", err)
}
ui.mutex.Lock()
ui.Mutex.Lock()
ui.TxBytes += uint64(len(data))
ui.mutex.Unlock()
ui.Mutex.Unlock()
return nil
}
@@ -145,39 +111,25 @@ 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.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
@@ -187,15 +139,17 @@ func (ui *UDPInterface) Start() error {
// 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(1064); err != nil {
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(1064); err != nil {
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()
@@ -203,19 +157,43 @@ func (ui *UDPInterface) Start() error {
return nil
}
func (ui *UDPInterface) Stop() error {
ui.Detach()
return nil
}
func (ui *UDPInterface) readLoop() {
buffer := make([]byte, 1064)
for ui.IsOnline() && !ui.IsDetached() {
n, remoteAddr, err := ui.conn.ReadFromUDP(buffer)
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 {
if ui.IsOnline() {
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
}
// Update RX stats
ui.mutex.Lock()
ui.Mutex.Lock()
// #nosec G115 - Network read sizes are always positive and within safe range
ui.RxBytes += uint64(n)
@@ -224,16 +202,11 @@ func (ui *UDPInterface) readLoop() {
debug.Log(debug.DEBUG_ALL, "UDP interface discovered peer", "name", ui.Name, "peer", remoteAddr.String())
ui.targetAddr = remoteAddr
}
ui.mutex.Unlock()
callback := ui.packetCallback
ui.Mutex.Unlock()
if ui.packetCallback != nil {
ui.packetCallback(buffer[:n], ui)
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

@@ -66,7 +66,6 @@ func TestNewUDPInterface(t *testing.T) {
func TestUDPInterfaceState(t *testing.T) {
// Basic state tests are covered by BaseInterface tests
// Add specific UDP ones if needed, e.g., involving the conn
addr := "127.0.0.1:0"
ui, _ := NewUDPInterface("udpState", addr, "", true)

View File

@@ -0,0 +1,68 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build tinygo
// +build tinygo
package interfaces
import (
"fmt"
"net"
"sync"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
)
type UDPInterface struct {
BaseInterface
conn net.Conn
addr *net.UDPAddr
targetAddr *net.UDPAddr
readBuffer []byte
done chan struct{}
stopOnce sync.Once
}
func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) {
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
var targetAddr *net.UDPAddr
if target != "" {
targetAddr, err = net.ResolveUDPAddr("udp", target)
if err != nil {
return nil, err
}
}
ui := &UDPInterface{
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
addr: udpAddr,
targetAddr: targetAddr,
readBuffer: make([]byte, common.NUM_1064),
done: make(chan struct{}),
}
ui.MTU = common.NUM_1064
return ui, nil
}
func (ui *UDPInterface) Start() error {
// TinyGo doesn't support UDP servers, only clients
return fmt.Errorf("UDPInterface not supported in TinyGo - UDP server functionality requires net.ListenUDP")
}
func (ui *UDPInterface) Send(data []byte, addr string) error {
// TinyGo doesn't support UDP sending
return fmt.Errorf("UDPInterface Send not supported in TinyGo - requires UDP client functionality")
}
func (ui *UDPInterface) Stop() error {
ui.Mutex.Lock()
defer ui.Mutex.Unlock()
ui.Online = false
return nil
}

View File

@@ -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

@@ -1,13 +1,13 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build js && wasm
// +build js,wasm
package interfaces
import (
"encoding/binary"
"fmt"
"net"
"sync"
"syscall/js"
"time"
@@ -15,12 +15,17 @@ import (
"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
mutex sync.RWMutex
messageQueue [][]byte
}
@@ -31,8 +36,8 @@ func NewWebSocketInterface(name string, wsURL string, enabled bool) (*WebSocketI
messageQueue: make([][]byte, 0),
}
ws.MTU = 1064
ws.Bitrate = 10000000
ws.MTU = WS_MTU
ws.Bitrate = WS_BITRATE
return ws, nil
}
@@ -50,41 +55,41 @@ func (wsi *WebSocketInterface) GetMode() common.InterfaceMode {
}
func (wsi *WebSocketInterface) IsOnline() bool {
wsi.mutex.RLock()
defer wsi.mutex.RUnlock()
wsi.Mutex.RLock()
defer wsi.Mutex.RUnlock()
return wsi.Online && wsi.connected
}
func (wsi *WebSocketInterface) IsDetached() bool {
wsi.mutex.RLock()
defer wsi.mutex.RUnlock()
wsi.Mutex.RLock()
defer wsi.Mutex.RUnlock()
return wsi.Detached
}
func (wsi *WebSocketInterface) Detach() {
wsi.mutex.Lock()
defer wsi.mutex.Unlock()
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.Mutex.Lock()
defer wsi.Mutex.Unlock()
wsi.Enabled = true
}
func (wsi *WebSocketInterface) Disable() {
wsi.mutex.Lock()
defer wsi.mutex.Unlock()
wsi.Mutex.Lock()
defer wsi.Mutex.Unlock()
wsi.Enabled = false
wsi.closeWebSocket()
}
func (wsi *WebSocketInterface) Start() error {
wsi.mutex.Lock()
defer wsi.mutex.Unlock()
wsi.Mutex.Lock()
defer wsi.Mutex.Unlock()
if wsi.ws.Truthy() {
return fmt.Errorf("WebSocket already started")
@@ -94,18 +99,18 @@ func (wsi *WebSocketInterface) Start() error {
ws.Set("binaryType", "arraybuffer")
ws.Set("onopen", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
wsi.mutex.Lock()
wsi.Mutex.Lock()
wsi.connected = true
wsi.Online = true
wsi.mutex.Unlock()
wsi.Mutex.Unlock()
debug.Log(debug.DEBUG_INFO, "WebSocket connected", "name", wsi.Name, "url", wsi.wsURL)
wsi.mutex.Lock()
wsi.Mutex.Lock()
queue := make([][]byte, len(wsi.messageQueue))
copy(queue, wsi.messageQueue)
wsi.messageQueue = wsi.messageQueue[:0]
wsi.mutex.Unlock()
wsi.Mutex.Unlock()
for _, msg := range queue {
wsi.sendWebSocketMessage(msg)
@@ -122,35 +127,27 @@ func (wsi *WebSocketInterface) Start() error {
event := args[0]
data := event.Get("data")
var packetData []byte
var packet []byte
if data.Type() == js.TypeString {
packetData = []byte(data.String())
packet = []byte(data.String())
} else if data.Type() == js.TypeObject {
array := js.Global().Get("Uint8Array").New(data)
length := array.Get("length").Int()
packetData = make([]byte, length)
js.CopyBytesToGo(packetData, array)
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(packetData) < 4 {
debug.Log(debug.DEBUG_ERROR, "WebSocket message too short", "bytes", len(packetData))
if len(packet) < 1 {
debug.Log(debug.DEBUG_ERROR, "WebSocket message empty")
return nil
}
packetLen := binary.BigEndian.Uint32(packetData[:4])
if len(packetData) < int(packetLen)+4 {
debug.Log(debug.DEBUG_ERROR, "WebSocket message incomplete", "expected", packetLen+4, "got", len(packetData))
return nil
}
packet := packetData[4 : 4+packetLen]
wsi.mutex.Lock()
wsi.Mutex.Lock()
wsi.RxBytes += uint64(len(packet))
wsi.mutex.Unlock()
wsi.Mutex.Unlock()
wsi.ProcessIncoming(packet)
@@ -163,15 +160,15 @@ func (wsi *WebSocketInterface) Start() error {
}))
ws.Set("onclose", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
wsi.mutex.Lock()
wsi.Mutex.Lock()
wsi.connected = false
wsi.Online = false
wsi.mutex.Unlock()
wsi.Mutex.Unlock()
debug.Log(debug.DEBUG_INFO, "WebSocket closed", "name", wsi.Name)
if wsi.Enabled && !wsi.Detached {
time.Sleep(2 * time.Second)
time.Sleep(WS_RECONNECT_DELAY)
go wsi.Start()
}
@@ -184,8 +181,8 @@ func (wsi *WebSocketInterface) Start() error {
}
func (wsi *WebSocketInterface) Stop() error {
wsi.mutex.Lock()
defer wsi.mutex.Unlock()
wsi.Mutex.Lock()
defer wsi.Mutex.Unlock()
wsi.Enabled = false
wsi.closeWebSocket()
return nil
@@ -205,14 +202,14 @@ func (wsi *WebSocketInterface) Send(data []byte, addr string) error {
return fmt.Errorf("interface not enabled")
}
wsi.mutex.Lock()
wsi.Mutex.Lock()
wsi.TxBytes += uint64(len(data))
wsi.mutex.Unlock()
wsi.Mutex.Unlock()
if !wsi.connected {
wsi.mutex.Lock()
wsi.Mutex.Lock()
wsi.messageQueue = append(wsi.messageQueue, data)
wsi.mutex.Unlock()
wsi.Mutex.Unlock()
return nil
}
@@ -228,13 +225,8 @@ func (wsi *WebSocketInterface) sendWebSocketMessage(data []byte) error {
return fmt.Errorf("WebSocket not open")
}
packetLen := uint32(len(data))
packet := make([]byte, 4+len(data))
binary.BigEndian.PutUint32(packet[:4], packetLen)
copy(packet[4:], data)
array := js.Global().Get("Uint8Array").New(len(packet))
js.CopyBytesToJS(array, packet)
array := js.Global().Get("Uint8Array").New(len(data))
js.CopyBytesToJS(array, data)
wsi.ws.Call("send", array)
@@ -255,7 +247,7 @@ func (wsi *WebSocketInterface) GetMTU() int {
}
func (wsi *WebSocketInterface) IsEnabled() bool {
wsi.mutex.RLock()
defer wsi.mutex.RUnlock()
wsi.Mutex.RLock()
defer wsi.Mutex.RUnlock()
return wsi.Enabled && wsi.Online && !wsi.Detached
}

View File

@@ -1,3 +1,5 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package link
import (
@@ -26,7 +28,6 @@ import (
"git.quad4.io/Networks/Reticulum-Go/pkg/resolver"
"git.quad4.io/Networks/Reticulum-Go/pkg/resource"
"git.quad4.io/Networks/Reticulum-Go/pkg/transport"
"github.com/vmihailenco/msgpack/v5"
)
const (
@@ -306,7 +307,7 @@ func (l *Link) Request(path string, data []byte, timeout time.Duration) (*Reques
pathHash := identity.TruncatedHash([]byte(path))
requestData := []interface{}{time.Now().Unix(), pathHash, data}
packedRequest, err := msgpack.Marshal(requestData)
packedRequest, err := common.MsgpackMarshal(requestData)
if err != nil {
return nil, fmt.Errorf("failed to pack request: %w", err)
}
@@ -1027,7 +1028,7 @@ func (l *Link) handleRequest(plaintext []byte, pkt *packet.Packet) error {
}
var requestData []interface{}
if err := msgpack.Unmarshal(plaintext, &requestData); err != nil {
if err := common.MsgpackUnmarshal(plaintext, &requestData); err != nil {
return fmt.Errorf("failed to unpack request: %w", err)
}
@@ -1058,7 +1059,7 @@ func (l *Link) handleRequest(plaintext []byte, pkt *packet.Packet) error {
case string:
requestPayload = []byte(payload)
default:
packed, err := msgpack.Marshal(payload)
packed, err := common.MsgpackMarshal(payload)
if err != nil {
return fmt.Errorf("failed to pack request_payload: %w", err)
}
@@ -1087,7 +1088,7 @@ func (l *Link) handleRequest(plaintext []byte, pkt *packet.Packet) error {
func (l *Link) handleResponse(plaintext []byte) error {
var responseData []interface{}
if err := msgpack.Unmarshal(plaintext, &responseData); err != nil {
if err := common.MsgpackUnmarshal(plaintext, &responseData); err != nil {
return fmt.Errorf("failed to unpack response: %w", err)
}
@@ -1122,7 +1123,7 @@ func (l *Link) handleResponse(plaintext []byte) error {
func (l *Link) sendResponse(requestID []byte, response interface{}) error {
responseData := []interface{}{requestID, response}
packedResponse, err := msgpack.Marshal(responseData)
packedResponse, err := common.MsgpackMarshal(responseData)
if err != nil {
return fmt.Errorf("failed to pack response: %w", err)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,12 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package resource
import (
"fmt"
"math"
"github.com/vmihailenco/msgpack/v5"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
)
const (
@@ -115,12 +117,12 @@ func (ra *ResourceAdvertisement) Pack(segment int) ([]byte, error) {
"m": hashmap,
}
return msgpack.Marshal(dict)
return common.MsgpackMarshal(dict)
}
func UnpackResourceAdvertisement(data []byte) (*ResourceAdvertisement, error) {
var dict map[string]interface{}
if err := msgpack.Unmarshal(data, &dict); err != nil {
if err := common.MsgpackUnmarshal(data, &dict); err != nil {
return nil, fmt.Errorf("failed to unpack advertisement: %w", err)
}

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package transport
import (
@@ -125,6 +127,8 @@ type Transport struct {
heldAnnounces map[string]*PathAnnounceEntry
transportIdentity *identity.Identity
pathRequestDest interface{}
done chan struct{}
stopOnce sync.Once
}
type DiscoveryPathRequest struct {
@@ -176,6 +180,7 @@ func NewTransport(cfg *common.ReticulumConfig) *Transport {
discoveryPRTags: make(map[string]bool),
announceTable: make(map[string]*PathAnnounceEntry),
heldAnnounces: make(map[string]*PathAnnounceEntry),
done: make(chan struct{}),
}
// TODO: Path table persistence
@@ -194,11 +199,16 @@ func (t *Transport) startMaintenanceJobs() {
ticker := time.NewTicker(common.FIVE * time.Second)
defer ticker.Stop()
for range ticker.C {
t.cleanupExpiredPaths()
t.cleanupExpiredDiscoveryRequests()
t.cleanupExpiredAnnounces()
t.cleanupExpiredReceipts()
for {
select {
case <-ticker.C:
t.cleanupExpiredPaths()
t.cleanupExpiredDiscoveryRequests()
t.cleanupExpiredAnnounces()
t.cleanupExpiredReceipts()
case <-t.done:
return
}
}
}
@@ -308,14 +318,12 @@ func (t *Transport) CreateIncomingLink(dest interface{}, networkIface common.Net
return nil
}
// Add GetTransportInstance function
func GetTransportInstance() *Transport {
transportMutex.Lock()
defer transportMutex.Unlock()
return transportInstance
}
// Update the interface methods
func (t *Transport) RegisterInterface(name string, iface common.NetworkInterface) error {
t.mutex.Lock()
defer t.mutex.Unlock()
@@ -340,8 +348,11 @@ func (t *Transport) GetInterface(name string) (common.NetworkInterface, error) {
return iface, nil
}
// Update the Close method
func (t *Transport) Close() error {
t.stopOnce.Do(func() {
close(t.done)
})
t.mutex.Lock()
defer t.mutex.Unlock()
@@ -483,14 +494,14 @@ func (t *Transport) UnregisterAnnounceHandler(handler announce.Handler) {
}
}
func (t *Transport) notifyAnnounceHandlers(destHash []byte, identity interface{}, appData []byte) {
func (t *Transport) notifyAnnounceHandlers(destHash []byte, identity interface{}, appData []byte, hops uint8) {
t.mutex.RLock()
handlers := make([]announce.Handler, len(t.announceHandlers))
copy(handlers, t.announceHandlers)
t.mutex.RUnlock()
for _, handler := range handlers {
if err := handler.ReceivedAnnounce(destHash, identity, appData); err != nil {
if err := handler.ReceivedAnnounce(destHash, identity, appData, hops); err != nil {
debug.Log(debug.DEBUG_ERROR, "Error in announce handler", "error", err)
}
}
@@ -607,7 +618,6 @@ func (t *Transport) RequestPath(destinationHash []byte, onInterface string, tag
return nil
}
// updatePathUnlocked updates path without acquiring mutex (caller must hold lock)
func (t *Transport) updatePathUnlocked(destinationHash []byte, nextHop []byte, interfaceName string, hops uint8) {
// Direct access to interfaces map since caller holds the lock
iface, exists := t.interfaces[interfaceName]
@@ -644,7 +654,11 @@ func (t *Transport) HandleAnnounce(data []byte, sourceIface common.NetworkInterf
appData := data[common.SIZE_16+common.SIZE_32+common.ONE:]
// Generate announce hash to check for duplicates
announceHash := sha256.Sum256(data)
// We exclude the hop count (byte 1) from the hash since it changes during propagation
// We also exclude the header (byte 0) just in case propagation flags change
// The destination hash (bytes 2-18) + payload (including random hash) is unique enough
hashData := data[common.TWO:]
announceHash := sha256.Sum256(hashData)
hashStr := string(announceHash[:])
t.mutex.Lock()
@@ -704,7 +718,7 @@ func (t *Transport) HandleAnnounce(data []byte, sourceIface common.NetworkInterf
}
// Notify handlers
t.notifyAnnounceHandlers(destHash, identity, appData)
t.notifyAnnounceHandlers(destHash, identity, appData, data[0])
return lastErr
}
@@ -745,7 +759,6 @@ func (p *LinkPacket) send() error {
header = append(header, 0x02) // Link packet type
header = append(header, p.Destination...)
// Add timestamp
ts := make([]byte, 8)
binary.BigEndian.PutUint64(ts, uint64(p.Timestamp.Unix())) // #nosec G115
header = append(header, ts...)
@@ -1074,7 +1087,11 @@ func (t *Transport) handleAnnouncePacket(data []byte, iface common.NetworkInterf
identity.Remember(data, destinationHash, pubKey, appData)
// Generate announce hash to check for duplicates
announceHash := sha256.Sum256(data)
// We exclude the hop count (byte 1) from the hash since it changes during propagation
// We also exclude the header (byte 0) just in case propagation flags change
// The destination hash (bytes 2-18) + payload (including random hash) is unique enough
hashData := data[common.TWO:]
announceHash := sha256.Sum256(hashData)
hashStr := string(announceHash[:])
debug.Log(debug.DEBUG_INFO, "Announce hash", "hash", fmt.Sprintf("%x", announceHash[:8]))
@@ -1102,7 +1119,7 @@ func (t *Transport) handleAnnouncePacket(data []byte, iface common.NetworkInterf
// Notify handlers first, regardless of forwarding limits
debug.Log(debug.DEBUG_INFO, "Notifying announce handlers", "destHash", fmt.Sprintf("%x", destinationHash), "appDataLen", len(appData))
t.notifyAnnounceHandlers(destinationHash, id, appData)
t.notifyAnnounceHandlers(destinationHash, id, appData, hopCount)
debug.Log(debug.DEBUG_INFO, "Announce handlers notified")
// Don't forward if max hops reached

View File

@@ -1,3 +1,5 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build js && wasm
// +build js,wasm
@@ -21,31 +23,82 @@ var (
reticulumTransport *transport.Transport
reticulumDest *destination.Destination
reticulumIdentity *identity.Identity
userName string
peerMap = make(map[string]string)
stats = struct {
packetsSent int
packetsReceived int
bytesSent int
bytesReceived int
}{}
packetCallback js.Value
announceHandler js.Value
)
// RegisterJSFunctions registers the Reticulum WASM API to the JavaScript global scope.
func RegisterJSFunctions() {
js.Global().Set("reticulum", js.ValueOf(map[string]interface{}{
"init": js.FuncOf(InitReticulum),
"getIdentity": js.FuncOf(GetIdentity),
"getDestination": js.FuncOf(GetDestination),
"announce": js.FuncOf(SendAnnounce),
"connect": js.FuncOf(ConnectWebSocket),
"disconnect": js.FuncOf(DisconnectWebSocket),
"isConnected": js.FuncOf(IsConnected),
"sendMessage": js.FuncOf(SendMessage),
"getStats": js.FuncOf(GetStats),
"init": js.FuncOf(InitReticulum),
"getIdentity": js.FuncOf(GetIdentity),
"getDestination": js.FuncOf(GetDestination),
"connect": js.FuncOf(ConnectWebSocket),
"disconnect": js.FuncOf(DisconnectWebSocket),
"isConnected": js.FuncOf(IsConnected),
"requestPath": js.FuncOf(RequestPath),
"getStats": js.FuncOf(GetStats),
"setPacketCallback": js.FuncOf(SetPacketCallback),
"setAnnounceCallback": js.FuncOf(SetAnnounceCallback),
"sendData": js.FuncOf(SendDataJS),
"announce": js.FuncOf(SendAnnounceJS),
}))
}
func SetPacketCallback(this js.Value, args []js.Value) interface{} {
if len(args) > 0 && args[0].Type() == js.TypeFunction {
packetCallback = args[0]
return js.ValueOf(true)
}
return js.ValueOf(false)
}
func SetAnnounceCallback(this js.Value, args []js.Value) interface{} {
if len(args) > 0 && args[0].Type() == js.TypeFunction {
announceHandler = args[0]
return js.ValueOf(true)
}
return js.ValueOf(false)
}
func RequestPath(this js.Value, args []js.Value) interface{} {
if len(args) < 1 {
return js.ValueOf(map[string]interface{}{
"error": "Destination hash required",
})
}
destHashHex := args[0].String()
destHash, err := hex.DecodeString(destHashHex)
if err != nil {
return js.ValueOf(map[string]interface{}{
"error": fmt.Sprintf("Invalid destination hash: %v", err),
})
}
if reticulumTransport == nil {
return js.ValueOf(map[string]interface{}{
"error": "Reticulum not initialized",
})
}
if err := reticulumTransport.RequestPath(destHash, "", nil, true); err != nil {
return js.ValueOf(map[string]interface{}{
"error": fmt.Sprintf("Failed to request path: %v", err),
})
}
return js.ValueOf(map[string]interface{}{
"success": true,
})
}
func GetStats(this js.Value, args []js.Value) interface{} {
return js.ValueOf(map[string]interface{}{
"packetsSent": stats.packetsSent,
@@ -68,8 +121,9 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
}
wsURL := args[0].String()
if len(args) >= 2 {
userName = args[1].String()
appName := "wasm_core"
if len(args) >= 2 && args[1].Type() == js.TypeString {
appName = args[1].String()
}
var id *identity.Identity
@@ -105,7 +159,7 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
id,
destination.IN,
destination.SINGLE,
"wasm_core",
appName,
t,
"browser",
)
@@ -118,16 +172,18 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
dest.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
stats.packetsReceived++
stats.bytesReceived += len(data)
js.Global().Call("onChatMessage", js.ValueOf(map[string]interface{}{
"text": string(data),
"from": "",
}))
if !packetCallback.IsUndefined() {
// Convert bytes to JS Uint8Array for performance and compatibility
uint8Array := js.Global().Get("Uint8Array").New(len(data))
js.CopyBytesToJS(uint8Array, data)
packetCallback.Invoke(uint8Array)
}
})
dest.SetProofStrategy(destination.PROVE_ALL)
t.RegisterAnnounceHandler(&announceHandler{})
t.RegisterAnnounceHandler(&genericAnnounceHandler{})
wsInterface, err := interfaces.NewWebSocketInterface("wasm0", wsURL, true)
if err != nil {
@@ -167,6 +223,16 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
})
}
// GetTransport returns the internal transport pointer.
func GetTransport() *transport.Transport {
return reticulumTransport
}
// GetDestinationPointer returns the internal destination pointer.
func GetDestinationPointer() *destination.Destination {
return reticulumDest
}
func GetIdentity(this js.Value, args []js.Value) interface{} {
if reticulumIdentity == nil {
return js.ValueOf(map[string]interface{}{
@@ -191,30 +257,19 @@ func GetDestination(this js.Value, args []js.Value) interface{} {
})
}
func SendAnnounce(this js.Value, args []js.Value) interface{} {
if reticulumDest == nil {
return js.ValueOf(map[string]interface{}{
"error": "Reticulum not initialized",
})
func IsConnected(this js.Value, args []js.Value) interface{} {
if reticulumTransport == nil {
return js.ValueOf(false)
}
var appData []byte
if len(args) >= 1 && args[0].String() != "" {
appData = []byte(args[0].String())
userName = args[0].String()
} else if userName != "" {
appData = []byte(userName)
ifaces := reticulumTransport.GetInterfaces()
for _, iface := range ifaces {
if iface.IsOnline() {
return js.ValueOf(true)
}
}
if err := reticulumDest.Announce(false, appData, nil); err != nil {
return js.ValueOf(map[string]interface{}{
"error": fmt.Sprintf("Failed to send announce: %v", err),
})
}
return js.ValueOf(map[string]interface{}{
"success": true,
})
return js.ValueOf(false)
}
func ConnectWebSocket(this js.Value, args []js.Value) interface{} {
@@ -228,7 +283,7 @@ func ConnectWebSocket(this js.Value, args []js.Value) interface{} {
for name, iface := range ifaces {
if iface.IsOnline() {
return js.ValueOf(map[string]interface{}{
"success": true,
"success": true,
"interface": name,
})
}
@@ -238,7 +293,7 @@ func ConnectWebSocket(this js.Value, args []js.Value) interface{} {
})
}
return js.ValueOf(map[string]interface{}{
"success": true,
"success": true,
"interface": name,
})
}
@@ -272,51 +327,37 @@ func DisconnectWebSocket(this js.Value, args []js.Value) interface{} {
})
}
func IsConnected(this js.Value, args []js.Value) interface{} {
if reticulumTransport == nil {
return js.ValueOf(false)
}
type genericAnnounceHandler struct{}
ifaces := reticulumTransport.GetInterfaces()
for _, iface := range ifaces {
if iface.IsOnline() {
return js.ValueOf(true)
}
}
return js.ValueOf(false)
}
type announceHandler struct{}
func (h *announceHandler) AspectFilter() []string {
func (h *genericAnnounceHandler) AspectFilter() []string {
return nil
}
func (h *announceHandler) ReceivePathResponses() bool {
func (h *genericAnnounceHandler) ReceivePathResponses() bool {
return false
}
func (h *announceHandler) ReceivedAnnounce(destHash []byte, ident interface{}, appData []byte) error {
hashStr := hex.EncodeToString(destHash)
peerMap[hashStr] = string(appData)
js.Global().Call("onPeerDiscovered", js.ValueOf(map[string]interface{}{
"hash": hashStr,
"appData": string(appData),
}))
func (h *genericAnnounceHandler) ReceivedAnnounce(destHash []byte, ident interface{}, appData []byte, hops uint8) error {
if !announceHandler.IsUndefined() {
hashStr := hex.EncodeToString(destHash)
announceHandler.Invoke(js.ValueOf(map[string]interface{}{
"hash": hashStr,
"appData": string(appData),
"hops": int(hops),
}))
}
return nil
}
func SendMessage(this js.Value, args []js.Value) interface{} {
// SendDataJS is the JS-facing wrapper for SendData
func SendDataJS(this js.Value, args []js.Value) interface{} {
if len(args) < 2 {
return js.ValueOf(map[string]interface{}{
"error": "Destination hash and message required",
"error": "Destination hash and data required",
})
}
destHashHex := args[0].String()
message := args[1].String()
destHash, err := hex.DecodeString(destHashHex)
if err != nil {
return js.ValueOf(map[string]interface{}{
@@ -324,6 +365,26 @@ func SendMessage(this js.Value, args []js.Value) interface{} {
})
}
// Support both string and Uint8Array data from JS
var data []byte
if args[1].Type() == js.TypeString {
data = []byte(args[1].String())
} else {
data = make([]byte, args[1].Length())
js.CopyBytesToGo(data, args[1])
}
return SendData(destHash, data)
}
// SendData is a generic function to send raw bytes to a destination
func SendData(destHash []byte, data []byte) interface{} {
if reticulumTransport == nil {
return js.ValueOf(map[string]interface{}{
"error": "Reticulum not initialized",
})
}
remoteIdentity, err := identity.Recall(destHash)
if err != nil {
return js.ValueOf(map[string]interface{}{
@@ -338,7 +399,7 @@ func SendMessage(this js.Value, args []js.Value) interface{} {
})
}
encrypted, err := targetDest.Encrypt([]byte(message))
encrypted, err := targetDest.Encrypt(data)
if err != nil {
return js.ValueOf(map[string]interface{}{
"error": fmt.Sprintf("Encryption failed: %v", err),
@@ -371,7 +432,42 @@ func SendMessage(this js.Value, args []js.Value) interface{} {
}
stats.packetsSent++
stats.bytesSent += len(message)
stats.bytesSent += len(data)
return js.ValueOf(map[string]interface{}{
"success": true,
})
}
// SendAnnounceJS is the JS-facing wrapper for SendAnnounce
func SendAnnounceJS(this js.Value, args []js.Value) interface{} {
var appData []byte
if len(args) >= 1 && args[0].Type() == js.TypeString {
appData = []byte(args[0].String())
} else if len(args) >= 1 && args[0].Type() == js.TypeObject {
appData = make([]byte, args[0].Length())
js.CopyBytesToGo(appData, args[0])
}
return SendAnnounce(appData)
}
// SendAnnounce is a generic function to send an announce
func SendAnnounce(appData []byte) interface{} {
if reticulumDest == nil {
return js.ValueOf(map[string]interface{}{
"error": "Reticulum not initialized",
})
}
if len(appData) > 0 {
reticulumDest.SetDefaultAppData(appData)
}
if err := reticulumDest.Announce(false, nil, nil); err != nil {
return js.ValueOf(map[string]interface{}{
"error": fmt.Sprintf("Failed to send announce: %v", err),
})
}
return js.ValueOf(map[string]interface{}{
"success": true,

147
pkg/wasm/wasm_test.go Normal file
View File

@@ -0,0 +1,147 @@
//go:build js && wasm
// +build js,wasm
package wasm
import (
"encoding/hex"
"fmt"
"syscall/js"
"testing"
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
)
func TestRegisterJSFunctions(t *testing.T) {
RegisterJSFunctions()
reticulum := js.Global().Get("reticulum")
if reticulum.IsUndefined() {
t.Fatal("reticulum object not registered in global scope")
}
functions := []string{
"init", "getIdentity", "getDestination", "announce",
"connect", "disconnect", "isConnected", "requestPath", "getStats",
"setPacketCallback", "setAnnounceCallback", "sendData",
}
for _, fn := range functions {
if reticulum.Get(fn).Type() != js.TypeFunction {
t.Errorf("function %s not registered or not a function", fn)
}
}
}
func TestGetStats(t *testing.T) {
// Reset stats
stats.packetsSent = 10
stats.packetsReceived = 5
stats.bytesSent = 100
stats.bytesReceived = 50
result := GetStats(js.Undefined(), nil)
val := result.(js.Value)
if val.Get("packetsSent").Int() != 10 {
t.Errorf("expected packetsSent 10, got %d", val.Get("packetsSent").Int())
}
if val.Get("packetsReceived").Int() != 5 {
t.Errorf("expected packetsReceived 5, got %d", val.Get("packetsReceived").Int())
}
}
func TestIsConnected(t *testing.T) {
reticulumTransport = nil
connected := IsConnected(js.Undefined(), nil).(js.Value).Bool()
if connected {
t.Error("expected connected to be false when transport is nil")
}
}
func TestInitReticulum(t *testing.T) {
// Mock JS global functions
js.Global().Set("log", js.FuncOf(func(this js.Value, args []js.Value) interface{} { return nil }))
// Test without arguments
result := InitReticulum(js.Undefined(), []js.Value{})
val := result.(js.Value)
if val.Get("error").IsUndefined() || val.Get("error").String() != "WebSocket URL required" {
t.Errorf("expected error 'WebSocket URL required', got %v", val.Get("error"))
}
// Test with valid URL and app name
wsURL := "ws://localhost:8080"
appName := "test_app"
result = InitReticulum(js.Undefined(), []js.Value{js.ValueOf(wsURL), js.ValueOf(appName)})
val = result.(js.Value)
if !val.Get("success").Bool() {
t.Errorf("InitReticulum failed: %v", val.Get("error"))
}
if reticulumIdentity == nil {
t.Fatal("reticulumIdentity should not be nil after successful init")
}
// Test with provided identity
id, _ := identity.NewIdentity()
idHex := id.GetHexHash()
// InitReticulum expects the FULL identity bytes in hex (64 bytes).
idBytes := id.GetPrivateKey()
idHexFull := hex.EncodeToString(idBytes)
result = InitReticulum(js.Undefined(), []js.Value{js.ValueOf(wsURL), js.ValueOf(appName), js.ValueOf(idHexFull)})
val = result.(js.Value)
if !val.Get("success").Bool() {
t.Errorf("InitReticulum with identity failed: %v", val.Get("error"))
}
if reticulumIdentity.GetHexHash() != idHex {
t.Errorf("expected identity hash %s, got %s", idHex, reticulumIdentity.GetHexHash())
}
}
func TestIdentityAndDestination(t *testing.T) {
// Ensure initialized
js.Global().Set("log", js.FuncOf(func(this js.Value, args []js.Value) interface{} { return nil }))
InitReticulum(js.Undefined(), []js.Value{js.ValueOf("ws://localhost")})
idResult := GetIdentity(js.Undefined(), nil).(js.Value)
if idResult.Get("hash").String() != reticulumIdentity.GetHexHash() {
t.Error("GetIdentity returned wrong hash")
}
destResult := GetDestination(js.Undefined(), nil).(js.Value)
expectedDest := fmt.Sprintf("%x", reticulumDest.GetHash())
if destResult.Get("hash").String() != expectedDest {
t.Errorf("GetDestination returned wrong hash, expected %s got %s", expectedDest, destResult.Get("hash").String())
}
}
func TestSendDataJS(t *testing.T) {
// Ensure initialized
InitReticulum(js.Undefined(), []js.Value{js.ValueOf("ws://localhost")})
// Create a mock peer
peerId, _ := identity.NewIdentity()
peerHash := peerId.Hash()
peerHashHex := hex.EncodeToString(peerHash)
// Manually add to known destinations so Recall works
identity.Remember([]byte("mock_packet"), peerHash, peerId.GetPublicKey(), []byte("peer_app_data"))
// Test SendDataJS with string
data := "Hello Peer!"
result := SendDataJS(js.Undefined(), []js.Value{js.ValueOf(peerHashHex), js.ValueOf(data)}).(js.Value)
if !result.Get("error").IsUndefined() {
errStr := result.Get("error").String()
if errStr != "Packet sending failed: no path to destination" {
t.Errorf("SendDataJS failed with unexpected error: %s", errStr)
}
} else if !result.Get("success").Bool() {
t.Errorf("SendDataJS failed without error message")
}
}