193 Commits

Author SHA1 Message Date
f097bb3241 security: use net.JoinHostPort instead of fmt.Sprintf
Some checks failed
Benchmark GC Performance / benchmark (push) Successful in 1m19s
Run Gosec / tests (push) Successful in 48s
Go Revive Lint / lint (push) Successful in 34s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 31s
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 34s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 29s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 27s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 48s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m28s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 27s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 27s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 27s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 27s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 27s
Performance Monitor / performance-monitor (push) Successful in 2m40s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 25s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 27s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
2025-11-09 00:05:36 -06:00
22fc5093db perf: combine multiple append calls in transport 2025-11-09 00:05:31 -06:00
fc95e54b2e update: Makefile with debug build target and update help text
Some checks failed
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 55s
Go Build Multi-Platform / build (amd64, darwin) (push) Failing after 58s
Go Build Multi-Platform / build (amd64, linux) (push) Failing after 54s
Benchmark GC Performance / benchmark (push) Successful in 1m28s
Go Build Multi-Platform / build (arm, freebsd) (push) Failing after 48s
Go Build Multi-Platform / build (arm, linux) (push) Failing after 46s
Go Build Multi-Platform / build (arm64, darwin) (push) Failing after 29s
Go Build Multi-Platform / build (arm64, windows) (push) Failing after 28s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m32s
Go Test Multi-Platform / Test (macos-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (windows-latest, amd64) (push) Has been cancelled
Go Test Multi-Platform / Test (macos-latest, arm64) (push) Has been cancelled
Go Build Multi-Platform / build (amd64, windows) (push) Failing after 50s
Go Build Multi-Platform / build (arm, windows) (push) Failing after 36s
Go Build Multi-Platform / build (arm64, linux) (push) Failing after 38s
Go Build Multi-Platform / build (arm64, freebsd) (push) Failing after 40s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 46s
Run Gosec / tests (push) Successful in 51s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Revive Lint / lint (push) Successful in 36s
Performance Monitor / performance-monitor (push) Has been cancelled
2025-11-09 00:01:19 -06:00
636d400f1e refactor: migrate to structured debug logging 2025-11-09 00:00:55 -06:00
fd5eb65bc0 fix: use context.TODO() in logger.Enabled check for improved clarity 2025-11-07 12:51:14 -06:00
4e13fe523b fix: resolve deepsource linter issues
- Fixed the append usage warning (CRT-D0001)
- Fixed the nil context warning (SCC-SA1012)
2025-11-07 12:47:47 -06:00
dd2cc3e3d9 update logging function to improve argument handling 2025-11-07 12:44:40 -06:00
353e9c6d9b Update logging to use new debug package
- Removed the old debug logging functions and replaced them with calls to the new debug package.
- Updated logging statements throughout the main.go file to utilize adjustable logging levels.
- Enhanced the clarity and structure of log messages for better debugging and traceability.
2025-11-07 12:41:29 -06:00
088ba3337d Add debug package for logging with adjustable levels
- Introduced a new debug package that allows for logging at various levels (1-7).
- Implemented initialization, logger retrieval, and logging functions.
- Added functionality to set and get the current debug level.
2025-11-07 12:41:14 -06:00
4cd2338095 Update README.md 2025-11-06 15:12:16 -06:00
c6cc1d8ca8 Update 2025-10-31 07:39:51 -05:00
0afb0e9ade Update 2025-10-31 07:39:41 -05:00
feeaa72102 Update GitHub Actions workflows
- Pin to full -length commit hash
- Add master alongside main
2025-10-31 07:39:34 -05:00
bb964445f3 add examples to gitignore for now 2025-10-30 18:57:48 -05:00
5369037a74 Add TinyGo build targets to Makefile and create GitHub Actions workflow for TinyGo builds 2025-10-30 18:54:23 -05:00
bb98248830 Update Go module dependencies and version in go.mod and go.sum files 2025-10-30 18:49:23 -05:00
575657bbc5 Remove cyclomatic and cognitive complexity rules from revive.toml configuration 2025-10-26 11:49:48 -05:00
8da4a759f5 Add FromHash method to create destinations from known hashes and enhance link handling capabilities 2025-10-07 22:44:08 -05:00
dff1489ee5 Add link ID generation in Establish method for connection requests 2025-10-07 22:44:02 -05:00
30c97bc9dd Implement identity recall functionality to retrieve existing identities by hash 2025-10-07 22:43:55 -05:00
005e2566aa Add destination registration and link handling in Transport 2025-10-07 22:43:45 -05:00
cc10830df3 Fix CreatePacket method in Announce struct to dynamically set context flag based on the existence of ratchet data. 2025-10-07 21:53:39 -05:00
b548e5711e Add destination type constants for packet handling in types.go 2025-10-07 21:31:45 -05:00
cc89bfef6e Update destination hash calculation in handleAnnouncePacket to use SHA256. 2025-10-07 21:31:40 -05:00
45a3ac1e87 Fix destination hash calculation by incorporating SHA256 for name hashing. 2025-10-07 21:31:16 -05:00
e39936ac30 Fix Announce struct to include destination name and hash validation.
Also update New and NewAnnounce functions to require destinationHash and destinationName parameters.
2025-10-07 21:30:57 -05:00
b601ae1c51 Add read loop for UDP interface to handle incoming packets asynchronously. 2025-10-07 21:30:27 -05:00
7d7a022736 Refine CPU usage calculation in performance monitoring script by truncating decimal values before summation. 2025-09-27 05:56:31 -05:00
0ac2a8d200 Add binary execution tests for Linux, macOS, and Windows in CI workflow. Enhance ARM cross-compilation testing with specific environment variables. 2025-09-27 05:54:30 -05:00
f3808a73e1 Remove caching step for Go modules in performance monitoring workflow 2025-09-27 05:51:43 -05:00
cb908fb143 Increase monitoring duration 2025-09-27 05:50:29 -05:00
f53194be25 Fix announce packet handling to align with RNS specification. Enhance payload parsing, signature verification, and destination hash validation. Improve logging for better debugging of announce packet processing. 2025-09-27 05:48:33 -05:00
ad732d1465 mark interface as online 2025-09-27 05:47:28 -05:00
b70a7d03af Add autointerface for testing 2025-09-27 05:47:17 -05:00
911fe3ea8e Add support for 32-byte Ed25519 2025-09-27 05:46:51 -05:00
b59bb349dc add basic performance monitoring action 2025-09-27 05:44:47 -05:00
08cbacd69f Update README badges for clarity and consistency in multi-platform testing 2025-09-27 05:44:03 -05:00
9a70a92261 Add support for multi-platform testing 2025-09-27 05:43:54 -05:00
be34168a1b Refine comment in TCPClientInterface to clarify HDLC framing usage for TCP connections 2025-09-27 04:41:36 -05:00
cebab6b2f3 Add debug logging and missing packet data 2025-09-27 04:41:25 -05:00
fdcb371582 Fix announce packet creation and sending logic to use the announce package. Enhance error handling and logging for interface checks during packet transmission. 2025-09-27 04:40:47 -05:00
f01b1f8bac Update Decrypt method in Identity to validate token structure and HMAC. Update extraction logic for ephemeral public key, ciphertext, and MAC, ensuring proper error handling for token size and HMAC validation. 2025-09-27 04:40:35 -05:00
a0eca36884 Update logging in HandlePacket and handleAnnouncePacket. 2025-09-27 04:30:59 -05:00
972d00df92 Fix TCPClientInterface readLoop and handlePacket methods to streamline HDLC framing logic and improve packet handling. Remove KISS framing support and update logging for received packets. Ensure outgoing data uses HDLC framing consistently. 2025-09-27 04:30:28 -05:00
483b6e562b Update announce packet creation and sending logic to utilize transport methods 2025-09-27 04:30:13 -05:00
cbb5ffa970 Cleanup incorrect or outdated code 2025-09-27 04:29:59 -05:00
b7cc0c39b4 Fix announce data parsing to include ratchet field and update length checks 2025-09-27 04:26:22 -05:00
982c173760 Add GitHub Actions workflow for benchmarking GC performance 2025-09-25 13:13:39 -05:00
49ca73ab3a Update TODO 2025-09-25 13:11:56 -05:00
43b224b4d7 Update README 2025-09-25 13:11:46 -05:00
456a95d569 Add benchmarking tests for packet and transport operations 2025-09-25 12:42:44 -05:00
53b2d18a79 Add benchmarking targets to Makefile for standard and experimental GC 2025-09-25 12:42:06 -05:00
8d7f86e15a Update README 2025-09-25 12:28:24 -05:00
40213eeac9 Add experimental greenteagc support, release build (strip debug symbols) and lint 2025-09-25 12:24:14 -05:00
5cb8b12a0f Update Dockerfiles to use Go version 1.25 with ARG for flexibility 2025-09-25 03:31:57 -05:00
2f165186d1 Update README 2025-09-25 03:29:24 -05:00
6cd3b15d78 Update Go version to 1.25 in workflow files 2025-09-25 03:29:16 -05:00
98c8d35f1e Update Go version to 1.25 and upgrade golang.org/x/crypto dependency to v0.42.0 2025-09-25 03:29:09 -05:00
064b2b10b8 Update revive.toml rules 2025-09-25 03:27:49 -05:00
a8d78d2784 Add Dockerfiles 2025-09-21 02:35:05 -05:00
5a0c70190f Add full-length commit hashes for actions for improved supply chain security. 2025-09-21 02:20:58 -05:00
d5bf7dc720 Update 2025-09-07 02:40:33 -05:00
8b4bca7939 Update interface registration logging 2025-09-07 02:24:37 -05:00
c004ff1a97 Fix packet header handling in Pack and Unpack methods to correct order of DestinationHash and TransportID. 2025-09-07 02:24:07 -05:00
38323da57d Fix hash calculation in Destination to use TruncatedHash and improve logging for interface announcements 2025-09-07 02:23:30 -05:00
2ffd12b3e1 Add Send method to TCPClientInterface and better logging in UDPInterface 2025-09-07 02:21:48 -05:00
069d4163eb Update README 2025-09-07 01:58:53 -05:00
93e1317789 Update wording for usage of LLMs 2025-09-07 01:58:41 -05:00
3b270e05c4 Update Go version to 1.24.6 and upgrade golang.org/x/crypto to v0.41.0 2025-09-07 01:58:22 -05:00
a05818b3a7 Add matrix link 2025-08-18 03:08:40 -05:00
df2b0a0079 Remove outdated development guidelines from CONTRIBUTING.md 2025-08-16 19:34:23 -05:00
c507e9125b Update README to include Go installation instructions 2025-08-16 19:34:15 -05:00
767110f3d0 Merge pull request #2 from MikeColes/paramter-order
match order of parameters to called function
2025-08-10 17:09:49 -05:00
Mike Coles
8e5f193caf match order of parameters to called function 2025-08-07 11:12:09 -04:00
fed33aadff add badge 2025-07-15 14:08:22 -05:00
d0c83ec1a2 update revive workflow 2025-07-15 14:06:18 -05:00
aa94bee606 fix workflow permissions 2025-07-15 14:02:22 -05:00
745609423f update 2025-07-15 14:01:10 -05:00
16e1c7e4eb add revive workflow 2025-07-15 13:59:27 -05:00
aec3672228 update 2025-07-15 13:55:04 -05:00
aace3abd6d update build workflow 2025-07-15 13:53:26 -05:00
ca3fefaae8 Add workflow permissions 2025-07-15 13:51:32 -05:00
d4f89735f6 add bearer 2025-07-15 13:51:13 -05:00
b37d393286 Update SECURITY.md to simplify vulnerability reporting instructions. 2025-07-15 13:51:07 -05:00
5e0c829cf6 Fix: Address various static analysis warnings
- **pkg/announce/announce.go**: Added error handling for `rand.Read` to log potential issues when generating random hashes.
- **pkg/buffer/buffer.go**: Removed a redundant `#nosec G115` comment as the line no longer triggers the warning.
- **pkg/cryptography/aes.go**: Added `#nosec G407` to explicitly acknowledge the use of `cipher.NewCBCEncrypter` which is acceptable in this context.
- **pkg/transport/transport.go**: Removed redundant `#nosec G115` comments as the lines no longer trigger the warning.
2025-07-15 13:45:48 -05:00
a80f2bb2ac Add a GetConfig method to the Transport struct. 2025-07-15 13:40:28 -05:00
7de206447a Migrate all AES encryption to AES-256-CBC and implement persistent ratchet storage. 2025-07-15 13:40:20 -05:00
f740514e2b Fix Destination announcing to use a dedicated announce package and improve transport integration. 2025-07-15 13:40:11 -05:00
b907dd93f1 Announce packet creation to strictly follow Reticulum specification. 2025-07-15 13:39:49 -05:00
011a6303eb Use destination-based announcing and consolidate ratchet path handling. 2025-07-15 13:39:39 -05:00
12f487d937 use AES-256-CBC only 2025-07-15 13:31:19 -05:00
b9aebc8406 gosec fixes and added #nosec where necassary 2025-07-06 00:33:50 -05:00
ffb3c3d4f4 Update Go version and x/crypto dependency to latest stable versions. 2025-07-06 00:09:53 -05:00
f291ba74e9 update 2025-07-06 00:09:41 -05:00
6e87fc9bcd go fmt 2025-07-06 00:09:14 -05:00
cb402e2bb6 add badges 2025-07-06 00:07:19 -05:00
fe5101340a Update TODO with AES 256 completion 2025-07-06 00:05:34 -05:00
dfac66e8bc add workflows 2025-07-06 00:05:11 -05:00
bc05835dae Add AES 256 and update AES test 2025-07-05 23:59:59 -05:00
Ivan
26371cdb6a Code cleanup of unused functions/variables 2025-05-07 18:35:45 -05:00
Ivan
41db0500af update x/crypto v0.37.0 > v0.38.0 2025-05-07 18:28:29 -05:00
Ivan
8114c3bda4 Add unit tests for configuration, cryptography, interfaces, and packet handling. 2025-05-07 18:24:52 -05:00
Ivan
3f141bf93b update 2025-05-07 18:24:07 -05:00
Ivan
a9bf658b03 update with badge 2025-05-07 18:23:50 -05:00
Ivan
ae9a35e3bb update 2025-05-04 03:47:13 -05:00
Ivan
32d32380d8 update license 2025-05-04 03:47:09 -05:00
Ivan
5e40f0bfe8 fix 2025-04-18 22:54:43 -05:00
315b35fc81 update 2025-04-18 22:53:45 -05:00
54dec6aa89 add 2025-04-18 22:53:42 -05:00
92c8faec11 update 2025-04-18 22:53:38 -05:00
2aff4989e5 Updated 2025-04-18 22:42:21 -05:00
f1d2a31be6 Updated 2025-04-18 22:42:12 -05:00
f604d1a3c8 create TODO 2025-04-18 22:41:54 -05:00
26a54436f7 remove 2025-04-18 22:41:48 -05:00
2fd85a1034 update x/crypto 2025-04-15 12:48:44 -05:00
c8e81cd9f0 Enhance node announcement handling and packet structure. Introduce node-specific metadata in the Reticulum struct, update announce packet creation to support new formats, and improve validation checks for announce data. Adjust minimum packet size requirements and refactor related functions for clarity and consistency. 2025-03-29 18:12:47 -05:00
2f61ce9bf3 remove github actions for now 2025-03-16 21:20:58 -05:00
b647e7c6c2 update 2025-03-12 00:25:39 -05:00
6b3990d399 more platforms 2025-03-11 23:06:56 -05:00
041b439a66 fix RTT for specific platforms 2025-03-11 23:06:26 -05:00
534982b99d remove old code 2025-03-11 22:51:38 -05:00
7379d07aba add revive config 2025-03-11 22:51:31 -05:00
03345bc256 update 2025-03-11 22:51:25 -05:00
e486923e8f update 2025-03-11 22:32:36 -05:00
d7f41b785f update 2025-03-11 22:22:55 -05:00
15303a21dc update 2025-03-11 22:22:51 -05:00
4d4863aeeb format 2025-03-11 22:18:40 -05:00
76a4103a56 cleanup 2025-03-11 22:18:09 -05:00
96348ce349 add: gosec 2025-03-11 22:18:02 -05:00
Sudo-Ivan
322711ba20 update 2025-02-14 13:06:11 -06:00
Sudo-Ivan
772248b31f update Go to 1.24 2025-02-14 13:05:47 -06:00
Ivan
fa1c80169e Update README.md 2025-02-14 16:04:46 +00:00
Sudo-Ivan
cb1e4a1115 update 2025-01-25 13:55:19 -06:00
Sudo-Ivan
836e97b17d add 2025-01-25 13:47:32 -06:00
Sudo-Ivan
87d3b4a58b update interfaces 2025-01-05 16:24:58 -06:00
Sudo-Ivan
77729e07e1 packet interceptor 2025-01-04 18:37:53 -06:00
Sudo-Ivan
79e1caa815 0.3.7 - announce packet improvements, cryptgraphy pkg, cleanup 2025-01-04 18:20:36 -06:00
Sudo-Ivan
a5b905bbaf cleanup/sort 2025-01-04 18:17:33 -06:00
Sudo-Ivan
c870406244 move cryptography to its own folder/files 2025-01-04 18:17:13 -06:00
Sudo-Ivan
ea8daf6bb2 0.3.6 - announce packet creation 2025-01-02 14:10:31 -06:00
Sudo-Ivan
d79406e354 update 2025-01-02 13:51:21 -06:00
Sudo-Ivan
f9b8d29780 update 2025-01-02 13:45:57 -06:00
Sudo-Ivan
0cebfb2193 update 2025-01-01 19:14:36 -06:00
Sudo-Ivan
9e229287e8 update 2025-01-01 19:12:40 -06:00
Sudo-Ivan
9508e6e195 update packet creation 2025-01-01 19:12:32 -06:00
Sudo-Ivan
5acbef454f 0.3.5 2025-01-01 18:31:58 -06:00
Sudo-Ivan
0862830431 0.3.4 2025-01-01 17:00:11 -06:00
Sudo-Ivan
6cdc02346f update 2025-01-01 03:12:26 -06:00
Sudo-Ivan
3ffd5b72a1 update 2025-01-01 02:03:12 -06:00
Sudo-Ivan
73af84e24f 0.3.3 2025-01-01 01:41:16 -06:00
Sudo-Ivan
ae40d2879c update and format 2025-01-01 00:58:37 -06:00
Sudo-Ivan
a2499e4a15 update 2025-01-01 00:46:47 -06:00
Sudo-Ivan
30ea1dd0c7 0.3.2 2025-01-01 00:40:25 -06:00
Sudo-Ivan
785bc7d782 update clients 2024-12-31 19:26:30 -06:00
Sudo-Ivan
144f5bea6a update 2024-12-31 19:26:21 -06:00
Sudo-Ivan
a3c701e205 add to-do item 2024-12-31 19:25:31 -06:00
Sudo-Ivan
a8a7607eb6 update binary name 2024-12-31 19:25:23 -06:00
Sudo-Ivan
a2947a3adb update announce.go 2024-12-31 17:07:23 -06:00
Sudo-Ivan
2cb37102fb update 2024-12-31 17:02:57 -06:00
Sudo-Ivan
54c401e2a5 buffer, channel, more transport constants 2024-12-31 17:02:51 -06:00
Sudo-Ivan
8df4039b18 update 2024-12-31 15:30:39 -06:00
Sudo-Ivan
12156adae9 update 2024-12-31 15:22:31 -06:00
Sudo-Ivan
a34e3d274e fix PGP key 2024-12-31 15:20:14 -06:00
Sudo-Ivan
f3d22dfcd4 0.3.1 2024-12-31 15:15:06 -06:00
Sudo-Ivan
99d8e44182 update 2024-12-31 14:41:05 -06:00
Sudo-Ivan
083991c997 add Makefile 2024-12-31 14:34:04 -06:00
Sudo-Ivan
9ca24d96ab move 2024-12-31 14:33:58 -06:00
Sudo-Ivan
b478ca346e update 2024-12-31 14:30:38 -06:00
Sudo-Ivan
20b532e005 update 2024-12-31 14:19:49 -06:00
Sudo-Ivan
80eac50632 update 2024-12-31 14:12:55 -06:00
Sudo-Ivan
f15d8f6a84 move To-Do to README 2024-12-31 14:12:17 -06:00
Sudo-Ivan
c523d6f542 update 2024-12-31 14:11:44 -06:00
Sudo-Ivan
8a175e3051 update 2024-12-31 14:00:10 -06:00
Sudo-Ivan
28d46921d3 0.3.0 2024-12-31 13:49:05 -06:00
Sudo-Ivan
613ceddb0b update common packages 2024-12-31 13:48:22 -06:00
Sudo-Ivan
599dd91979 announce: ratchet support, trunacted hash fix 2024-12-31 13:48:22 -06:00
deepsource-io[bot]
e724886578 ci: add .deepsource.toml 2024-12-31 17:51:22 +00:00
Sudo-Ivan
3034c0b0b4 link physical stats 2024-12-31 11:30:30 -06:00
Sudo-Ivan
3ed2c67742 interface: add interface mods, types and more 2024-12-31 11:22:37 -06:00
Sudo-Ivan
f2c146b7c5 transport: update constants, functions 2024-12-31 11:21:55 -06:00
Sudo-Ivan
59cef5e56a ephermeral keypair, ratchets, shared secret and more 2024-12-31 10:39:31 -06:00
Sudo-Ivan
ef613cc873 0.2.9 - rachets and cryptography 2024-12-30 23:41:18 -06:00
Sudo-Ivan
7a7ce84778 0.2.8 2024-12-30 12:58:43 -06:00
Sudo-Ivan
7ef7e60a87 0.2.7 2024-12-30 11:35:11 -06:00
Sudo-Ivan
73349d4a28 add: AES-CBC 2024-12-30 11:25:45 -06:00
Sudo-Ivan
31128a6758 0.2.6 2024-12-30 11:09:25 -06:00
Sudo-Ivan
566ce5da96 update 2024-12-30 04:22:01 -06:00
Sudo-Ivan
139926be05 0.2.5 2024-12-30 04:00:52 -06:00
Sudo-Ivan
decbd8f29a 0.2.4 2024-12-30 03:50:52 -06:00
Sudo-Ivan
0f5f5cbb13 0.2.3 2024-12-30 02:54:49 -06:00
Sudo-Ivan
a2476c9551 0.2.2 2024-12-30 02:43:35 -06:00
Sudo-Ivan
bfc75a2290 0.2.1 2024-12-30 02:34:38 -06:00
Sudo-Ivan
2e01fa565d update 0.2.0 2024-12-30 02:26:51 -06:00
74 changed files with 10069 additions and 1940 deletions

7
.deepsource.toml Normal file
View File

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

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

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

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

@@ -0,0 +1,87 @@
name: Go Build Multi-Platform
on:
push:
branches: [ "main", "master" ]
tags:
- 'v*'
pull_request:
branches: [ "main", "master" ]
jobs:
build:
permissions:
contents: write
strategy:
matrix:
goos: [linux, windows, darwin, freebsd]
goarch: [amd64, arm64, arm]
exclude:
- goos: darwin
goarch: arm
runs-on: ubuntu-latest
outputs:
build_complete: ${{ steps.build_step.outcome == 'success' }}
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: '1.25'
- name: Build
id: build_step
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarch == 'arm' && '6' || '' }}
run: |
output_name="reticulum-go-${GOOS}-${GOARCH}"
if [ "$GOOS" = "windows" ]; then
output_name+=".exe"
fi
go build -v -ldflags="-s -w" -o "${output_name}" ./cmd/reticulum-go
echo "Built: ${output_name}"
- name: Calculate SHA256 Checksum
run: |
output_name="reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}"
if [ "${{ matrix.goos }}" = "windows" ]; then
output_name+=".exe"
fi
sha256sum "${output_name}" > "${output_name}.sha256"
echo "Calculated SHA256 for ${output_name}"
- name: Upload Artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}
path: reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}*
release:
name: Create Release
runs-on: ubuntu-latest
needs: build
if: startsWith(github.ref, 'refs/tags/')
permissions:
contents: write
steps:
- name: Download All Build Artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
path: ./release-assets
- name: List downloaded files (for debugging)
run: ls -R ./release-assets
- name: Create GitHub Release
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
with:
files: ./release-assets/*/*

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

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

27
.github/workflows/gosec.yml vendored Normal file
View File

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

View File

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

29
.github/workflows/revive.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Go Revive Lint
on:
push:
branches: [ "main", "master" ]
pull_request:
branches: [ "main", "master" ]
jobs:
lint:
permissions:
contents: read
pull-requests: read
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: '1.25'
- name: Install revive
run: go install github.com/mgechev/revive@latest
- name: Run revive
run: |
revive -config revive.toml -formatter stylish ./...

64
.github/workflows/tinygo.yml vendored Normal file
View File

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

9
.gitignore vendored
View File

@@ -1,8 +1,9 @@
reticulum-client
reticulum-server
bin/
logs/
*.log
.env
.json
bin/
examples/

35
CONTRIBUTING.md Normal file
View File

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

9
LICENSE Normal file
View File

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

156
Makefile Normal file
View File

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

@@ -1,4 +1,64 @@
[![Socket Badge](https://socket.dev/api/badge/go/package/github.com/sudo-ivan/reticulum-go?version=v0.4.0)](https://socket.dev/go/package/github.com/sudo-ivan/reticulum-go)
![Multi-Platform Tests](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/go-test.yml/badge.svg)
![Gosec Scan](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/gosec.yml/badge.svg)
[![Multi-Platform Build](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/build.yml/badge.svg)](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/build.yml)
[![Revive Linter](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/revive.yml/badge.svg)](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/revive.yml)
# Reticulum-Go
Reticulum Network Stack in Go.
A Go implementation of the [Reticulum Network Protocol](https://github.com/markqvist/Reticulum).
> [!WARNING]
> This project is currently in development and is not yet compatible with the Python reference implementation.
## Goals
- To be fully compatible with the Python reference implementation.
- Additional privacy and security features.
- Support for a broader range of platforms and architectures old and new.
## Quick Start
### Prerequisites
- Go 1.24 or later
### Build
```bash
make build
```
### Run
```bash
make run
```
### Test
```bash
make test
```
## Embedded systems and WebAssembly
For building for WebAssembly and embedded systems, see the [tinygo branch](https://github.com/Sudo-Ivan/Reticulum-Go/tree/tinygo). Requires TinyGo 0.37.0+.
```bash
make tinygo-build
make tinygo-wasm
```
### Experimental Features
Build with experimental Green Tea GC (Go 1.25+):
```bash
make build-experimental
```
## Official Channels
- [Telegram](https://t.me/reticulum_go)
- [Matrix](https://matrix.to/#/#reticulum-go-dev:matrix.org)

25
SECURITY.md Normal file
View File

@@ -0,0 +1,25 @@
# Security Policy
We use [Socket](https://socket.dev/), [Deepsource](https://deepsource.com/) and [gosec](https://github.com/securego/gosec) for this project.
## Supply Chain Security
- All actions are pinned to a commit hash.
## Cryptography Dependencies
- golang.org/x/crypto for core cryptographic primitives
- hkdf
- curve25519
- go/crypto
- ed25519
- sha256
- rand
- aes
- cipher
- hmac
## Reporting a Vulnerability
Please report any security vulnerabilities using Github reporting tool or email to [rns@quad4.io](mailto:rns@quad4.io)

172
TODO.md Normal file
View File

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

102
To-Do
View File

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

View File

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

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

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

View File

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

View File

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

View File

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

39
docker/Dockerfile Normal file
View File

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

27
docker/Dockerfile.build Normal file
View File

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

9
go.mod
View File

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

22
go.sum
View File

@@ -1,8 +1,14 @@
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,28 +1,31 @@
package config
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/pelletier/go-toml"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
)
const (
DefaultSharedInstancePort = 37428
DefaultInstanceControlPort = 37429
DefaultLogLevel = 4
DefaultLogLevel = 4
)
func DefaultConfig() *common.ReticulumConfig {
return &common.ReticulumConfig{
EnableTransport: false,
ShareInstance: true,
SharedInstancePort: DefaultSharedInstancePort,
InstanceControlPort: DefaultInstanceControlPort,
EnableTransport: true,
ShareInstance: true,
SharedInstancePort: DefaultSharedInstancePort,
InstanceControlPort: DefaultInstanceControlPort,
PanicOnInterfaceErr: false,
LogLevel: DefaultLogLevel,
Interfaces: make(map[string]common.InterfaceConfig),
Interfaces: make(map[string]*common.InterfaceConfig),
}
}
@@ -31,7 +34,7 @@ func GetConfigPath() (string, error) {
if err != nil {
return "", err
}
return filepath.Join(homeDir, ".reticulum", "config"), nil
return filepath.Join(homeDir, ".reticulum-go", "config"), nil
}
func EnsureConfigDir() error {
@@ -40,65 +43,212 @@ func EnsureConfigDir() error {
return err
}
configDir := filepath.Join(homeDir, ".reticulum")
return os.MkdirAll(configDir, 0755)
configDir := filepath.Join(homeDir, ".reticulum-go")
return os.MkdirAll(configDir, 0700) // #nosec G301
}
// parseValue parses string values into appropriate types
func parseValue(value string) interface{} {
value = strings.TrimSpace(value)
// Try bool
if value == "true" {
return true
}
if value == "false" {
return false
}
// Try int
if i, err := strconv.Atoi(value); err == nil {
return i
}
// Return as string
return value
}
// LoadConfig loads the configuration from the specified path
func LoadConfig(path string) (*common.ReticulumConfig, error) {
data, err := os.ReadFile(path)
file, err := os.Open(path) // #nosec G304
if err != nil {
return nil, err
}
defer file.Close()
cfg := DefaultConfig()
if err := toml.Unmarshal(data, cfg); err != nil {
return nil, err
cfg.ConfigPath = path
scanner := bufio.NewScanner(file)
var currentInterface *common.InterfaceConfig
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// Skip comments and empty lines
if line == "" || strings.HasPrefix(line, "#") {
continue
}
// Handle interface sections
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
name := strings.Trim(line, "[]")
currentInterface = &common.InterfaceConfig{Name: name}
cfg.Interfaces[name] = currentInterface
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
if currentInterface != nil {
// Parse interface config
switch key {
case "type":
currentInterface.Type = value
case "enabled":
currentInterface.Enabled = value == "true"
case "address":
currentInterface.Address = value
case "port":
currentInterface.Port, _ = strconv.Atoi(value)
case "target_host":
currentInterface.TargetHost = value
case "target_port":
currentInterface.TargetPort, _ = strconv.Atoi(value)
case "discovery_port":
currentInterface.DiscoveryPort, _ = strconv.Atoi(value)
case "data_port":
currentInterface.DataPort, _ = strconv.Atoi(value)
case "discovery_scope":
currentInterface.DiscoveryScope = value
case "group_id":
currentInterface.GroupID = value
}
} else {
// Parse global config
switch key {
case "enable_transport":
cfg.EnableTransport = value == "true"
case "share_instance":
cfg.ShareInstance = value == "true"
case "shared_instance_port":
cfg.SharedInstancePort, _ = strconv.Atoi(value)
case "instance_control_port":
cfg.InstanceControlPort, _ = strconv.Atoi(value)
case "panic_on_interface_error":
cfg.PanicOnInterfaceErr = value == "true"
case "loglevel":
cfg.LogLevel, _ = strconv.Atoi(value)
}
}
}
cfg.ConfigPath = path
return cfg, nil
}
// SaveConfig saves the configuration to the specified path
func SaveConfig(cfg *common.ReticulumConfig) error {
data, err := toml.Marshal(cfg)
if err != nil {
return err
if cfg.ConfigPath == "" {
return fmt.Errorf("config path not set")
}
return os.WriteFile(cfg.ConfigPath, data, 0644)
var builder strings.Builder
// Write global config
builder.WriteString("# Reticulum Configuration\n")
builder.WriteString(fmt.Sprintf("enable_transport = %v\n", cfg.EnableTransport))
builder.WriteString(fmt.Sprintf("share_instance = %v\n", cfg.ShareInstance))
builder.WriteString(fmt.Sprintf("shared_instance_port = %d\n", cfg.SharedInstancePort))
builder.WriteString(fmt.Sprintf("instance_control_port = %d\n", cfg.InstanceControlPort))
builder.WriteString(fmt.Sprintf("panic_on_interface_error = %v\n", cfg.PanicOnInterfaceErr))
builder.WriteString(fmt.Sprintf("loglevel = %d\n\n", cfg.LogLevel))
// Write interface configs
for name, iface := range cfg.Interfaces {
builder.WriteString(fmt.Sprintf("[%s]\n", name))
builder.WriteString(fmt.Sprintf("type = %s\n", iface.Type))
builder.WriteString(fmt.Sprintf("enabled = %v\n", iface.Enabled))
if iface.Address != "" {
builder.WriteString(fmt.Sprintf("address = %s\n", iface.Address))
}
if iface.Port != 0 {
builder.WriteString(fmt.Sprintf("port = %d\n", iface.Port))
}
if iface.TargetHost != "" {
builder.WriteString(fmt.Sprintf("target_host = %s\n", iface.TargetHost))
}
if iface.TargetPort != 0 {
builder.WriteString(fmt.Sprintf("target_port = %d\n", iface.TargetPort))
}
if iface.DiscoveryPort != 0 {
builder.WriteString(fmt.Sprintf("discovery_port = %d\n", iface.DiscoveryPort))
}
if iface.DataPort != 0 {
builder.WriteString(fmt.Sprintf("data_port = %d\n", iface.DataPort))
}
if iface.DiscoveryScope != "" {
builder.WriteString(fmt.Sprintf("discovery_scope = %s\n", iface.DiscoveryScope))
}
if iface.GroupID != "" {
builder.WriteString(fmt.Sprintf("group_id = %s\n", iface.GroupID))
}
builder.WriteString("\n")
}
return os.WriteFile(cfg.ConfigPath, []byte(builder.String()), 0600) // #nosec G306
}
// CreateDefaultConfig creates a default configuration file
func CreateDefaultConfig(path string) error {
cfg := DefaultConfig()
cfg.ConfigPath = path
// Add default interface
cfg.Interfaces["Default Interface"] = common.InterfaceConfig{
Type: "AutoInterface",
Enabled: false,
// Add Auto Interface
cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{
Type: "AutoInterface",
Enabled: true,
GroupID: "reticulum",
DiscoveryScope: "link",
DiscoveryPort: 29716,
DataPort: 42671,
}
// Add default quad4net interface
cfg.Interfaces["quad4net tcp"] = common.InterfaceConfig{
// Add default interfaces
cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
Type: "TCPClientInterface",
Enabled: true,
TargetHost: "127.0.0.1",
TargetPort: 4242,
Name: "Go-RNS-Testnet",
}
cfg.Interfaces["Quad4 TCP"] = &common.InterfaceConfig{
Type: "TCPClientInterface",
Enabled: true,
TargetHost: "rns.quad4.io",
TargetPort: 4242,
Name: "Quad4 TCP",
}
data, err := toml.Marshal(cfg)
if err != nil {
cfg.Interfaces["Local UDP"] = &common.InterfaceConfig{
Type: "UDPInterface",
Enabled: false,
Address: "0.0.0.0",
Port: 37696,
}
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { // #nosec G301
return err
}
// Create config directory if it doesn't exist
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
return os.WriteFile(path, data, 0644)
return SaveConfig(cfg)
}
// InitConfig initializes the configuration system
@@ -118,4 +268,4 @@ func InitConfig() (*common.ReticulumConfig, error) {
// Load config
return LoadConfig(configPath)
}
}

View File

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

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

@@ -0,0 +1,7 @@
package announce
type Handler interface {
AspectFilter() []string
ReceivedAnnounce(destHash []byte, identity interface{}, appData []byte) error
ReceivePathResponses() bool
}

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

@@ -0,0 +1,252 @@
package buffer
import (
"bufio"
"bytes"
"compress/bzip2"
"encoding/binary"
"io"
"sync"
"github.com/Sudo-Ivan/reticulum-go/pkg/channel"
)
const (
StreamIDMax = 0x3fff // 16383
MaxChunkLen = 16 * 1024
MaxDataLen = 457 // MDU - 2 - 6 (2 for stream header, 6 for channel envelope)
CompressTries = 4
)
type StreamDataMessage struct {
StreamID uint16
Data []byte
EOF bool
Compressed bool
}
func (m *StreamDataMessage) Pack() ([]byte, error) {
headerVal := uint16(m.StreamID & StreamIDMax)
if m.EOF {
headerVal |= 0x8000
}
if m.Compressed {
headerVal |= 0x4000
}
buf := new(bytes.Buffer)
if err := binary.Write(buf, binary.BigEndian, headerVal); err != nil { // #nosec G104
return nil, err // Or handle the error appropriately
}
buf.Write(m.Data)
return buf.Bytes(), nil
}
func (m *StreamDataMessage) GetType() uint16 {
return 0x01 // Assign appropriate message type constant
}
func (m *StreamDataMessage) Unpack(data []byte) error {
if len(data) < 2 {
return io.ErrShortBuffer
}
header := binary.BigEndian.Uint16(data[:2])
m.StreamID = header & StreamIDMax
m.EOF = (header & 0x8000) != 0
m.Compressed = (header & 0x4000) != 0
m.Data = data[2:]
return nil
}
type RawChannelReader struct {
streamID int
channel *channel.Channel
buffer *bytes.Buffer
eof bool
callbacks []func(int)
mutex sync.RWMutex
}
func NewRawChannelReader(streamID int, ch *channel.Channel) *RawChannelReader {
reader := &RawChannelReader{
streamID: streamID,
channel: ch,
buffer: bytes.NewBuffer(nil),
callbacks: make([]func(int), 0),
}
ch.AddMessageHandler(reader.HandleMessage)
return reader
}
func (r *RawChannelReader) AddReadyCallback(cb func(int)) {
r.mutex.Lock()
defer r.mutex.Unlock()
r.callbacks = append(r.callbacks, cb)
}
func (r *RawChannelReader) RemoveReadyCallback(cb func(int)) {
r.mutex.Lock()
defer r.mutex.Unlock()
for i, fn := range r.callbacks {
if &fn == &cb {
r.callbacks = append(r.callbacks[:i], r.callbacks[i+1:]...)
break
}
}
}
func (r *RawChannelReader) Read(p []byte) (n int, err error) {
r.mutex.Lock()
defer r.mutex.Unlock()
if r.buffer.Len() == 0 && r.eof {
return 0, io.EOF
}
n, err = r.buffer.Read(p)
if err == io.EOF && !r.eof {
err = nil
}
return
}
func (r *RawChannelReader) HandleMessage(msg channel.MessageBase) bool { // #nosec G115
if streamMsg, ok := msg.(*StreamDataMessage); ok && streamMsg.StreamID == uint16(r.streamID) {
r.mutex.Lock()
defer r.mutex.Unlock()
if streamMsg.Compressed {
decompressed := decompressData(streamMsg.Data)
r.buffer.Write(decompressed)
} else {
r.buffer.Write(streamMsg.Data)
}
if streamMsg.EOF {
r.eof = true
}
// Notify callbacks
for _, cb := range r.callbacks {
cb(r.buffer.Len())
}
return true
}
return false
}
type RawChannelWriter struct {
streamID int
channel *channel.Channel
eof bool
}
func NewRawChannelWriter(streamID int, ch *channel.Channel) *RawChannelWriter {
return &RawChannelWriter{
streamID: streamID,
channel: ch,
}
}
func (w *RawChannelWriter) Write(p []byte) (n int, err error) {
if len(p) > MaxChunkLen {
p = p[:MaxChunkLen]
}
msg := &StreamDataMessage{
StreamID: uint16(w.streamID), // #nosec G115
Data: p,
EOF: w.eof,
}
if len(p) > 32 {
for try := 1; try < CompressTries; try++ {
chunkLen := len(p) / try
compressed := compressData(p[:chunkLen])
if len(compressed) < MaxDataLen && len(compressed) < chunkLen {
msg.Data = compressed
msg.Compressed = true
break
}
}
}
if err := w.channel.Send(msg); err != nil {
return 0, err
}
return len(p), nil
}
func (w *RawChannelWriter) Close() error {
w.eof = true
_, err := w.Write(nil)
return err
}
type Buffer struct {
ReadWriter *bufio.ReadWriter
}
func (b *Buffer) Write(p []byte) (n int, err error) {
return b.ReadWriter.Write(p)
}
func (b *Buffer) Read(p []byte) (n int, err error) {
return b.ReadWriter.Read(p)
}
func (b *Buffer) Close() error {
if err := b.ReadWriter.Writer.Flush(); err != nil {
return err
}
return nil
}
func CreateReader(streamID int, ch *channel.Channel, readyCallback func(int)) *bufio.Reader {
raw := NewRawChannelReader(streamID, ch)
if readyCallback != nil {
raw.AddReadyCallback(readyCallback)
}
return bufio.NewReader(raw)
}
func CreateWriter(streamID int, ch *channel.Channel) *bufio.Writer {
raw := NewRawChannelWriter(streamID, ch)
return bufio.NewWriter(raw)
}
func CreateBidirectionalBuffer(receiveStreamID, sendStreamID int, ch *channel.Channel, readyCallback func(int)) *bufio.ReadWriter {
reader := CreateReader(receiveStreamID, ch, readyCallback)
writer := CreateWriter(sendStreamID, ch)
return bufio.NewReadWriter(reader, writer)
}
func compressData(data []byte) []byte {
var compressed bytes.Buffer
w := bytes.NewBuffer(data)
r := bzip2.NewReader(w)
_, err := io.Copy(&compressed, r) // #nosec G104 #nosec G110
if err != nil {
// Handle error, e.g., log it or return an error
return nil
}
return compressed.Bytes()
}
func decompressData(data []byte) []byte {
reader := bzip2.NewReader(bytes.NewReader(data))
var decompressed bytes.Buffer
// Limit the amount of data read to prevent decompression bombs
limitedReader := io.LimitReader(reader, MaxChunkLen) // #nosec G110
_, err := io.Copy(&decompressed, limitedReader)
if err != nil {
// Handle error, e.g., log it or return an error
return nil
}
return decompressed.Bytes()
}

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

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

View File

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

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

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

View File

@@ -1,28 +1,61 @@
package common
const (
// Interface Types
IF_TYPE_UDP InterfaceType = iota
IF_TYPE_TCP
IF_TYPE_UNIX
// Interface Types
IF_TYPE_NONE InterfaceType = iota
IF_TYPE_UDP
IF_TYPE_TCP
IF_TYPE_UNIX
IF_TYPE_I2P
IF_TYPE_BLUETOOTH
IF_TYPE_SERIAL
IF_TYPE_AUTO
// Interface Modes
IF_MODE_FULL InterfaceMode = iota
IF_MODE_POINT
IF_MODE_GATEWAY
// Interface Modes
IF_MODE_FULL InterfaceMode = iota
IF_MODE_POINT
IF_MODE_GATEWAY
IF_MODE_ACCESS_POINT
IF_MODE_ROAMING
IF_MODE_BOUNDARY
// Transport Modes
TRANSPORT_MODE_DIRECT TransportMode = iota
TRANSPORT_MODE_RELAY
TRANSPORT_MODE_GATEWAY
// Transport Modes
TRANSPORT_MODE_DIRECT TransportMode = iota
TRANSPORT_MODE_RELAY
TRANSPORT_MODE_GATEWAY
// Path Status
PATH_STATUS_UNKNOWN PathStatus = iota
PATH_STATUS_DIRECT
PATH_STATUS_RELAY
PATH_STATUS_FAILED
// Path Status
PATH_STATUS_UNKNOWN PathStatus = iota
PATH_STATUS_DIRECT
PATH_STATUS_RELAY
PATH_STATUS_FAILED
// Common Constants
DEFAULT_MTU = 1500
MAX_PACKET_SIZE = 65535
)
// Resource Status
RESOURCE_STATUS_PENDING = 0x00
RESOURCE_STATUS_ACTIVE = 0x01
RESOURCE_STATUS_COMPLETE = 0x02
RESOURCE_STATUS_FAILED = 0x03
RESOURCE_STATUS_CANCELLED = 0x04
// Link Status
LINK_STATUS_PENDING = 0x00
LINK_STATUS_ACTIVE = 0x01
LINK_STATUS_CLOSED = 0x02
LINK_STATUS_FAILED = 0x03
// Direction Constants
IN = 0x01
OUT = 0x02
// Common Constants
DEFAULT_MTU = 1500
MAX_PACKET_SIZE = 65535
BITRATE_MINIMUM = 5
// Timeouts and Intervals
ESTABLISH_TIMEOUT = 6
KEEPALIVE_INTERVAL = 360
STALE_TIME = 720
PATH_REQUEST_TTL = 300
ANNOUNCE_TIMEOUT = 15
)

View File

@@ -1,57 +1,211 @@
package common
import (
"net"
"sync"
"time"
"encoding/binary"
"net"
"sync"
"time"
)
// NetworkInterface combines both low-level and high-level interface requirements
// NetworkInterface defines the interface for all network communication methods
type NetworkInterface interface {
// Low-level network operations
Start() error
Stop() error
Send(data []byte, address string) error
Receive() ([]byte, string, error)
GetType() InterfaceType
GetMode() InterfaceMode
GetMTU() int
// High-level packet operations
ProcessIncoming([]byte)
ProcessOutgoing([]byte) error
SendPathRequest([]byte) error
SendLinkPacket([]byte, []byte, time.Time) error
Detach()
SetPacketCallback(PacketCallback)
// Additional required fields
GetName() string
GetConn() net.Conn
IsEnabled() bool
// Core interface operations
Start() error
Stop() error
Enable()
Disable()
Detach()
// Network operations
Send(data []byte, address string) error
GetConn() net.Conn
GetMTU() int
GetName() string
// Interface properties
GetType() InterfaceType
GetMode() InterfaceMode
IsEnabled() bool
IsOnline() bool
IsDetached() bool
GetBandwidthAvailable() bool
// Packet handling
ProcessIncoming([]byte)
ProcessOutgoing([]byte) error
SendPathRequest([]byte) error
SendLinkPacket([]byte, []byte, time.Time) error
SetPacketCallback(PacketCallback)
GetPacketCallback() PacketCallback
}
type PacketCallback func([]byte, interface{})
// BaseInterface provides common implementation
// BaseInterface provides common implementation for network interfaces
type BaseInterface struct {
Name string
Mode InterfaceMode
Type InterfaceType
Online bool
Detached bool
IN bool
OUT bool
MTU int
Bitrate int64
TxBytes uint64
RxBytes uint64
mutex sync.RWMutex
owner interface{}
packetCallback PacketCallback
}
Name string
Mode InterfaceMode
Type InterfaceType
Online bool
Enabled bool
Detached bool
IN bool
OUT bool
MTU int
Bitrate int64
TxBytes uint64
RxBytes uint64
lastTx time.Time
Mutex sync.RWMutex
Owner interface{}
PacketCallback PacketCallback
}
// NewBaseInterface creates a new BaseInterface instance
func NewBaseInterface(name string, ifaceType InterfaceType, enabled bool) BaseInterface {
return BaseInterface{
Name: name,
Type: ifaceType,
Mode: IF_MODE_FULL,
Enabled: enabled,
MTU: DEFAULT_MTU,
Bitrate: BITRATE_MINIMUM,
lastTx: time.Now(),
}
}
// Default implementations for BaseInterface
func (i *BaseInterface) GetType() InterfaceType {
return i.Type
}
func (i *BaseInterface) GetMode() InterfaceMode {
return i.Mode
}
func (i *BaseInterface) GetMTU() int {
return i.MTU
}
func (i *BaseInterface) GetName() string {
return i.Name
}
func (i *BaseInterface) IsEnabled() bool {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.Enabled && i.Online && !i.Detached
}
func (i *BaseInterface) IsOnline() bool {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.Online
}
func (i *BaseInterface) IsDetached() bool {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.Detached
}
func (i *BaseInterface) SetPacketCallback(callback PacketCallback) {
i.Mutex.Lock()
defer i.Mutex.Unlock()
i.PacketCallback = callback
}
func (i *BaseInterface) GetPacketCallback() PacketCallback {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return i.PacketCallback
}
func (i *BaseInterface) Detach() {
i.Mutex.Lock()
defer i.Mutex.Unlock()
i.Detached = true
i.Online = false
}
func (i *BaseInterface) Enable() {
i.Mutex.Lock()
defer i.Mutex.Unlock()
i.Enabled = true
i.Online = true
}
func (i *BaseInterface) Disable() {
i.Mutex.Lock()
defer i.Mutex.Unlock()
i.Enabled = false
i.Online = false
}
// Default implementations that should be overridden by specific interfaces
func (i *BaseInterface) Start() error {
return nil
}
func (i *BaseInterface) Stop() error {
return nil
}
func (i *BaseInterface) GetConn() net.Conn {
return nil
}
func (i *BaseInterface) Send(data []byte, address string) error {
return i.ProcessOutgoing(data)
}
func (i *BaseInterface) ProcessIncoming(data []byte) {
if i.PacketCallback != nil {
i.PacketCallback(data, i)
}
}
func (i *BaseInterface) ProcessOutgoing(data []byte) error {
return nil
}
func (i *BaseInterface) SendPathRequest(data []byte) error {
return i.Send(data, "")
}
func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.Time) error {
// Create link packet
packet := make([]byte, 0, len(dest)+len(data)+9) // 1 byte type + dest + 8 byte timestamp
packet = append(packet, 0x02) // Link packet type
packet = append(packet, dest...)
// 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, "")
}
func (i *BaseInterface) GetBandwidthAvailable() bool {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
// If no transmission in last second, bandwidth is available
if time.Since(i.lastTx) > time.Second {
return true
}
// Calculate current bandwidth usage
bytesPerSec := float64(i.TxBytes) / time.Since(i.lastTx).Seconds()
currentUsage := bytesPerSec * 8 // Convert to bits/sec
// Check if usage is below threshold (2% of total bitrate)
maxUsage := float64(i.Bitrate) * 0.02 // 2% propagation rate
return currentUsage < maxUsage
}

View File

@@ -4,33 +4,79 @@ import (
"time"
)
// Interface related types
type InterfaceMode byte
type InterfaceType byte
// Destination type constants
const (
DESTINATION_SINGLE = 0x00
DESTINATION_GROUP = 0x01
DESTINATION_PLAIN = 0x02
)
// Transport related types
type TransportMode byte
type PathStatus byte
// Common structs
// Path represents routing information for a destination
type Path struct {
Interface NetworkInterface
Address string
Status PathStatus
LastSeen time.Time
NextHop []byte
Hops uint8
LastUpdated time.Time
Interface NetworkInterface
LastSeen time.Time
NextHop []byte
Hops uint8
LastUpdated time.Time
HopCount uint8
}
// Common callbacks
type ProofRequestedCallback func(interface{}) bool
type ProofRequestedCallback func([]byte, []byte)
type LinkEstablishedCallback func(interface{})
type PacketCallback func([]byte, NetworkInterface)
// Request handler
// RequestHandler manages path requests and responses
type RequestHandler struct {
Path string
ResponseGenerator func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity interface{}, requestedAt int64) []byte
AllowMode byte
AllowedList [][]byte
}
}
// Interface types
type InterfaceMode byte
type InterfaceType byte
// RatchetIDReceiver holds ratchet ID information
type RatchetIDReceiver struct {
LatestRatchetID []byte
}
// NetworkStats holds interface statistics
type NetworkStats struct {
BytesSent uint64
BytesReceived uint64
PacketsSent uint64
PacketsReceived uint64
LastUpdated time.Time
}
// LinkStatus represents the current state of a link
type LinkStatus struct {
Established bool
LastSeen time.Time
RTT time.Duration
Quality float64
Hops uint8
}
// PathRequest represents a path discovery request
type PathRequest struct {
DestinationHash []byte
Tag []byte
TTL int
Recursive bool
}
// PathResponse represents a path discovery response
type PathResponse struct {
DestinationHash []byte
NextHop []byte
Hops uint8
Tag []byte
}

View File

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

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

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

View File

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

View File

@@ -0,0 +1,22 @@
package cryptography
import (
"crypto/sha256"
"golang.org/x/crypto/curve25519"
)
const (
SHA256Size = 32
)
// GetBasepoint returns the standard Curve25519 basepoint
func GetBasepoint() []byte {
return curve25519.Basepoint
}
func Hash(data []byte) []byte {
h := sha256.New()
h.Write(data)
return h.Sum(nil)
}

View File

@@ -0,0 +1,25 @@
package cryptography
import (
"crypto/rand"
"golang.org/x/crypto/curve25519"
)
func GenerateKeyPair() (privateKey, publicKey []byte, err error) {
privateKey = make([]byte, curve25519.ScalarSize)
if _, err := rand.Read(privateKey); err != nil {
return nil, nil, err
}
publicKey, err = curve25519.X25519(privateKey, curve25519.Basepoint)
if err != nil {
return nil, nil, err
}
return privateKey, publicKey, nil
}
func DeriveSharedSecret(privateKey, peerPublicKey []byte) ([]byte, error) {
return curve25519.X25519(privateKey, peerPublicKey)
}

View File

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

View File

@@ -0,0 +1,18 @@
package cryptography
import (
"crypto/ed25519"
"crypto/rand"
)
func GenerateSigningKeyPair() (ed25519.PublicKey, ed25519.PrivateKey, error) {
return ed25519.GenerateKey(rand.Reader)
}
func Sign(privateKey ed25519.PrivateKey, message []byte) []byte {
return ed25519.Sign(privateKey, message)
}
func Verify(publicKey ed25519.PublicKey, message, signature []byte) bool {
return ed25519.Verify(publicKey, message, signature)
}

View File

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

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

@@ -0,0 +1,17 @@
package cryptography
import (
"crypto/sha256"
"io"
"golang.org/x/crypto/hkdf"
)
func DeriveKey(secret, salt, info []byte, length int) ([]byte, error) {
hkdfReader := hkdf.New(sha256.New, secret, salt, info)
key := make([]byte, length)
if _, err := io.ReadFull(hkdfReader, key); err != nil {
return nil, err
}
return key, nil
}

View File

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

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

@@ -0,0 +1,26 @@
package cryptography
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
)
func GenerateHMACKey(size int) ([]byte, error) {
key := make([]byte, size)
if _, err := rand.Read(key); err != nil {
return nil, err
}
return key, nil
}
func ComputeHMAC(key, message []byte) []byte {
h := hmac.New(sha256.New, key)
h.Write(message)
return h.Sum(nil)
}
func ValidateHMAC(key, message, messageHMAC []byte) bool {
expectedHMAC := ComputeHMAC(key, message)
return hmac.Equal(messageHMAC, expectedHMAC)
}

View File

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

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

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

View File

@@ -2,20 +2,26 @@ package destination
import (
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"sync"
"github.com/Sudo-Ivan/reticulum-go/pkg/announce"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
"github.com/Sudo-Ivan/reticulum-go/pkg/debug"
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
)
const (
// Destination direction types
// The IN bit specifies that the destination can receive traffic.
// The OUT bit specifies that the destination can send traffic.
// A destination can be both IN and OUT.
IN = 0x01
OUT = 0x02
// Destination types
SINGLE = 0x00
GROUP = 0x01
PLAIN = 0x02
@@ -44,39 +50,38 @@ type RequestHandler struct {
}
type Destination struct {
identity *identity.Identity
direction byte
destType byte
appName string
aspects []string
hash []byte
acceptsLinks bool
proofStrategy byte
packetCallback PacketCallback
proofCallback ProofRequestedCallback
linkCallback LinkEstablishedCallback
identity *identity.Identity
direction byte
destType byte
appName string
aspects []string
hashValue []byte
transport *transport.Transport
acceptsLinks bool
proofStrategy byte
packetCallback PacketCallback
proofCallback ProofRequestedCallback
linkCallback LinkEstablishedCallback
ratchetsEnabled bool
ratchetPath string
ratchetCount int
ratchetInterval int
enforceRatchets bool
defaultAppData []byte
defaultAppData []byte
mutex sync.RWMutex
requestHandlers map[string]*RequestHandler
callbacks struct {
packetReceived common.PacketCallback
proofRequested common.ProofRequestedCallback
linkEstablished common.LinkEstablishedCallback
}
}
func New(id *identity.Identity, direction byte, destType byte, appName string, aspects ...string) (*Destination, error) {
func New(id *identity.Identity, direction byte, destType byte, appName string, transport *transport.Transport, aspects ...string) (*Destination, error) {
debug.Log(debug.DEBUG_INFO, "Creating new destination", "app", appName, "type", destType, "direction", direction)
if id == nil {
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil")
return nil, errors.New("identity cannot be nil")
}
@@ -86,6 +91,7 @@ func New(id *identity.Identity, direction byte, destType byte, appName string, a
destType: destType,
appName: appName,
aspects: aspects,
transport: transport,
acceptsLinks: false,
proofStrategy: PROVE_NONE,
ratchetCount: RATCHET_COUNT,
@@ -94,19 +100,63 @@ func New(id *identity.Identity, direction byte, destType byte, appName string, a
}
// Generate destination hash
d.hash = d.Hash()
d.hashValue = d.calculateHash()
debug.Log(debug.DEBUG_VERBOSE, "Created destination with hash", "hash", fmt.Sprintf("%x", d.hashValue))
return d, nil
}
func (d *Destination) Hash() []byte {
nameHash := sha256.Sum256([]byte(d.ExpandName()))
identityHash := sha256.Sum256(d.identity.GetPublicKey())
// FromHash creates a destination from a known hash (e.g., from an announce).
// This is used by clients to create destination objects for servers they've discovered.
func FromHash(hash []byte, id *identity.Identity, destType byte, transport *transport.Transport) (*Destination, error) {
debug.Log(debug.DEBUG_INFO, "Creating destination from hash", "hash", fmt.Sprintf("%x", hash))
if id == nil {
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil")
return nil, errors.New("identity cannot be nil")
}
d := &Destination{
identity: id,
direction: OUT,
destType: destType,
hashValue: hash,
transport: transport,
acceptsLinks: false,
proofStrategy: PROVE_NONE,
ratchetCount: RATCHET_COUNT,
ratchetInterval: RATCHET_INTERVAL,
requestHandlers: make(map[string]*RequestHandler),
}
debug.Log(debug.DEBUG_VERBOSE, "Created destination from hash", "hash", fmt.Sprintf("%x", hash))
return d, nil
}
func (d *Destination) calculateHash() []byte {
debug.Log(debug.DEBUG_TRACE, "Calculating hash for destination", "name", d.ExpandName())
// destination_hash = SHA256(name_hash_10bytes + identity_hash_16bytes)[:16]
// Identity hash is the truncated hash of the public key (16 bytes)
identityHash := identity.TruncatedHash(d.identity.GetPublicKey())
combined := append(nameHash[:], identityHash[:]...)
finalHash := sha256.Sum256(combined)
// Name hash is the FULL 32-byte SHA256, then we take first 10 bytes for concatenation
nameHashFull := sha256.Sum256([]byte(d.ExpandName()))
nameHash10 := nameHashFull[:10] // Only use 10 bytes
debug.Log(debug.DEBUG_ALL, "Identity hash", "hash", fmt.Sprintf("%x", identityHash))
debug.Log(debug.DEBUG_ALL, "Name hash (10 bytes)", "hash", fmt.Sprintf("%x", nameHash10))
// Concatenate name_hash (10 bytes) + identity_hash (16 bytes) = 26 bytes
combined := append(nameHash10, identityHash...)
return finalHash[:16] // Truncated to 128 bits
// Then hash again and truncate to 16 bytes
finalHashFull := sha256.Sum256(combined)
finalHash := finalHashFull[:16]
debug.Log(debug.DEBUG_VERBOSE, "Calculated destination hash", "hash", fmt.Sprintf("%x", finalHash))
return finalHash
}
func (d *Destination) ExpandName() string {
@@ -121,76 +171,62 @@ func (d *Destination) Announce(appData []byte) error {
d.mutex.Lock()
defer d.mutex.Unlock()
// If no specific appData provided, use default
debug.Log(debug.DEBUG_VERBOSE, "Announcing destination", "name", d.ExpandName())
if appData == nil {
appData = d.defaultAppData
}
// Create announce packet
packet := make([]byte, 0)
// Add destination hash
packet = append(packet, d.hash...)
// Add identity public key
packet = append(packet, d.identity.GetPublicKey()...)
// Add flags byte
flags := byte(0)
if d.acceptsLinks {
flags |= 0x01
}
if d.ratchetsEnabled {
flags |= 0x02
}
packet = append(packet, flags)
// Add proof strategy
packet = append(packet, d.proofStrategy)
// Add app data length and data if present
if appData != nil {
appDataLen := uint16(len(appData))
lenBytes := make([]byte, 2)
binary.BigEndian.PutUint16(lenBytes, appDataLen)
packet = append(packet, lenBytes...)
packet = append(packet, appData...)
} else {
// No app data
packet = append(packet, 0x00, 0x00)
}
// Add ratchet data if enabled
if d.ratchetsEnabled {
// Add ratchet interval
intervalBytes := make([]byte, 4)
binary.BigEndian.PutUint32(intervalBytes, uint32(d.ratchetInterval))
packet = append(packet, intervalBytes...)
// Add current ratchet key
ratchetKey := d.identity.GetCurrentRatchetKey()
if ratchetKey == nil {
return errors.New("failed to get current ratchet key")
}
packet = append(packet, ratchetKey...)
}
// Sign the announce packet
signature, err := d.Sign(packet)
// Create announce packet using announce package
// Pass the destination hash, name, and app data
announce, err := announce.New(d.identity, d.hashValue, d.ExpandName(), appData, false, d.transport.GetConfig())
if err != nil {
return fmt.Errorf("failed to sign announce packet: %w", err)
return fmt.Errorf("failed to create announce: %w", err)
}
packet = append(packet, signature...)
// Send announce packet through transport layer
// This will need to be implemented in the transport package
return transport.SendAnnounce(packet)
packet := announce.GetPacket()
if packet == nil {
return errors.New("failed to create announce packet")
}
// Send announce packet to all interfaces
debug.Log(debug.DEBUG_VERBOSE, "Sending announce packet to all interfaces")
if d.transport == nil {
return errors.New("transport not initialized")
}
interfaces := d.transport.GetInterfaces()
debug.Log(debug.DEBUG_ALL, "Got interfaces from transport", "count", len(interfaces))
var lastErr error
for name, iface := range interfaces {
debug.Log(debug.DEBUG_ALL, "Checking interface", "name", name, "enabled", iface.IsEnabled(), "online", iface.IsOnline())
if iface.IsEnabled() && iface.IsOnline() {
debug.Log(debug.DEBUG_ALL, "Sending announce to interface", "name", name, "bytes", len(packet))
if err := iface.Send(packet, ""); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to send announce on interface", "name", name, "error", err)
lastErr = err
} else {
debug.Log(debug.DEBUG_ALL, "Successfully sent announce to interface", "name", name)
}
} else {
debug.Log(debug.DEBUG_ALL, "Skipping interface", "name", name, "reason", "not enabled or not online")
}
}
return lastErr
}
func (d *Destination) AcceptsLinks(accepts bool) {
d.mutex.Lock()
defer d.mutex.Unlock()
d.acceptsLinks = accepts
// Register with transport if accepting links
if accepts && d.transport != nil {
d.transport.RegisterDestination(d.hashValue, d)
debug.Log(debug.DEBUG_VERBOSE, "Destination registered with transport for link requests", "hash", fmt.Sprintf("%x", d.hashValue))
}
}
func (d *Destination) SetLinkEstablishedCallback(callback common.LinkEstablishedCallback) {
@@ -199,6 +235,31 @@ func (d *Destination) SetLinkEstablishedCallback(callback common.LinkEstablished
d.linkCallback = callback
}
func (d *Destination) GetLinkCallback() common.LinkEstablishedCallback {
d.mutex.RLock()
defer d.mutex.RUnlock()
return d.linkCallback
}
func (d *Destination) HandleIncomingLinkRequest(linkID []byte, transport interface{}, networkIface common.NetworkInterface) error {
debug.Log(debug.DEBUG_INFO, "Handling incoming link request for destination", "hash", fmt.Sprintf("%x", d.GetHash()))
// Import link package here to avoid circular dependency at package level
// We'll use dynamic import by having the caller create the link
// For now, just call the callback with a placeholder
if d.linkCallback != nil {
debug.Log(debug.DEBUG_INFO, "Calling link established callback")
// Pass linkID as the link object for now
// The callback will need to handle creating the actual link
d.linkCallback(linkID)
} else {
debug.Log(debug.DEBUG_VERBOSE, "No link callback set")
}
return nil
}
func (d *Destination) SetPacketCallback(callback common.PacketCallback) {
d.mutex.Lock()
defer d.mutex.Unlock()
@@ -220,7 +281,7 @@ func (d *Destination) SetProofStrategy(strategy byte) {
func (d *Destination) EnableRatchets(path string) bool {
d.mutex.Lock()
defer d.mutex.Unlock()
d.ratchetsEnabled = true
d.ratchetPath = path
return true
@@ -236,7 +297,7 @@ func (d *Destination) SetRetainedRatchets(count int) bool {
if count < 1 {
return false
}
d.mutex.Lock()
defer d.mutex.Unlock()
d.ratchetCount = count
@@ -247,7 +308,7 @@ func (d *Destination) SetRatchetInterval(interval int) bool {
if interval < 1 {
return false
}
d.mutex.Lock()
defer d.mutex.Unlock()
d.ratchetInterval = interval
@@ -275,7 +336,7 @@ func (d *Destination) RegisterRequestHandler(path string, responseGen func(strin
return errors.New("invalid allow mode")
}
if allow == ALLOW_LIST && (allowedList == nil || len(allowedList) == 0) {
if allow == ALLOW_LIST && len(allowedList) == 0 {
return errors.New("allowed list required for ALLOW_LIST mode")
}
@@ -305,19 +366,32 @@ func (d *Destination) DeregisterRequestHandler(path string) bool {
func (d *Destination) Encrypt(plaintext []byte) ([]byte, error) {
if d.destType == PLAIN {
debug.Log(debug.DEBUG_VERBOSE, "Using plaintext transmission for PLAIN destination")
return plaintext, nil
}
if d.identity == nil {
debug.Log(debug.DEBUG_INFO, "Cannot encrypt: no identity available")
return nil, errors.New("no identity available for encryption")
}
debug.Log(debug.DEBUG_VERBOSE, "Encrypting bytes for destination", "bytes", len(plaintext), "destType", d.destType)
switch d.destType {
case SINGLE:
return d.identity.Encrypt(plaintext, nil)
recipientKey := d.identity.GetPublicKey()
debug.Log(debug.DEBUG_VERBOSE, "Encrypting for single recipient", "key", fmt.Sprintf("%x", recipientKey[:8]))
return d.identity.Encrypt(plaintext, recipientKey)
case GROUP:
return d.identity.EncryptSymmetric(plaintext)
key := d.identity.GetCurrentRatchetKey()
if key == nil {
debug.Log(debug.DEBUG_INFO, "Cannot encrypt: no ratchet key available")
return nil, errors.New("no ratchet key available")
}
debug.Log(debug.DEBUG_VERBOSE, "Encrypting for group with ratchet key", "key", fmt.Sprintf("%x", key[:8]))
return d.identity.EncryptWithHMAC(plaintext, key)
default:
debug.Log(debug.DEBUG_INFO, "Unsupported destination type for encryption", "destType", d.destType)
return nil, errors.New("unsupported destination type for encryption")
}
}
@@ -331,14 +405,15 @@ func (d *Destination) Decrypt(ciphertext []byte) ([]byte, error) {
return nil, errors.New("no identity available for decryption")
}
switch d.destType {
case SINGLE:
return d.identity.Decrypt(ciphertext, nil)
case GROUP:
return d.identity.DecryptSymmetric(ciphertext)
default:
return nil, errors.New("unsupported destination type for decryption")
}
// Create empty ratchet receiver to get latest ratchet ID if available
ratchetReceiver := &common.RatchetIDReceiver{}
// Call Decrypt with full parameter list:
// - ciphertext: the encrypted data
// - ratchets: nil since we're not providing specific ratchets
// - enforceRatchets: false to allow fallback to normal decryption
// - ratchetIDReceiver: to receive the latest ratchet ID used
return d.identity.Decrypt(ciphertext, nil, false, ratchetReceiver)
}
func (d *Destination) Sign(data []byte) ([]byte, error) {
@@ -347,4 +422,33 @@ func (d *Destination) Sign(data []byte) ([]byte, error) {
}
signature := d.identity.Sign(data)
return signature, nil
}
}
func (d *Destination) GetPublicKey() []byte {
if d.identity == nil {
return nil
}
return d.identity.GetPublicKey()
}
func (d *Destination) GetIdentity() *identity.Identity {
return d.identity
}
func (d *Destination) GetType() byte {
return d.destType
}
func (d *Destination) GetHash() []byte {
d.mutex.RLock()
defer d.mutex.RUnlock()
if d.hashValue == nil {
d.mutex.RUnlock()
d.mutex.Lock()
defer d.mutex.Unlock()
if d.hashValue == nil {
d.hashValue = d.calculateHash()
}
}
return d.hashValue
}

View File

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,279 @@
package interfaces
import (
"fmt"
"log"
"net"
"sync"
"time"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
)
const (
DEFAULT_DISCOVERY_PORT = 29716
DEFAULT_DATA_PORT = 42671
BITRATE_GUESS = 10 * 1000 * 1000
PEERING_TIMEOUT = 7500 * time.Millisecond
SCOPE_LINK = "2"
SCOPE_ADMIN = "4"
SCOPE_SITE = "5"
SCOPE_ORGANISATION = "8"
SCOPE_GLOBAL = "e"
)
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.UDPConn
multicastEchoes map[string]time.Time
mutex sync.RWMutex
outboundConn *net.UDPConn
}
type Peer struct {
ifaceName string
lastHeard time.Time
conn *net.UDPConn
}
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: false,
OUT: false,
MTU: common.DEFAULT_MTU,
Bitrate: BITRATE_MINIMUM,
},
discoveryPort: DEFAULT_DISCOVERY_PORT,
dataPort: DEFAULT_DATA_PORT,
discoveryScope: SCOPE_LINK,
peers: make(map[string]*Peer),
linkLocalAddrs: make([]string, 0),
adoptedInterfaces: make(map[string]string),
interfaceServers: make(map[string]*net.UDPConn),
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 {
interfaces, err := net.Interfaces()
if err != nil {
return fmt.Errorf("failed to list interfaces: %v", err)
}
for _, iface := range interfaces {
if err := ai.configureInterface(&iface); err != nil {
log.Printf("Failed to configure interface %s: %v", iface.Name, err)
continue
}
}
if len(ai.adoptedInterfaces) == 0 {
return fmt.Errorf("no suitable interfaces found")
}
// Mark interface as online
ai.Online = true
ai.Enabled = true
go ai.peerJobs()
return nil
}
func (ai *AutoInterface) configureInterface(iface *net.Interface) error {
addrs, err := iface.Addrs()
if err != nil {
return err
}
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.IsLinkLocalUnicast() {
ai.adoptedInterfaces[iface.Name] = ipnet.IP.String()
ai.multicastEchoes[iface.Name] = time.Now()
if err := ai.startDiscoveryListener(iface); err != nil {
return err
}
if err := ai.startDataListener(iface); err != nil {
return err
}
break
}
}
return nil
}
func (ai *AutoInterface) startDiscoveryListener(iface *net.Interface) error {
addr := &net.UDPAddr{
IP: net.ParseIP(fmt.Sprintf("ff%s%s::1", ai.discoveryScope, SCOPE_LINK)),
Port: ai.discoveryPort,
Zone: iface.Name,
}
conn, err := net.ListenMulticastUDP("udp6", iface, addr)
if err != nil {
return err
}
go ai.handleDiscovery(conn, iface.Name)
return nil
}
func (ai *AutoInterface) startDataListener(iface *net.Interface) error {
addr := &net.UDPAddr{
IP: net.IPv6zero,
Port: ai.dataPort,
Zone: iface.Name,
}
conn, err := net.ListenUDP("udp6", addr)
if err != nil {
return err
}
ai.interfaceServers[iface.Name] = conn
go ai.handleData(conn)
return nil
}
func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) {
buf := make([]byte, 1024)
for {
_, remoteAddr, err := conn.ReadFromUDP(buf)
if err != nil {
log.Printf("Discovery read error: %v", err)
continue
}
ai.handlePeerAnnounce(remoteAddr, ifaceName)
}
}
func (ai *AutoInterface) handleData(conn *net.UDPConn) {
buf := make([]byte, ai.GetMTU())
for {
n, _, err := conn.ReadFromUDP(buf)
if err != nil {
if !ai.IsDetached() {
log.Printf("Data read error: %v", err)
}
return
}
if callback := ai.GetPacketCallback(); callback != nil {
callback(buf[:n], ai)
}
}
}
func (ai *AutoInterface) handlePeerAnnounce(addr *net.UDPAddr, ifaceName string) {
ai.mutex.Lock()
defer ai.mutex.Unlock()
peerAddr := addr.IP.String()
for _, localAddr := range ai.linkLocalAddrs {
if peerAddr == localAddr {
ai.multicastEchoes[ifaceName] = time.Now()
return
}
}
if _, exists := ai.peers[peerAddr]; !exists {
ai.peers[peerAddr] = &Peer{
ifaceName: ifaceName,
lastHeard: time.Now(),
}
log.Printf("Added peer %s on %s", peerAddr, ifaceName)
} else {
ai.peers[peerAddr].lastHeard = time.Now()
}
}
func (ai *AutoInterface) peerJobs() {
ticker := time.NewTicker(PEERING_TIMEOUT)
for range ticker.C {
ai.mutex.Lock()
now := time.Now()
for addr, peer := range ai.peers {
if now.Sub(peer.lastHeard) > PEERING_TIMEOUT {
delete(ai.peers, addr)
log.Printf("Removed timed out peer %s", addr)
}
}
ai.mutex.Unlock()
}
}
func (ai *AutoInterface) Send(data []byte, address string) error {
ai.mutex.RLock()
defer ai.mutex.RUnlock()
for _, peer := range ai.peers {
addr := &net.UDPAddr{
IP: net.ParseIP(address),
Port: ai.dataPort,
Zone: peer.ifaceName,
}
if ai.outboundConn == nil {
var err error
ai.outboundConn, err = net.ListenUDP("udp6", &net.UDPAddr{Port: 0})
if err != nil {
return err
}
}
if _, err := ai.outboundConn.WriteToUDP(data, addr); err != nil {
log.Printf("Failed to send to peer %s: %v", address, err)
continue
}
}
return nil
}
func (ai *AutoInterface) Stop() error {
ai.mutex.Lock()
defer ai.mutex.Unlock()
for _, server := range ai.interfaceServers {
server.Close() // #nosec G104
}
if ai.outboundConn != nil {
ai.outboundConn.Close() // #nosec G104
}
return nil
}

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

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

View File

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

View File

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

View File

@@ -3,8 +3,12 @@ package interfaces
import (
"fmt"
"net"
"runtime"
"sync"
"time"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
"github.com/Sudo-Ivan/reticulum-go/pkg/debug"
)
const (
@@ -17,103 +21,102 @@ const (
KISS_TFEND = 0xDC
KISS_TFESC = 0xDD
TCP_USER_TIMEOUT = 24
TCP_PROBE_AFTER = 5
TCP_PROBE_INTERVAL = 2
TCP_USER_TIMEOUT = 24
TCP_PROBE_AFTER = 5
TCP_PROBE_INTERVAL = 2
TCP_PROBES = 12
RECONNECT_WAIT = 5
INITIAL_TIMEOUT = 5
INITIAL_BACKOFF = time.Second
MAX_BACKOFF = time.Minute * 5
)
type TCPClientInterface struct {
Interface
conn net.Conn
targetAddr string
targetPort int
kissFraming bool
i2pTunneled bool
initiator bool
reconnecting bool
neverConnected bool
writing bool
BaseInterface
conn net.Conn
targetAddr string
targetPort int
kissFraming bool
i2pTunneled bool
initiator bool
reconnecting bool
neverConnected bool
writing bool
maxReconnectTries int
packetBuffer []byte
packetType byte
packetBuffer []byte
packetType byte
mutex sync.RWMutex
enabled bool
TxBytes uint64
RxBytes uint64
lastTx time.Time
lastRx time.Time
}
func NewTCPClient(name string, targetAddr string, targetPort int, kissFraming bool, i2pTunneled bool) (*TCPClientInterface, error) {
func NewTCPClientInterface(name string, targetHost string, targetPort int, kissFraming bool, i2pTunneled bool, enabled bool) (*TCPClientInterface, error) {
tc := &TCPClientInterface{
Interface: Interface{
Name: name,
Mode: MODE_FULL,
MTU: 1064,
Bitrate: 10000000, // 10Mbps estimate
},
targetAddr: targetAddr,
targetPort: targetPort,
kissFraming: kissFraming,
i2pTunneled: i2pTunneled,
initiator: true,
BaseInterface: NewBaseInterface(name, common.IF_TYPE_TCP, enabled),
targetAddr: targetHost,
targetPort: targetPort,
kissFraming: kissFraming,
i2pTunneled: i2pTunneled,
initiator: true,
enabled: enabled,
maxReconnectTries: TCP_PROBES,
packetBuffer: make([]byte, 0),
neverConnected: true,
}
if err := tc.connect(true); err != nil {
go tc.reconnect()
} else {
if enabled {
addr := net.JoinHostPort(targetHost, fmt.Sprintf("%d", targetPort))
conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
tc.conn = conn
tc.Online = true
go tc.readLoop()
}
return tc, nil
}
func (tc *TCPClientInterface) connect(initial bool) error {
addr := fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort)
conn, err := net.DialTimeout("tcp", addr, time.Second*INITIAL_TIMEOUT)
func (tc *TCPClientInterface) Start() error {
tc.mutex.Lock()
defer tc.mutex.Unlock()
if !tc.Enabled {
return fmt.Errorf("interface not enabled")
}
if tc.conn != nil {
tc.Online = true
go tc.readLoop()
return nil
}
addr := net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort))
conn, err := net.Dial("tcp", addr)
if err != nil {
if initial {
return fmt.Errorf("initial connection failed: %v", err)
}
return err
}
tc.conn = conn
tc.Online = true
tc.writing = false
tc.neverConnected = false
// Set TCP options
if tcpConn, ok := conn.(*net.TCPConn); ok {
tcpConn.SetNoDelay(true)
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(time.Second * TCP_PROBE_INTERVAL)
}
return nil
}
func (tc *TCPClientInterface) reconnect() {
if tc.initiator && !tc.reconnecting {
tc.reconnecting = true
attempts := 0
for !tc.Online {
time.Sleep(time.Second * RECONNECT_WAIT)
attempts++
if tc.maxReconnectTries > 0 && attempts > tc.maxReconnectTries {
tc.teardown()
break
}
if err := tc.connect(false); err != nil {
continue
}
go tc.readLoop()
break
// Set platform-specific timeouts
switch runtime.GOOS {
case "linux":
if err := tc.setTimeoutsLinux(); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to set Linux TCP timeouts", "error", err)
}
case "darwin":
if err := tc.setTimeoutsOSX(); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to set OSX TCP timeouts", "error", err)
}
tc.reconnecting = false
}
tc.Online = true
go tc.readLoop()
return nil
}
func (tc *TCPClientInterface) readLoop() {
@@ -134,51 +137,30 @@ 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++ {
b := buffer[i]
if tc.kissFraming {
// KISS framing logic
if inFrame && b == KISS_FEND {
inFrame = false
if b == HDLC_FLAG {
if inFrame && len(dataBuffer) > 0 {
tc.handlePacket(dataBuffer)
dataBuffer = dataBuffer[:0]
} else if b == KISS_FEND {
inFrame = true
} else if inFrame {
if b == KISS_FESC {
escape = true
} else {
if escape {
if b == KISS_TFEND {
b = KISS_FEND
}
if b == KISS_TFESC {
b = KISS_FESC
}
escape = false
}
dataBuffer = append(dataBuffer, b)
}
}
} else {
// HDLC framing logic
if inFrame && b == HDLC_FLAG {
inFrame = false
tc.handlePacket(dataBuffer)
dataBuffer = dataBuffer[:0]
} else if b == HDLC_FLAG {
inFrame = true
} else if inFrame {
if b == HDLC_ESC {
escape = true
} else {
if escape {
b ^= HDLC_ESC_MASK
escape = false
}
dataBuffer = append(dataBuffer, b)
inFrame = !inFrame
continue
}
if inFrame {
if b == HDLC_ESC {
escape = true
} else {
if escape {
b ^= HDLC_ESC_MASK
escape = false
}
dataBuffer = append(dataBuffer, b)
}
}
}
@@ -187,26 +169,44 @@ func (tc *TCPClientInterface) readLoop() {
func (tc *TCPClientInterface) handlePacket(data []byte) {
if len(data) < 1 {
debug.Log(debug.DEBUG_ALL, "Received invalid packet: empty")
return
}
packetType := data[0]
payload := data[1:]
tc.mutex.Lock()
tc.RxBytes += uint64(len(data))
lastRx := time.Now()
tc.lastRx = lastRx
tc.mutex.Unlock()
switch packetType {
case 0x01: // Path request
tc.Interface.ProcessIncoming(payload)
case 0x02: // Link packet
if len(payload) < 40 { // minimum size for link packet
return
}
tc.Interface.ProcessIncoming(payload)
default:
// Unknown packet type
return
debug.Log(debug.DEBUG_ALL, "Received packet", "type", fmt.Sprintf("0x%02x", data[0]), "size", len(data))
// For RNS packets, call the packet callback directly
if callback := tc.GetPacketCallback(); callback != nil {
debug.Log(debug.DEBUG_ALL, "Calling packet callback for RNS packet")
callback(data, tc)
} else {
debug.Log(debug.DEBUG_ALL, "No packet callback set for TCP interface")
}
}
// Send implements the interface Send method for TCP interface
func (tc *TCPClientInterface) Send(data []byte, address string) error {
debug.Log(debug.DEBUG_ALL, "TCP interface sending bytes", "name", tc.Name, "bytes", len(data))
if !tc.IsEnabled() || !tc.IsOnline() {
return fmt.Errorf("TCP interface %s is not online", tc.Name)
}
// For TCP interface, we need to prepend a packet type byte for announce packets
// RNS TCP protocol expects: [packet_type][data]
frame := make([]byte, 0, len(data)+1)
frame = append(frame, 0x01) // Announce packet type
frame = append(frame, data...)
return tc.ProcessOutgoing(frame)
}
func (tc *TCPClientInterface) ProcessOutgoing(data []byte) error {
if !tc.Online {
return fmt.Errorf("interface offline")
@@ -215,22 +215,20 @@ func (tc *TCPClientInterface) ProcessOutgoing(data []byte) error {
tc.writing = true
defer func() { tc.writing = false }()
// For TCP connections, use HDLC framing
var frame []byte
if tc.kissFraming {
frame = append([]byte{KISS_FEND}, escapeKISS(data)...)
frame = append(frame, KISS_FEND)
} else {
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
frame = append(frame, HDLC_FLAG)
}
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
frame = append(frame, HDLC_FLAG)
if _, err := tc.conn.Write(frame); err != nil {
tc.teardown()
return fmt.Errorf("write failed: %v", err)
}
// Update TX stats before sending
tc.UpdateStats(uint64(len(frame)), false)
tc.Interface.ProcessOutgoing(data)
return nil
debug.Log(debug.DEBUG_ALL, "TCP interface writing to network", "name", tc.Name, "bytes", len(frame))
_, err := tc.conn.Write(frame)
if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "TCP interface write failed", "name", tc.Name, "error", err)
}
return err
}
func (tc *TCPClientInterface) teardown() {
@@ -238,7 +236,7 @@ func (tc *TCPClientInterface) teardown() {
tc.IN = false
tc.OUT = false
if tc.conn != nil {
tc.conn.Close()
tc.conn.Close() // #nosec G104
}
}
@@ -269,130 +267,237 @@ func escapeKISS(data []byte) []byte {
return escaped
}
type TCPServerInterface struct {
Interface
server net.Listener
bindAddr string
bindPort int
i2pTunneled bool
preferIPv6 bool
spawned []*TCPClientInterface
spawnedMutex sync.RWMutex
func (tc *TCPClientInterface) SetPacketCallback(cb common.PacketCallback) {
tc.packetCallback = cb
}
func NewTCPServer(name string, bindAddr string, bindPort int, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) {
ts := &TCPServerInterface{
Interface: Interface{
Name: name,
Mode: MODE_FULL,
MTU: 1064,
Bitrate: 10000000, // 10Mbps estimate
},
bindAddr: bindAddr,
bindPort: bindPort,
i2pTunneled: i2pTunneled,
preferIPv6: preferIPv6,
spawned: make([]*TCPClientInterface, 0),
}
// Resolve bind address
var addr string
if ts.bindAddr == "" {
if ts.preferIPv6 {
addr = fmt.Sprintf("[::0]:%d", ts.bindPort)
} else {
addr = fmt.Sprintf("0.0.0.0:%d", ts.bindPort)
}
} else {
addr = fmt.Sprintf("%s:%d", ts.bindAddr, ts.bindPort)
}
// Create listener
var err error
ts.server, err = net.Listen("tcp", addr)
if err != nil {
return nil, fmt.Errorf("failed to create TCP listener: %v", err)
}
ts.Online = true
ts.IN = true
// Start accept loop
go ts.acceptLoop()
return ts, nil
func (tc *TCPClientInterface) IsEnabled() bool {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
return tc.enabled && tc.Online && !tc.Detached
}
func (ts *TCPServerInterface) acceptLoop() {
for {
conn, err := ts.server.Accept()
if err != nil {
if !ts.Detached {
// Log error and continue accepting
continue
}
func (tc *TCPClientInterface) GetName() string {
return tc.Name
}
func (tc *TCPClientInterface) GetPacketCallback() common.PacketCallback {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
return tc.packetCallback
}
func (tc *TCPClientInterface) IsDetached() bool {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
return tc.Detached
}
func (tc *TCPClientInterface) IsOnline() bool {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
return tc.Online
}
func (tc *TCPClientInterface) reconnect() {
tc.mutex.Lock()
if tc.reconnecting {
tc.mutex.Unlock()
return
}
tc.reconnecting = true
tc.mutex.Unlock()
backoff := time.Second
maxBackoff := time.Minute * 5
retries := 0
for retries < tc.maxReconnectTries {
tc.teardown()
addr := net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort))
conn, err := net.Dial("tcp", addr)
if err == nil {
tc.mutex.Lock()
tc.conn = conn
tc.Online = true
tc.neverConnected = false
tc.reconnecting = false
tc.mutex.Unlock()
go tc.readLoop()
return
}
// Create new client interface for this connection
client := &TCPClientInterface{
Interface: Interface{
Name: fmt.Sprintf("Client-%s-%s", ts.Name, conn.RemoteAddr()),
Mode: ts.Mode,
MTU: ts.MTU,
},
conn: conn,
i2pTunneled: ts.i2pTunneled,
// Log reconnection attempt
fmt.Printf("Failed to reconnect to %s (attempt %d/%d): %v\n",
net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort)), retries+1, tc.maxReconnectTries, err)
// Wait with exponential backoff
time.Sleep(backoff)
// Increase backoff time exponentially
backoff *= 2
if backoff > maxBackoff {
backoff = maxBackoff
}
// Configure TCP options
if tcpConn, ok := conn.(*net.TCPConn); ok {
tcpConn.SetNoDelay(true)
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(time.Duration(TCP_PROBE_INTERVAL) * time.Second)
retries++
}
tc.mutex.Lock()
tc.reconnecting = false
tc.mutex.Unlock()
// If we've exhausted all retries, perform final teardown
tc.teardown()
fmt.Printf("Failed to reconnect to %s after %d attempts\n",
net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort)), tc.maxReconnectTries)
}
func (tc *TCPClientInterface) Enable() {
tc.mutex.Lock()
defer tc.mutex.Unlock()
tc.Online = true
}
func (tc *TCPClientInterface) Disable() {
tc.mutex.Lock()
defer tc.mutex.Unlock()
tc.Online = false
}
func (tc *TCPClientInterface) IsConnected() bool {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
return tc.conn != nil && tc.Online && !tc.reconnecting
}
func (tc *TCPClientInterface) GetRTT() time.Duration {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
if !tc.IsConnected() {
return 0
}
if tcpConn, ok := tc.conn.(*net.TCPConn); ok {
var rtt time.Duration = 0
if runtime.GOOS == "linux" {
if info, err := tcpConn.SyscallConn(); err == nil {
if err := info.Control(func(fd uintptr) { // #nosec G104
rtt = platformGetRTT(fd)
}); err != nil {
debug.Log(debug.DEBUG_ERROR, "Error in SyscallConn Control", "error", err)
}
}
}
return rtt
}
client.Online = true
client.IN = ts.IN
client.OUT = ts.OUT
return 0
}
// Add to spawned interfaces
ts.spawnedMutex.Lock()
ts.spawned = append(ts.spawned, client)
ts.spawnedMutex.Unlock()
func (tc *TCPClientInterface) GetTxBytes() uint64 {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
return tc.TxBytes
}
// Start client read loop
go client.readLoop()
func (tc *TCPClientInterface) GetRxBytes() uint64 {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
return tc.RxBytes
}
func (tc *TCPClientInterface) UpdateStats(bytes uint64, isRx bool) {
tc.mutex.Lock()
defer tc.mutex.Unlock()
now := time.Now()
if isRx {
tc.RxBytes += bytes
tc.lastRx = now
debug.Log(debug.DEBUG_TRACE, "Interface RX stats", "name", tc.Name, "bytes", bytes, "total", tc.RxBytes, "last", tc.lastRx)
} else {
tc.TxBytes += bytes
tc.lastTx = now
debug.Log(debug.DEBUG_TRACE, "Interface TX stats", "name", tc.Name, "bytes", bytes, "total", tc.TxBytes, "last", tc.lastTx)
}
}
func (ts *TCPServerInterface) Detach() {
ts.Interface.Detach()
if ts.server != nil {
ts.server.Close()
}
ts.spawnedMutex.Lock()
for _, client := range ts.spawned {
client.Detach()
}
ts.spawned = nil
ts.spawnedMutex.Unlock()
func (tc *TCPClientInterface) GetStats() (tx uint64, rx uint64, lastTx time.Time, lastRx time.Time) {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
return tc.TxBytes, tc.RxBytes, tc.lastTx, tc.lastRx
}
func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
ts.spawnedMutex.RLock()
defer ts.spawnedMutex.RUnlock()
func (tc *TCPClientInterface) setTimeoutsLinux() error {
tcpConn, ok := tc.conn.(*net.TCPConn)
if !ok {
return fmt.Errorf("not a TCP connection")
}
var lastErr error
for _, client := range ts.spawned {
if err := client.ProcessOutgoing(data); err != nil {
lastErr = err
if !tc.i2pTunneled {
if err := tcpConn.SetKeepAlive(true); err != nil {
return err
}
if err := tcpConn.SetKeepAlivePeriod(time.Duration(TCP_PROBE_INTERVAL) * time.Second); err != nil {
return err
}
}
return lastErr
return nil
}
func (tc *TCPClientInterface) setTimeoutsOSX() error {
tcpConn, ok := tc.conn.(*net.TCPConn)
if !ok {
return fmt.Errorf("not a TCP connection")
}
if err := tcpConn.SetKeepAlive(true); err != nil {
return err
}
return nil
}
type TCPServerInterface struct {
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
}
func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFraming bool, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) {
ts := &TCPServerInterface{
BaseInterface: BaseInterface{
Name: name,
Mode: common.IF_MODE_FULL,
Type: common.IF_TYPE_TCP,
Online: false,
MTU: common.DEFAULT_MTU,
Detached: false,
},
connections: make(map[string]net.Conn),
bindAddr: bindAddr,
bindPort: bindPort,
preferIPv6: preferIPv6,
kissFraming: kissFraming,
i2pTunneled: i2pTunneled,
}
return ts, nil
}
func (ts *TCPServerInterface) String() string {
@@ -401,8 +506,164 @@ func (ts *TCPServerInterface) String() string {
if ts.preferIPv6 {
addr = "[::0]"
} else {
addr = "0.0.0.0"
addr = "0.0.0.0"
}
}
return fmt.Sprintf("TCPServerInterface[%s/%s:%d]", ts.Name, addr, ts.bindPort)
}
}
func (ts *TCPServerInterface) SetPacketCallback(callback common.PacketCallback) {
ts.mutex.Lock()
defer ts.mutex.Unlock()
ts.packetCallback = callback
}
func (ts *TCPServerInterface) GetPacketCallback() common.PacketCallback {
ts.mutex.RLock()
defer ts.mutex.RUnlock()
return ts.packetCallback
}
func (ts *TCPServerInterface) IsEnabled() bool {
ts.mutex.RLock()
defer ts.mutex.RUnlock()
return ts.BaseInterface.Enabled && ts.BaseInterface.Online && !ts.BaseInterface.Detached
}
func (ts *TCPServerInterface) GetName() string {
return ts.Name
}
func (ts *TCPServerInterface) IsDetached() bool {
ts.mutex.RLock()
defer ts.mutex.RUnlock()
return ts.BaseInterface.Detached
}
func (ts *TCPServerInterface) IsOnline() bool {
ts.mutex.RLock()
defer ts.mutex.RUnlock()
return ts.Online
}
func (ts *TCPServerInterface) Enable() {
ts.mutex.Lock()
defer ts.mutex.Unlock()
ts.Online = true
}
func (ts *TCPServerInterface) Disable() {
ts.mutex.Lock()
defer ts.mutex.Unlock()
ts.Online = false
}
func (ts *TCPServerInterface) Start() error {
ts.mutex.Lock()
defer ts.mutex.Unlock()
addr := net.JoinHostPort(ts.bindAddr, fmt.Sprintf("%d", ts.bindPort))
listener, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("failed to start TCP server: %w", err)
}
ts.Online = true
// Accept connections in a goroutine
go func() {
for {
conn, err := listener.Accept()
if err != nil {
if !ts.Online {
return // Normal shutdown
}
debug.Log(debug.DEBUG_ERROR, "Error accepting connection", "error", err)
continue
}
// Handle each connection in a separate goroutine
go ts.handleConnection(conn)
}
}()
return nil
}
func (ts *TCPServerInterface) Stop() error {
ts.mutex.Lock()
defer ts.mutex.Unlock()
ts.Online = false
return nil
}
func (ts *TCPServerInterface) GetTxBytes() uint64 {
ts.mutex.RLock()
defer ts.mutex.RUnlock()
return ts.TxBytes
}
func (ts *TCPServerInterface) GetRxBytes() uint64 {
ts.mutex.RLock()
defer ts.mutex.RUnlock()
return ts.RxBytes
}
func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
addr := conn.RemoteAddr().String()
ts.mutex.Lock()
ts.connections[addr] = conn
ts.mutex.Unlock()
defer func() {
ts.mutex.Lock()
delete(ts.connections, addr)
ts.mutex.Unlock()
conn.Close() // #nosec G104
}()
buffer := make([]byte, ts.MTU)
for {
n, err := conn.Read(buffer)
if err != nil {
return
}
ts.mutex.Lock()
ts.RxBytes += uint64(n) // #nosec G115
ts.mutex.Unlock()
if ts.packetCallback != nil {
ts.packetCallback(buffer[:n], ts)
}
}
}
func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
ts.mutex.RLock()
defer ts.mutex.RUnlock()
if !ts.Online {
return fmt.Errorf("interface offline")
}
var frame []byte
if ts.kissFraming {
frame = append([]byte{KISS_FEND}, escapeKISS(data)...)
frame = append(frame, KISS_FEND)
} else {
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
frame = append(frame, HDLC_FLAG)
}
ts.TxBytes += uint64(len(frame))
for _, conn := range ts.connections {
if _, err := conn.Write(frame); err != nil {
debug.Log(debug.DEBUG_VERBOSE, "Error writing to connection", "address", conn.RemoteAddr(), "error", err)
}
}
return nil
}

View File

@@ -0,0 +1,14 @@
//go:build !linux
// +build !linux
package interfaces
import (
"time"
)
// platformGetRTT is defined in OS-specific files
// Default implementation for non-Linux platforms
func platformGetRTT(fd uintptr) time.Duration {
return 0
}

View File

@@ -0,0 +1,32 @@
//go:build linux
// +build linux
package interfaces
import (
"syscall"
"time"
"unsafe"
)
func platformGetRTT(fd uintptr) time.Duration {
var info syscall.TCPInfo
size := uint32(syscall.SizeofTCPInfo)
_, _, err := syscall.Syscall6(
syscall.SYS_GETSOCKOPT,
fd,
syscall.SOL_TCP,
syscall.TCP_INFO,
uintptr(unsafe.Pointer(&info)), // #nosec G103
uintptr(unsafe.Pointer(&size)), // #nosec G103
0,
)
if err != 0 {
return 0
}
// RTT is in microseconds, convert to Duration
return time.Duration(info.Rtt) * time.Microsecond
}

View File

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

View File

@@ -4,85 +4,127 @@ import (
"fmt"
"net"
"sync"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
"github.com/Sudo-Ivan/reticulum-go/pkg/debug"
)
type UDPInterface struct {
Interface
conn *net.UDPConn
listenAddr *net.UDPAddr
BaseInterface
conn *net.UDPConn
addr *net.UDPAddr
targetAddr *net.UDPAddr
mutex sync.RWMutex
readBuffer []byte
}
func NewUDPInterface(name string, listenAddr string, targetAddr string) (*UDPInterface, error) {
ui := &UDPInterface{
Interface: Interface{
Name: name,
Mode: MODE_FULL,
MTU: 1500,
Bitrate: 100000000, // 100Mbps estimate for UDP
},
readBuffer: make([]byte, 65535),
}
// Parse listen address
laddr, err := net.ResolveUDPAddr("udp", listenAddr)
func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) {
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, fmt.Errorf("invalid listen address: %v", err)
return nil, err
}
ui.listenAddr = laddr
// Parse target address if provided
if targetAddr != "" {
taddr, err := net.ResolveUDPAddr("udp", targetAddr)
var targetAddr *net.UDPAddr
if target != "" {
targetAddr, err = net.ResolveUDPAddr("udp", target)
if err != nil {
return nil, fmt.Errorf("invalid target address: %v", err)
return nil, err
}
ui.targetAddr = taddr
ui.OUT = true
}
// Create UDP connection
conn, err := net.ListenUDP("udp", ui.listenAddr)
if err != nil {
return nil, fmt.Errorf("failed to listen on UDP: %v", err)
ui := &UDPInterface{
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
addr: udpAddr,
targetAddr: targetAddr,
readBuffer: make([]byte, common.DEFAULT_MTU),
}
ui.conn = conn
ui.IN = true
ui.Online = true
// Start read loop
go ui.readLoop()
return ui, nil
}
func (ui *UDPInterface) readLoop() {
for {
if !ui.Online {
return
}
func (ui *UDPInterface) GetName() string {
return ui.Name
}
n, addr, err := ui.conn.ReadFromUDP(ui.readBuffer)
if err != nil {
if !ui.Detached {
// Log error
}
continue
}
func (ui *UDPInterface) GetType() common.InterfaceType {
return ui.Type
}
// Copy received data
data := make([]byte, n)
copy(data, ui.readBuffer[:n])
func (ui *UDPInterface) GetMode() common.InterfaceMode {
return ui.Mode
}
// Process packet
ui.ProcessIncoming(data)
func (ui *UDPInterface) IsOnline() bool {
ui.mutex.RLock()
defer ui.mutex.RUnlock()
return ui.Online
}
func (ui *UDPInterface) IsDetached() bool {
ui.mutex.RLock()
defer ui.mutex.RUnlock()
return ui.Detached
}
func (ui *UDPInterface) Detach() {
ui.mutex.Lock()
defer ui.mutex.Unlock()
ui.Detached = true
if ui.conn != nil {
ui.conn.Close() // #nosec G104
}
}
func (ui *UDPInterface) Send(data []byte, addr string) error {
debug.Log(debug.DEBUG_ALL, "UDP interface sending bytes", "name", ui.Name, "bytes", len(data))
if !ui.IsEnabled() {
return fmt.Errorf("interface not enabled")
}
if ui.targetAddr == nil {
return fmt.Errorf("no target address configured")
}
// Update TX stats before sending
ui.mutex.Lock()
ui.TxBytes += uint64(len(data))
ui.mutex.Unlock()
_, err := ui.conn.WriteTo(data, ui.targetAddr)
if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "UDP interface write failed", "name", ui.Name, "error", err)
} else {
debug.Log(debug.DEBUG_ALL, "UDP interface sent bytes successfully", "name", ui.Name, "bytes", len(data))
}
return err
}
func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) {
ui.mutex.Lock()
defer ui.mutex.Unlock()
ui.packetCallback = callback
}
func (ui *UDPInterface) GetPacketCallback() common.PacketCallback {
ui.mutex.RLock()
defer ui.mutex.RUnlock()
return ui.packetCallback
}
func (ui *UDPInterface) ProcessIncoming(data []byte) {
if callback := ui.GetPacketCallback(); callback != nil {
callback(data, ui)
}
}
func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
if !ui.Online || ui.targetAddr == nil {
return fmt.Errorf("interface offline or no target address configured")
if !ui.IsOnline() {
return fmt.Errorf("interface offline")
}
if ui.targetAddr == nil {
return fmt.Errorf("no target address configured")
}
_, err := ui.conn.WriteToUDP(data, ui.targetAddr)
@@ -90,13 +132,89 @@ func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
return fmt.Errorf("UDP write failed: %v", err)
}
ui.Interface.ProcessOutgoing(data)
ui.mutex.Lock()
ui.TxBytes += uint64(len(data))
ui.mutex.Unlock()
return nil
}
func (ui *UDPInterface) Detach() {
ui.Interface.Detach()
if ui.conn != nil {
ui.conn.Close()
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 {
conn, err := net.ListenUDP("udp", ui.addr)
if err != nil {
return err
}
}
ui.conn = conn
ui.Online = true
// Start the read loop in a goroutine
go ui.readLoop()
return nil
}
func (ui *UDPInterface) readLoop() {
buffer := make([]byte, common.DEFAULT_MTU)
for ui.IsOnline() && !ui.IsDetached() {
n, remoteAddr, err := ui.conn.ReadFromUDP(buffer)
if err != nil {
if ui.IsOnline() {
debug.Log(debug.DEBUG_ERROR, "Error reading from UDP interface", "name", ui.Name, "error", err)
}
return
}
ui.mutex.Lock()
if ui.targetAddr == nil {
debug.Log(debug.DEBUG_ALL, "UDP interface discovered peer", "name", ui.Name, "peer", remoteAddr.String())
ui.targetAddr = remoteAddr
}
ui.mutex.Unlock()
if ui.packetCallback != nil {
ui.packetCallback(buffer[:n], ui)
}
}
}
func (ui *UDPInterface) IsEnabled() bool {
ui.mutex.RLock()
defer ui.mutex.RUnlock()
return ui.Enabled && ui.Online && !ui.Detached
}

View File

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

View File

@@ -5,104 +5,191 @@ import (
"crypto/cipher"
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io"
"log"
"sync"
"time"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
"github.com/Sudo-Ivan/reticulum-go/pkg/destination"
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
"github.com/Sudo-Ivan/reticulum-go/pkg/packet"
"github.com/Sudo-Ivan/reticulum-go/pkg/pathfinder"
"github.com/Sudo-Ivan/reticulum-go/pkg/resolver"
"github.com/Sudo-Ivan/reticulum-go/pkg/resource"
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
)
const (
CURVE = "Curve25519"
ESTABLISHMENT_TIMEOUT_PER_HOP = 6
KEEPALIVE_TIMEOUT_FACTOR = 4
STALE_GRACE = 2
KEEPALIVE = 360
STALE_TIME = 720
KEEPALIVE_TIMEOUT_FACTOR = 4
STALE_GRACE = 2
KEEPALIVE = 360
STALE_TIME = 720
ACCEPT_NONE = 0x00
ACCEPT_ALL = 0x01
ACCEPT_APP = 0x02
STATUS_PENDING = 0x00
STATUS_ACTIVE = 0x01
STATUS_CLOSED = 0x02
STATUS_FAILED = 0x03
STATUS_PENDING = 0x00
STATUS_ACTIVE = 0x01
STATUS_CLOSED = 0x02
STATUS_FAILED = 0x03
PROVE_NONE = 0x00
PROVE_ALL = 0x01
PROVE_APP = 0x02
WATCHDOG_MIN_SLEEP = 0.025
WATCHDOG_INTERVAL = 0.1
)
type Link struct {
mutex sync.RWMutex
destination interface{}
status byte
establishedAt time.Time
lastInbound time.Time
lastOutbound time.Time
lastDataReceived time.Time
lastDataSent time.Time
remoteIdentity *identity.Identity
sessionKey []byte
linkID []byte
mutex sync.RWMutex
destination *destination.Destination
status byte
networkInterface common.NetworkInterface
establishedAt time.Time
lastInbound time.Time
lastOutbound time.Time
lastDataReceived time.Time
lastDataSent time.Time
pathFinder *pathfinder.PathFinder
remoteIdentity *identity.Identity
sessionKey []byte
linkID []byte
rtt float64
establishmentRate float64
trackPhyStats bool
rssi float64
snr float64
q float64
resourceStrategy byte
establishedCallback func(*Link)
closedCallback func(*Link)
packetCallback func([]byte, *packet.Packet)
resourceCallback func(interface{}) bool
resourceStartedCallback func(interface{})
closedCallback func(*Link)
packetCallback func([]byte, *packet.Packet)
identifiedCallback func(*Link, *identity.Identity)
teardownReason byte
hmacKey []byte
transport *transport.Transport
rssi float64
snr float64
q float64
resourceCallback func(interface{}) bool
resourceStartedCallback func(interface{})
resourceConcludedCallback func(interface{})
remoteIdentifiedCallback func(*Link, *identity.Identity)
resourceStrategy byte
proofStrategy byte
proofCallback func(*packet.Packet) bool
trackPhyStats bool
watchdogLock bool
watchdogActive bool
establishmentTimeout time.Duration
keepalive time.Duration
staleTime time.Duration
initiator bool
}
func New(dest interface{}, establishedCb func(*Link), closedCb func(*Link)) *Link {
l := &Link{
destination: dest,
status: STATUS_PENDING,
establishedAt: time.Time{},
lastInbound: time.Time{},
lastOutbound: time.Time{},
lastDataReceived: time.Time{},
lastDataSent: time.Time{},
resourceStrategy: ACCEPT_NONE,
establishedCallback: establishedCb,
closedCallback: closedCb,
func NewLink(dest *destination.Destination, transport *transport.Transport, networkIface common.NetworkInterface, establishedCallback func(*Link), closedCallback func(*Link)) *Link {
return &Link{
destination: dest,
status: STATUS_PENDING,
transport: transport,
networkInterface: networkIface,
establishedCallback: establishedCallback,
closedCallback: closedCallback,
establishedAt: time.Time{}, // Zero time until established
lastInbound: time.Time{},
lastOutbound: time.Time{},
lastDataReceived: time.Time{},
lastDataSent: time.Time{},
pathFinder: pathfinder.NewPathFinder(),
watchdogLock: false,
watchdogActive: false,
establishmentTimeout: time.Duration(ESTABLISHMENT_TIMEOUT_PER_HOP * float64(time.Second)),
keepalive: time.Duration(KEEPALIVE * float64(time.Second)),
staleTime: time.Duration(STALE_TIME * float64(time.Second)),
initiator: false,
}
return l
}
func (l *Link) Identify(id *identity.Identity) error {
func (l *Link) Establish() error {
l.mutex.Lock()
defer l.mutex.Unlock()
if l.status != STATUS_ACTIVE {
return errors.New("link not active")
if l.status != STATUS_PENDING {
log.Printf("[DEBUG-3] Cannot establish link: invalid status %d", l.status)
return errors.New("link already established or failed")
}
// Create identification message
idMsg := append(id.GetPublicKey(), id.Sign(l.linkID)...)
// Encrypt and send identification
err := l.SendPacket(idMsg)
if err != nil {
destPublicKey := l.destination.GetPublicKey()
if destPublicKey == nil {
log.Printf("[DEBUG-3] Cannot establish link: destination has no public key")
return errors.New("destination has no public key")
}
// Generate link ID for this connection
l.linkID = make([]byte, 16)
if _, err := rand.Read(l.linkID); err != nil {
log.Printf("[DEBUG-3] Failed to generate link ID: %v", err)
return fmt.Errorf("failed to generate link ID: %w", err)
}
l.initiator = true
log.Printf("[DEBUG-4] Creating link request packet for destination %x with link ID %x", destPublicKey[:8], l.linkID[:8])
p := &packet.Packet{
HeaderType: packet.HeaderType1,
PacketType: packet.PacketTypeLinkReq,
TransportType: 0,
Context: packet.ContextLinkIdentify,
ContextFlag: packet.FlagUnset,
Hops: 0,
DestinationType: l.destination.GetType(),
DestinationHash: l.destination.GetHash(),
Data: l.linkID,
CreateReceipt: true,
}
if err := p.Pack(); err != nil {
log.Printf("[DEBUG-3] Failed to pack link request packet: %v", err)
return err
}
return nil
log.Printf("[DEBUG-4] Sending link request packet with ID %x", l.linkID[:8])
return l.transport.SendPacket(p)
}
func (l *Link) Identify(id *identity.Identity) error {
if !l.IsActive() {
return errors.New("link not active")
}
p := &packet.Packet{
HeaderType: packet.HeaderType1,
PacketType: packet.PacketTypeData,
TransportType: 0,
Context: packet.ContextLinkIdentify,
ContextFlag: packet.FlagUnset,
Hops: 0,
DestinationType: l.destination.GetType(),
DestinationHash: l.destination.GetHash(),
Data: id.GetPublicKey(),
CreateReceipt: true,
}
if err := p.Pack(); err != nil {
return err
}
return l.transport.SendPacket(p)
}
func (l *Link) HandleIdentification(data []byte) error {
@@ -110,26 +197,33 @@ func (l *Link) HandleIdentification(data []byte) error {
defer l.mutex.Unlock()
if len(data) < ed25519.PublicKeySize+ed25519.SignatureSize {
return errors.New("invalid identification data")
log.Printf("[DEBUG-3] Invalid identification data length: %d bytes", len(data))
return errors.New("invalid identification data length")
}
pubKey := data[:ed25519.PublicKeySize]
signature := data[ed25519.PublicKeySize:]
remoteIdentity := &identity.Identity{}
if !remoteIdentity.LoadPublicKey(pubKey) {
return errors.New("invalid remote public key")
log.Printf("[DEBUG-4] Processing identification from public key %x", pubKey[:8])
remoteIdentity := identity.FromPublicKey(pubKey)
if remoteIdentity == nil {
log.Printf("[DEBUG-3] Invalid remote identity from public key %x", pubKey[:8])
return errors.New("invalid remote identity")
}
// Verify signature of link ID
if !remoteIdentity.Verify(l.linkID, signature) {
return errors.New("invalid identification signature")
signData := append(l.linkID, pubKey...)
if !remoteIdentity.Verify(signData, signature) {
log.Printf("[DEBUG-3] Invalid signature from remote identity %x", pubKey[:8])
return errors.New("invalid signature")
}
log.Printf("[DEBUG-4] Remote identity verified successfully: %x", pubKey[:8])
l.remoteIdentity = remoteIdentity
if l.remoteIdentifiedCallback != nil {
l.remoteIdentifiedCallback(l, remoteIdentity)
if l.identifiedCallback != nil {
log.Printf("[DEBUG-4] Executing identified callback for remote identity %x", pubKey[:8])
l.identifiedCallback(l, remoteIdentity)
}
return nil
@@ -158,8 +252,8 @@ func (l *Link) Request(path string, data []byte, timeout time.Duration) (*Reques
receipt := &RequestReceipt{
requestID: requestID,
status: STATUS_PENDING,
sentAt: time.Now(),
status: STATUS_PENDING,
sentAt: time.Now(),
}
// Send request
@@ -184,12 +278,12 @@ func (l *Link) Request(path string, data []byte, timeout time.Duration) (*Reques
}
type RequestReceipt struct {
mutex sync.RWMutex
requestID []byte
status byte
sentAt time.Time
receivedAt time.Time
response []byte
mutex sync.RWMutex
requestID []byte
status byte
sentAt time.Time
receivedAt time.Time
response []byte
}
func (r *RequestReceipt) GetRequestID() []byte {
@@ -233,21 +327,40 @@ func (l *Link) TrackPhyStats(track bool) {
l.trackPhyStats = track
}
func (l *Link) UpdatePhyStats(rssi, snr, q float64) {
l.mutex.Lock()
defer l.mutex.Unlock()
if l.trackPhyStats {
l.rssi = rssi
l.snr = snr
l.q = q
}
}
func (l *Link) GetRSSI() float64 {
l.mutex.RLock()
defer l.mutex.RUnlock()
if !l.trackPhyStats {
return 0
}
return l.rssi
}
func (l *Link) GetSNR() float64 {
l.mutex.RLock()
defer l.mutex.RUnlock()
if !l.trackPhyStats {
return 0
}
return l.snr
}
func (l *Link) GetQ() float64 {
l.mutex.RLock()
defer l.mutex.RUnlock()
if !l.trackPhyStats {
return 0
}
return l.q
}
@@ -319,7 +432,7 @@ func (l *Link) GetRemoteIdentity() *identity.Identity {
func (l *Link) Teardown() {
l.mutex.Lock()
defer l.mutex.Unlock()
if l.status == STATUS_ACTIVE {
l.status = STATUS_CLOSED
if l.closedCallback != nil {
@@ -361,85 +474,58 @@ func (l *Link) SetResourceConcludedCallback(callback func(interface{})) {
func (l *Link) SetRemoteIdentifiedCallback(callback func(*Link, *identity.Identity)) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.remoteIdentifiedCallback = callback
l.identifiedCallback = callback
}
func (l *Link) SetResourceStrategy(strategy byte) error {
if strategy != ACCEPT_NONE && strategy != ACCEPT_ALL && strategy != ACCEPT_APP {
return errors.New("unsupported resource strategy")
}
l.mutex.Lock()
defer l.mutex.Unlock()
l.resourceStrategy = strategy
return nil
}
func NewLink(destination interface{}, establishedCallback func(*Link), closedCallback func(*Link)) *Link {
l := &Link{
destination: destination,
status: STATUS_PENDING,
establishedAt: time.Time{},
lastInbound: time.Time{},
lastOutbound: time.Time{},
lastDataReceived: time.Time{},
lastDataSent: time.Time{},
establishedCallback: establishedCallback,
closedCallback: closedCallback,
resourceStrategy: ACCEPT_NONE,
trackPhyStats: false,
}
return l
}
func (l *Link) Establish() error {
l.mutex.Lock()
defer l.mutex.Unlock()
if l.status != STATUS_PENDING {
return errors.New("link already established or failed")
}
// Generate session key using ECDH
ephemeralKey := make([]byte, 32)
if _, err := rand.Read(ephemeralKey); err != nil {
return err
}
l.sessionKey = ephemeralKey
l.establishedAt = time.Now()
l.status = STATUS_ACTIVE
if l.establishedCallback != nil {
l.establishedCallback(l)
}
return nil
}
func (l *Link) SendPacket(data []byte) error {
l.mutex.Lock()
defer l.mutex.Unlock()
if l.status != STATUS_ACTIVE {
log.Printf("[DEBUG-3] Cannot send packet: link not active (status: %d)", l.status)
return errors.New("link not active")
}
// Encrypt data using session key
encryptedData, err := l.encrypt(data)
log.Printf("[DEBUG-4] Encrypting packet of %d bytes", len(data))
encrypted, err := l.encrypt(data)
if err != nil {
log.Printf("[DEBUG-3] Failed to encrypt packet: %v", err)
return err
}
p := &packet.Packet{
HeaderType: packet.HeaderType1,
PacketType: packet.PacketTypeData,
TransportType: 0,
Context: packet.ContextNone,
ContextFlag: packet.FlagUnset,
Hops: 0,
DestinationType: l.destination.GetType(),
DestinationHash: l.destination.GetHash(),
Data: encrypted,
CreateReceipt: false,
}
if err := p.Pack(); err != nil {
return err
}
log.Printf("[DEBUG-4] Sending encrypted packet of %d bytes", len(encrypted))
l.lastOutbound = time.Now()
l.lastDataSent = time.Now()
if l.packetCallback != nil {
l.packetCallback(encryptedData, nil)
}
return nil
return l.transport.SendPacket(p)
}
func (l *Link) HandleInbound(data []byte) error {
@@ -447,20 +533,28 @@ func (l *Link) HandleInbound(data []byte) error {
defer l.mutex.Unlock()
if l.status != STATUS_ACTIVE {
log.Printf("[DEBUG-3] Dropping inbound packet: link not active (status: %d)", l.status)
return errors.New("link not active")
}
// Decrypt data using session key
decryptedData, err := l.decrypt(data)
if err != nil {
return err
// Decode and log packet details
l.decodePacket(data)
// Decrypt if we have a session key
if l.sessionKey != nil {
decrypted, err := l.decrypt(data)
if err != nil {
log.Printf("[DEBUG-3] Failed to decrypt packet: %v", err)
return err
}
data = decrypted
}
l.lastInbound = time.Now()
l.lastDataReceived = time.Now()
if l.packetCallback != nil {
l.packetCallback(decryptedData, nil)
l.packetCallback(data, nil)
}
return nil
@@ -476,17 +570,27 @@ func (l *Link) encrypt(data []byte) ([]byte, error) {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
// Generate IV
iv := make([]byte, aes.BlockSize)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
// Add PKCS7 padding
padding := aes.BlockSize - len(data)%aes.BlockSize
padtext := make([]byte, len(data)+padding)
copy(padtext, data)
for i := len(data); i < len(padtext); i++ {
padtext[i] = byte(padding)
}
return gcm.Seal(nonce, nonce, data, nil), nil
// Encrypt
mode := cipher.NewCBCEncrypter(block, iv) // #nosec G407
ciphertext := make([]byte, len(padtext))
mode.CryptBlocks(ciphertext, padtext)
// Prepend IV to ciphertext
return append(iv, ciphertext...), nil
}
func (l *Link) decrypt(data []byte) ([]byte, error) {
@@ -499,31 +603,34 @@ func (l *Link) decrypt(data []byte) ([]byte, error) {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
if len(data) < aes.BlockSize {
return nil, errors.New("ciphertext too short")
}
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
return gcm.Open(nil, nonce, ciphertext, nil)
}
iv := data[:aes.BlockSize]
ciphertext := data[aes.BlockSize:]
func (l *Link) UpdatePhyStats(rssi float64, snr float64, q float64) {
if !l.trackPhyStats {
return
if len(ciphertext)%aes.BlockSize != 0 {
return nil, errors.New("ciphertext is not a multiple of block size")
}
l.mutex.Lock()
defer l.mutex.Unlock()
l.rssi = rssi
l.snr = snr
l.q = q
mode := cipher.NewCBCDecrypter(block, iv)
plaintext := make([]byte, len(ciphertext))
mode.CryptBlocks(plaintext, ciphertext)
// Remove PKCS7 padding
padding := int(plaintext[len(plaintext)-1])
if padding > aes.BlockSize || padding == 0 {
return nil, errors.New("invalid padding")
}
for i := len(plaintext) - padding; i < len(plaintext); i++ {
if plaintext[i] != byte(padding) {
return nil, errors.New("invalid padding")
}
}
return plaintext[:len(plaintext)-padding], nil
}
func (l *Link) GetRTT() float64 {
@@ -546,4 +653,242 @@ func (l *Link) GetStatus() byte {
func (l *Link) IsActive() bool {
return l.GetStatus() == STATUS_ACTIVE
}
}
func (l *Link) SendResource(res *resource.Resource) error {
l.mutex.Lock()
defer l.mutex.Unlock()
if l.status != STATUS_ACTIVE {
l.teardownReason = STATUS_FAILED
return errors.New("link not active")
}
// Activate the resource
res.Activate()
// Send the resource data as packets
buffer := make([]byte, resource.DEFAULT_SEGMENT_SIZE)
for {
n, err := res.Read(buffer)
if err == io.EOF {
break
}
if err != nil {
l.teardownReason = STATUS_FAILED
return fmt.Errorf("error reading resource: %v", err)
}
if err := l.SendPacket(buffer[:n]); err != nil {
l.teardownReason = STATUS_FAILED
return fmt.Errorf("error sending resource packet: %v", err)
}
}
return nil
}
func (l *Link) maintainLink() {
ticker := time.NewTicker(time.Second * KEEPALIVE)
defer ticker.Stop()
for range ticker.C {
if l.status != STATUS_ACTIVE {
return
}
inactiveTime := l.InactiveFor()
if inactiveTime > float64(STALE_TIME) {
l.mutex.Lock()
l.teardownReason = STATUS_FAILED
l.mutex.Unlock()
l.Teardown()
return
}
noDataTime := l.NoDataFor()
if noDataTime > float64(KEEPALIVE) {
l.mutex.Lock()
err := l.SendPacket([]byte{})
if err != nil {
l.teardownReason = STATUS_FAILED
l.mutex.Unlock()
l.Teardown()
return
}
l.mutex.Unlock()
}
}
}
func (l *Link) Start() {
go l.maintainLink()
}
func (l *Link) SetProofStrategy(strategy byte) error {
if strategy != PROVE_NONE && strategy != PROVE_ALL && strategy != PROVE_APP {
return errors.New("invalid proof strategy")
}
l.mutex.Lock()
defer l.mutex.Unlock()
l.proofStrategy = strategy
return nil
}
func (l *Link) SetProofCallback(callback func(*packet.Packet) bool) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.proofCallback = callback
}
func (l *Link) HandleProofRequest(packet *packet.Packet) bool {
l.mutex.RLock()
defer l.mutex.RUnlock()
switch l.proofStrategy {
case PROVE_NONE:
return false
case PROVE_ALL:
return true
case PROVE_APP:
if l.proofCallback != nil {
return l.proofCallback(packet)
}
return false
default:
return false
}
}
func (l *Link) decodePacket(data []byte) {
if len(data) < 1 {
log.Printf("[DEBUG-7] Invalid packet: zero length")
return
}
packetType := data[0]
log.Printf("[DEBUG-7] Packet Analysis:")
log.Printf("[DEBUG-7] - Size: %d bytes", len(data))
log.Printf("[DEBUG-7] - Type: 0x%02x", packetType)
switch packetType {
case packet.PacketTypeData:
log.Printf("[DEBUG-7] - Type Description: Data Packet")
if len(data) > 1 {
log.Printf("[DEBUG-7] - Payload Size: %d bytes", len(data)-1)
}
case packet.PacketTypeLinkReq:
log.Printf("[DEBUG-7] - Type Description: Link Management")
if len(data) > 32 {
log.Printf("[DEBUG-7] - Link ID: %x", data[1:33])
}
case packet.PacketTypeAnnounce:
log.Printf("[DEBUG-7] Received announce packet (%d bytes)", len(data))
if len(data) < packet.MinAnnounceSize {
log.Printf("[DEBUG-3] Announce packet too short: %d bytes", len(data))
return
}
destHash := data[2:18]
encKey := data[18:50]
signKey := data[50:82]
nameHash := data[82:92]
randomHash := data[92:102]
signature := data[102:166]
appData := data[166:]
pubKey := append(encKey, signKey...)
validationData := make([]byte, 0, 164)
validationData = append(validationData, destHash...)
validationData = append(validationData, encKey...)
validationData = append(validationData, signKey...)
validationData = append(validationData, nameHash...)
validationData = append(validationData, randomHash...)
if identity.ValidateAnnounce(validationData, destHash, pubKey, signature, appData) {
log.Printf("[DEBUG-4] Valid announce from %x", pubKey[:8])
if err := l.transport.HandleAnnounce(destHash, l.networkInterface); err != nil {
log.Printf("[DEBUG-3] Failed to handle announce: %v", err)
}
} else {
log.Printf("[DEBUG-3] Invalid announce signature from %x", pubKey[:8])
}
case packet.PacketTypeProof:
log.Printf("[DEBUG-7] - Type Description: RNS Discovery")
if len(data) > 17 {
searchHash := data[1:17]
log.Printf("[DEBUG-7] - Searching for Hash: %x", searchHash)
if id, err := resolver.ResolveIdentity(hex.EncodeToString(searchHash)); err == nil {
log.Printf("[DEBUG-7] - Found matching identity: %s", id.GetHexHash())
}
}
default:
log.Printf("[DEBUG-7] - Type Description: Unknown (0x%02x)", packetType)
log.Printf("[DEBUG-7] - Raw Hex: %x", data)
}
}
// Helper function for min of two ints
func min(a, b int) int {
if a < b {
return a
}
return b
}
func (l *Link) startWatchdog() {
if l.watchdogActive {
return
}
l.watchdogActive = true
go l.watchdog()
}
func (l *Link) watchdog() {
for l.status != STATUS_CLOSED {
l.mutex.Lock()
if l.watchdogLock {
l.mutex.Unlock()
time.Sleep(time.Duration(WATCHDOG_MIN_SLEEP * float64(time.Second)))
continue
}
var sleepTime float64 = WATCHDOG_INTERVAL
switch l.status {
case STATUS_ACTIVE:
lastActivity := l.lastInbound
if l.lastOutbound.After(lastActivity) {
lastActivity = l.lastOutbound
}
if time.Since(lastActivity) > l.keepalive {
if l.initiator {
if err := l.SendPacket([]byte{}); err != nil { // #nosec G104
log.Printf("[DEBUG-3] Failed to send keepalive packet: %v", err)
}
}
if time.Since(lastActivity) > l.staleTime {
l.status = STATUS_CLOSED
l.teardownReason = STATUS_FAILED
if l.closedCallback != nil {
l.closedCallback(l)
}
}
}
}
l.mutex.Unlock()
time.Sleep(time.Duration(sleepTime * float64(time.Second)))
}
l.watchdogActive = false
}

View File

@@ -3,11 +3,7 @@ package packet
const (
// MTU constants
EncryptedMDU = 383 // Maximum size of payload data in encrypted packet
PlainMDU = 464 // Maximum size of payload data in unencrypted packet
// Header Types
HeaderType1 = 0 // Two byte header, one 16 byte address field
HeaderType2 = 1 // Two byte header, two 16 byte address fields
PlainMDU = 464 // Maximum size of payload data in unencrypted packet
// Propagation Types
PropagationBroadcast = 0
@@ -19,9 +15,7 @@ const (
DestinationPlain = 2
DestinationLink = 3
// Packet Types
PacketData = 0
PacketAnnounce = 1
PacketLinkRequest = 2
PacketProof = 3
)
// Minimum packet sizes
MinAnnounceSize = 170 // header(2) + desthash(16) + context(1) + enckey(32) + signkey(32) +
// namehash(10) + randomhash(10) + signature(64) + min appdata(3)
)

View File

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

331
pkg/packet/packet_test.go Normal file
View File

@@ -0,0 +1,331 @@
package packet
import (
"bytes"
"crypto/rand"
"testing"
)
func randomBytes(n int) []byte {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
panic("Failed to generate random bytes: " + err.Error())
}
return b
}
func TestPacketPackUnpack(t *testing.T) {
testCases := []struct {
name string
headerType byte
packetType byte
transportType byte
destType byte
context byte
contextFlag byte
dataSize int
needsTransportID bool
}{
{
name: "HeaderType1_Data_NoContextFlag",
headerType: HeaderType1,
packetType: PacketTypeData,
transportType: 0x01, // Example
destType: 0x02, // Example
context: ContextNone,
contextFlag: FlagUnset,
dataSize: 100,
needsTransportID: false,
},
{
name: "HeaderType2_Announce_ContextFlagSet",
headerType: HeaderType2,
packetType: PacketTypeAnnounce,
transportType: 0x01, // Changed from 0x0F (15) to 1 (valid 1-bit value)
destType: 0x01, // Example
context: ContextResourceAdv,
contextFlag: FlagSet,
dataSize: 50,
needsTransportID: true,
},
{
name: "HeaderType1_EmptyData",
headerType: HeaderType1,
packetType: PacketTypeProof,
transportType: 0x00,
destType: 0x00,
context: ContextLRProof,
contextFlag: FlagSet,
dataSize: 0,
needsTransportID: false,
},
{
name: "HeaderType2_MaxHops", // Hops are set manually before pack
headerType: HeaderType2,
packetType: PacketTypeLinkReq,
transportType: 0x01, // Changed from 0x05 (5) to 1 (valid 1-bit value)
destType: 0x03,
context: ContextLinkIdentify,
contextFlag: FlagUnset,
dataSize: 200,
needsTransportID: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
originalData := randomBytes(tc.dataSize)
originalDestHash := randomBytes(16) // Truncated dest hash
var originalTransportID []byte
if tc.needsTransportID {
originalTransportID = randomBytes(16)
}
p := &Packet{
HeaderType: tc.headerType,
PacketType: tc.packetType,
TransportType: tc.transportType,
Context: tc.context,
ContextFlag: tc.contextFlag,
Hops: 5, // Example hops
DestinationType: tc.destType,
DestinationHash: originalDestHash,
TransportID: originalTransportID,
Data: originalData,
Packed: false,
}
// Test Pack
err := p.Pack()
if err != nil {
t.Fatalf("Pack() failed: %v", err)
}
if !p.Packed {
t.Error("Pack() did not set Packed flag to true")
}
if len(p.Raw) == 0 {
t.Error("Pack() resulted in empty Raw data")
}
// Create a new packet from the raw data for unpacking
unpackTarget := &Packet{Raw: p.Raw}
// Test Unpack
err = unpackTarget.Unpack()
if err != nil {
t.Fatalf("Unpack() failed: %v", err)
}
// Verify unpacked fields match original
if unpackTarget.HeaderType != tc.headerType {
t.Errorf("Unpacked HeaderType = %d; want %d", unpackTarget.HeaderType, tc.headerType)
}
if unpackTarget.PacketType != tc.packetType {
t.Errorf("Unpacked PacketType = %d; want %d", unpackTarget.PacketType, tc.packetType)
}
if unpackTarget.TransportType != tc.transportType {
t.Errorf("Unpacked TransportType = %d; want %d", unpackTarget.TransportType, tc.transportType)
}
if unpackTarget.Context != tc.context {
t.Errorf("Unpacked Context = %d; want %d", unpackTarget.Context, tc.context)
}
if unpackTarget.ContextFlag != tc.contextFlag {
t.Errorf("Unpacked ContextFlag = %d; want %d", unpackTarget.ContextFlag, tc.contextFlag)
}
if unpackTarget.Hops != 5 { // Should match the Hops set before packing
t.Errorf("Unpacked Hops = %d; want %d", unpackTarget.Hops, 5)
}
if unpackTarget.DestinationType != tc.destType {
t.Errorf("Unpacked DestinationType = %d; want %d", unpackTarget.DestinationType, tc.destType)
}
if !bytes.Equal(unpackTarget.DestinationHash, originalDestHash) {
t.Errorf("Unpacked DestinationHash = %x; want %x", unpackTarget.DestinationHash, originalDestHash)
}
if !bytes.Equal(unpackTarget.Data, originalData) {
t.Errorf("Unpacked Data = %x; want %x", unpackTarget.Data, originalData)
}
if tc.needsTransportID {
if !bytes.Equal(unpackTarget.TransportID, originalTransportID) {
t.Errorf("Unpacked TransportID = %x; want %x", unpackTarget.TransportID, originalTransportID)
}
} else {
if unpackTarget.TransportID != nil {
t.Errorf("Unpacked TransportID = %x; want nil", unpackTarget.TransportID)
}
}
})
}
}
func TestPackMTUExceeded(t *testing.T) {
p := &Packet{
HeaderType: HeaderType1,
PacketType: PacketTypeData,
DestinationHash: randomBytes(16),
Context: ContextNone,
Data: randomBytes(MTU + 10), // Exceed MTU
}
err := p.Pack()
if err == nil {
t.Errorf("Pack() should have failed due to exceeding MTU, but it didn't")
}
}
func TestUnpackTooShort(t *testing.T) {
testCases := []struct {
name string
raw []byte
}{
{"VeryShort", []byte{0x01}},
{"HeaderType1MinShort", []byte{0x00, 0x05, 0x01, 0x02}}, // Missing parts of dest hash
{"HeaderType2MinShort", []byte{0x40, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}}, // Missing dest hash
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
p := &Packet{Raw: tc.raw}
err := p.Unpack()
if err == nil {
t.Errorf("Unpack() should have failed for short packet, but it didn't")
}
})
}
}
func TestPacketHashing(t *testing.T) {
// Create two identical packets
data := randomBytes(50)
destHash := randomBytes(16)
p1 := &Packet{
HeaderType: HeaderType1,
PacketType: PacketTypeData,
TransportType: 0x01,
Context: ContextNone,
ContextFlag: FlagUnset,
Hops: 2,
DestinationType: 0x02,
DestinationHash: destHash,
Data: data,
}
p2 := &Packet{
HeaderType: HeaderType1,
PacketType: PacketTypeData,
TransportType: 0x01,
Context: ContextNone,
ContextFlag: FlagUnset,
Hops: 2,
DestinationType: 0x02,
DestinationHash: destHash,
Data: data,
}
// Pack both
if err := p1.Pack(); err != nil {
t.Fatalf("p1.Pack() failed: %v", err)
}
if err := p2.Pack(); err != nil {
t.Fatalf("p2.Pack() failed: %v", err)
}
// Hashes should be identical
hash1 := p1.GetHash()
hash2 := p2.GetHash()
if !bytes.Equal(hash1, hash2) {
t.Errorf("Hashes of identical packets differ:\nHash1: %x\nHash2: %x", hash1, hash2)
}
if !bytes.Equal(p1.PacketHash, hash1) {
t.Errorf("p1.PacketHash (%x) does not match GetHash() (%x)", p1.PacketHash, hash1)
}
// Change a non-hashable field (hops) in p2
p2.Hops = 3
p2.Raw[1] = 3 // Need to modify Raw as Pack isn't called again
hash3 := p2.GetHash()
if !bytes.Equal(hash1, hash3) {
t.Errorf("Hash changed after modifying non-hashable Hops field:\nHash1: %x\nHash3: %x", hash1, hash3)
}
// Change a hashable field (data) in p2
p2.Data = append(p2.Data, 0x99)
p2.Raw = append(p2.Raw, 0x99) // Modify Raw to reflect data change
hash4 := p2.GetHash()
if bytes.Equal(hash1, hash4) {
t.Errorf("Hash did not change after modifying hashable Data field")
}
// Test HeaderType2 hashing difference
p3 := &Packet{
HeaderType: HeaderType2,
PacketType: PacketTypeData,
TransportType: 0x01,
Context: ContextNone,
ContextFlag: FlagUnset,
Hops: 2,
DestinationType: 0x02,
DestinationHash: destHash,
TransportID: randomBytes(16),
Data: data,
}
if err := p3.Pack(); err != nil {
t.Fatalf("p3.Pack() failed: %v", err)
}
hash5 := p3.GetHash()
_ = hash5 // Use hash5 to avoid unused variable error
}
// BenchmarkPacketOperations benchmarks packet creation, packing, and hashing
func BenchmarkPacketOperations(b *testing.B) {
// Prepare test data (keep under MTU limit)
data := randomBytes(256)
transportID := randomBytes(16)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// Create packet
packet := NewPacket(0x00, data, PacketTypeData, ContextNone, 0x00, HeaderType1, transportID, false, 0x00)
// Pack the packet
if err := packet.Pack(); err != nil {
b.Fatalf("Packet.Pack() failed: %v", err)
}
// Get hash (triggers crypto operations)
_ = packet.GetHash()
}
}
// BenchmarkPacketSerializeDeserialize benchmarks the full pack/unpack cycle
func BenchmarkPacketSerializeDeserialize(b *testing.B) {
// Prepare test data (keep under MTU limit)
data := randomBytes(256)
transportID := randomBytes(16)
// Create and pack original packet
originalPacket := NewPacket(0x00, data, PacketTypeData, ContextNone, 0x00, HeaderType1, transportID, false, 0x00)
if err := originalPacket.Pack(); err != nil {
b.Fatalf("Original packet.Pack() failed: %v", err)
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// Create new packet from raw data
packet := &Packet{Raw: make([]byte, len(originalPacket.Raw))}
copy(packet.Raw, originalPacket.Raw)
// Unpack the packet
if err := packet.Unpack(); err != nil {
b.Fatalf("Packet.Unpack() failed: %v", err)
}
// Re-pack
if err := packet.Pack(); err != nil {
b.Fatalf("Packet.Pack() failed: %v", err)
}
}
}

View File

@@ -0,0 +1,34 @@
package pathfinder
import "time"
type PathFinder struct {
paths map[string]Path
}
type Path struct {
NextHop []byte
Interface string
HopCount byte
LastUpdated int64
}
func NewPathFinder() *PathFinder {
return &PathFinder{
paths: make(map[string]Path),
}
}
func (p *PathFinder) AddPath(destHash string, nextHop []byte, iface string, hops byte) {
p.paths[destHash] = Path{
NextHop: nextHop,
Interface: iface,
HopCount: hops,
LastUpdated: time.Now().Unix(),
}
}
func (p *PathFinder) GetPath(destHash string) (Path, bool) {
path, exists := p.paths[destHash]
return path, exists
}

193
pkg/rate/rate.go Normal file
View File

@@ -0,0 +1,193 @@
package rate
import (
"sync"
"time"
)
const (
DefaultAnnounceRateTarget = 3600.0 // Default 1 hour between announces
DefaultAnnounceRateGrace = 3 // Default number of grace announces
DefaultAnnounceRatePenalty = 7200.0 // Default 2 hour penalty
DefaultBurstFreqNew = 3.5 // Default announces/sec for new interfaces
DefaultBurstFreq = 12.0 // Default announces/sec for established interfaces
DefaultBurstHold = 60 // Default seconds to hold after burst
DefaultBurstPenalty = 300 // Default seconds penalty after burst
DefaultMaxHeldAnnounces = 256 // Default max announces in hold queue
DefaultHeldReleaseInterval = 30 // Default seconds between releasing held announces
)
type Limiter struct {
rate float64
interval time.Duration
lastUpdate time.Time
allowance float64
mutex sync.Mutex
}
func NewLimiter(rate float64, interval time.Duration) *Limiter {
return &Limiter{
rate: rate,
interval: interval,
lastUpdate: time.Now(),
allowance: rate,
}
}
func (l *Limiter) Allow() bool {
l.mutex.Lock()
defer l.mutex.Unlock()
now := time.Now()
elapsed := now.Sub(l.lastUpdate)
l.lastUpdate = now
l.allowance += elapsed.Seconds() * l.rate
if l.allowance > l.rate {
l.allowance = l.rate
}
if l.allowance < 1.0 {
return false
}
l.allowance -= 1.0
return true
}
// AnnounceRateControl handles per-destination announce rate limiting
type AnnounceRateControl struct {
rateTarget float64
rateGrace int
ratePenalty float64
announceHistory map[string][]time.Time // Maps dest hash to announce times
mutex sync.RWMutex
}
func NewAnnounceRateControl(target float64, grace int, penalty float64) *AnnounceRateControl {
return &AnnounceRateControl{
rateTarget: target,
rateGrace: grace,
ratePenalty: penalty,
announceHistory: make(map[string][]time.Time),
}
}
func (arc *AnnounceRateControl) AllowAnnounce(destHash string) bool {
arc.mutex.Lock()
defer arc.mutex.Unlock()
history := arc.announceHistory[destHash]
now := time.Now()
// Cleanup old history entries
cutoff := now.Add(-24 * time.Hour)
newHistory := []time.Time{}
for _, t := range history {
if t.After(cutoff) {
newHistory = append(newHistory, t)
}
}
history = newHistory
// Allow if within grace period
if len(history) < arc.rateGrace {
arc.announceHistory[destHash] = append(history, now)
return true
}
// Check rate
lastAnnounce := history[len(history)-1]
waitTime := arc.rateTarget
if len(history) > arc.rateGrace {
waitTime += arc.ratePenalty
}
if now.Sub(lastAnnounce).Seconds() < waitTime {
return false
}
arc.announceHistory[destHash] = append(history, now)
return true
}
// IngressControl handles new destination announce rate limiting
type IngressControl struct {
enabled bool
burstFreqNew float64
burstFreq float64
burstHold time.Duration
burstPenalty time.Duration
maxHeldAnnounces int
heldReleaseInterval time.Duration
heldAnnounces map[string][]byte // Maps announce hash to announce data
lastBurst time.Time
announceCount int
mutex sync.RWMutex
}
func NewIngressControl(enabled bool) *IngressControl {
return &IngressControl{
enabled: enabled,
burstFreqNew: DefaultBurstFreqNew,
burstFreq: DefaultBurstFreq,
burstHold: time.Duration(DefaultBurstHold) * time.Second,
burstPenalty: time.Duration(DefaultBurstPenalty) * time.Second,
maxHeldAnnounces: DefaultMaxHeldAnnounces,
heldReleaseInterval: time.Duration(DefaultHeldReleaseInterval) * time.Second,
heldAnnounces: make(map[string][]byte),
lastBurst: time.Now(),
}
}
func (ic *IngressControl) ProcessAnnounce(announceHash string, announceData []byte, isNewDest bool) bool {
if !ic.enabled {
return true
}
ic.mutex.Lock()
defer ic.mutex.Unlock()
now := time.Now()
elapsed := now.Sub(ic.lastBurst)
// Reset counter if enough time has passed
if elapsed > ic.burstHold+ic.burstPenalty {
ic.announceCount = 0
ic.lastBurst = now
}
// Check burst frequency
maxFreq := ic.burstFreq
if isNewDest {
maxFreq = ic.burstFreqNew
}
ic.announceCount++
burstFreq := float64(ic.announceCount) / elapsed.Seconds()
// Hold announce if burst frequency exceeded
if burstFreq > maxFreq {
if len(ic.heldAnnounces) < ic.maxHeldAnnounces {
ic.heldAnnounces[announceHash] = announceData
}
return false
}
return true
}
func (ic *IngressControl) ReleaseHeldAnnounce() (string, []byte, bool) {
ic.mutex.Lock()
defer ic.mutex.Unlock()
// Return first held announce if any exist
for hash, data := range ic.heldAnnounces {
delete(ic.heldAnnounces, hash)
return hash, data, true
}
return "", nil, false
}

74
pkg/resolver/resolver.go Normal file
View File

@@ -0,0 +1,74 @@
package resolver
import (
"crypto/sha256"
"encoding/hex"
"errors"
"strings"
"sync"
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
)
type Resolver struct {
cache map[string]*identity.Identity
cacheLock sync.RWMutex
}
func New() *Resolver {
return &Resolver{
cache: make(map[string]*identity.Identity),
}
}
func (r *Resolver) ResolveIdentity(fullName string) (*identity.Identity, error) {
if fullName == "" {
return nil, errors.New("empty identity name")
}
r.cacheLock.RLock()
if cachedIdentity, exists := r.cache[fullName]; exists {
r.cacheLock.RUnlock()
return cachedIdentity, nil
}
r.cacheLock.RUnlock()
// Hash the full name to create a deterministic identity
h := sha256.New()
h.Write([]byte(fullName))
nameHash := h.Sum(nil)[:identity.NAME_HASH_LENGTH/8]
hashStr := hex.EncodeToString(nameHash)
// Check if this identity is known
if knownData, exists := identity.GetKnownDestination(hashStr); exists {
if id, ok := knownData[2].(*identity.Identity); ok {
r.cacheLock.Lock()
r.cache[fullName] = id
r.cacheLock.Unlock()
return id, nil
}
}
// Split name into parts for hierarchical resolution
parts := strings.Split(fullName, ".")
if len(parts) < 2 {
return nil, errors.New("invalid identity name format")
}
// Create new identity if not found
id, err := identity.New()
if err != nil {
return nil, err
}
r.cacheLock.Lock()
r.cache[fullName] = id
r.cacheLock.Unlock()
return id, nil
}
func ResolveIdentity(fullName string) (*identity.Identity, error) {
r := New()
return r.ResolveIdentity(fullName)
}

View File

@@ -2,13 +2,12 @@ package resource
import (
"crypto/sha256"
"encoding/binary"
"errors"
"io"
"path/filepath"
"strings"
"sync"
"time"
"path/filepath"
)
const (
@@ -19,88 +18,90 @@ const (
STATUS_CANCELLED = 0x04
DEFAULT_SEGMENT_SIZE = 384 // Based on ENCRYPTED_MDU
MAX_SEGMENTS = 65535
CLEANUP_INTERVAL = 300 // 5 minutes
MAX_SEGMENTS = 65535
CLEANUP_INTERVAL = 300 // 5 minutes
// Window size constants
WINDOW = 4
WINDOW_MIN = 2
WINDOW_MAX_SLOW = 10
WINDOW_MIN = 2
WINDOW_MAX_SLOW = 10
WINDOW_MAX_VERY_SLOW = 4
WINDOW_MAX_FAST = 75
WINDOW_MAX = WINDOW_MAX_FAST
WINDOW_MAX_FAST = 75
WINDOW_MAX = WINDOW_MAX_FAST
// Rate thresholds
FAST_RATE_THRESHOLD = WINDOW_MAX_SLOW - WINDOW - 2
FAST_RATE_THRESHOLD = WINDOW_MAX_SLOW - WINDOW - 2
VERY_SLOW_RATE_THRESHOLD = 2
// Transfer rates (bytes per second)
RATE_FAST = (50 * 1000) / 8 // 50 Kbps
RATE_VERY_SLOW = (2 * 1000) / 8 // 2 Kbps
RATE_FAST = (50 * 1000) / 8 // 50 Kbps
RATE_VERY_SLOW = (2 * 1000) / 8 // 2 Kbps
// Window flexibility
WINDOW_FLEXIBILITY = 4
// Hash and segment constants
MAPHASH_LEN = 4
MAPHASH_LEN = 4
RANDOM_HASH_SIZE = 4
// Size limits
MAX_EFFICIENT_SIZE = 16*1024*1024 - 1 // ~16MB
MAX_EFFICIENT_SIZE = 16*1024*1024 - 1 // ~16MB
AUTO_COMPRESS_MAX_SIZE = MAX_EFFICIENT_SIZE
// Timeouts and retries
PART_TIMEOUT_FACTOR = 4
PART_TIMEOUT_FACTOR = 4
PART_TIMEOUT_FACTOR_AFTER_RTT = 2
PROOF_TIMEOUT_FACTOR = 3
MAX_RETRIES = 16
MAX_ADV_RETRIES = 4
SENDER_GRACE_TIME = 10.0
PROCESSING_GRACE = 1.0
RETRY_GRACE_TIME = 0.25
PER_RETRY_DELAY = 0.5
PROOF_TIMEOUT_FACTOR = 3
MAX_RETRIES = 16
MAX_ADV_RETRIES = 4
SENDER_GRACE_TIME = 10.0
PROCESSING_GRACE = 1.0
RETRY_GRACE_TIME = 0.25
PER_RETRY_DELAY = 0.5
)
type Resource struct {
mutex sync.RWMutex
mutex sync.RWMutex
data []byte
fileHandle io.ReadWriteSeeker
fileName string
hash []byte
randomHash []byte
originalHash []byte
status byte
compressed bool
autoCompress bool
encrypted bool
split bool
segments uint16
segmentIndex uint16
totalSegments uint16
completedParts map[uint16]bool
transferSize int64
dataSize int64
progress float64
window int
windowMax int
windowMin int
windowFlexibility int
rtt float64
fastRateRounds int
status byte
compressed bool
autoCompress bool
encrypted bool
split bool
segments uint16
segmentIndex uint16
totalSegments uint16
completedParts map[uint16]bool
transferSize int64
dataSize int64
progress float64
window int
windowMax int
windowMin int
windowFlexibility int
rtt float64
fastRateRounds int
verySlowRateRounds int
createdAt time.Time
completedAt time.Time
callback func(*Resource)
progressCallback func(*Resource)
createdAt time.Time
completedAt time.Time
callback func(*Resource)
progressCallback func(*Resource)
readOffset int64
}
func New(data interface{}, autoCompress bool) (*Resource, error) {
r := &Resource{
status: STATUS_PENDING,
compressed: false,
autoCompress: autoCompress,
autoCompress: autoCompress,
completedParts: make(map[uint16]bool),
createdAt: time.Now(),
progress: 0.0,
createdAt: time.Now(),
progress: 0.0,
}
switch v := data.(type) {
@@ -118,12 +119,16 @@ func New(data interface{}, autoCompress bool) (*Resource, error) {
if err != nil {
return nil, err
}
if namer, ok := v.(interface{ Name() string }); ok {
r.fileName = namer.Name()
}
default:
return nil, errors.New("unsupported data type")
}
// Calculate segments needed
r.segments = uint16((r.dataSize + DEFAULT_SEGMENT_SIZE - 1) / DEFAULT_SEGMENT_SIZE)
r.segments = uint16((r.dataSize + DEFAULT_SEGMENT_SIZE - 1) / DEFAULT_SEGMENT_SIZE) // #nosec G115
if r.segments > MAX_SEGMENTS {
return nil, errors.New("resource too large")
}
@@ -138,10 +143,10 @@ func New(data interface{}, autoCompress bool) (*Resource, error) {
r.transferSize = int64(float64(r.dataSize) * compressibility)
} else if r.fileHandle != nil {
// For file handles, use extension-based estimation
ext := strings.ToLower(filepath.Ext(r.fileHandle.Name()))
ext := strings.ToLower(filepath.Ext(r.fileName))
r.transferSize = estimateFileCompression(r.dataSize, ext)
}
// Ensure minimum size and add compression overhead
if r.transferSize < r.dataSize/10 {
r.transferSize = r.dataSize / 10
@@ -223,7 +228,7 @@ func (r *Resource) IsCompressed() bool {
func (r *Resource) Cancel() {
r.mutex.Lock()
defer r.mutex.Unlock()
if r.status == STATUS_PENDING || r.status == STATUS_ACTIVE {
r.status = STATUS_CANCELLED
r.completedAt = time.Now()
@@ -342,13 +347,13 @@ func estimateCompressibility(data []byte) float64 {
if len(data) < sampleSize {
sampleSize = len(data)
}
// Count unique bytes in sample
uniqueBytes := make(map[byte]struct{})
for i := 0; i < sampleSize; i++ {
uniqueBytes[data[i]] = struct{}{}
}
// Calculate entropy-based compression estimate
uniqueRatio := float64(len(uniqueBytes)) / float64(sampleSize)
return 0.3 + (0.7 * uniqueRatio) // Base compression ratio between 0.3 and 1.0
@@ -357,13 +362,13 @@ func estimateCompressibility(data []byte) float64 {
func estimateFileCompression(size int64, extension string) int64 {
// Compression ratio estimates based on common file types
compressionRatios := map[string]float64{
".txt": 0.4, // Text compresses well
".txt": 0.4, // Text compresses well
".log": 0.4,
".json": 0.4,
".xml": 0.4,
".html": 0.4,
".csv": 0.5,
".doc": 0.8, // Already compressed
".doc": 0.8, // Already compressed
".docx": 0.95,
".pdf": 0.95,
".jpg": 0.99, // Already compressed
@@ -376,11 +381,43 @@ func estimateFileCompression(size int64, extension string) int64 {
".gz": 0.99,
".rar": 0.99,
}
ratio, exists := compressionRatios[extension]
if !exists {
ratio = 0.7 // Default compression ratio for unknown types
}
return int64(float64(size) * ratio)
}
}
func (r *Resource) Read(p []byte) (n int, err error) {
r.mutex.Lock()
defer r.mutex.Unlock()
if r.data != nil {
if r.readOffset >= int64(len(r.data)) {
return 0, io.EOF
}
n = copy(p, r.data[r.readOffset:])
r.readOffset += int64(n)
return n, nil
}
if r.fileHandle != nil {
return r.fileHandle.Read(p)
}
return 0, errors.New("no data source available")
}
func (r *Resource) GetName() string {
r.mutex.RLock()
defer r.mutex.RUnlock()
return r.fileName
}
func (r *Resource) GetSize() int64 {
r.mutex.RLock()
defer r.mutex.RUnlock()
return r.dataSize
}

170
pkg/transport/announce.go Normal file
View File

@@ -0,0 +1,170 @@
package transport
import (
"crypto/sha256"
"encoding/hex"
"sort"
"sync"
"time"
"github.com/Sudo-Ivan/reticulum-go/pkg/rate"
)
const (
MaxRetries = 3
RetryInterval = 5 * time.Second
MaxQueueSize = 1000
MinPriorityDelta = 0.1
DefaultPropagationRate = 0.02 // 2% of bandwidth for announces
)
type AnnounceEntry struct {
Data []byte
HopCount int
RetryCount int
LastRetry time.Time
SourceIface string
Priority float64
Hash string
}
type AnnounceManager struct {
announces map[string]*AnnounceEntry
announceQueue map[string][]*AnnounceEntry
rateLimiter *rate.Limiter
mutex sync.RWMutex
}
func NewAnnounceManager() *AnnounceManager {
return &AnnounceManager{
announces: make(map[string]*AnnounceEntry),
announceQueue: make(map[string][]*AnnounceEntry),
rateLimiter: rate.NewLimiter(DefaultPropagationRate, 1),
mutex: sync.RWMutex{},
}
}
func (am *AnnounceManager) ProcessAnnounce(data []byte, sourceIface string) error {
hash := sha256.Sum256(data)
hashStr := hex.EncodeToString(hash[:])
am.mutex.Lock()
defer am.mutex.Unlock()
if entry, exists := am.announces[hashStr]; exists {
if entry.HopCount <= int(data[0]) {
return nil
}
entry.HopCount = int(data[0])
entry.Data = data
entry.RetryCount = 0
entry.LastRetry = time.Now()
entry.Priority = calculatePriority(data[0], 0)
return nil
}
entry := &AnnounceEntry{
Data: data,
HopCount: int(data[0]),
RetryCount: 0,
LastRetry: time.Now(),
SourceIface: sourceIface,
Priority: calculatePriority(data[0], 0),
Hash: hashStr,
}
am.announces[hashStr] = entry
for iface := range am.announceQueue {
if iface != sourceIface {
am.queueAnnounce(entry, iface)
}
}
return nil
}
func (am *AnnounceManager) queueAnnounce(entry *AnnounceEntry, iface string) {
queue := am.announceQueue[iface]
if len(queue) >= MaxQueueSize {
// Remove lowest priority announce if queue is full
queue = queue[:len(queue)-1]
}
insertIdx := sort.Search(len(queue), func(i int) bool {
return queue[i].Priority < entry.Priority
})
queue = append(queue[:insertIdx], append([]*AnnounceEntry{entry}, queue[insertIdx:]...)...)
am.announceQueue[iface] = queue
}
func (am *AnnounceManager) GetNextAnnounce(iface string) *AnnounceEntry {
am.mutex.Lock()
defer am.mutex.Unlock()
queue := am.announceQueue[iface]
if len(queue) == 0 {
return nil
}
entry := queue[0]
now := time.Now()
if entry.RetryCount >= MaxRetries {
am.announceQueue[iface] = queue[1:]
delete(am.announces, entry.Hash)
return am.GetNextAnnounce(iface)
}
if now.Sub(entry.LastRetry) < RetryInterval {
return nil
}
if !am.rateLimiter.Allow() {
return nil
}
entry.RetryCount++
entry.LastRetry = now
entry.Priority = calculatePriority(byte(entry.HopCount), entry.RetryCount)
am.announceQueue[iface] = queue[1:]
am.queueAnnounce(entry, iface)
return entry
}
func calculatePriority(hopCount byte, retryCount int) float64 {
basePriority := 1.0 / float64(hopCount)
retryPenalty := float64(retryCount) * MinPriorityDelta
return basePriority - retryPenalty
}
func (am *AnnounceManager) CleanupExpired() {
am.mutex.Lock()
defer am.mutex.Unlock()
now := time.Now()
expiredHashes := make([]string, 0)
for hash, entry := range am.announces {
if entry.RetryCount >= MaxRetries || now.Sub(entry.LastRetry) > RetryInterval*MaxRetries {
expiredHashes = append(expiredHashes, hash)
}
}
for _, hash := range expiredHashes {
delete(am.announces, hash)
for iface, queue := range am.announceQueue {
newQueue := make([]*AnnounceEntry, 0, len(queue))
for _, entry := range queue {
if entry.Hash != hash {
newQueue = append(newQueue, entry)
}
}
am.announceQueue[iface] = newQueue
}
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
package transport
import (
"crypto/rand"
"testing"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
)
func randomBytes(n int) []byte {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
panic("Failed to generate random bytes: " + err.Error())
}
return b
}
// BenchmarkTransportDestinationCreation benchmarks destination creation
func BenchmarkTransportDestinationCreation(b *testing.B) {
// Create a basic config for transport
config := &common.ReticulumConfig{
ConfigPath: "/tmp/test_config",
}
transport := NewTransport(config)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// Create destination (this allocates and initializes destination objects)
dest := transport.NewDestination(nil, OUT, SINGLE, "test_app")
_ = dest // Use the destination to avoid optimization
}
}
// BenchmarkTransportPathLookup benchmarks path lookup operations
func BenchmarkTransportPathLookup(b *testing.B) {
// Create a basic config for transport
config := &common.ReticulumConfig{
ConfigPath: "/tmp/test_config",
}
transport := NewTransport(config)
// Pre-populate with some destinations
destHash1 := randomBytes(16)
destHash2 := randomBytes(16)
destHash3 := randomBytes(16)
// Create some destinations
transport.NewDestination(nil, OUT, SINGLE, "test_app")
transport.NewDestination(nil, OUT, SINGLE, "test_app")
transport.NewDestination(nil, OUT, SINGLE, "test_app")
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// Test path lookup operations (these involve map lookups and allocations)
_ = transport.HasPath(destHash1)
_ = transport.HasPath(destHash2)
_ = transport.HasPath(destHash3)
}
}
// BenchmarkTransportHopsCalculation benchmarks hops calculation
func BenchmarkTransportHopsCalculation(b *testing.B) {
// Create a basic config for transport
config := &common.ReticulumConfig{
ConfigPath: "/tmp/test_config",
}
transport := NewTransport(config)
// Create some destinations
destHash := randomBytes(16)
transport.NewDestination(nil, OUT, SINGLE, "test_app")
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// Test hops calculation (involves internal data structure access)
_ = transport.HopsTo(destHash)
}
}

42
revive.toml Normal file
View File

@@ -0,0 +1,42 @@
ignoreGeneratedHeader = false
severity = "warning"
confidence = 0.8
errorCode = 1
warningCode = 0
[rule.add-constant]
[rule.argument-limit]
[rule.atomic]
[rule.bare-return]
[rule.blank-imports]
[rule.bool-literal-in-expr]
[rule.confusing-naming]
[rule.confusing-results]
[rule.constant-logical-expr]
[rule.context-as-argument]
[rule.context-keys-type]
[rule.deep-exit]
[rule.duplicated-imports]
[rule.early-return]
[rule.empty-block]
[rule.error-naming]
[rule.error-return]
[rule.error-strings]
[rule.errorf]
[rule.exported]
[rule.if-return]
[rule.increment-decrement]
[rule.indent-error-flow]
[rule.modifies-parameter]
[rule.modifies-value-receiver]
[rule.range]
[rule.receiver-naming]
[rule.redefines-builtin-id]
[rule.string-format]
[rule.struct-tag]
[rule.superfluous-else]
[rule.time-naming]
[rule.unexported-return]
[rule.unnecessary-stmt]
[rule.unreachable-code]
[rule.var-declaration]

View File

@@ -1,53 +0,0 @@
#!/bin/bash
# Build the client and server
echo "Building Reticulum client..."
go build -o bin/reticulum-client ./cmd/client
go build -o bin/reticulum ./cmd/reticulum
# Check if build was successful
if [ $? -ne 0 ]; then
echo "Build failed!"
exit 1
fi
# Create directories
mkdir -p logs
mkdir -p bin
# Start the Reticulum server first
echo "Starting Reticulum server..."
./bin/reticulum > logs/server.log 2>&1 &
echo $! > logs/server.pid
sleep 2 # Give server time to start
# Generate identities for both clients
echo "Generating identities..."
CLIENT1_HASH=$(./bin/reticulum-client -config configs/test-client1.toml -generate-identity 2>&1 | grep "Identity hash:" | cut -d' ' -f3)
CLIENT2_HASH=$(./bin/reticulum-client -config configs/test-client2.toml -generate-identity 2>&1 | grep "Identity hash:" | cut -d' ' -f3)
echo "Client 1 Hash: $CLIENT1_HASH"
echo "Client 2 Hash: $CLIENT2_HASH"
# Function to run client
run_client() {
local config=$1
local target=$2
local logfile=$3
echo "Starting client with config: $config targeting: $target"
./bin/reticulum-client -config "$config" -target "$target" > "$logfile" 2>&1 &
echo $! > "$logfile.pid"
echo "Client started with PID: $(cat $logfile.pid)"
}
# Run both clients targeting each other
run_client "configs/test-client1.toml" "$CLIENT2_HASH" "logs/client1.log"
run_client "configs/test-client2.toml" "$CLIENT1_HASH" "logs/client2.log"
echo
echo "Both clients are running. To stop everything:"
echo "kill \$(cat logs/*.pid)"
echo
echo "To view logs:"
echo "tail -f logs/client1.log"
echo "tail -f logs/client2.log"

View File

@@ -0,0 +1,114 @@
#!/bin/bash
set -e
echo "Starting Reticulum-Go with memory and CPU monitoring..."
# Start the process in background
./bin/reticulum-go &
PID=$!
echo "Process started with PID: $PID"
# Initialize tracking variables
MAX_RSS=0
MAX_VSZ=0
MAX_CPU=0
SAMPLES=0
TOTAL_RSS=0
TOTAL_VSZ=0
TOTAL_CPU=0
END_TIME=$((SECONDS + 120))
while [ $SECONDS -lt $END_TIME ] && kill -0 $PID 2>/dev/null; do
# Get memory and CPU info using ps
if PROC_INFO=$(ps -o pid,rss,vsz,pcpu --no-headers -p $PID 2>/dev/null); then
RSS=$(echo $PROC_INFO | awk '{print $2}') # RSS in KB
VSZ=$(echo $PROC_INFO | awk '{print $3}') # VSZ in KB
CPU=$(echo $PROC_INFO | awk '{print $4}') # CPU percentage
if [ -n "$RSS" ] && [ -n "$VSZ" ] && [ -n "$CPU" ]; then
SAMPLES=$((SAMPLES + 1))
TOTAL_RSS=$((TOTAL_RSS + RSS))
TOTAL_VSZ=$((TOTAL_VSZ + VSZ))
CPU_INT=$(echo $CPU | cut -d. -f1)
TOTAL_CPU=$((TOTAL_CPU + CPU_INT))
if [ $RSS -gt $MAX_RSS ]; then
MAX_RSS=$RSS
fi
if [ $VSZ -gt $MAX_VSZ ]; then
MAX_VSZ=$VSZ
fi
# CPU is already a percentage (0-100), so compare as integers
CPU_INT=$(echo $CPU | cut -d. -f1)
if [ $CPU_INT -gt $MAX_CPU ]; then
MAX_CPU=$CPU_INT
fi
fi
fi
sleep 0.1 # Sample every 100ms
done
# Stop the process if still running
if kill -0 $PID 2>/dev/null; then
echo "Stopping process..."
kill $PID 2>/dev/null || true
sleep 1
kill -9 $PID 2>/dev/null || true
fi
# Calculate averages
if [ $SAMPLES -gt 0 ]; then
AVG_RSS=$((TOTAL_RSS / SAMPLES))
AVG_VSZ=$((TOTAL_VSZ / SAMPLES))
AVG_CPU=$((TOTAL_CPU / SAMPLES))
else
AVG_RSS=0
AVG_VSZ=0
AVG_CPU=0
fi
# Convert to MB and GB
MAX_RSS_MB=$((MAX_RSS / 1024))
MAX_RSS_GB=$((MAX_RSS_MB / 1024))
AVG_RSS_MB=$((AVG_RSS / 1024))
AVG_RSS_GB=$((AVG_RSS_MB / 1024))
MAX_VSZ_MB=$((MAX_VSZ / 1024))
MAX_VSZ_GB=$((MAX_VSZ_MB / 1024))
AVG_VSZ_MB=$((AVG_VSZ / 1024))
AVG_VSZ_GB=$((AVG_VSZ_MB / 1024))
# Output results
echo "=== Performance Usage Report ==="
echo "Monitoring duration: 120 seconds"
echo "Samples collected: $SAMPLES"
echo ""
echo "## CPU Usage - Processor Utilization (since process start)"
echo "- Max CPU: ${MAX_CPU}%"
echo "- Avg CPU: ${AVG_CPU}%"
echo "- Note: Low CPU usage is normal for I/O-bound network applications"
echo ""
echo "## RSS (Resident Set Size) - Actual Memory Used"
echo "- Max RSS: ${MAX_RSS} KB (${MAX_RSS_MB} MB / ${MAX_RSS_GB} GB)"
echo "- Avg RSS: ${AVG_RSS} KB (${AVG_RSS_MB} MB / ${AVG_RSS_GB} GB)"
echo ""
echo "## VSZ (Virtual Memory Size) - Total Virtual Memory"
echo "- Max VSZ: ${MAX_VSZ} KB (${MAX_VSZ_MB} MB / ${MAX_VSZ_GB} GB)"
echo "- Avg VSZ: ${AVG_VSZ} KB (${AVG_VSZ_MB} MB / ${AVG_VSZ_GB} GB)"
echo ""
# Output for potential future use
echo "MAX_CPU=$MAX_CPU" >> $GITHUB_OUTPUT
echo "AVG_CPU=$AVG_CPU" >> $GITHUB_OUTPUT
echo "MAX_RSS_MB=$MAX_RSS_MB" >> $GITHUB_OUTPUT
echo "AVG_RSS_MB=$AVG_RSS_MB" >> $GITHUB_OUTPUT
echo "MAX_VSZ_MB=$MAX_VSZ_MB" >> $GITHUB_OUTPUT
echo "AVG_VSZ_MB=$AVG_VSZ_MB" >> $GITHUB_OUTPUT