76 Commits

Author SHA1 Message Date
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
51 changed files with 6939 additions and 1863 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"

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

@@ -0,0 +1,28 @@
name: "Security Scan"
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
schedule:
- cron: '0 0 * * 0'
jobs:
tests:
runs-on: ubuntu-latest
permissions:
security-events: write
env:
GO111MODULE: on
steps:
- name: Checkout Source
uses: actions/checkout@v4
- name: Run Gosec Security Scanner
uses: securego/gosec@master
with:
args: '-no-fail -fmt sarif -out results.sarif ./...'
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: results.sarif

16
.gitignore vendored
View File

@@ -1,8 +1,16 @@
reticulum-client
reticulum-server
bin/
logs/ logs/
*.log
.env .env
.json .json
bin/
RNS/
test-network/go-rns-network/reticulum/storage/
test-network/go-rns-network/reticulum/interfaces/
test-network/go-rns-network/nomadnetwork
/test-network/go-rns-network/
/test-network/go-rns-network-client/

9
LICENSE Normal file
View File

@@ -0,0 +1,9 @@
Copyright 2024 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.

97
Makefile Normal file
View File

@@ -0,0 +1,97 @@
GOCMD=go
GOBUILD=$(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 clean test coverage deps help
all: clean deps build test
build:
@mkdir -p $(BUILD_DIR)
$(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_PACKAGE)
clean:
@rm -rf $(BUILD_DIR)
$(GOCLEAN)
test:
$(GOTEST) -v $(ALL_PACKAGES)
coverage:
$(GOTEST) -coverprofile=coverage.out $(ALL_PACKAGES)
$(GOCMD) tool cover -html=coverage.out
deps:
$(GOMOD) download
$(GOMOD) verify
build-linux:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=linux GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm $(MAIN_PACKAGE)
build-windows:
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-arm64.exe $(MAIN_PACKAGE)
build-darwin:
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 $(MAIN_PACKAGE)
build-freebsd:
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-amd64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=freebsd GOARCH=386 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-386 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-arm64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-arm $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=freebsd GOARCH=riscv64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-riscv64 $(MAIN_PACKAGE)
build-openbsd:
CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-amd64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=openbsd GOARCH=386 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-386 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-arm64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=openbsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-arm $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=openbsd GOARCH=ppc64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-ppc64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=openbsd GOARCH=riscv64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-riscv64 $(MAIN_PACKAGE)
build-netbsd:
CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-amd64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=netbsd GOARCH=386 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-386 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm64 $(MAIN_PACKAGE)
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm $(MAIN_PACKAGE)
build-all: build-linux build-windows build-darwin build-freebsd build-openbsd build-netbsd
run:
@./$(BUILD_DIR)/$(BINARY_NAME)
install:
$(GOMOD) download
help:
@echo "Available targets:"
@echo " all - Clean, download dependencies, build and test"
@echo " build - Build binary"
@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-all - Build for all platforms and architectures"
@echo " run - Run reticulum binary"
@echo " install - Install dependencies"

189
README.md
View File

@@ -1,4 +1,191 @@
# Reticulum-Go # Reticulum-Go
Reticulum Network Stack in Go. [Reticulum Network](https://github.com/markqvist/Reticulum) implementation in Go `1.24`.
Aiming for full spec compatibility with the Python version 0.9.2.
# Testing
```
make install
make build
make run
```
## Linter
[Revive](https://github.com/mgechev/revive)
```bash
revive -config revive.toml -formatter friendly ./pkg/* ./cmd/* ./internal/*
```
## External Packages
- `golang.org/x/crypto` - Cryptographic primitives
## To-Do List
### Core Components (In Progress)
- [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-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
- [x] Announce implementation
- [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 benchmarks
- [ ] Security auditing (When Reticulum is 1.0 / stable)
### 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

47
SECURITY.md Normal file
View File

@@ -0,0 +1,47 @@
# Security Policy
I use [Socket](https://socket.dev/), [Deepsource](https://deepsource.com/) and [gosec](https://github.com/securego/gosec) for this project.
## 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 to [rns@quad4.io](mailto:rns@quad4.io)
**PGP Key:**
```
-----BEGIN PGP PUBLIC KEY BLOCK-----
xjMEZ3RaxBYJKwYBBAHaRw8BAQdAcW8OFXyQ6KuqoTWKVbULYgakD/CeW50y
W0KFou8WwJTNG3Juc0BxdWFkNC5pbyA8cm5zQHF1YWQ0LmlvPsLAEQQTFgoA
gwWCZ3RaxAMLCQcJkJm7qyNLc8pmRRQAAAAAABwAIHNhbHRAbm90YXRpb25z
Lm9wZW5wZ3Bqcy5vcmdVRY9jqwrIm+oRWRFnnBjKUcqvkG/kwkQZ3T74Xz3K
QQMVCggEFgACAQIZAQKbAwIeARYhBG62BFzXpfHCy0yV95m7qyNLc8pmAACS
oQD+K8oIaGx3tOlQbBV5AT3pHCaqXpRoL4W0V4JWc3VCi+MA/iiW6peitoae
+YhKE5lnkiU1jP47VuItQDNt+fNyqNAOzjgEZ3RaxBIKKwYBBAGXVQEFAQEH
QOBQyIb3gXV0Uih/V9Yx5JsFavxSenCtncNXx5KM6cB8AwEIB8K+BBgWCgBw
BYJndFrECZCZu6sjS3PKZkUUAAAAAAAcACBzYWx0QG5vdGF0aW9ucy5vcGVu
cGdwanMub3Jnpqm3qWGYB50CM/kuv+byGwQ3wxIGIpRlK8pwT4l+wXICmwwW
IQRutgRc16XxwstMlfeZu6sjS3PKZgAAzm0BAIKHfL9G+IzCX9B1gVGcG9an
j+gC4y9FrEsmFEBpvGeXAP93FfhO447jWijmxsImTtHTyvhpfeR3a7huFFyi
lh60DA==
=Nm9f
-----END PGP PUBLIC KEY BLOCK-----
```
## Gosec Command
`gosec ./cmd/* ./pkg/* ./internal/*`

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

View File

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

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

@@ -0,0 +1,642 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"runtime"
"sync"
"syscall"
"time"
"github.com/Sudo-Ivan/reticulum-go/internal/config"
"github.com/Sudo-Ivan/reticulum-go/pkg/announce"
"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/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 (
debugLevel = flag.Int("debug", 7, "Debug level (0-7)")
interceptPackets = flag.Bool("intercept-packets", false, "Enable packet interception")
interceptOutput = flag.String("intercept-output", "packets.log", "Output file for intercepted packets")
)
func debugLog(level int, format string, v ...interface{}) {
if *debugLevel >= level {
log.Printf("[DEBUG-%d] %s", level, fmt.Sprintf(format, v...))
}
}
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
DEBUG_CRITICAL = 1 // Critical errors
DEBUG_ERROR = 2 // Non-critical errors
DEBUG_INFO = 3 // Important information
DEBUG_VERBOSE = 4 // Detailed information
DEBUG_TRACE = 5 // Very detailed tracing
DEBUG_PACKETS = 6 // Packet-level details
DEBUG_ALL = 7 // Everything including identity operations
APP_NAME = "Go-Client"
APP_ASPECT = "node"
)
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
}
type announceRecord struct {
}
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 = "Go Client"
}
if cfg.AppAspect == "" {
cfg.AppAspect = "node"
}
if err := initializeDirectories(); err != nil {
return nil, fmt.Errorf("failed to initialize directories: %v", err)
}
debugLog(3, "Directories initialized")
t := transport.NewTransport(cfg)
debugLog(3, "Transport initialized")
identity, err := identity.NewIdentity()
if err != nil {
return nil, fmt.Errorf("failed to create identity: %v", err)
}
debugLog(2, "Created new identity: %x", identity.Hash())
// Create destination
debugLog(DEBUG_INFO, "Creating destination...")
dest, err := destination.New(
identity,
destination.IN,
destination.SINGLE,
APP_NAME,
APP_ASPECT,
)
if err != nil {
return nil, fmt.Errorf("failed to create destination: %v", err)
}
debugLog(DEBUG_INFO, "Created destination with hash: %x", dest.GetHash())
// Set default app data for announces
appData := []byte(fmt.Sprintf(`{"app":"%s","aspect":"%s","version":"0.1.0"}`, APP_NAME, APP_ASPECT))
dest.SetDefaultAppData(appData)
// Enable destination features
dest.AcceptsLinks(true)
dest.EnableRatchets("") // Empty string for default path
dest.SetProofStrategy(destination.PROVE_APP)
debugLog(DEBUG_VERBOSE, "Configured destination features")
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,
}
// 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.Enabled,
true, // IN
true, // OUT
)
case "UDPInterface":
iface, err = interfaces.NewUDPInterface(
name,
ifaceConfig.Address,
ifaceConfig.TargetHost,
ifaceConfig.Enabled,
)
case "AutoInterface":
iface, err = interfaces.NewAutoInterface(name, ifaceConfig)
default:
debugLog(1, "Unknown interface type: %s", ifaceConfig.Type)
continue
}
if err != nil {
if cfg.PanicOnInterfaceErr {
return nil, fmt.Errorf("failed to create interface %s: %v", name, err)
}
debugLog(1, "Error creating interface %s: %v", name, err)
continue
}
// Set packet callback
iface.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
if r.transport != nil {
r.transport.HandlePacket(data, ni)
}
})
debugLog(2, "Configuring interface %s (type=%s)...", name, ifaceConfig.Type)
r.interfaces = append(r.interfaces, iface)
debugLog(3, "Interface %s started successfully", name)
}
return r, nil
}
func (r *Reticulum) handleInterface(iface common.NetworkInterface) {
debugLog(DEBUG_INFO, "Setting up interface %s (type=%T)", iface.GetName(), 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)
debugLog(DEBUG_PACKETS, "Interface %s: Reading %d bytes from buffer", iface.GetName(), size)
iface.ProcessIncoming(data)
if len(data) > 0 {
debugLog(DEBUG_TRACE, "Interface %s: Received packet type 0x%02x", iface.GetName(), 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())
}
debugLog(DEBUG_VERBOSE, stats)
}
}
}
}
func main() {
flag.Parse()
debugLog(1, "Initializing Reticulum (Debug Level: %d)...", *debugLevel)
cfg, err := config.InitConfig()
if err != nil {
log.Fatalf("Failed to initialize config: %v", err)
}
debugLog(2, "Configuration loaded from: %s", cfg.ConfigPath)
// Add default TCP interfaces if none configured
if len(cfg.Interfaces) == 0 {
debugLog(2, "No interfaces configured, adding default TCP interfaces")
cfg.Interfaces = make(map[string]*common.InterfaceConfig)
cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
Type: "TCPClientInterface",
Enabled: true,
TargetHost: "127.0.0.1",
TargetPort: 4242,
Name: "Go-RNS-Testnet",
}
cfg.Interfaces["Quad4 TCP"] = &common.InterfaceConfig{
Type: "TCPClientInterface",
Enabled: true,
TargetHost: "rns.quad4.io",
TargetPort: 4242,
Name: "Quad4 TCP",
}
}
r, err := NewReticulum(cfg)
if err != nil {
log.Fatalf("Failed to create Reticulum instance: %v", err)
}
// Create announce using r.identity
announce, err := announce.NewAnnounce(
r.identity,
[]byte("HELLO WORLD"),
nil,
false,
r.config,
)
if err != nil {
log.Fatalf("Failed to create announce: %v", err)
}
// 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 {
log.Fatalf("Failed to start Reticulum: %v", err)
}
// Send initial announces after interfaces are ready
time.Sleep(2 * time.Second) // Give interfaces time to connect
for _, iface := range r.interfaces {
if netIface, ok := iface.(common.NetworkInterface); ok {
if netIface.IsEnabled() && netIface.IsOnline() {
debugLog(2, "Sending initial announce on interface %s", netIface.GetName())
if err := announce.Propagate([]common.NetworkInterface{netIface}); err != nil {
debugLog(1, "Failed to propagate initial announce: %v", err)
}
}
}
}
// Start periodic announces
go func() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
debugLog(3, "Starting periodic announce cycle")
for _, iface := range r.interfaces {
if netIface, ok := iface.(common.NetworkInterface); ok {
if netIface.IsEnabled() && netIface.IsOnline() {
debugLog(2, "Sending periodic announce on interface %s", netIface.GetName())
if err := announce.Propagate([]common.NetworkInterface{netIface}); err != nil {
debugLog(1, "Failed to propagate periodic announce: %v", err)
}
}
}
}
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
debugLog(1, "Shutting down...")
if err := r.Stop(); err != nil {
debugLog(1, "Error during shutdown: %v", err)
}
debugLog(1, "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, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %v", dir, err)
}
}
return nil
}
func (r *Reticulum) Start() error {
debugLog(2, "Starting Reticulum...")
if err := r.transport.Start(); err != nil {
return fmt.Errorf("failed to start transport: %v", err)
}
debugLog(3, "Transport started successfully")
// Start interfaces
for _, iface := range r.interfaces {
debugLog(2, "Starting interface %s...", iface.GetName())
if err := iface.Start(); err != nil {
if r.config.PanicOnInterfaceErr {
return fmt.Errorf("failed to start interface %s: %v", iface.GetName(), err)
}
debugLog(1, "Error starting interface %s: %v", iface.GetName(), err)
continue
}
if netIface, ok := iface.(common.NetworkInterface); ok {
r.handleInterface(netIface)
}
debugLog(3, "Interface %s started successfully", iface.GetName())
}
// Wait for interfaces to initialize
time.Sleep(2 * time.Second)
// Send initial announce once per interface
initialAnnounce, err := announce.NewAnnounce(
r.identity,
createAppData(r.config.AppName, r.config.AppAspect),
nil,
false,
r.config,
)
if err != nil {
return fmt.Errorf("failed to create announce: %v", err)
}
for _, iface := range r.interfaces {
if netIface, ok := iface.(common.NetworkInterface); ok {
if netIface.IsEnabled() && netIface.IsOnline() {
debugLog(2, "Sending initial announce on interface %s", netIface.GetName())
if err := initialAnnounce.Propagate([]common.NetworkInterface{netIface}); err != nil {
debugLog(1, "Failed to send initial announce on interface %s: %v", netIface.GetName(), err)
}
// Add delay between interfaces
time.Sleep(100 * time.Millisecond)
}
}
}
// Start periodic announce goroutine with rate limiting
go func() {
ticker := time.NewTicker(ANNOUNCE_RATE_TARGET * time.Second)
defer ticker.Stop()
announceCount := 0
for range ticker.C {
announceCount++
debugLog(3, "Starting periodic announce cycle #%d", announceCount)
periodicAnnounce, err := announce.NewAnnounce(
r.identity,
createAppData(r.config.AppName, r.config.AppAspect),
nil,
false,
r.config,
)
if err != nil {
debugLog(1, "Failed to create periodic announce: %v", err)
continue
}
// Send to each interface with rate limiting
for _, iface := range r.interfaces {
if netIface, ok := iface.(common.NetworkInterface); ok {
if netIface.IsEnabled() && netIface.IsOnline() {
// Apply rate limiting after grace period
if announceCount > ANNOUNCE_RATE_GRACE {
time.Sleep(time.Duration(ANNOUNCE_RATE_PENALTY) * time.Second)
}
debugLog(2, "Sending periodic announce on interface %s", netIface.GetName())
if err := periodicAnnounce.Propagate([]common.NetworkInterface{netIface}); err != nil {
debugLog(1, "Failed to send periodic announce on interface %s: %v", netIface.GetName(), err)
continue
}
}
}
}
}
}()
go r.monitorInterfaces()
debugLog(2, "Reticulum started successfully")
return nil
}
func (r *Reticulum) Stop() error {
debugLog(2, "Stopping Reticulum...")
for _, buf := range r.buffers {
if err := buf.Close(); err != nil {
debugLog(1, "Error closing buffer: %v", err)
}
}
for _, ch := range r.channels {
if err := ch.Close(); err != nil {
debugLog(1, "Error closing channel: %v", err)
}
}
for _, iface := range r.interfaces {
if err := iface.Stop(); err != nil {
debugLog(1, "Error stopping interface %s: %v", iface.GetName(), err)
}
}
if err := r.transport.Close(); err != nil {
return fmt.Errorf("failed to close transport: %v", err)
}
debugLog(2, "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 {
debugLog(DEBUG_INFO, "Received announce from %x", destHash)
debugLog(DEBUG_PACKETS, "Raw announce data: %x", appData)
// Parse msgpack array
if len(appData) > 0 {
if appData[0] == 0x92 { // msgpack array of 2 elements
var pos = 1
// Parse first element (name)
if appData[pos] == 0xc4 { // bin 8 format
nameLen := int(appData[pos+1])
name := string(appData[pos+2 : pos+2+nameLen])
pos += 2 + nameLen
debugLog(DEBUG_VERBOSE, "Announce name: %s", name)
// Parse second element (app data)
if pos < len(appData) && appData[pos] == 0xc4 { // bin 8 format
dataLen := int(appData[pos+1])
data := appData[pos+2 : pos+2+dataLen]
debugLog(DEBUG_VERBOSE, "Announce app data: %s", string(data))
}
}
}
}
// Type assert and log identity details
if identity, ok := id.(*identity.Identity); ok {
debugLog(DEBUG_ALL, "Identity details:")
debugLog(DEBUG_ALL, " Hash: %s", identity.GetHexHash())
debugLog(DEBUG_ALL, " Public Key: %x", identity.GetPublicKey())
ratchets := identity.GetRatchets()
debugLog(DEBUG_ALL, " Active Ratchets: %d", len(ratchets))
if len(ratchets) > 0 {
ratchetKey := identity.GetCurrentRatchetKey()
if ratchetKey != nil {
ratchetID := identity.GetRatchetID(ratchetKey)
debugLog(DEBUG_ALL, " Current Ratchet ID: %x", ratchetID)
}
}
// Store announce in history
h.reticulum.announceHistoryMu.Lock()
h.reticulum.announceHistory[identity.GetHexHash()] = announceRecord{
// You can add fields here to store relevant announce data
}
h.reticulum.announceHistoryMu.Unlock()
debugLog(DEBUG_VERBOSE, "Stored announce in history for identity %s", identity.GetHexHash())
}
return nil
}
func (h *AnnounceHandler) ReceivePathResponses() bool {
return true
}
func (r *Reticulum) GetDestination() *destination.Destination {
return r.destination
}
func createAppData(appName, appAspect string) []byte {
nameString := fmt.Sprintf("%s.%s", appName, appAspect)
// Create MessagePack array with 2 elements
appData := []byte{0x92} // Fix array with 2 elements
// First element: name string (always use str 8 format for consistency)
nameBytes := []byte(nameString)
appData = append(appData, 0xd9) // str 8 format
appData = append(appData, byte(len(nameBytes))) // length
appData = append(appData, nameBytes...) // string data
// Second element: version string (always use str 8 format for consistency)
version := "0.1.0"
versionBytes := []byte(version)
appData = append(appData, 0xd9) // str 8 format
appData = append(appData, byte(len(versionBytes))) // length
appData = append(appData, versionBytes...) // string data
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"

8
go.mod
View File

@@ -1,9 +1,5 @@
module github.com/Sudo-Ivan/reticulum-go module github.com/Sudo-Ivan/reticulum-go
go 1.23.4 go 1.24.0
require ( require golang.org/x/crypto v0.31.0
github.com/pelletier/go-toml v1.9.5
golang.org/x/crypto v0.31.0
gopkg.in/yaml.v3 v3.0.1
)

6
go.sum
View File

@@ -1,8 +1,2 @@
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 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 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=

View File

@@ -1,28 +1,31 @@
package config package config
import ( import (
"bufio"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings"
"github.com/pelletier/go-toml"
"github.com/Sudo-Ivan/reticulum-go/pkg/common" "github.com/Sudo-Ivan/reticulum-go/pkg/common"
) )
const ( const (
DefaultSharedInstancePort = 37428 DefaultSharedInstancePort = 37428
DefaultInstanceControlPort = 37429 DefaultInstanceControlPort = 37429
DefaultLogLevel = 4 DefaultLogLevel = 4
) )
func DefaultConfig() *common.ReticulumConfig { func DefaultConfig() *common.ReticulumConfig {
return &common.ReticulumConfig{ return &common.ReticulumConfig{
EnableTransport: false, EnableTransport: true,
ShareInstance: true, ShareInstance: true,
SharedInstancePort: DefaultSharedInstancePort, SharedInstancePort: DefaultSharedInstancePort,
InstanceControlPort: DefaultInstanceControlPort, InstanceControlPort: DefaultInstanceControlPort,
PanicOnInterfaceErr: false, PanicOnInterfaceErr: false,
LogLevel: DefaultLogLevel, 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 { if err != nil {
return "", err return "", err
} }
return filepath.Join(homeDir, ".reticulum", "config"), nil return filepath.Join(homeDir, ".reticulum-go", "config"), nil
} }
func EnsureConfigDir() error { func EnsureConfigDir() error {
@@ -40,65 +43,212 @@ func EnsureConfigDir() error {
return err return err
} }
configDir := filepath.Join(homeDir, ".reticulum") configDir := filepath.Join(homeDir, ".reticulum-go")
return os.MkdirAll(configDir, 0755) return os.MkdirAll(configDir, 0755)
} }
// 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 // LoadConfig loads the configuration from the specified path
func LoadConfig(path string) (*common.ReticulumConfig, error) { func LoadConfig(path string) (*common.ReticulumConfig, error) {
data, err := os.ReadFile(path) file, err := os.Open(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer file.Close()
cfg := DefaultConfig() cfg := DefaultConfig()
if err := toml.Unmarshal(data, cfg); err != nil { cfg.ConfigPath = path
return nil, err
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 return cfg, nil
} }
// SaveConfig saves the configuration to the specified path // SaveConfig saves the configuration to the specified path
func SaveConfig(cfg *common.ReticulumConfig) error { func SaveConfig(cfg *common.ReticulumConfig) error {
data, err := toml.Marshal(cfg) if cfg.ConfigPath == "" {
if err != nil { return fmt.Errorf("config path not set")
return err
} }
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()), 0644)
} }
// CreateDefaultConfig creates a default configuration file // CreateDefaultConfig creates a default configuration file
func CreateDefaultConfig(path string) error { func CreateDefaultConfig(path string) error {
cfg := DefaultConfig() cfg := DefaultConfig()
cfg.ConfigPath = path
// Add default interface // Add Auto Interface
cfg.Interfaces["Default Interface"] = common.InterfaceConfig{ cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{
Type: "AutoInterface", Type: "AutoInterface",
Enabled: false, Enabled: true,
GroupID: "reticulum",
DiscoveryScope: "link",
DiscoveryPort: 29716,
DataPort: 42671,
} }
// Add default quad4net interface // Add default interfaces
cfg.Interfaces["quad4net tcp"] = common.InterfaceConfig{ cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
Type: "TCPClientInterface",
Enabled: true,
TargetHost: "127.0.0.1",
TargetPort: 4242,
Name: "Go-RNS-Testnet",
}
cfg.Interfaces["Quad4 TCP"] = &common.InterfaceConfig{
Type: "TCPClientInterface", Type: "TCPClientInterface",
Enabled: true, Enabled: true,
TargetHost: "rns.quad4.io", TargetHost: "rns.quad4.io",
TargetPort: 4242, TargetPort: 4242,
Name: "Quad4 TCP",
} }
data, err := toml.Marshal(cfg) cfg.Interfaces["Local UDP"] = &common.InterfaceConfig{
if err != nil { Type: "UDPInterface",
return err Enabled: false,
Address: "0.0.0.0",
Port: 37696,
} }
// Create config directory if it doesn't exist
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err return err
} }
return os.WriteFile(path, data, 0644) return SaveConfig(cfg)
} }
// InitConfig initializes the configuration system // InitConfig initializes the configuration system
@@ -118,4 +268,4 @@ func InitConfig() (*common.ReticulumConfig, error) {
// Load config // Load config
return LoadConfig(configPath) return LoadConfig(configPath)
} }

