Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae9a35e3bb | ||
|
|
32d32380d8 | ||
|
|
5e40f0bfe8 | ||
| 315b35fc81 | |||
| 54dec6aa89 | |||
| 92c8faec11 | |||
| 2aff4989e5 | |||
| f1d2a31be6 | |||
| f604d1a3c8 | |||
| 26a54436f7 | |||
| 2fd85a1034 | |||
| c8e81cd9f0 | |||
| 2f61ce9bf3 | |||
| b647e7c6c2 | |||
| 6b3990d399 | |||
| 041b439a66 | |||
| 534982b99d | |||
| 7379d07aba | |||
| 03345bc256 | |||
| e486923e8f | |||
| d7f41b785f | |||
| 15303a21dc | |||
| 4d4863aeeb | |||
| 76a4103a56 | |||
| 96348ce349 | |||
|
|
322711ba20 | ||
|
|
772248b31f | ||
|
|
fa1c80169e | ||
|
|
cb1e4a1115 | ||
|
|
836e97b17d | ||
|
|
87d3b4a58b | ||
|
|
77729e07e1 | ||
|
|
79e1caa815 | ||
|
|
a5b905bbaf | ||
|
|
c870406244 | ||
|
|
ea8daf6bb2 | ||
|
|
d79406e354 | ||
|
|
f9b8d29780 | ||
|
|
0cebfb2193 | ||
|
|
9e229287e8 | ||
|
|
9508e6e195 | ||
|
|
5acbef454f | ||
|
|
0862830431 | ||
|
|
6cdc02346f | ||
|
|
3ffd5b72a1 | ||
|
|
73af84e24f | ||
|
|
ae40d2879c | ||
|
|
a2499e4a15 | ||
|
|
30ea1dd0c7 | ||
|
|
785bc7d782 | ||
|
|
144f5bea6a | ||
|
|
a3c701e205 | ||
|
|
a8a7607eb6 | ||
|
|
a2947a3adb | ||
|
|
2cb37102fb | ||
|
|
54c401e2a5 | ||
|
|
8df4039b18 | ||
|
|
12156adae9 | ||
|
|
a34e3d274e | ||
|
|
f3d22dfcd4 | ||
|
|
99d8e44182 | ||
|
|
083991c997 | ||
|
|
9ca24d96ab | ||
|
|
b478ca346e | ||
|
|
20b532e005 | ||
|
|
80eac50632 | ||
|
|
f15d8f6a84 | ||
|
|
c523d6f542 | ||
|
|
8a175e3051 | ||
|
|
28d46921d3 | ||
|
|
613ceddb0b | ||
|
|
599dd91979 | ||
|
|
e724886578 | ||
|
|
3034c0b0b4 | ||
|
|
3ed2c67742 | ||
|
|
f2c146b7c5 | ||
|
|
59cef5e56a | ||
|
|
ef613cc873 | ||
|
|
7a7ce84778 | ||
|
|
7ef7e60a87 | ||
|
|
73349d4a28 | ||
|
|
31128a6758 | ||
|
|
566ce5da96 | ||
|
|
139926be05 | ||
|
|
decbd8f29a | ||
|
|
0f5f5cbb13 | ||
|
|
a2476c9551 | ||
|
|
bfc75a2290 | ||
|
|
2e01fa565d |
7
.deepsource.toml
Normal file
7
.deepsource.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
version = 1
|
||||
|
||||
[[analyzers]]
|
||||
name = "go"
|
||||
|
||||
[analyzers.meta]
|
||||
import_root = "github.com/Sudo-Ivan/Reticulum-Go"
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,8 +1,7 @@
|
||||
reticulum-client
|
||||
reticulum-server
|
||||
|
||||
bin/
|
||||
logs/
|
||||
*.log
|
||||
|
||||
.env
|
||||
.json
|
||||
|
||||
bin/
|
||||
|
||||
21
CONTRIBUTING.md
Normal file
21
CONTRIBUTING.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Contributing
|
||||
|
||||
Be good to each other.
|
||||
|
||||
## Development
|
||||
|
||||
By contributing to this project you agree to the following:
|
||||
|
||||
- All code must be tested using `gosec`.
|
||||
- All code must be formatted with `gofmt`.
|
||||
- All code must be documented.
|
||||
|
||||
## Communication
|
||||
|
||||
Feel free to join our seperate matrix channel for this implementation.
|
||||
|
||||
- [Matrix](https://matrix.to/#/#reticulum-go-dev:matrix.org)
|
||||
|
||||
## Usage of LLMs and other Generative AI tools
|
||||
|
||||
We would prefer if you did not use LLMs and other generative AI tools to write critical parts of the code.
|
||||
9
LICENSE
Normal file
9
LICENSE
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
Copyright 2024-2025 Sudo-Ivan / Quad4.io
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
106
Makefile
Normal file
106
Makefile
Normal file
@@ -0,0 +1,106 @@
|
||||
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-arm:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-arm $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-arm64 $(MAIN_PACKAGE)
|
||||
|
||||
build-riscv:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-riscv64 $(MAIN_PACKAGE)
|
||||
|
||||
build-all: build-linux build-windows build-darwin build-freebsd build-openbsd build-netbsd build-arm build-riscv
|
||||
|
||||
run:
|
||||
@./$(BUILD_DIR)/$(BINARY_NAME)
|
||||
|
||||
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-arm - Build for ARM architectures (arm, arm64)"
|
||||
@echo " build-riscv - Build for RISC-V architecture (riscv64)"
|
||||
@echo " build-all - Build for all platforms and architectures"
|
||||
@echo " run - Run reticulum binary"
|
||||
@echo " install - Install dependencies"
|
||||
26
README.md
26
README.md
@@ -1,4 +1,28 @@
|
||||
# Reticulum-Go
|
||||
|
||||
Reticulum Network Stack in Go.
|
||||
> [!WARNING]
|
||||
> This project is still work in progress. Currently not compatible with the Python version.
|
||||
|
||||
[Reticulum Network](https://github.com/markqvist/Reticulum) implementation in Go `1.24+`.
|
||||
|
||||
Aiming to be fully compatible with the Python version.
|
||||
|
||||
# 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` `v0.37.0` - Cryptographic primitives
|
||||
|
||||
51
SECURITY.md
Normal file
51
SECURITY.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Security Policy
|
||||
|
||||
We use [Socket](https://socket.dev/), [Deepsource](https://deepsource.com/) and [gosec](https://github.com/securego/gosec) for this project.
|
||||
|
||||
## Strict Verfication of Contributors and Code Quality
|
||||
|
||||
We are strict about the quality of the code and the contributors. Please read the [CONTRIBUTING.md](CONTRIBUTING.md) file for more information.
|
||||
|
||||
## 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/*`
|
||||
167
TODO.md
Normal file
167
TODO.md
Normal file
@@ -0,0 +1,167 @@
|
||||
### Core Components (In Progress)
|
||||
|
||||
Last Updated: 2025-04-18
|
||||
|
||||
- [x] Basic Configuration System
|
||||
- [x] Basic config structure
|
||||
- [x] Default settings
|
||||
- [x] Config file loading/saving
|
||||
- [x] Path management
|
||||
|
||||
- [x] Constants Definition (Testing required)
|
||||
- [x] Packet constants
|
||||
- [x] MTU constants
|
||||
- [x] Header types
|
||||
- [x] Additional protocol constants
|
||||
|
||||
- [x] Identity Management (Testing required)
|
||||
- [x] Identity creation
|
||||
- [x] Key pair generation
|
||||
- [x] Identity storage/recall
|
||||
- [x] Public key handling
|
||||
- [x] Signature verification
|
||||
- [x] Hash functions
|
||||
|
||||
- [x] Cryptographic Primitives (Testing required)
|
||||
- [x] Ed25519
|
||||
- [x] Curve25519
|
||||
- [x] AES-128-CBC
|
||||
- [ ] AES-256-CBC
|
||||
- [x] SHA-256
|
||||
- [x] HKDF
|
||||
- [x] Secure random number generation
|
||||
- [x] HMAC
|
||||
|
||||
- [x] Packet Handling (In Progress)
|
||||
- [x] Packet creation
|
||||
- [x] Packet validation
|
||||
- [x] Basic proof system
|
||||
- [x] Packet encryption/decryption
|
||||
- [x] Signature verification
|
||||
- [x] Announce packet structure
|
||||
- [ ] Testing of packet encrypt/decrypt/sign/proof
|
||||
- [ ] Cross-client packet compatibility
|
||||
|
||||
- [x] Transport Layer (In Progress)
|
||||
- [x] Path management
|
||||
- [x] Basic packet routing
|
||||
- [x] Announce handling
|
||||
- [x] Link management
|
||||
- [x] Resource cleanup
|
||||
- [x] Network layer integration
|
||||
- [x] Basic announce implementation
|
||||
- [ ] Testing announce from go client to python client
|
||||
- [ ] Testing path finding and caching
|
||||
- [ ] Announce propagation optimization
|
||||
|
||||
- [x] Channel System (Testing Required)
|
||||
- [x] Channel creation and management
|
||||
- [x] Message handling
|
||||
- [x] Channel encryption
|
||||
- [x] Channel authentication
|
||||
- [x] Channel callbacks
|
||||
- [x] Integration with Buffer system
|
||||
- [ ] Testing with real network conditions
|
||||
- [ ] Cross-client compatibility testing
|
||||
|
||||
- [x] Buffer System (Testing Required)
|
||||
- [x] Raw channel reader/writer
|
||||
- [x] Buffered stream implementation
|
||||
- [x] Compression support
|
||||
- [ ] Testing with Channel system
|
||||
- [ ] Cross-client compatibility testing
|
||||
|
||||
- [x] Resolver System (Testing Required)
|
||||
- [x] Name resolution
|
||||
- [x] Cache management
|
||||
- [x] Announce handling
|
||||
- [x] Path resolution
|
||||
- [x] Integration with Transport layer
|
||||
- [ ] Testing with live network
|
||||
- [ ] Cross-client compatibility testing
|
||||
|
||||
### Interface Implementation (In Progress)
|
||||
- [x] UDP Interface
|
||||
- [x] TCP Interface
|
||||
- [x] Auto Interface
|
||||
- [ ] Local Interface (In Progress)
|
||||
- [ ] I2P Interface
|
||||
- [ ] Pipe Interface
|
||||
- [ ] RNode Interface
|
||||
- [ ] RNode Multiinterface
|
||||
- [ ] Serial Interface
|
||||
- [ ] AX25KISS Interface
|
||||
- [ ] Interface Discovery
|
||||
- [ ] Interface Modes
|
||||
- [ ] Full mode
|
||||
- [ ] Gateway mode
|
||||
- [ ] Access point mode
|
||||
- [ ] Roaming mode
|
||||
- [ ] Boundary mode
|
||||
|
||||
- [ ] Hot reloading interfaces
|
||||
|
||||
### Destination System (Testing required)
|
||||
- [x] Destination creation
|
||||
- [x] Destination types (IN/OUT)
|
||||
- [x] Destination aspects
|
||||
- [ ] Announce implementation (Fixing)
|
||||
- [x] Ratchet support
|
||||
- [x] Request handlers
|
||||
|
||||
### Link System (Testing required)
|
||||
- [x] Link establishment
|
||||
- [x] Link teardown
|
||||
- [x] Basic packet transfer
|
||||
- [x] Encryption/Decryption
|
||||
- [x] Identity verification
|
||||
- [x] Request/Response handling
|
||||
- [x] Session key management
|
||||
- [x] Link state tracking
|
||||
|
||||
### Resource System (Testing required)
|
||||
- [x] Resource creation
|
||||
- [x] Resource transfer
|
||||
- [x] Compression
|
||||
- [x] Progress tracking
|
||||
- [x] Segmentation
|
||||
- [x] Cleanup routines
|
||||
|
||||
### Compatibility
|
||||
- [ ] RNS Utilities.
|
||||
- [ ] Reticulum config.
|
||||
|
||||
|
||||
### Testing & Validation (Priority)
|
||||
- [ ] Unit tests for all components
|
||||
- [ ] Identity tests
|
||||
- [ ] Packet tests
|
||||
- [ ] Transport tests
|
||||
- [ ] Interface tests
|
||||
- [ ] Announce tests
|
||||
- [ ] Channel tests
|
||||
- [ ] Buffer tests
|
||||
- [ ] Resolver tests
|
||||
- [ ] Link tests
|
||||
- [ ] Resource tests
|
||||
- [ ] Integration tests
|
||||
- [ ] Go client to Go client
|
||||
- [ ] Go client to Python client
|
||||
- [ ] Interface compatibility
|
||||
- [ ] Path finding and resolution
|
||||
- [ ] Channel system end-to-end
|
||||
- [ ] Buffer system performance
|
||||
- [ ] Cross-client compatibility tests
|
||||
- [ ] Performance 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
|
||||
102
To-Do
102
To-Do
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
725
cmd/reticulum-go/main.go
Normal file
725
cmd/reticulum-go/main.go
Normal file
@@ -0,0 +1,725 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"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" // Always use "node" for node announces
|
||||
)
|
||||
|
||||
type Reticulum struct {
|
||||
config *common.ReticulumConfig
|
||||
transport *transport.Transport
|
||||
interfaces []interfaces.Interface
|
||||
channels map[string]*channel.Channel
|
||||
buffers map[string]*buffer.Buffer
|
||||
pathRequests map[string]*common.PathRequest
|
||||
announceHistory map[string]announceRecord
|
||||
announceHistoryMu sync.RWMutex
|
||||
identity *identity.Identity
|
||||
destination *destination.Destination
|
||||
|
||||
// Node-specific information
|
||||
maxTransferSize int16 // Max transfer size in KB
|
||||
nodeEnabled bool // Whether this node is enabled
|
||||
nodeTimestamp int64 // Last node announcement timestamp
|
||||
}
|
||||
|
||||
type announceRecord struct {
|
||||
timestamp int64
|
||||
appData []byte
|
||||
}
|
||||
|
||||
func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
|
||||
if cfg == nil {
|
||||
cfg = config.DefaultConfig()
|
||||
}
|
||||
|
||||
// Set default app name and aspect if not provided
|
||||
if cfg.AppName == "" {
|
||||
cfg.AppName = APP_NAME
|
||||
}
|
||||
if cfg.AppAspect == "" {
|
||||
cfg.AppAspect = APP_ASPECT // Always use "node" for node announcements
|
||||
}
|
||||
|
||||
if err := initializeDirectories(); err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize directories: %v", err)
|
||||
}
|
||||
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,
|
||||
"reticulum",
|
||||
"node",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create destination: %v", err)
|
||||
}
|
||||
debugLog(DEBUG_INFO, "Created destination with hash: %x", dest.GetHash())
|
||||
|
||||
// Set node metadata
|
||||
nodeTimestamp := time.Now().Unix()
|
||||
|
||||
r := &Reticulum{
|
||||
config: cfg,
|
||||
transport: t,
|
||||
interfaces: make([]interfaces.Interface, 0),
|
||||
channels: make(map[string]*channel.Channel),
|
||||
buffers: make(map[string]*buffer.Buffer),
|
||||
pathRequests: make(map[string]*common.PathRequest),
|
||||
announceHistory: make(map[string]announceRecord),
|
||||
identity: identity,
|
||||
destination: dest,
|
||||
|
||||
// Node-specific information
|
||||
maxTransferSize: 500, // Default 500KB
|
||||
nodeEnabled: true, // Enabled by default
|
||||
nodeTimestamp: nodeTimestamp,
|
||||
}
|
||||
|
||||
// Enable destination features
|
||||
dest.AcceptsLinks(true)
|
||||
dest.EnableRatchets("") // Empty string for default path
|
||||
dest.SetProofStrategy(destination.PROVE_APP)
|
||||
debugLog(DEBUG_VERBOSE, "Configured destination features")
|
||||
|
||||
// Initialize interfaces from config
|
||||
for name, ifaceConfig := range cfg.Interfaces {
|
||||
if !ifaceConfig.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
var iface interfaces.Interface
|
||||
var err error
|
||||
|
||||
switch ifaceConfig.Type {
|
||||
case "TCPClientInterface":
|
||||
iface, err = interfaces.NewTCPClientInterface(
|
||||
name,
|
||||
ifaceConfig.TargetHost,
|
||||
ifaceConfig.TargetPort,
|
||||
ifaceConfig.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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Start periodic announces
|
||||
go func() {
|
||||
ticker := time.NewTicker(5 * time.Minute) // Adjust interval as needed
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
debugLog(3, "Starting periodic announce cycle")
|
||||
|
||||
// Create a new announce packet for this cycle
|
||||
periodicAnnounce, err := announce.NewAnnounce(
|
||||
r.identity,
|
||||
r.createNodeAppData(),
|
||||
nil, // No ratchet ID for now
|
||||
false,
|
||||
r.config,
|
||||
)
|
||||
if err != nil {
|
||||
debugLog(1, "Failed to create periodic announce: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Propagate announce to all online interfaces
|
||||
var onlineInterfaces []common.NetworkInterface
|
||||
for _, iface := range r.interfaces {
|
||||
if netIface, ok := iface.(common.NetworkInterface); ok {
|
||||
if netIface.IsEnabled() && netIface.IsOnline() {
|
||||
onlineInterfaces = append(onlineInterfaces, netIface)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(onlineInterfaces) > 0 {
|
||||
debugLog(2, "Sending periodic announce on %d interfaces", len(onlineInterfaces))
|
||||
if err := periodicAnnounce.Propagate(onlineInterfaces); err != nil {
|
||||
debugLog(1, "Failed to propagate periodic announce: %v", err)
|
||||
}
|
||||
} else {
|
||||
debugLog(3, "No online interfaces for periodic announce")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
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,
|
||||
r.createNodeAppData(),
|
||||
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,
|
||||
r.createNodeAppData(),
|
||||
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)
|
||||
|
||||
var isNode bool
|
||||
var nodeEnabled bool
|
||||
var nodeTimestamp int64
|
||||
var nodeMaxSize int16
|
||||
|
||||
// Parse msgpack array
|
||||
if len(appData) > 0 {
|
||||
if appData[0] == 0x92 {
|
||||
// Format [name, ticket] for standard peers
|
||||
debugLog(DEBUG_VERBOSE, "Received standard peer announce")
|
||||
isNode = false
|
||||
var pos = 1
|
||||
|
||||
// Parse first element (NameBytes)
|
||||
if pos+1 < len(appData) && appData[pos] == 0xc4 {
|
||||
nameLen := int(appData[pos+1])
|
||||
if pos+2+nameLen <= len(appData) {
|
||||
nameBytes := appData[pos+2 : pos+2+nameLen]
|
||||
name := string(nameBytes)
|
||||
pos += 2 + nameLen
|
||||
debugLog(DEBUG_VERBOSE, "Peer name: %s (bytes: %x)", name, nameBytes)
|
||||
|
||||
// Parse second element (TicketValue)
|
||||
if pos < len(appData) {
|
||||
ticketValue := appData[pos] // Assuming fixint for now
|
||||
debugLog(DEBUG_VERBOSE, "Peer ticket value: %d", ticketValue)
|
||||
} else {
|
||||
debugLog(DEBUG_ERROR, "Could not parse ticket value from announce appData")
|
||||
}
|
||||
} else {
|
||||
debugLog(DEBUG_ERROR, "Could not parse name bytes from announce appData")
|
||||
}
|
||||
} else {
|
||||
debugLog(DEBUG_ERROR, "Announce appData name is not in expected bin 8 format")
|
||||
}
|
||||
} else if appData[0] == 0x93 {
|
||||
// Format [enable, timestamp, maxsize] for nodes
|
||||
debugLog(DEBUG_VERBOSE, "Received node announce")
|
||||
isNode = true
|
||||
var pos = 1
|
||||
|
||||
// Parse first element (Boolean enable/disable)
|
||||
if pos < len(appData) {
|
||||
if appData[pos] == 0xc3 {
|
||||
nodeEnabled = true
|
||||
} else if appData[pos] == 0xc2 {
|
||||
nodeEnabled = false
|
||||
} else {
|
||||
debugLog(DEBUG_ERROR, "Unexpected format for node enabled status: %x", appData[pos])
|
||||
}
|
||||
pos++
|
||||
debugLog(DEBUG_VERBOSE, "Node enabled: %v", nodeEnabled)
|
||||
|
||||
// Parse second element (Int32 timestamp)
|
||||
if pos+4 < len(appData) && appData[pos] == 0xd2 {
|
||||
pos++
|
||||
timestamp := binary.BigEndian.Uint32(appData[pos : pos+4])
|
||||
nodeTimestamp = int64(timestamp)
|
||||
pos += 4
|
||||
debugLog(DEBUG_VERBOSE, "Node timestamp: %d (%s)", timestamp, time.Unix(nodeTimestamp, 0))
|
||||
|
||||
// Parse third element (Int16 max transfer size)
|
||||
if pos+2 < len(appData) && appData[pos] == 0xd1 {
|
||||
pos++
|
||||
maxSize := binary.BigEndian.Uint16(appData[pos : pos+2])
|
||||
nodeMaxSize = int16(maxSize)
|
||||
debugLog(DEBUG_VERBOSE, "Node max transfer size: %d KB", nodeMaxSize)
|
||||
} else {
|
||||
debugLog(DEBUG_ERROR, "Could not parse max transfer size from node announce")
|
||||
}
|
||||
} else {
|
||||
debugLog(DEBUG_ERROR, "Could not parse timestamp from node announce")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debugLog(DEBUG_VERBOSE, "Unknown announce data format: %x", appData)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a better record with more info
|
||||
recordType := "peer"
|
||||
if isNode {
|
||||
recordType = "node"
|
||||
debugLog(DEBUG_INFO, "Storing node in announce history: enabled=%v, timestamp=%d, maxsize=%dKB",
|
||||
nodeEnabled, nodeTimestamp, nodeMaxSize)
|
||||
}
|
||||
|
||||
h.reticulum.announceHistoryMu.Lock()
|
||||
h.reticulum.announceHistory[identity.GetHexHash()] = announceRecord{
|
||||
timestamp: time.Now().Unix(),
|
||||
appData: appData,
|
||||
}
|
||||
h.reticulum.announceHistoryMu.Unlock()
|
||||
|
||||
debugLog(DEBUG_VERBOSE, "Stored %s announce in history for identity %s", recordType, identity.GetHexHash())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *AnnounceHandler) ReceivePathResponses() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Reticulum) GetDestination() *destination.Destination {
|
||||
return r.destination
|
||||
}
|
||||
|
||||
func (r *Reticulum) createNodeAppData() []byte {
|
||||
// Create a msgpack array with 3 elements
|
||||
// [Bool, Int32, Int16] for [enable, timestamp, max_transfer_size]
|
||||
appData := []byte{0x93} // Array with 3 elements
|
||||
|
||||
// Element 0: Boolean for enable/disable peer
|
||||
if r.nodeEnabled {
|
||||
appData = append(appData, 0xc3) // true
|
||||
} else {
|
||||
appData = append(appData, 0xc2) // false
|
||||
}
|
||||
|
||||
// Element 1: Int32 timestamp (current time)
|
||||
// Update the timestamp when creating new announcements
|
||||
r.nodeTimestamp = time.Now().Unix()
|
||||
appData = append(appData, 0xd2) // int32 format
|
||||
timeBytes := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(timeBytes, uint32(r.nodeTimestamp))
|
||||
appData = append(appData, timeBytes...)
|
||||
|
||||
// Element 2: Int16 max transfer size in KB
|
||||
appData = append(appData, 0xd1) // int16 format
|
||||
sizeBytes := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(sizeBytes, uint16(r.maxTransferSize))
|
||||
appData = append(appData, sizeBytes...)
|
||||
|
||||
log.Printf("[DEBUG-7] Created node appData (msgpack [enable=%v, timestamp=%d, maxsize=%d]): %x",
|
||||
r.nodeEnabled, r.nodeTimestamp, r.maxTransferSize, appData)
|
||||
return appData
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
8
go.mod
@@ -1,9 +1,5 @@
|
||||
module github.com/Sudo-Ivan/reticulum-go
|
||||
|
||||
go 1.23.4
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
golang.org/x/crypto v0.31.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
require golang.org/x/crypto v0.37.0
|
||||
|
||||
10
go.sum
10
go.sum
@@ -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/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=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pelletier/go-toml"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultSharedInstancePort = 37428
|
||||
DefaultInstanceControlPort = 37429
|
||||
DefaultLogLevel = 4
|
||||
DefaultLogLevel = 4
|
||||
)
|
||||
|
||||
func DefaultConfig() *common.ReticulumConfig {
|
||||
return &common.ReticulumConfig{
|
||||
EnableTransport: false,
|
||||
ShareInstance: true,
|
||||
SharedInstancePort: DefaultSharedInstancePort,
|
||||
InstanceControlPort: DefaultInstanceControlPort,
|
||||
EnableTransport: true,
|
||||
ShareInstance: true,
|
||||
SharedInstancePort: DefaultSharedInstancePort,
|
||||
InstanceControlPort: DefaultInstanceControlPort,
|
||||
PanicOnInterfaceErr: false,
|
||||
LogLevel: DefaultLogLevel,
|
||||
Interfaces: make(map[string]common.InterfaceConfig),
|
||||
Interfaces: make(map[string]*common.InterfaceConfig),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +34,7 @@ func GetConfigPath() (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(homeDir, ".reticulum", "config"), nil
|
||||
return filepath.Join(homeDir, ".reticulum-go", "config"), nil
|
||||
}
|
||||
|
||||
func EnsureConfigDir() error {
|
||||
@@ -40,65 +43,212 @@ func EnsureConfigDir() error {
|
||||
return err
|
||||
}
|
||||
|
||||
configDir := filepath.Join(homeDir, ".reticulum")
|
||||
configDir := filepath.Join(homeDir, ".reticulum-go")
|
||||
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
|
||||
func LoadConfig(path string) (*common.ReticulumConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
cfg := DefaultConfig()
|
||||
if err := toml.Unmarshal(data, cfg); err != nil {
|
||||
return nil, err
|
||||
cfg.ConfigPath = path
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
var currentInterface *common.InterfaceConfig
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
// Skip comments and empty lines
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle interface sections
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
name := strings.Trim(line, "[]")
|
||||
currentInterface = &common.InterfaceConfig{Name: name}
|
||||
cfg.Interfaces[name] = currentInterface
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
if currentInterface != nil {
|
||||
// Parse interface config
|
||||
switch key {
|
||||
case "type":
|
||||
currentInterface.Type = value
|
||||
case "enabled":
|
||||
currentInterface.Enabled = value == "true"
|
||||
case "address":
|
||||
currentInterface.Address = value
|
||||
case "port":
|
||||
currentInterface.Port, _ = strconv.Atoi(value)
|
||||
case "target_host":
|
||||
currentInterface.TargetHost = value
|
||||
case "target_port":
|
||||
currentInterface.TargetPort, _ = strconv.Atoi(value)
|
||||
case "discovery_port":
|
||||
currentInterface.DiscoveryPort, _ = strconv.Atoi(value)
|
||||
case "data_port":
|
||||
currentInterface.DataPort, _ = strconv.Atoi(value)
|
||||
case "discovery_scope":
|
||||
currentInterface.DiscoveryScope = value
|
||||
case "group_id":
|
||||
currentInterface.GroupID = value
|
||||
}
|
||||
} else {
|
||||
// Parse global config
|
||||
switch key {
|
||||
case "enable_transport":
|
||||
cfg.EnableTransport = value == "true"
|
||||
case "share_instance":
|
||||
cfg.ShareInstance = value == "true"
|
||||
case "shared_instance_port":
|
||||
cfg.SharedInstancePort, _ = strconv.Atoi(value)
|
||||
case "instance_control_port":
|
||||
cfg.InstanceControlPort, _ = strconv.Atoi(value)
|
||||
case "panic_on_interface_error":
|
||||
cfg.PanicOnInterfaceErr = value == "true"
|
||||
case "loglevel":
|
||||
cfg.LogLevel, _ = strconv.Atoi(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg.ConfigPath = path
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// SaveConfig saves the configuration to the specified path
|
||||
func SaveConfig(cfg *common.ReticulumConfig) error {
|
||||
data, err := toml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
if cfg.ConfigPath == "" {
|
||||
return fmt.Errorf("config path not set")
|
||||
}
|
||||
|
||||
return os.WriteFile(cfg.ConfigPath, data, 0644)
|
||||
var builder strings.Builder
|
||||
|
||||
// Write global config
|
||||
builder.WriteString("# Reticulum Configuration\n")
|
||||
builder.WriteString(fmt.Sprintf("enable_transport = %v\n", cfg.EnableTransport))
|
||||
builder.WriteString(fmt.Sprintf("share_instance = %v\n", cfg.ShareInstance))
|
||||
builder.WriteString(fmt.Sprintf("shared_instance_port = %d\n", cfg.SharedInstancePort))
|
||||
builder.WriteString(fmt.Sprintf("instance_control_port = %d\n", cfg.InstanceControlPort))
|
||||
builder.WriteString(fmt.Sprintf("panic_on_interface_error = %v\n", cfg.PanicOnInterfaceErr))
|
||||
builder.WriteString(fmt.Sprintf("loglevel = %d\n\n", cfg.LogLevel))
|
||||
|
||||
// Write interface configs
|
||||
for name, iface := range cfg.Interfaces {
|
||||
builder.WriteString(fmt.Sprintf("[%s]\n", name))
|
||||
builder.WriteString(fmt.Sprintf("type = %s\n", iface.Type))
|
||||
builder.WriteString(fmt.Sprintf("enabled = %v\n", iface.Enabled))
|
||||
|
||||
if iface.Address != "" {
|
||||
builder.WriteString(fmt.Sprintf("address = %s\n", iface.Address))
|
||||
}
|
||||
if iface.Port != 0 {
|
||||
builder.WriteString(fmt.Sprintf("port = %d\n", iface.Port))
|
||||
}
|
||||
if iface.TargetHost != "" {
|
||||
builder.WriteString(fmt.Sprintf("target_host = %s\n", iface.TargetHost))
|
||||
}
|
||||
if iface.TargetPort != 0 {
|
||||
builder.WriteString(fmt.Sprintf("target_port = %d\n", iface.TargetPort))
|
||||
}
|
||||
if iface.DiscoveryPort != 0 {
|
||||
builder.WriteString(fmt.Sprintf("discovery_port = %d\n", iface.DiscoveryPort))
|
||||
}
|
||||
if iface.DataPort != 0 {
|
||||
builder.WriteString(fmt.Sprintf("data_port = %d\n", iface.DataPort))
|
||||
}
|
||||
if iface.DiscoveryScope != "" {
|
||||
builder.WriteString(fmt.Sprintf("discovery_scope = %s\n", iface.DiscoveryScope))
|
||||
}
|
||||
if iface.GroupID != "" {
|
||||
builder.WriteString(fmt.Sprintf("group_id = %s\n", iface.GroupID))
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
return os.WriteFile(cfg.ConfigPath, []byte(builder.String()), 0644)
|
||||
}
|
||||
|
||||
// CreateDefaultConfig creates a default configuration file
|
||||
func CreateDefaultConfig(path string) error {
|
||||
cfg := DefaultConfig()
|
||||
cfg.ConfigPath = path
|
||||
|
||||
// Add default interface
|
||||
cfg.Interfaces["Default Interface"] = common.InterfaceConfig{
|
||||
Type: "AutoInterface",
|
||||
Enabled: false,
|
||||
// Add Auto Interface
|
||||
cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{
|
||||
Type: "AutoInterface",
|
||||
Enabled: true,
|
||||
GroupID: "reticulum",
|
||||
DiscoveryScope: "link",
|
||||
DiscoveryPort: 29716,
|
||||
DataPort: 42671,
|
||||
}
|
||||
|
||||
// Add default quad4net interface
|
||||
cfg.Interfaces["quad4net tcp"] = common.InterfaceConfig{
|
||||
// Add default interfaces
|
||||
cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
|
||||
Type: "TCPClientInterface",
|
||||
Enabled: true,
|
||||
TargetHost: "127.0.0.1",
|
||||
TargetPort: 4242,
|
||||
Name: "Go-RNS-Testnet",
|
||||
}
|
||||
|
||||
cfg.Interfaces["Quad4 TCP"] = &common.InterfaceConfig{
|
||||
Type: "TCPClientInterface",
|
||||
Enabled: true,
|
||||
TargetHost: "rns.quad4.io",
|
||||
TargetPort: 4242,
|
||||
Name: "Quad4 TCP",
|
||||
}
|
||||
|
||||
data, err := toml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
cfg.Interfaces["Local UDP"] = &common.InterfaceConfig{
|
||||
Type: "UDPInterface",
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(path, data, 0644)
|
||||
return SaveConfig(cfg)
|
||||
}
|
||||
|
||||
// InitConfig initializes the configuration system
|
||||
@@ -118,4 +268,4 @@ func InitConfig() (*common.ReticulumConfig, error) {
|
||||
|
||||
// Load config
|
||||
return LoadConfig(configPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,47 @@
|
||||
package announce
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
||||
)
|
||||
|
||||
const (
|
||||
PACKET_TYPE_DATA = 0x00
|
||||
PACKET_TYPE_ANNOUNCE = 0x01
|
||||
PACKET_TYPE_LINK = 0x02
|
||||
PACKET_TYPE_PROOF = 0x03
|
||||
|
||||
// Announce Types
|
||||
ANNOUNCE_NONE = 0x00
|
||||
ANNOUNCE_PATH = 0x01
|
||||
ANNOUNCE_IDENTITY = 0x02
|
||||
|
||||
// Header Types
|
||||
HEADER_TYPE_1 = 0x00 // One address field
|
||||
HEADER_TYPE_2 = 0x01 // Two address fields
|
||||
|
||||
// Propagation Types
|
||||
PROP_TYPE_BROADCAST = 0x00
|
||||
PROP_TYPE_TRANSPORT = 0x01
|
||||
|
||||
DEST_TYPE_SINGLE = 0x00
|
||||
DEST_TYPE_GROUP = 0x01
|
||||
DEST_TYPE_PLAIN = 0x02
|
||||
DEST_TYPE_LINK = 0x03
|
||||
|
||||
// IFAC Flag
|
||||
IFAC_NONE = 0x00
|
||||
IFAC_AUTH = 0x80
|
||||
|
||||
MAX_HOPS = 128
|
||||
PROPAGATION_RATE = 0.02 // 2% of interface bandwidth
|
||||
RETRY_INTERVAL = 300 // 5 minutes
|
||||
@@ -24,27 +50,37 @@ const (
|
||||
|
||||
type AnnounceHandler interface {
|
||||
AspectFilter() []string
|
||||
ReceivedAnnounce(destinationHash []byte, announcedIdentity *identity.Identity, appData []byte) error
|
||||
ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte) error
|
||||
ReceivePathResponses() bool
|
||||
}
|
||||
|
||||
type Announce struct {
|
||||
mutex sync.RWMutex
|
||||
mutex *sync.RWMutex
|
||||
destinationHash []byte
|
||||
identity *identity.Identity
|
||||
appData []byte
|
||||
hops uint8
|
||||
timestamp int64
|
||||
signature []byte
|
||||
pathResponse bool
|
||||
retries int
|
||||
handlers []AnnounceHandler
|
||||
identity *identity.Identity
|
||||
appData []byte
|
||||
config *common.ReticulumConfig
|
||||
hops uint8
|
||||
timestamp int64
|
||||
signature []byte
|
||||
pathResponse bool
|
||||
retries int
|
||||
handlers []AnnounceHandler
|
||||
ratchetID []byte
|
||||
packet []byte
|
||||
hash []byte
|
||||
}
|
||||
|
||||
func New(dest *identity.Identity, appData []byte, pathResponse bool) (*Announce, error) {
|
||||
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{
|
||||
mutex: &sync.RWMutex{},
|
||||
identity: dest,
|
||||
appData: appData,
|
||||
config: config,
|
||||
hops: 0,
|
||||
timestamp: time.Now().Unix(),
|
||||
pathResponse: pathResponse,
|
||||
@@ -52,46 +88,59 @@ func New(dest *identity.Identity, appData []byte, pathResponse bool) (*Announce,
|
||||
handlers: make([]AnnounceHandler, 0),
|
||||
}
|
||||
|
||||
// Generate destination hash
|
||||
hash := sha256.New()
|
||||
hash.Write(dest.GetPublicKey())
|
||||
a.destinationHash = hash.Sum(nil)[:16] // Truncated hash
|
||||
// Generate truncated hash from public key
|
||||
pubKey := dest.GetPublicKey()
|
||||
hash := sha256.Sum256(pubKey)
|
||||
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...)
|
||||
if a.ratchetID != nil {
|
||||
signData = append(signData, a.ratchetID...)
|
||||
}
|
||||
a.signature = dest.Sign(signData)
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a *Announce) Propagate(interfaces []transport.Interface) error {
|
||||
a.mutex.Lock()
|
||||
defer a.mutex.Unlock()
|
||||
func (a *Announce) Propagate(interfaces []common.NetworkInterface) error {
|
||||
a.mutex.RLock()
|
||||
defer a.mutex.RUnlock()
|
||||
|
||||
if a.hops >= MAX_HOPS {
|
||||
return errors.New("maximum hop count reached")
|
||||
log.Printf("[DEBUG-7] Propagating announce across %d interfaces", len(interfaces))
|
||||
|
||||
var packet []byte
|
||||
if a.packet != nil {
|
||||
log.Printf("[DEBUG-7] Using cached packet (%d bytes)", len(a.packet))
|
||||
packet = a.packet
|
||||
} else {
|
||||
log.Printf("[DEBUG-7] Creating new packet")
|
||||
packet = a.CreatePacket()
|
||||
a.packet = packet
|
||||
}
|
||||
|
||||
// Increment hop count
|
||||
a.hops++
|
||||
|
||||
// Create announce packet
|
||||
packet := make([]byte, 0)
|
||||
packet = append(packet, a.destinationHash...)
|
||||
packet = append(packet, a.identity.GetPublicKey()...)
|
||||
packet = append(packet, byte(a.hops))
|
||||
|
||||
if a.appData != nil {
|
||||
packet = append(packet, a.appData...)
|
||||
}
|
||||
|
||||
packet = append(packet, a.signature...)
|
||||
|
||||
// Propagate to all interfaces
|
||||
for _, iface := range interfaces {
|
||||
if err := iface.SendAnnounce(packet, a.pathResponse); err != nil {
|
||||
return err
|
||||
if !iface.IsEnabled() {
|
||||
log.Printf("[DEBUG-7] Skipping disabled interface: %s", iface.GetName())
|
||||
continue
|
||||
}
|
||||
if !iface.GetBandwidthAvailable() {
|
||||
log.Printf("[DEBUG-7] Skipping interface with insufficient bandwidth: %s", iface.GetName())
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-7] Sending announce on interface %s", iface.GetName())
|
||||
if err := iface.Send(packet, ""); err != nil {
|
||||
log.Printf("[DEBUG-7] Failed to send on interface %s: %v", iface.GetName(), err)
|
||||
return fmt.Errorf("failed to propagate on interface %s: %w", iface.GetName(), err)
|
||||
}
|
||||
log.Printf("[DEBUG-7] Successfully sent announce on interface %s", iface.GetName())
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -118,28 +167,117 @@ func (a *Announce) HandleAnnounce(data []byte) error {
|
||||
a.mutex.Lock()
|
||||
defer a.mutex.Unlock()
|
||||
|
||||
// Validate announce data
|
||||
if len(data) < 16+32+1 { // Min size: hash + pubkey + hops
|
||||
return errors.New("invalid announce data")
|
||||
log.Printf("[DEBUG-7] Handling announce packet of %d bytes", len(data))
|
||||
|
||||
// Minimum packet size validation
|
||||
// header(2) + desthash(16) + context(1) + enckey(32) + signkey(32) + namehash(10) +
|
||||
// randomhash(10) + signature(64) + min app data(3)
|
||||
if len(data) < 170 {
|
||||
log.Printf("[DEBUG-7] Invalid announce data length: %d bytes (minimum 170)", len(data))
|
||||
return errors.New("invalid announce data length")
|
||||
}
|
||||
|
||||
// Extract fields
|
||||
destHash := data[:16]
|
||||
pubKey := data[16:48]
|
||||
hops := data[48]
|
||||
appData := data[49 : len(data)-64]
|
||||
signature := data[len(data)-64:]
|
||||
// Extract header and check packet type
|
||||
header := data[:2]
|
||||
if header[0]&0x03 != PACKET_TYPE_ANNOUNCE {
|
||||
return errors.New("not an announce packet")
|
||||
}
|
||||
|
||||
// Get hop count
|
||||
hopCount := header[1]
|
||||
if hopCount > MAX_HOPS {
|
||||
log.Printf("[DEBUG-7] Announce exceeded max hops: %d", hopCount)
|
||||
return errors.New("announce exceeded maximum hop count")
|
||||
}
|
||||
|
||||
// Parse the packet based on header type
|
||||
headerType := (header[0] & 0b01000000) >> 6
|
||||
var contextByte byte
|
||||
var packetData []byte
|
||||
|
||||
if headerType == HEADER_TYPE_2 {
|
||||
// Header type 2 format: header(2) + desthash(16) + transportid(16) + context(1) + data
|
||||
if len(data) < 35 {
|
||||
return errors.New("header type 2 packet too short")
|
||||
}
|
||||
destHash := data[2:18]
|
||||
transportID := data[18:34]
|
||||
contextByte = data[34]
|
||||
packetData = data[35:]
|
||||
|
||||
log.Printf("[DEBUG-7] Header type 2 announce: destHash=%x, transportID=%x, context=%d",
|
||||
destHash, transportID, contextByte)
|
||||
} else {
|
||||
// Header type 1 format: header(2) + desthash(16) + context(1) + data
|
||||
if len(data) < 19 {
|
||||
return errors.New("header type 1 packet too short")
|
||||
}
|
||||
destHash := data[2:18]
|
||||
contextByte = data[18]
|
||||
packetData = data[19:]
|
||||
|
||||
log.Printf("[DEBUG-7] Header type 1 announce: destHash=%x, context=%d",
|
||||
destHash, contextByte)
|
||||
}
|
||||
|
||||
// Now parse the data portion according to the spec
|
||||
// Public Key (32) + Signing Key (32) + Name Hash (10) + Random Hash (10) + [Ratchet] + Signature (64) + App Data
|
||||
|
||||
if len(packetData) < 148 { // 32 + 32 + 10 + 10 + 64
|
||||
return errors.New("announce data too short")
|
||||
}
|
||||
|
||||
// Extract the components
|
||||
encKey := packetData[:32]
|
||||
signKey := packetData[32:64]
|
||||
nameHash := packetData[64:74]
|
||||
randomHash := packetData[74:84]
|
||||
|
||||
// The next field could be a ratchet (32 bytes) or signature (64 bytes)
|
||||
// We need to detect this somehow or use a flag
|
||||
// For now, assume no ratchet
|
||||
|
||||
signature := packetData[84:148]
|
||||
appData := packetData[148:]
|
||||
|
||||
log.Printf("[DEBUG-7] Announce fields: encKey=%x, signKey=%x", encKey, signKey)
|
||||
log.Printf("[DEBUG-7] Name hash=%x, random hash=%x", nameHash, randomHash)
|
||||
log.Printf("[DEBUG-7] Signature=%x, appDataLen=%d", signature[:8], len(appData))
|
||||
|
||||
// Get the destination hash from header
|
||||
var destHash []byte
|
||||
if headerType == HEADER_TYPE_2 {
|
||||
destHash = data[2:18]
|
||||
} else {
|
||||
destHash = data[2:18]
|
||||
}
|
||||
|
||||
// Combine public keys
|
||||
pubKey := append(encKey, signKey...)
|
||||
|
||||
// Create announced identity from public keys
|
||||
announcedIdentity := identity.FromPublicKey(pubKey)
|
||||
if announcedIdentity == nil {
|
||||
return errors.New("invalid identity public key")
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
signData := append(destHash, appData...)
|
||||
if !a.identity.Verify(signData, signature) {
|
||||
signedData := make([]byte, 0)
|
||||
signedData = append(signedData, destHash...)
|
||||
signedData = append(signedData, encKey...)
|
||||
signedData = append(signedData, signKey...)
|
||||
signedData = append(signedData, nameHash...)
|
||||
signedData = append(signedData, randomHash...)
|
||||
signedData = append(signedData, appData...)
|
||||
|
||||
if !announcedIdentity.Verify(signedData, signature) {
|
||||
return errors.New("invalid announce signature")
|
||||
}
|
||||
|
||||
// Process announce with registered handlers
|
||||
// Process with handlers
|
||||
for _, handler := range a.handlers {
|
||||
if handler.ReceivePathResponses() || !a.pathResponse {
|
||||
if err := handler.ReceivedAnnounce(destHash, a.identity, appData); err != nil {
|
||||
if err := handler.ReceivedAnnounce(destHash, announcedIdentity, appData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -148,7 +286,7 @@ func (a *Announce) HandleAnnounce(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Announce) RequestPath(destHash []byte, onInterface transport.Interface) error {
|
||||
func (a *Announce) RequestPath(destHash []byte, onInterface common.NetworkInterface) error {
|
||||
a.mutex.Lock()
|
||||
defer a.mutex.Unlock()
|
||||
|
||||
@@ -158,9 +296,222 @@ func (a *Announce) RequestPath(destHash []byte, onInterface transport.Interface)
|
||||
packet = append(packet, byte(0)) // Initial hop count
|
||||
|
||||
// Send path request
|
||||
if err := onInterface.SendPathRequest(packet); err != nil {
|
||||
if err := onInterface.Send(packet, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// CreateHeader creates a Reticulum packet header according to spec
|
||||
func CreateHeader(ifacFlag byte, headerType byte, contextFlag byte, propType byte, destType byte, packetType byte, hops byte) []byte {
|
||||
header := make([]byte, 2)
|
||||
|
||||
// First byte: [IFAC Flag], [Header Type], [Context Flag], [Propagation Type], [Destination Type] and [Packet Type]
|
||||
header[0] = ifacFlag | (headerType << 6) | (contextFlag << 5) |
|
||||
(propType << 4) | (destType << 2) | packetType
|
||||
|
||||
// Second byte: Number of hops
|
||||
header[1] = hops
|
||||
|
||||
return header
|
||||
}
|
||||
|
||||
func (a *Announce) CreatePacket() []byte {
|
||||
// Create header
|
||||
header := CreateHeader(
|
||||
IFAC_NONE,
|
||||
HEADER_TYPE_2, // Use header type 2 for announces
|
||||
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...)
|
||||
|
||||
// If using header type 2, add transport ID (16 bytes)
|
||||
// For broadcast announces, this is filled with zeroes
|
||||
transportID := make([]byte, 16)
|
||||
packet = append(packet, transportID...)
|
||||
|
||||
// Add context byte
|
||||
packet = append(packet, byte(0)) // Context byte, 0 for announces
|
||||
|
||||
// Add public key parts (32 bytes each)
|
||||
pubKey := a.identity.GetPublicKey()
|
||||
encKey := pubKey[:32] // Encryption key
|
||||
signKey := pubKey[32:] // Signing key
|
||||
|
||||
// Start building data portion according to spec
|
||||
data := make([]byte, 0, 32+32+10+10+32+64+len(a.appData))
|
||||
data = append(data, encKey...) // Encryption key (32 bytes)
|
||||
data = append(data, signKey...) // Signing key (32 bytes)
|
||||
|
||||
// Determine if this is a node announce based on appData format
|
||||
var appName string
|
||||
if len(a.appData) > 2 && a.appData[0] == 0x93 {
|
||||
// This is a node announcement
|
||||
appName = "reticulum.node"
|
||||
} else if len(a.appData) > 3 && a.appData[0] == 0x92 && a.appData[1] == 0xc4 {
|
||||
nameLen := int(a.appData[2])
|
||||
if 3+nameLen <= len(a.appData) {
|
||||
appName = string(a.appData[3 : 3+nameLen])
|
||||
} else {
|
||||
appName = fmt.Sprintf("%s.%s", a.config.AppName, a.config.AppAspect)
|
||||
}
|
||||
} else {
|
||||
// Default fallback using config values
|
||||
appName = fmt.Sprintf("%s.%s", a.config.AppName, a.config.AppAspect)
|
||||
}
|
||||
|
||||
// Add name hash (10 bytes)
|
||||
nameHash := sha256.Sum256([]byte(appName))
|
||||
nameHash10 := nameHash[:10]
|
||||
log.Printf("[DEBUG-6] Using name hash for '%s': %x", appName, nameHash10)
|
||||
data = append(data, nameHash10...)
|
||||
|
||||
// Add random hash (10 bytes) - 5 bytes random + 5 bytes time
|
||||
randomHash := make([]byte, 10)
|
||||
rand.Read(randomHash[:5])
|
||||
timeBytes := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(timeBytes, uint64(time.Now().Unix()))
|
||||
copy(randomHash[5:], timeBytes[:5])
|
||||
data = append(data, randomHash...)
|
||||
|
||||
// Add ratchet ID (32 bytes) - required in the packet format
|
||||
if a.ratchetID != nil {
|
||||
data = append(data, a.ratchetID...)
|
||||
} else {
|
||||
// If there's no ratchet, add 32 zero bytes as placeholder
|
||||
data = append(data, make([]byte, 32)...)
|
||||
}
|
||||
|
||||
// Create validation data for signature
|
||||
// Signature consists of destination hash, public keys, name hash, random hash, and app data
|
||||
validationData := make([]byte, 0)
|
||||
validationData = append(validationData, a.destinationHash...)
|
||||
validationData = append(validationData, encKey...)
|
||||
validationData = append(validationData, signKey...)
|
||||
validationData = append(validationData, nameHash10...)
|
||||
validationData = append(validationData, randomHash...)
|
||||
validationData = append(validationData, a.appData...)
|
||||
|
||||
// Add signature (64 bytes)
|
||||
signature := a.identity.Sign(validationData)
|
||||
data = append(data, signature...)
|
||||
|
||||
// Add app data
|
||||
if len(a.appData) > 0 {
|
||||
data = append(data, a.appData...)
|
||||
}
|
||||
|
||||
// Combine header and data
|
||||
packet = append(packet, data...)
|
||||
|
||||
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 {
|
||||
// Use CreatePacket to generate the packet
|
||||
a.packet = a.CreatePacket()
|
||||
}
|
||||
|
||||
return a.packet
|
||||
}
|
||||
|
||||
7
pkg/announce/handler.go
Normal file
7
pkg/announce/handler.go
Normal 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
240
pkg/buffer/buffer.go
Normal 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
218
pkg/channel/channel.go
Normal 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
|
||||
}
|
||||
@@ -1,29 +1,93 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
DEFAULT_SHARED_INSTANCE_PORT = 37428
|
||||
DEFAULT_INSTANCE_CONTROL_PORT = 37429
|
||||
DEFAULT_LOG_LEVEL = 20
|
||||
)
|
||||
|
||||
// ConfigProvider interface for accessing configuration
|
||||
type ConfigProvider interface {
|
||||
GetConfigPath() string
|
||||
GetLogLevel() int
|
||||
GetInterfaces() map[string]InterfaceConfig
|
||||
GetConfigPath() string
|
||||
GetLogLevel() int
|
||||
GetInterfaces() map[string]InterfaceConfig
|
||||
}
|
||||
|
||||
// InterfaceConfig represents interface configuration
|
||||
type InterfaceConfig struct {
|
||||
Type string `toml:"type"`
|
||||
Enabled bool `toml:"enabled"`
|
||||
TargetHost string `toml:"target_host,omitempty"`
|
||||
TargetPort int `toml:"target_port,omitempty"`
|
||||
Interface string `toml:"interface,omitempty"`
|
||||
Name string
|
||||
Type string
|
||||
Enabled bool
|
||||
Address string
|
||||
Port int
|
||||
TargetHost string
|
||||
TargetPort int
|
||||
TargetAddress string
|
||||
Interface string
|
||||
KISSFraming bool
|
||||
I2PTunneled bool
|
||||
PreferIPv6 bool
|
||||
MaxReconnTries int
|
||||
Bitrate int64
|
||||
MTU int
|
||||
GroupID string
|
||||
DiscoveryScope string
|
||||
DiscoveryPort int
|
||||
DataPort int
|
||||
}
|
||||
|
||||
// ReticulumConfig represents the main configuration structure
|
||||
type ReticulumConfig struct {
|
||||
EnableTransport bool `toml:"enable_transport"`
|
||||
ShareInstance bool `toml:"share_instance"`
|
||||
SharedInstancePort int `toml:"shared_instance_port"`
|
||||
InstanceControlPort int `toml:"instance_control_port"`
|
||||
PanicOnInterfaceErr bool `toml:"panic_on_interface_error"`
|
||||
LogLevel int `toml:"loglevel"`
|
||||
ConfigPath string `toml:"-"`
|
||||
Interfaces map[string]InterfaceConfig
|
||||
}
|
||||
ConfigPath string
|
||||
EnableTransport bool
|
||||
ShareInstance bool
|
||||
SharedInstancePort int
|
||||
InstanceControlPort int
|
||||
PanicOnInterfaceErr bool
|
||||
LogLevel int
|
||||
Interfaces map[string]*InterfaceConfig
|
||||
AppName string
|
||||
AppAspect string
|
||||
}
|
||||
|
||||
// NewReticulumConfig creates a new ReticulumConfig with default values
|
||||
func NewReticulumConfig() *ReticulumConfig {
|
||||
return &ReticulumConfig{
|
||||
EnableTransport: true,
|
||||
ShareInstance: false,
|
||||
SharedInstancePort: DEFAULT_SHARED_INSTANCE_PORT,
|
||||
InstanceControlPort: DEFAULT_INSTANCE_CONTROL_PORT,
|
||||
PanicOnInterfaceErr: false,
|
||||
LogLevel: DEFAULT_LOG_LEVEL,
|
||||
Interfaces: make(map[string]*InterfaceConfig),
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks if the configuration is valid
|
||||
func (c *ReticulumConfig) Validate() error {
|
||||
if c.SharedInstancePort < 1 || c.SharedInstancePort > 65535 {
|
||||
return fmt.Errorf("invalid shared instance port: %d", c.SharedInstancePort)
|
||||
}
|
||||
if c.InstanceControlPort < 1 || c.InstanceControlPort > 65535 {
|
||||
return fmt.Errorf("invalid instance control port: %d", c.InstanceControlPort)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DefaultConfig() *ReticulumConfig {
|
||||
return &ReticulumConfig{
|
||||
EnableTransport: true,
|
||||
ShareInstance: false,
|
||||
SharedInstancePort: DEFAULT_SHARED_INSTANCE_PORT,
|
||||
InstanceControlPort: DEFAULT_INSTANCE_CONTROL_PORT,
|
||||
PanicOnInterfaceErr: false,
|
||||
LogLevel: DEFAULT_LOG_LEVEL,
|
||||
Interfaces: make(map[string]*InterfaceConfig),
|
||||
AppName: "Go Client",
|
||||
AppAspect: "node",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,61 @@
|
||||
package common
|
||||
|
||||
const (
|
||||
// Interface Types
|
||||
IF_TYPE_UDP InterfaceType = iota
|
||||
IF_TYPE_TCP
|
||||
IF_TYPE_UNIX
|
||||
// Interface Types
|
||||
IF_TYPE_NONE InterfaceType = iota
|
||||
IF_TYPE_UDP
|
||||
IF_TYPE_TCP
|
||||
IF_TYPE_UNIX
|
||||
IF_TYPE_I2P
|
||||
IF_TYPE_BLUETOOTH
|
||||
IF_TYPE_SERIAL
|
||||
IF_TYPE_AUTO
|
||||
|
||||
// Interface Modes
|
||||
IF_MODE_FULL InterfaceMode = iota
|
||||
IF_MODE_POINT
|
||||
IF_MODE_GATEWAY
|
||||
// Interface Modes
|
||||
IF_MODE_FULL InterfaceMode = iota
|
||||
IF_MODE_POINT
|
||||
IF_MODE_GATEWAY
|
||||
IF_MODE_ACCESS_POINT
|
||||
IF_MODE_ROAMING
|
||||
IF_MODE_BOUNDARY
|
||||
|
||||
// Transport Modes
|
||||
TRANSPORT_MODE_DIRECT TransportMode = iota
|
||||
TRANSPORT_MODE_RELAY
|
||||
TRANSPORT_MODE_GATEWAY
|
||||
// Transport Modes
|
||||
TRANSPORT_MODE_DIRECT TransportMode = iota
|
||||
TRANSPORT_MODE_RELAY
|
||||
TRANSPORT_MODE_GATEWAY
|
||||
|
||||
// Path Status
|
||||
PATH_STATUS_UNKNOWN PathStatus = iota
|
||||
PATH_STATUS_DIRECT
|
||||
PATH_STATUS_RELAY
|
||||
PATH_STATUS_FAILED
|
||||
// Path Status
|
||||
PATH_STATUS_UNKNOWN PathStatus = iota
|
||||
PATH_STATUS_DIRECT
|
||||
PATH_STATUS_RELAY
|
||||
PATH_STATUS_FAILED
|
||||
|
||||
// Common Constants
|
||||
DEFAULT_MTU = 1500
|
||||
MAX_PACKET_SIZE = 65535
|
||||
)
|
||||
// Resource Status
|
||||
RESOURCE_STATUS_PENDING = 0x00
|
||||
RESOURCE_STATUS_ACTIVE = 0x01
|
||||
RESOURCE_STATUS_COMPLETE = 0x02
|
||||
RESOURCE_STATUS_FAILED = 0x03
|
||||
RESOURCE_STATUS_CANCELLED = 0x04
|
||||
|
||||
// Link Status
|
||||
LINK_STATUS_PENDING = 0x00
|
||||
LINK_STATUS_ACTIVE = 0x01
|
||||
LINK_STATUS_CLOSED = 0x02
|
||||
LINK_STATUS_FAILED = 0x03
|
||||
|
||||
// Direction Constants
|
||||
IN = 0x01
|
||||
OUT = 0x02
|
||||
|
||||
// Common Constants
|
||||
DEFAULT_MTU = 1500
|
||||
MAX_PACKET_SIZE = 65535
|
||||
BITRATE_MINIMUM = 5
|
||||
|
||||
// Timeouts and Intervals
|
||||
ESTABLISH_TIMEOUT = 6
|
||||
KEEPALIVE_INTERVAL = 360
|
||||
STALE_TIME = 720
|
||||
PATH_REQUEST_TTL = 300
|
||||
ANNOUNCE_TIMEOUT = 15
|
||||
)
|
||||
|
||||
@@ -1,57 +1,211 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NetworkInterface combines both low-level and high-level interface requirements
|
||||
// NetworkInterface defines the interface for all network communication methods
|
||||
type NetworkInterface interface {
|
||||
// Low-level network operations
|
||||
Start() error
|
||||
Stop() error
|
||||
Send(data []byte, address string) error
|
||||
Receive() ([]byte, string, error)
|
||||
GetType() InterfaceType
|
||||
GetMode() InterfaceMode
|
||||
GetMTU() int
|
||||
|
||||
// High-level packet operations
|
||||
ProcessIncoming([]byte)
|
||||
ProcessOutgoing([]byte) error
|
||||
SendPathRequest([]byte) error
|
||||
SendLinkPacket([]byte, []byte, time.Time) error
|
||||
Detach()
|
||||
SetPacketCallback(PacketCallback)
|
||||
|
||||
// Additional required fields
|
||||
GetName() string
|
||||
GetConn() net.Conn
|
||||
IsEnabled() bool
|
||||
// Core interface operations
|
||||
Start() error
|
||||
Stop() error
|
||||
Enable()
|
||||
Disable()
|
||||
Detach()
|
||||
|
||||
// Network operations
|
||||
Send(data []byte, address string) error
|
||||
GetConn() net.Conn
|
||||
GetMTU() int
|
||||
GetName() string
|
||||
|
||||
// Interface properties
|
||||
GetType() InterfaceType
|
||||
GetMode() InterfaceMode
|
||||
IsEnabled() bool
|
||||
IsOnline() bool
|
||||
IsDetached() bool
|
||||
GetBandwidthAvailable() bool
|
||||
|
||||
// Packet handling
|
||||
ProcessIncoming([]byte)
|
||||
ProcessOutgoing([]byte) error
|
||||
SendPathRequest([]byte) error
|
||||
SendLinkPacket([]byte, []byte, time.Time) error
|
||||
SetPacketCallback(PacketCallback)
|
||||
GetPacketCallback() PacketCallback
|
||||
}
|
||||
|
||||
type PacketCallback func([]byte, interface{})
|
||||
|
||||
// BaseInterface provides common implementation
|
||||
// BaseInterface provides common implementation for network interfaces
|
||||
type BaseInterface struct {
|
||||
Name string
|
||||
Mode InterfaceMode
|
||||
Type InterfaceType
|
||||
|
||||
Online bool
|
||||
Detached bool
|
||||
|
||||
IN bool
|
||||
OUT bool
|
||||
|
||||
MTU int
|
||||
Bitrate int64
|
||||
|
||||
TxBytes uint64
|
||||
RxBytes uint64
|
||||
|
||||
mutex sync.RWMutex
|
||||
owner interface{}
|
||||
packetCallback PacketCallback
|
||||
}
|
||||
Name string
|
||||
Mode InterfaceMode
|
||||
Type InterfaceType
|
||||
Online bool
|
||||
Enabled bool
|
||||
Detached bool
|
||||
|
||||
IN bool
|
||||
OUT bool
|
||||
|
||||
MTU int
|
||||
Bitrate int64
|
||||
|
||||
TxBytes uint64
|
||||
RxBytes uint64
|
||||
lastTx time.Time
|
||||
|
||||
Mutex sync.RWMutex
|
||||
Owner interface{}
|
||||
PacketCallback PacketCallback
|
||||
}
|
||||
|
||||
// NewBaseInterface creates a new BaseInterface instance
|
||||
func NewBaseInterface(name string, ifaceType InterfaceType, enabled bool) BaseInterface {
|
||||
return BaseInterface{
|
||||
Name: name,
|
||||
Type: ifaceType,
|
||||
Mode: IF_MODE_FULL,
|
||||
Enabled: enabled,
|
||||
MTU: DEFAULT_MTU,
|
||||
Bitrate: BITRATE_MINIMUM,
|
||||
lastTx: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Default implementations for BaseInterface
|
||||
func (i *BaseInterface) GetType() InterfaceType {
|
||||
return i.Type
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetMode() InterfaceMode {
|
||||
return i.Mode
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetMTU() int {
|
||||
return i.MTU
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetName() string {
|
||||
return i.Name
|
||||
}
|
||||
|
||||
func (i *BaseInterface) IsEnabled() bool {
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
return i.Enabled && i.Online && !i.Detached
|
||||
}
|
||||
|
||||
func (i *BaseInterface) IsOnline() bool {
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
return i.Online
|
||||
}
|
||||
|
||||
func (i *BaseInterface) IsDetached() bool {
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
return i.Detached
|
||||
}
|
||||
|
||||
func (i *BaseInterface) SetPacketCallback(callback PacketCallback) {
|
||||
i.Mutex.Lock()
|
||||
defer i.Mutex.Unlock()
|
||||
i.PacketCallback = callback
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetPacketCallback() PacketCallback {
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
return i.PacketCallback
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Detach() {
|
||||
i.Mutex.Lock()
|
||||
defer i.Mutex.Unlock()
|
||||
i.Detached = true
|
||||
i.Online = false
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Enable() {
|
||||
i.Mutex.Lock()
|
||||
defer i.Mutex.Unlock()
|
||||
i.Enabled = true
|
||||
i.Online = true
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Disable() {
|
||||
i.Mutex.Lock()
|
||||
defer i.Mutex.Unlock()
|
||||
i.Enabled = false
|
||||
i.Online = false
|
||||
}
|
||||
|
||||
// Default implementations that should be overridden by specific interfaces
|
||||
func (i *BaseInterface) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetConn() net.Conn {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Send(data []byte, address string) error {
|
||||
return i.ProcessOutgoing(data)
|
||||
}
|
||||
|
||||
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
||||
if i.PacketCallback != nil {
|
||||
i.PacketCallback(data, i)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *BaseInterface) ProcessOutgoing(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *BaseInterface) SendPathRequest(data []byte) error {
|
||||
return i.Send(data, "")
|
||||
}
|
||||
|
||||
func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.Time) error {
|
||||
// Create link packet
|
||||
packet := make([]byte, 0, len(dest)+len(data)+9) // 1 byte type + dest + 8 byte timestamp
|
||||
packet = append(packet, 0x02) // Link packet type
|
||||
packet = append(packet, dest...)
|
||||
|
||||
// Add timestamp
|
||||
ts := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix()))
|
||||
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
|
||||
}
|
||||
|
||||
@@ -4,33 +4,72 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Interface related types
|
||||
type InterfaceMode byte
|
||||
type InterfaceType byte
|
||||
|
||||
// Transport related types
|
||||
type TransportMode byte
|
||||
type PathStatus byte
|
||||
|
||||
// Common structs
|
||||
// Path represents routing information for a destination
|
||||
type Path struct {
|
||||
Interface NetworkInterface
|
||||
Address string
|
||||
Status PathStatus
|
||||
LastSeen time.Time
|
||||
NextHop []byte
|
||||
Hops uint8
|
||||
LastUpdated time.Time
|
||||
Interface NetworkInterface
|
||||
LastSeen time.Time
|
||||
NextHop []byte
|
||||
Hops uint8
|
||||
LastUpdated time.Time
|
||||
HopCount uint8
|
||||
}
|
||||
|
||||
// Common callbacks
|
||||
type ProofRequestedCallback func(interface{}) bool
|
||||
type ProofRequestedCallback func([]byte, []byte)
|
||||
type LinkEstablishedCallback func(interface{})
|
||||
type PacketCallback func([]byte, NetworkInterface)
|
||||
|
||||
// Request handler
|
||||
// RequestHandler manages path requests and responses
|
||||
type RequestHandler struct {
|
||||
Path string
|
||||
ResponseGenerator func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity interface{}, requestedAt int64) []byte
|
||||
AllowMode byte
|
||||
AllowedList [][]byte
|
||||
}
|
||||
}
|
||||
|
||||
// Interface types
|
||||
type InterfaceMode byte
|
||||
type InterfaceType byte
|
||||
|
||||
// RatchetIDReceiver holds ratchet ID information
|
||||
type RatchetIDReceiver struct {
|
||||
LatestRatchetID []byte
|
||||
}
|
||||
|
||||
// NetworkStats holds interface statistics
|
||||
type NetworkStats struct {
|
||||
BytesSent uint64
|
||||
BytesReceived uint64
|
||||
PacketsSent uint64
|
||||
PacketsReceived uint64
|
||||
LastUpdated time.Time
|
||||
}
|
||||
|
||||
// LinkStatus represents the current state of a link
|
||||
type LinkStatus struct {
|
||||
Established bool
|
||||
LastSeen time.Time
|
||||
RTT time.Duration
|
||||
Quality float64
|
||||
Hops uint8
|
||||
}
|
||||
|
||||
// PathRequest represents a path discovery request
|
||||
type PathRequest struct {
|
||||
DestinationHash []byte
|
||||
Tag []byte
|
||||
TTL int
|
||||
Recursive bool
|
||||
}
|
||||
|
||||
// PathResponse represents a path discovery response
|
||||
type PathResponse struct {
|
||||
DestinationHash []byte
|
||||
NextHop []byte
|
||||
Hops uint8
|
||||
Tag []byte
|
||||
}
|
||||
|
||||
@@ -1,49 +1,270 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"gopkg.in/yaml.v3"
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Identity struct {
|
||||
Name string `yaml:"name"`
|
||||
StoragePath string `yaml:"storage_path"`
|
||||
} `yaml:"identity"`
|
||||
Identity struct {
|
||||
Name string
|
||||
StoragePath string
|
||||
}
|
||||
|
||||
Interfaces []struct {
|
||||
Name string `yaml:"name"`
|
||||
Type string `yaml:"type"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
ListenPort int `yaml:"listen_port"`
|
||||
ListenIP string `yaml:"listen_ip"`
|
||||
KissFraming bool `yaml:"kiss_framing"`
|
||||
I2PTunneled bool `yaml:"i2p_tunneled"`
|
||||
} `yaml:"interfaces"`
|
||||
Interfaces []struct {
|
||||
Name string
|
||||
Type string
|
||||
Enabled bool
|
||||
ListenPort int
|
||||
ListenIP string
|
||||
KissFraming bool
|
||||
I2PTunneled bool
|
||||
}
|
||||
|
||||
Transport struct {
|
||||
AnnounceInterval int `yaml:"announce_interval"`
|
||||
PathRequestTimeout int `yaml:"path_request_timeout"`
|
||||
MaxHops int `yaml:"max_hops"`
|
||||
BitrateLimit int64 `yaml:"bitrate_limit"`
|
||||
} `yaml:"transport"`
|
||||
Transport struct {
|
||||
AnnounceInterval int
|
||||
PathRequestTimeout int
|
||||
MaxHops int
|
||||
BitrateLimit int64
|
||||
}
|
||||
|
||||
Logging struct {
|
||||
Level string `yaml:"level"`
|
||||
File string `yaml:"file"`
|
||||
} `yaml:"logging"`
|
||||
Logging struct {
|
||||
Level string
|
||||
File string
|
||||
}
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg := &Config{}
|
||||
scanner := bufio.NewScanner(file)
|
||||
var currentSection string
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
// Skip comments and empty lines
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle sections
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
currentSection = strings.Trim(line, "[]")
|
||||
|
||||
// If this is an interface section, append new interface
|
||||
if strings.HasPrefix(currentSection, "interface ") {
|
||||
cfg.Interfaces = append(cfg.Interfaces, struct {
|
||||
Name string
|
||||
Type string
|
||||
Enabled bool
|
||||
ListenPort int
|
||||
ListenIP string
|
||||
KissFraming bool
|
||||
I2PTunneled bool
|
||||
}{})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse key-value pairs
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
switch currentSection {
|
||||
case "identity":
|
||||
switch key {
|
||||
case "name":
|
||||
cfg.Identity.Name = value
|
||||
case "storage_path":
|
||||
cfg.Identity.StoragePath = value
|
||||
}
|
||||
|
||||
case "transport":
|
||||
switch key {
|
||||
case "announce_interval":
|
||||
cfg.Transport.AnnounceInterval, _ = strconv.Atoi(value)
|
||||
case "path_request_timeout":
|
||||
cfg.Transport.PathRequestTimeout, _ = strconv.Atoi(value)
|
||||
case "max_hops":
|
||||
cfg.Transport.MaxHops, _ = strconv.Atoi(value)
|
||||
case "bitrate_limit":
|
||||
cfg.Transport.BitrateLimit, _ = strconv.ParseInt(value, 10, 64)
|
||||
}
|
||||
|
||||
case "logging":
|
||||
switch key {
|
||||
case "level":
|
||||
cfg.Logging.Level = value
|
||||
case "file":
|
||||
cfg.Logging.File = value
|
||||
}
|
||||
|
||||
default:
|
||||
// Handle interface sections
|
||||
if strings.HasPrefix(currentSection, "interface ") && len(cfg.Interfaces) > 0 {
|
||||
iface := &cfg.Interfaces[len(cfg.Interfaces)-1]
|
||||
switch key {
|
||||
case "name":
|
||||
iface.Name = value
|
||||
case "type":
|
||||
iface.Type = value
|
||||
case "enabled":
|
||||
iface.Enabled = value == "true"
|
||||
case "listen_port":
|
||||
iface.ListenPort, _ = strconv.Atoi(value)
|
||||
case "listen_ip":
|
||||
iface.ListenIP = value
|
||||
case "kiss_framing":
|
||||
iface.KissFraming = value == "true"
|
||||
case "i2p_tunneled":
|
||||
iface.I2PTunneled = value == "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func SaveConfig(cfg *Config, path string) error {
|
||||
var builder strings.Builder
|
||||
|
||||
// Write Identity section
|
||||
builder.WriteString("[identity]\n")
|
||||
builder.WriteString(fmt.Sprintf("name = %s\n", cfg.Identity.Name))
|
||||
builder.WriteString(fmt.Sprintf("storage_path = %s\n\n", cfg.Identity.StoragePath))
|
||||
|
||||
// Write Transport section
|
||||
builder.WriteString("[transport]\n")
|
||||
builder.WriteString(fmt.Sprintf("announce_interval = %d\n", cfg.Transport.AnnounceInterval))
|
||||
builder.WriteString(fmt.Sprintf("path_request_timeout = %d\n", cfg.Transport.PathRequestTimeout))
|
||||
builder.WriteString(fmt.Sprintf("max_hops = %d\n", cfg.Transport.MaxHops))
|
||||
builder.WriteString(fmt.Sprintf("bitrate_limit = %d\n\n", cfg.Transport.BitrateLimit))
|
||||
|
||||
// Write Logging section
|
||||
builder.WriteString("[logging]\n")
|
||||
builder.WriteString(fmt.Sprintf("level = %s\n", cfg.Logging.Level))
|
||||
builder.WriteString(fmt.Sprintf("file = %s\n\n", cfg.Logging.File))
|
||||
|
||||
// Write Interface sections
|
||||
for _, iface := range cfg.Interfaces {
|
||||
builder.WriteString(fmt.Sprintf("[interface %s]\n", iface.Name))
|
||||
builder.WriteString(fmt.Sprintf("type = %s\n", iface.Type))
|
||||
builder.WriteString(fmt.Sprintf("enabled = %v\n", iface.Enabled))
|
||||
builder.WriteString(fmt.Sprintf("listen_port = %d\n", iface.ListenPort))
|
||||
builder.WriteString(fmt.Sprintf("listen_ip = %s\n", iface.ListenIP))
|
||||
builder.WriteString(fmt.Sprintf("kiss_framing = %v\n", iface.KissFraming))
|
||||
builder.WriteString(fmt.Sprintf("i2p_tunneled = %v\n\n", iface.I2PTunneled))
|
||||
}
|
||||
|
||||
return os.WriteFile(path, []byte(builder.String()), 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
63
pkg/cryptography/aes.go
Normal 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
|
||||
}
|
||||
22
pkg/cryptography/constants.go
Normal file
22
pkg/cryptography/constants.go
Normal 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)
|
||||
}
|
||||
25
pkg/cryptography/curve25519.go
Normal file
25
pkg/cryptography/curve25519.go
Normal 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)
|
||||
}
|
||||
18
pkg/cryptography/ed25519.go
Normal file
18
pkg/cryptography/ed25519.go
Normal 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
17
pkg/cryptography/hkdf.go
Normal 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
26
pkg/cryptography/hmac.go
Normal 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)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
@@ -30,6 +31,15 @@ const (
|
||||
|
||||
RATCHET_COUNT = 512 // Default number of retained ratchet keys
|
||||
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
|
||||
@@ -44,39 +54,41 @@ type RequestHandler struct {
|
||||
}
|
||||
|
||||
type Destination struct {
|
||||
identity *identity.Identity
|
||||
direction byte
|
||||
destType byte
|
||||
appName string
|
||||
aspects []string
|
||||
hash []byte
|
||||
|
||||
acceptsLinks bool
|
||||
proofStrategy byte
|
||||
|
||||
packetCallback PacketCallback
|
||||
proofCallback ProofRequestedCallback
|
||||
linkCallback LinkEstablishedCallback
|
||||
|
||||
identity *identity.Identity
|
||||
direction byte
|
||||
destType byte
|
||||
appName string
|
||||
aspects []string
|
||||
hashValue []byte
|
||||
|
||||
acceptsLinks bool
|
||||
proofStrategy byte
|
||||
|
||||
packetCallback PacketCallback
|
||||
proofCallback ProofRequestedCallback
|
||||
linkCallback LinkEstablishedCallback
|
||||
|
||||
ratchetsEnabled bool
|
||||
ratchetPath string
|
||||
ratchetCount int
|
||||
ratchetInterval int
|
||||
enforceRatchets bool
|
||||
|
||||
defaultAppData []byte
|
||||
|
||||
defaultAppData []byte
|
||||
mutex sync.RWMutex
|
||||
|
||||
requestHandlers map[string]*RequestHandler
|
||||
callbacks struct {
|
||||
packetReceived common.PacketCallback
|
||||
proofRequested common.ProofRequestedCallback
|
||||
linkEstablished common.LinkEstablishedCallback
|
||||
}
|
||||
}
|
||||
|
||||
func debugLog(level int, format string, v ...interface{}) {
|
||||
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) {
|
||||
debugLog(DEBUG_INFO, "Creating new destination: app=%s type=%d direction=%d", appName, destType, direction)
|
||||
|
||||
if id == nil {
|
||||
debugLog(DEBUG_ERROR, "Cannot create destination: identity is 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
|
||||
d.hash = d.Hash()
|
||||
|
||||
d.hashValue = d.calculateHash()
|
||||
debugLog(DEBUG_VERBOSE, "Created destination with hash: %x", d.hashValue)
|
||||
|
||||
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()))
|
||||
identityHash := sha256.Sum256(d.identity.GetPublicKey())
|
||||
|
||||
|
||||
debugLog(DEBUG_ALL, "Name hash: %x", nameHash)
|
||||
debugLog(DEBUG_ALL, "Identity hash: %x", identityHash)
|
||||
|
||||
combined := append(nameHash[:], identityHash[:]...)
|
||||
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 {
|
||||
@@ -121,69 +142,60 @@ func (d *Destination) Announce(appData []byte) error {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
log.Printf("[DEBUG-4] Creating announce packet for destination %s", d.ExpandName())
|
||||
|
||||
// If no specific appData provided, use default
|
||||
if appData == nil {
|
||||
log.Printf("[DEBUG-4] Using default app data for announce")
|
||||
appData = d.defaultAppData
|
||||
}
|
||||
|
||||
// Create announce packet
|
||||
packet := make([]byte, 0)
|
||||
packet := make([]byte, 0, 256) // Pre-allocate reasonable size
|
||||
|
||||
// Add destination hash
|
||||
packet = append(packet, d.hash...)
|
||||
// Add packet type and header
|
||||
packet = append(packet, 0x01) // PACKET_TYPE_ANNOUNCE
|
||||
packet = append(packet, 0x00) // Initial hop count
|
||||
|
||||
// Add identity public key
|
||||
packet = append(packet, d.identity.GetPublicKey()...)
|
||||
// Add destination hash (16 bytes)
|
||||
packet = append(packet, d.hashValue...)
|
||||
log.Printf("[DEBUG-4] Added destination hash %x to announce", d.hashValue[:8])
|
||||
|
||||
// Add flags byte
|
||||
flags := byte(0)
|
||||
if d.acceptsLinks {
|
||||
flags |= 0x01
|
||||
}
|
||||
if d.ratchetsEnabled {
|
||||
flags |= 0x02
|
||||
}
|
||||
packet = append(packet, flags)
|
||||
// Add identity public key (32 bytes)
|
||||
pubKey := d.identity.GetPublicKey()
|
||||
packet = append(packet, pubKey...)
|
||||
log.Printf("[DEBUG-4] Added public key %x to announce", pubKey[:8])
|
||||
|
||||
// Add proof strategy
|
||||
packet = append(packet, d.proofStrategy)
|
||||
|
||||
// Add app data length and data if present
|
||||
if appData != nil {
|
||||
appDataLen := uint16(len(appData))
|
||||
lenBytes := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(lenBytes, appDataLen)
|
||||
packet = append(packet, lenBytes...)
|
||||
packet = append(packet, appData...)
|
||||
} else {
|
||||
// No app data
|
||||
packet = append(packet, 0x00, 0x00)
|
||||
}
|
||||
// Add app data with length prefix
|
||||
appDataLen := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(appDataLen, uint16(len(appData)))
|
||||
packet = append(packet, appDataLen...)
|
||||
packet = append(packet, appData...)
|
||||
log.Printf("[DEBUG-4] Added %d bytes of app data to announce", len(appData))
|
||||
|
||||
// Add ratchet data if enabled
|
||||
if d.ratchetsEnabled {
|
||||
// Add ratchet interval
|
||||
intervalBytes := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(intervalBytes, uint32(d.ratchetInterval))
|
||||
packet = append(packet, intervalBytes...)
|
||||
|
||||
// Add current ratchet key
|
||||
log.Printf("[DEBUG-4] Adding ratchet data to announce")
|
||||
ratchetKey := d.identity.GetCurrentRatchetKey()
|
||||
if ratchetKey == nil {
|
||||
log.Printf("[DEBUG-3] Failed to get current ratchet key")
|
||||
return errors.New("failed to get current ratchet key")
|
||||
}
|
||||
packet = append(packet, ratchetKey...)
|
||||
log.Printf("[DEBUG-4] Added ratchet key %x to announce", ratchetKey[:8])
|
||||
}
|
||||
|
||||
// Sign the announce packet
|
||||
signature, err := d.Sign(packet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sign announce packet: %w", err)
|
||||
// Sign the announce packet (64 bytes)
|
||||
signData := append(d.hashValue, appData...)
|
||||
if d.ratchetsEnabled {
|
||||
signData = append(signData, d.identity.GetCurrentRatchetKey()...)
|
||||
}
|
||||
signature := d.identity.Sign(signData)
|
||||
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
|
||||
// This will need to be implemented in the transport package
|
||||
// Send announce packet through transport
|
||||
log.Printf("[DEBUG-4] Sending announce packet through transport layer")
|
||||
return transport.SendAnnounce(packet)
|
||||
}
|
||||
|
||||
@@ -220,7 +232,7 @@ func (d *Destination) SetProofStrategy(strategy byte) {
|
||||
func (d *Destination) EnableRatchets(path string) bool {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
|
||||
d.ratchetsEnabled = true
|
||||
d.ratchetPath = path
|
||||
return true
|
||||
@@ -236,7 +248,7 @@ func (d *Destination) SetRetainedRatchets(count int) bool {
|
||||
if count < 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
d.ratchetCount = count
|
||||
@@ -247,7 +259,7 @@ func (d *Destination) SetRatchetInterval(interval int) bool {
|
||||
if interval < 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
d.ratchetInterval = interval
|
||||
@@ -275,7 +287,7 @@ func (d *Destination) RegisterRequestHandler(path string, responseGen func(strin
|
||||
return errors.New("invalid allow mode")
|
||||
}
|
||||
|
||||
if allow == ALLOW_LIST && (allowedList == nil || len(allowedList) == 0) {
|
||||
if allow == ALLOW_LIST && len(allowedList) == 0 {
|
||||
return errors.New("allowed list required for ALLOW_LIST mode")
|
||||
}
|
||||
|
||||
@@ -305,19 +317,32 @@ func (d *Destination) DeregisterRequestHandler(path string) bool {
|
||||
|
||||
func (d *Destination) Encrypt(plaintext []byte) ([]byte, error) {
|
||||
if d.destType == PLAIN {
|
||||
log.Printf("[DEBUG-4] Using plaintext transmission for PLAIN destination")
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
if d.identity == nil {
|
||||
log.Printf("[DEBUG-3] Cannot encrypt: no identity available")
|
||||
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 {
|
||||
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:
|
||||
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:
|
||||
log.Printf("[DEBUG-3] Unsupported destination type %d for encryption", d.destType)
|
||||
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")
|
||||
}
|
||||
|
||||
switch d.destType {
|
||||
case SINGLE:
|
||||
return d.identity.Decrypt(ciphertext, nil)
|
||||
case GROUP:
|
||||
return d.identity.DecryptSymmetric(ciphertext)
|
||||
default:
|
||||
return nil, errors.New("unsupported destination type for decryption")
|
||||
}
|
||||
// Create empty ratchet receiver to get latest ratchet ID if available
|
||||
ratchetReceiver := &common.RatchetIDReceiver{}
|
||||
|
||||
// Call Decrypt with full parameter list:
|
||||
// - ciphertext: the encrypted data
|
||||
// - ratchets: nil since we're not providing specific ratchets
|
||||
// - enforceRatchets: false to allow fallback to normal decryption
|
||||
// - ratchetIDReceiver: to receive the latest ratchet ID used
|
||||
return d.identity.Decrypt(ciphertext, nil, false, ratchetReceiver)
|
||||
}
|
||||
|
||||
func (d *Destination) Sign(data []byte) ([]byte, error) {
|
||||
@@ -347,4 +373,33 @@ func (d *Destination) Sign(data []byte) ([]byte, error) {
|
||||
}
|
||||
signature := d.identity.Sign(data)
|
||||
return signature, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Destination) GetPublicKey() []byte {
|
||||
if d.identity == nil {
|
||||
return nil
|
||||
}
|
||||
return d.identity.GetPublicKey()
|
||||
}
|
||||
|
||||
func (d *Destination) GetIdentity() *identity.Identity {
|
||||
return d.identity
|
||||
}
|
||||
|
||||
func (d *Destination) GetType() byte {
|
||||
return d.destType
|
||||
}
|
||||
|
||||
func (d *Destination) GetHash() []byte {
|
||||
d.mutex.RLock()
|
||||
defer d.mutex.RUnlock()
|
||||
if d.hashValue == nil {
|
||||
d.mutex.RUnlock()
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
if d.hashValue == nil {
|
||||
d.hashValue = d.calculateHash()
|
||||
}
|
||||
}
|
||||
return d.hashValue
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
277
pkg/interfaces/auto.go
Normal file
277
pkg/interfaces/auto.go
Normal 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
|
||||
}
|
||||
@@ -1,21 +1,102 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
)
|
||||
|
||||
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 {
|
||||
common.BaseInterface
|
||||
Name string
|
||||
Mode common.InterfaceMode
|
||||
Type common.InterfaceType
|
||||
Online bool
|
||||
Enabled bool
|
||||
Detached bool
|
||||
IN bool
|
||||
OUT bool
|
||||
MTU int
|
||||
Bitrate int64
|
||||
TxBytes uint64
|
||||
RxBytes uint64
|
||||
lastTx time.Time
|
||||
|
||||
mutex sync.RWMutex
|
||||
packetCallback common.PacketCallback
|
||||
}
|
||||
|
||||
func NewBaseInterface(name string, ifType common.InterfaceType, enabled bool) BaseInterface {
|
||||
return BaseInterface{
|
||||
Name: name,
|
||||
Mode: common.IF_MODE_FULL,
|
||||
Type: ifType,
|
||||
Online: false,
|
||||
Enabled: enabled,
|
||||
Detached: false,
|
||||
IN: false,
|
||||
OUT: false,
|
||||
MTU: common.DEFAULT_MTU,
|
||||
Bitrate: BITRATE_MINIMUM,
|
||||
lastTx: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||
@@ -24,28 +105,38 @@ func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||
i.packetCallback = callback
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetPacketCallback() common.PacketCallback {
|
||||
i.mutex.RLock()
|
||||
defer i.mutex.RUnlock()
|
||||
return i.packetCallback
|
||||
}
|
||||
|
||||
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
||||
i.mutex.Lock()
|
||||
i.RxBytes += uint64(len(data))
|
||||
i.mutex.Unlock()
|
||||
|
||||
i.mutex.RLock()
|
||||
callback := i.packetCallback
|
||||
i.mutex.RUnlock()
|
||||
|
||||
|
||||
if callback != nil {
|
||||
callback(data, i)
|
||||
}
|
||||
|
||||
i.RxBytes += uint64(len(data))
|
||||
}
|
||||
|
||||
func (i *BaseInterface) ProcessOutgoing(data []byte) error {
|
||||
i.TxBytes += uint64(len(data))
|
||||
return nil
|
||||
}
|
||||
if !i.Online || i.Detached {
|
||||
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()
|
||||
defer i.mutex.Unlock()
|
||||
i.Detached = true
|
||||
i.Online = false
|
||||
i.TxBytes += uint64(len(data))
|
||||
i.mutex.Unlock()
|
||||
|
||||
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 {
|
||||
@@ -53,7 +144,7 @@ func (i *BaseInterface) SendPathRequest(packet []byte) error {
|
||||
return fmt.Errorf("interface offline or detached")
|
||||
}
|
||||
|
||||
frame := make([]byte, 0, len(packet)+2)
|
||||
frame := make([]byte, 0, len(packet)+1)
|
||||
frame = append(frame, 0x01)
|
||||
frame = append(frame, packet...)
|
||||
|
||||
@@ -68,12 +159,156 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
|
||||
frame := make([]byte, 0, len(dest)+len(data)+9)
|
||||
frame = append(frame, 0x02)
|
||||
frame = append(frame, dest...)
|
||||
|
||||
|
||||
ts := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix()))
|
||||
frame = append(frame, ts...)
|
||||
|
||||
frame = append(frame, data...)
|
||||
|
||||
return i.ProcessOutgoing(frame)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Detach() {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
i.Detached = true
|
||||
i.Online = false
|
||||
}
|
||||
|
||||
func (i *BaseInterface) IsEnabled() bool {
|
||||
i.mutex.RLock()
|
||||
defer i.mutex.RUnlock()
|
||||
return i.Enabled && i.Online && !i.Detached
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Enable() {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
|
||||
prevState := i.Enabled
|
||||
i.Enabled = true
|
||||
i.Online = true
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,13 @@ package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -17,103 +21,102 @@ const (
|
||||
KISS_TFEND = 0xDC
|
||||
KISS_TFESC = 0xDD
|
||||
|
||||
TCP_USER_TIMEOUT = 24
|
||||
TCP_PROBE_AFTER = 5
|
||||
TCP_PROBE_INTERVAL = 2
|
||||
TCP_USER_TIMEOUT = 24
|
||||
TCP_PROBE_AFTER = 5
|
||||
TCP_PROBE_INTERVAL = 2
|
||||
TCP_PROBES = 12
|
||||
RECONNECT_WAIT = 5
|
||||
INITIAL_TIMEOUT = 5
|
||||
INITIAL_BACKOFF = time.Second
|
||||
MAX_BACKOFF = time.Minute * 5
|
||||
)
|
||||
|
||||
type TCPClientInterface struct {
|
||||
Interface
|
||||
conn net.Conn
|
||||
targetAddr string
|
||||
targetPort int
|
||||
kissFraming bool
|
||||
i2pTunneled bool
|
||||
initiator bool
|
||||
reconnecting bool
|
||||
neverConnected bool
|
||||
writing bool
|
||||
BaseInterface
|
||||
conn net.Conn
|
||||
targetAddr string
|
||||
targetPort int
|
||||
kissFraming bool
|
||||
i2pTunneled bool
|
||||
initiator bool
|
||||
reconnecting bool
|
||||
neverConnected bool
|
||||
writing bool
|
||||
maxReconnectTries int
|
||||
packetBuffer []byte
|
||||
packetType byte
|
||||
packetBuffer []byte
|
||||
packetType byte
|
||||
mutex sync.RWMutex
|
||||
enabled bool
|
||||
TxBytes uint64
|
||||
RxBytes uint64
|
||||
lastTx time.Time
|
||||
lastRx time.Time
|
||||
}
|
||||
|
||||
func NewTCPClient(name string, targetAddr string, targetPort int, kissFraming bool, i2pTunneled bool) (*TCPClientInterface, error) {
|
||||
func NewTCPClientInterface(name string, targetHost string, targetPort int, kissFraming bool, i2pTunneled bool, enabled bool) (*TCPClientInterface, error) {
|
||||
tc := &TCPClientInterface{
|
||||
Interface: Interface{
|
||||
Name: name,
|
||||
Mode: MODE_FULL,
|
||||
MTU: 1064,
|
||||
Bitrate: 10000000, // 10Mbps estimate
|
||||
},
|
||||
targetAddr: targetAddr,
|
||||
targetPort: targetPort,
|
||||
kissFraming: kissFraming,
|
||||
i2pTunneled: i2pTunneled,
|
||||
initiator: true,
|
||||
BaseInterface: NewBaseInterface(name, common.IF_TYPE_TCP, enabled),
|
||||
targetAddr: targetHost,
|
||||
targetPort: targetPort,
|
||||
kissFraming: kissFraming,
|
||||
i2pTunneled: i2pTunneled,
|
||||
initiator: true,
|
||||
enabled: enabled,
|
||||
maxReconnectTries: TCP_PROBES,
|
||||
packetBuffer: make([]byte, 0),
|
||||
neverConnected: true,
|
||||
}
|
||||
|
||||
if err := tc.connect(true); err != nil {
|
||||
go tc.reconnect()
|
||||
} else {
|
||||
if enabled {
|
||||
addr := fmt.Sprintf("%s:%d", targetHost, targetPort)
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tc.conn = conn
|
||||
tc.Online = true
|
||||
go tc.readLoop()
|
||||
}
|
||||
|
||||
return tc, nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) connect(initial bool) error {
|
||||
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)
|
||||
conn, err := net.DialTimeout("tcp", addr, time.Second*INITIAL_TIMEOUT)
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
if initial {
|
||||
return fmt.Errorf("initial connection failed: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
tc.conn = conn
|
||||
tc.Online = true
|
||||
tc.writing = false
|
||||
tc.neverConnected = false
|
||||
|
||||
// Set TCP options
|
||||
if tcpConn, ok := conn.(*net.TCPConn); ok {
|
||||
tcpConn.SetNoDelay(true)
|
||||
tcpConn.SetKeepAlive(true)
|
||||
tcpConn.SetKeepAlivePeriod(time.Second * TCP_PROBE_INTERVAL)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) reconnect() {
|
||||
if tc.initiator && !tc.reconnecting {
|
||||
tc.reconnecting = true
|
||||
attempts := 0
|
||||
|
||||
for !tc.Online {
|
||||
time.Sleep(time.Second * RECONNECT_WAIT)
|
||||
attempts++
|
||||
|
||||
if tc.maxReconnectTries > 0 && attempts > tc.maxReconnectTries {
|
||||
tc.teardown()
|
||||
break
|
||||
}
|
||||
|
||||
if err := tc.connect(false); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
go tc.readLoop()
|
||||
break
|
||||
// Set platform-specific timeouts
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
if err := tc.setTimeoutsLinux(); err != nil {
|
||||
log.Printf("[DEBUG-2] Failed to set Linux TCP timeouts: %v", err)
|
||||
}
|
||||
case "darwin":
|
||||
if err := tc.setTimeoutsOSX(); err != nil {
|
||||
log.Printf("[DEBUG-2] Failed to set OSX TCP timeouts: %v", err)
|
||||
}
|
||||
|
||||
tc.reconnecting = false
|
||||
}
|
||||
|
||||
tc.Online = true
|
||||
go tc.readLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) readLoop() {
|
||||
@@ -134,26 +137,31 @@ func (tc *TCPClientInterface) readLoop() {
|
||||
return
|
||||
}
|
||||
|
||||
// Update RX bytes for raw received data
|
||||
tc.UpdateStats(uint64(n), true)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
b := buffer[i]
|
||||
|
||||
if tc.kissFraming {
|
||||
// KISS framing logic
|
||||
if inFrame && b == KISS_FEND {
|
||||
inFrame = false
|
||||
tc.handlePacket(dataBuffer)
|
||||
dataBuffer = dataBuffer[:0]
|
||||
} else if b == KISS_FEND {
|
||||
inFrame = true
|
||||
} else if inFrame {
|
||||
if b == KISS_FEND {
|
||||
if inFrame && len(dataBuffer) > 0 {
|
||||
tc.handlePacket(dataBuffer)
|
||||
dataBuffer = dataBuffer[:0]
|
||||
}
|
||||
inFrame = !inFrame
|
||||
continue
|
||||
}
|
||||
|
||||
if inFrame {
|
||||
if b == KISS_FESC {
|
||||
escape = true
|
||||
} else {
|
||||
if escape {
|
||||
if b == KISS_TFEND {
|
||||
b = KISS_FEND
|
||||
}
|
||||
if b == KISS_TFESC {
|
||||
} else if b == KISS_TFESC {
|
||||
b = KISS_FESC
|
||||
}
|
||||
escape = false
|
||||
@@ -163,13 +171,16 @@ func (tc *TCPClientInterface) readLoop() {
|
||||
}
|
||||
} else {
|
||||
// HDLC framing logic
|
||||
if inFrame && b == HDLC_FLAG {
|
||||
inFrame = false
|
||||
tc.handlePacket(dataBuffer)
|
||||
dataBuffer = dataBuffer[:0]
|
||||
} else if b == HDLC_FLAG {
|
||||
inFrame = true
|
||||
} else if inFrame {
|
||||
if b == HDLC_FLAG {
|
||||
if inFrame && len(dataBuffer) > 0 {
|
||||
tc.handlePacket(dataBuffer)
|
||||
dataBuffer = dataBuffer[:0]
|
||||
}
|
||||
inFrame = !inFrame
|
||||
continue
|
||||
}
|
||||
|
||||
if inFrame {
|
||||
if b == HDLC_ESC {
|
||||
escape = true
|
||||
} else {
|
||||
@@ -187,20 +198,40 @@ func (tc *TCPClientInterface) readLoop() {
|
||||
|
||||
func (tc *TCPClientInterface) handlePacket(data []byte) {
|
||||
if len(data) < 1 {
|
||||
log.Printf("[DEBUG-7] Received invalid packet: empty")
|
||||
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:]
|
||||
|
||||
switch packetType {
|
||||
case 0x01: // Path request
|
||||
tc.Interface.ProcessIncoming(payload)
|
||||
switch tc.packetType {
|
||||
case 0x01: // Announce packet
|
||||
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
|
||||
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
|
||||
}
|
||||
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:
|
||||
// Unknown packet type
|
||||
return
|
||||
@@ -224,13 +255,11 @@ func (tc *TCPClientInterface) ProcessOutgoing(data []byte) error {
|
||||
frame = append(frame, HDLC_FLAG)
|
||||
}
|
||||
|
||||
if _, err := tc.conn.Write(frame); err != nil {
|
||||
tc.teardown()
|
||||
return fmt.Errorf("write failed: %v", err)
|
||||
}
|
||||
// Update TX stats before sending
|
||||
tc.UpdateStats(uint64(len(frame)), false)
|
||||
|
||||
tc.Interface.ProcessOutgoing(data)
|
||||
return nil
|
||||
_, err := tc.conn.Write(frame)
|
||||
return err
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) teardown() {
|
||||
@@ -269,130 +298,237 @@ func escapeKISS(data []byte) []byte {
|
||||
return escaped
|
||||
}
|
||||
|
||||
type TCPServerInterface struct {
|
||||
Interface
|
||||
server net.Listener
|
||||
bindAddr string
|
||||
bindPort int
|
||||
i2pTunneled bool
|
||||
preferIPv6 bool
|
||||
spawned []*TCPClientInterface
|
||||
spawnedMutex sync.RWMutex
|
||||
func (tc *TCPClientInterface) SetPacketCallback(cb common.PacketCallback) {
|
||||
tc.packetCallback = cb
|
||||
}
|
||||
|
||||
func NewTCPServer(name string, bindAddr string, bindPort int, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) {
|
||||
ts := &TCPServerInterface{
|
||||
Interface: Interface{
|
||||
Name: name,
|
||||
Mode: MODE_FULL,
|
||||
MTU: 1064,
|
||||
Bitrate: 10000000, // 10Mbps estimate
|
||||
},
|
||||
bindAddr: bindAddr,
|
||||
bindPort: bindPort,
|
||||
i2pTunneled: i2pTunneled,
|
||||
preferIPv6: preferIPv6,
|
||||
spawned: make([]*TCPClientInterface, 0),
|
||||
}
|
||||
|
||||
// Resolve bind address
|
||||
var addr string
|
||||
if ts.bindAddr == "" {
|
||||
if ts.preferIPv6 {
|
||||
addr = fmt.Sprintf("[::0]:%d", ts.bindPort)
|
||||
} else {
|
||||
addr = fmt.Sprintf("0.0.0.0:%d", ts.bindPort)
|
||||
}
|
||||
} else {
|
||||
addr = fmt.Sprintf("%s:%d", ts.bindAddr, ts.bindPort)
|
||||
}
|
||||
|
||||
// Create listener
|
||||
var err error
|
||||
ts.server, err = net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create TCP listener: %v", err)
|
||||
}
|
||||
|
||||
ts.Online = true
|
||||
ts.IN = true
|
||||
|
||||
// Start accept loop
|
||||
go ts.acceptLoop()
|
||||
|
||||
return ts, nil
|
||||
func (tc *TCPClientInterface) IsEnabled() bool {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
return tc.enabled && tc.Online && !tc.Detached
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) acceptLoop() {
|
||||
for {
|
||||
conn, err := ts.server.Accept()
|
||||
if err != nil {
|
||||
if !ts.Detached {
|
||||
// Log error and continue accepting
|
||||
continue
|
||||
}
|
||||
func (tc *TCPClientInterface) GetName() string {
|
||||
return tc.Name
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) GetPacketCallback() common.PacketCallback {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
return tc.packetCallback
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) IsDetached() bool {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
return tc.Detached
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) IsOnline() bool {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
return tc.Online
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) reconnect() {
|
||||
tc.mutex.Lock()
|
||||
if tc.reconnecting {
|
||||
tc.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
tc.reconnecting = true
|
||||
tc.mutex.Unlock()
|
||||
|
||||
backoff := time.Second
|
||||
maxBackoff := time.Minute * 5
|
||||
retries := 0
|
||||
|
||||
for retries < tc.maxReconnectTries {
|
||||
tc.teardown()
|
||||
|
||||
addr := 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
|
||||
}
|
||||
|
||||
// Create new client interface for this connection
|
||||
client := &TCPClientInterface{
|
||||
Interface: Interface{
|
||||
Name: fmt.Sprintf("Client-%s-%s", ts.Name, conn.RemoteAddr()),
|
||||
Mode: ts.Mode,
|
||||
MTU: ts.MTU,
|
||||
},
|
||||
conn: conn,
|
||||
i2pTunneled: ts.i2pTunneled,
|
||||
// Log reconnection attempt
|
||||
fmt.Printf("Failed to reconnect to %s (attempt %d/%d): %v\n",
|
||||
addr, retries+1, tc.maxReconnectTries, err)
|
||||
|
||||
// Wait with exponential backoff
|
||||
time.Sleep(backoff)
|
||||
|
||||
// Increase backoff time exponentially
|
||||
backoff *= 2
|
||||
if backoff > maxBackoff {
|
||||
backoff = maxBackoff
|
||||
}
|
||||
|
||||
// Configure TCP options
|
||||
if tcpConn, ok := conn.(*net.TCPConn); ok {
|
||||
tcpConn.SetNoDelay(true)
|
||||
tcpConn.SetKeepAlive(true)
|
||||
tcpConn.SetKeepAlivePeriod(time.Duration(TCP_PROBE_INTERVAL) * time.Second)
|
||||
retries++
|
||||
}
|
||||
|
||||
tc.mutex.Lock()
|
||||
tc.reconnecting = false
|
||||
tc.mutex.Unlock()
|
||||
|
||||
// If we've exhausted all retries, perform final teardown
|
||||
tc.teardown()
|
||||
fmt.Printf("Failed to reconnect to %s after %d attempts\n",
|
||||
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
|
||||
client.IN = ts.IN
|
||||
client.OUT = ts.OUT
|
||||
return 0
|
||||
}
|
||||
|
||||
// Add to spawned interfaces
|
||||
ts.spawnedMutex.Lock()
|
||||
ts.spawned = append(ts.spawned, client)
|
||||
ts.spawnedMutex.Unlock()
|
||||
func (tc *TCPClientInterface) GetTxBytes() uint64 {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
return tc.TxBytes
|
||||
}
|
||||
|
||||
// Start client read loop
|
||||
go client.readLoop()
|
||||
func (tc *TCPClientInterface) GetRxBytes() uint64 {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
return tc.RxBytes
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) UpdateStats(bytes uint64, isRx bool) {
|
||||
tc.mutex.Lock()
|
||||
defer tc.mutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
if isRx {
|
||||
tc.RxBytes += bytes
|
||||
tc.lastRx = now
|
||||
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() {
|
||||
ts.Interface.Detach()
|
||||
|
||||
if ts.server != nil {
|
||||
ts.server.Close()
|
||||
}
|
||||
|
||||
ts.spawnedMutex.Lock()
|
||||
for _, client := range ts.spawned {
|
||||
client.Detach()
|
||||
}
|
||||
ts.spawned = nil
|
||||
ts.spawnedMutex.Unlock()
|
||||
func (tc *TCPClientInterface) GetStats() (tx uint64, rx uint64, lastTx time.Time, lastRx time.Time) {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
return tc.TxBytes, tc.RxBytes, tc.lastTx, tc.lastRx
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
|
||||
ts.spawnedMutex.RLock()
|
||||
defer ts.spawnedMutex.RUnlock()
|
||||
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, client := range ts.spawned {
|
||||
if err := client.ProcessOutgoing(data); err != nil {
|
||||
lastErr = err
|
||||
if !tc.i2pTunneled {
|
||||
if err := tcpConn.SetKeepAlive(true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tcpConn.SetKeepAlivePeriod(time.Duration(TCP_PROBE_INTERVAL) * time.Second); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return lastErr
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlive(true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type TCPServerInterface struct {
|
||||
BaseInterface
|
||||
connections map[string]net.Conn
|
||||
mutex sync.RWMutex
|
||||
bindAddr string
|
||||
bindPort int
|
||||
preferIPv6 bool
|
||||
kissFraming bool
|
||||
i2pTunneled bool
|
||||
packetCallback common.PacketCallback
|
||||
TxBytes uint64
|
||||
RxBytes uint64
|
||||
}
|
||||
|
||||
func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFraming bool, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) {
|
||||
ts := &TCPServerInterface{
|
||||
BaseInterface: BaseInterface{
|
||||
Name: name,
|
||||
Mode: common.IF_MODE_FULL,
|
||||
Type: common.IF_TYPE_TCP,
|
||||
Online: false,
|
||||
MTU: common.DEFAULT_MTU,
|
||||
Detached: false,
|
||||
},
|
||||
connections: make(map[string]net.Conn),
|
||||
bindAddr: bindAddr,
|
||||
bindPort: bindPort,
|
||||
preferIPv6: preferIPv6,
|
||||
kissFraming: kissFraming,
|
||||
i2pTunneled: i2pTunneled,
|
||||
}
|
||||
|
||||
return ts, nil
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) String() string {
|
||||
@@ -401,8 +537,165 @@ func (ts *TCPServerInterface) String() string {
|
||||
if ts.preferIPv6 {
|
||||
addr = "[::0]"
|
||||
} else {
|
||||
addr = "0.0.0.0"
|
||||
addr = "0.0.0.0"
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("TCPServerInterface[%s/%s:%d]", ts.Name, addr, ts.bindPort)
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
ts.packetCallback = callback
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) GetPacketCallback() common.PacketCallback {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
return ts.packetCallback
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) IsEnabled() bool {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
return ts.BaseInterface.Enabled && ts.BaseInterface.Online && !ts.BaseInterface.Detached
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) GetName() string {
|
||||
return ts.Name
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) IsDetached() bool {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
return ts.BaseInterface.Detached
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) IsOnline() bool {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
return ts.Online
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) Enable() {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
ts.Online = true
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) Disable() {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
ts.Online = false
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) Start() error {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
|
||||
addr := 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
|
||||
}
|
||||
|
||||
14
pkg/interfaces/tcp_common.go
Normal file
14
pkg/interfaces/tcp_common.go
Normal 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
|
||||
}
|
||||
32
pkg/interfaces/tcp_linux.go
Normal file
32
pkg/interfaces/tcp_linux.go
Normal 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
|
||||
}
|
||||
@@ -2,87 +2,117 @@ package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
)
|
||||
|
||||
type UDPInterface struct {
|
||||
Interface
|
||||
conn *net.UDPConn
|
||||
listenAddr *net.UDPAddr
|
||||
BaseInterface
|
||||
conn *net.UDPConn
|
||||
addr *net.UDPAddr
|
||||
targetAddr *net.UDPAddr
|
||||
mutex sync.RWMutex
|
||||
readBuffer []byte
|
||||
}
|
||||
|
||||
func NewUDPInterface(name string, listenAddr string, targetAddr string) (*UDPInterface, error) {
|
||||
ui := &UDPInterface{
|
||||
Interface: Interface{
|
||||
Name: name,
|
||||
Mode: MODE_FULL,
|
||||
MTU: 1500,
|
||||
Bitrate: 100000000, // 100Mbps estimate for UDP
|
||||
},
|
||||
readBuffer: make([]byte, 65535),
|
||||
}
|
||||
|
||||
// Parse listen address
|
||||
laddr, err := net.ResolveUDPAddr("udp", listenAddr)
|
||||
func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) {
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid listen address: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
ui.listenAddr = laddr
|
||||
|
||||
// Parse target address if provided
|
||||
if targetAddr != "" {
|
||||
taddr, err := net.ResolveUDPAddr("udp", targetAddr)
|
||||
var targetAddr *net.UDPAddr
|
||||
if target != "" {
|
||||
targetAddr, err = net.ResolveUDPAddr("udp", target)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid target address: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
ui.targetAddr = taddr
|
||||
ui.OUT = true
|
||||
}
|
||||
|
||||
// Create UDP connection
|
||||
conn, err := net.ListenUDP("udp", ui.listenAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to listen on UDP: %v", err)
|
||||
ui := &UDPInterface{
|
||||
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
|
||||
addr: udpAddr,
|
||||
targetAddr: targetAddr,
|
||||
readBuffer: make([]byte, common.DEFAULT_MTU),
|
||||
}
|
||||
ui.conn = conn
|
||||
ui.IN = true
|
||||
ui.Online = true
|
||||
|
||||
// Start read loop
|
||||
go ui.readLoop()
|
||||
|
||||
return ui, nil
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) readLoop() {
|
||||
for {
|
||||
if !ui.Online {
|
||||
return
|
||||
}
|
||||
func (ui *UDPInterface) GetName() string {
|
||||
return ui.Name
|
||||
}
|
||||
|
||||
n, addr, err := ui.conn.ReadFromUDP(ui.readBuffer)
|
||||
if err != nil {
|
||||
if !ui.Detached {
|
||||
// Log error
|
||||
}
|
||||
continue
|
||||
}
|
||||
func (ui *UDPInterface) GetType() common.InterfaceType {
|
||||
return ui.Type
|
||||
}
|
||||
|
||||
// Copy received data
|
||||
data := make([]byte, n)
|
||||
copy(data, ui.readBuffer[:n])
|
||||
func (ui *UDPInterface) GetMode() common.InterfaceMode {
|
||||
return ui.Mode
|
||||
}
|
||||
|
||||
// Process packet
|
||||
ui.ProcessIncoming(data)
|
||||
func (ui *UDPInterface) IsOnline() bool {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
return ui.Online
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) IsDetached() bool {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
return ui.Detached
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Detach() {
|
||||
ui.mutex.Lock()
|
||||
defer ui.mutex.Unlock()
|
||||
ui.Detached = true
|
||||
if ui.conn != nil {
|
||||
ui.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if !ui.Online || ui.targetAddr == nil {
|
||||
return fmt.Errorf("interface offline or no target address configured")
|
||||
if !ui.IsOnline() {
|
||||
return fmt.Errorf("interface offline")
|
||||
}
|
||||
|
||||
if ui.targetAddr == nil {
|
||||
return fmt.Errorf("no target address configured")
|
||||
}
|
||||
|
||||
_, err := ui.conn.WriteToUDP(data, ui.targetAddr)
|
||||
@@ -90,13 +120,88 @@ func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
|
||||
return fmt.Errorf("UDP write failed: %v", err)
|
||||
}
|
||||
|
||||
ui.Interface.ProcessOutgoing(data)
|
||||
ui.mutex.Lock()
|
||||
ui.TxBytes += uint64(len(data))
|
||||
ui.mutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Detach() {
|
||||
ui.Interface.Detach()
|
||||
if ui.conn != nil {
|
||||
ui.conn.Close()
|
||||
func (ui *UDPInterface) GetConn() net.Conn {
|
||||
return ui.conn
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetTxBytes() uint64 {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
return ui.TxBytes
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetRxBytes() uint64 {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
return ui.RxBytes
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetMTU() int {
|
||||
return ui.MTU
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetBitrate() int {
|
||||
return int(ui.Bitrate)
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Enable() {
|
||||
ui.mutex.Lock()
|
||||
defer ui.mutex.Unlock()
|
||||
ui.Online = true
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Disable() {
|
||||
ui.mutex.Lock()
|
||||
defer ui.mutex.Unlock()
|
||||
ui.Online = false
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Start() error {
|
||||
conn, err := net.ListenUDP("udp", ui.addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ui.conn = conn
|
||||
ui.Online = true
|
||||
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
|
||||
}
|
||||
|
||||
659
pkg/link/link.go
659
pkg/link/link.go
@@ -5,104 +5,183 @@ import (
|
||||
"crypto/cipher"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/destination"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/packet"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/pathfinder"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/resolver"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/resource"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
||||
)
|
||||
|
||||
const (
|
||||
CURVE = "Curve25519"
|
||||
|
||||
ESTABLISHMENT_TIMEOUT_PER_HOP = 6
|
||||
KEEPALIVE_TIMEOUT_FACTOR = 4
|
||||
STALE_GRACE = 2
|
||||
KEEPALIVE = 360
|
||||
STALE_TIME = 720
|
||||
KEEPALIVE_TIMEOUT_FACTOR = 4
|
||||
STALE_GRACE = 2
|
||||
KEEPALIVE = 360
|
||||
STALE_TIME = 720
|
||||
|
||||
ACCEPT_NONE = 0x00
|
||||
ACCEPT_ALL = 0x01
|
||||
ACCEPT_APP = 0x02
|
||||
|
||||
STATUS_PENDING = 0x00
|
||||
STATUS_ACTIVE = 0x01
|
||||
STATUS_CLOSED = 0x02
|
||||
STATUS_FAILED = 0x03
|
||||
STATUS_PENDING = 0x00
|
||||
STATUS_ACTIVE = 0x01
|
||||
STATUS_CLOSED = 0x02
|
||||
STATUS_FAILED = 0x03
|
||||
|
||||
PROVE_NONE = 0x00
|
||||
PROVE_ALL = 0x01
|
||||
PROVE_APP = 0x02
|
||||
|
||||
WATCHDOG_MIN_SLEEP = 0.025
|
||||
WATCHDOG_INTERVAL = 0.1
|
||||
)
|
||||
|
||||
type Link struct {
|
||||
mutex sync.RWMutex
|
||||
destination interface{}
|
||||
status byte
|
||||
establishedAt time.Time
|
||||
lastInbound time.Time
|
||||
lastOutbound time.Time
|
||||
lastDataReceived time.Time
|
||||
lastDataSent time.Time
|
||||
|
||||
remoteIdentity *identity.Identity
|
||||
sessionKey []byte
|
||||
linkID []byte
|
||||
|
||||
mutex sync.RWMutex
|
||||
destination *destination.Destination
|
||||
status byte
|
||||
networkInterface common.NetworkInterface
|
||||
establishedAt time.Time
|
||||
lastInbound time.Time
|
||||
lastOutbound time.Time
|
||||
lastDataReceived time.Time
|
||||
lastDataSent time.Time
|
||||
pathFinder *pathfinder.PathFinder
|
||||
|
||||
remoteIdentity *identity.Identity
|
||||
sessionKey []byte
|
||||
linkID []byte
|
||||
|
||||
rtt float64
|
||||
establishmentRate float64
|
||||
|
||||
trackPhyStats bool
|
||||
rssi float64
|
||||
snr float64
|
||||
q float64
|
||||
|
||||
resourceStrategy byte
|
||||
|
||||
|
||||
establishedCallback func(*Link)
|
||||
closedCallback func(*Link)
|
||||
packetCallback func([]byte, *packet.Packet)
|
||||
resourceCallback func(interface{}) bool
|
||||
resourceStartedCallback func(interface{})
|
||||
closedCallback func(*Link)
|
||||
packetCallback func([]byte, *packet.Packet)
|
||||
identifiedCallback func(*Link, *identity.Identity)
|
||||
|
||||
teardownReason byte
|
||||
hmacKey []byte
|
||||
transport *transport.Transport
|
||||
|
||||
rssi float64
|
||||
snr float64
|
||||
q float64
|
||||
resourceCallback func(interface{}) bool
|
||||
resourceStartedCallback func(interface{})
|
||||
resourceConcludedCallback func(interface{})
|
||||
remoteIdentifiedCallback func(*Link, *identity.Identity)
|
||||
resourceStrategy byte
|
||||
proofStrategy byte
|
||||
proofCallback func(*packet.Packet) bool
|
||||
trackPhyStats bool
|
||||
|
||||
watchdogLock bool
|
||||
watchdogActive bool
|
||||
establishmentTimeout time.Duration
|
||||
keepalive time.Duration
|
||||
staleTime time.Duration
|
||||
initiator bool
|
||||
}
|
||||
|
||||
func New(dest interface{}, establishedCb func(*Link), closedCb func(*Link)) *Link {
|
||||
l := &Link{
|
||||
destination: dest,
|
||||
status: STATUS_PENDING,
|
||||
establishedAt: time.Time{},
|
||||
lastInbound: time.Time{},
|
||||
lastOutbound: time.Time{},
|
||||
lastDataReceived: time.Time{},
|
||||
lastDataSent: time.Time{},
|
||||
resourceStrategy: ACCEPT_NONE,
|
||||
establishedCallback: establishedCb,
|
||||
closedCallback: closedCb,
|
||||
func NewLink(dest *destination.Destination, transport *transport.Transport, networkIface common.NetworkInterface, establishedCallback func(*Link), closedCallback func(*Link)) *Link {
|
||||
return &Link{
|
||||
destination: dest,
|
||||
status: STATUS_PENDING,
|
||||
transport: transport,
|
||||
networkInterface: networkIface,
|
||||
establishedCallback: establishedCallback,
|
||||
closedCallback: closedCallback,
|
||||
establishedAt: time.Time{}, // Zero time until established
|
||||
lastInbound: time.Time{},
|
||||
lastOutbound: time.Time{},
|
||||
lastDataReceived: time.Time{},
|
||||
lastDataSent: time.Time{},
|
||||
pathFinder: pathfinder.NewPathFinder(),
|
||||
|
||||
watchdogLock: false,
|
||||
watchdogActive: false,
|
||||
establishmentTimeout: time.Duration(ESTABLISHMENT_TIMEOUT_PER_HOP * float64(time.Second)),
|
||||
keepalive: time.Duration(KEEPALIVE * float64(time.Second)),
|
||||
staleTime: time.Duration(STALE_TIME * float64(time.Second)),
|
||||
initiator: false,
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Link) Identify(id *identity.Identity) error {
|
||||
func (l *Link) Establish() error {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
if l.status != STATUS_ACTIVE {
|
||||
return errors.New("link not active")
|
||||
if l.status != STATUS_PENDING {
|
||||
log.Printf("[DEBUG-3] Cannot establish link: invalid status %d", l.status)
|
||||
return errors.New("link already established or failed")
|
||||
}
|
||||
|
||||
// Create identification message
|
||||
idMsg := append(id.GetPublicKey(), id.Sign(l.linkID)...)
|
||||
|
||||
// Encrypt and send identification
|
||||
err := l.SendPacket(idMsg)
|
||||
if err != nil {
|
||||
destPublicKey := l.destination.GetPublicKey()
|
||||
if destPublicKey == nil {
|
||||
log.Printf("[DEBUG-3] Cannot establish link: destination has no public key")
|
||||
return errors.New("destination has no public key")
|
||||
}
|
||||
|
||||
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 nil
|
||||
log.Printf("[DEBUG-4] Sending link request packet with ID %x", l.linkID[:8])
|
||||
return l.transport.SendPacket(p)
|
||||
}
|
||||
|
||||
func (l *Link) Identify(id *identity.Identity) error {
|
||||
if !l.IsActive() {
|
||||
return errors.New("link not active")
|
||||
}
|
||||
|
||||
p := &packet.Packet{
|
||||
HeaderType: packet.HeaderType1,
|
||||
PacketType: packet.PacketTypeData,
|
||||
TransportType: 0,
|
||||
Context: packet.ContextLinkIdentify,
|
||||
ContextFlag: packet.FlagUnset,
|
||||
Hops: 0,
|
||||
DestinationType: l.destination.GetType(),
|
||||
DestinationHash: l.destination.GetHash(),
|
||||
Data: id.GetPublicKey(),
|
||||
CreateReceipt: true,
|
||||
}
|
||||
|
||||
if err := p.Pack(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.transport.SendPacket(p)
|
||||
}
|
||||
|
||||
func (l *Link) HandleIdentification(data []byte) error {
|
||||
@@ -110,26 +189,33 @@ func (l *Link) HandleIdentification(data []byte) error {
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
if len(data) < ed25519.PublicKeySize+ed25519.SignatureSize {
|
||||
return errors.New("invalid identification data")
|
||||
log.Printf("[DEBUG-3] Invalid identification data length: %d bytes", len(data))
|
||||
return errors.New("invalid identification data length")
|
||||
}
|
||||
|
||||
pubKey := data[:ed25519.PublicKeySize]
|
||||
signature := data[ed25519.PublicKeySize:]
|
||||
|
||||
remoteIdentity := &identity.Identity{}
|
||||
if !remoteIdentity.LoadPublicKey(pubKey) {
|
||||
return errors.New("invalid remote public key")
|
||||
log.Printf("[DEBUG-4] Processing identification from public key %x", pubKey[:8])
|
||||
|
||||
remoteIdentity := identity.FromPublicKey(pubKey)
|
||||
if remoteIdentity == nil {
|
||||
log.Printf("[DEBUG-3] Invalid remote identity from public key %x", pubKey[:8])
|
||||
return errors.New("invalid remote identity")
|
||||
}
|
||||
|
||||
// Verify signature of link ID
|
||||
if !remoteIdentity.Verify(l.linkID, signature) {
|
||||
return errors.New("invalid identification signature")
|
||||
signData := append(l.linkID, pubKey...)
|
||||
if !remoteIdentity.Verify(signData, signature) {
|
||||
log.Printf("[DEBUG-3] Invalid signature from remote identity %x", pubKey[:8])
|
||||
return errors.New("invalid signature")
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-4] Remote identity verified successfully: %x", pubKey[:8])
|
||||
l.remoteIdentity = remoteIdentity
|
||||
|
||||
if l.remoteIdentifiedCallback != nil {
|
||||
l.remoteIdentifiedCallback(l, remoteIdentity)
|
||||
if l.identifiedCallback != nil {
|
||||
log.Printf("[DEBUG-4] Executing identified callback for remote identity %x", pubKey[:8])
|
||||
l.identifiedCallback(l, remoteIdentity)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -158,8 +244,8 @@ func (l *Link) Request(path string, data []byte, timeout time.Duration) (*Reques
|
||||
|
||||
receipt := &RequestReceipt{
|
||||
requestID: requestID,
|
||||
status: STATUS_PENDING,
|
||||
sentAt: time.Now(),
|
||||
status: STATUS_PENDING,
|
||||
sentAt: time.Now(),
|
||||
}
|
||||
|
||||
// Send request
|
||||
@@ -184,12 +270,12 @@ func (l *Link) Request(path string, data []byte, timeout time.Duration) (*Reques
|
||||
}
|
||||
|
||||
type RequestReceipt struct {
|
||||
mutex sync.RWMutex
|
||||
requestID []byte
|
||||
status byte
|
||||
sentAt time.Time
|
||||
receivedAt time.Time
|
||||
response []byte
|
||||
mutex sync.RWMutex
|
||||
requestID []byte
|
||||
status byte
|
||||
sentAt time.Time
|
||||
receivedAt time.Time
|
||||
response []byte
|
||||
}
|
||||
|
||||
func (r *RequestReceipt) GetRequestID() []byte {
|
||||
@@ -233,21 +319,40 @@ func (l *Link) TrackPhyStats(track bool) {
|
||||
l.trackPhyStats = track
|
||||
}
|
||||
|
||||
func (l *Link) UpdatePhyStats(rssi, snr, q float64) {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
if l.trackPhyStats {
|
||||
l.rssi = rssi
|
||||
l.snr = snr
|
||||
l.q = q
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Link) GetRSSI() float64 {
|
||||
l.mutex.RLock()
|
||||
defer l.mutex.RUnlock()
|
||||
if !l.trackPhyStats {
|
||||
return 0
|
||||
}
|
||||
return l.rssi
|
||||
}
|
||||
|
||||
func (l *Link) GetSNR() float64 {
|
||||
l.mutex.RLock()
|
||||
defer l.mutex.RUnlock()
|
||||
if !l.trackPhyStats {
|
||||
return 0
|
||||
}
|
||||
return l.snr
|
||||
}
|
||||
|
||||
func (l *Link) GetQ() float64 {
|
||||
l.mutex.RLock()
|
||||
defer l.mutex.RUnlock()
|
||||
if !l.trackPhyStats {
|
||||
return 0
|
||||
}
|
||||
return l.q
|
||||
}
|
||||
|
||||
@@ -319,7 +424,7 @@ func (l *Link) GetRemoteIdentity() *identity.Identity {
|
||||
func (l *Link) Teardown() {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
|
||||
if l.status == STATUS_ACTIVE {
|
||||
l.status = STATUS_CLOSED
|
||||
if l.closedCallback != nil {
|
||||
@@ -361,85 +466,58 @@ func (l *Link) SetResourceConcludedCallback(callback func(interface{})) {
|
||||
func (l *Link) SetRemoteIdentifiedCallback(callback func(*Link, *identity.Identity)) {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
l.remoteIdentifiedCallback = callback
|
||||
l.identifiedCallback = callback
|
||||
}
|
||||
|
||||
func (l *Link) SetResourceStrategy(strategy byte) error {
|
||||
if strategy != ACCEPT_NONE && strategy != ACCEPT_ALL && strategy != ACCEPT_APP {
|
||||
return errors.New("unsupported resource strategy")
|
||||
}
|
||||
|
||||
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
l.resourceStrategy = strategy
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewLink(destination interface{}, establishedCallback func(*Link), closedCallback func(*Link)) *Link {
|
||||
l := &Link{
|
||||
destination: destination,
|
||||
status: STATUS_PENDING,
|
||||
establishedAt: time.Time{},
|
||||
lastInbound: time.Time{},
|
||||
lastOutbound: time.Time{},
|
||||
lastDataReceived: time.Time{},
|
||||
lastDataSent: time.Time{},
|
||||
establishedCallback: establishedCallback,
|
||||
closedCallback: closedCallback,
|
||||
resourceStrategy: ACCEPT_NONE,
|
||||
trackPhyStats: false,
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Link) Establish() error {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
if l.status != STATUS_PENDING {
|
||||
return errors.New("link already established or failed")
|
||||
}
|
||||
|
||||
// Generate session key using ECDH
|
||||
ephemeralKey := make([]byte, 32)
|
||||
if _, err := rand.Read(ephemeralKey); err != nil {
|
||||
return err
|
||||
}
|
||||
l.sessionKey = ephemeralKey
|
||||
|
||||
l.establishedAt = time.Now()
|
||||
l.status = STATUS_ACTIVE
|
||||
|
||||
if l.establishedCallback != nil {
|
||||
l.establishedCallback(l)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Link) SendPacket(data []byte) error {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
if l.status != STATUS_ACTIVE {
|
||||
log.Printf("[DEBUG-3] Cannot send packet: link not active (status: %d)", l.status)
|
||||
return errors.New("link not active")
|
||||
}
|
||||
|
||||
// Encrypt data using session key
|
||||
encryptedData, err := l.encrypt(data)
|
||||
log.Printf("[DEBUG-4] Encrypting packet of %d bytes", len(data))
|
||||
encrypted, err := l.encrypt(data)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-3] Failed to encrypt packet: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
p := &packet.Packet{
|
||||
HeaderType: packet.HeaderType1,
|
||||
PacketType: packet.PacketTypeData,
|
||||
TransportType: 0,
|
||||
Context: packet.ContextNone,
|
||||
ContextFlag: packet.FlagUnset,
|
||||
Hops: 0,
|
||||
DestinationType: l.destination.GetType(),
|
||||
DestinationHash: l.destination.GetHash(),
|
||||
Data: encrypted,
|
||||
CreateReceipt: false,
|
||||
}
|
||||
|
||||
if err := p.Pack(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-4] Sending encrypted packet of %d bytes", len(encrypted))
|
||||
l.lastOutbound = time.Now()
|
||||
l.lastDataSent = time.Now()
|
||||
|
||||
if l.packetCallback != nil {
|
||||
l.packetCallback(encryptedData, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
return l.transport.SendPacket(p)
|
||||
}
|
||||
|
||||
func (l *Link) HandleInbound(data []byte) error {
|
||||
@@ -447,20 +525,28 @@ func (l *Link) HandleInbound(data []byte) error {
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
if l.status != STATUS_ACTIVE {
|
||||
log.Printf("[DEBUG-3] Dropping inbound packet: link not active (status: %d)", l.status)
|
||||
return errors.New("link not active")
|
||||
}
|
||||
|
||||
// Decrypt data using session key
|
||||
decryptedData, err := l.decrypt(data)
|
||||
if err != nil {
|
||||
return err
|
||||
// Decode and log packet details
|
||||
l.decodePacket(data)
|
||||
|
||||
// Decrypt if we have a session key
|
||||
if l.sessionKey != nil {
|
||||
decrypted, err := l.decrypt(data)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-3] Failed to decrypt packet: %v", err)
|
||||
return err
|
||||
}
|
||||
data = decrypted
|
||||
}
|
||||
|
||||
l.lastInbound = time.Now()
|
||||
l.lastDataReceived = time.Now()
|
||||
|
||||
if l.packetCallback != nil {
|
||||
l.packetCallback(decryptedData, nil)
|
||||
l.packetCallback(data, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -476,17 +562,27 @@ func (l *Link) encrypt(data []byte) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
// Generate IV
|
||||
iv := make([]byte, aes.BlockSize)
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, err
|
||||
// Add PKCS7 padding
|
||||
padding := aes.BlockSize - len(data)%aes.BlockSize
|
||||
padtext := make([]byte, len(data)+padding)
|
||||
copy(padtext, data)
|
||||
for i := len(data); i < len(padtext); i++ {
|
||||
padtext[i] = byte(padding)
|
||||
}
|
||||
|
||||
return gcm.Seal(nonce, nonce, data, nil), nil
|
||||
// Encrypt
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
ciphertext := make([]byte, len(padtext))
|
||||
mode.CryptBlocks(ciphertext, padtext)
|
||||
|
||||
// Prepend IV to ciphertext
|
||||
return append(iv, ciphertext...), nil
|
||||
}
|
||||
|
||||
func (l *Link) decrypt(data []byte) ([]byte, error) {
|
||||
@@ -499,31 +595,34 @@ func (l *Link) decrypt(data []byte) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
if len(data) < aes.BlockSize {
|
||||
return nil, errors.New("ciphertext too short")
|
||||
}
|
||||
|
||||
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
|
||||
return gcm.Open(nil, nonce, ciphertext, nil)
|
||||
}
|
||||
iv := data[:aes.BlockSize]
|
||||
ciphertext := data[aes.BlockSize:]
|
||||
|
||||
func (l *Link) UpdatePhyStats(rssi float64, snr float64, q float64) {
|
||||
if !l.trackPhyStats {
|
||||
return
|
||||
if len(ciphertext)%aes.BlockSize != 0 {
|
||||
return nil, errors.New("ciphertext is not a multiple of block size")
|
||||
}
|
||||
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
l.rssi = rssi
|
||||
l.snr = snr
|
||||
l.q = q
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
plaintext := make([]byte, len(ciphertext))
|
||||
mode.CryptBlocks(plaintext, ciphertext)
|
||||
|
||||
// Remove PKCS7 padding
|
||||
padding := int(plaintext[len(plaintext)-1])
|
||||
if padding > aes.BlockSize || padding == 0 {
|
||||
return nil, errors.New("invalid padding")
|
||||
}
|
||||
|
||||
for i := len(plaintext) - padding; i < len(plaintext); i++ {
|
||||
if plaintext[i] != byte(padding) {
|
||||
return nil, errors.New("invalid padding")
|
||||
}
|
||||
}
|
||||
|
||||
return plaintext[:len(plaintext)-padding], nil
|
||||
}
|
||||
|
||||
func (l *Link) GetRTT() float64 {
|
||||
@@ -546,4 +645,240 @@ func (l *Link) GetStatus() byte {
|
||||
|
||||
func (l *Link) IsActive() bool {
|
||||
return l.GetStatus() == STATUS_ACTIVE
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Link) SendResource(res *resource.Resource) error {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
if l.status != STATUS_ACTIVE {
|
||||
l.teardownReason = STATUS_FAILED
|
||||
return errors.New("link not active")
|
||||
}
|
||||
|
||||
// Activate the resource
|
||||
res.Activate()
|
||||
|
||||
// Send the resource data as packets
|
||||
buffer := make([]byte, resource.DEFAULT_SEGMENT_SIZE)
|
||||
for {
|
||||
n, err := res.Read(buffer)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
l.teardownReason = STATUS_FAILED
|
||||
return fmt.Errorf("error reading resource: %v", err)
|
||||
}
|
||||
|
||||
if err := l.SendPacket(buffer[:n]); err != nil {
|
||||
l.teardownReason = STATUS_FAILED
|
||||
return fmt.Errorf("error sending resource packet: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Link) maintainLink() {
|
||||
ticker := time.NewTicker(time.Second * KEEPALIVE)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if l.status != STATUS_ACTIVE {
|
||||
return
|
||||
}
|
||||
|
||||
inactiveTime := l.InactiveFor()
|
||||
if inactiveTime > float64(STALE_TIME) {
|
||||
l.mutex.Lock()
|
||||
l.teardownReason = STATUS_FAILED
|
||||
l.mutex.Unlock()
|
||||
l.Teardown()
|
||||
return
|
||||
}
|
||||
|
||||
noDataTime := l.NoDataFor()
|
||||
if noDataTime > float64(KEEPALIVE) {
|
||||
l.mutex.Lock()
|
||||
err := l.SendPacket([]byte{})
|
||||
if err != nil {
|
||||
l.teardownReason = STATUS_FAILED
|
||||
l.mutex.Unlock()
|
||||
l.Teardown()
|
||||
return
|
||||
}
|
||||
l.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Link) Start() {
|
||||
go l.maintainLink()
|
||||
}
|
||||
|
||||
func (l *Link) SetProofStrategy(strategy byte) error {
|
||||
if strategy != PROVE_NONE && strategy != PROVE_ALL && strategy != PROVE_APP {
|
||||
return errors.New("invalid proof strategy")
|
||||
}
|
||||
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
l.proofStrategy = strategy
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Link) SetProofCallback(callback func(*packet.Packet) bool) {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
l.proofCallback = callback
|
||||
}
|
||||
|
||||
func (l *Link) HandleProofRequest(packet *packet.Packet) bool {
|
||||
l.mutex.RLock()
|
||||
defer l.mutex.RUnlock()
|
||||
|
||||
switch l.proofStrategy {
|
||||
case PROVE_NONE:
|
||||
return false
|
||||
case PROVE_ALL:
|
||||
return true
|
||||
case PROVE_APP:
|
||||
if l.proofCallback != nil {
|
||||
return l.proofCallback(packet)
|
||||
}
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Link) decodePacket(data []byte) {
|
||||
if len(data) < 1 {
|
||||
log.Printf("[DEBUG-7] Invalid packet: zero length")
|
||||
return
|
||||
}
|
||||
|
||||
packetType := data[0]
|
||||
log.Printf("[DEBUG-7] Packet Analysis:")
|
||||
log.Printf("[DEBUG-7] - Size: %d bytes", len(data))
|
||||
log.Printf("[DEBUG-7] - Type: 0x%02x", packetType)
|
||||
|
||||
switch packetType {
|
||||
case packet.PacketTypeData:
|
||||
log.Printf("[DEBUG-7] - Type Description: Data Packet")
|
||||
if len(data) > 1 {
|
||||
log.Printf("[DEBUG-7] - Payload Size: %d bytes", len(data)-1)
|
||||
}
|
||||
|
||||
case packet.PacketTypeLinkReq:
|
||||
log.Printf("[DEBUG-7] - Type Description: Link Management")
|
||||
if len(data) > 32 {
|
||||
log.Printf("[DEBUG-7] - Link ID: %x", data[1:33])
|
||||
}
|
||||
|
||||
case packet.PacketTypeAnnounce:
|
||||
log.Printf("[DEBUG-7] Received announce packet (%d bytes)", len(data))
|
||||
if len(data) < packet.MinAnnounceSize {
|
||||
log.Printf("[DEBUG-3] Announce packet too short: %d bytes", len(data))
|
||||
return
|
||||
}
|
||||
|
||||
destHash := data[2:18]
|
||||
encKey := data[18:50]
|
||||
signKey := data[50:82]
|
||||
nameHash := data[82:92]
|
||||
randomHash := data[92:102]
|
||||
signature := data[102:166]
|
||||
appData := data[166:]
|
||||
|
||||
pubKey := append(encKey, signKey...)
|
||||
|
||||
validationData := make([]byte, 0, 164)
|
||||
validationData = append(validationData, destHash...)
|
||||
validationData = append(validationData, encKey...)
|
||||
validationData = append(validationData, signKey...)
|
||||
validationData = append(validationData, nameHash...)
|
||||
validationData = append(validationData, randomHash...)
|
||||
|
||||
if identity.ValidateAnnounce(validationData, destHash, pubKey, signature, appData) {
|
||||
log.Printf("[DEBUG-4] Valid announce from %x", pubKey[:8])
|
||||
if err := l.transport.HandleAnnounce(destHash, l.networkInterface); err != nil {
|
||||
log.Printf("[DEBUG-3] Failed to handle announce: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[DEBUG-3] Invalid announce signature from %x", pubKey[:8])
|
||||
}
|
||||
|
||||
case packet.PacketTypeProof:
|
||||
log.Printf("[DEBUG-7] - Type Description: RNS Discovery")
|
||||
if len(data) > 17 {
|
||||
searchHash := data[1:17]
|
||||
log.Printf("[DEBUG-7] - Searching for Hash: %x", searchHash)
|
||||
|
||||
if id, err := resolver.ResolveIdentity(hex.EncodeToString(searchHash)); err == nil {
|
||||
log.Printf("[DEBUG-7] - Found matching identity: %s", id.GetHexHash())
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
log.Printf("[DEBUG-7] - Type Description: Unknown (0x%02x)", packetType)
|
||||
log.Printf("[DEBUG-7] - Raw Hex: %x", data)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function for min of two ints
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (l *Link) startWatchdog() {
|
||||
if l.watchdogActive {
|
||||
return
|
||||
}
|
||||
|
||||
l.watchdogActive = true
|
||||
go l.watchdog()
|
||||
}
|
||||
|
||||
func (l *Link) watchdog() {
|
||||
for l.status != STATUS_CLOSED {
|
||||
l.mutex.Lock()
|
||||
if l.watchdogLock {
|
||||
l.mutex.Unlock()
|
||||
time.Sleep(time.Duration(WATCHDOG_MIN_SLEEP * float64(time.Second)))
|
||||
continue
|
||||
}
|
||||
|
||||
var sleepTime float64 = WATCHDOG_INTERVAL
|
||||
|
||||
switch l.status {
|
||||
case STATUS_ACTIVE:
|
||||
lastActivity := l.lastInbound
|
||||
if l.lastOutbound.After(lastActivity) {
|
||||
lastActivity = l.lastOutbound
|
||||
}
|
||||
|
||||
if time.Since(lastActivity) > l.keepalive {
|
||||
if l.initiator {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -3,11 +3,7 @@ package packet
|
||||
const (
|
||||
// MTU constants
|
||||
EncryptedMDU = 383 // Maximum size of payload data in encrypted packet
|
||||
PlainMDU = 464 // Maximum size of payload data in unencrypted packet
|
||||
|
||||
// Header Types
|
||||
HeaderType1 = 0 // Two byte header, one 16 byte address field
|
||||
HeaderType2 = 1 // Two byte header, two 16 byte address fields
|
||||
PlainMDU = 464 // Maximum size of payload data in unencrypted packet
|
||||
|
||||
// Propagation Types
|
||||
PropagationBroadcast = 0
|
||||
@@ -19,9 +15,7 @@ const (
|
||||
DestinationPlain = 2
|
||||
DestinationLink = 3
|
||||
|
||||
// Packet Types
|
||||
PacketData = 0
|
||||
PacketAnnounce = 1
|
||||
PacketLinkRequest = 2
|
||||
PacketProof = 3
|
||||
)
|
||||
// Minimum packet sizes
|
||||
MinAnnounceSize = 170 // header(2) + desthash(16) + context(1) + enckey(32) + signkey(32) +
|
||||
// namehash(10) + randomhash(10) + signature(64) + min appdata(3)
|
||||
)
|
||||
|
||||
@@ -1,171 +1,307 @@
|
||||
package packet
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderSize = 2
|
||||
AddressSize = 16
|
||||
ContextSize = 1
|
||||
MaxDataSize = 465 // Maximum size of payload data
|
||||
)
|
||||
// Packet Types
|
||||
PacketTypeData = 0x00
|
||||
PacketTypeAnnounce = 0x01
|
||||
PacketTypeLinkReq = 0x02
|
||||
PacketTypeProof = 0x03
|
||||
|
||||
// Header flags and types
|
||||
const (
|
||||
// First byte flags
|
||||
IFACFlag = 0x80 // Interface authentication code flag
|
||||
HeaderTypeFlag = 0x40 // Header type flag
|
||||
ContextFlag = 0x20 // Context flag
|
||||
PropagationFlags = 0x18 // Propagation type flags (bits 3-4)
|
||||
DestinationFlags = 0x06 // Destination type flags (bits 1-2)
|
||||
PacketTypeFlags = 0x01 // Packet type flags (bit 0)
|
||||
// Header Types
|
||||
HeaderType1 = 0x00
|
||||
HeaderType2 = 0x01
|
||||
|
||||
// Second byte
|
||||
HopsField = 0xFF // Number of hops (entire byte)
|
||||
// Context Types
|
||||
ContextNone = 0x00
|
||||
ContextResource = 0x01
|
||||
ContextResourceAdv = 0x02
|
||||
ContextResourceReq = 0x03
|
||||
ContextResourceHMU = 0x04
|
||||
ContextResourcePRF = 0x05
|
||||
ContextResourceICL = 0x06
|
||||
ContextResourceRCL = 0x07
|
||||
ContextCacheReq = 0x08
|
||||
ContextRequest = 0x09
|
||||
ContextResponse = 0x0A
|
||||
ContextPathResponse = 0x0B
|
||||
ContextCommand = 0x0C
|
||||
ContextCmdStatus = 0x0D
|
||||
ContextChannel = 0x0E
|
||||
ContextKeepalive = 0xFA
|
||||
ContextLinkIdentify = 0xFB
|
||||
ContextLinkClose = 0xFC
|
||||
ContextLinkProof = 0xFD
|
||||
ContextLRRTT = 0xFE
|
||||
ContextLRProof = 0xFF
|
||||
|
||||
// Flag Values
|
||||
FlagSet = 0x01
|
||||
FlagUnset = 0x00
|
||||
|
||||
// Header sizes
|
||||
HeaderMaxSize = 64
|
||||
MTU = 500
|
||||
|
||||
AddressSize = 32 // Size of address/hash fields in bytes
|
||||
)
|
||||
|
||||
type Packet struct {
|
||||
Header [2]byte
|
||||
Addresses []byte // Either 16 or 32 bytes depending on header type
|
||||
Context byte
|
||||
Data []byte
|
||||
AccessCode []byte // Optional: Only present if IFAC flag is set
|
||||
HeaderType byte
|
||||
PacketType byte
|
||||
TransportType byte
|
||||
Context byte
|
||||
ContextFlag byte
|
||||
Hops byte
|
||||
|
||||
DestinationType byte
|
||||
DestinationHash []byte
|
||||
TransportID []byte
|
||||
Data []byte
|
||||
|
||||
Raw []byte
|
||||
Packed bool
|
||||
Sent bool
|
||||
CreateReceipt bool
|
||||
FromPacked bool
|
||||
|
||||
SentAt time.Time
|
||||
PacketHash []byte
|
||||
RatchetID []byte
|
||||
|
||||
RSSI *float64
|
||||
SNR *float64
|
||||
Q *float64
|
||||
|
||||
Addresses []byte
|
||||
}
|
||||
|
||||
func NewPacket(headerType, propagationType, destinationType, packetType byte, hops byte) *Packet {
|
||||
p := &Packet{
|
||||
Header: [2]byte{0, hops},
|
||||
Addresses: make([]byte, 0),
|
||||
Data: make([]byte, 0),
|
||||
func NewPacket(destType byte, data []byte, packetType byte, context byte,
|
||||
transportType byte, headerType byte, transportID []byte, createReceipt bool,
|
||||
contextFlag byte) *Packet {
|
||||
|
||||
return &Packet{
|
||||
HeaderType: headerType,
|
||||
PacketType: packetType,
|
||||
TransportType: transportType,
|
||||
Context: context,
|
||||
ContextFlag: contextFlag,
|
||||
Hops: 0,
|
||||
DestinationType: destType,
|
||||
Data: data,
|
||||
TransportID: transportID,
|
||||
CreateReceipt: createReceipt,
|
||||
Packed: false,
|
||||
Sent: false,
|
||||
FromPacked: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Packet) Pack() error {
|
||||
if p.Packed {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set header type
|
||||
if headerType == HeaderType2 {
|
||||
p.Header[0] |= HeaderTypeFlag
|
||||
p.Addresses = make([]byte, 2*AddressSize) // Two address fields
|
||||
log.Printf("[DEBUG-6] Packing packet: type=%d, header=%d", p.PacketType, p.HeaderType)
|
||||
|
||||
// Create header byte
|
||||
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 {
|
||||
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.Header[0] |= (propagationType << 3) & PropagationFlags
|
||||
|
||||
// Set destination type
|
||||
p.Header[0] |= (destinationType << 1) & DestinationFlags
|
||||
|
||||
// Set packet type
|
||||
p.Header[0] |= packetType & PacketTypeFlags
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Packet) SetAccessCode(code []byte) {
|
||||
p.AccessCode = code
|
||||
p.Header[0] |= IFACFlag
|
||||
}
|
||||
|
||||
func (p *Packet) SetContext(context byte) {
|
||||
p.Context = context
|
||||
p.Header[0] |= ContextFlag
|
||||
}
|
||||
|
||||
func (p *Packet) SetData(data []byte) error {
|
||||
if len(data) > MaxDataSize {
|
||||
return errors.New("data exceeds maximum allowed size")
|
||||
}
|
||||
p.Data = data
|
||||
p.Packed = false
|
||||
p.updateHash()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Packet) SetAddress(index int, address []byte) error {
|
||||
if len(address) != AddressSize {
|
||||
return errors.New("invalid address size")
|
||||
func (p *Packet) GetHash() []byte {
|
||||
hashable := p.getHashablePart()
|
||||
hash := sha256.Sum256(hashable)
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
func (p *Packet) getHashablePart() []byte {
|
||||
hashable := []byte{p.Raw[0] & 0b00001111}
|
||||
if p.HeaderType == HeaderType2 {
|
||||
hashable = append(hashable, p.Raw[18:]...)
|
||||
} else {
|
||||
hashable = append(hashable, p.Raw[2:]...)
|
||||
}
|
||||
|
||||
offset := index * AddressSize
|
||||
if offset+AddressSize > len(p.Addresses) {
|
||||
return errors.New("address index out of range")
|
||||
}
|
||||
|
||||
copy(p.Addresses[offset:], address)
|
||||
return nil
|
||||
return hashable
|
||||
}
|
||||
|
||||
func (p *Packet) updateHash() {
|
||||
p.PacketHash = p.GetHash()
|
||||
}
|
||||
|
||||
func (p *Packet) Serialize() ([]byte, error) {
|
||||
totalSize := HeaderSize + len(p.Addresses) + ContextSize + len(p.Data)
|
||||
if p.AccessCode != nil {
|
||||
totalSize += len(p.AccessCode)
|
||||
if !p.Packed {
|
||||
if err := p.Pack(); err != nil {
|
||||
return nil, fmt.Errorf("failed to pack packet: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
buffer := make([]byte, totalSize)
|
||||
offset := 0
|
||||
p.Addresses = p.DestinationHash
|
||||
|
||||
// Write header
|
||||
copy(buffer[offset:], p.Header[:])
|
||||
offset += HeaderSize
|
||||
|
||||
// Write access code if present
|
||||
if p.AccessCode != nil {
|
||||
copy(buffer[offset:], p.AccessCode)
|
||||
offset += len(p.AccessCode)
|
||||
}
|
||||
|
||||
// Write addresses
|
||||
copy(buffer[offset:], p.Addresses)
|
||||
offset += len(p.Addresses)
|
||||
|
||||
// Write context
|
||||
buffer[offset] = p.Context
|
||||
offset += ContextSize
|
||||
|
||||
// Write data
|
||||
copy(buffer[offset:], p.Data)
|
||||
|
||||
return buffer, nil
|
||||
return p.Raw, nil
|
||||
}
|
||||
|
||||
func ParsePacket(data []byte) (*Packet, error) {
|
||||
if len(data) < HeaderSize {
|
||||
return nil, errors.New("packet data too short")
|
||||
func NewAnnouncePacket(destHash []byte, identity *identity.Identity, appData []byte, transportID []byte) (*Packet, error) {
|
||||
log.Printf("[DEBUG-7] Creating new announce packet: destHash=%x, appData=%s", destHash, fmt.Sprintf("%x", appData))
|
||||
|
||||
// Get public key separated into encryption and signing keys
|
||||
pubKey := identity.GetPublicKey()
|
||||
encKey := pubKey[:32]
|
||||
signKey := pubKey[32:]
|
||||
log.Printf("[DEBUG-6] Using public keys: encKey=%x, signKey=%x", encKey, signKey)
|
||||
|
||||
// Parse app name from first msgpack element if possible
|
||||
// For nodes, we'll use "reticulum.node" as the name hash
|
||||
var appName string
|
||||
if len(appData) > 2 && appData[0] == 0x93 {
|
||||
// This is a node announce, use standard node name
|
||||
appName = "reticulum.node"
|
||||
} else if len(appData) > 3 && appData[0] == 0x92 && appData[1] == 0xc4 {
|
||||
// Try to extract name from peer announce appData
|
||||
nameLen := int(appData[2])
|
||||
if 3+nameLen <= len(appData) {
|
||||
appName = string(appData[3 : 3+nameLen])
|
||||
} else {
|
||||
// Default fallback
|
||||
appName = "reticulum-go.node"
|
||||
}
|
||||
} else {
|
||||
// Default fallback
|
||||
appName = "reticulum-go.node"
|
||||
}
|
||||
|
||||
// Create name hash (10 bytes)
|
||||
nameHash := sha256.Sum256([]byte(appName))
|
||||
nameHash10 := nameHash[:10]
|
||||
log.Printf("[DEBUG-6] Using name hash for '%s': %x", appName, nameHash10)
|
||||
|
||||
// Create random hash (10 bytes) - 5 bytes random + 5 bytes time
|
||||
randomHash := make([]byte, 10)
|
||||
rand.Read(randomHash[:5])
|
||||
timeBytes := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(timeBytes, uint64(time.Now().Unix()))
|
||||
copy(randomHash[5:], timeBytes[:5])
|
||||
log.Printf("[DEBUG-6] Generated random hash: %x", randomHash)
|
||||
|
||||
// Prepare ratchet ID if available (not yet implemented)
|
||||
var ratchetID []byte
|
||||
|
||||
// Prepare data for signature
|
||||
// Signature consists of destination hash, public keys, name hash, random hash, and app data
|
||||
signedData := make([]byte, 0, len(destHash)+len(encKey)+len(signKey)+len(nameHash10)+len(randomHash)+len(appData))
|
||||
signedData = append(signedData, destHash...)
|
||||
signedData = append(signedData, encKey...)
|
||||
signedData = append(signedData, signKey...)
|
||||
signedData = append(signedData, nameHash10...)
|
||||
signedData = append(signedData, randomHash...)
|
||||
signedData = append(signedData, appData...)
|
||||
log.Printf("[DEBUG-5] Created signed data (%d bytes)", len(signedData))
|
||||
|
||||
// Sign the data
|
||||
signature := identity.Sign(signedData)
|
||||
log.Printf("[DEBUG-6] Generated signature: %x", signature)
|
||||
|
||||
// Combine all fields according to spec
|
||||
// Data structure: Public Key (32) + Signing Key (32) + Name Hash (10) + Random Hash (10) + Ratchet (optional) + Signature (64) + App Data
|
||||
data := make([]byte, 0, 32+32+10+10+64+len(appData))
|
||||
data = append(data, encKey...) // Encryption key (32 bytes)
|
||||
data = append(data, signKey...) // Signing key (32 bytes)
|
||||
data = append(data, nameHash10...) // Name hash (10 bytes)
|
||||
data = append(data, randomHash...) // Random hash (10 bytes)
|
||||
if ratchetID != nil {
|
||||
data = append(data, ratchetID...) // Ratchet ID (32 bytes if present)
|
||||
}
|
||||
data = append(data, signature...) // Signature (64 bytes)
|
||||
data = append(data, appData...) // Application data (variable)
|
||||
|
||||
log.Printf("[DEBUG-5] Combined packet data (%d bytes)", len(data))
|
||||
|
||||
// Create the packet with header type 2 (two address fields)
|
||||
p := &Packet{
|
||||
Header: [2]byte{data[0], data[1]},
|
||||
HeaderType: HeaderType2,
|
||||
PacketType: PacketTypeAnnounce,
|
||||
TransportID: transportID,
|
||||
DestinationHash: destHash,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
offset := HeaderSize
|
||||
|
||||
// Handle access code if present
|
||||
if p.Header[0]&IFACFlag != 0 {
|
||||
// Access code handling would go here
|
||||
// For now, we'll assume no access code
|
||||
return nil, errors.New("access code handling not implemented")
|
||||
}
|
||||
|
||||
// Determine address size based on header type
|
||||
addrLen := AddressSize
|
||||
if p.Header[0]&HeaderTypeFlag != 0 {
|
||||
addrLen = 2 * AddressSize
|
||||
}
|
||||
|
||||
if len(data[offset:]) < addrLen+ContextSize {
|
||||
return nil, errors.New("packet data too short for addresses and context")
|
||||
}
|
||||
|
||||
// Copy addresses
|
||||
p.Addresses = make([]byte, addrLen)
|
||||
copy(p.Addresses, data[offset:offset+addrLen])
|
||||
offset += addrLen
|
||||
|
||||
// Copy context
|
||||
p.Context = data[offset]
|
||||
offset++
|
||||
|
||||
// Copy remaining data
|
||||
p.Data = make([]byte, len(data)-offset)
|
||||
copy(p.Data, data[offset:])
|
||||
|
||||
log.Printf("[DEBUG-4] Created announce packet: type=%d, header=%d", p.PacketType, p.HeaderType)
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
34
pkg/pathfinder/pathfinder.go
Normal file
34
pkg/pathfinder/pathfinder.go
Normal 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
193
pkg/rate/rate.go
Normal 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
74
pkg/resolver/resolver.go
Normal 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)
|
||||
}
|
||||
@@ -2,13 +2,12 @@ package resource
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -19,88 +18,90 @@ const (
|
||||
STATUS_CANCELLED = 0x04
|
||||
|
||||
DEFAULT_SEGMENT_SIZE = 384 // Based on ENCRYPTED_MDU
|
||||
MAX_SEGMENTS = 65535
|
||||
CLEANUP_INTERVAL = 300 // 5 minutes
|
||||
MAX_SEGMENTS = 65535
|
||||
CLEANUP_INTERVAL = 300 // 5 minutes
|
||||
|
||||
// Window size constants
|
||||
WINDOW = 4
|
||||
WINDOW_MIN = 2
|
||||
WINDOW_MAX_SLOW = 10
|
||||
WINDOW_MIN = 2
|
||||
WINDOW_MAX_SLOW = 10
|
||||
WINDOW_MAX_VERY_SLOW = 4
|
||||
WINDOW_MAX_FAST = 75
|
||||
WINDOW_MAX = WINDOW_MAX_FAST
|
||||
|
||||
WINDOW_MAX_FAST = 75
|
||||
WINDOW_MAX = WINDOW_MAX_FAST
|
||||
|
||||
// Rate thresholds
|
||||
FAST_RATE_THRESHOLD = WINDOW_MAX_SLOW - WINDOW - 2
|
||||
FAST_RATE_THRESHOLD = WINDOW_MAX_SLOW - WINDOW - 2
|
||||
VERY_SLOW_RATE_THRESHOLD = 2
|
||||
|
||||
|
||||
// Transfer rates (bytes per second)
|
||||
RATE_FAST = (50 * 1000) / 8 // 50 Kbps
|
||||
RATE_VERY_SLOW = (2 * 1000) / 8 // 2 Kbps
|
||||
|
||||
RATE_FAST = (50 * 1000) / 8 // 50 Kbps
|
||||
RATE_VERY_SLOW = (2 * 1000) / 8 // 2 Kbps
|
||||
|
||||
// Window flexibility
|
||||
WINDOW_FLEXIBILITY = 4
|
||||
|
||||
|
||||
// Hash and segment constants
|
||||
MAPHASH_LEN = 4
|
||||
MAPHASH_LEN = 4
|
||||
RANDOM_HASH_SIZE = 4
|
||||
|
||||
|
||||
// Size limits
|
||||
MAX_EFFICIENT_SIZE = 16*1024*1024 - 1 // ~16MB
|
||||
MAX_EFFICIENT_SIZE = 16*1024*1024 - 1 // ~16MB
|
||||
AUTO_COMPRESS_MAX_SIZE = MAX_EFFICIENT_SIZE
|
||||
|
||||
|
||||
// Timeouts and retries
|
||||
PART_TIMEOUT_FACTOR = 4
|
||||
PART_TIMEOUT_FACTOR = 4
|
||||
PART_TIMEOUT_FACTOR_AFTER_RTT = 2
|
||||
PROOF_TIMEOUT_FACTOR = 3
|
||||
MAX_RETRIES = 16
|
||||
MAX_ADV_RETRIES = 4
|
||||
SENDER_GRACE_TIME = 10.0
|
||||
PROCESSING_GRACE = 1.0
|
||||
RETRY_GRACE_TIME = 0.25
|
||||
PER_RETRY_DELAY = 0.5
|
||||
PROOF_TIMEOUT_FACTOR = 3
|
||||
MAX_RETRIES = 16
|
||||
MAX_ADV_RETRIES = 4
|
||||
SENDER_GRACE_TIME = 10.0
|
||||
PROCESSING_GRACE = 1.0
|
||||
RETRY_GRACE_TIME = 0.25
|
||||
PER_RETRY_DELAY = 0.5
|
||||
)
|
||||
|
||||
type Resource struct {
|
||||
mutex sync.RWMutex
|
||||
mutex sync.RWMutex
|
||||
data []byte
|
||||
fileHandle io.ReadWriteSeeker
|
||||
fileName string
|
||||
hash []byte
|
||||
randomHash []byte
|
||||
originalHash []byte
|
||||
status byte
|
||||
compressed bool
|
||||
autoCompress bool
|
||||
encrypted bool
|
||||
split bool
|
||||
segments uint16
|
||||
segmentIndex uint16
|
||||
totalSegments uint16
|
||||
completedParts map[uint16]bool
|
||||
transferSize int64
|
||||
dataSize int64
|
||||
progress float64
|
||||
window int
|
||||
windowMax int
|
||||
windowMin int
|
||||
windowFlexibility int
|
||||
rtt float64
|
||||
fastRateRounds int
|
||||
status byte
|
||||
compressed bool
|
||||
autoCompress bool
|
||||
encrypted bool
|
||||
split bool
|
||||
segments uint16
|
||||
segmentIndex uint16
|
||||
totalSegments uint16
|
||||
completedParts map[uint16]bool
|
||||
transferSize int64
|
||||
dataSize int64
|
||||
progress float64
|
||||
window int
|
||||
windowMax int
|
||||
windowMin int
|
||||
windowFlexibility int
|
||||
rtt float64
|
||||
fastRateRounds int
|
||||
verySlowRateRounds int
|
||||
createdAt time.Time
|
||||
completedAt time.Time
|
||||
callback func(*Resource)
|
||||
progressCallback func(*Resource)
|
||||
createdAt time.Time
|
||||
completedAt time.Time
|
||||
callback func(*Resource)
|
||||
progressCallback func(*Resource)
|
||||
readOffset int64
|
||||
}
|
||||
|
||||
func New(data interface{}, autoCompress bool) (*Resource, error) {
|
||||
r := &Resource{
|
||||
status: STATUS_PENDING,
|
||||
compressed: false,
|
||||
autoCompress: autoCompress,
|
||||
autoCompress: autoCompress,
|
||||
completedParts: make(map[uint16]bool),
|
||||
createdAt: time.Now(),
|
||||
progress: 0.0,
|
||||
createdAt: time.Now(),
|
||||
progress: 0.0,
|
||||
}
|
||||
|
||||
switch v := data.(type) {
|
||||
@@ -118,6 +119,10 @@ func New(data interface{}, autoCompress bool) (*Resource, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if namer, ok := v.(interface{ Name() string }); ok {
|
||||
r.fileName = namer.Name()
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unsupported data type")
|
||||
}
|
||||
@@ -138,10 +143,10 @@ func New(data interface{}, autoCompress bool) (*Resource, error) {
|
||||
r.transferSize = int64(float64(r.dataSize) * compressibility)
|
||||
} else if r.fileHandle != nil {
|
||||
// For file handles, use extension-based estimation
|
||||
ext := strings.ToLower(filepath.Ext(r.fileHandle.Name()))
|
||||
ext := strings.ToLower(filepath.Ext(r.fileName))
|
||||
r.transferSize = estimateFileCompression(r.dataSize, ext)
|
||||
}
|
||||
|
||||
|
||||
// Ensure minimum size and add compression overhead
|
||||
if r.transferSize < r.dataSize/10 {
|
||||
r.transferSize = r.dataSize / 10
|
||||
@@ -223,7 +228,7 @@ func (r *Resource) IsCompressed() bool {
|
||||
func (r *Resource) Cancel() {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
|
||||
if r.status == STATUS_PENDING || r.status == STATUS_ACTIVE {
|
||||
r.status = STATUS_CANCELLED
|
||||
r.completedAt = time.Now()
|
||||
@@ -342,13 +347,13 @@ func estimateCompressibility(data []byte) float64 {
|
||||
if len(data) < sampleSize {
|
||||
sampleSize = len(data)
|
||||
}
|
||||
|
||||
|
||||
// Count unique bytes in sample
|
||||
uniqueBytes := make(map[byte]struct{})
|
||||
for i := 0; i < sampleSize; i++ {
|
||||
uniqueBytes[data[i]] = struct{}{}
|
||||
}
|
||||
|
||||
|
||||
// Calculate entropy-based compression estimate
|
||||
uniqueRatio := float64(len(uniqueBytes)) / float64(sampleSize)
|
||||
return 0.3 + (0.7 * uniqueRatio) // Base compression ratio between 0.3 and 1.0
|
||||
@@ -357,13 +362,13 @@ func estimateCompressibility(data []byte) float64 {
|
||||
func estimateFileCompression(size int64, extension string) int64 {
|
||||
// Compression ratio estimates based on common file types
|
||||
compressionRatios := map[string]float64{
|
||||
".txt": 0.4, // Text compresses well
|
||||
".txt": 0.4, // Text compresses well
|
||||
".log": 0.4,
|
||||
".json": 0.4,
|
||||
".xml": 0.4,
|
||||
".html": 0.4,
|
||||
".csv": 0.5,
|
||||
".doc": 0.8, // Already compressed
|
||||
".doc": 0.8, // Already compressed
|
||||
".docx": 0.95,
|
||||
".pdf": 0.95,
|
||||
".jpg": 0.99, // Already compressed
|
||||
@@ -376,11 +381,43 @@ func estimateFileCompression(size int64, extension string) int64 {
|
||||
".gz": 0.99,
|
||||
".rar": 0.99,
|
||||
}
|
||||
|
||||
|
||||
ratio, exists := compressionRatios[extension]
|
||||
if !exists {
|
||||
ratio = 0.7 // Default compression ratio for unknown types
|
||||
}
|
||||
|
||||
|
||||
return int64(float64(size) * ratio)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resource) Read(p []byte) (n int, err error) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
if r.data != nil {
|
||||
if r.readOffset >= int64(len(r.data)) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n = copy(p, r.data[r.readOffset:])
|
||||
r.readOffset += int64(n)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
if r.fileHandle != nil {
|
||||
return r.fileHandle.Read(p)
|
||||
}
|
||||
|
||||
return 0, errors.New("no data source available")
|
||||
}
|
||||
|
||||
func (r *Resource) GetName() string {
|
||||
r.mutex.RLock()
|
||||
defer r.mutex.RUnlock()
|
||||
return r.fileName
|
||||
}
|
||||
|
||||
func (r *Resource) GetSize() int64 {
|
||||
r.mutex.RLock()
|
||||
defer r.mutex.RUnlock()
|
||||
return r.dataSize
|
||||
}
|
||||
|
||||
170
pkg/transport/announce.go
Normal file
170
pkg/transport/announce.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
52
revive.toml
Normal file
52
revive.toml
Normal 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]
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user