View File

@@ -1,21 +1,47 @@
package announce package announce
import ( import (
"crypto/rand"
"crypto/sha256" "crypto/sha256"
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt"
"log"
"sync" "sync"
"time" "time"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
"github.com/Sudo-Ivan/reticulum-go/pkg/identity" "github.com/Sudo-Ivan/reticulum-go/pkg/identity"
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
) )
const ( const (
PACKET_TYPE_DATA = 0x00
PACKET_TYPE_ANNOUNCE = 0x01
PACKET_TYPE_LINK = 0x02
PACKET_TYPE_PROOF = 0x03
// Announce Types
ANNOUNCE_NONE = 0x00 ANNOUNCE_NONE = 0x00
ANNOUNCE_PATH = 0x01 ANNOUNCE_PATH = 0x01
ANNOUNCE_IDENTITY = 0x02 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 MAX_HOPS = 128
PROPAGATION_RATE = 0.02 // 2% of interface bandwidth PROPAGATION_RATE = 0.02 // 2% of interface bandwidth
RETRY_INTERVAL = 300 // 5 minutes RETRY_INTERVAL = 300 // 5 minutes
@@ -24,27 +50,37 @@ const (
type AnnounceHandler interface { type AnnounceHandler interface {
AspectFilter() []string AspectFilter() []string
ReceivedAnnounce(destinationHash []byte, announcedIdentity *identity.Identity, appData []byte) error ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte) error
ReceivePathResponses() bool ReceivePathResponses() bool
} }
type Announce struct { type Announce struct {
mutex sync.RWMutex mutex *sync.RWMutex
destinationHash []byte destinationHash []byte
identity *identity.Identity identity *identity.Identity
appData []byte appData []byte
hops uint8 config *common.ReticulumConfig
timestamp int64 hops uint8
signature []byte timestamp int64
pathResponse bool signature []byte
retries int pathResponse bool
handlers []AnnounceHandler retries int
handlers []AnnounceHandler
ratchetID []byte
packet []byte
hash []byte
} }
func New(dest *identity.Identity, appData []byte, pathResponse bool) (*Announce, error) { func New(dest *identity.Identity, appData []byte, pathResponse bool, config *common.ReticulumConfig) (*Announce, error) {
if dest == nil {
return nil, errors.New("destination identity required")
}
a := &Announce{ a := &Announce{
mutex: &sync.RWMutex{},
identity: dest, identity: dest,
appData: appData, appData: appData,
config: config,
hops: 0, hops: 0,
timestamp: time.Now().Unix(), timestamp: time.Now().Unix(),
pathResponse: pathResponse, pathResponse: pathResponse,
@@ -52,46 +88,59 @@ func New(dest *identity.Identity, appData []byte, pathResponse bool) (*Announce,
handlers: make([]AnnounceHandler, 0), handlers: make([]AnnounceHandler, 0),
} }
// Generate destination hash // Generate truncated hash from public key
hash := sha256.New() pubKey := dest.GetPublicKey()
hash.Write(dest.GetPublicKey()) hash := sha256.Sum256(pubKey)
a.destinationHash = hash.Sum(nil)[:16] // Truncated hash a.destinationHash = hash[:identity.TRUNCATED_HASHLENGTH/8]
// Sign the announce // Get current ratchet ID if enabled
currentRatchet := dest.GetCurrentRatchetKey()
if currentRatchet != nil {
a.ratchetID = dest.GetRatchetID(currentRatchet)
}
// Sign announce data
signData := append(a.destinationHash, a.appData...) signData := append(a.destinationHash, a.appData...)
if a.ratchetID != nil {
signData = append(signData, a.ratchetID...)
}
a.signature = dest.Sign(signData) a.signature = dest.Sign(signData)
return a, nil return a, nil
} }
func (a *Announce) Propagate(interfaces []transport.Interface) error { func (a *Announce) Propagate(interfaces []common.NetworkInterface) error {
a.mutex.Lock() a.mutex.RLock()
defer a.mutex.Unlock() defer a.mutex.RUnlock()
if a.hops >= MAX_HOPS { log.Printf("[DEBUG-7] Propagating announce across %d interfaces", len(interfaces))
return errors.New("maximum hop count reached")
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 { for _, iface := range interfaces {
if err := iface.SendAnnounce(packet, a.pathResponse); err != nil { if !iface.IsEnabled() {
return err 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 return nil
@@ -118,28 +167,53 @@ func (a *Announce) HandleAnnounce(data []byte) error {
a.mutex.Lock() a.mutex.Lock()
defer a.mutex.Unlock() defer a.mutex.Unlock()
// Validate announce data log.Printf("[DEBUG-7] Handling announce packet of %d bytes", len(data))
if len(data) < 16+32+1 { // Min size: hash + pubkey + hops
return errors.New("invalid announce data") // Minimum packet size validation (header(2) + desthash(16) + enckey(32) + signkey(32) + namehash(10) +
// randomhash(10) + signature(64) + min app data(3))
if len(data) < 169 {
log.Printf("[DEBUG-7] Invalid announce data length: %d bytes", len(data))
return errors.New("invalid announce data length")
} }
// Extract fields // Parse fields
destHash := data[:16] header := data[:2]
pubKey := data[16:48] hopCount := header[1]
hops := data[48] destHash := data[2:18]
appData := data[49 : len(data)-64] encKey := data[18:50]
signature := data[len(data)-64:] signKey := data[50:82]
nameHash := data[82:92]
randomHash := data[92:102]
signature := data[102:166]
appData := data[166:]
log.Printf("[DEBUG-7] Announce fields: destHash=%x, encKey=%x, signKey=%x",
destHash, encKey, signKey)
log.Printf("[DEBUG-7] Name hash=%x, random hash=%x", nameHash, randomHash)
// Validate hop count
if hopCount > MAX_HOPS {
log.Printf("[DEBUG-7] Announce exceeded max hops: %d", hopCount)
return errors.New("announce exceeded maximum hop count")
}
// Create announced identity from public keys
pubKey := append(encKey, signKey...)
announcedIdentity := identity.FromPublicKey(pubKey)
if announcedIdentity == nil {
return errors.New("invalid identity public key")
}
// Verify signature // Verify signature
signData := append(destHash, appData...) signData := append(destHash, appData...)
if !a.identity.Verify(signData, signature) { if !announcedIdentity.Verify(signData, signature) {
return errors.New("invalid announce signature") return errors.New("invalid announce signature")
} }
// Process announce with registered handlers // Process with handlers
for _, handler := range a.handlers { for _, handler := range a.handlers {
if handler.ReceivePathResponses() || !a.pathResponse { if handler.ReceivePathResponses() || !a.pathResponse {
if err := handler.ReceivedAnnounce(destHash, a.identity, appData); err != nil { if err := handler.ReceivedAnnounce(destHash, announcedIdentity, appData); err != nil {
return err return err
} }
} }
@@ -148,7 +222,7 @@ func (a *Announce) HandleAnnounce(data []byte) error {
return nil 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() a.mutex.Lock()
defer a.mutex.Unlock() defer a.mutex.Unlock()
@@ -158,9 +232,203 @@ func (a *Announce) RequestPath(destHash []byte, onInterface transport.Interface)
packet = append(packet, byte(0)) // Initial hop count packet = append(packet, byte(0)) // Initial hop count
// Send path request // Send path request
if err := onInterface.SendPathRequest(packet); err != nil { if err := onInterface.Send(packet, ""); err != nil {
return err return err
} }
return nil 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 {
// Create header
header := CreateHeader(
IFAC_NONE,
HEADER_TYPE_1,
0, // No context flag
PROP_TYPE_BROADCAST,
DEST_TYPE_SINGLE,
PACKET_TYPE_ANNOUNCE,
a.hops,
)
packet := header
// Add destination hash (16 bytes)
packet = append(packet, a.destinationHash...)
// Add public key parts (32 bytes each)
pubKey := a.identity.GetPublicKey()
packet = append(packet, pubKey[:32]...) // Encryption key
packet = append(packet, pubKey[32:]...) // Signing key
// Add name hash (10 bytes)
nameHash := sha256.Sum256([]byte(fmt.Sprintf("%s.%s", a.config.AppName, a.config.AppAspect)))
packet = append(packet, nameHash[:10]...)
// Add random hash (10 bytes)
randomBytes := make([]byte, 10)
rand.Read(randomBytes)
packet = append(packet, randomBytes...)
// Create validation data for signature
validationData := make([]byte, 0)
validationData = append(validationData, a.destinationHash...)
validationData = append(validationData, pubKey[:32]...) // Encryption key
validationData = append(validationData, pubKey[32:]...) // Signing key
validationData = append(validationData, nameHash[:10]...)
validationData = append(validationData, randomBytes...)
validationData = append(validationData, a.appData...)
// Add signature (64 bytes)
signature := a.identity.Sign(validationData)
packet = append(packet, signature...)
// Add app data
if len(a.appData) > 0 {
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)))
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, appData []byte, ratchetID []byte, pathResponse bool, config *common.ReticulumConfig) (*Announce, error) {
log.Printf("[DEBUG-7] Creating new announce: appDataLen=%d, hasRatchet=%v, pathResponse=%v",
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")
}
destHash := identity.Hash()
log.Printf("[DEBUG-7] Generated 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 {
// 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)
}
// Construct packet
packet := make([]byte, 0)
packet = append(packet, PACKET_TYPE_ANNOUNCE)
packet = append(packet, a.destinationHash...)
packet = append(packet, a.identity.GetPublicKey()...)
packet = append(packet, a.hops)
packet = append(packet, a.appData...)
if a.ratchetID != nil {
packet = append(packet, a.ratchetID...)
}
// Add signature
signData := append(a.destinationHash, a.appData...)
if a.ratchetID != nil {
signData = append(signData, a.ratchetID...)
}
signature := a.identity.Sign(signData)
packet = append(packet, signature...)
a.packet = packet
}
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
}

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

@@ -0,0 +1,240 @@
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)
binary.Write(buf, binary.BigEndian, headerVal)
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 {
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),
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)
io.Copy(&compressed, r)
return compressed.Bytes()
}
func decompressData(data []byte) []byte {
reader := bzip2.NewReader(bytes.NewReader(data))
var decompressed bytes.Buffer
io.Copy(&decompressed, reader)
return decompressed.Bytes()
}

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

@@ -0,0 +1,218 @@
package channel
import (
"errors"
"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++
c.link.Resend(packet)
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 package common
import (
"fmt"
)
const (
DEFAULT_SHARED_INSTANCE_PORT = 37428
DEFAULT_INSTANCE_CONTROL_PORT = 37429
DEFAULT_LOG_LEVEL = 20
)
// ConfigProvider interface for accessing configuration // ConfigProvider interface for accessing configuration
type ConfigProvider interface { type ConfigProvider interface {
GetConfigPath() string GetConfigPath() string
GetLogLevel() int GetLogLevel() int
GetInterfaces() map[string]InterfaceConfig GetInterfaces() map[string]InterfaceConfig
} }
// InterfaceConfig represents interface configuration // InterfaceConfig represents interface configuration
type InterfaceConfig struct { type InterfaceConfig struct {
Type string `toml:"type"` Name string
Enabled bool `toml:"enabled"` Type string
TargetHost string `toml:"target_host,omitempty"` Enabled bool
TargetPort int `toml:"target_port,omitempty"` Address string
Interface string `toml:"interface,omitempty"` 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 // ReticulumConfig represents the main configuration structure
type ReticulumConfig struct { type ReticulumConfig struct {
EnableTransport bool `toml:"enable_transport"` ConfigPath string
ShareInstance bool `toml:"share_instance"` EnableTransport bool
SharedInstancePort int `toml:"shared_instance_port"` ShareInstance bool
InstanceControlPort int `toml:"instance_control_port"` SharedInstancePort int
PanicOnInterfaceErr bool `toml:"panic_on_interface_error"` InstanceControlPort int
LogLevel int `toml:"loglevel"` PanicOnInterfaceErr bool
ConfigPath string `toml:"-"` LogLevel int
Interfaces map[string]InterfaceConfig 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",
}
}

View File

@@ -1,28 +1,61 @@
package common package common
const ( const (
// Interface Types // Interface Types
IF_TYPE_UDP InterfaceType = iota IF_TYPE_NONE InterfaceType = iota
IF_TYPE_TCP IF_TYPE_UDP
IF_TYPE_UNIX IF_TYPE_TCP
IF_TYPE_UNIX
IF_TYPE_I2P
IF_TYPE_BLUETOOTH
IF_TYPE_SERIAL
IF_TYPE_AUTO
// Interface Modes // Interface Modes
IF_MODE_FULL InterfaceMode = iota IF_MODE_FULL InterfaceMode = iota
IF_MODE_POINT IF_MODE_POINT
IF_MODE_GATEWAY IF_MODE_GATEWAY
IF_MODE_ACCESS_POINT
IF_MODE_ROAMING
IF_MODE_BOUNDARY
// Transport Modes // Transport Modes
TRANSPORT_MODE_DIRECT TransportMode = iota TRANSPORT_MODE_DIRECT TransportMode = iota
TRANSPORT_MODE_RELAY TRANSPORT_MODE_RELAY
TRANSPORT_MODE_GATEWAY TRANSPORT_MODE_GATEWAY
// Path Status // Path Status
PATH_STATUS_UNKNOWN PathStatus = iota PATH_STATUS_UNKNOWN PathStatus = iota
PATH_STATUS_DIRECT PATH_STATUS_DIRECT
PATH_STATUS_RELAY PATH_STATUS_RELAY
PATH_STATUS_FAILED PATH_STATUS_FAILED
// Common Constants // Resource Status
DEFAULT_MTU = 1500 RESOURCE_STATUS_PENDING = 0x00
MAX_PACKET_SIZE = 65535 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 package common
import ( import (
"net" "encoding/binary"
"sync" "net"
"time" "sync"
"time"
) )
// NetworkInterface combines both low-level and high-level interface requirements // NetworkInterface defines the interface for all network communication methods
type NetworkInterface interface { type NetworkInterface interface {
// Low-level network operations // Core interface operations
Start() error Start() error
Stop() error Stop() error
Send(data []byte, address string) error Enable()
Receive() ([]byte, string, error) Disable()
GetType() InterfaceType Detach()
GetMode() InterfaceMode
GetMTU() int // Network operations
Send(data []byte, address string) error
// High-level packet operations GetConn() net.Conn
ProcessIncoming([]byte) GetMTU() int
ProcessOutgoing([]byte) error GetName() string
SendPathRequest([]byte) error
SendLinkPacket([]byte, []byte, time.Time) error // Interface properties
Detach() GetType() InterfaceType
SetPacketCallback(PacketCallback) GetMode() InterfaceMode
IsEnabled() bool
// Additional required fields IsOnline() bool
GetName() string IsDetached() bool
GetConn() net.Conn GetBandwidthAvailable() bool
IsEnabled() 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 for network interfaces
// BaseInterface provides common implementation
type BaseInterface struct { type BaseInterface struct {
Name string Name string
Mode InterfaceMode Mode InterfaceMode
Type InterfaceType Type InterfaceType
Online bool
Online bool Enabled bool
Detached bool Detached bool
IN bool IN bool
OUT bool OUT bool
MTU int MTU int
Bitrate int64 Bitrate int64
TxBytes uint64 TxBytes uint64
RxBytes uint64 RxBytes uint64
lastTx time.Time
mutex sync.RWMutex
owner interface{} Mutex sync.RWMutex
packetCallback PacketCallback 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()))
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,72 @@ import (
"time" "time"
) )
// Interface related types
type InterfaceMode byte
type InterfaceType byte
// Transport related types // Transport related types
type TransportMode byte type TransportMode byte
type PathStatus byte type PathStatus byte
// Common structs // Path represents routing information for a destination
type Path struct { type Path struct {
Interface NetworkInterface Interface NetworkInterface
Address string LastSeen time.Time
Status PathStatus NextHop []byte
LastSeen time.Time Hops uint8
NextHop []byte LastUpdated time.Time
Hops uint8 HopCount uint8
LastUpdated time.Time
} }
// Common callbacks // Common callbacks
type ProofRequestedCallback func(interface{}) bool type ProofRequestedCallback func([]byte, []byte)
type LinkEstablishedCallback func(interface{}) type LinkEstablishedCallback func(interface{})
type PacketCallback func([]byte, NetworkInterface)
// Request handler // RequestHandler manages path requests and responses
type RequestHandler struct { type RequestHandler struct {
Path string Path string
ResponseGenerator func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity interface{}, requestedAt int64) []byte ResponseGenerator func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity interface{}, requestedAt int64) []byte
AllowMode byte AllowMode byte
AllowedList [][]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 package config
import ( import (
"io/ioutil" "bufio"
"gopkg.in/yaml.v3" "fmt"
"os"
"path/filepath"
"strconv"
"strings"
) )
type Config struct { type Config struct {
Identity struct { Identity struct {
Name string `yaml:"name"` Name string
StoragePath string `yaml:"storage_path"` StoragePath string
} `yaml:"identity"` }
Interfaces []struct { Interfaces []struct {
Name string `yaml:"name"` Name string
Type string `yaml:"type"` Type string
Enabled bool `yaml:"enabled"` Enabled bool
ListenPort int `yaml:"listen_port"` ListenPort int
ListenIP string `yaml:"listen_ip"` ListenIP string
KissFraming bool `yaml:"kiss_framing"` KissFraming bool
I2PTunneled bool `yaml:"i2p_tunneled"` I2PTunneled bool
} `yaml:"interfaces"` }
Transport struct { Transport struct {
AnnounceInterval int `yaml:"announce_interval"` AnnounceInterval int
PathRequestTimeout int `yaml:"path_request_timeout"` PathRequestTimeout int
MaxHops int `yaml:"max_hops"` MaxHops int
BitrateLimit int64 `yaml:"bitrate_limit"` BitrateLimit int64
} `yaml:"transport"` }
Logging struct { Logging struct {
Level string `yaml:"level"` Level string
File string `yaml:"file"` File string
} `yaml:"logging"` }
} }
func LoadConfig(path string) (*Config, error) { func LoadConfig(path string) (*Config, error) {
data, err := ioutil.ReadFile(path) file, err := os.Open(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer file.Close()
var cfg Config cfg := &Config{}
if err := yaml.Unmarshal(data, &cfg); err != nil { scanner := bufio.NewScanner(file)
return nil, err 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()), 0644)
}
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, 0755)
}
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
}

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

@@ -0,0 +1,63 @@
package cryptography
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"errors"
"io"
)
func EncryptAESCBC(key, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// Generate 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
mode := cipher.NewCBCEncrypter(block, iv)
ciphertext := make([]byte, len(padtext))
mode.CryptBlocks(ciphertext, padtext)
return append(iv, ciphertext...), nil
}
func DecryptAESCBC(key, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(ciphertext) < aes.BlockSize {
return nil, errors.New("ciphertext too short")
}
iv := ciphertext[:aes.BlockSize]
ciphertext = ciphertext[aes.BlockSize:]
if len(ciphertext)%aes.BlockSize != 0 {
return nil, errors.New("ciphertext is not a multiple of block size")
}
mode := cipher.NewCBCDecrypter(block, iv)
plaintext := make([]byte, len(ciphertext))
mode.CryptBlocks(plaintext, ciphertext)
// Remove PKCS7 padding
padding := int(plaintext[len(plaintext)-1])
return plaintext[:len(plaintext)-padding], nil
}

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,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)
}

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
}

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

@@ -5,6 +5,7 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"log"
"sync" "sync"
"github.com/Sudo-Ivan/reticulum-go/pkg/common" "github.com/Sudo-Ivan/reticulum-go/pkg/common"
@@ -30,6 +31,15 @@ const (
RATCHET_COUNT = 512 // Default number of retained ratchet keys RATCHET_COUNT = 512 // Default number of retained ratchet keys
RATCHET_INTERVAL = 1800 // Minimum interval between ratchet rotations in seconds RATCHET_INTERVAL = 1800 // Minimum interval between ratchet rotations in seconds
// Debug levels
DEBUG_CRITICAL = 1 // Critical errors
DEBUG_ERROR = 2 // Non-critical errors
DEBUG_INFO = 3 // Important information
DEBUG_VERBOSE = 4 // Detailed information
DEBUG_TRACE = 5 // Very detailed tracing
DEBUG_PACKETS = 6 // Packet-level details
DEBUG_ALL = 7 // Everything
) )
type PacketCallback = common.PacketCallback type PacketCallback = common.PacketCallback
@@ -44,39 +54,41 @@ type RequestHandler struct {
} }
type Destination struct { type Destination struct {
identity *identity.Identity identity *identity.Identity
direction byte direction byte
destType byte destType byte
appName string appName string
aspects []string aspects []string
hash []byte hashValue []byte
acceptsLinks bool acceptsLinks bool
proofStrategy byte proofStrategy byte
packetCallback PacketCallback packetCallback PacketCallback
proofCallback ProofRequestedCallback proofCallback ProofRequestedCallback
linkCallback LinkEstablishedCallback linkCallback LinkEstablishedCallback
ratchetsEnabled bool ratchetsEnabled bool
ratchetPath string ratchetPath string
ratchetCount int ratchetCount int
ratchetInterval int ratchetInterval int
enforceRatchets bool enforceRatchets bool
defaultAppData []byte defaultAppData []byte
mutex sync.RWMutex mutex sync.RWMutex
requestHandlers map[string]*RequestHandler requestHandlers map[string]*RequestHandler
callbacks struct { }
packetReceived common.PacketCallback
proofRequested common.ProofRequestedCallback func debugLog(level int, format string, v ...interface{}) {
linkEstablished common.LinkEstablishedCallback log.Printf("[DEBUG-%d] %s", level, fmt.Sprintf(format, v...))
}
} }
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, aspects ...string) (*Destination, error) {
debugLog(DEBUG_INFO, "Creating new destination: app=%s type=%d direction=%d", appName, destType, direction)
if id == nil { if id == nil {
debugLog(DEBUG_ERROR, "Cannot create destination: identity is nil")
return nil, errors.New("identity cannot be nil") return nil, errors.New("identity cannot be nil")
} }
@@ -94,19 +106,28 @@ func New(id *identity.Identity, direction byte, destType byte, appName string, a
} }
// Generate destination hash // Generate destination hash
d.hash = d.Hash() d.hashValue = d.calculateHash()
debugLog(DEBUG_VERBOSE, "Created destination with hash: %x", d.hashValue)
return d, nil return d, nil
} }
func (d *Destination) Hash() []byte { func (d *Destination) calculateHash() []byte {
debugLog(DEBUG_TRACE, "Calculating hash for destination %s", d.ExpandName())
nameHash := sha256.Sum256([]byte(d.ExpandName())) nameHash := sha256.Sum256([]byte(d.ExpandName()))
identityHash := sha256.Sum256(d.identity.GetPublicKey()) identityHash := sha256.Sum256(d.identity.GetPublicKey())
debugLog(DEBUG_ALL, "Name hash: %x", nameHash)
debugLog(DEBUG_ALL, "Identity hash: %x", identityHash)
combined := append(nameHash[:], identityHash[:]...) combined := append(nameHash[:], identityHash[:]...)
finalHash := sha256.Sum256(combined) finalHash := sha256.Sum256(combined)
return finalHash[:16] // Truncated to 128 bits truncated := finalHash[:16]
debugLog(DEBUG_VERBOSE, "Calculated destination hash: %x", truncated)
return truncated
} }
func (d *Destination) ExpandName() string { func (d *Destination) ExpandName() string {
@@ -121,69 +142,60 @@ func (d *Destination) Announce(appData []byte) error {
d.mutex.Lock() d.mutex.Lock()
defer d.mutex.Unlock() defer d.mutex.Unlock()
log.Printf("[DEBUG-4] Creating announce packet for destination %s", d.ExpandName())
// If no specific appData provided, use default // If no specific appData provided, use default
if appData == nil { if appData == nil {
log.Printf("[DEBUG-4] Using default app data for announce")
appData = d.defaultAppData appData = d.defaultAppData
} }
// Create announce packet // Create announce packet
packet := make([]byte, 0) packet := make([]byte, 0, 256) // Pre-allocate reasonable size
// Add destination hash // Add packet type and header
packet = append(packet, d.hash...) packet = append(packet, 0x01) // PACKET_TYPE_ANNOUNCE
packet = append(packet, 0x00) // Initial hop count
// Add identity public key // Add destination hash (16 bytes)
packet = append(packet, d.identity.GetPublicKey()...) packet = append(packet, d.hashValue...)
log.Printf("[DEBUG-4] Added destination hash %x to announce", d.hashValue[:8])
// Add flags byte // Add identity public key (32 bytes)
flags := byte(0) pubKey := d.identity.GetPublicKey()
if d.acceptsLinks { packet = append(packet, pubKey...)
flags |= 0x01 log.Printf("[DEBUG-4] Added public key %x to announce", pubKey[:8])
}
if d.ratchetsEnabled {
flags |= 0x02
}
packet = append(packet, flags)
// Add proof strategy // Add app data with length prefix
packet = append(packet, d.proofStrategy) appDataLen := make([]byte, 2)
binary.BigEndian.PutUint16(appDataLen, uint16(len(appData)))
// Add app data length and data if present packet = append(packet, appDataLen...)
if appData != nil { packet = append(packet, appData...)
appDataLen := uint16(len(appData)) log.Printf("[DEBUG-4] Added %d bytes of app data to announce", 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 // Add ratchet data if enabled
if d.ratchetsEnabled { if d.ratchetsEnabled {
// Add ratchet interval log.Printf("[DEBUG-4] Adding ratchet data to announce")
intervalBytes := make([]byte, 4)
binary.BigEndian.PutUint32(intervalBytes, uint32(d.ratchetInterval))
packet = append(packet, intervalBytes...)
// Add current ratchet key
ratchetKey := d.identity.GetCurrentRatchetKey() ratchetKey := d.identity.GetCurrentRatchetKey()
if ratchetKey == nil { if ratchetKey == nil {
log.Printf("[DEBUG-3] Failed to get current ratchet key")
return errors.New("failed to get current ratchet key") return errors.New("failed to get current ratchet key")
} }
packet = append(packet, ratchetKey...) packet = append(packet, ratchetKey...)
log.Printf("[DEBUG-4] Added ratchet key %x to announce", ratchetKey[:8])
} }
// Sign the announce packet // Sign the announce packet (64 bytes)
signature, err := d.Sign(packet) signData := append(d.hashValue, appData...)
if err != nil { if d.ratchetsEnabled {
return fmt.Errorf("failed to sign announce packet: %w", err) signData = append(signData, d.identity.GetCurrentRatchetKey()...)
} }
signature := d.identity.Sign(signData)
packet = append(packet, signature...) packet = append(packet, signature...)
log.Printf("[DEBUG-4] Added signature to announce packet (total size: %d bytes)", len(packet))
// Send announce packet through transport layer // Send announce packet through transport
// This will need to be implemented in the transport package log.Printf("[DEBUG-4] Sending announce packet through transport layer")
return transport.SendAnnounce(packet) return transport.SendAnnounce(packet)
} }
@@ -220,7 +232,7 @@ func (d *Destination) SetProofStrategy(strategy byte) {
func (d *Destination) EnableRatchets(path string) bool { func (d *Destination) EnableRatchets(path string) bool {
d.mutex.Lock() d.mutex.Lock()
defer d.mutex.Unlock() defer d.mutex.Unlock()
d.ratchetsEnabled = true d.ratchetsEnabled = true
d.ratchetPath = path d.ratchetPath = path
return true return true
@@ -236,7 +248,7 @@ func (d *Destination) SetRetainedRatchets(count int) bool {
if count < 1 { if count < 1 {
return false return false
} }
d.mutex.Lock() d.mutex.Lock()
defer d.mutex.Unlock() defer d.mutex.Unlock()
d.ratchetCount = count d.ratchetCount = count
@@ -247,7 +259,7 @@ func (d *Destination) SetRatchetInterval(interval int) bool {
if interval < 1 { if interval < 1 {
return false return false
} }
d.mutex.Lock() d.mutex.Lock()
defer d.mutex.Unlock() defer d.mutex.Unlock()
d.ratchetInterval = interval d.ratchetInterval = interval
@@ -275,7 +287,7 @@ func (d *Destination) RegisterRequestHandler(path string, responseGen func(strin
return errors.New("invalid allow mode") 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") return errors.New("allowed list required for ALLOW_LIST mode")
} }
@@ -305,19 +317,32 @@ func (d *Destination) DeregisterRequestHandler(path string) bool {
func (d *Destination) Encrypt(plaintext []byte) ([]byte, error) { func (d *Destination) Encrypt(plaintext []byte) ([]byte, error) {
if d.destType == PLAIN { if d.destType == PLAIN {
log.Printf("[DEBUG-4] Using plaintext transmission for PLAIN destination")
return plaintext, nil return plaintext, nil
} }
if d.identity == nil { if d.identity == nil {
log.Printf("[DEBUG-3] Cannot encrypt: no identity available")
return nil, errors.New("no identity available for encryption") return nil, errors.New("no identity available for encryption")
} }
log.Printf("[DEBUG-4] Encrypting %d bytes for destination type %d", len(plaintext), d.destType)
switch d.destType { switch d.destType {
case SINGLE: case SINGLE:
return d.identity.Encrypt(plaintext, nil) recipientKey := d.identity.GetPublicKey()
log.Printf("[DEBUG-4] Encrypting for single recipient with key %x", recipientKey[:8])
return d.identity.Encrypt(plaintext, recipientKey)
case GROUP: case GROUP:
return d.identity.EncryptSymmetric(plaintext) key := d.identity.GetCurrentRatchetKey()
if key == nil {
log.Printf("[DEBUG-3] Cannot encrypt: no ratchet key available")
return nil, errors.New("no ratchet key available")
}
log.Printf("[DEBUG-4] Encrypting for group with ratchet key %x", key[:8])
return d.identity.EncryptWithHMAC(plaintext, key)
default: default:
log.Printf("[DEBUG-3] Unsupported destination type %d for encryption", d.destType)
return nil, errors.New("unsupported destination type for encryption") return nil, errors.New("unsupported destination type for encryption")
} }
} }
@@ -331,14 +356,15 @@ func (d *Destination) Decrypt(ciphertext []byte) ([]byte, error) {
return nil, errors.New("no identity available for decryption") return nil, errors.New("no identity available for decryption")
} }
switch d.destType { // Create empty ratchet receiver to get latest ratchet ID if available
case SINGLE: ratchetReceiver := &common.RatchetIDReceiver{}
return d.identity.Decrypt(ciphertext, nil)
case GROUP: // Call Decrypt with full parameter list:
return d.identity.DecryptSymmetric(ciphertext) // - ciphertext: the encrypted data
default: // - ratchets: nil since we're not providing specific ratchets
return nil, errors.New("unsupported destination type for decryption") // - 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) { func (d *Destination) Sign(data []byte) ([]byte, error) {
@@ -347,4 +373,33 @@ func (d *Destination) Sign(data []byte) ([]byte, error) {
} }
signature := d.identity.Sign(data) signature := d.identity.Sign(data)
return signature, nil 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

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

@@ -0,0 +1,277 @@
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) {
base := &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,
}
ai := &AutoInterface{
BaseInterface: *base,
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")
}
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 {
n, remoteAddr, err := conn.ReadFromUDP(buf)
if err != nil {
log.Printf("Discovery read error: %v", err)
continue
}
ai.handlePeerAnnounce(remoteAddr, buf[:n], 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, data []byte, 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()
}
if ai.outboundConn != nil {
ai.outboundConn.Close()
}
return nil
}

View File

@@ -1,21 +1,102 @@
package interfaces package interfaces
import ( import (
"encoding/binary"
"fmt" "fmt"
"log"
"net"
"sync" "sync"
"time" "time"
"encoding/binary"
"github.com/Sudo-Ivan/reticulum-go/pkg/common" "github.com/Sudo-Ivan/reticulum-go/pkg/common"
) )
const ( 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
DEBUG_LEVEL = 4 // Default debug level for interface logging
// Debug levels
DEBUG_CRITICAL = 1
DEBUG_ERROR = 2
DEBUG_INFO = 3
DEBUG_VERBOSE = 4
DEBUG_TRACE = 5
DEBUG_PACKETS = 6
DEBUG_ALL = 7
) )
// 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 { 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) { func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {
@@ -24,28 +105,38 @@ func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {
i.packetCallback = callback 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) { func (i *BaseInterface) ProcessIncoming(data []byte) {
i.mutex.Lock()
i.RxBytes += uint64(len(data))
i.mutex.Unlock()
i.mutex.RLock() i.mutex.RLock()
callback := i.packetCallback callback := i.packetCallback
i.mutex.RUnlock() i.mutex.RUnlock()
if callback != nil { if callback != nil {
callback(data, i) callback(data, i)
} }
i.RxBytes += uint64(len(data))
} }
func (i *BaseInterface) ProcessOutgoing(data []byte) error { func (i *BaseInterface) ProcessOutgoing(data []byte) error {
i.TxBytes += uint64(len(data)) if !i.Online || i.Detached {
return nil log.Printf("[DEBUG-1] Interface %s: Cannot process outgoing packet - interface offline or detached", i.Name)
} return fmt.Errorf("interface offline or detached")
}
func (i *BaseInterface) Detach() {
i.mutex.Lock() i.mutex.Lock()
defer i.mutex.Unlock() i.TxBytes += uint64(len(data))
i.Detached = true i.mutex.Unlock()
i.Online = false
log.Printf("[DEBUG-%d] Interface %s: Processed outgoing packet of %d bytes, total TX: %d", DEBUG_LEVEL, i.Name, len(data), i.TxBytes)
return nil
} }
func (i *BaseInterface) SendPathRequest(packet []byte) error { func (i *BaseInterface) SendPathRequest(packet []byte) error {
@@ -53,7 +144,7 @@ func (i *BaseInterface) SendPathRequest(packet []byte) error {
return fmt.Errorf("interface offline or detached") 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, 0x01)
frame = append(frame, packet...) frame = append(frame, packet...)
@@ -68,12 +159,156 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
frame := make([]byte, 0, len(dest)+len(data)+9) frame := make([]byte, 0, len(dest)+len(data)+9)
frame = append(frame, 0x02) frame = append(frame, 0x02)
frame = append(frame, dest...) frame = append(frame, dest...)
ts := make([]byte, 8) ts := make([]byte, 8)
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix()))
frame = append(frame, ts...) frame = append(frame, ts...)
frame = append(frame, data...) frame = append(frame, data...)
return i.ProcessOutgoing(frame) 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
log.Printf("[DEBUG-%d] Interface %s: State changed - Enabled: %v->%v, Online: %v->%v", DEBUG_INFO, i.Name, prevState, i.Enabled, !i.Online, i.Online)
}
func (i *BaseInterface) Disable() {
i.mutex.Lock()
defer i.mutex.Unlock()
i.Enabled = false
i.Online = false
log.Printf("[DEBUG-2] Interface %s: Disabled and offline", 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 {
log.Printf("[DEBUG-%d] Interface %s: Sending %d bytes to %s", DEBUG_LEVEL, i.Name, len(data), address)
err := i.ProcessOutgoing(data)
if err != nil {
log.Printf("[DEBUG-1] Interface %s: Failed to send data: %v", i.Name, 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 {
log.Printf("[DEBUG-%d] Interface %s: Bandwidth available (idle for %.2fs)", DEBUG_VERBOSE, i.Name, timeSinceLastTx.Seconds())
return true
}
bytesPerSec := float64(i.TxBytes) / timeSinceLastTx.Seconds()
currentUsage := bytesPerSec * 8
maxUsage := float64(i.Bitrate) * PROPAGATION_RATE
available := currentUsage < maxUsage
log.Printf("[DEBUG-%d] Interface %s: Bandwidth stats - Current: %.2f bps, Max: %.2f bps, Usage: %.1f%%, Available: %v", DEBUG_VERBOSE, i.Name, currentUsage, maxUsage, (currentUsage/maxUsage)*100, available)
return available
}
func (i *BaseInterface) updateBandwidthStats(bytes uint64) {
i.mutex.Lock()
defer i.mutex.Unlock()
i.TxBytes += bytes
i.lastTx = time.Now()
log.Printf("[DEBUG-%d] Interface %s: Updated bandwidth stats - TX bytes: %d, Last TX: %v", DEBUG_LEVEL, i.Name, i.TxBytes, i.lastTx)
}
type InterceptedInterface struct {
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 {
log.Printf("[DEBUG-2] Failed to intercept outgoing packet: %v", err)
}
}
// Call original send
return i.originalSend(data, addr)
}

View File

@@ -2,9 +2,13 @@ package interfaces
import ( import (
"fmt" "fmt"
"log"
"net" "net"
"runtime"
"sync" "sync"
"time" "time"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
) )
const ( const (
@@ -17,103 +21,102 @@ const (
KISS_TFEND = 0xDC KISS_TFEND = 0xDC
KISS_TFESC = 0xDD KISS_TFESC = 0xDD
TCP_USER_TIMEOUT = 24 TCP_USER_TIMEOUT = 24
TCP_PROBE_AFTER = 5 TCP_PROBE_AFTER = 5
TCP_PROBE_INTERVAL = 2 TCP_PROBE_INTERVAL = 2
TCP_PROBES = 12 TCP_PROBES = 12
RECONNECT_WAIT = 5 RECONNECT_WAIT = 5
INITIAL_TIMEOUT = 5 INITIAL_TIMEOUT = 5
INITIAL_BACKOFF = time.Second
MAX_BACKOFF = time.Minute * 5
) )
type TCPClientInterface struct { type TCPClientInterface struct {
Interface BaseInterface
conn net.Conn conn net.Conn
targetAddr string targetAddr string
targetPort int targetPort int
kissFraming bool kissFraming bool
i2pTunneled bool i2pTunneled bool
initiator bool initiator bool
reconnecting bool reconnecting bool
neverConnected bool neverConnected bool
writing bool writing bool
maxReconnectTries int maxReconnectTries int
packetBuffer []byte packetBuffer []byte
packetType 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{ tc := &TCPClientInterface{
Interface: Interface{ BaseInterface: NewBaseInterface(name, common.IF_TYPE_TCP, enabled),
Name: name, targetAddr: targetHost,
Mode: MODE_FULL, targetPort: targetPort,
MTU: 1064, kissFraming: kissFraming,
Bitrate: 10000000, // 10Mbps estimate i2pTunneled: i2pTunneled,
}, initiator: true,
targetAddr: targetAddr, enabled: enabled,
targetPort: targetPort, maxReconnectTries: TCP_PROBES,
kissFraming: kissFraming, packetBuffer: make([]byte, 0),
i2pTunneled: i2pTunneled, neverConnected: true,
initiator: true,
} }
if err := tc.connect(true); err != nil { if enabled {
go tc.reconnect() addr := fmt.Sprintf("%s:%d", targetHost, targetPort)
} else { conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
tc.conn = conn
tc.Online = true
go tc.readLoop() go tc.readLoop()
} }
return tc, nil return tc, nil
} }
func (tc *TCPClientInterface) connect(initial bool) error { 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 := fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort) addr := fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort)
conn, err := net.DialTimeout("tcp", addr, time.Second*INITIAL_TIMEOUT) conn, err := net.Dial("tcp", addr)
if err != nil { if err != nil {
if initial {
return fmt.Errorf("initial connection failed: %v", err)
}
return err return err
} }
tc.conn = conn tc.conn = conn
tc.Online = true
tc.writing = false
tc.neverConnected = false
// Set TCP options // Set platform-specific timeouts
if tcpConn, ok := conn.(*net.TCPConn); ok { switch runtime.GOOS {
tcpConn.SetNoDelay(true) case "linux":
tcpConn.SetKeepAlive(true) if err := tc.setTimeoutsLinux(); err != nil {
tcpConn.SetKeepAlivePeriod(time.Second * TCP_PROBE_INTERVAL) log.Printf("[DEBUG-2] Failed to set Linux TCP timeouts: %v", err)
} }
case "darwin":
return nil if err := tc.setTimeoutsOSX(); err != nil {
} log.Printf("[DEBUG-2] Failed to set OSX TCP timeouts: %v", err)
func (tc *TCPClientInterface) reconnect() {
if tc.initiator && !tc.reconnecting {
tc.reconnecting = true
attempts := 0
for !tc.Online {
time.Sleep(time.Second * RECONNECT_WAIT)
attempts++
if tc.maxReconnectTries > 0 && attempts > tc.maxReconnectTries {
tc.teardown()
break
}
if err := tc.connect(false); err != nil {
continue
}
go tc.readLoop()
break
} }
tc.reconnecting = false
} }
tc.Online = true
go tc.readLoop()
return nil
} }
func (tc *TCPClientInterface) readLoop() { func (tc *TCPClientInterface) readLoop() {
@@ -134,26 +137,31 @@ func (tc *TCPClientInterface) readLoop() {
return return
} }
// Update RX bytes for raw received data
tc.UpdateStats(uint64(n), true)
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
b := buffer[i] b := buffer[i]
if tc.kissFraming { if tc.kissFraming {
// KISS framing logic // KISS framing logic
if inFrame && b == KISS_FEND { if b == KISS_FEND {
inFrame = false if inFrame && len(dataBuffer) > 0 {
tc.handlePacket(dataBuffer) tc.handlePacket(dataBuffer)
dataBuffer = dataBuffer[:0] dataBuffer = dataBuffer[:0]
} else if b == KISS_FEND { }
inFrame = true inFrame = !inFrame
} else if inFrame { continue
}
if inFrame {
if b == KISS_FESC { if b == KISS_FESC {
escape = true escape = true
} else { } else {
if escape { if escape {
if b == KISS_TFEND { if b == KISS_TFEND {
b = KISS_FEND b = KISS_FEND
} } else if b == KISS_TFESC {
if b == KISS_TFESC {
b = KISS_FESC b = KISS_FESC
} }
escape = false escape = false
@@ -163,13 +171,16 @@ func (tc *TCPClientInterface) readLoop() {
} }
} else { } else {
// HDLC framing logic // HDLC framing logic
if inFrame && b == HDLC_FLAG { if b == HDLC_FLAG {
inFrame = false if inFrame && len(dataBuffer) > 0 {
tc.handlePacket(dataBuffer) tc.handlePacket(dataBuffer)
dataBuffer = dataBuffer[:0] dataBuffer = dataBuffer[:0]
} else if b == HDLC_FLAG { }
inFrame = true inFrame = !inFrame
} else if inFrame { continue
}
if inFrame {
if b == HDLC_ESC { if b == HDLC_ESC {
escape = true escape = true
} else { } else {
@@ -187,20 +198,40 @@ func (tc *TCPClientInterface) readLoop() {
func (tc *TCPClientInterface) handlePacket(data []byte) { func (tc *TCPClientInterface) handlePacket(data []byte) {
if len(data) < 1 { if len(data) < 1 {
log.Printf("[DEBUG-7] Received invalid packet: empty")
return return
} }
packetType := data[0] tc.mutex.Lock()
tc.packetType = data[0]
tc.RxBytes += uint64(len(data))
lastRx := time.Now()
tc.lastRx = lastRx
tc.mutex.Unlock()
log.Printf("[DEBUG-7] Received packet: type=0x%02x, size=%d bytes", tc.packetType, len(data))
payload := data[1:] payload := data[1:]
switch packetType { switch tc.packetType {
case 0x01: // Path request case 0x01: // Announce packet
tc.Interface.ProcessIncoming(payload) log.Printf("[DEBUG-7] Processing announce packet: payload=%d bytes", len(payload))
if len(payload) >= 53 {
tc.BaseInterface.ProcessIncoming(payload)
} else {
log.Printf("[DEBUG-7] Announce packet too small: %d bytes", len(payload))
}
case 0x02: // Link packet case 0x02: // Link packet
if len(payload) < 40 { // minimum size for link packet log.Printf("[DEBUG-7] Processing link packet: payload=%d bytes", len(payload))
if len(payload) < 40 {
log.Printf("[DEBUG-7] Link packet too small: %d bytes", len(payload))
return return
} }
tc.Interface.ProcessIncoming(payload) tc.BaseInterface.ProcessIncoming(payload)
case 0x03: // Announce packet
tc.BaseInterface.ProcessIncoming(payload)
case 0x04: // Transport packet
tc.BaseInterface.ProcessIncoming(payload)
default: default:
// Unknown packet type // Unknown packet type
return return
@@ -224,13 +255,11 @@ func (tc *TCPClientInterface) ProcessOutgoing(data []byte) error {
frame = append(frame, HDLC_FLAG) frame = append(frame, HDLC_FLAG)
} }
if _, err := tc.conn.Write(frame); err != nil { // Update TX stats before sending
tc.teardown() tc.UpdateStats(uint64(len(frame)), false)
return fmt.Errorf("write failed: %v", err)
}
tc.Interface.ProcessOutgoing(data) _, err := tc.conn.Write(frame)
return nil return err
} }
func (tc *TCPClientInterface) teardown() { func (tc *TCPClientInterface) teardown() {
@@ -269,130 +298,237 @@ func escapeKISS(data []byte) []byte {
return escaped return escaped
} }
type TCPServerInterface struct { func (tc *TCPClientInterface) SetPacketCallback(cb common.PacketCallback) {
Interface tc.packetCallback = cb
server net.Listener
bindAddr string
bindPort int
i2pTunneled bool
preferIPv6 bool
spawned []*TCPClientInterface
spawnedMutex sync.RWMutex
} }
func NewTCPServer(name string, bindAddr string, bindPort int, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) { func (tc *TCPClientInterface) IsEnabled() bool {
ts := &TCPServerInterface{ tc.mutex.RLock()
Interface: Interface{ defer tc.mutex.RUnlock()
Name: name, return tc.enabled && tc.Online && !tc.Detached
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 (ts *TCPServerInterface) acceptLoop() { func (tc *TCPClientInterface) GetName() string {
for { return tc.Name
conn, err := ts.server.Accept() }
if err != nil {
if !ts.Detached { func (tc *TCPClientInterface) GetPacketCallback() common.PacketCallback {
// Log error and continue accepting tc.mutex.RLock()
continue 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 := fmt.Sprintf("%s:%d", tc.targetAddr, 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 return
} }
// Create new client interface for this connection // Log reconnection attempt
client := &TCPClientInterface{ fmt.Printf("Failed to reconnect to %s (attempt %d/%d): %v\n",
Interface: Interface{ addr, retries+1, tc.maxReconnectTries, err)
Name: fmt.Sprintf("Client-%s-%s", ts.Name, conn.RemoteAddr()),
Mode: ts.Mode, // Wait with exponential backoff
MTU: ts.MTU, time.Sleep(backoff)
},
conn: conn, // Increase backoff time exponentially
i2pTunneled: ts.i2pTunneled, backoff *= 2
if backoff > maxBackoff {
backoff = maxBackoff
} }
// Configure TCP options retries++
if tcpConn, ok := conn.(*net.TCPConn); ok { }
tcpConn.SetNoDelay(true)
tcpConn.SetKeepAlive(true) tc.mutex.Lock()
tcpConn.SetKeepAlivePeriod(time.Duration(TCP_PROBE_INTERVAL) * time.Second) 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",
fmt.Sprintf("%s:%d", tc.targetAddr, 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 {
info.Control(func(fd uintptr) {
rtt = platformGetRTT(fd)
})
}
} }
return rtt
}
client.Online = true return 0
client.IN = ts.IN }
client.OUT = ts.OUT
// Add to spawned interfaces func (tc *TCPClientInterface) GetTxBytes() uint64 {
ts.spawnedMutex.Lock() tc.mutex.RLock()
ts.spawned = append(ts.spawned, client) defer tc.mutex.RUnlock()
ts.spawnedMutex.Unlock() return tc.TxBytes
}
// Start client read loop func (tc *TCPClientInterface) GetRxBytes() uint64 {
go client.readLoop() tc.mutex.RLock()
defer tc.mutex.RUnlock()
return tc.RxBytes
}
func (tc *TCPClientInterface) UpdateStats(bytes uint64, isRx bool) {
tc.mutex.Lock()
defer tc.mutex.Unlock()
now := time.Now()
if isRx {
tc.RxBytes += bytes
tc.lastRx = now
log.Printf("[DEBUG-5] Interface %s RX stats: bytes=%d total=%d last=%v",
tc.Name, bytes, tc.RxBytes, tc.lastRx)
} else {
tc.TxBytes += bytes
tc.lastTx = now
log.Printf("[DEBUG-5] Interface %s TX stats: bytes=%d total=%d last=%v",
tc.Name, bytes, tc.TxBytes, tc.lastTx)
} }
} }
func (ts *TCPServerInterface) Detach() { func (tc *TCPClientInterface) GetStats() (tx uint64, rx uint64, lastTx time.Time, lastRx time.Time) {
ts.Interface.Detach() tc.mutex.RLock()
defer tc.mutex.RUnlock()
if ts.server != nil { return tc.TxBytes, tc.RxBytes, tc.lastTx, tc.lastRx
ts.server.Close()
}
ts.spawnedMutex.Lock()
for _, client := range ts.spawned {
client.Detach()
}
ts.spawned = nil
ts.spawnedMutex.Unlock()
} }
func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error { func (tc *TCPClientInterface) setTimeoutsLinux() error {
ts.spawnedMutex.RLock() tcpConn, ok := tc.conn.(*net.TCPConn)
defer ts.spawnedMutex.RUnlock() if !ok {
return fmt.Errorf("not a TCP connection")
}
var lastErr error if !tc.i2pTunneled {
for _, client := range ts.spawned { if err := tcpConn.SetKeepAlive(true); err != nil {
if err := client.ProcessOutgoing(data); err != nil { return err
lastErr = 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 { func (ts *TCPServerInterface) String() string {
@@ -401,8 +537,165 @@ func (ts *TCPServerInterface) String() string {
if ts.preferIPv6 { if ts.preferIPv6 {
addr = "[::0]" addr = "[::0]"
} else { } else {
addr = "0.0.0.0" addr = "0.0.0.0"
} }
} }
return fmt.Sprintf("TCPServerInterface[%s/%s:%d]", ts.Name, addr, ts.bindPort) 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 := fmt.Sprintf("%s:%d", ts.bindAddr, 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
}
log.Printf("[DEBUG-2] Error accepting connection: %v", 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()
}()
buffer := make([]byte, ts.MTU)
for {
n, err := conn.Read(buffer)
if err != nil {
return
}
ts.mutex.Lock()
ts.RxBytes += uint64(n)
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 {
log.Printf("[DEBUG-4] Error writing to connection %s: %v",
conn.RemoteAddr(), 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)),
uintptr(unsafe.Pointer(&size)),
0,
)
if err != 0 {
return 0
}
// RTT is in microseconds, convert to Duration
return time.Duration(info.Rtt) * time.Microsecond
}

View File

@@ -2,87 +2,117 @@ package interfaces
import ( import (
"fmt" "fmt"
"log"
"net" "net"
"sync" "sync"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
) )
type UDPInterface struct { type UDPInterface struct {
Interface BaseInterface
conn *net.UDPConn conn *net.UDPConn
listenAddr *net.UDPAddr addr *net.UDPAddr
targetAddr *net.UDPAddr targetAddr *net.UDPAddr
mutex sync.RWMutex
readBuffer []byte readBuffer []byte
} }
func NewUDPInterface(name string, listenAddr string, targetAddr string) (*UDPInterface, error) { func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) {
ui := &UDPInterface{ udpAddr, err := net.ResolveUDPAddr("udp", addr)
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)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid listen address: %v", err) return nil, err
} }
ui.listenAddr = laddr
// Parse target address if provided var targetAddr *net.UDPAddr
if targetAddr != "" { if target != "" {
taddr, err := net.ResolveUDPAddr("udp", targetAddr) targetAddr, err = net.ResolveUDPAddr("udp", target)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid target address: %v", err) return nil, err
} }
ui.targetAddr = taddr
ui.OUT = true
} }
// Create UDP connection ui := &UDPInterface{
conn, err := net.ListenUDP("udp", ui.listenAddr) BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
if err != nil { addr: udpAddr,
return nil, fmt.Errorf("failed to listen on UDP: %v", err) 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 return ui, nil
} }
func (ui *UDPInterface) readLoop() { func (ui *UDPInterface) GetName() string {
for { return ui.Name
if !ui.Online { }
return
}
n, addr, err := ui.conn.ReadFromUDP(ui.readBuffer) func (ui *UDPInterface) GetType() common.InterfaceType {
if err != nil { return ui.Type
if !ui.Detached { }
// Log error
}
continue
}
// Copy received data func (ui *UDPInterface) GetMode() common.InterfaceMode {
data := make([]byte, n) return ui.Mode
copy(data, ui.readBuffer[:n]) }
// Process packet func (ui *UDPInterface) IsOnline() bool {
ui.ProcessIncoming(data) 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()
}
}
func (ui *UDPInterface) Send(data []byte, addr string) error {
if !ui.IsEnabled() {
return fmt.Errorf("interface not enabled")
}
if ui.targetAddr == nil {
return fmt.Errorf("no target address configured")
}
_, err := ui.conn.WriteTo(data, ui.targetAddr)
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 { func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
if !ui.Online || ui.targetAddr == nil { if !ui.IsOnline() {
return fmt.Errorf("interface offline or no target address configured") return fmt.Errorf("interface offline")
}
if ui.targetAddr == nil {
return fmt.Errorf("no target address configured")
} }
_, err := ui.conn.WriteToUDP(data, ui.targetAddr) _, err := ui.conn.WriteToUDP(data, ui.targetAddr)
@@ -90,13 +120,88 @@ func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
return fmt.Errorf("UDP write failed: %v", err) 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 return nil
} }
func (ui *UDPInterface) Detach() { func (ui *UDPInterface) GetConn() net.Conn {
ui.Interface.Detach() return ui.conn
if ui.conn != nil { }
ui.conn.Close()
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
return nil
}
func (ui *UDPInterface) readLoop() {
buffer := make([]byte, ui.MTU)
for {
if ui.IsDetached() {
return
}
n, addr, err := ui.conn.ReadFromUDP(buffer)
if err != nil {
if !ui.IsDetached() {
log.Printf("UDP read error: %v", err)
}
return
}
ui.mutex.Lock()
ui.RxBytes += uint64(n)
ui.mutex.Unlock()
log.Printf("Received %d bytes from %s", n, addr.String())
if callback := ui.GetPacketCallback(); callback != nil {
callback(buffer[:n], ui)
}
}
}
func (ui *UDPInterface) IsEnabled() bool {
ui.mutex.RLock()
defer ui.mutex.RUnlock()
return ui.Enabled && ui.Online && !ui.Detached
}

View File

@@ -5,104 +5,183 @@ import (
"crypto/cipher" "crypto/cipher"
"crypto/ed25519" "crypto/ed25519"
"crypto/rand" "crypto/rand"
"crypto/sha256" "encoding/hex"
"encoding/binary"
"errors" "errors"
"fmt"
"io" "io"
"log"
"sync" "sync"
"time" "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/identity"
"github.com/Sudo-Ivan/reticulum-go/pkg/packet" "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 ( const (
CURVE = "Curve25519" CURVE = "Curve25519"
ESTABLISHMENT_TIMEOUT_PER_HOP = 6 ESTABLISHMENT_TIMEOUT_PER_HOP = 6
KEEPALIVE_TIMEOUT_FACTOR = 4 KEEPALIVE_TIMEOUT_FACTOR = 4
STALE_GRACE = 2 STALE_GRACE = 2
KEEPALIVE = 360 KEEPALIVE = 360
STALE_TIME = 720 STALE_TIME = 720
ACCEPT_NONE = 0x00 ACCEPT_NONE = 0x00
ACCEPT_ALL = 0x01 ACCEPT_ALL = 0x01
ACCEPT_APP = 0x02 ACCEPT_APP = 0x02
STATUS_PENDING = 0x00 STATUS_PENDING = 0x00
STATUS_ACTIVE = 0x01 STATUS_ACTIVE = 0x01
STATUS_CLOSED = 0x02 STATUS_CLOSED = 0x02
STATUS_FAILED = 0x03 STATUS_FAILED = 0x03
PROVE_NONE = 0x00
PROVE_ALL = 0x01
PROVE_APP = 0x02
WATCHDOG_MIN_SLEEP = 0.025
WATCHDOG_INTERVAL = 0.1
) )
type Link struct { type Link struct {
mutex sync.RWMutex mutex sync.RWMutex
destination interface{} destination *destination.Destination
status byte status byte
establishedAt time.Time networkInterface common.NetworkInterface
lastInbound time.Time establishedAt time.Time
lastOutbound time.Time lastInbound time.Time
lastDataReceived time.Time lastOutbound time.Time
lastDataSent time.Time lastDataReceived time.Time
lastDataSent time.Time
remoteIdentity *identity.Identity pathFinder *pathfinder.PathFinder
sessionKey []byte
linkID []byte remoteIdentity *identity.Identity
sessionKey []byte
linkID []byte
rtt float64 rtt float64
establishmentRate float64 establishmentRate float64
trackPhyStats bool
rssi float64
snr float64
q float64
resourceStrategy byte
establishedCallback func(*Link) establishedCallback func(*Link)
closedCallback func(*Link) closedCallback func(*Link)
packetCallback func([]byte, *packet.Packet) packetCallback func([]byte, *packet.Packet)
resourceCallback func(interface{}) bool identifiedCallback func(*Link, *identity.Identity)
resourceStartedCallback func(interface{})
teardownReason byte
hmacKey []byte
transport *transport.Transport
rssi float64
snr float64
q float64
resourceCallback func(interface{}) bool
resourceStartedCallback func(interface{})
resourceConcludedCallback 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 { func NewLink(dest *destination.Destination, transport *transport.Transport, networkIface common.NetworkInterface, establishedCallback func(*Link), closedCallback func(*Link)) *Link {
l := &Link{ return &Link{
destination: dest, destination: dest,
status: STATUS_PENDING, status: STATUS_PENDING,
establishedAt: time.Time{}, transport: transport,
lastInbound: time.Time{}, networkInterface: networkIface,
lastOutbound: time.Time{}, establishedCallback: establishedCallback,
lastDataReceived: time.Time{}, closedCallback: closedCallback,
lastDataSent: time.Time{}, establishedAt: time.Time{}, // Zero time until established
resourceStrategy: ACCEPT_NONE, lastInbound: time.Time{},
establishedCallback: establishedCb, lastOutbound: time.Time{},
closedCallback: closedCb, 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() l.mutex.Lock()
defer l.mutex.Unlock() defer l.mutex.Unlock()
if l.status != STATUS_ACTIVE { if l.status != STATUS_PENDING {
return errors.New("link not active") log.Printf("[DEBUG-3] Cannot establish link: invalid status %d", l.status)
return errors.New("link already established or failed")
} }
// Create identification message destPublicKey := l.destination.GetPublicKey()
idMsg := append(id.GetPublicKey(), id.Sign(l.linkID)...) if destPublicKey == nil {
log.Printf("[DEBUG-3] Cannot establish link: destination has no public key")
// Encrypt and send identification return errors.New("destination has no public key")
err := l.SendPacket(idMsg) }
if err != nil {
log.Printf("[DEBUG-4] Creating link request packet for destination %x", destPublicKey[: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 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 { func (l *Link) HandleIdentification(data []byte) error {
@@ -110,26 +189,33 @@ func (l *Link) HandleIdentification(data []byte) error {
defer l.mutex.Unlock() defer l.mutex.Unlock()
if len(data) < ed25519.PublicKeySize+ed25519.SignatureSize { 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] pubKey := data[:ed25519.PublicKeySize]
signature := data[ed25519.PublicKeySize:] signature := data[ed25519.PublicKeySize:]
remoteIdentity := &identity.Identity{} log.Printf("[DEBUG-4] Processing identification from public key %x", pubKey[:8])
if !remoteIdentity.LoadPublicKey(pubKey) {
return errors.New("invalid remote public key") 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 signData := append(l.linkID, pubKey...)
if !remoteIdentity.Verify(l.linkID, signature) { if !remoteIdentity.Verify(signData, signature) {
return errors.New("invalid identification 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 l.remoteIdentity = remoteIdentity
if l.remoteIdentifiedCallback != nil { if l.identifiedCallback != nil {
l.remoteIdentifiedCallback(l, remoteIdentity) log.Printf("[DEBUG-4] Executing identified callback for remote identity %x", pubKey[:8])
l.identifiedCallback(l, remoteIdentity)
} }
return nil return nil
@@ -158,8 +244,8 @@ func (l *Link) Request(path string, data []byte, timeout time.Duration) (*Reques
receipt := &RequestReceipt{ receipt := &RequestReceipt{
requestID: requestID, requestID: requestID,
status: STATUS_PENDING, status: STATUS_PENDING,
sentAt: time.Now(), sentAt: time.Now(),
} }
// Send request // Send request
@@ -184,12 +270,12 @@ func (l *Link) Request(path string, data []byte, timeout time.Duration) (*Reques
} }
type RequestReceipt struct { type RequestReceipt struct {
mutex sync.RWMutex mutex sync.RWMutex
requestID []byte requestID []byte
status byte status byte
sentAt time.Time sentAt time.Time
receivedAt time.Time receivedAt time.Time
response []byte response []byte
} }
func (r *RequestReceipt) GetRequestID() []byte { func (r *RequestReceipt) GetRequestID() []byte {
@@ -233,21 +319,40 @@ func (l *Link) TrackPhyStats(track bool) {
l.trackPhyStats = track 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 { func (l *Link) GetRSSI() float64 {
l.mutex.RLock() l.mutex.RLock()
defer l.mutex.RUnlock() defer l.mutex.RUnlock()
if !l.trackPhyStats {
return 0
}
return l.rssi return l.rssi
} }
func (l *Link) GetSNR() float64 { func (l *Link) GetSNR() float64 {
l.mutex.RLock() l.mutex.RLock()
defer l.mutex.RUnlock() defer l.mutex.RUnlock()
if !l.trackPhyStats {
return 0
}
return l.snr return l.snr
} }
func (l *Link) GetQ() float64 { func (l *Link) GetQ() float64 {
l.mutex.RLock() l.mutex.RLock()
defer l.mutex.RUnlock() defer l.mutex.RUnlock()
if !l.trackPhyStats {
return 0
}
return l.q return l.q
} }
@@ -319,7 +424,7 @@ func (l *Link) GetRemoteIdentity() *identity.Identity {
func (l *Link) Teardown() { func (l *Link) Teardown() {
l.mutex.Lock() l.mutex.Lock()
defer l.mutex.Unlock() defer l.mutex.Unlock()
if l.status == STATUS_ACTIVE { if l.status == STATUS_ACTIVE {
l.status = STATUS_CLOSED l.status = STATUS_CLOSED
if l.closedCallback != nil { if l.closedCallback != nil {
@@ -361,85 +466,58 @@ func (l *Link) SetResourceConcludedCallback(callback func(interface{})) {
func (l *Link) SetRemoteIdentifiedCallback(callback func(*Link, *identity.Identity)) { func (l *Link) SetRemoteIdentifiedCallback(callback func(*Link, *identity.Identity)) {
l.mutex.Lock() l.mutex.Lock()
defer l.mutex.Unlock() defer l.mutex.Unlock()
l.remoteIdentifiedCallback = callback l.identifiedCallback = callback
} }
func (l *Link) SetResourceStrategy(strategy byte) error { func (l *Link) SetResourceStrategy(strategy byte) error {
if strategy != ACCEPT_NONE && strategy != ACCEPT_ALL && strategy != ACCEPT_APP { if strategy != ACCEPT_NONE && strategy != ACCEPT_ALL && strategy != ACCEPT_APP {
return errors.New("unsupported resource strategy") return errors.New("unsupported resource strategy")
} }
l.mutex.Lock() l.mutex.Lock()
defer l.mutex.Unlock() defer l.mutex.Unlock()
l.resourceStrategy = strategy l.resourceStrategy = strategy
return nil 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 { func (l *Link) SendPacket(data []byte) error {
l.mutex.Lock() l.mutex.Lock()
defer l.mutex.Unlock() defer l.mutex.Unlock()
if l.status != STATUS_ACTIVE { 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") return errors.New("link not active")
} }
// Encrypt data using session key log.Printf("[DEBUG-4] Encrypting packet of %d bytes", len(data))
encryptedData, err := l.encrypt(data) encrypted, err := l.encrypt(data)
if err != nil { if err != nil {
log.Printf("[DEBUG-3] Failed to encrypt packet: %v", err)
return 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.lastOutbound = time.Now()
l.lastDataSent = time.Now() l.lastDataSent = time.Now()
if l.packetCallback != nil { return l.transport.SendPacket(p)
l.packetCallback(encryptedData, nil)
}
return nil
} }
func (l *Link) HandleInbound(data []byte) error { func (l *Link) HandleInbound(data []byte) error {
@@ -447,20 +525,28 @@ func (l *Link) HandleInbound(data []byte) error {
defer l.mutex.Unlock() defer l.mutex.Unlock()
if l.status != STATUS_ACTIVE { 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") return errors.New("link not active")
} }
// Decrypt data using session key // Decode and log packet details
decryptedData, err := l.decrypt(data) l.decodePacket(data)
if err != nil {
return err // 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.lastInbound = time.Now()
l.lastDataReceived = time.Now() l.lastDataReceived = time.Now()
if l.packetCallback != nil { if l.packetCallback != nil {
l.packetCallback(decryptedData, nil) l.packetCallback(data, nil)
} }
return nil return nil
@@ -476,17 +562,27 @@ func (l *Link) encrypt(data []byte) ([]byte, error) {
return nil, err return nil, err
} }
gcm, err := cipher.NewGCM(block) // Generate IV
if err != nil { iv := make([]byte, aes.BlockSize)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err return nil, err
} }
nonce := make([]byte, gcm.NonceSize()) // Add PKCS7 padding
if _, err := io.ReadFull(rand.Reader, nonce); err != nil { padding := aes.BlockSize - len(data)%aes.BlockSize
return nil, err 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)
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) { func (l *Link) decrypt(data []byte) ([]byte, error) {
@@ -499,31 +595,34 @@ func (l *Link) decrypt(data []byte) ([]byte, error) {
return nil, err return nil, err
} }
gcm, err := cipher.NewGCM(block) if len(data) < aes.BlockSize {
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return nil, errors.New("ciphertext too short") return nil, errors.New("ciphertext too short")
} }
nonce, ciphertext := data[:nonceSize], data[nonceSize:] iv := data[:aes.BlockSize]
return gcm.Open(nil, nonce, ciphertext, nil) ciphertext := data[aes.BlockSize:]
}
func (l *Link) UpdatePhyStats(rssi float64, snr float64, q float64) { if len(ciphertext)%aes.BlockSize != 0 {
if !l.trackPhyStats { return nil, errors.New("ciphertext is not a multiple of block size")
return
} }
l.mutex.Lock() mode := cipher.NewCBCDecrypter(block, iv)
defer l.mutex.Unlock() plaintext := make([]byte, len(ciphertext))
mode.CryptBlocks(plaintext, ciphertext)
l.rssi = rssi
l.snr = snr // Remove PKCS7 padding
l.q = q 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 { func (l *Link) GetRTT() float64 {
@@ -546,4 +645,240 @@ func (l *Link) GetStatus() byte {
func (l *Link) IsActive() bool { func (l *Link) IsActive() bool {
return l.GetStatus() == STATUS_ACTIVE 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 {
l.SendPacket([]byte{}) // Keepalive packet
}
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 ( const (
// MTU constants // MTU constants
EncryptedMDU = 383 // Maximum size of payload data in encrypted packet EncryptedMDU = 383 // Maximum size of payload data in encrypted packet
PlainMDU = 464 // Maximum size of payload data in unencrypted 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
// Propagation Types // Propagation Types
PropagationBroadcast = 0 PropagationBroadcast = 0
@@ -19,9 +15,7 @@ const (
DestinationPlain = 2 DestinationPlain = 2
DestinationLink = 3 DestinationLink = 3
// Packet Types // Minimum packet sizes
PacketData = 0 MinAnnounceSize = 169 // header(2) + desthash(16) + enckey(32) + signkey(32) +
PacketAnnounce = 1 // namehash(10) + randomhash(10) + signature(64) + min appdata(3)
PacketLinkRequest = 2 )
PacketProof = 3
)

View File

@@ -1,171 +1,250 @@
package packet package packet
import ( import (
"encoding/binary" "crypto/sha256"
"errors" "errors"
"fmt"
"log"
"time"
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
) )
const ( const (
HeaderSize = 2 // Packet Types
AddressSize = 16 PacketTypeData = 0x00
ContextSize = 1 PacketTypeAnnounce = 0x01
MaxDataSize = 465 // Maximum size of payload data PacketTypeLinkReq = 0x02
) PacketTypeProof = 0x03
// Header flags and types // Header Types
const ( HeaderType1 = 0x00
// First byte flags HeaderType2 = 0x01
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)
// Second byte // Context Types
HopsField = 0xFF // Number of hops (entire byte) 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 { type Packet struct {
Header [2]byte HeaderType byte
Addresses []byte // Either 16 or 32 bytes depending on header type PacketType byte
Context byte TransportType byte
Data []byte Context byte
AccessCode []byte // Optional: Only present if IFAC flag is set 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 { func NewPacket(destType byte, data []byte, packetType byte, context byte,
p := &Packet{ transportType byte, headerType byte, transportID []byte, createReceipt bool,
Header: [2]byte{0, hops}, contextFlag byte) *Packet {
Addresses: make([]byte, 0),
Data: make([]byte, 0), 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 log.Printf("[DEBUG-6] Packing packet: type=%d, header=%d", p.PacketType, p.HeaderType)
if headerType == HeaderType2 {
p.Header[0] |= HeaderTypeFlag // Create header byte
p.Addresses = make([]byte, 2*AddressSize) // Two address fields flags := byte(p.HeaderType<<6) | byte(p.ContextFlag<<5) |
byte(p.TransportType<<4) | byte(p.DestinationType<<2) | byte(p.PacketType)
header := []byte{flags, p.Hops}
log.Printf("[DEBUG-5] Created packet header: flags=%08b, hops=%d", flags, p.Hops)
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.DestinationHash...)
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 {
if len(p.Raw) < 2*dstLen+3 {
return errors.New("packet too short for header type 2")
}
p.TransportID = p.Raw[2 : dstLen+2]
p.DestinationHash = p.Raw[dstLen+2 : 2*dstLen+2]
p.Context = p.Raw[2*dstLen+2]
p.Data = p.Raw[2*dstLen+3:]
} else { } else {
p.Addresses = make([]byte, AddressSize) // One address field 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.Packed = false
p.Header[0] |= (propagationType << 3) & PropagationFlags p.updateHash()
// 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
return nil return nil
} }
func (p *Packet) SetAddress(index int, address []byte) error { func (p *Packet) GetHash() []byte {
if len(address) != AddressSize { hashable := p.getHashablePart()
return errors.New("invalid address size") hash := sha256.Sum256(hashable)
return hash[:]
}
func (p *Packet) getHashablePart() []byte {
hashable := []byte{p.Raw[0] & 0b00001111}
if p.HeaderType == HeaderType2 {
hashable = append(hashable, p.Raw[18:]...)
} else {
hashable = append(hashable, p.Raw[2:]...)
} }
return hashable
offset := index * AddressSize }
if offset+AddressSize > len(p.Addresses) {
return errors.New("address index out of range") func (p *Packet) updateHash() {
} p.PacketHash = p.GetHash()
copy(p.Addresses[offset:], address)
return nil
} }
func (p *Packet) Serialize() ([]byte, error) { func (p *Packet) Serialize() ([]byte, error) {
totalSize := HeaderSize + len(p.Addresses) + ContextSize + len(p.Data) if !p.Packed {
if p.AccessCode != nil { if err := p.Pack(); err != nil {
totalSize += len(p.AccessCode) return nil, fmt.Errorf("failed to pack packet: %w", err)
}
} }
buffer := make([]byte, totalSize) p.Addresses = p.DestinationHash
offset := 0
// Write header return p.Raw, nil
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
} }
func ParsePacket(data []byte) (*Packet, error) { func NewAnnouncePacket(destHash []byte, identity *identity.Identity, appData []byte, transportID []byte) (*Packet, error) {
if len(data) < HeaderSize { log.Printf("[DEBUG-7] Creating new announce packet: destHash=%x, appData=%s", destHash, string(appData))
return nil, errors.New("packet data too short")
} // Create combined public key
pubKey := identity.GetPublicKey()
log.Printf("[DEBUG-6] Using public key: %x", pubKey)
// Create signed data
signedData := append(destHash, pubKey...)
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 data
data := append(pubKey, appData...)
data = append(data, signature...)
log.Printf("[DEBUG-5] Combined packet data (%d bytes)", len(data))
p := &Packet{ p := &Packet{
Header: [2]byte{data[0], data[1]}, HeaderType: HeaderType2,
PacketType: PacketTypeAnnounce,
TransportID: transportID,
DestinationHash: destHash,
Data: data,
} }
offset := HeaderSize log.Printf("[DEBUG-4] Created announce packet: type=%d, header=%d", p.PacketType, p.HeaderType)
// 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:])
return p, nil return p, nil
} }

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

52
revive.toml Normal file
View File

@@ -0,0 +1,52 @@
ignoreGeneratedHeader = false
severity = "warning"
confidence = 0.8
errorCode = 1
warningCode = 0
[rule.cyclomatic]
arguments = [10]
[rule.cognitive-complexity]
arguments = [7]
[rule.function-result-limit]
arguments = [3]
[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.package-comments]
[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.unused-parameter]
[rule.unused-receiver]
[rule.var-declaration]
[rule.var-naming]

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"