Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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"
|
||||||
28
.github/workflows/gosec.yml
vendored
Normal file
28
.github/workflows/gosec.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: "Security Scan"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * 0'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
env:
|
||||||
|
GO111MODULE: on
|
||||||
|
steps:
|
||||||
|
- name: Checkout Source
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Run Gosec Security Scanner
|
||||||
|
uses: securego/gosec@master
|
||||||
|
with:
|
||||||
|
args: '-no-fail -fmt sarif -out results.sarif ./...'
|
||||||
|
- name: Upload SARIF file
|
||||||
|
uses: github/codeql-action/upload-sarif@v2
|
||||||
|
with:
|
||||||
|
sarif_file: results.sarif
|
||||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1,8 +1,16 @@
|
|||||||
reticulum-client
|
|
||||||
reticulum-server
|
|
||||||
|
|
||||||
bin/
|
|
||||||
logs/
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
.env
|
.env
|
||||||
.json
|
.json
|
||||||
|
|
||||||
|
bin/
|
||||||
|
|
||||||
|
RNS/
|
||||||
|
|
||||||
|
test-network/go-rns-network/reticulum/storage/
|
||||||
|
test-network/go-rns-network/reticulum/interfaces/
|
||||||
|
test-network/go-rns-network/nomadnetwork
|
||||||
|
|
||||||
|
/test-network/go-rns-network/
|
||||||
|
/test-network/go-rns-network-client/
|
||||||
9
LICENSE
Normal file
9
LICENSE
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
|
||||||
|
Copyright 2024 Sudo-Ivan / Quad4.io
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
97
Makefile
Normal file
97
Makefile
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
GOCMD=go
|
||||||
|
GOBUILD=$(GOCMD) build
|
||||||
|
GOCLEAN=$(GOCMD) clean
|
||||||
|
GOTEST=$(GOCMD) test
|
||||||
|
GOGET=$(GOCMD) get
|
||||||
|
GOMOD=$(GOCMD) mod
|
||||||
|
BINARY_NAME=reticulum-go
|
||||||
|
BINARY_UNIX=$(BINARY_NAME)_unix
|
||||||
|
|
||||||
|
BUILD_DIR=bin
|
||||||
|
|
||||||
|
MAIN_PACKAGE=./cmd/reticulum-go
|
||||||
|
|
||||||
|
ALL_PACKAGES=$$(go list ./... | grep -v /vendor/)
|
||||||
|
|
||||||
|
.PHONY: all build clean test coverage deps help
|
||||||
|
|
||||||
|
all: clean deps build test
|
||||||
|
|
||||||
|
build:
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
$(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_PACKAGE)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@rm -rf $(BUILD_DIR)
|
||||||
|
$(GOCLEAN)
|
||||||
|
|
||||||
|
test:
|
||||||
|
$(GOTEST) -v $(ALL_PACKAGES)
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
$(GOTEST) -coverprofile=coverage.out $(ALL_PACKAGES)
|
||||||
|
$(GOCMD) tool cover -html=coverage.out
|
||||||
|
|
||||||
|
deps:
|
||||||
|
$(GOMOD) download
|
||||||
|
$(GOMOD) verify
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 $(MAIN_PACKAGE)
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 $(MAIN_PACKAGE)
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm $(MAIN_PACKAGE)
|
||||||
|
|
||||||
|
build-windows:
|
||||||
|
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe $(MAIN_PACKAGE)
|
||||||
|
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-arm64.exe $(MAIN_PACKAGE)
|
||||||
|
|
||||||
|
build-darwin:
|
||||||
|
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 $(MAIN_PACKAGE)
|
||||||
|
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 $(MAIN_PACKAGE)
|
||||||
|
|
||||||
|
build-freebsd:
|
||||||
|
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-amd64 $(MAIN_PACKAGE)
|
||||||
|
CGO_ENABLED=0 GOOS=freebsd GOARCH=386 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-386 $(MAIN_PACKAGE)
|
||||||
|
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-arm64 $(MAIN_PACKAGE)
|
||||||
|
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-arm $(MAIN_PACKAGE)
|
||||||
|
CGO_ENABLED=0 GOOS=freebsd GOARCH=riscv64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-riscv64 $(MAIN_PACKAGE)
|
||||||
|
|
||||||
|
build-openbsd:
|
||||||
|
CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-amd64 $(MAIN_PACKAGE)
|
||||||
|
CGO_ENABLED=0 GOOS=openbsd GOARCH=386 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-386 $(MAIN_PACKAGE)
|
||||||
|
CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-arm64 $(MAIN_PACKAGE)
|
||||||
|
CGO_ENABLED=0 GOOS=openbsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-arm $(MAIN_PACKAGE)
|
||||||
|
CGO_ENABLED=0 GOOS=openbsd GOARCH=ppc64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-ppc64 $(MAIN_PACKAGE)
|
||||||
|
CGO_ENABLED=0 GOOS=openbsd GOARCH=riscv64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-riscv64 $(MAIN_PACKAGE)
|
||||||
|
|
||||||
|
build-netbsd:
|
||||||
|
CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-amd64 $(MAIN_PACKAGE)
|
||||||
|
CGO_ENABLED=0 GOOS=netbsd GOARCH=386 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-386 $(MAIN_PACKAGE)
|
||||||
|
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm64 $(MAIN_PACKAGE)
|
||||||
|
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm $(MAIN_PACKAGE)
|
||||||
|
|
||||||
|
build-all: build-linux build-windows build-darwin build-freebsd build-openbsd build-netbsd
|
||||||
|
|
||||||
|
run:
|
||||||
|
@./$(BUILD_DIR)/$(BINARY_NAME)
|
||||||
|
|
||||||
|
install:
|
||||||
|
$(GOMOD) download
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo " all - Clean, download dependencies, build and test"
|
||||||
|
@echo " build - Build binary"
|
||||||
|
@echo " clean - Remove build artifacts"
|
||||||
|
@echo " test - Run tests"
|
||||||
|
@echo " coverage - Generate test coverage report"
|
||||||
|
@echo " deps - Download dependencies"
|
||||||
|
@echo " build-linux - Build for Linux (amd64, arm64, arm)"
|
||||||
|
@echo " build-windows- Build for Windows (amd64, arm64)"
|
||||||
|
@echo " build-darwin - Build for MacOS (amd64, arm64)"
|
||||||
|
@echo " build-freebsd- Build for FreeBSD (amd64, 386, arm64, arm, riscv64)"
|
||||||
|
@echo " build-openbsd- Build for OpenBSD (amd64, 386, arm64, arm, ppc64, riscv64)"
|
||||||
|
@echo " build-netbsd - Build for NetBSD (amd64, 386, arm64, arm)"
|
||||||
|
@echo " build-all - Build for all platforms and architectures"
|
||||||
|
@echo " run - Run reticulum binary"
|
||||||
|
@echo " install - Install dependencies"
|
||||||
189
README.md
189
README.md
@@ -1,4 +1,191 @@
|
|||||||
# Reticulum-Go
|
# Reticulum-Go
|
||||||
|
|
||||||
Reticulum Network Stack in Go.
|
[Reticulum Network](https://github.com/markqvist/Reticulum) implementation in Go `1.24`.
|
||||||
|
|
||||||
|
Aiming for full spec compatibility with the Python version 0.9.2.
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
```
|
||||||
|
make install
|
||||||
|
make build
|
||||||
|
make run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Linter
|
||||||
|
|
||||||
|
[Revive](https://github.com/mgechev/revive)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
revive -config revive.toml -formatter friendly ./pkg/* ./cmd/* ./internal/*
|
||||||
|
```
|
||||||
|
|
||||||
|
## External Packages
|
||||||
|
|
||||||
|
- `golang.org/x/crypto` - Cryptographic primitives
|
||||||
|
|
||||||
|
## To-Do List
|
||||||
|
|
||||||
|
### Core Components (In Progress)
|
||||||
|
- [x] Basic Configuration System
|
||||||
|
- [x] Basic config structure
|
||||||
|
- [x] Default settings
|
||||||
|
- [x] Config file loading/saving
|
||||||
|
- [x] Path management
|
||||||
|
|
||||||
|
- [x] Constants Definition (Testing required)
|
||||||
|
- [x] Packet constants
|
||||||
|
- [x] MTU constants
|
||||||
|
- [x] Header types
|
||||||
|
- [x] Additional protocol constants
|
||||||
|
|
||||||
|
- [x] Identity Management (Testing required)
|
||||||
|
- [x] Identity creation
|
||||||
|
- [x] Key pair generation
|
||||||
|
- [x] Identity storage/recall
|
||||||
|
- [x] Public key handling
|
||||||
|
- [x] Signature verification
|
||||||
|
- [x] Hash functions
|
||||||
|
|
||||||
|
- [x] Cryptographic Primitives (Testing required)
|
||||||
|
- [x] Ed25519
|
||||||
|
- [x] Curve25519
|
||||||
|
- [x] AES-CBC
|
||||||
|
- [x] SHA-256
|
||||||
|
- [x] HKDF
|
||||||
|
- [x] Secure random number generation
|
||||||
|
- [x] HMAC
|
||||||
|
|
||||||
|
- [x] Packet Handling (In Progress)
|
||||||
|
- [x] Packet creation
|
||||||
|
- [x] Packet validation
|
||||||
|
- [x] Basic proof system
|
||||||
|
- [x] Packet encryption/decryption
|
||||||
|
- [x] Signature verification
|
||||||
|
- [x] Announce packet structure
|
||||||
|
- [ ] Testing of packet encrypt/decrypt/sign/proof
|
||||||
|
- [ ] Cross-client packet compatibility
|
||||||
|
|
||||||
|
- [x] Transport Layer (In Progress)
|
||||||
|
- [x] Path management
|
||||||
|
- [x] Basic packet routing
|
||||||
|
- [x] Announce handling
|
||||||
|
- [x] Link management
|
||||||
|
- [x] Resource cleanup
|
||||||
|
- [x] Network layer integration
|
||||||
|
- [x] Basic announce implementation
|
||||||
|
- [ ] Testing announce from go client to python client
|
||||||
|
- [ ] Testing path finding and caching
|
||||||
|
- [ ] Announce propagation optimization
|
||||||
|
|
||||||
|
- [x] Channel System (Testing Required)
|
||||||
|
- [x] Channel creation and management
|
||||||
|
- [x] Message handling
|
||||||
|
- [x] Channel encryption
|
||||||
|
- [x] Channel authentication
|
||||||
|
- [x] Channel callbacks
|
||||||
|
- [x] Integration with Buffer system
|
||||||
|
- [ ] Testing with real network conditions
|
||||||
|
- [ ] Cross-client compatibility testing
|
||||||
|
|
||||||
|
- [x] Buffer System (Testing Required)
|
||||||
|
- [x] Raw channel reader/writer
|
||||||
|
- [x] Buffered stream implementation
|
||||||
|
- [x] Compression support
|
||||||
|
- [ ] Testing with Channel system
|
||||||
|
- [ ] Cross-client compatibility testing
|
||||||
|
|
||||||
|
- [x] Resolver System (Testing Required)
|
||||||
|
- [x] Name resolution
|
||||||
|
- [x] Cache management
|
||||||
|
- [x] Announce handling
|
||||||
|
- [x] Path resolution
|
||||||
|
- [x] Integration with Transport layer
|
||||||
|
- [ ] Testing with live network
|
||||||
|
- [ ] Cross-client compatibility testing
|
||||||
|
|
||||||
|
### Interface Implementation (In Progress)
|
||||||
|
- [x] UDP Interface
|
||||||
|
- [x] TCP Interface
|
||||||
|
- [x] Auto Interface
|
||||||
|
- [ ] Local Interface (In Progress)
|
||||||
|
- [ ] I2P Interface
|
||||||
|
- [ ] Pipe Interface
|
||||||
|
- [ ] RNode Interface
|
||||||
|
- [ ] RNode Multiinterface
|
||||||
|
- [ ] Serial Interface
|
||||||
|
- [ ] AX25KISS Interface
|
||||||
|
- [ ] Interface Discovery
|
||||||
|
- [ ] Interface Modes
|
||||||
|
- [ ] Full mode
|
||||||
|
- [ ] Gateway mode
|
||||||
|
- [ ] Access point mode
|
||||||
|
- [ ] Roaming mode
|
||||||
|
- [ ] Boundary mode
|
||||||
|
|
||||||
|
- [ ] Hot reloading interfaces
|
||||||
|
|
||||||
|
### Destination System (Testing required)
|
||||||
|
- [x] Destination creation
|
||||||
|
- [x] Destination types (IN/OUT)
|
||||||
|
- [x] Destination aspects
|
||||||
|
- [x] Announce implementation
|
||||||
|
- [x] Ratchet support
|
||||||
|
- [x] Request handlers
|
||||||
|
|
||||||
|
### Link System (Testing required)
|
||||||
|
- [x] Link establishment
|
||||||
|
- [x] Link teardown
|
||||||
|
- [x] Basic packet transfer
|
||||||
|
- [x] Encryption/Decryption
|
||||||
|
- [x] Identity verification
|
||||||
|
- [x] Request/Response handling
|
||||||
|
- [x] Session key management
|
||||||
|
- [x] Link state tracking
|
||||||
|
|
||||||
|
### Resource System (Testing required)
|
||||||
|
- [x] Resource creation
|
||||||
|
- [x] Resource transfer
|
||||||
|
- [x] Compression
|
||||||
|
- [x] Progress tracking
|
||||||
|
- [x] Segmentation
|
||||||
|
- [x] Cleanup routines
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
- [ ] RNS Utilities.
|
||||||
|
- [ ] Reticulum config.
|
||||||
|
|
||||||
|
|
||||||
|
### Testing & Validation (Priority)
|
||||||
|
- [ ] Unit tests for all components
|
||||||
|
- [ ] Identity tests
|
||||||
|
- [ ] Packet tests
|
||||||
|
- [ ] Transport tests
|
||||||
|
- [ ] Interface tests
|
||||||
|
- [ ] Announce tests
|
||||||
|
- [ ] Channel tests
|
||||||
|
- [ ] Buffer tests
|
||||||
|
- [ ] Resolver tests
|
||||||
|
- [ ] Link tests
|
||||||
|
- [ ] Resource tests
|
||||||
|
- [ ] Integration tests
|
||||||
|
- [ ] Go client to Go client
|
||||||
|
- [ ] Go client to Python client
|
||||||
|
- [ ] Interface compatibility
|
||||||
|
- [ ] Path finding and resolution
|
||||||
|
- [ ] Channel system end-to-end
|
||||||
|
- [ ] Buffer system performance
|
||||||
|
- [ ] Cross-client compatibility tests
|
||||||
|
- [ ] Performance benchmarks
|
||||||
|
- [ ] Security auditing (When Reticulum is 1.0 / stable)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- [ ] API documentation
|
||||||
|
- [ ] Usage examples
|
||||||
|
|
||||||
|
### Cleanup
|
||||||
|
- [ ] Separate Cryptography from identity.go to their own files
|
||||||
|
- [ ] Move constants to their own files
|
||||||
|
- [ ] Remove default community interfaces in default config creation after testing
|
||||||
|
- [ ] Optimize announce packet creation and caching
|
||||||
|
- [ ] Improve debug logging system
|
||||||
47
SECURITY.md
Normal file
47
SECURITY.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
I use [Socket](https://socket.dev/), [Deepsource](https://deepsource.com/) and [gosec](https://github.com/securego/gosec) for this project.
|
||||||
|
|
||||||
|
## Cryptography Dependencies
|
||||||
|
|
||||||
|
- golang.org/x/crypto for core cryptographic primitives
|
||||||
|
- hkdf
|
||||||
|
- curve25519
|
||||||
|
|
||||||
|
- go/crypto
|
||||||
|
- ed25519
|
||||||
|
- sha256
|
||||||
|
- rand
|
||||||
|
- aes
|
||||||
|
- cipher
|
||||||
|
- hmac
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Please report any security vulnerabilities to [rns@quad4.io](mailto:rns@quad4.io)
|
||||||
|
|
||||||
|
**PGP Key:**
|
||||||
|
|
||||||
|
```
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
xjMEZ3RaxBYJKwYBBAHaRw8BAQdAcW8OFXyQ6KuqoTWKVbULYgakD/CeW50y
|
||||||
|
W0KFou8WwJTNG3Juc0BxdWFkNC5pbyA8cm5zQHF1YWQ0LmlvPsLAEQQTFgoA
|
||||||
|
gwWCZ3RaxAMLCQcJkJm7qyNLc8pmRRQAAAAAABwAIHNhbHRAbm90YXRpb25z
|
||||||
|
Lm9wZW5wZ3Bqcy5vcmdVRY9jqwrIm+oRWRFnnBjKUcqvkG/kwkQZ3T74Xz3K
|
||||||
|
QQMVCggEFgACAQIZAQKbAwIeARYhBG62BFzXpfHCy0yV95m7qyNLc8pmAACS
|
||||||
|
oQD+K8oIaGx3tOlQbBV5AT3pHCaqXpRoL4W0V4JWc3VCi+MA/iiW6peitoae
|
||||||
|
+YhKE5lnkiU1jP47VuItQDNt+fNyqNAOzjgEZ3RaxBIKKwYBBAGXVQEFAQEH
|
||||||
|
QOBQyIb3gXV0Uih/V9Yx5JsFavxSenCtncNXx5KM6cB8AwEIB8K+BBgWCgBw
|
||||||
|
BYJndFrECZCZu6sjS3PKZkUUAAAAAAAcACBzYWx0QG5vdGF0aW9ucy5vcGVu
|
||||||
|
cGdwanMub3Jnpqm3qWGYB50CM/kuv+byGwQ3wxIGIpRlK8pwT4l+wXICmwwW
|
||||||
|
IQRutgRc16XxwstMlfeZu6sjS3PKZgAAzm0BAIKHfL9G+IzCX9B1gVGcG9an
|
||||||
|
j+gC4y9FrEsmFEBpvGeXAP93FfhO447jWijmxsImTtHTyvhpfeR3a7huFFyi
|
||||||
|
lh60DA==
|
||||||
|
=Nm9f
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gosec Command
|
||||||
|
|
||||||
|
`gosec ./cmd/* ./pkg/* ./internal/*`
|
||||||
102
To-Do
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
|
|
||||||
BIN
assets/status-update-2jan2025.png
Normal file
BIN
assets/status-update-2jan2025.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 315 KiB |
@@ -1,137 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/internal/config"
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/destination"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
configPath = flag.String("config", "", "Path to config file")
|
|
||||||
targetHash = flag.String("target", "", "Target destination hash")
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
var cfg *common.ReticulumConfig
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if *configPath == "" {
|
|
||||||
cfg, err = config.InitConfig()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to initialize config: %v", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cfg, err = config.LoadConfig(*configPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to load config: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable transport by default for client
|
|
||||||
cfg.EnableTransport = true
|
|
||||||
|
|
||||||
// Initialize transport
|
|
||||||
transport, err := transport.NewTransport(cfg)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to initialize transport: %v", err)
|
|
||||||
}
|
|
||||||
defer transport.Close()
|
|
||||||
|
|
||||||
// If target specified, establish connection
|
|
||||||
if *targetHash != "" {
|
|
||||||
destHash, err := identity.HashFromHex(*targetHash)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Invalid destination hash: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request path if needed
|
|
||||||
if !transport.HasPath(destHash) {
|
|
||||||
fmt.Println("Requesting path to destination...")
|
|
||||||
if err := transport.RequestPath(destHash, "", nil, true); err != nil {
|
|
||||||
log.Fatalf("Failed to request path: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get destination identity
|
|
||||||
destIdentity, err := identity.Recall(destHash)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to recall identity: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create destination
|
|
||||||
dest, err := destination.New(
|
|
||||||
destIdentity,
|
|
||||||
destination.OUT,
|
|
||||||
destination.SINGLE,
|
|
||||||
"client",
|
|
||||||
"direct",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to create destination: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable and configure ratchets
|
|
||||||
dest.SetRetainedRatchets(destination.RATCHET_COUNT)
|
|
||||||
dest.SetRatchetInterval(destination.RATCHET_INTERVAL)
|
|
||||||
dest.EnforceRatchets()
|
|
||||||
|
|
||||||
// Create link
|
|
||||||
link := transport.NewLink(dest.Hash(), func() {
|
|
||||||
fmt.Println("Link established")
|
|
||||||
}, func() {
|
|
||||||
fmt.Println("Link closed")
|
|
||||||
})
|
|
||||||
|
|
||||||
defer link.Teardown()
|
|
||||||
|
|
||||||
// Set packet callback
|
|
||||||
link.SetPacketCallback(func(data []byte) {
|
|
||||||
fmt.Printf("Received: %s\n", string(data))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Start interactive loop
|
|
||||||
go interactiveLoop(link)
|
|
||||||
} else {
|
|
||||||
fmt.Println("No target specified. Use -target <hash> to connect to a destination")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for interrupt
|
|
||||||
sigChan := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
<-sigChan
|
|
||||||
}
|
|
||||||
|
|
||||||
func interactiveLoop(link *transport.Link) {
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
|
||||||
for {
|
|
||||||
fmt.Print("> ")
|
|
||||||
input, err := reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error reading input: %v\n", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
input = strings.TrimSpace(input)
|
|
||||||
if input == "quit" || input == "exit" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := link.Send([]byte(input)); err != nil {
|
|
||||||
fmt.Printf("Failed to send: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
642
cmd/reticulum-go/main.go
Normal file
642
cmd/reticulum-go/main.go
Normal file
@@ -0,0 +1,642 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/internal/config"
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/announce"
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/buffer"
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/channel"
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/destination"
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/interfaces"
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/packet"
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
debugLevel = flag.Int("debug", 7, "Debug level (0-7)")
|
||||||
|
interceptPackets = flag.Bool("intercept-packets", false, "Enable packet interception")
|
||||||
|
interceptOutput = flag.String("intercept-output", "packets.log", "Output file for intercepted packets")
|
||||||
|
)
|
||||||
|
|
||||||
|
func debugLog(level int, format string, v ...interface{}) {
|
||||||
|
if *debugLevel >= level {
|
||||||
|
log.Printf("[DEBUG-%d] %s", level, fmt.Sprintf(format, v...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ANNOUNCE_RATE_TARGET = 3600 // Default target time between announces (1 hour)
|
||||||
|
ANNOUNCE_RATE_GRACE = 3 // Number of grace announces before enforcing rate
|
||||||
|
ANNOUNCE_RATE_PENALTY = 7200 // Additional penalty time for rate violations
|
||||||
|
MAX_ANNOUNCE_HOPS = 128 // Maximum number of hops for announces
|
||||||
|
DEBUG_CRITICAL = 1 // Critical errors
|
||||||
|
DEBUG_ERROR = 2 // Non-critical errors
|
||||||
|
DEBUG_INFO = 3 // Important information
|
||||||
|
DEBUG_VERBOSE = 4 // Detailed information
|
||||||
|
DEBUG_TRACE = 5 // Very detailed tracing
|
||||||
|
DEBUG_PACKETS = 6 // Packet-level details
|
||||||
|
DEBUG_ALL = 7 // Everything including identity operations
|
||||||
|
APP_NAME = "Go-Client"
|
||||||
|
APP_ASPECT = "node"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Reticulum struct {
|
||||||
|
config *common.ReticulumConfig
|
||||||
|
transport *transport.Transport
|
||||||
|
interfaces []interfaces.Interface
|
||||||
|
channels map[string]*channel.Channel
|
||||||
|
buffers map[string]*buffer.Buffer
|
||||||
|
pathRequests map[string]*common.PathRequest
|
||||||
|
announceHistory map[string]announceRecord
|
||||||
|
announceHistoryMu sync.RWMutex
|
||||||
|
identity *identity.Identity
|
||||||
|
destination *destination.Destination
|
||||||
|
}
|
||||||
|
|
||||||
|
type announceRecord struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
cfg = config.DefaultConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default app name and aspect if not provided
|
||||||
|
if cfg.AppName == "" {
|
||||||
|
cfg.AppName = "Go Client"
|
||||||
|
}
|
||||||
|
if cfg.AppAspect == "" {
|
||||||
|
cfg.AppAspect = "node"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := initializeDirectories(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize directories: %v", err)
|
||||||
|
}
|
||||||
|
debugLog(3, "Directories initialized")
|
||||||
|
|
||||||
|
t := transport.NewTransport(cfg)
|
||||||
|
debugLog(3, "Transport initialized")
|
||||||
|
|
||||||
|
identity, err := identity.NewIdentity()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create identity: %v", err)
|
||||||
|
}
|
||||||
|
debugLog(2, "Created new identity: %x", identity.Hash())
|
||||||
|
|
||||||
|
// Create destination
|
||||||
|
debugLog(DEBUG_INFO, "Creating destination...")
|
||||||
|
dest, err := destination.New(
|
||||||
|
identity,
|
||||||
|
destination.IN,
|
||||||
|
destination.SINGLE,
|
||||||
|
APP_NAME,
|
||||||
|
APP_ASPECT,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create destination: %v", err)
|
||||||
|
}
|
||||||
|
debugLog(DEBUG_INFO, "Created destination with hash: %x", dest.GetHash())
|
||||||
|
|
||||||
|
// Set default app data for announces
|
||||||
|
appData := []byte(fmt.Sprintf(`{"app":"%s","aspect":"%s","version":"0.1.0"}`, APP_NAME, APP_ASPECT))
|
||||||
|
dest.SetDefaultAppData(appData)
|
||||||
|
|
||||||
|
// Enable destination features
|
||||||
|
dest.AcceptsLinks(true)
|
||||||
|
dest.EnableRatchets("") // Empty string for default path
|
||||||
|
dest.SetProofStrategy(destination.PROVE_APP)
|
||||||
|
debugLog(DEBUG_VERBOSE, "Configured destination features")
|
||||||
|
|
||||||
|
r := &Reticulum{
|
||||||
|
config: cfg,
|
||||||
|
transport: t,
|
||||||
|
interfaces: make([]interfaces.Interface, 0),
|
||||||
|
channels: make(map[string]*channel.Channel),
|
||||||
|
buffers: make(map[string]*buffer.Buffer),
|
||||||
|
pathRequests: make(map[string]*common.PathRequest),
|
||||||
|
announceHistory: make(map[string]announceRecord),
|
||||||
|
identity: identity,
|
||||||
|
destination: dest,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize interfaces from config
|
||||||
|
for name, ifaceConfig := range cfg.Interfaces {
|
||||||
|
if !ifaceConfig.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var iface interfaces.Interface
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch ifaceConfig.Type {
|
||||||
|
case "TCPClientInterface":
|
||||||
|
iface, err = interfaces.NewTCPClientInterface(
|
||||||
|
name,
|
||||||
|
ifaceConfig.TargetHost,
|
||||||
|
ifaceConfig.TargetPort,
|
||||||
|
ifaceConfig.Enabled,
|
||||||
|
true, // IN
|
||||||
|
true, // OUT
|
||||||
|
)
|
||||||
|
case "UDPInterface":
|
||||||
|
iface, err = interfaces.NewUDPInterface(
|
||||||
|
name,
|
||||||
|
ifaceConfig.Address,
|
||||||
|
ifaceConfig.TargetHost,
|
||||||
|
ifaceConfig.Enabled,
|
||||||
|
)
|
||||||
|
case "AutoInterface":
|
||||||
|
iface, err = interfaces.NewAutoInterface(name, ifaceConfig)
|
||||||
|
default:
|
||||||
|
debugLog(1, "Unknown interface type: %s", ifaceConfig.Type)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if cfg.PanicOnInterfaceErr {
|
||||||
|
return nil, fmt.Errorf("failed to create interface %s: %v", name, err)
|
||||||
|
}
|
||||||
|
debugLog(1, "Error creating interface %s: %v", name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set packet callback
|
||||||
|
iface.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
|
||||||
|
if r.transport != nil {
|
||||||
|
r.transport.HandlePacket(data, ni)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
debugLog(2, "Configuring interface %s (type=%s)...", name, ifaceConfig.Type)
|
||||||
|
r.interfaces = append(r.interfaces, iface)
|
||||||
|
debugLog(3, "Interface %s started successfully", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reticulum) handleInterface(iface common.NetworkInterface) {
|
||||||
|
debugLog(DEBUG_INFO, "Setting up interface %s (type=%T)", iface.GetName(), iface)
|
||||||
|
|
||||||
|
ch := channel.NewChannel(&transportWrapper{r.transport})
|
||||||
|
r.channels[iface.GetName()] = ch
|
||||||
|
|
||||||
|
rw := buffer.CreateBidirectionalBuffer(
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
ch,
|
||||||
|
func(size int) {
|
||||||
|
data := make([]byte, size)
|
||||||
|
debugLog(DEBUG_PACKETS, "Interface %s: Reading %d bytes from buffer", iface.GetName(), size)
|
||||||
|
iface.ProcessIncoming(data)
|
||||||
|
|
||||||
|
if len(data) > 0 {
|
||||||
|
debugLog(DEBUG_TRACE, "Interface %s: Received packet type 0x%02x", iface.GetName(), data[0])
|
||||||
|
r.transport.HandlePacket(data, iface)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
r.buffers[iface.GetName()] = &buffer.Buffer{
|
||||||
|
ReadWriter: rw,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reticulum) monitorInterfaces() {
|
||||||
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
for _, iface := range r.interfaces {
|
||||||
|
if tcpClient, ok := iface.(*interfaces.TCPClientInterface); ok {
|
||||||
|
stats := fmt.Sprintf("Interface %s status - Connected: %v, TX: %d bytes (%.2f Kbps), RX: %d bytes (%.2f Kbps)",
|
||||||
|
iface.GetName(),
|
||||||
|
tcpClient.IsConnected(),
|
||||||
|
tcpClient.GetTxBytes(),
|
||||||
|
float64(tcpClient.GetTxBytes()*8)/(5*1024),
|
||||||
|
tcpClient.GetRxBytes(),
|
||||||
|
float64(tcpClient.GetRxBytes()*8)/(5*1024),
|
||||||
|
)
|
||||||
|
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
stats = fmt.Sprintf("%s, RTT: %v", stats, tcpClient.GetRTT())
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog(DEBUG_VERBOSE, stats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
debugLog(1, "Initializing Reticulum (Debug Level: %d)...", *debugLevel)
|
||||||
|
|
||||||
|
cfg, err := config.InitConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to initialize config: %v", err)
|
||||||
|
}
|
||||||
|
debugLog(2, "Configuration loaded from: %s", cfg.ConfigPath)
|
||||||
|
|
||||||
|
// Add default TCP interfaces if none configured
|
||||||
|
if len(cfg.Interfaces) == 0 {
|
||||||
|
debugLog(2, "No interfaces configured, adding default TCP interfaces")
|
||||||
|
cfg.Interfaces = make(map[string]*common.InterfaceConfig)
|
||||||
|
|
||||||
|
cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
|
||||||
|
Type: "TCPClientInterface",
|
||||||
|
Enabled: true,
|
||||||
|
TargetHost: "127.0.0.1",
|
||||||
|
TargetPort: 4242,
|
||||||
|
Name: "Go-RNS-Testnet",
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Interfaces["Quad4 TCP"] = &common.InterfaceConfig{
|
||||||
|
Type: "TCPClientInterface",
|
||||||
|
Enabled: true,
|
||||||
|
TargetHost: "rns.quad4.io",
|
||||||
|
TargetPort: 4242,
|
||||||
|
Name: "Quad4 TCP",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := NewReticulum(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create Reticulum instance: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create announce using r.identity
|
||||||
|
announce, err := announce.NewAnnounce(
|
||||||
|
r.identity,
|
||||||
|
[]byte("HELLO WORLD"),
|
||||||
|
nil,
|
||||||
|
false,
|
||||||
|
r.config,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create announce: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start monitoring interfaces
|
||||||
|
go r.monitorInterfaces()
|
||||||
|
|
||||||
|
// Register announce handler
|
||||||
|
handler := NewAnnounceHandler(r, []string{"*"})
|
||||||
|
r.transport.RegisterAnnounceHandler(handler)
|
||||||
|
|
||||||
|
// Start Reticulum
|
||||||
|
if err := r.Start(); err != nil {
|
||||||
|
log.Fatalf("Failed to start Reticulum: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send initial announces after interfaces are ready
|
||||||
|
time.Sleep(2 * time.Second) // Give interfaces time to connect
|
||||||
|
for _, iface := range r.interfaces {
|
||||||
|
if netIface, ok := iface.(common.NetworkInterface); ok {
|
||||||
|
if netIface.IsEnabled() && netIface.IsOnline() {
|
||||||
|
debugLog(2, "Sending initial announce on interface %s", netIface.GetName())
|
||||||
|
if err := announce.Propagate([]common.NetworkInterface{netIface}); err != nil {
|
||||||
|
debugLog(1, "Failed to propagate initial announce: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start periodic announces
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(5 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
debugLog(3, "Starting periodic announce cycle")
|
||||||
|
for _, iface := range r.interfaces {
|
||||||
|
if netIface, ok := iface.(common.NetworkInterface); ok {
|
||||||
|
if netIface.IsEnabled() && netIface.IsOnline() {
|
||||||
|
debugLog(2, "Sending periodic announce on interface %s", netIface.GetName())
|
||||||
|
if err := announce.Propagate([]common.NetworkInterface{netIface}); err != nil {
|
||||||
|
debugLog(1, "Failed to propagate periodic announce: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-sigChan
|
||||||
|
|
||||||
|
debugLog(1, "Shutting down...")
|
||||||
|
if err := r.Stop(); err != nil {
|
||||||
|
debugLog(1, "Error during shutdown: %v", err)
|
||||||
|
}
|
||||||
|
debugLog(1, "Goodbye!")
|
||||||
|
}
|
||||||
|
|
||||||
|
type transportWrapper struct {
|
||||||
|
*transport.Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *transportWrapper) GetRTT() float64 {
|
||||||
|
return 0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *transportWrapper) RTT() float64 {
|
||||||
|
return tw.GetRTT()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *transportWrapper) GetStatus() int {
|
||||||
|
return transport.STATUS_ACTIVE
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *transportWrapper) Send(data []byte) interface{} {
|
||||||
|
p := &packet.Packet{
|
||||||
|
PacketType: packet.PacketTypeData,
|
||||||
|
Hops: 0,
|
||||||
|
Data: data,
|
||||||
|
HeaderType: packet.HeaderType1,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := tw.Transport.SendPacket(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *transportWrapper) Resend(p interface{}) error {
|
||||||
|
if pkt, ok := p.(*packet.Packet); ok {
|
||||||
|
return tw.Transport.SendPacket(pkt)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("invalid packet type")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *transportWrapper) SetPacketTimeout(packet interface{}, callback func(interface{}), timeout time.Duration) {
|
||||||
|
time.AfterFunc(timeout, func() {
|
||||||
|
callback(packet)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *transportWrapper) SetPacketDelivered(packet interface{}, callback func(interface{})) {
|
||||||
|
callback(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initializeDirectories() error {
|
||||||
|
dirs := []string{
|
||||||
|
".reticulum-go",
|
||||||
|
".reticulum-go/storage",
|
||||||
|
".reticulum-go/storage/destinations",
|
||||||
|
".reticulum-go/storage/identities",
|
||||||
|
".reticulum-go/storage/ratchets",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create directory %s: %v", dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reticulum) Start() error {
|
||||||
|
debugLog(2, "Starting Reticulum...")
|
||||||
|
|
||||||
|
if err := r.transport.Start(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start transport: %v", err)
|
||||||
|
}
|
||||||
|
debugLog(3, "Transport started successfully")
|
||||||
|
|
||||||
|
// Start interfaces
|
||||||
|
for _, iface := range r.interfaces {
|
||||||
|
debugLog(2, "Starting interface %s...", iface.GetName())
|
||||||
|
if err := iface.Start(); err != nil {
|
||||||
|
if r.config.PanicOnInterfaceErr {
|
||||||
|
return fmt.Errorf("failed to start interface %s: %v", iface.GetName(), err)
|
||||||
|
}
|
||||||
|
debugLog(1, "Error starting interface %s: %v", iface.GetName(), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if netIface, ok := iface.(common.NetworkInterface); ok {
|
||||||
|
r.handleInterface(netIface)
|
||||||
|
}
|
||||||
|
debugLog(3, "Interface %s started successfully", iface.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for interfaces to initialize
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
// Send initial announce once per interface
|
||||||
|
initialAnnounce, err := announce.NewAnnounce(
|
||||||
|
r.identity,
|
||||||
|
createAppData(r.config.AppName, r.config.AppAspect),
|
||||||
|
nil,
|
||||||
|
false,
|
||||||
|
r.config,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create announce: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, iface := range r.interfaces {
|
||||||
|
if netIface, ok := iface.(common.NetworkInterface); ok {
|
||||||
|
if netIface.IsEnabled() && netIface.IsOnline() {
|
||||||
|
debugLog(2, "Sending initial announce on interface %s", netIface.GetName())
|
||||||
|
if err := initialAnnounce.Propagate([]common.NetworkInterface{netIface}); err != nil {
|
||||||
|
debugLog(1, "Failed to send initial announce on interface %s: %v", netIface.GetName(), err)
|
||||||
|
}
|
||||||
|
// Add delay between interfaces
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start periodic announce goroutine with rate limiting
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(ANNOUNCE_RATE_TARGET * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
announceCount := 0
|
||||||
|
for range ticker.C {
|
||||||
|
announceCount++
|
||||||
|
debugLog(3, "Starting periodic announce cycle #%d", announceCount)
|
||||||
|
|
||||||
|
periodicAnnounce, err := announce.NewAnnounce(
|
||||||
|
r.identity,
|
||||||
|
createAppData(r.config.AppName, r.config.AppAspect),
|
||||||
|
nil,
|
||||||
|
false,
|
||||||
|
r.config,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
debugLog(1, "Failed to create periodic announce: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send to each interface with rate limiting
|
||||||
|
for _, iface := range r.interfaces {
|
||||||
|
if netIface, ok := iface.(common.NetworkInterface); ok {
|
||||||
|
if netIface.IsEnabled() && netIface.IsOnline() {
|
||||||
|
// Apply rate limiting after grace period
|
||||||
|
if announceCount > ANNOUNCE_RATE_GRACE {
|
||||||
|
time.Sleep(time.Duration(ANNOUNCE_RATE_PENALTY) * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog(2, "Sending periodic announce on interface %s", netIface.GetName())
|
||||||
|
if err := periodicAnnounce.Propagate([]common.NetworkInterface{netIface}); err != nil {
|
||||||
|
debugLog(1, "Failed to send periodic announce on interface %s: %v", netIface.GetName(), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go r.monitorInterfaces()
|
||||||
|
|
||||||
|
debugLog(2, "Reticulum started successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reticulum) Stop() error {
|
||||||
|
debugLog(2, "Stopping Reticulum...")
|
||||||
|
|
||||||
|
for _, buf := range r.buffers {
|
||||||
|
if err := buf.Close(); err != nil {
|
||||||
|
debugLog(1, "Error closing buffer: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ch := range r.channels {
|
||||||
|
if err := ch.Close(); err != nil {
|
||||||
|
debugLog(1, "Error closing channel: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, iface := range r.interfaces {
|
||||||
|
if err := iface.Stop(); err != nil {
|
||||||
|
debugLog(1, "Error stopping interface %s: %v", iface.GetName(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.transport.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close transport: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog(2, "Reticulum stopped successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnnounceHandler struct {
|
||||||
|
aspectFilter []string
|
||||||
|
reticulum *Reticulum
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnnounceHandler(r *Reticulum, aspectFilter []string) *AnnounceHandler {
|
||||||
|
return &AnnounceHandler{
|
||||||
|
aspectFilter: aspectFilter,
|
||||||
|
reticulum: r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnnounceHandler) AspectFilter() []string {
|
||||||
|
return h.aspectFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appData []byte) error {
|
||||||
|
debugLog(DEBUG_INFO, "Received announce from %x", destHash)
|
||||||
|
debugLog(DEBUG_PACKETS, "Raw announce data: %x", appData)
|
||||||
|
|
||||||
|
// Parse msgpack array
|
||||||
|
if len(appData) > 0 {
|
||||||
|
if appData[0] == 0x92 { // msgpack array of 2 elements
|
||||||
|
var pos = 1
|
||||||
|
|
||||||
|
// Parse first element (name)
|
||||||
|
if appData[pos] == 0xc4 { // bin 8 format
|
||||||
|
nameLen := int(appData[pos+1])
|
||||||
|
name := string(appData[pos+2 : pos+2+nameLen])
|
||||||
|
pos += 2 + nameLen
|
||||||
|
debugLog(DEBUG_VERBOSE, "Announce name: %s", name)
|
||||||
|
|
||||||
|
// Parse second element (app data)
|
||||||
|
if pos < len(appData) && appData[pos] == 0xc4 { // bin 8 format
|
||||||
|
dataLen := int(appData[pos+1])
|
||||||
|
data := appData[pos+2 : pos+2+dataLen]
|
||||||
|
debugLog(DEBUG_VERBOSE, "Announce app data: %s", string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type assert and log identity details
|
||||||
|
if identity, ok := id.(*identity.Identity); ok {
|
||||||
|
debugLog(DEBUG_ALL, "Identity details:")
|
||||||
|
debugLog(DEBUG_ALL, " Hash: %s", identity.GetHexHash())
|
||||||
|
debugLog(DEBUG_ALL, " Public Key: %x", identity.GetPublicKey())
|
||||||
|
|
||||||
|
ratchets := identity.GetRatchets()
|
||||||
|
debugLog(DEBUG_ALL, " Active Ratchets: %d", len(ratchets))
|
||||||
|
|
||||||
|
if len(ratchets) > 0 {
|
||||||
|
ratchetKey := identity.GetCurrentRatchetKey()
|
||||||
|
if ratchetKey != nil {
|
||||||
|
ratchetID := identity.GetRatchetID(ratchetKey)
|
||||||
|
debugLog(DEBUG_ALL, " Current Ratchet ID: %x", ratchetID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store announce in history
|
||||||
|
h.reticulum.announceHistoryMu.Lock()
|
||||||
|
h.reticulum.announceHistory[identity.GetHexHash()] = announceRecord{
|
||||||
|
// You can add fields here to store relevant announce data
|
||||||
|
}
|
||||||
|
h.reticulum.announceHistoryMu.Unlock()
|
||||||
|
|
||||||
|
debugLog(DEBUG_VERBOSE, "Stored announce in history for identity %s", identity.GetHexHash())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnnounceHandler) ReceivePathResponses() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reticulum) GetDestination() *destination.Destination {
|
||||||
|
return r.destination
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAppData(appName, appAspect string) []byte {
|
||||||
|
nameString := fmt.Sprintf("%s.%s", appName, appAspect)
|
||||||
|
|
||||||
|
// Create MessagePack array with 2 elements
|
||||||
|
appData := []byte{0x92} // Fix array with 2 elements
|
||||||
|
|
||||||
|
// First element: name string (always use str 8 format for consistency)
|
||||||
|
nameBytes := []byte(nameString)
|
||||||
|
appData = append(appData, 0xd9) // str 8 format
|
||||||
|
appData = append(appData, byte(len(nameBytes))) // length
|
||||||
|
appData = append(appData, nameBytes...) // string data
|
||||||
|
|
||||||
|
// Second element: version string (always use str 8 format for consistency)
|
||||||
|
version := "0.1.0"
|
||||||
|
versionBytes := []byte(version)
|
||||||
|
appData = append(appData, 0xd9) // str 8 format
|
||||||
|
appData = append(appData, byte(len(versionBytes))) // length
|
||||||
|
appData = append(appData, versionBytes...) // string data
|
||||||
|
|
||||||
|
return appData
|
||||||
|
}
|
||||||
@@ -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
|
module github.com/Sudo-Ivan/reticulum-go
|
||||||
|
|
||||||
go 1.23.4
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require golang.org/x/crypto v0.31.0
|
||||||
github.com/pelletier/go-toml v1.9.5
|
|
||||||
golang.org/x/crypto v0.31.0
|
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
|
||||||
)
|
|
||||||
|
|||||||
6
go.sum
6
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 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|||||||
@@ -1,28 +1,31 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/pelletier/go-toml"
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DefaultSharedInstancePort = 37428
|
DefaultSharedInstancePort = 37428
|
||||||
DefaultInstanceControlPort = 37429
|
DefaultInstanceControlPort = 37429
|
||||||
DefaultLogLevel = 4
|
DefaultLogLevel = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
func DefaultConfig() *common.ReticulumConfig {
|
func DefaultConfig() *common.ReticulumConfig {
|
||||||
return &common.ReticulumConfig{
|
return &common.ReticulumConfig{
|
||||||
EnableTransport: false,
|
EnableTransport: true,
|
||||||
ShareInstance: true,
|
ShareInstance: true,
|
||||||
SharedInstancePort: DefaultSharedInstancePort,
|
SharedInstancePort: DefaultSharedInstancePort,
|
||||||
InstanceControlPort: DefaultInstanceControlPort,
|
InstanceControlPort: DefaultInstanceControlPort,
|
||||||
PanicOnInterfaceErr: false,
|
PanicOnInterfaceErr: false,
|
||||||
LogLevel: DefaultLogLevel,
|
LogLevel: DefaultLogLevel,
|
||||||
Interfaces: make(map[string]common.InterfaceConfig),
|
Interfaces: make(map[string]*common.InterfaceConfig),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +34,7 @@ func GetConfigPath() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return filepath.Join(homeDir, ".reticulum", "config"), nil
|
return filepath.Join(homeDir, ".reticulum-go", "config"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnsureConfigDir() error {
|
func EnsureConfigDir() error {
|
||||||
@@ -40,65 +43,212 @@ func EnsureConfigDir() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Join(homeDir, ".reticulum")
|
configDir := filepath.Join(homeDir, ".reticulum-go")
|
||||||
return os.MkdirAll(configDir, 0755)
|
return os.MkdirAll(configDir, 0755)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseValue parses string values into appropriate types
|
||||||
|
func parseValue(value string) interface{} {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
|
||||||
|
// Try bool
|
||||||
|
if value == "true" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if value == "false" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try int
|
||||||
|
if i, err := strconv.Atoi(value); err == nil {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return as string
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
// LoadConfig loads the configuration from the specified path
|
// LoadConfig loads the configuration from the specified path
|
||||||
func LoadConfig(path string) (*common.ReticulumConfig, error) {
|
func LoadConfig(path string) (*common.ReticulumConfig, error) {
|
||||||
data, err := os.ReadFile(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
cfg := DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
if err := toml.Unmarshal(data, cfg); err != nil {
|
cfg.ConfigPath = path
|
||||||
return nil, err
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
var currentInterface *common.InterfaceConfig
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
|
// Skip comments and empty lines
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle interface sections
|
||||||
|
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||||
|
name := strings.Trim(line, "[]")
|
||||||
|
currentInterface = &common.InterfaceConfig{Name: name}
|
||||||
|
cfg.Interfaces[name] = currentInterface
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
|
||||||
|
if currentInterface != nil {
|
||||||
|
// Parse interface config
|
||||||
|
switch key {
|
||||||
|
case "type":
|
||||||
|
currentInterface.Type = value
|
||||||
|
case "enabled":
|
||||||
|
currentInterface.Enabled = value == "true"
|
||||||
|
case "address":
|
||||||
|
currentInterface.Address = value
|
||||||
|
case "port":
|
||||||
|
currentInterface.Port, _ = strconv.Atoi(value)
|
||||||
|
case "target_host":
|
||||||
|
currentInterface.TargetHost = value
|
||||||
|
case "target_port":
|
||||||
|
currentInterface.TargetPort, _ = strconv.Atoi(value)
|
||||||
|
case "discovery_port":
|
||||||
|
currentInterface.DiscoveryPort, _ = strconv.Atoi(value)
|
||||||
|
case "data_port":
|
||||||
|
currentInterface.DataPort, _ = strconv.Atoi(value)
|
||||||
|
case "discovery_scope":
|
||||||
|
currentInterface.DiscoveryScope = value
|
||||||
|
case "group_id":
|
||||||
|
currentInterface.GroupID = value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Parse global config
|
||||||
|
switch key {
|
||||||
|
case "enable_transport":
|
||||||
|
cfg.EnableTransport = value == "true"
|
||||||
|
case "share_instance":
|
||||||
|
cfg.ShareInstance = value == "true"
|
||||||
|
case "shared_instance_port":
|
||||||
|
cfg.SharedInstancePort, _ = strconv.Atoi(value)
|
||||||
|
case "instance_control_port":
|
||||||
|
cfg.InstanceControlPort, _ = strconv.Atoi(value)
|
||||||
|
case "panic_on_interface_error":
|
||||||
|
cfg.PanicOnInterfaceErr = value == "true"
|
||||||
|
case "loglevel":
|
||||||
|
cfg.LogLevel, _ = strconv.Atoi(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.ConfigPath = path
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveConfig saves the configuration to the specified path
|
// SaveConfig saves the configuration to the specified path
|
||||||
func SaveConfig(cfg *common.ReticulumConfig) error {
|
func SaveConfig(cfg *common.ReticulumConfig) error {
|
||||||
data, err := toml.Marshal(cfg)
|
if cfg.ConfigPath == "" {
|
||||||
if err != nil {
|
return fmt.Errorf("config path not set")
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(cfg.ConfigPath, data, 0644)
|
var builder strings.Builder
|
||||||
|
|
||||||
|
// Write global config
|
||||||
|
builder.WriteString("# Reticulum Configuration\n")
|
||||||
|
builder.WriteString(fmt.Sprintf("enable_transport = %v\n", cfg.EnableTransport))
|
||||||
|
builder.WriteString(fmt.Sprintf("share_instance = %v\n", cfg.ShareInstance))
|
||||||
|
builder.WriteString(fmt.Sprintf("shared_instance_port = %d\n", cfg.SharedInstancePort))
|
||||||
|
builder.WriteString(fmt.Sprintf("instance_control_port = %d\n", cfg.InstanceControlPort))
|
||||||
|
builder.WriteString(fmt.Sprintf("panic_on_interface_error = %v\n", cfg.PanicOnInterfaceErr))
|
||||||
|
builder.WriteString(fmt.Sprintf("loglevel = %d\n\n", cfg.LogLevel))
|
||||||
|
|
||||||
|
// Write interface configs
|
||||||
|
for name, iface := range cfg.Interfaces {
|
||||||
|
builder.WriteString(fmt.Sprintf("[%s]\n", name))
|
||||||
|
builder.WriteString(fmt.Sprintf("type = %s\n", iface.Type))
|
||||||
|
builder.WriteString(fmt.Sprintf("enabled = %v\n", iface.Enabled))
|
||||||
|
|
||||||
|
if iface.Address != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("address = %s\n", iface.Address))
|
||||||
|
}
|
||||||
|
if iface.Port != 0 {
|
||||||
|
builder.WriteString(fmt.Sprintf("port = %d\n", iface.Port))
|
||||||
|
}
|
||||||
|
if iface.TargetHost != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("target_host = %s\n", iface.TargetHost))
|
||||||
|
}
|
||||||
|
if iface.TargetPort != 0 {
|
||||||
|
builder.WriteString(fmt.Sprintf("target_port = %d\n", iface.TargetPort))
|
||||||
|
}
|
||||||
|
if iface.DiscoveryPort != 0 {
|
||||||
|
builder.WriteString(fmt.Sprintf("discovery_port = %d\n", iface.DiscoveryPort))
|
||||||
|
}
|
||||||
|
if iface.DataPort != 0 {
|
||||||
|
builder.WriteString(fmt.Sprintf("data_port = %d\n", iface.DataPort))
|
||||||
|
}
|
||||||
|
if iface.DiscoveryScope != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("discovery_scope = %s\n", iface.DiscoveryScope))
|
||||||
|
}
|
||||||
|
if iface.GroupID != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("group_id = %s\n", iface.GroupID))
|
||||||
|
}
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(cfg.ConfigPath, []byte(builder.String()), 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateDefaultConfig creates a default configuration file
|
// CreateDefaultConfig creates a default configuration file
|
||||||
func CreateDefaultConfig(path string) error {
|
func CreateDefaultConfig(path string) error {
|
||||||
cfg := DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
|
cfg.ConfigPath = path
|
||||||
|
|
||||||
// Add default interface
|
// Add Auto Interface
|
||||||
cfg.Interfaces["Default Interface"] = common.InterfaceConfig{
|
cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{
|
||||||
Type: "AutoInterface",
|
Type: "AutoInterface",
|
||||||
Enabled: false,
|
Enabled: true,
|
||||||
|
GroupID: "reticulum",
|
||||||
|
DiscoveryScope: "link",
|
||||||
|
DiscoveryPort: 29716,
|
||||||
|
DataPort: 42671,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add default quad4net interface
|
// Add default interfaces
|
||||||
cfg.Interfaces["quad4net tcp"] = common.InterfaceConfig{
|
cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
|
||||||
|
Type: "TCPClientInterface",
|
||||||
|
Enabled: true,
|
||||||
|
TargetHost: "127.0.0.1",
|
||||||
|
TargetPort: 4242,
|
||||||
|
Name: "Go-RNS-Testnet",
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Interfaces["Quad4 TCP"] = &common.InterfaceConfig{
|
||||||
Type: "TCPClientInterface",
|
Type: "TCPClientInterface",
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
TargetHost: "rns.quad4.io",
|
TargetHost: "rns.quad4.io",
|
||||||
TargetPort: 4242,
|
TargetPort: 4242,
|
||||||
|
Name: "Quad4 TCP",
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := toml.Marshal(cfg)
|
cfg.Interfaces["Local UDP"] = &common.InterfaceConfig{
|
||||||
if err != nil {
|
Type: "UDPInterface",
|
||||||
return err
|
Enabled: false,
|
||||||
|
Address: "0.0.0.0",
|
||||||
|
Port: 37696,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create config directory if it doesn't exist
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(path, data, 0644)
|
return SaveConfig(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitConfig initializes the configuration system
|
// InitConfig initializes the configuration system
|
||||||
@@ -118,4 +268,4 @@ func InitConfig() (*common.ReticulumConfig, error) {
|
|||||||
|
|
||||||
// Load config
|
// Load config
|
||||||
return LoadConfig(configPath)
|
return LoadConfig(configPath)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,47 @@
|
|||||||
package announce
|
package announce
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
PACKET_TYPE_DATA = 0x00
|
||||||
|
PACKET_TYPE_ANNOUNCE = 0x01
|
||||||
|
PACKET_TYPE_LINK = 0x02
|
||||||
|
PACKET_TYPE_PROOF = 0x03
|
||||||
|
|
||||||
|
// Announce Types
|
||||||
ANNOUNCE_NONE = 0x00
|
ANNOUNCE_NONE = 0x00
|
||||||
ANNOUNCE_PATH = 0x01
|
ANNOUNCE_PATH = 0x01
|
||||||
ANNOUNCE_IDENTITY = 0x02
|
ANNOUNCE_IDENTITY = 0x02
|
||||||
|
|
||||||
|
// Header Types
|
||||||
|
HEADER_TYPE_1 = 0x00 // One address field
|
||||||
|
HEADER_TYPE_2 = 0x01 // Two address fields
|
||||||
|
|
||||||
|
// Propagation Types
|
||||||
|
PROP_TYPE_BROADCAST = 0x00
|
||||||
|
PROP_TYPE_TRANSPORT = 0x01
|
||||||
|
|
||||||
|
DEST_TYPE_SINGLE = 0x00
|
||||||
|
DEST_TYPE_GROUP = 0x01
|
||||||
|
DEST_TYPE_PLAIN = 0x02
|
||||||
|
DEST_TYPE_LINK = 0x03
|
||||||
|
|
||||||
|
// IFAC Flag
|
||||||
|
IFAC_NONE = 0x00
|
||||||
|
IFAC_AUTH = 0x80
|
||||||
|
|
||||||
MAX_HOPS = 128
|
MAX_HOPS = 128
|
||||||
PROPAGATION_RATE = 0.02 // 2% of interface bandwidth
|
PROPAGATION_RATE = 0.02 // 2% of interface bandwidth
|
||||||
RETRY_INTERVAL = 300 // 5 minutes
|
RETRY_INTERVAL = 300 // 5 minutes
|
||||||
@@ -24,27 +50,37 @@ const (
|
|||||||
|
|
||||||
type AnnounceHandler interface {
|
type AnnounceHandler interface {
|
||||||
AspectFilter() []string
|
AspectFilter() []string
|
||||||
ReceivedAnnounce(destinationHash []byte, announcedIdentity *identity.Identity, appData []byte) error
|
ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte) error
|
||||||
ReceivePathResponses() bool
|
ReceivePathResponses() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Announce struct {
|
type Announce struct {
|
||||||
mutex sync.RWMutex
|
mutex *sync.RWMutex
|
||||||
destinationHash []byte
|
destinationHash []byte
|
||||||
identity *identity.Identity
|
identity *identity.Identity
|
||||||
appData []byte
|
appData []byte
|
||||||
hops uint8
|
config *common.ReticulumConfig
|
||||||
timestamp int64
|
hops uint8
|
||||||
signature []byte
|
timestamp int64
|
||||||
pathResponse bool
|
signature []byte
|
||||||
retries int
|
pathResponse bool
|
||||||
handlers []AnnounceHandler
|
retries int
|
||||||
|
handlers []AnnounceHandler
|
||||||
|
ratchetID []byte
|
||||||
|
packet []byte
|
||||||
|
hash []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(dest *identity.Identity, appData []byte, pathResponse bool) (*Announce, error) {
|
func New(dest *identity.Identity, appData []byte, pathResponse bool, config *common.ReticulumConfig) (*Announce, error) {
|
||||||
|
if dest == nil {
|
||||||
|
return nil, errors.New("destination identity required")
|
||||||
|
}
|
||||||
|
|
||||||
a := &Announce{
|
a := &Announce{
|
||||||
|
mutex: &sync.RWMutex{},
|
||||||
identity: dest,
|
identity: dest,
|
||||||
appData: appData,
|
appData: appData,
|
||||||
|
config: config,
|
||||||
hops: 0,
|
hops: 0,
|
||||||
timestamp: time.Now().Unix(),
|
timestamp: time.Now().Unix(),
|
||||||
pathResponse: pathResponse,
|
pathResponse: pathResponse,
|
||||||
@@ -52,46 +88,59 @@ func New(dest *identity.Identity, appData []byte, pathResponse bool) (*Announce,
|
|||||||
handlers: make([]AnnounceHandler, 0),
|
handlers: make([]AnnounceHandler, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate destination hash
|
// Generate truncated hash from public key
|
||||||
hash := sha256.New()
|
pubKey := dest.GetPublicKey()
|
||||||
hash.Write(dest.GetPublicKey())
|
hash := sha256.Sum256(pubKey)
|
||||||
a.destinationHash = hash.Sum(nil)[:16] // Truncated hash
|
a.destinationHash = hash[:identity.TRUNCATED_HASHLENGTH/8]
|
||||||
|
|
||||||
// Sign the announce
|
// Get current ratchet ID if enabled
|
||||||
|
currentRatchet := dest.GetCurrentRatchetKey()
|
||||||
|
if currentRatchet != nil {
|
||||||
|
a.ratchetID = dest.GetRatchetID(currentRatchet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign announce data
|
||||||
signData := append(a.destinationHash, a.appData...)
|
signData := append(a.destinationHash, a.appData...)
|
||||||
|
if a.ratchetID != nil {
|
||||||
|
signData = append(signData, a.ratchetID...)
|
||||||
|
}
|
||||||
a.signature = dest.Sign(signData)
|
a.signature = dest.Sign(signData)
|
||||||
|
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Announce) Propagate(interfaces []transport.Interface) error {
|
func (a *Announce) Propagate(interfaces []common.NetworkInterface) error {
|
||||||
a.mutex.Lock()
|
a.mutex.RLock()
|
||||||
defer a.mutex.Unlock()
|
defer a.mutex.RUnlock()
|
||||||
|
|
||||||
if a.hops >= MAX_HOPS {
|
log.Printf("[DEBUG-7] Propagating announce across %d interfaces", len(interfaces))
|
||||||
return errors.New("maximum hop count reached")
|
|
||||||
|
var packet []byte
|
||||||
|
if a.packet != nil {
|
||||||
|
log.Printf("[DEBUG-7] Using cached packet (%d bytes)", len(a.packet))
|
||||||
|
packet = a.packet
|
||||||
|
} else {
|
||||||
|
log.Printf("[DEBUG-7] Creating new packet")
|
||||||
|
packet = a.CreatePacket()
|
||||||
|
a.packet = packet
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment hop count
|
|
||||||
a.hops++
|
|
||||||
|
|
||||||
// Create announce packet
|
|
||||||
packet := make([]byte, 0)
|
|
||||||
packet = append(packet, a.destinationHash...)
|
|
||||||
packet = append(packet, a.identity.GetPublicKey()...)
|
|
||||||
packet = append(packet, byte(a.hops))
|
|
||||||
|
|
||||||
if a.appData != nil {
|
|
||||||
packet = append(packet, a.appData...)
|
|
||||||
}
|
|
||||||
|
|
||||||
packet = append(packet, a.signature...)
|
|
||||||
|
|
||||||
// Propagate to all interfaces
|
|
||||||
for _, iface := range interfaces {
|
for _, iface := range interfaces {
|
||||||
if err := iface.SendAnnounce(packet, a.pathResponse); err != nil {
|
if !iface.IsEnabled() {
|
||||||
return err
|
log.Printf("[DEBUG-7] Skipping disabled interface: %s", iface.GetName())
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
if !iface.GetBandwidthAvailable() {
|
||||||
|
log.Printf("[DEBUG-7] Skipping interface with insufficient bandwidth: %s", iface.GetName())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG-7] Sending announce on interface %s", iface.GetName())
|
||||||
|
if err := iface.Send(packet, ""); err != nil {
|
||||||
|
log.Printf("[DEBUG-7] Failed to send on interface %s: %v", iface.GetName(), err)
|
||||||
|
return fmt.Errorf("failed to propagate on interface %s: %w", iface.GetName(), err)
|
||||||
|
}
|
||||||
|
log.Printf("[DEBUG-7] Successfully sent announce on interface %s", iface.GetName())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -118,28 +167,53 @@ func (a *Announce) HandleAnnounce(data []byte) error {
|
|||||||
a.mutex.Lock()
|
a.mutex.Lock()
|
||||||
defer a.mutex.Unlock()
|
defer a.mutex.Unlock()
|
||||||
|
|
||||||
// Validate announce data
|
log.Printf("[DEBUG-7] Handling announce packet of %d bytes", len(data))
|
||||||
if len(data) < 16+32+1 { // Min size: hash + pubkey + hops
|
|
||||||
return errors.New("invalid announce data")
|
// Minimum packet size validation (header(2) + desthash(16) + enckey(32) + signkey(32) + namehash(10) +
|
||||||
|
// randomhash(10) + signature(64) + min app data(3))
|
||||||
|
if len(data) < 169 {
|
||||||
|
log.Printf("[DEBUG-7] Invalid announce data length: %d bytes", len(data))
|
||||||
|
return errors.New("invalid announce data length")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract fields
|
// Parse fields
|
||||||
destHash := data[:16]
|
header := data[:2]
|
||||||
pubKey := data[16:48]
|
hopCount := header[1]
|
||||||
hops := data[48]
|
destHash := data[2:18]
|
||||||
appData := data[49 : len(data)-64]
|
encKey := data[18:50]
|
||||||
signature := data[len(data)-64:]
|
signKey := data[50:82]
|
||||||
|
nameHash := data[82:92]
|
||||||
|
randomHash := data[92:102]
|
||||||
|
signature := data[102:166]
|
||||||
|
appData := data[166:]
|
||||||
|
|
||||||
|
log.Printf("[DEBUG-7] Announce fields: destHash=%x, encKey=%x, signKey=%x",
|
||||||
|
destHash, encKey, signKey)
|
||||||
|
log.Printf("[DEBUG-7] Name hash=%x, random hash=%x", nameHash, randomHash)
|
||||||
|
|
||||||
|
// Validate hop count
|
||||||
|
if hopCount > MAX_HOPS {
|
||||||
|
log.Printf("[DEBUG-7] Announce exceeded max hops: %d", hopCount)
|
||||||
|
return errors.New("announce exceeded maximum hop count")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create announced identity from public keys
|
||||||
|
pubKey := append(encKey, signKey...)
|
||||||
|
announcedIdentity := identity.FromPublicKey(pubKey)
|
||||||
|
if announcedIdentity == nil {
|
||||||
|
return errors.New("invalid identity public key")
|
||||||
|
}
|
||||||
|
|
||||||
// Verify signature
|
// Verify signature
|
||||||
signData := append(destHash, appData...)
|
signData := append(destHash, appData...)
|
||||||
if !a.identity.Verify(signData, signature) {
|
if !announcedIdentity.Verify(signData, signature) {
|
||||||
return errors.New("invalid announce signature")
|
return errors.New("invalid announce signature")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process announce with registered handlers
|
// Process with handlers
|
||||||
for _, handler := range a.handlers {
|
for _, handler := range a.handlers {
|
||||||
if handler.ReceivePathResponses() || !a.pathResponse {
|
if handler.ReceivePathResponses() || !a.pathResponse {
|
||||||
if err := handler.ReceivedAnnounce(destHash, a.identity, appData); err != nil {
|
if err := handler.ReceivedAnnounce(destHash, announcedIdentity, appData); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,7 +222,7 @@ func (a *Announce) HandleAnnounce(data []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Announce) RequestPath(destHash []byte, onInterface transport.Interface) error {
|
func (a *Announce) RequestPath(destHash []byte, onInterface common.NetworkInterface) error {
|
||||||
a.mutex.Lock()
|
a.mutex.Lock()
|
||||||
defer a.mutex.Unlock()
|
defer a.mutex.Unlock()
|
||||||
|
|
||||||
@@ -158,9 +232,203 @@ func (a *Announce) RequestPath(destHash []byte, onInterface transport.Interface)
|
|||||||
packet = append(packet, byte(0)) // Initial hop count
|
packet = append(packet, byte(0)) // Initial hop count
|
||||||
|
|
||||||
// Send path request
|
// Send path request
|
||||||
if err := onInterface.SendPathRequest(packet); err != nil {
|
if err := onInterface.Send(packet, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateHeader creates a Reticulum packet header according to spec
|
||||||
|
func CreateHeader(ifacFlag byte, headerType byte, contextFlag byte, propType byte, destType byte, packetType byte, hops byte) []byte {
|
||||||
|
header := make([]byte, 2)
|
||||||
|
|
||||||
|
// First byte: [IFAC Flag], [Header Type], [Context Flag], [Propagation Type], [Destination Type] and [Packet Type]
|
||||||
|
header[0] = ifacFlag | (headerType << 6) | (contextFlag << 5) |
|
||||||
|
(propType << 4) | (destType << 2) | packetType
|
||||||
|
|
||||||
|
// Second byte: Number of hops
|
||||||
|
header[1] = hops
|
||||||
|
|
||||||
|
return header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Announce) CreatePacket() []byte {
|
||||||
|
// Create header
|
||||||
|
header := CreateHeader(
|
||||||
|
IFAC_NONE,
|
||||||
|
HEADER_TYPE_1,
|
||||||
|
0, // No context flag
|
||||||
|
PROP_TYPE_BROADCAST,
|
||||||
|
DEST_TYPE_SINGLE,
|
||||||
|
PACKET_TYPE_ANNOUNCE,
|
||||||
|
a.hops,
|
||||||
|
)
|
||||||
|
|
||||||
|
packet := header
|
||||||
|
|
||||||
|
// Add destination hash (16 bytes)
|
||||||
|
packet = append(packet, a.destinationHash...)
|
||||||
|
|
||||||
|
// Add public key parts (32 bytes each)
|
||||||
|
pubKey := a.identity.GetPublicKey()
|
||||||
|
packet = append(packet, pubKey[:32]...) // Encryption key
|
||||||
|
packet = append(packet, pubKey[32:]...) // Signing key
|
||||||
|
|
||||||
|
// Add name hash (10 bytes)
|
||||||
|
nameHash := sha256.Sum256([]byte(fmt.Sprintf("%s.%s", a.config.AppName, a.config.AppAspect)))
|
||||||
|
packet = append(packet, nameHash[:10]...)
|
||||||
|
|
||||||
|
// Add random hash (10 bytes)
|
||||||
|
randomBytes := make([]byte, 10)
|
||||||
|
rand.Read(randomBytes)
|
||||||
|
packet = append(packet, randomBytes...)
|
||||||
|
|
||||||
|
// Create validation data for signature
|
||||||
|
validationData := make([]byte, 0)
|
||||||
|
validationData = append(validationData, a.destinationHash...)
|
||||||
|
validationData = append(validationData, pubKey[:32]...) // Encryption key
|
||||||
|
validationData = append(validationData, pubKey[32:]...) // Signing key
|
||||||
|
validationData = append(validationData, nameHash[:10]...)
|
||||||
|
validationData = append(validationData, randomBytes...)
|
||||||
|
validationData = append(validationData, a.appData...)
|
||||||
|
|
||||||
|
// Add signature (64 bytes)
|
||||||
|
signature := a.identity.Sign(validationData)
|
||||||
|
packet = append(packet, signature...)
|
||||||
|
|
||||||
|
// Add app data
|
||||||
|
if len(a.appData) > 0 {
|
||||||
|
packet = append(packet, a.appData...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return packet
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnnouncePacket struct {
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnnouncePacket(pubKey []byte, appData []byte, announceID []byte) *AnnouncePacket {
|
||||||
|
packet := &AnnouncePacket{}
|
||||||
|
|
||||||
|
// Build packet data
|
||||||
|
packet.Data = make([]byte, 0, len(pubKey)+len(appData)+len(announceID)+4)
|
||||||
|
|
||||||
|
// Add header
|
||||||
|
packet.Data = append(packet.Data, PACKET_TYPE_ANNOUNCE)
|
||||||
|
packet.Data = append(packet.Data, ANNOUNCE_IDENTITY)
|
||||||
|
|
||||||
|
// Add public key
|
||||||
|
packet.Data = append(packet.Data, pubKey...)
|
||||||
|
|
||||||
|
// Add app data length and content
|
||||||
|
appDataLen := make([]byte, 2)
|
||||||
|
binary.BigEndian.PutUint16(appDataLen, uint16(len(appData)))
|
||||||
|
packet.Data = append(packet.Data, appDataLen...)
|
||||||
|
packet.Data = append(packet.Data, appData...)
|
||||||
|
|
||||||
|
// Add announce ID
|
||||||
|
packet.Data = append(packet.Data, announceID...)
|
||||||
|
|
||||||
|
return packet
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAnnounce creates a new announce packet for a destination
|
||||||
|
func NewAnnounce(identity *identity.Identity, appData []byte, ratchetID []byte, pathResponse bool, config *common.ReticulumConfig) (*Announce, error) {
|
||||||
|
log.Printf("[DEBUG-7] Creating new announce: appDataLen=%d, hasRatchet=%v, pathResponse=%v",
|
||||||
|
len(appData), ratchetID != nil, pathResponse)
|
||||||
|
|
||||||
|
if identity == nil {
|
||||||
|
log.Printf("[DEBUG-7] Error: nil identity provided")
|
||||||
|
return nil, errors.New("identity cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("config cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
destHash := identity.Hash()
|
||||||
|
log.Printf("[DEBUG-7] Generated destination hash: %x", destHash)
|
||||||
|
|
||||||
|
a := &Announce{
|
||||||
|
identity: identity,
|
||||||
|
appData: appData,
|
||||||
|
ratchetID: ratchetID,
|
||||||
|
pathResponse: pathResponse,
|
||||||
|
destinationHash: destHash,
|
||||||
|
hops: 0,
|
||||||
|
mutex: &sync.RWMutex{},
|
||||||
|
handlers: make([]AnnounceHandler, 0),
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG-7] Created announce object: destHash=%x, hops=%d",
|
||||||
|
a.destinationHash, a.hops)
|
||||||
|
|
||||||
|
// Create initial packet
|
||||||
|
packet := a.CreatePacket()
|
||||||
|
a.packet = packet
|
||||||
|
|
||||||
|
// Generate hash
|
||||||
|
hash := a.Hash()
|
||||||
|
log.Printf("[DEBUG-7] Generated announce hash: %x", hash)
|
||||||
|
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Announce) Hash() []byte {
|
||||||
|
if a.hash == nil {
|
||||||
|
// Generate hash from announce data
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write(a.destinationHash)
|
||||||
|
h.Write(a.identity.GetPublicKey())
|
||||||
|
h.Write([]byte{a.hops})
|
||||||
|
h.Write(a.appData)
|
||||||
|
if a.ratchetID != nil {
|
||||||
|
h.Write(a.ratchetID)
|
||||||
|
}
|
||||||
|
a.hash = h.Sum(nil)
|
||||||
|
}
|
||||||
|
return a.hash
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Announce) GetPacket() []byte {
|
||||||
|
a.mutex.Lock()
|
||||||
|
defer a.mutex.Unlock()
|
||||||
|
|
||||||
|
if a.packet == nil {
|
||||||
|
// Generate hash from announce data
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write(a.destinationHash)
|
||||||
|
h.Write(a.identity.GetPublicKey())
|
||||||
|
h.Write([]byte{a.hops})
|
||||||
|
h.Write(a.appData)
|
||||||
|
if a.ratchetID != nil {
|
||||||
|
h.Write(a.ratchetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct packet
|
||||||
|
packet := make([]byte, 0)
|
||||||
|
packet = append(packet, PACKET_TYPE_ANNOUNCE)
|
||||||
|
packet = append(packet, a.destinationHash...)
|
||||||
|
packet = append(packet, a.identity.GetPublicKey()...)
|
||||||
|
packet = append(packet, a.hops)
|
||||||
|
packet = append(packet, a.appData...)
|
||||||
|
if a.ratchetID != nil {
|
||||||
|
packet = append(packet, a.ratchetID...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add signature
|
||||||
|
signData := append(a.destinationHash, a.appData...)
|
||||||
|
if a.ratchetID != nil {
|
||||||
|
signData = append(signData, a.ratchetID...)
|
||||||
|
}
|
||||||
|
signature := a.identity.Sign(signData)
|
||||||
|
packet = append(packet, signature...)
|
||||||
|
|
||||||
|
a.packet = packet
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.packet
|
||||||
|
}
|
||||||
|
|||||||
7
pkg/announce/handler.go
Normal file
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
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DEFAULT_SHARED_INSTANCE_PORT = 37428
|
||||||
|
DEFAULT_INSTANCE_CONTROL_PORT = 37429
|
||||||
|
DEFAULT_LOG_LEVEL = 20
|
||||||
|
)
|
||||||
|
|
||||||
// ConfigProvider interface for accessing configuration
|
// ConfigProvider interface for accessing configuration
|
||||||
type ConfigProvider interface {
|
type ConfigProvider interface {
|
||||||
GetConfigPath() string
|
GetConfigPath() string
|
||||||
GetLogLevel() int
|
GetLogLevel() int
|
||||||
GetInterfaces() map[string]InterfaceConfig
|
GetInterfaces() map[string]InterfaceConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// InterfaceConfig represents interface configuration
|
// InterfaceConfig represents interface configuration
|
||||||
type InterfaceConfig struct {
|
type InterfaceConfig struct {
|
||||||
Type string `toml:"type"`
|
Name string
|
||||||
Enabled bool `toml:"enabled"`
|
Type string
|
||||||
TargetHost string `toml:"target_host,omitempty"`
|
Enabled bool
|
||||||
TargetPort int `toml:"target_port,omitempty"`
|
Address string
|
||||||
Interface string `toml:"interface,omitempty"`
|
Port int
|
||||||
|
TargetHost string
|
||||||
|
TargetPort int
|
||||||
|
TargetAddress string
|
||||||
|
Interface string
|
||||||
|
KISSFraming bool
|
||||||
|
I2PTunneled bool
|
||||||
|
PreferIPv6 bool
|
||||||
|
MaxReconnTries int
|
||||||
|
Bitrate int64
|
||||||
|
MTU int
|
||||||
|
GroupID string
|
||||||
|
DiscoveryScope string
|
||||||
|
DiscoveryPort int
|
||||||
|
DataPort int
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReticulumConfig represents the main configuration structure
|
// ReticulumConfig represents the main configuration structure
|
||||||
type ReticulumConfig struct {
|
type ReticulumConfig struct {
|
||||||
EnableTransport bool `toml:"enable_transport"`
|
ConfigPath string
|
||||||
ShareInstance bool `toml:"share_instance"`
|
EnableTransport bool
|
||||||
SharedInstancePort int `toml:"shared_instance_port"`
|
ShareInstance bool
|
||||||
InstanceControlPort int `toml:"instance_control_port"`
|
SharedInstancePort int
|
||||||
PanicOnInterfaceErr bool `toml:"panic_on_interface_error"`
|
InstanceControlPort int
|
||||||
LogLevel int `toml:"loglevel"`
|
PanicOnInterfaceErr bool
|
||||||
ConfigPath string `toml:"-"`
|
LogLevel int
|
||||||
Interfaces map[string]InterfaceConfig
|
Interfaces map[string]*InterfaceConfig
|
||||||
}
|
AppName string
|
||||||
|
AppAspect string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReticulumConfig creates a new ReticulumConfig with default values
|
||||||
|
func NewReticulumConfig() *ReticulumConfig {
|
||||||
|
return &ReticulumConfig{
|
||||||
|
EnableTransport: true,
|
||||||
|
ShareInstance: false,
|
||||||
|
SharedInstancePort: DEFAULT_SHARED_INSTANCE_PORT,
|
||||||
|
InstanceControlPort: DEFAULT_INSTANCE_CONTROL_PORT,
|
||||||
|
PanicOnInterfaceErr: false,
|
||||||
|
LogLevel: DEFAULT_LOG_LEVEL,
|
||||||
|
Interfaces: make(map[string]*InterfaceConfig),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the configuration is valid
|
||||||
|
func (c *ReticulumConfig) Validate() error {
|
||||||
|
if c.SharedInstancePort < 1 || c.SharedInstancePort > 65535 {
|
||||||
|
return fmt.Errorf("invalid shared instance port: %d", c.SharedInstancePort)
|
||||||
|
}
|
||||||
|
if c.InstanceControlPort < 1 || c.InstanceControlPort > 65535 {
|
||||||
|
return fmt.Errorf("invalid instance control port: %d", c.InstanceControlPort)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultConfig() *ReticulumConfig {
|
||||||
|
return &ReticulumConfig{
|
||||||
|
EnableTransport: true,
|
||||||
|
ShareInstance: false,
|
||||||
|
SharedInstancePort: DEFAULT_SHARED_INSTANCE_PORT,
|
||||||
|
InstanceControlPort: DEFAULT_INSTANCE_CONTROL_PORT,
|
||||||
|
PanicOnInterfaceErr: false,
|
||||||
|
LogLevel: DEFAULT_LOG_LEVEL,
|
||||||
|
Interfaces: make(map[string]*InterfaceConfig),
|
||||||
|
AppName: "Go Client",
|
||||||
|
AppAspect: "node",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,28 +1,61 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Interface Types
|
// Interface Types
|
||||||
IF_TYPE_UDP InterfaceType = iota
|
IF_TYPE_NONE InterfaceType = iota
|
||||||
IF_TYPE_TCP
|
IF_TYPE_UDP
|
||||||
IF_TYPE_UNIX
|
IF_TYPE_TCP
|
||||||
|
IF_TYPE_UNIX
|
||||||
|
IF_TYPE_I2P
|
||||||
|
IF_TYPE_BLUETOOTH
|
||||||
|
IF_TYPE_SERIAL
|
||||||
|
IF_TYPE_AUTO
|
||||||
|
|
||||||
// Interface Modes
|
// Interface Modes
|
||||||
IF_MODE_FULL InterfaceMode = iota
|
IF_MODE_FULL InterfaceMode = iota
|
||||||
IF_MODE_POINT
|
IF_MODE_POINT
|
||||||
IF_MODE_GATEWAY
|
IF_MODE_GATEWAY
|
||||||
|
IF_MODE_ACCESS_POINT
|
||||||
|
IF_MODE_ROAMING
|
||||||
|
IF_MODE_BOUNDARY
|
||||||
|
|
||||||
// Transport Modes
|
// Transport Modes
|
||||||
TRANSPORT_MODE_DIRECT TransportMode = iota
|
TRANSPORT_MODE_DIRECT TransportMode = iota
|
||||||
TRANSPORT_MODE_RELAY
|
TRANSPORT_MODE_RELAY
|
||||||
TRANSPORT_MODE_GATEWAY
|
TRANSPORT_MODE_GATEWAY
|
||||||
|
|
||||||
// Path Status
|
// Path Status
|
||||||
PATH_STATUS_UNKNOWN PathStatus = iota
|
PATH_STATUS_UNKNOWN PathStatus = iota
|
||||||
PATH_STATUS_DIRECT
|
PATH_STATUS_DIRECT
|
||||||
PATH_STATUS_RELAY
|
PATH_STATUS_RELAY
|
||||||
PATH_STATUS_FAILED
|
PATH_STATUS_FAILED
|
||||||
|
|
||||||
// Common Constants
|
// Resource Status
|
||||||
DEFAULT_MTU = 1500
|
RESOURCE_STATUS_PENDING = 0x00
|
||||||
MAX_PACKET_SIZE = 65535
|
RESOURCE_STATUS_ACTIVE = 0x01
|
||||||
)
|
RESOURCE_STATUS_COMPLETE = 0x02
|
||||||
|
RESOURCE_STATUS_FAILED = 0x03
|
||||||
|
RESOURCE_STATUS_CANCELLED = 0x04
|
||||||
|
|
||||||
|
// Link Status
|
||||||
|
LINK_STATUS_PENDING = 0x00
|
||||||
|
LINK_STATUS_ACTIVE = 0x01
|
||||||
|
LINK_STATUS_CLOSED = 0x02
|
||||||
|
LINK_STATUS_FAILED = 0x03
|
||||||
|
|
||||||
|
// Direction Constants
|
||||||
|
IN = 0x01
|
||||||
|
OUT = 0x02
|
||||||
|
|
||||||
|
// Common Constants
|
||||||
|
DEFAULT_MTU = 1500
|
||||||
|
MAX_PACKET_SIZE = 65535
|
||||||
|
BITRATE_MINIMUM = 5
|
||||||
|
|
||||||
|
// Timeouts and Intervals
|
||||||
|
ESTABLISH_TIMEOUT = 6
|
||||||
|
KEEPALIVE_INTERVAL = 360
|
||||||
|
STALE_TIME = 720
|
||||||
|
PATH_REQUEST_TTL = 300
|
||||||
|
ANNOUNCE_TIMEOUT = 15
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,57 +1,211 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"encoding/binary"
|
||||||
"sync"
|
"net"
|
||||||
"time"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NetworkInterface combines both low-level and high-level interface requirements
|
// NetworkInterface defines the interface for all network communication methods
|
||||||
type NetworkInterface interface {
|
type NetworkInterface interface {
|
||||||
// Low-level network operations
|
// Core interface operations
|
||||||
Start() error
|
Start() error
|
||||||
Stop() error
|
Stop() error
|
||||||
Send(data []byte, address string) error
|
Enable()
|
||||||
Receive() ([]byte, string, error)
|
Disable()
|
||||||
GetType() InterfaceType
|
Detach()
|
||||||
GetMode() InterfaceMode
|
|
||||||
GetMTU() int
|
// Network operations
|
||||||
|
Send(data []byte, address string) error
|
||||||
// High-level packet operations
|
GetConn() net.Conn
|
||||||
ProcessIncoming([]byte)
|
GetMTU() int
|
||||||
ProcessOutgoing([]byte) error
|
GetName() string
|
||||||
SendPathRequest([]byte) error
|
|
||||||
SendLinkPacket([]byte, []byte, time.Time) error
|
// Interface properties
|
||||||
Detach()
|
GetType() InterfaceType
|
||||||
SetPacketCallback(PacketCallback)
|
GetMode() InterfaceMode
|
||||||
|
IsEnabled() bool
|
||||||
// Additional required fields
|
IsOnline() bool
|
||||||
GetName() string
|
IsDetached() bool
|
||||||
GetConn() net.Conn
|
GetBandwidthAvailable() bool
|
||||||
IsEnabled() bool
|
|
||||||
|
// Packet handling
|
||||||
|
ProcessIncoming([]byte)
|
||||||
|
ProcessOutgoing([]byte) error
|
||||||
|
SendPathRequest([]byte) error
|
||||||
|
SendLinkPacket([]byte, []byte, time.Time) error
|
||||||
|
SetPacketCallback(PacketCallback)
|
||||||
|
GetPacketCallback() PacketCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
type PacketCallback func([]byte, interface{})
|
// BaseInterface provides common implementation for network interfaces
|
||||||
|
|
||||||
// BaseInterface provides common implementation
|
|
||||||
type BaseInterface struct {
|
type BaseInterface struct {
|
||||||
Name string
|
Name string
|
||||||
Mode InterfaceMode
|
Mode InterfaceMode
|
||||||
Type InterfaceType
|
Type InterfaceType
|
||||||
|
Online bool
|
||||||
Online bool
|
Enabled bool
|
||||||
Detached bool
|
Detached bool
|
||||||
|
|
||||||
IN bool
|
IN bool
|
||||||
OUT bool
|
OUT bool
|
||||||
|
|
||||||
MTU int
|
MTU int
|
||||||
Bitrate int64
|
Bitrate int64
|
||||||
|
|
||||||
TxBytes uint64
|
TxBytes uint64
|
||||||
RxBytes uint64
|
RxBytes uint64
|
||||||
|
lastTx time.Time
|
||||||
mutex sync.RWMutex
|
|
||||||
owner interface{}
|
Mutex sync.RWMutex
|
||||||
packetCallback PacketCallback
|
Owner interface{}
|
||||||
}
|
PacketCallback PacketCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBaseInterface creates a new BaseInterface instance
|
||||||
|
func NewBaseInterface(name string, ifaceType InterfaceType, enabled bool) BaseInterface {
|
||||||
|
return BaseInterface{
|
||||||
|
Name: name,
|
||||||
|
Type: ifaceType,
|
||||||
|
Mode: IF_MODE_FULL,
|
||||||
|
Enabled: enabled,
|
||||||
|
MTU: DEFAULT_MTU,
|
||||||
|
Bitrate: BITRATE_MINIMUM,
|
||||||
|
lastTx: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default implementations for BaseInterface
|
||||||
|
func (i *BaseInterface) GetType() InterfaceType {
|
||||||
|
return i.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetMode() InterfaceMode {
|
||||||
|
return i.Mode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetMTU() int {
|
||||||
|
return i.MTU
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetName() string {
|
||||||
|
return i.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) IsEnabled() bool {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.Enabled && i.Online && !i.Detached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) IsOnline() bool {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.Online
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) IsDetached() bool {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.Detached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) SetPacketCallback(callback PacketCallback) {
|
||||||
|
i.Mutex.Lock()
|
||||||
|
defer i.Mutex.Unlock()
|
||||||
|
i.PacketCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetPacketCallback() PacketCallback {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.PacketCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Detach() {
|
||||||
|
i.Mutex.Lock()
|
||||||
|
defer i.Mutex.Unlock()
|
||||||
|
i.Detached = true
|
||||||
|
i.Online = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Enable() {
|
||||||
|
i.Mutex.Lock()
|
||||||
|
defer i.Mutex.Unlock()
|
||||||
|
i.Enabled = true
|
||||||
|
i.Online = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Disable() {
|
||||||
|
i.Mutex.Lock()
|
||||||
|
defer i.Mutex.Unlock()
|
||||||
|
i.Enabled = false
|
||||||
|
i.Online = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default implementations that should be overridden by specific interfaces
|
||||||
|
func (i *BaseInterface) Start() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Stop() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetConn() net.Conn {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Send(data []byte, address string) error {
|
||||||
|
return i.ProcessOutgoing(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
||||||
|
if i.PacketCallback != nil {
|
||||||
|
i.PacketCallback(data, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) ProcessOutgoing(data []byte) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) SendPathRequest(data []byte) error {
|
||||||
|
return i.Send(data, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.Time) error {
|
||||||
|
// Create link packet
|
||||||
|
packet := make([]byte, 0, len(dest)+len(data)+9) // 1 byte type + dest + 8 byte timestamp
|
||||||
|
packet = append(packet, 0x02) // Link packet type
|
||||||
|
packet = append(packet, dest...)
|
||||||
|
|
||||||
|
// Add timestamp
|
||||||
|
ts := make([]byte, 8)
|
||||||
|
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix()))
|
||||||
|
packet = append(packet, ts...)
|
||||||
|
|
||||||
|
// Add data
|
||||||
|
packet = append(packet, data...)
|
||||||
|
|
||||||
|
return i.Send(packet, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetBandwidthAvailable() bool {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
|
||||||
|
// If no transmission in last second, bandwidth is available
|
||||||
|
if time.Since(i.lastTx) > time.Second {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate current bandwidth usage
|
||||||
|
bytesPerSec := float64(i.TxBytes) / time.Since(i.lastTx).Seconds()
|
||||||
|
currentUsage := bytesPerSec * 8 // Convert to bits/sec
|
||||||
|
|
||||||
|
// Check if usage is below threshold (2% of total bitrate)
|
||||||
|
maxUsage := float64(i.Bitrate) * 0.02 // 2% propagation rate
|
||||||
|
return currentUsage < maxUsage
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,33 +4,72 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Interface related types
|
|
||||||
type InterfaceMode byte
|
|
||||||
type InterfaceType byte
|
|
||||||
|
|
||||||
// Transport related types
|
// Transport related types
|
||||||
type TransportMode byte
|
type TransportMode byte
|
||||||
type PathStatus byte
|
type PathStatus byte
|
||||||
|
|
||||||
// Common structs
|
// Path represents routing information for a destination
|
||||||
type Path struct {
|
type Path struct {
|
||||||
Interface NetworkInterface
|
Interface NetworkInterface
|
||||||
Address string
|
LastSeen time.Time
|
||||||
Status PathStatus
|
NextHop []byte
|
||||||
LastSeen time.Time
|
Hops uint8
|
||||||
NextHop []byte
|
LastUpdated time.Time
|
||||||
Hops uint8
|
HopCount uint8
|
||||||
LastUpdated time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common callbacks
|
// Common callbacks
|
||||||
type ProofRequestedCallback func(interface{}) bool
|
type ProofRequestedCallback func([]byte, []byte)
|
||||||
type LinkEstablishedCallback func(interface{})
|
type LinkEstablishedCallback func(interface{})
|
||||||
|
type PacketCallback func([]byte, NetworkInterface)
|
||||||
|
|
||||||
// Request handler
|
// RequestHandler manages path requests and responses
|
||||||
type RequestHandler struct {
|
type RequestHandler struct {
|
||||||
Path string
|
Path string
|
||||||
ResponseGenerator func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity interface{}, requestedAt int64) []byte
|
ResponseGenerator func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity interface{}, requestedAt int64) []byte
|
||||||
AllowMode byte
|
AllowMode byte
|
||||||
AllowedList [][]byte
|
AllowedList [][]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interface types
|
||||||
|
type InterfaceMode byte
|
||||||
|
type InterfaceType byte
|
||||||
|
|
||||||
|
// RatchetIDReceiver holds ratchet ID information
|
||||||
|
type RatchetIDReceiver struct {
|
||||||
|
LatestRatchetID []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkStats holds interface statistics
|
||||||
|
type NetworkStats struct {
|
||||||
|
BytesSent uint64
|
||||||
|
BytesReceived uint64
|
||||||
|
PacketsSent uint64
|
||||||
|
PacketsReceived uint64
|
||||||
|
LastUpdated time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinkStatus represents the current state of a link
|
||||||
|
type LinkStatus struct {
|
||||||
|
Established bool
|
||||||
|
LastSeen time.Time
|
||||||
|
RTT time.Duration
|
||||||
|
Quality float64
|
||||||
|
Hops uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathRequest represents a path discovery request
|
||||||
|
type PathRequest struct {
|
||||||
|
DestinationHash []byte
|
||||||
|
Tag []byte
|
||||||
|
TTL int
|
||||||
|
Recursive bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathResponse represents a path discovery response
|
||||||
|
type PathResponse struct {
|
||||||
|
DestinationHash []byte
|
||||||
|
NextHop []byte
|
||||||
|
Hops uint8
|
||||||
|
Tag []byte
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,49 +1,270 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"bufio"
|
||||||
"gopkg.in/yaml.v3"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Identity struct {
|
Identity struct {
|
||||||
Name string `yaml:"name"`
|
Name string
|
||||||
StoragePath string `yaml:"storage_path"`
|
StoragePath string
|
||||||
} `yaml:"identity"`
|
}
|
||||||
|
|
||||||
Interfaces []struct {
|
Interfaces []struct {
|
||||||
Name string `yaml:"name"`
|
Name string
|
||||||
Type string `yaml:"type"`
|
Type string
|
||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool
|
||||||
ListenPort int `yaml:"listen_port"`
|
ListenPort int
|
||||||
ListenIP string `yaml:"listen_ip"`
|
ListenIP string
|
||||||
KissFraming bool `yaml:"kiss_framing"`
|
KissFraming bool
|
||||||
I2PTunneled bool `yaml:"i2p_tunneled"`
|
I2PTunneled bool
|
||||||
} `yaml:"interfaces"`
|
}
|
||||||
|
|
||||||
Transport struct {
|
Transport struct {
|
||||||
AnnounceInterval int `yaml:"announce_interval"`
|
AnnounceInterval int
|
||||||
PathRequestTimeout int `yaml:"path_request_timeout"`
|
PathRequestTimeout int
|
||||||
MaxHops int `yaml:"max_hops"`
|
MaxHops int
|
||||||
BitrateLimit int64 `yaml:"bitrate_limit"`
|
BitrateLimit int64
|
||||||
} `yaml:"transport"`
|
}
|
||||||
|
|
||||||
Logging struct {
|
Logging struct {
|
||||||
Level string `yaml:"level"`
|
Level string
|
||||||
File string `yaml:"file"`
|
File string
|
||||||
} `yaml:"logging"`
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(path string) (*Config, error) {
|
func LoadConfig(path string) (*Config, error) {
|
||||||
data, err := ioutil.ReadFile(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
var cfg Config
|
cfg := &Config{}
|
||||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
scanner := bufio.NewScanner(file)
|
||||||
return nil, err
|
var currentSection string
|
||||||
}
|
|
||||||
|
|
||||||
return &cfg, nil
|
for scanner.Scan() {
|
||||||
}
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
|
// Skip comments and empty lines
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle sections
|
||||||
|
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||||
|
currentSection = strings.Trim(line, "[]")
|
||||||
|
|
||||||
|
// If this is an interface section, append new interface
|
||||||
|
if strings.HasPrefix(currentSection, "interface ") {
|
||||||
|
cfg.Interfaces = append(cfg.Interfaces, struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Enabled bool
|
||||||
|
ListenPort int
|
||||||
|
ListenIP string
|
||||||
|
KissFraming bool
|
||||||
|
I2PTunneled bool
|
||||||
|
}{})
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse key-value pairs
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
|
||||||
|
switch currentSection {
|
||||||
|
case "identity":
|
||||||
|
switch key {
|
||||||
|
case "name":
|
||||||
|
cfg.Identity.Name = value
|
||||||
|
case "storage_path":
|
||||||
|
cfg.Identity.StoragePath = value
|
||||||
|
}
|
||||||
|
|
||||||
|
case "transport":
|
||||||
|
switch key {
|
||||||
|
case "announce_interval":
|
||||||
|
cfg.Transport.AnnounceInterval, _ = strconv.Atoi(value)
|
||||||
|
case "path_request_timeout":
|
||||||
|
cfg.Transport.PathRequestTimeout, _ = strconv.Atoi(value)
|
||||||
|
case "max_hops":
|
||||||
|
cfg.Transport.MaxHops, _ = strconv.Atoi(value)
|
||||||
|
case "bitrate_limit":
|
||||||
|
cfg.Transport.BitrateLimit, _ = strconv.ParseInt(value, 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "logging":
|
||||||
|
switch key {
|
||||||
|
case "level":
|
||||||
|
cfg.Logging.Level = value
|
||||||
|
case "file":
|
||||||
|
cfg.Logging.File = value
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Handle interface sections
|
||||||
|
if strings.HasPrefix(currentSection, "interface ") && len(cfg.Interfaces) > 0 {
|
||||||
|
iface := &cfg.Interfaces[len(cfg.Interfaces)-1]
|
||||||
|
switch key {
|
||||||
|
case "name":
|
||||||
|
iface.Name = value
|
||||||
|
case "type":
|
||||||
|
iface.Type = value
|
||||||
|
case "enabled":
|
||||||
|
iface.Enabled = value == "true"
|
||||||
|
case "listen_port":
|
||||||
|
iface.ListenPort, _ = strconv.Atoi(value)
|
||||||
|
case "listen_ip":
|
||||||
|
iface.ListenIP = value
|
||||||
|
case "kiss_framing":
|
||||||
|
iface.KissFraming = value == "true"
|
||||||
|
case "i2p_tunneled":
|
||||||
|
iface.I2PTunneled = value == "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveConfig(cfg *Config, path string) error {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
// Write Identity section
|
||||||
|
builder.WriteString("[identity]\n")
|
||||||
|
builder.WriteString(fmt.Sprintf("name = %s\n", cfg.Identity.Name))
|
||||||
|
builder.WriteString(fmt.Sprintf("storage_path = %s\n\n", cfg.Identity.StoragePath))
|
||||||
|
|
||||||
|
// Write Transport section
|
||||||
|
builder.WriteString("[transport]\n")
|
||||||
|
builder.WriteString(fmt.Sprintf("announce_interval = %d\n", cfg.Transport.AnnounceInterval))
|
||||||
|
builder.WriteString(fmt.Sprintf("path_request_timeout = %d\n", cfg.Transport.PathRequestTimeout))
|
||||||
|
builder.WriteString(fmt.Sprintf("max_hops = %d\n", cfg.Transport.MaxHops))
|
||||||
|
builder.WriteString(fmt.Sprintf("bitrate_limit = %d\n\n", cfg.Transport.BitrateLimit))
|
||||||
|
|
||||||
|
// Write Logging section
|
||||||
|
builder.WriteString("[logging]\n")
|
||||||
|
builder.WriteString(fmt.Sprintf("level = %s\n", cfg.Logging.Level))
|
||||||
|
builder.WriteString(fmt.Sprintf("file = %s\n\n", cfg.Logging.File))
|
||||||
|
|
||||||
|
// Write Interface sections
|
||||||
|
for _, iface := range cfg.Interfaces {
|
||||||
|
builder.WriteString(fmt.Sprintf("[interface %s]\n", iface.Name))
|
||||||
|
builder.WriteString(fmt.Sprintf("type = %s\n", iface.Type))
|
||||||
|
builder.WriteString(fmt.Sprintf("enabled = %v\n", iface.Enabled))
|
||||||
|
builder.WriteString(fmt.Sprintf("listen_port = %d\n", iface.ListenPort))
|
||||||
|
builder.WriteString(fmt.Sprintf("listen_ip = %s\n", iface.ListenIP))
|
||||||
|
builder.WriteString(fmt.Sprintf("kiss_framing = %v\n", iface.KissFraming))
|
||||||
|
builder.WriteString(fmt.Sprintf("i2p_tunneled = %v\n\n", iface.I2PTunneled))
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(path, []byte(builder.String()), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetConfigDir() string {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to current directory if home directory cannot be determined
|
||||||
|
return ".reticulum-go"
|
||||||
|
}
|
||||||
|
return filepath.Join(homeDir, ".reticulum-go")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDefaultConfigPath() string {
|
||||||
|
return filepath.Join(GetConfigDir(), "config")
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnsureConfigDir() error {
|
||||||
|
configDir := GetConfigDir()
|
||||||
|
return os.MkdirAll(configDir, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitConfig() (*Config, error) {
|
||||||
|
// Ensure config directory exists
|
||||||
|
if err := EnsureConfigDir(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create config directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := GetDefaultConfigPath()
|
||||||
|
|
||||||
|
// Check if config file exists
|
||||||
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
// Create default config
|
||||||
|
cfg := &Config{}
|
||||||
|
|
||||||
|
// Set default values
|
||||||
|
cfg.Identity.Name = "reticulum-node"
|
||||||
|
cfg.Identity.StoragePath = filepath.Join(GetConfigDir(), "storage")
|
||||||
|
|
||||||
|
cfg.Transport.AnnounceInterval = 300
|
||||||
|
cfg.Transport.PathRequestTimeout = 15
|
||||||
|
cfg.Transport.MaxHops = 8
|
||||||
|
cfg.Transport.BitrateLimit = 1000000
|
||||||
|
|
||||||
|
cfg.Logging.Level = "info"
|
||||||
|
cfg.Logging.File = filepath.Join(GetConfigDir(), "reticulum.log")
|
||||||
|
|
||||||
|
// Add default interfaces
|
||||||
|
cfg.Interfaces = append(cfg.Interfaces, struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Enabled bool
|
||||||
|
ListenPort int
|
||||||
|
ListenIP string
|
||||||
|
KissFraming bool
|
||||||
|
I2PTunneled bool
|
||||||
|
}{
|
||||||
|
Name: "Local UDP",
|
||||||
|
Type: "UDPInterface",
|
||||||
|
Enabled: true,
|
||||||
|
ListenPort: 37697,
|
||||||
|
ListenIP: "0.0.0.0",
|
||||||
|
})
|
||||||
|
|
||||||
|
cfg.Interfaces = append(cfg.Interfaces, struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Enabled bool
|
||||||
|
ListenPort int
|
||||||
|
ListenIP string
|
||||||
|
KissFraming bool
|
||||||
|
I2PTunneled bool
|
||||||
|
}{
|
||||||
|
Name: "Auto Discovery",
|
||||||
|
Type: "AutoInterface",
|
||||||
|
Enabled: true,
|
||||||
|
ListenPort: 29717,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Save default config
|
||||||
|
if err := SaveConfig(cfg, configPath); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save default config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config
|
||||||
|
cfg, err := LoadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|||||||
63
pkg/cryptography/aes.go
Normal file
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"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||||
@@ -30,6 +31,15 @@ const (
|
|||||||
|
|
||||||
RATCHET_COUNT = 512 // Default number of retained ratchet keys
|
RATCHET_COUNT = 512 // Default number of retained ratchet keys
|
||||||
RATCHET_INTERVAL = 1800 // Minimum interval between ratchet rotations in seconds
|
RATCHET_INTERVAL = 1800 // Minimum interval between ratchet rotations in seconds
|
||||||
|
|
||||||
|
// Debug levels
|
||||||
|
DEBUG_CRITICAL = 1 // Critical errors
|
||||||
|
DEBUG_ERROR = 2 // Non-critical errors
|
||||||
|
DEBUG_INFO = 3 // Important information
|
||||||
|
DEBUG_VERBOSE = 4 // Detailed information
|
||||||
|
DEBUG_TRACE = 5 // Very detailed tracing
|
||||||
|
DEBUG_PACKETS = 6 // Packet-level details
|
||||||
|
DEBUG_ALL = 7 // Everything
|
||||||
)
|
)
|
||||||
|
|
||||||
type PacketCallback = common.PacketCallback
|
type PacketCallback = common.PacketCallback
|
||||||
@@ -44,39 +54,41 @@ type RequestHandler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Destination struct {
|
type Destination struct {
|
||||||
identity *identity.Identity
|
identity *identity.Identity
|
||||||
direction byte
|
direction byte
|
||||||
destType byte
|
destType byte
|
||||||
appName string
|
appName string
|
||||||
aspects []string
|
aspects []string
|
||||||
hash []byte
|
hashValue []byte
|
||||||
|
|
||||||
acceptsLinks bool
|
acceptsLinks bool
|
||||||
proofStrategy byte
|
proofStrategy byte
|
||||||
|
|
||||||
packetCallback PacketCallback
|
packetCallback PacketCallback
|
||||||
proofCallback ProofRequestedCallback
|
proofCallback ProofRequestedCallback
|
||||||
linkCallback LinkEstablishedCallback
|
linkCallback LinkEstablishedCallback
|
||||||
|
|
||||||
ratchetsEnabled bool
|
ratchetsEnabled bool
|
||||||
ratchetPath string
|
ratchetPath string
|
||||||
ratchetCount int
|
ratchetCount int
|
||||||
ratchetInterval int
|
ratchetInterval int
|
||||||
enforceRatchets bool
|
enforceRatchets bool
|
||||||
|
|
||||||
defaultAppData []byte
|
defaultAppData []byte
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
|
|
||||||
requestHandlers map[string]*RequestHandler
|
requestHandlers map[string]*RequestHandler
|
||||||
callbacks struct {
|
}
|
||||||
packetReceived common.PacketCallback
|
|
||||||
proofRequested common.ProofRequestedCallback
|
func debugLog(level int, format string, v ...interface{}) {
|
||||||
linkEstablished common.LinkEstablishedCallback
|
log.Printf("[DEBUG-%d] %s", level, fmt.Sprintf(format, v...))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(id *identity.Identity, direction byte, destType byte, appName string, aspects ...string) (*Destination, error) {
|
func New(id *identity.Identity, direction byte, destType byte, appName string, aspects ...string) (*Destination, error) {
|
||||||
|
debugLog(DEBUG_INFO, "Creating new destination: app=%s type=%d direction=%d", appName, destType, direction)
|
||||||
|
|
||||||
if id == nil {
|
if id == nil {
|
||||||
|
debugLog(DEBUG_ERROR, "Cannot create destination: identity is nil")
|
||||||
return nil, errors.New("identity cannot be nil")
|
return nil, errors.New("identity cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,19 +106,28 @@ func New(id *identity.Identity, direction byte, destType byte, appName string, a
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate destination hash
|
// Generate destination hash
|
||||||
d.hash = d.Hash()
|
d.hashValue = d.calculateHash()
|
||||||
|
debugLog(DEBUG_VERBOSE, "Created destination with hash: %x", d.hashValue)
|
||||||
|
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Destination) Hash() []byte {
|
func (d *Destination) calculateHash() []byte {
|
||||||
|
debugLog(DEBUG_TRACE, "Calculating hash for destination %s", d.ExpandName())
|
||||||
|
|
||||||
nameHash := sha256.Sum256([]byte(d.ExpandName()))
|
nameHash := sha256.Sum256([]byte(d.ExpandName()))
|
||||||
identityHash := sha256.Sum256(d.identity.GetPublicKey())
|
identityHash := sha256.Sum256(d.identity.GetPublicKey())
|
||||||
|
|
||||||
|
debugLog(DEBUG_ALL, "Name hash: %x", nameHash)
|
||||||
|
debugLog(DEBUG_ALL, "Identity hash: %x", identityHash)
|
||||||
|
|
||||||
combined := append(nameHash[:], identityHash[:]...)
|
combined := append(nameHash[:], identityHash[:]...)
|
||||||
finalHash := sha256.Sum256(combined)
|
finalHash := sha256.Sum256(combined)
|
||||||
|
|
||||||
return finalHash[:16] // Truncated to 128 bits
|
truncated := finalHash[:16]
|
||||||
|
debugLog(DEBUG_VERBOSE, "Calculated destination hash: %x", truncated)
|
||||||
|
|
||||||
|
return truncated
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Destination) ExpandName() string {
|
func (d *Destination) ExpandName() string {
|
||||||
@@ -121,69 +142,60 @@ func (d *Destination) Announce(appData []byte) error {
|
|||||||
d.mutex.Lock()
|
d.mutex.Lock()
|
||||||
defer d.mutex.Unlock()
|
defer d.mutex.Unlock()
|
||||||
|
|
||||||
|
log.Printf("[DEBUG-4] Creating announce packet for destination %s", d.ExpandName())
|
||||||
|
|
||||||
// If no specific appData provided, use default
|
// If no specific appData provided, use default
|
||||||
if appData == nil {
|
if appData == nil {
|
||||||
|
log.Printf("[DEBUG-4] Using default app data for announce")
|
||||||
appData = d.defaultAppData
|
appData = d.defaultAppData
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create announce packet
|
// Create announce packet
|
||||||
packet := make([]byte, 0)
|
packet := make([]byte, 0, 256) // Pre-allocate reasonable size
|
||||||
|
|
||||||
// Add destination hash
|
// Add packet type and header
|
||||||
packet = append(packet, d.hash...)
|
packet = append(packet, 0x01) // PACKET_TYPE_ANNOUNCE
|
||||||
|
packet = append(packet, 0x00) // Initial hop count
|
||||||
|
|
||||||
// Add identity public key
|
// Add destination hash (16 bytes)
|
||||||
packet = append(packet, d.identity.GetPublicKey()...)
|
packet = append(packet, d.hashValue...)
|
||||||
|
log.Printf("[DEBUG-4] Added destination hash %x to announce", d.hashValue[:8])
|
||||||
|
|
||||||
// Add flags byte
|
// Add identity public key (32 bytes)
|
||||||
flags := byte(0)
|
pubKey := d.identity.GetPublicKey()
|
||||||
if d.acceptsLinks {
|
packet = append(packet, pubKey...)
|
||||||
flags |= 0x01
|
log.Printf("[DEBUG-4] Added public key %x to announce", pubKey[:8])
|
||||||
}
|
|
||||||
if d.ratchetsEnabled {
|
|
||||||
flags |= 0x02
|
|
||||||
}
|
|
||||||
packet = append(packet, flags)
|
|
||||||
|
|
||||||
// Add proof strategy
|
// Add app data with length prefix
|
||||||
packet = append(packet, d.proofStrategy)
|
appDataLen := make([]byte, 2)
|
||||||
|
binary.BigEndian.PutUint16(appDataLen, uint16(len(appData)))
|
||||||
// Add app data length and data if present
|
packet = append(packet, appDataLen...)
|
||||||
if appData != nil {
|
packet = append(packet, appData...)
|
||||||
appDataLen := uint16(len(appData))
|
log.Printf("[DEBUG-4] Added %d bytes of app data to announce", len(appData))
|
||||||
lenBytes := make([]byte, 2)
|
|
||||||
binary.BigEndian.PutUint16(lenBytes, appDataLen)
|
|
||||||
packet = append(packet, lenBytes...)
|
|
||||||
packet = append(packet, appData...)
|
|
||||||
} else {
|
|
||||||
// No app data
|
|
||||||
packet = append(packet, 0x00, 0x00)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add ratchet data if enabled
|
// Add ratchet data if enabled
|
||||||
if d.ratchetsEnabled {
|
if d.ratchetsEnabled {
|
||||||
// Add ratchet interval
|
log.Printf("[DEBUG-4] Adding ratchet data to announce")
|
||||||
intervalBytes := make([]byte, 4)
|
|
||||||
binary.BigEndian.PutUint32(intervalBytes, uint32(d.ratchetInterval))
|
|
||||||
packet = append(packet, intervalBytes...)
|
|
||||||
|
|
||||||
// Add current ratchet key
|
|
||||||
ratchetKey := d.identity.GetCurrentRatchetKey()
|
ratchetKey := d.identity.GetCurrentRatchetKey()
|
||||||
if ratchetKey == nil {
|
if ratchetKey == nil {
|
||||||
|
log.Printf("[DEBUG-3] Failed to get current ratchet key")
|
||||||
return errors.New("failed to get current ratchet key")
|
return errors.New("failed to get current ratchet key")
|
||||||
}
|
}
|
||||||
packet = append(packet, ratchetKey...)
|
packet = append(packet, ratchetKey...)
|
||||||
|
log.Printf("[DEBUG-4] Added ratchet key %x to announce", ratchetKey[:8])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign the announce packet
|
// Sign the announce packet (64 bytes)
|
||||||
signature, err := d.Sign(packet)
|
signData := append(d.hashValue, appData...)
|
||||||
if err != nil {
|
if d.ratchetsEnabled {
|
||||||
return fmt.Errorf("failed to sign announce packet: %w", err)
|
signData = append(signData, d.identity.GetCurrentRatchetKey()...)
|
||||||
}
|
}
|
||||||
|
signature := d.identity.Sign(signData)
|
||||||
packet = append(packet, signature...)
|
packet = append(packet, signature...)
|
||||||
|
log.Printf("[DEBUG-4] Added signature to announce packet (total size: %d bytes)", len(packet))
|
||||||
|
|
||||||
// Send announce packet through transport layer
|
// Send announce packet through transport
|
||||||
// This will need to be implemented in the transport package
|
log.Printf("[DEBUG-4] Sending announce packet through transport layer")
|
||||||
return transport.SendAnnounce(packet)
|
return transport.SendAnnounce(packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +232,7 @@ func (d *Destination) SetProofStrategy(strategy byte) {
|
|||||||
func (d *Destination) EnableRatchets(path string) bool {
|
func (d *Destination) EnableRatchets(path string) bool {
|
||||||
d.mutex.Lock()
|
d.mutex.Lock()
|
||||||
defer d.mutex.Unlock()
|
defer d.mutex.Unlock()
|
||||||
|
|
||||||
d.ratchetsEnabled = true
|
d.ratchetsEnabled = true
|
||||||
d.ratchetPath = path
|
d.ratchetPath = path
|
||||||
return true
|
return true
|
||||||
@@ -236,7 +248,7 @@ func (d *Destination) SetRetainedRatchets(count int) bool {
|
|||||||
if count < 1 {
|
if count < 1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
d.mutex.Lock()
|
d.mutex.Lock()
|
||||||
defer d.mutex.Unlock()
|
defer d.mutex.Unlock()
|
||||||
d.ratchetCount = count
|
d.ratchetCount = count
|
||||||
@@ -247,7 +259,7 @@ func (d *Destination) SetRatchetInterval(interval int) bool {
|
|||||||
if interval < 1 {
|
if interval < 1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
d.mutex.Lock()
|
d.mutex.Lock()
|
||||||
defer d.mutex.Unlock()
|
defer d.mutex.Unlock()
|
||||||
d.ratchetInterval = interval
|
d.ratchetInterval = interval
|
||||||
@@ -275,7 +287,7 @@ func (d *Destination) RegisterRequestHandler(path string, responseGen func(strin
|
|||||||
return errors.New("invalid allow mode")
|
return errors.New("invalid allow mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
if allow == ALLOW_LIST && (allowedList == nil || len(allowedList) == 0) {
|
if allow == ALLOW_LIST && len(allowedList) == 0 {
|
||||||
return errors.New("allowed list required for ALLOW_LIST mode")
|
return errors.New("allowed list required for ALLOW_LIST mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,19 +317,32 @@ func (d *Destination) DeregisterRequestHandler(path string) bool {
|
|||||||
|
|
||||||
func (d *Destination) Encrypt(plaintext []byte) ([]byte, error) {
|
func (d *Destination) Encrypt(plaintext []byte) ([]byte, error) {
|
||||||
if d.destType == PLAIN {
|
if d.destType == PLAIN {
|
||||||
|
log.Printf("[DEBUG-4] Using plaintext transmission for PLAIN destination")
|
||||||
return plaintext, nil
|
return plaintext, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.identity == nil {
|
if d.identity == nil {
|
||||||
|
log.Printf("[DEBUG-3] Cannot encrypt: no identity available")
|
||||||
return nil, errors.New("no identity available for encryption")
|
return nil, errors.New("no identity available for encryption")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG-4] Encrypting %d bytes for destination type %d", len(plaintext), d.destType)
|
||||||
|
|
||||||
switch d.destType {
|
switch d.destType {
|
||||||
case SINGLE:
|
case SINGLE:
|
||||||
return d.identity.Encrypt(plaintext, nil)
|
recipientKey := d.identity.GetPublicKey()
|
||||||
|
log.Printf("[DEBUG-4] Encrypting for single recipient with key %x", recipientKey[:8])
|
||||||
|
return d.identity.Encrypt(plaintext, recipientKey)
|
||||||
case GROUP:
|
case GROUP:
|
||||||
return d.identity.EncryptSymmetric(plaintext)
|
key := d.identity.GetCurrentRatchetKey()
|
||||||
|
if key == nil {
|
||||||
|
log.Printf("[DEBUG-3] Cannot encrypt: no ratchet key available")
|
||||||
|
return nil, errors.New("no ratchet key available")
|
||||||
|
}
|
||||||
|
log.Printf("[DEBUG-4] Encrypting for group with ratchet key %x", key[:8])
|
||||||
|
return d.identity.EncryptWithHMAC(plaintext, key)
|
||||||
default:
|
default:
|
||||||
|
log.Printf("[DEBUG-3] Unsupported destination type %d for encryption", d.destType)
|
||||||
return nil, errors.New("unsupported destination type for encryption")
|
return nil, errors.New("unsupported destination type for encryption")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -331,14 +356,15 @@ func (d *Destination) Decrypt(ciphertext []byte) ([]byte, error) {
|
|||||||
return nil, errors.New("no identity available for decryption")
|
return nil, errors.New("no identity available for decryption")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch d.destType {
|
// Create empty ratchet receiver to get latest ratchet ID if available
|
||||||
case SINGLE:
|
ratchetReceiver := &common.RatchetIDReceiver{}
|
||||||
return d.identity.Decrypt(ciphertext, nil)
|
|
||||||
case GROUP:
|
// Call Decrypt with full parameter list:
|
||||||
return d.identity.DecryptSymmetric(ciphertext)
|
// - ciphertext: the encrypted data
|
||||||
default:
|
// - ratchets: nil since we're not providing specific ratchets
|
||||||
return nil, errors.New("unsupported destination type for decryption")
|
// - enforceRatchets: false to allow fallback to normal decryption
|
||||||
}
|
// - ratchetIDReceiver: to receive the latest ratchet ID used
|
||||||
|
return d.identity.Decrypt(ciphertext, nil, false, ratchetReceiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Destination) Sign(data []byte) ([]byte, error) {
|
func (d *Destination) Sign(data []byte) ([]byte, error) {
|
||||||
@@ -347,4 +373,33 @@ func (d *Destination) Sign(data []byte) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
signature := d.identity.Sign(data)
|
signature := d.identity.Sign(data)
|
||||||
return signature, nil
|
return signature, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Destination) GetPublicKey() []byte {
|
||||||
|
if d.identity == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return d.identity.GetPublicKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Destination) GetIdentity() *identity.Identity {
|
||||||
|
return d.identity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Destination) GetType() byte {
|
||||||
|
return d.destType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Destination) GetHash() []byte {
|
||||||
|
d.mutex.RLock()
|
||||||
|
defer d.mutex.RUnlock()
|
||||||
|
if d.hashValue == nil {
|
||||||
|
d.mutex.RUnlock()
|
||||||
|
d.mutex.Lock()
|
||||||
|
defer d.mutex.Unlock()
|
||||||
|
if d.hashValue == nil {
|
||||||
|
d.hashValue = d.calculateHash()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d.hashValue
|
||||||
|
}
|
||||||
|
|||||||
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
|
package interfaces
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"encoding/binary"
|
|
||||||
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
BITRATE_MINIMUM = 5 // Minimum required bitrate in bits/sec
|
BITRATE_MINIMUM = 1200 // Minimum bitrate in bits/second
|
||||||
|
MODE_FULL = 0x01
|
||||||
|
|
||||||
|
// Interface modes
|
||||||
|
MODE_GATEWAY = 0x02
|
||||||
|
MODE_ACCESS_POINT = 0x03
|
||||||
|
MODE_ROAMING = 0x04
|
||||||
|
MODE_BOUNDARY = 0x05
|
||||||
|
|
||||||
|
// Interface types
|
||||||
|
TYPE_UDP = 0x01
|
||||||
|
TYPE_TCP = 0x02
|
||||||
|
|
||||||
|
PROPAGATION_RATE = 0.02 // 2% of interface bandwidth
|
||||||
|
|
||||||
|
DEBUG_LEVEL = 4 // Default debug level for interface logging
|
||||||
|
|
||||||
|
// Debug levels
|
||||||
|
DEBUG_CRITICAL = 1
|
||||||
|
DEBUG_ERROR = 2
|
||||||
|
DEBUG_INFO = 3
|
||||||
|
DEBUG_VERBOSE = 4
|
||||||
|
DEBUG_TRACE = 5
|
||||||
|
DEBUG_PACKETS = 6
|
||||||
|
DEBUG_ALL = 7
|
||||||
)
|
)
|
||||||
|
|
||||||
// BaseInterface embeds common.BaseInterface and implements common.Interface
|
type Interface interface {
|
||||||
|
GetName() string
|
||||||
|
GetType() common.InterfaceType
|
||||||
|
GetMode() common.InterfaceMode
|
||||||
|
IsOnline() bool
|
||||||
|
IsDetached() bool
|
||||||
|
IsEnabled() bool
|
||||||
|
Detach()
|
||||||
|
Enable()
|
||||||
|
Disable()
|
||||||
|
Send(data []byte, addr string) error
|
||||||
|
SetPacketCallback(common.PacketCallback)
|
||||||
|
GetPacketCallback() common.PacketCallback
|
||||||
|
ProcessIncoming([]byte)
|
||||||
|
ProcessOutgoing([]byte) error
|
||||||
|
SendPathRequest([]byte) error
|
||||||
|
SendLinkPacket([]byte, []byte, time.Time) error
|
||||||
|
Start() error
|
||||||
|
Stop() error
|
||||||
|
GetMTU() int
|
||||||
|
GetConn() net.Conn
|
||||||
|
GetBandwidthAvailable() bool
|
||||||
|
common.NetworkInterface
|
||||||
|
}
|
||||||
|
|
||||||
type BaseInterface struct {
|
type BaseInterface struct {
|
||||||
common.BaseInterface
|
Name string
|
||||||
|
Mode common.InterfaceMode
|
||||||
|
Type common.InterfaceType
|
||||||
|
Online bool
|
||||||
|
Enabled bool
|
||||||
|
Detached bool
|
||||||
|
IN bool
|
||||||
|
OUT bool
|
||||||
|
MTU int
|
||||||
|
Bitrate int64
|
||||||
|
TxBytes uint64
|
||||||
|
RxBytes uint64
|
||||||
|
lastTx time.Time
|
||||||
|
|
||||||
|
mutex sync.RWMutex
|
||||||
|
packetCallback common.PacketCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBaseInterface(name string, ifType common.InterfaceType, enabled bool) BaseInterface {
|
||||||
|
return BaseInterface{
|
||||||
|
Name: name,
|
||||||
|
Mode: common.IF_MODE_FULL,
|
||||||
|
Type: ifType,
|
||||||
|
Online: false,
|
||||||
|
Enabled: enabled,
|
||||||
|
Detached: false,
|
||||||
|
IN: false,
|
||||||
|
OUT: false,
|
||||||
|
MTU: common.DEFAULT_MTU,
|
||||||
|
Bitrate: BITRATE_MINIMUM,
|
||||||
|
lastTx: time.Now(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {
|
func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||||
@@ -24,28 +105,38 @@ func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {
|
|||||||
i.packetCallback = callback
|
i.packetCallback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetPacketCallback() common.PacketCallback {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
return i.packetCallback
|
||||||
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
||||||
|
i.mutex.Lock()
|
||||||
|
i.RxBytes += uint64(len(data))
|
||||||
|
i.mutex.Unlock()
|
||||||
|
|
||||||
i.mutex.RLock()
|
i.mutex.RLock()
|
||||||
callback := i.packetCallback
|
callback := i.packetCallback
|
||||||
i.mutex.RUnlock()
|
i.mutex.RUnlock()
|
||||||
|
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
callback(data, i)
|
callback(data, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
i.RxBytes += uint64(len(data))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) ProcessOutgoing(data []byte) error {
|
func (i *BaseInterface) ProcessOutgoing(data []byte) error {
|
||||||
i.TxBytes += uint64(len(data))
|
if !i.Online || i.Detached {
|
||||||
return nil
|
log.Printf("[DEBUG-1] Interface %s: Cannot process outgoing packet - interface offline or detached", i.Name)
|
||||||
}
|
return fmt.Errorf("interface offline or detached")
|
||||||
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) Detach() {
|
|
||||||
i.mutex.Lock()
|
i.mutex.Lock()
|
||||||
defer i.mutex.Unlock()
|
i.TxBytes += uint64(len(data))
|
||||||
i.Detached = true
|
i.mutex.Unlock()
|
||||||
i.Online = false
|
|
||||||
|
log.Printf("[DEBUG-%d] Interface %s: Processed outgoing packet of %d bytes, total TX: %d", DEBUG_LEVEL, i.Name, len(data), i.TxBytes)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) SendPathRequest(packet []byte) error {
|
func (i *BaseInterface) SendPathRequest(packet []byte) error {
|
||||||
@@ -53,7 +144,7 @@ func (i *BaseInterface) SendPathRequest(packet []byte) error {
|
|||||||
return fmt.Errorf("interface offline or detached")
|
return fmt.Errorf("interface offline or detached")
|
||||||
}
|
}
|
||||||
|
|
||||||
frame := make([]byte, 0, len(packet)+2)
|
frame := make([]byte, 0, len(packet)+1)
|
||||||
frame = append(frame, 0x01)
|
frame = append(frame, 0x01)
|
||||||
frame = append(frame, packet...)
|
frame = append(frame, packet...)
|
||||||
|
|
||||||
@@ -68,12 +159,156 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
|
|||||||
frame := make([]byte, 0, len(dest)+len(data)+9)
|
frame := make([]byte, 0, len(dest)+len(data)+9)
|
||||||
frame = append(frame, 0x02)
|
frame = append(frame, 0x02)
|
||||||
frame = append(frame, dest...)
|
frame = append(frame, dest...)
|
||||||
|
|
||||||
ts := make([]byte, 8)
|
ts := make([]byte, 8)
|
||||||
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix()))
|
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix()))
|
||||||
frame = append(frame, ts...)
|
frame = append(frame, ts...)
|
||||||
|
|
||||||
frame = append(frame, data...)
|
frame = append(frame, data...)
|
||||||
|
|
||||||
return i.ProcessOutgoing(frame)
|
return i.ProcessOutgoing(frame)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Detach() {
|
||||||
|
i.mutex.Lock()
|
||||||
|
defer i.mutex.Unlock()
|
||||||
|
i.Detached = true
|
||||||
|
i.Online = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) IsEnabled() bool {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
return i.Enabled && i.Online && !i.Detached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Enable() {
|
||||||
|
i.mutex.Lock()
|
||||||
|
defer i.mutex.Unlock()
|
||||||
|
|
||||||
|
prevState := i.Enabled
|
||||||
|
i.Enabled = true
|
||||||
|
i.Online = true
|
||||||
|
|
||||||
|
log.Printf("[DEBUG-%d] Interface %s: State changed - Enabled: %v->%v, Online: %v->%v", DEBUG_INFO, i.Name, prevState, i.Enabled, !i.Online, i.Online)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Disable() {
|
||||||
|
i.mutex.Lock()
|
||||||
|
defer i.mutex.Unlock()
|
||||||
|
i.Enabled = false
|
||||||
|
i.Online = false
|
||||||
|
log.Printf("[DEBUG-2] Interface %s: Disabled and offline", i.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetName() string {
|
||||||
|
return i.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetType() common.InterfaceType {
|
||||||
|
return i.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetMode() common.InterfaceMode {
|
||||||
|
return i.Mode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetMTU() int {
|
||||||
|
return i.MTU
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) IsOnline() bool {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
return i.Online
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) IsDetached() bool {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
return i.Detached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Start() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Stop() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Send(data []byte, address string) error {
|
||||||
|
log.Printf("[DEBUG-%d] Interface %s: Sending %d bytes to %s", DEBUG_LEVEL, i.Name, len(data), address)
|
||||||
|
|
||||||
|
err := i.ProcessOutgoing(data)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[DEBUG-1] Interface %s: Failed to send data: %v", i.Name, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
i.updateBandwidthStats(uint64(len(data)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetConn() net.Conn {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetBandwidthAvailable() bool {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
timeSinceLastTx := now.Sub(i.lastTx)
|
||||||
|
|
||||||
|
if timeSinceLastTx > time.Second {
|
||||||
|
log.Printf("[DEBUG-%d] Interface %s: Bandwidth available (idle for %.2fs)", DEBUG_VERBOSE, i.Name, timeSinceLastTx.Seconds())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesPerSec := float64(i.TxBytes) / timeSinceLastTx.Seconds()
|
||||||
|
currentUsage := bytesPerSec * 8
|
||||||
|
maxUsage := float64(i.Bitrate) * PROPAGATION_RATE
|
||||||
|
|
||||||
|
available := currentUsage < maxUsage
|
||||||
|
log.Printf("[DEBUG-%d] Interface %s: Bandwidth stats - Current: %.2f bps, Max: %.2f bps, Usage: %.1f%%, Available: %v", DEBUG_VERBOSE, i.Name, currentUsage, maxUsage, (currentUsage/maxUsage)*100, available)
|
||||||
|
|
||||||
|
return available
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) updateBandwidthStats(bytes uint64) {
|
||||||
|
i.mutex.Lock()
|
||||||
|
defer i.mutex.Unlock()
|
||||||
|
|
||||||
|
i.TxBytes += bytes
|
||||||
|
i.lastTx = time.Now()
|
||||||
|
|
||||||
|
log.Printf("[DEBUG-%d] Interface %s: Updated bandwidth stats - TX bytes: %d, Last TX: %v", DEBUG_LEVEL, i.Name, i.TxBytes, i.lastTx)
|
||||||
|
}
|
||||||
|
|
||||||
|
type InterceptedInterface struct {
|
||||||
|
Interface
|
||||||
|
interceptor func([]byte, common.NetworkInterface) error
|
||||||
|
originalSend func([]byte, string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create constructor for intercepted interface
|
||||||
|
func NewInterceptedInterface(base Interface, interceptor func([]byte, common.NetworkInterface) error) *InterceptedInterface {
|
||||||
|
return &InterceptedInterface{
|
||||||
|
Interface: base,
|
||||||
|
interceptor: interceptor,
|
||||||
|
originalSend: base.Send,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Send method for intercepted interface
|
||||||
|
func (i *InterceptedInterface) Send(data []byte, addr string) error {
|
||||||
|
// Call interceptor if provided
|
||||||
|
if i.interceptor != nil && len(data) > 0 {
|
||||||
|
if err := i.interceptor(data, i); err != nil {
|
||||||
|
log.Printf("[DEBUG-2] Failed to intercept outgoing packet: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call original send
|
||||||
|
return i.originalSend(data, addr)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ package interfaces
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -17,103 +21,102 @@ const (
|
|||||||
KISS_TFEND = 0xDC
|
KISS_TFEND = 0xDC
|
||||||
KISS_TFESC = 0xDD
|
KISS_TFESC = 0xDD
|
||||||
|
|
||||||
TCP_USER_TIMEOUT = 24
|
TCP_USER_TIMEOUT = 24
|
||||||
TCP_PROBE_AFTER = 5
|
TCP_PROBE_AFTER = 5
|
||||||
TCP_PROBE_INTERVAL = 2
|
TCP_PROBE_INTERVAL = 2
|
||||||
TCP_PROBES = 12
|
TCP_PROBES = 12
|
||||||
RECONNECT_WAIT = 5
|
RECONNECT_WAIT = 5
|
||||||
INITIAL_TIMEOUT = 5
|
INITIAL_TIMEOUT = 5
|
||||||
|
INITIAL_BACKOFF = time.Second
|
||||||
|
MAX_BACKOFF = time.Minute * 5
|
||||||
)
|
)
|
||||||
|
|
||||||
type TCPClientInterface struct {
|
type TCPClientInterface struct {
|
||||||
Interface
|
BaseInterface
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
targetAddr string
|
targetAddr string
|
||||||
targetPort int
|
targetPort int
|
||||||
kissFraming bool
|
kissFraming bool
|
||||||
i2pTunneled bool
|
i2pTunneled bool
|
||||||
initiator bool
|
initiator bool
|
||||||
reconnecting bool
|
reconnecting bool
|
||||||
neverConnected bool
|
neverConnected bool
|
||||||
writing bool
|
writing bool
|
||||||
maxReconnectTries int
|
maxReconnectTries int
|
||||||
packetBuffer []byte
|
packetBuffer []byte
|
||||||
packetType byte
|
packetType byte
|
||||||
|
mutex sync.RWMutex
|
||||||
|
enabled bool
|
||||||
|
TxBytes uint64
|
||||||
|
RxBytes uint64
|
||||||
|
lastTx time.Time
|
||||||
|
lastRx time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTCPClient(name string, targetAddr string, targetPort int, kissFraming bool, i2pTunneled bool) (*TCPClientInterface, error) {
|
func NewTCPClientInterface(name string, targetHost string, targetPort int, kissFraming bool, i2pTunneled bool, enabled bool) (*TCPClientInterface, error) {
|
||||||
tc := &TCPClientInterface{
|
tc := &TCPClientInterface{
|
||||||
Interface: Interface{
|
BaseInterface: NewBaseInterface(name, common.IF_TYPE_TCP, enabled),
|
||||||
Name: name,
|
targetAddr: targetHost,
|
||||||
Mode: MODE_FULL,
|
targetPort: targetPort,
|
||||||
MTU: 1064,
|
kissFraming: kissFraming,
|
||||||
Bitrate: 10000000, // 10Mbps estimate
|
i2pTunneled: i2pTunneled,
|
||||||
},
|
initiator: true,
|
||||||
targetAddr: targetAddr,
|
enabled: enabled,
|
||||||
targetPort: targetPort,
|
maxReconnectTries: TCP_PROBES,
|
||||||
kissFraming: kissFraming,
|
packetBuffer: make([]byte, 0),
|
||||||
i2pTunneled: i2pTunneled,
|
neverConnected: true,
|
||||||
initiator: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tc.connect(true); err != nil {
|
if enabled {
|
||||||
go tc.reconnect()
|
addr := fmt.Sprintf("%s:%d", targetHost, targetPort)
|
||||||
} else {
|
conn, err := net.Dial("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tc.conn = conn
|
||||||
|
tc.Online = true
|
||||||
go tc.readLoop()
|
go tc.readLoop()
|
||||||
}
|
}
|
||||||
|
|
||||||
return tc, nil
|
return tc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) connect(initial bool) error {
|
func (tc *TCPClientInterface) Start() error {
|
||||||
|
tc.mutex.Lock()
|
||||||
|
defer tc.mutex.Unlock()
|
||||||
|
|
||||||
|
if !tc.Enabled {
|
||||||
|
return fmt.Errorf("interface not enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.conn != nil {
|
||||||
|
tc.Online = true
|
||||||
|
go tc.readLoop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
addr := fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort)
|
addr := fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort)
|
||||||
conn, err := net.DialTimeout("tcp", addr, time.Second*INITIAL_TIMEOUT)
|
conn, err := net.Dial("tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if initial {
|
|
||||||
return fmt.Errorf("initial connection failed: %v", err)
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tc.conn = conn
|
tc.conn = conn
|
||||||
tc.Online = true
|
|
||||||
tc.writing = false
|
|
||||||
tc.neverConnected = false
|
|
||||||
|
|
||||||
// Set TCP options
|
// Set platform-specific timeouts
|
||||||
if tcpConn, ok := conn.(*net.TCPConn); ok {
|
switch runtime.GOOS {
|
||||||
tcpConn.SetNoDelay(true)
|
case "linux":
|
||||||
tcpConn.SetKeepAlive(true)
|
if err := tc.setTimeoutsLinux(); err != nil {
|
||||||
tcpConn.SetKeepAlivePeriod(time.Second * TCP_PROBE_INTERVAL)
|
log.Printf("[DEBUG-2] Failed to set Linux TCP timeouts: %v", err)
|
||||||
}
|
}
|
||||||
|
case "darwin":
|
||||||
return nil
|
if err := tc.setTimeoutsOSX(); err != nil {
|
||||||
}
|
log.Printf("[DEBUG-2] Failed to set OSX TCP timeouts: %v", err)
|
||||||
|
|
||||||
func (tc *TCPClientInterface) reconnect() {
|
|
||||||
if tc.initiator && !tc.reconnecting {
|
|
||||||
tc.reconnecting = true
|
|
||||||
attempts := 0
|
|
||||||
|
|
||||||
for !tc.Online {
|
|
||||||
time.Sleep(time.Second * RECONNECT_WAIT)
|
|
||||||
attempts++
|
|
||||||
|
|
||||||
if tc.maxReconnectTries > 0 && attempts > tc.maxReconnectTries {
|
|
||||||
tc.teardown()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tc.connect(false); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
go tc.readLoop()
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tc.reconnecting = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tc.Online = true
|
||||||
|
go tc.readLoop()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) readLoop() {
|
func (tc *TCPClientInterface) readLoop() {
|
||||||
@@ -134,26 +137,31 @@ func (tc *TCPClientInterface) readLoop() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update RX bytes for raw received data
|
||||||
|
tc.UpdateStats(uint64(n), true)
|
||||||
|
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
b := buffer[i]
|
b := buffer[i]
|
||||||
|
|
||||||
if tc.kissFraming {
|
if tc.kissFraming {
|
||||||
// KISS framing logic
|
// KISS framing logic
|
||||||
if inFrame && b == KISS_FEND {
|
if b == KISS_FEND {
|
||||||
inFrame = false
|
if inFrame && len(dataBuffer) > 0 {
|
||||||
tc.handlePacket(dataBuffer)
|
tc.handlePacket(dataBuffer)
|
||||||
dataBuffer = dataBuffer[:0]
|
dataBuffer = dataBuffer[:0]
|
||||||
} else if b == KISS_FEND {
|
}
|
||||||
inFrame = true
|
inFrame = !inFrame
|
||||||
} else if inFrame {
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if inFrame {
|
||||||
if b == KISS_FESC {
|
if b == KISS_FESC {
|
||||||
escape = true
|
escape = true
|
||||||
} else {
|
} else {
|
||||||
if escape {
|
if escape {
|
||||||
if b == KISS_TFEND {
|
if b == KISS_TFEND {
|
||||||
b = KISS_FEND
|
b = KISS_FEND
|
||||||
}
|
} else if b == KISS_TFESC {
|
||||||
if b == KISS_TFESC {
|
|
||||||
b = KISS_FESC
|
b = KISS_FESC
|
||||||
}
|
}
|
||||||
escape = false
|
escape = false
|
||||||
@@ -163,13 +171,16 @@ func (tc *TCPClientInterface) readLoop() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// HDLC framing logic
|
// HDLC framing logic
|
||||||
if inFrame && b == HDLC_FLAG {
|
if b == HDLC_FLAG {
|
||||||
inFrame = false
|
if inFrame && len(dataBuffer) > 0 {
|
||||||
tc.handlePacket(dataBuffer)
|
tc.handlePacket(dataBuffer)
|
||||||
dataBuffer = dataBuffer[:0]
|
dataBuffer = dataBuffer[:0]
|
||||||
} else if b == HDLC_FLAG {
|
}
|
||||||
inFrame = true
|
inFrame = !inFrame
|
||||||
} else if inFrame {
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if inFrame {
|
||||||
if b == HDLC_ESC {
|
if b == HDLC_ESC {
|
||||||
escape = true
|
escape = true
|
||||||
} else {
|
} else {
|
||||||
@@ -187,20 +198,40 @@ func (tc *TCPClientInterface) readLoop() {
|
|||||||
|
|
||||||
func (tc *TCPClientInterface) handlePacket(data []byte) {
|
func (tc *TCPClientInterface) handlePacket(data []byte) {
|
||||||
if len(data) < 1 {
|
if len(data) < 1 {
|
||||||
|
log.Printf("[DEBUG-7] Received invalid packet: empty")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
packetType := data[0]
|
tc.mutex.Lock()
|
||||||
|
tc.packetType = data[0]
|
||||||
|
tc.RxBytes += uint64(len(data))
|
||||||
|
lastRx := time.Now()
|
||||||
|
tc.lastRx = lastRx
|
||||||
|
tc.mutex.Unlock()
|
||||||
|
|
||||||
|
log.Printf("[DEBUG-7] Received packet: type=0x%02x, size=%d bytes", tc.packetType, len(data))
|
||||||
|
|
||||||
payload := data[1:]
|
payload := data[1:]
|
||||||
|
|
||||||
switch packetType {
|
switch tc.packetType {
|
||||||
case 0x01: // Path request
|
case 0x01: // Announce packet
|
||||||
tc.Interface.ProcessIncoming(payload)
|
log.Printf("[DEBUG-7] Processing announce packet: payload=%d bytes", len(payload))
|
||||||
|
if len(payload) >= 53 {
|
||||||
|
tc.BaseInterface.ProcessIncoming(payload)
|
||||||
|
} else {
|
||||||
|
log.Printf("[DEBUG-7] Announce packet too small: %d bytes", len(payload))
|
||||||
|
}
|
||||||
case 0x02: // Link packet
|
case 0x02: // Link packet
|
||||||
if len(payload) < 40 { // minimum size for link packet
|
log.Printf("[DEBUG-7] Processing link packet: payload=%d bytes", len(payload))
|
||||||
|
if len(payload) < 40 {
|
||||||
|
log.Printf("[DEBUG-7] Link packet too small: %d bytes", len(payload))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tc.Interface.ProcessIncoming(payload)
|
tc.BaseInterface.ProcessIncoming(payload)
|
||||||
|
case 0x03: // Announce packet
|
||||||
|
tc.BaseInterface.ProcessIncoming(payload)
|
||||||
|
case 0x04: // Transport packet
|
||||||
|
tc.BaseInterface.ProcessIncoming(payload)
|
||||||
default:
|
default:
|
||||||
// Unknown packet type
|
// Unknown packet type
|
||||||
return
|
return
|
||||||
@@ -224,13 +255,11 @@ func (tc *TCPClientInterface) ProcessOutgoing(data []byte) error {
|
|||||||
frame = append(frame, HDLC_FLAG)
|
frame = append(frame, HDLC_FLAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := tc.conn.Write(frame); err != nil {
|
// Update TX stats before sending
|
||||||
tc.teardown()
|
tc.UpdateStats(uint64(len(frame)), false)
|
||||||
return fmt.Errorf("write failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tc.Interface.ProcessOutgoing(data)
|
_, err := tc.conn.Write(frame)
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) teardown() {
|
func (tc *TCPClientInterface) teardown() {
|
||||||
@@ -269,130 +298,237 @@ func escapeKISS(data []byte) []byte {
|
|||||||
return escaped
|
return escaped
|
||||||
}
|
}
|
||||||
|
|
||||||
type TCPServerInterface struct {
|
func (tc *TCPClientInterface) SetPacketCallback(cb common.PacketCallback) {
|
||||||
Interface
|
tc.packetCallback = cb
|
||||||
server net.Listener
|
|
||||||
bindAddr string
|
|
||||||
bindPort int
|
|
||||||
i2pTunneled bool
|
|
||||||
preferIPv6 bool
|
|
||||||
spawned []*TCPClientInterface
|
|
||||||
spawnedMutex sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTCPServer(name string, bindAddr string, bindPort int, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) {
|
func (tc *TCPClientInterface) IsEnabled() bool {
|
||||||
ts := &TCPServerInterface{
|
tc.mutex.RLock()
|
||||||
Interface: Interface{
|
defer tc.mutex.RUnlock()
|
||||||
Name: name,
|
return tc.enabled && tc.Online && !tc.Detached
|
||||||
Mode: MODE_FULL,
|
|
||||||
MTU: 1064,
|
|
||||||
Bitrate: 10000000, // 10Mbps estimate
|
|
||||||
},
|
|
||||||
bindAddr: bindAddr,
|
|
||||||
bindPort: bindPort,
|
|
||||||
i2pTunneled: i2pTunneled,
|
|
||||||
preferIPv6: preferIPv6,
|
|
||||||
spawned: make([]*TCPClientInterface, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve bind address
|
|
||||||
var addr string
|
|
||||||
if ts.bindAddr == "" {
|
|
||||||
if ts.preferIPv6 {
|
|
||||||
addr = fmt.Sprintf("[::0]:%d", ts.bindPort)
|
|
||||||
} else {
|
|
||||||
addr = fmt.Sprintf("0.0.0.0:%d", ts.bindPort)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
addr = fmt.Sprintf("%s:%d", ts.bindAddr, ts.bindPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create listener
|
|
||||||
var err error
|
|
||||||
ts.server, err = net.Listen("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create TCP listener: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ts.Online = true
|
|
||||||
ts.IN = true
|
|
||||||
|
|
||||||
// Start accept loop
|
|
||||||
go ts.acceptLoop()
|
|
||||||
|
|
||||||
return ts, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) acceptLoop() {
|
func (tc *TCPClientInterface) GetName() string {
|
||||||
for {
|
return tc.Name
|
||||||
conn, err := ts.server.Accept()
|
}
|
||||||
if err != nil {
|
|
||||||
if !ts.Detached {
|
func (tc *TCPClientInterface) GetPacketCallback() common.PacketCallback {
|
||||||
// Log error and continue accepting
|
tc.mutex.RLock()
|
||||||
continue
|
defer tc.mutex.RUnlock()
|
||||||
}
|
return tc.packetCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) IsDetached() bool {
|
||||||
|
tc.mutex.RLock()
|
||||||
|
defer tc.mutex.RUnlock()
|
||||||
|
return tc.Detached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) IsOnline() bool {
|
||||||
|
tc.mutex.RLock()
|
||||||
|
defer tc.mutex.RUnlock()
|
||||||
|
return tc.Online
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) reconnect() {
|
||||||
|
tc.mutex.Lock()
|
||||||
|
if tc.reconnecting {
|
||||||
|
tc.mutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tc.reconnecting = true
|
||||||
|
tc.mutex.Unlock()
|
||||||
|
|
||||||
|
backoff := time.Second
|
||||||
|
maxBackoff := time.Minute * 5
|
||||||
|
retries := 0
|
||||||
|
|
||||||
|
for retries < tc.maxReconnectTries {
|
||||||
|
tc.teardown()
|
||||||
|
|
||||||
|
addr := fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort)
|
||||||
|
|
||||||
|
conn, err := net.Dial("tcp", addr)
|
||||||
|
if err == nil {
|
||||||
|
tc.mutex.Lock()
|
||||||
|
tc.conn = conn
|
||||||
|
tc.Online = true
|
||||||
|
|
||||||
|
tc.neverConnected = false
|
||||||
|
tc.reconnecting = false
|
||||||
|
tc.mutex.Unlock()
|
||||||
|
|
||||||
|
go tc.readLoop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new client interface for this connection
|
// Log reconnection attempt
|
||||||
client := &TCPClientInterface{
|
fmt.Printf("Failed to reconnect to %s (attempt %d/%d): %v\n",
|
||||||
Interface: Interface{
|
addr, retries+1, tc.maxReconnectTries, err)
|
||||||
Name: fmt.Sprintf("Client-%s-%s", ts.Name, conn.RemoteAddr()),
|
|
||||||
Mode: ts.Mode,
|
// Wait with exponential backoff
|
||||||
MTU: ts.MTU,
|
time.Sleep(backoff)
|
||||||
},
|
|
||||||
conn: conn,
|
// Increase backoff time exponentially
|
||||||
i2pTunneled: ts.i2pTunneled,
|
backoff *= 2
|
||||||
|
if backoff > maxBackoff {
|
||||||
|
backoff = maxBackoff
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure TCP options
|
retries++
|
||||||
if tcpConn, ok := conn.(*net.TCPConn); ok {
|
}
|
||||||
tcpConn.SetNoDelay(true)
|
|
||||||
tcpConn.SetKeepAlive(true)
|
tc.mutex.Lock()
|
||||||
tcpConn.SetKeepAlivePeriod(time.Duration(TCP_PROBE_INTERVAL) * time.Second)
|
tc.reconnecting = false
|
||||||
|
tc.mutex.Unlock()
|
||||||
|
|
||||||
|
// If we've exhausted all retries, perform final teardown
|
||||||
|
tc.teardown()
|
||||||
|
fmt.Printf("Failed to reconnect to %s after %d attempts\n",
|
||||||
|
fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort), tc.maxReconnectTries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) Enable() {
|
||||||
|
tc.mutex.Lock()
|
||||||
|
defer tc.mutex.Unlock()
|
||||||
|
tc.Online = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) Disable() {
|
||||||
|
tc.mutex.Lock()
|
||||||
|
defer tc.mutex.Unlock()
|
||||||
|
tc.Online = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) IsConnected() bool {
|
||||||
|
tc.mutex.RLock()
|
||||||
|
defer tc.mutex.RUnlock()
|
||||||
|
return tc.conn != nil && tc.Online && !tc.reconnecting
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) GetRTT() time.Duration {
|
||||||
|
tc.mutex.RLock()
|
||||||
|
defer tc.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !tc.IsConnected() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if tcpConn, ok := tc.conn.(*net.TCPConn); ok {
|
||||||
|
var rtt time.Duration = 0
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
if info, err := tcpConn.SyscallConn(); err == nil {
|
||||||
|
info.Control(func(fd uintptr) {
|
||||||
|
rtt = platformGetRTT(fd)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return rtt
|
||||||
|
}
|
||||||
|
|
||||||
client.Online = true
|
return 0
|
||||||
client.IN = ts.IN
|
}
|
||||||
client.OUT = ts.OUT
|
|
||||||
|
|
||||||
// Add to spawned interfaces
|
func (tc *TCPClientInterface) GetTxBytes() uint64 {
|
||||||
ts.spawnedMutex.Lock()
|
tc.mutex.RLock()
|
||||||
ts.spawned = append(ts.spawned, client)
|
defer tc.mutex.RUnlock()
|
||||||
ts.spawnedMutex.Unlock()
|
return tc.TxBytes
|
||||||
|
}
|
||||||
|
|
||||||
// Start client read loop
|
func (tc *TCPClientInterface) GetRxBytes() uint64 {
|
||||||
go client.readLoop()
|
tc.mutex.RLock()
|
||||||
|
defer tc.mutex.RUnlock()
|
||||||
|
return tc.RxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) UpdateStats(bytes uint64, isRx bool) {
|
||||||
|
tc.mutex.Lock()
|
||||||
|
defer tc.mutex.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if isRx {
|
||||||
|
tc.RxBytes += bytes
|
||||||
|
tc.lastRx = now
|
||||||
|
log.Printf("[DEBUG-5] Interface %s RX stats: bytes=%d total=%d last=%v",
|
||||||
|
tc.Name, bytes, tc.RxBytes, tc.lastRx)
|
||||||
|
} else {
|
||||||
|
tc.TxBytes += bytes
|
||||||
|
tc.lastTx = now
|
||||||
|
log.Printf("[DEBUG-5] Interface %s TX stats: bytes=%d total=%d last=%v",
|
||||||
|
tc.Name, bytes, tc.TxBytes, tc.lastTx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) Detach() {
|
func (tc *TCPClientInterface) GetStats() (tx uint64, rx uint64, lastTx time.Time, lastRx time.Time) {
|
||||||
ts.Interface.Detach()
|
tc.mutex.RLock()
|
||||||
|
defer tc.mutex.RUnlock()
|
||||||
if ts.server != nil {
|
return tc.TxBytes, tc.RxBytes, tc.lastTx, tc.lastRx
|
||||||
ts.server.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
ts.spawnedMutex.Lock()
|
|
||||||
for _, client := range ts.spawned {
|
|
||||||
client.Detach()
|
|
||||||
}
|
|
||||||
ts.spawned = nil
|
|
||||||
ts.spawnedMutex.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
|
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||||
ts.spawnedMutex.RLock()
|
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||||
defer ts.spawnedMutex.RUnlock()
|
if !ok {
|
||||||
|
return fmt.Errorf("not a TCP connection")
|
||||||
|
}
|
||||||
|
|
||||||
var lastErr error
|
if !tc.i2pTunneled {
|
||||||
for _, client := range ts.spawned {
|
if err := tcpConn.SetKeepAlive(true); err != nil {
|
||||||
if err := client.ProcessOutgoing(data); err != nil {
|
return err
|
||||||
lastErr = err
|
}
|
||||||
|
if err := tcpConn.SetKeepAlivePeriod(time.Duration(TCP_PROBE_INTERVAL) * time.Second); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return lastErr
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||||
|
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("not a TCP connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tcpConn.SetKeepAlive(true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TCPServerInterface struct {
|
||||||
|
BaseInterface
|
||||||
|
connections map[string]net.Conn
|
||||||
|
mutex sync.RWMutex
|
||||||
|
bindAddr string
|
||||||
|
bindPort int
|
||||||
|
preferIPv6 bool
|
||||||
|
kissFraming bool
|
||||||
|
i2pTunneled bool
|
||||||
|
packetCallback common.PacketCallback
|
||||||
|
TxBytes uint64
|
||||||
|
RxBytes uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFraming bool, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) {
|
||||||
|
ts := &TCPServerInterface{
|
||||||
|
BaseInterface: BaseInterface{
|
||||||
|
Name: name,
|
||||||
|
Mode: common.IF_MODE_FULL,
|
||||||
|
Type: common.IF_TYPE_TCP,
|
||||||
|
Online: false,
|
||||||
|
MTU: common.DEFAULT_MTU,
|
||||||
|
Detached: false,
|
||||||
|
},
|
||||||
|
connections: make(map[string]net.Conn),
|
||||||
|
bindAddr: bindAddr,
|
||||||
|
bindPort: bindPort,
|
||||||
|
preferIPv6: preferIPv6,
|
||||||
|
kissFraming: kissFraming,
|
||||||
|
i2pTunneled: i2pTunneled,
|
||||||
|
}
|
||||||
|
|
||||||
|
return ts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) String() string {
|
func (ts *TCPServerInterface) String() string {
|
||||||
@@ -401,8 +537,165 @@ func (ts *TCPServerInterface) String() string {
|
|||||||
if ts.preferIPv6 {
|
if ts.preferIPv6 {
|
||||||
addr = "[::0]"
|
addr = "[::0]"
|
||||||
} else {
|
} else {
|
||||||
addr = "0.0.0.0"
|
addr = "0.0.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("TCPServerInterface[%s/%s:%d]", ts.Name, addr, ts.bindPort)
|
return fmt.Sprintf("TCPServerInterface[%s/%s:%d]", ts.Name, addr, ts.bindPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||||
|
ts.mutex.Lock()
|
||||||
|
defer ts.mutex.Unlock()
|
||||||
|
ts.packetCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) GetPacketCallback() common.PacketCallback {
|
||||||
|
ts.mutex.RLock()
|
||||||
|
defer ts.mutex.RUnlock()
|
||||||
|
return ts.packetCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) IsEnabled() bool {
|
||||||
|
ts.mutex.RLock()
|
||||||
|
defer ts.mutex.RUnlock()
|
||||||
|
return ts.BaseInterface.Enabled && ts.BaseInterface.Online && !ts.BaseInterface.Detached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) GetName() string {
|
||||||
|
return ts.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) IsDetached() bool {
|
||||||
|
ts.mutex.RLock()
|
||||||
|
defer ts.mutex.RUnlock()
|
||||||
|
return ts.BaseInterface.Detached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) IsOnline() bool {
|
||||||
|
ts.mutex.RLock()
|
||||||
|
defer ts.mutex.RUnlock()
|
||||||
|
return ts.Online
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) Enable() {
|
||||||
|
ts.mutex.Lock()
|
||||||
|
defer ts.mutex.Unlock()
|
||||||
|
ts.Online = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) Disable() {
|
||||||
|
ts.mutex.Lock()
|
||||||
|
defer ts.mutex.Unlock()
|
||||||
|
ts.Online = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) Start() error {
|
||||||
|
ts.mutex.Lock()
|
||||||
|
defer ts.mutex.Unlock()
|
||||||
|
|
||||||
|
addr := fmt.Sprintf("%s:%d", ts.bindAddr, ts.bindPort)
|
||||||
|
listener, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start TCP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.Online = true
|
||||||
|
|
||||||
|
// Accept connections in a goroutine
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
if !ts.Online {
|
||||||
|
return // Normal shutdown
|
||||||
|
}
|
||||||
|
log.Printf("[DEBUG-2] Error accepting connection: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle each connection in a separate goroutine
|
||||||
|
go ts.handleConnection(conn)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) Stop() error {
|
||||||
|
ts.mutex.Lock()
|
||||||
|
defer ts.mutex.Unlock()
|
||||||
|
|
||||||
|
ts.Online = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) GetTxBytes() uint64 {
|
||||||
|
ts.mutex.RLock()
|
||||||
|
defer ts.mutex.RUnlock()
|
||||||
|
return ts.TxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) GetRxBytes() uint64 {
|
||||||
|
ts.mutex.RLock()
|
||||||
|
defer ts.mutex.RUnlock()
|
||||||
|
return ts.RxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
|
||||||
|
addr := conn.RemoteAddr().String()
|
||||||
|
ts.mutex.Lock()
|
||||||
|
ts.connections[addr] = conn
|
||||||
|
ts.mutex.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
ts.mutex.Lock()
|
||||||
|
delete(ts.connections, addr)
|
||||||
|
ts.mutex.Unlock()
|
||||||
|
conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
buffer := make([]byte, ts.MTU)
|
||||||
|
for {
|
||||||
|
n, err := conn.Read(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.mutex.Lock()
|
||||||
|
ts.RxBytes += uint64(n)
|
||||||
|
ts.mutex.Unlock()
|
||||||
|
|
||||||
|
if ts.packetCallback != nil {
|
||||||
|
ts.packetCallback(buffer[:n], ts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
|
||||||
|
ts.mutex.RLock()
|
||||||
|
defer ts.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !ts.Online {
|
||||||
|
return fmt.Errorf("interface offline")
|
||||||
|
}
|
||||||
|
|
||||||
|
var frame []byte
|
||||||
|
if ts.kissFraming {
|
||||||
|
frame = append([]byte{KISS_FEND}, escapeKISS(data)...)
|
||||||
|
frame = append(frame, KISS_FEND)
|
||||||
|
} else {
|
||||||
|
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
|
||||||
|
frame = append(frame, HDLC_FLAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.TxBytes += uint64(len(frame))
|
||||||
|
|
||||||
|
for _, conn := range ts.connections {
|
||||||
|
if _, err := conn.Write(frame); err != nil {
|
||||||
|
log.Printf("[DEBUG-4] Error writing to connection %s: %v",
|
||||||
|
conn.RemoteAddr(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UDPInterface struct {
|
type UDPInterface struct {
|
||||||
Interface
|
BaseInterface
|
||||||
conn *net.UDPConn
|
conn *net.UDPConn
|
||||||
listenAddr *net.UDPAddr
|
addr *net.UDPAddr
|
||||||
targetAddr *net.UDPAddr
|
targetAddr *net.UDPAddr
|
||||||
|
mutex sync.RWMutex
|
||||||
readBuffer []byte
|
readBuffer []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUDPInterface(name string, listenAddr string, targetAddr string) (*UDPInterface, error) {
|
func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) {
|
||||||
ui := &UDPInterface{
|
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||||
Interface: Interface{
|
|
||||||
Name: name,
|
|
||||||
Mode: MODE_FULL,
|
|
||||||
MTU: 1500,
|
|
||||||
Bitrate: 100000000, // 100Mbps estimate for UDP
|
|
||||||
},
|
|
||||||
readBuffer: make([]byte, 65535),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse listen address
|
|
||||||
laddr, err := net.ResolveUDPAddr("udp", listenAddr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid listen address: %v", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
ui.listenAddr = laddr
|
|
||||||
|
|
||||||
// Parse target address if provided
|
var targetAddr *net.UDPAddr
|
||||||
if targetAddr != "" {
|
if target != "" {
|
||||||
taddr, err := net.ResolveUDPAddr("udp", targetAddr)
|
targetAddr, err = net.ResolveUDPAddr("udp", target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid target address: %v", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
ui.targetAddr = taddr
|
|
||||||
ui.OUT = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create UDP connection
|
ui := &UDPInterface{
|
||||||
conn, err := net.ListenUDP("udp", ui.listenAddr)
|
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
|
||||||
if err != nil {
|
addr: udpAddr,
|
||||||
return nil, fmt.Errorf("failed to listen on UDP: %v", err)
|
targetAddr: targetAddr,
|
||||||
|
readBuffer: make([]byte, common.DEFAULT_MTU),
|
||||||
}
|
}
|
||||||
ui.conn = conn
|
|
||||||
ui.IN = true
|
|
||||||
ui.Online = true
|
|
||||||
|
|
||||||
// Start read loop
|
|
||||||
go ui.readLoop()
|
|
||||||
|
|
||||||
return ui, nil
|
return ui, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) readLoop() {
|
func (ui *UDPInterface) GetName() string {
|
||||||
for {
|
return ui.Name
|
||||||
if !ui.Online {
|
}
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
n, addr, err := ui.conn.ReadFromUDP(ui.readBuffer)
|
func (ui *UDPInterface) GetType() common.InterfaceType {
|
||||||
if err != nil {
|
return ui.Type
|
||||||
if !ui.Detached {
|
}
|
||||||
// Log error
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy received data
|
func (ui *UDPInterface) GetMode() common.InterfaceMode {
|
||||||
data := make([]byte, n)
|
return ui.Mode
|
||||||
copy(data, ui.readBuffer[:n])
|
}
|
||||||
|
|
||||||
// Process packet
|
func (ui *UDPInterface) IsOnline() bool {
|
||||||
ui.ProcessIncoming(data)
|
ui.mutex.RLock()
|
||||||
|
defer ui.mutex.RUnlock()
|
||||||
|
return ui.Online
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) IsDetached() bool {
|
||||||
|
ui.mutex.RLock()
|
||||||
|
defer ui.mutex.RUnlock()
|
||||||
|
return ui.Detached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) Detach() {
|
||||||
|
ui.mutex.Lock()
|
||||||
|
defer ui.mutex.Unlock()
|
||||||
|
ui.Detached = true
|
||||||
|
if ui.conn != nil {
|
||||||
|
ui.conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) Send(data []byte, addr string) error {
|
||||||
|
if !ui.IsEnabled() {
|
||||||
|
return fmt.Errorf("interface not enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.targetAddr == nil {
|
||||||
|
return fmt.Errorf("no target address configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ui.conn.WriteTo(data, ui.targetAddr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||||
|
ui.mutex.Lock()
|
||||||
|
defer ui.mutex.Unlock()
|
||||||
|
ui.packetCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) GetPacketCallback() common.PacketCallback {
|
||||||
|
ui.mutex.RLock()
|
||||||
|
defer ui.mutex.RUnlock()
|
||||||
|
return ui.packetCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) ProcessIncoming(data []byte) {
|
||||||
|
if callback := ui.GetPacketCallback(); callback != nil {
|
||||||
|
callback(data, ui)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
|
func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
|
||||||
if !ui.Online || ui.targetAddr == nil {
|
if !ui.IsOnline() {
|
||||||
return fmt.Errorf("interface offline or no target address configured")
|
return fmt.Errorf("interface offline")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.targetAddr == nil {
|
||||||
|
return fmt.Errorf("no target address configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := ui.conn.WriteToUDP(data, ui.targetAddr)
|
_, err := ui.conn.WriteToUDP(data, ui.targetAddr)
|
||||||
@@ -90,13 +120,88 @@ func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
|
|||||||
return fmt.Errorf("UDP write failed: %v", err)
|
return fmt.Errorf("UDP write failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.Interface.ProcessOutgoing(data)
|
ui.mutex.Lock()
|
||||||
|
ui.TxBytes += uint64(len(data))
|
||||||
|
ui.mutex.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) Detach() {
|
func (ui *UDPInterface) GetConn() net.Conn {
|
||||||
ui.Interface.Detach()
|
return ui.conn
|
||||||
if ui.conn != nil {
|
}
|
||||||
ui.conn.Close()
|
|
||||||
|
func (ui *UDPInterface) GetTxBytes() uint64 {
|
||||||
|
ui.mutex.RLock()
|
||||||
|
defer ui.mutex.RUnlock()
|
||||||
|
return ui.TxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) GetRxBytes() uint64 {
|
||||||
|
ui.mutex.RLock()
|
||||||
|
defer ui.mutex.RUnlock()
|
||||||
|
return ui.RxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) GetMTU() int {
|
||||||
|
return ui.MTU
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) GetBitrate() int {
|
||||||
|
return int(ui.Bitrate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) Enable() {
|
||||||
|
ui.mutex.Lock()
|
||||||
|
defer ui.mutex.Unlock()
|
||||||
|
ui.Online = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) Disable() {
|
||||||
|
ui.mutex.Lock()
|
||||||
|
defer ui.mutex.Unlock()
|
||||||
|
ui.Online = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) Start() error {
|
||||||
|
conn, err := net.ListenUDP("udp", ui.addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
ui.conn = conn
|
||||||
|
ui.Online = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) readLoop() {
|
||||||
|
buffer := make([]byte, ui.MTU)
|
||||||
|
for {
|
||||||
|
if ui.IsDetached() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
n, addr, err := ui.conn.ReadFromUDP(buffer)
|
||||||
|
if err != nil {
|
||||||
|
if !ui.IsDetached() {
|
||||||
|
log.Printf("UDP read error: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.mutex.Lock()
|
||||||
|
ui.RxBytes += uint64(n)
|
||||||
|
ui.mutex.Unlock()
|
||||||
|
|
||||||
|
log.Printf("Received %d bytes from %s", n, addr.String())
|
||||||
|
|
||||||
|
if callback := ui.GetPacketCallback(); callback != nil {
|
||||||
|
callback(buffer[:n], ui)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) IsEnabled() bool {
|
||||||
|
ui.mutex.RLock()
|
||||||
|
defer ui.mutex.RUnlock()
|
||||||
|
return ui.Enabled && ui.Online && !ui.Detached
|
||||||
|
}
|
||||||
|
|||||||
659
pkg/link/link.go
659
pkg/link/link.go
@@ -5,104 +5,183 @@ import (
|
|||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"encoding/hex"
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/destination"
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/packet"
|
"github.com/Sudo-Ivan/reticulum-go/pkg/packet"
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/pathfinder"
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/resolver"
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/resource"
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CURVE = "Curve25519"
|
CURVE = "Curve25519"
|
||||||
|
|
||||||
ESTABLISHMENT_TIMEOUT_PER_HOP = 6
|
ESTABLISHMENT_TIMEOUT_PER_HOP = 6
|
||||||
KEEPALIVE_TIMEOUT_FACTOR = 4
|
KEEPALIVE_TIMEOUT_FACTOR = 4
|
||||||
STALE_GRACE = 2
|
STALE_GRACE = 2
|
||||||
KEEPALIVE = 360
|
KEEPALIVE = 360
|
||||||
STALE_TIME = 720
|
STALE_TIME = 720
|
||||||
|
|
||||||
ACCEPT_NONE = 0x00
|
ACCEPT_NONE = 0x00
|
||||||
ACCEPT_ALL = 0x01
|
ACCEPT_ALL = 0x01
|
||||||
ACCEPT_APP = 0x02
|
ACCEPT_APP = 0x02
|
||||||
|
|
||||||
STATUS_PENDING = 0x00
|
STATUS_PENDING = 0x00
|
||||||
STATUS_ACTIVE = 0x01
|
STATUS_ACTIVE = 0x01
|
||||||
STATUS_CLOSED = 0x02
|
STATUS_CLOSED = 0x02
|
||||||
STATUS_FAILED = 0x03
|
STATUS_FAILED = 0x03
|
||||||
|
|
||||||
|
PROVE_NONE = 0x00
|
||||||
|
PROVE_ALL = 0x01
|
||||||
|
PROVE_APP = 0x02
|
||||||
|
|
||||||
|
WATCHDOG_MIN_SLEEP = 0.025
|
||||||
|
WATCHDOG_INTERVAL = 0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
type Link struct {
|
type Link struct {
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
destination interface{}
|
destination *destination.Destination
|
||||||
status byte
|
status byte
|
||||||
establishedAt time.Time
|
networkInterface common.NetworkInterface
|
||||||
lastInbound time.Time
|
establishedAt time.Time
|
||||||
lastOutbound time.Time
|
lastInbound time.Time
|
||||||
lastDataReceived time.Time
|
lastOutbound time.Time
|
||||||
lastDataSent time.Time
|
lastDataReceived time.Time
|
||||||
|
lastDataSent time.Time
|
||||||
remoteIdentity *identity.Identity
|
pathFinder *pathfinder.PathFinder
|
||||||
sessionKey []byte
|
|
||||||
linkID []byte
|
remoteIdentity *identity.Identity
|
||||||
|
sessionKey []byte
|
||||||
|
linkID []byte
|
||||||
|
|
||||||
rtt float64
|
rtt float64
|
||||||
establishmentRate float64
|
establishmentRate float64
|
||||||
|
|
||||||
trackPhyStats bool
|
|
||||||
rssi float64
|
|
||||||
snr float64
|
|
||||||
q float64
|
|
||||||
|
|
||||||
resourceStrategy byte
|
|
||||||
|
|
||||||
establishedCallback func(*Link)
|
establishedCallback func(*Link)
|
||||||
closedCallback func(*Link)
|
closedCallback func(*Link)
|
||||||
packetCallback func([]byte, *packet.Packet)
|
packetCallback func([]byte, *packet.Packet)
|
||||||
resourceCallback func(interface{}) bool
|
identifiedCallback func(*Link, *identity.Identity)
|
||||||
resourceStartedCallback func(interface{})
|
|
||||||
|
teardownReason byte
|
||||||
|
hmacKey []byte
|
||||||
|
transport *transport.Transport
|
||||||
|
|
||||||
|
rssi float64
|
||||||
|
snr float64
|
||||||
|
q float64
|
||||||
|
resourceCallback func(interface{}) bool
|
||||||
|
resourceStartedCallback func(interface{})
|
||||||
resourceConcludedCallback func(interface{})
|
resourceConcludedCallback func(interface{})
|
||||||
remoteIdentifiedCallback func(*Link, *identity.Identity)
|
resourceStrategy byte
|
||||||
|
proofStrategy byte
|
||||||
|
proofCallback func(*packet.Packet) bool
|
||||||
|
trackPhyStats bool
|
||||||
|
|
||||||
|
watchdogLock bool
|
||||||
|
watchdogActive bool
|
||||||
|
establishmentTimeout time.Duration
|
||||||
|
keepalive time.Duration
|
||||||
|
staleTime time.Duration
|
||||||
|
initiator bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(dest interface{}, establishedCb func(*Link), closedCb func(*Link)) *Link {
|
func NewLink(dest *destination.Destination, transport *transport.Transport, networkIface common.NetworkInterface, establishedCallback func(*Link), closedCallback func(*Link)) *Link {
|
||||||
l := &Link{
|
return &Link{
|
||||||
destination: dest,
|
destination: dest,
|
||||||
status: STATUS_PENDING,
|
status: STATUS_PENDING,
|
||||||
establishedAt: time.Time{},
|
transport: transport,
|
||||||
lastInbound: time.Time{},
|
networkInterface: networkIface,
|
||||||
lastOutbound: time.Time{},
|
establishedCallback: establishedCallback,
|
||||||
lastDataReceived: time.Time{},
|
closedCallback: closedCallback,
|
||||||
lastDataSent: time.Time{},
|
establishedAt: time.Time{}, // Zero time until established
|
||||||
resourceStrategy: ACCEPT_NONE,
|
lastInbound: time.Time{},
|
||||||
establishedCallback: establishedCb,
|
lastOutbound: time.Time{},
|
||||||
closedCallback: closedCb,
|
lastDataReceived: time.Time{},
|
||||||
|
lastDataSent: time.Time{},
|
||||||
|
pathFinder: pathfinder.NewPathFinder(),
|
||||||
|
|
||||||
|
watchdogLock: false,
|
||||||
|
watchdogActive: false,
|
||||||
|
establishmentTimeout: time.Duration(ESTABLISHMENT_TIMEOUT_PER_HOP * float64(time.Second)),
|
||||||
|
keepalive: time.Duration(KEEPALIVE * float64(time.Second)),
|
||||||
|
staleTime: time.Duration(STALE_TIME * float64(time.Second)),
|
||||||
|
initiator: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
return l
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Link) Identify(id *identity.Identity) error {
|
func (l *Link) Establish() error {
|
||||||
l.mutex.Lock()
|
l.mutex.Lock()
|
||||||
defer l.mutex.Unlock()
|
defer l.mutex.Unlock()
|
||||||
|
|
||||||
if l.status != STATUS_ACTIVE {
|
if l.status != STATUS_PENDING {
|
||||||
return errors.New("link not active")
|
log.Printf("[DEBUG-3] Cannot establish link: invalid status %d", l.status)
|
||||||
|
return errors.New("link already established or failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create identification message
|
destPublicKey := l.destination.GetPublicKey()
|
||||||
idMsg := append(id.GetPublicKey(), id.Sign(l.linkID)...)
|
if destPublicKey == nil {
|
||||||
|
log.Printf("[DEBUG-3] Cannot establish link: destination has no public key")
|
||||||
// Encrypt and send identification
|
return errors.New("destination has no public key")
|
||||||
err := l.SendPacket(idMsg)
|
}
|
||||||
if err != nil {
|
|
||||||
|
log.Printf("[DEBUG-4] Creating link request packet for destination %x", destPublicKey[:8])
|
||||||
|
|
||||||
|
p := &packet.Packet{
|
||||||
|
HeaderType: packet.HeaderType1,
|
||||||
|
PacketType: packet.PacketTypeLinkReq,
|
||||||
|
TransportType: 0,
|
||||||
|
Context: packet.ContextLinkIdentify,
|
||||||
|
ContextFlag: packet.FlagUnset,
|
||||||
|
Hops: 0,
|
||||||
|
DestinationType: l.destination.GetType(),
|
||||||
|
DestinationHash: l.destination.GetHash(),
|
||||||
|
Data: l.linkID,
|
||||||
|
CreateReceipt: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.Pack(); err != nil {
|
||||||
|
log.Printf("[DEBUG-3] Failed to pack link request packet: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
log.Printf("[DEBUG-4] Sending link request packet with ID %x", l.linkID[:8])
|
||||||
|
return l.transport.SendPacket(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Link) Identify(id *identity.Identity) error {
|
||||||
|
if !l.IsActive() {
|
||||||
|
return errors.New("link not active")
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &packet.Packet{
|
||||||
|
HeaderType: packet.HeaderType1,
|
||||||
|
PacketType: packet.PacketTypeData,
|
||||||
|
TransportType: 0,
|
||||||
|
Context: packet.ContextLinkIdentify,
|
||||||
|
ContextFlag: packet.FlagUnset,
|
||||||
|
Hops: 0,
|
||||||
|
DestinationType: l.destination.GetType(),
|
||||||
|
DestinationHash: l.destination.GetHash(),
|
||||||
|
Data: id.GetPublicKey(),
|
||||||
|
CreateReceipt: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.Pack(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.transport.SendPacket(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Link) HandleIdentification(data []byte) error {
|
func (l *Link) HandleIdentification(data []byte) error {
|
||||||
@@ -110,26 +189,33 @@ func (l *Link) HandleIdentification(data []byte) error {
|
|||||||
defer l.mutex.Unlock()
|
defer l.mutex.Unlock()
|
||||||
|
|
||||||
if len(data) < ed25519.PublicKeySize+ed25519.SignatureSize {
|
if len(data) < ed25519.PublicKeySize+ed25519.SignatureSize {
|
||||||
return errors.New("invalid identification data")
|
log.Printf("[DEBUG-3] Invalid identification data length: %d bytes", len(data))
|
||||||
|
return errors.New("invalid identification data length")
|
||||||
}
|
}
|
||||||
|
|
||||||
pubKey := data[:ed25519.PublicKeySize]
|
pubKey := data[:ed25519.PublicKeySize]
|
||||||
signature := data[ed25519.PublicKeySize:]
|
signature := data[ed25519.PublicKeySize:]
|
||||||
|
|
||||||
remoteIdentity := &identity.Identity{}
|
log.Printf("[DEBUG-4] Processing identification from public key %x", pubKey[:8])
|
||||||
if !remoteIdentity.LoadPublicKey(pubKey) {
|
|
||||||
return errors.New("invalid remote public key")
|
remoteIdentity := identity.FromPublicKey(pubKey)
|
||||||
|
if remoteIdentity == nil {
|
||||||
|
log.Printf("[DEBUG-3] Invalid remote identity from public key %x", pubKey[:8])
|
||||||
|
return errors.New("invalid remote identity")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify signature of link ID
|
signData := append(l.linkID, pubKey...)
|
||||||
if !remoteIdentity.Verify(l.linkID, signature) {
|
if !remoteIdentity.Verify(signData, signature) {
|
||||||
return errors.New("invalid identification signature")
|
log.Printf("[DEBUG-3] Invalid signature from remote identity %x", pubKey[:8])
|
||||||
|
return errors.New("invalid signature")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG-4] Remote identity verified successfully: %x", pubKey[:8])
|
||||||
l.remoteIdentity = remoteIdentity
|
l.remoteIdentity = remoteIdentity
|
||||||
|
|
||||||
if l.remoteIdentifiedCallback != nil {
|
if l.identifiedCallback != nil {
|
||||||
l.remoteIdentifiedCallback(l, remoteIdentity)
|
log.Printf("[DEBUG-4] Executing identified callback for remote identity %x", pubKey[:8])
|
||||||
|
l.identifiedCallback(l, remoteIdentity)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -158,8 +244,8 @@ func (l *Link) Request(path string, data []byte, timeout time.Duration) (*Reques
|
|||||||
|
|
||||||
receipt := &RequestReceipt{
|
receipt := &RequestReceipt{
|
||||||
requestID: requestID,
|
requestID: requestID,
|
||||||
status: STATUS_PENDING,
|
status: STATUS_PENDING,
|
||||||
sentAt: time.Now(),
|
sentAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send request
|
// Send request
|
||||||
@@ -184,12 +270,12 @@ func (l *Link) Request(path string, data []byte, timeout time.Duration) (*Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RequestReceipt struct {
|
type RequestReceipt struct {
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
requestID []byte
|
requestID []byte
|
||||||
status byte
|
status byte
|
||||||
sentAt time.Time
|
sentAt time.Time
|
||||||
receivedAt time.Time
|
receivedAt time.Time
|
||||||
response []byte
|
response []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RequestReceipt) GetRequestID() []byte {
|
func (r *RequestReceipt) GetRequestID() []byte {
|
||||||
@@ -233,21 +319,40 @@ func (l *Link) TrackPhyStats(track bool) {
|
|||||||
l.trackPhyStats = track
|
l.trackPhyStats = track
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Link) UpdatePhyStats(rssi, snr, q float64) {
|
||||||
|
l.mutex.Lock()
|
||||||
|
defer l.mutex.Unlock()
|
||||||
|
if l.trackPhyStats {
|
||||||
|
l.rssi = rssi
|
||||||
|
l.snr = snr
|
||||||
|
l.q = q
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (l *Link) GetRSSI() float64 {
|
func (l *Link) GetRSSI() float64 {
|
||||||
l.mutex.RLock()
|
l.mutex.RLock()
|
||||||
defer l.mutex.RUnlock()
|
defer l.mutex.RUnlock()
|
||||||
|
if !l.trackPhyStats {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
return l.rssi
|
return l.rssi
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Link) GetSNR() float64 {
|
func (l *Link) GetSNR() float64 {
|
||||||
l.mutex.RLock()
|
l.mutex.RLock()
|
||||||
defer l.mutex.RUnlock()
|
defer l.mutex.RUnlock()
|
||||||
|
if !l.trackPhyStats {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
return l.snr
|
return l.snr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Link) GetQ() float64 {
|
func (l *Link) GetQ() float64 {
|
||||||
l.mutex.RLock()
|
l.mutex.RLock()
|
||||||
defer l.mutex.RUnlock()
|
defer l.mutex.RUnlock()
|
||||||
|
if !l.trackPhyStats {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
return l.q
|
return l.q
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,7 +424,7 @@ func (l *Link) GetRemoteIdentity() *identity.Identity {
|
|||||||
func (l *Link) Teardown() {
|
func (l *Link) Teardown() {
|
||||||
l.mutex.Lock()
|
l.mutex.Lock()
|
||||||
defer l.mutex.Unlock()
|
defer l.mutex.Unlock()
|
||||||
|
|
||||||
if l.status == STATUS_ACTIVE {
|
if l.status == STATUS_ACTIVE {
|
||||||
l.status = STATUS_CLOSED
|
l.status = STATUS_CLOSED
|
||||||
if l.closedCallback != nil {
|
if l.closedCallback != nil {
|
||||||
@@ -361,85 +466,58 @@ func (l *Link) SetResourceConcludedCallback(callback func(interface{})) {
|
|||||||
func (l *Link) SetRemoteIdentifiedCallback(callback func(*Link, *identity.Identity)) {
|
func (l *Link) SetRemoteIdentifiedCallback(callback func(*Link, *identity.Identity)) {
|
||||||
l.mutex.Lock()
|
l.mutex.Lock()
|
||||||
defer l.mutex.Unlock()
|
defer l.mutex.Unlock()
|
||||||
l.remoteIdentifiedCallback = callback
|
l.identifiedCallback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Link) SetResourceStrategy(strategy byte) error {
|
func (l *Link) SetResourceStrategy(strategy byte) error {
|
||||||
if strategy != ACCEPT_NONE && strategy != ACCEPT_ALL && strategy != ACCEPT_APP {
|
if strategy != ACCEPT_NONE && strategy != ACCEPT_ALL && strategy != ACCEPT_APP {
|
||||||
return errors.New("unsupported resource strategy")
|
return errors.New("unsupported resource strategy")
|
||||||
}
|
}
|
||||||
|
|
||||||
l.mutex.Lock()
|
l.mutex.Lock()
|
||||||
defer l.mutex.Unlock()
|
defer l.mutex.Unlock()
|
||||||
l.resourceStrategy = strategy
|
l.resourceStrategy = strategy
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLink(destination interface{}, establishedCallback func(*Link), closedCallback func(*Link)) *Link {
|
|
||||||
l := &Link{
|
|
||||||
destination: destination,
|
|
||||||
status: STATUS_PENDING,
|
|
||||||
establishedAt: time.Time{},
|
|
||||||
lastInbound: time.Time{},
|
|
||||||
lastOutbound: time.Time{},
|
|
||||||
lastDataReceived: time.Time{},
|
|
||||||
lastDataSent: time.Time{},
|
|
||||||
establishedCallback: establishedCallback,
|
|
||||||
closedCallback: closedCallback,
|
|
||||||
resourceStrategy: ACCEPT_NONE,
|
|
||||||
trackPhyStats: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Link) Establish() error {
|
|
||||||
l.mutex.Lock()
|
|
||||||
defer l.mutex.Unlock()
|
|
||||||
|
|
||||||
if l.status != STATUS_PENDING {
|
|
||||||
return errors.New("link already established or failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate session key using ECDH
|
|
||||||
ephemeralKey := make([]byte, 32)
|
|
||||||
if _, err := rand.Read(ephemeralKey); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
l.sessionKey = ephemeralKey
|
|
||||||
|
|
||||||
l.establishedAt = time.Now()
|
|
||||||
l.status = STATUS_ACTIVE
|
|
||||||
|
|
||||||
if l.establishedCallback != nil {
|
|
||||||
l.establishedCallback(l)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Link) SendPacket(data []byte) error {
|
func (l *Link) SendPacket(data []byte) error {
|
||||||
l.mutex.Lock()
|
l.mutex.Lock()
|
||||||
defer l.mutex.Unlock()
|
defer l.mutex.Unlock()
|
||||||
|
|
||||||
if l.status != STATUS_ACTIVE {
|
if l.status != STATUS_ACTIVE {
|
||||||
|
log.Printf("[DEBUG-3] Cannot send packet: link not active (status: %d)", l.status)
|
||||||
return errors.New("link not active")
|
return errors.New("link not active")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt data using session key
|
log.Printf("[DEBUG-4] Encrypting packet of %d bytes", len(data))
|
||||||
encryptedData, err := l.encrypt(data)
|
encrypted, err := l.encrypt(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("[DEBUG-3] Failed to encrypt packet: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p := &packet.Packet{
|
||||||
|
HeaderType: packet.HeaderType1,
|
||||||
|
PacketType: packet.PacketTypeData,
|
||||||
|
TransportType: 0,
|
||||||
|
Context: packet.ContextNone,
|
||||||
|
ContextFlag: packet.FlagUnset,
|
||||||
|
Hops: 0,
|
||||||
|
DestinationType: l.destination.GetType(),
|
||||||
|
DestinationHash: l.destination.GetHash(),
|
||||||
|
Data: encrypted,
|
||||||
|
CreateReceipt: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.Pack(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG-4] Sending encrypted packet of %d bytes", len(encrypted))
|
||||||
l.lastOutbound = time.Now()
|
l.lastOutbound = time.Now()
|
||||||
l.lastDataSent = time.Now()
|
l.lastDataSent = time.Now()
|
||||||
|
|
||||||
if l.packetCallback != nil {
|
return l.transport.SendPacket(p)
|
||||||
l.packetCallback(encryptedData, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Link) HandleInbound(data []byte) error {
|
func (l *Link) HandleInbound(data []byte) error {
|
||||||
@@ -447,20 +525,28 @@ func (l *Link) HandleInbound(data []byte) error {
|
|||||||
defer l.mutex.Unlock()
|
defer l.mutex.Unlock()
|
||||||
|
|
||||||
if l.status != STATUS_ACTIVE {
|
if l.status != STATUS_ACTIVE {
|
||||||
|
log.Printf("[DEBUG-3] Dropping inbound packet: link not active (status: %d)", l.status)
|
||||||
return errors.New("link not active")
|
return errors.New("link not active")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt data using session key
|
// Decode and log packet details
|
||||||
decryptedData, err := l.decrypt(data)
|
l.decodePacket(data)
|
||||||
if err != nil {
|
|
||||||
return err
|
// Decrypt if we have a session key
|
||||||
|
if l.sessionKey != nil {
|
||||||
|
decrypted, err := l.decrypt(data)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[DEBUG-3] Failed to decrypt packet: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data = decrypted
|
||||||
}
|
}
|
||||||
|
|
||||||
l.lastInbound = time.Now()
|
l.lastInbound = time.Now()
|
||||||
l.lastDataReceived = time.Now()
|
l.lastDataReceived = time.Now()
|
||||||
|
|
||||||
if l.packetCallback != nil {
|
if l.packetCallback != nil {
|
||||||
l.packetCallback(decryptedData, nil)
|
l.packetCallback(data, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -476,17 +562,27 @@ func (l *Link) encrypt(data []byte) ([]byte, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
gcm, err := cipher.NewGCM(block)
|
// Generate IV
|
||||||
if err != nil {
|
iv := make([]byte, aes.BlockSize)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
nonce := make([]byte, gcm.NonceSize())
|
// Add PKCS7 padding
|
||||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
padding := aes.BlockSize - len(data)%aes.BlockSize
|
||||||
return nil, err
|
padtext := make([]byte, len(data)+padding)
|
||||||
|
copy(padtext, data)
|
||||||
|
for i := len(data); i < len(padtext); i++ {
|
||||||
|
padtext[i] = byte(padding)
|
||||||
}
|
}
|
||||||
|
|
||||||
return gcm.Seal(nonce, nonce, data, nil), nil
|
// Encrypt
|
||||||
|
mode := cipher.NewCBCEncrypter(block, iv)
|
||||||
|
ciphertext := make([]byte, len(padtext))
|
||||||
|
mode.CryptBlocks(ciphertext, padtext)
|
||||||
|
|
||||||
|
// Prepend IV to ciphertext
|
||||||
|
return append(iv, ciphertext...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Link) decrypt(data []byte) ([]byte, error) {
|
func (l *Link) decrypt(data []byte) ([]byte, error) {
|
||||||
@@ -499,31 +595,34 @@ func (l *Link) decrypt(data []byte) ([]byte, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
gcm, err := cipher.NewGCM(block)
|
if len(data) < aes.BlockSize {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
nonceSize := gcm.NonceSize()
|
|
||||||
if len(data) < nonceSize {
|
|
||||||
return nil, errors.New("ciphertext too short")
|
return nil, errors.New("ciphertext too short")
|
||||||
}
|
}
|
||||||
|
|
||||||
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
|
iv := data[:aes.BlockSize]
|
||||||
return gcm.Open(nil, nonce, ciphertext, nil)
|
ciphertext := data[aes.BlockSize:]
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Link) UpdatePhyStats(rssi float64, snr float64, q float64) {
|
if len(ciphertext)%aes.BlockSize != 0 {
|
||||||
if !l.trackPhyStats {
|
return nil, errors.New("ciphertext is not a multiple of block size")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
l.mutex.Lock()
|
mode := cipher.NewCBCDecrypter(block, iv)
|
||||||
defer l.mutex.Unlock()
|
plaintext := make([]byte, len(ciphertext))
|
||||||
|
mode.CryptBlocks(plaintext, ciphertext)
|
||||||
l.rssi = rssi
|
|
||||||
l.snr = snr
|
// Remove PKCS7 padding
|
||||||
l.q = q
|
padding := int(plaintext[len(plaintext)-1])
|
||||||
|
if padding > aes.BlockSize || padding == 0 {
|
||||||
|
return nil, errors.New("invalid padding")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(plaintext) - padding; i < len(plaintext); i++ {
|
||||||
|
if plaintext[i] != byte(padding) {
|
||||||
|
return nil, errors.New("invalid padding")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext[:len(plaintext)-padding], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Link) GetRTT() float64 {
|
func (l *Link) GetRTT() float64 {
|
||||||
@@ -546,4 +645,240 @@ func (l *Link) GetStatus() byte {
|
|||||||
|
|
||||||
func (l *Link) IsActive() bool {
|
func (l *Link) IsActive() bool {
|
||||||
return l.GetStatus() == STATUS_ACTIVE
|
return l.GetStatus() == STATUS_ACTIVE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Link) SendResource(res *resource.Resource) error {
|
||||||
|
l.mutex.Lock()
|
||||||
|
defer l.mutex.Unlock()
|
||||||
|
|
||||||
|
if l.status != STATUS_ACTIVE {
|
||||||
|
l.teardownReason = STATUS_FAILED
|
||||||
|
return errors.New("link not active")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate the resource
|
||||||
|
res.Activate()
|
||||||
|
|
||||||
|
// Send the resource data as packets
|
||||||
|
buffer := make([]byte, resource.DEFAULT_SEGMENT_SIZE)
|
||||||
|
for {
|
||||||
|
n, err := res.Read(buffer)
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
l.teardownReason = STATUS_FAILED
|
||||||
|
return fmt.Errorf("error reading resource: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := l.SendPacket(buffer[:n]); err != nil {
|
||||||
|
l.teardownReason = STATUS_FAILED
|
||||||
|
return fmt.Errorf("error sending resource packet: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Link) maintainLink() {
|
||||||
|
ticker := time.NewTicker(time.Second * KEEPALIVE)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
if l.status != STATUS_ACTIVE {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inactiveTime := l.InactiveFor()
|
||||||
|
if inactiveTime > float64(STALE_TIME) {
|
||||||
|
l.mutex.Lock()
|
||||||
|
l.teardownReason = STATUS_FAILED
|
||||||
|
l.mutex.Unlock()
|
||||||
|
l.Teardown()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
noDataTime := l.NoDataFor()
|
||||||
|
if noDataTime > float64(KEEPALIVE) {
|
||||||
|
l.mutex.Lock()
|
||||||
|
err := l.SendPacket([]byte{})
|
||||||
|
if err != nil {
|
||||||
|
l.teardownReason = STATUS_FAILED
|
||||||
|
l.mutex.Unlock()
|
||||||
|
l.Teardown()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.mutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Link) Start() {
|
||||||
|
go l.maintainLink()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Link) SetProofStrategy(strategy byte) error {
|
||||||
|
if strategy != PROVE_NONE && strategy != PROVE_ALL && strategy != PROVE_APP {
|
||||||
|
return errors.New("invalid proof strategy")
|
||||||
|
}
|
||||||
|
|
||||||
|
l.mutex.Lock()
|
||||||
|
defer l.mutex.Unlock()
|
||||||
|
l.proofStrategy = strategy
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Link) SetProofCallback(callback func(*packet.Packet) bool) {
|
||||||
|
l.mutex.Lock()
|
||||||
|
defer l.mutex.Unlock()
|
||||||
|
l.proofCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Link) HandleProofRequest(packet *packet.Packet) bool {
|
||||||
|
l.mutex.RLock()
|
||||||
|
defer l.mutex.RUnlock()
|
||||||
|
|
||||||
|
switch l.proofStrategy {
|
||||||
|
case PROVE_NONE:
|
||||||
|
return false
|
||||||
|
case PROVE_ALL:
|
||||||
|
return true
|
||||||
|
case PROVE_APP:
|
||||||
|
if l.proofCallback != nil {
|
||||||
|
return l.proofCallback(packet)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Link) decodePacket(data []byte) {
|
||||||
|
if len(data) < 1 {
|
||||||
|
log.Printf("[DEBUG-7] Invalid packet: zero length")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
packetType := data[0]
|
||||||
|
log.Printf("[DEBUG-7] Packet Analysis:")
|
||||||
|
log.Printf("[DEBUG-7] - Size: %d bytes", len(data))
|
||||||
|
log.Printf("[DEBUG-7] - Type: 0x%02x", packetType)
|
||||||
|
|
||||||
|
switch packetType {
|
||||||
|
case packet.PacketTypeData:
|
||||||
|
log.Printf("[DEBUG-7] - Type Description: Data Packet")
|
||||||
|
if len(data) > 1 {
|
||||||
|
log.Printf("[DEBUG-7] - Payload Size: %d bytes", len(data)-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
case packet.PacketTypeLinkReq:
|
||||||
|
log.Printf("[DEBUG-7] - Type Description: Link Management")
|
||||||
|
if len(data) > 32 {
|
||||||
|
log.Printf("[DEBUG-7] - Link ID: %x", data[1:33])
|
||||||
|
}
|
||||||
|
|
||||||
|
case packet.PacketTypeAnnounce:
|
||||||
|
log.Printf("[DEBUG-7] Received announce packet (%d bytes)", len(data))
|
||||||
|
if len(data) < packet.MinAnnounceSize {
|
||||||
|
log.Printf("[DEBUG-3] Announce packet too short: %d bytes", len(data))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
destHash := data[2:18]
|
||||||
|
encKey := data[18:50]
|
||||||
|
signKey := data[50:82]
|
||||||
|
nameHash := data[82:92]
|
||||||
|
randomHash := data[92:102]
|
||||||
|
signature := data[102:166]
|
||||||
|
appData := data[166:]
|
||||||
|
|
||||||
|
pubKey := append(encKey, signKey...)
|
||||||
|
|
||||||
|
validationData := make([]byte, 0, 164)
|
||||||
|
validationData = append(validationData, destHash...)
|
||||||
|
validationData = append(validationData, encKey...)
|
||||||
|
validationData = append(validationData, signKey...)
|
||||||
|
validationData = append(validationData, nameHash...)
|
||||||
|
validationData = append(validationData, randomHash...)
|
||||||
|
|
||||||
|
if identity.ValidateAnnounce(validationData, destHash, pubKey, signature, appData) {
|
||||||
|
log.Printf("[DEBUG-4] Valid announce from %x", pubKey[:8])
|
||||||
|
if err := l.transport.HandleAnnounce(destHash, l.networkInterface); err != nil {
|
||||||
|
log.Printf("[DEBUG-3] Failed to handle announce: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("[DEBUG-3] Invalid announce signature from %x", pubKey[:8])
|
||||||
|
}
|
||||||
|
|
||||||
|
case packet.PacketTypeProof:
|
||||||
|
log.Printf("[DEBUG-7] - Type Description: RNS Discovery")
|
||||||
|
if len(data) > 17 {
|
||||||
|
searchHash := data[1:17]
|
||||||
|
log.Printf("[DEBUG-7] - Searching for Hash: %x", searchHash)
|
||||||
|
|
||||||
|
if id, err := resolver.ResolveIdentity(hex.EncodeToString(searchHash)); err == nil {
|
||||||
|
log.Printf("[DEBUG-7] - Found matching identity: %s", id.GetHexHash())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Printf("[DEBUG-7] - Type Description: Unknown (0x%02x)", packetType)
|
||||||
|
log.Printf("[DEBUG-7] - Raw Hex: %x", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for min of two ints
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Link) startWatchdog() {
|
||||||
|
if l.watchdogActive {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.watchdogActive = true
|
||||||
|
go l.watchdog()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Link) watchdog() {
|
||||||
|
for l.status != STATUS_CLOSED {
|
||||||
|
l.mutex.Lock()
|
||||||
|
if l.watchdogLock {
|
||||||
|
l.mutex.Unlock()
|
||||||
|
time.Sleep(time.Duration(WATCHDOG_MIN_SLEEP * float64(time.Second)))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var sleepTime float64 = WATCHDOG_INTERVAL
|
||||||
|
|
||||||
|
switch l.status {
|
||||||
|
case STATUS_ACTIVE:
|
||||||
|
lastActivity := l.lastInbound
|
||||||
|
if l.lastOutbound.After(lastActivity) {
|
||||||
|
lastActivity = l.lastOutbound
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Since(lastActivity) > l.keepalive {
|
||||||
|
if l.initiator {
|
||||||
|
l.SendPacket([]byte{}) // Keepalive packet
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Since(lastActivity) > l.staleTime {
|
||||||
|
l.status = STATUS_CLOSED
|
||||||
|
l.teardownReason = STATUS_FAILED
|
||||||
|
if l.closedCallback != nil {
|
||||||
|
l.closedCallback(l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l.mutex.Unlock()
|
||||||
|
time.Sleep(time.Duration(sleepTime * float64(time.Second)))
|
||||||
|
}
|
||||||
|
l.watchdogActive = false
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,11 +3,7 @@ package packet
|
|||||||
const (
|
const (
|
||||||
// MTU constants
|
// MTU constants
|
||||||
EncryptedMDU = 383 // Maximum size of payload data in encrypted packet
|
EncryptedMDU = 383 // Maximum size of payload data in encrypted packet
|
||||||
PlainMDU = 464 // Maximum size of payload data in unencrypted packet
|
PlainMDU = 464 // Maximum size of payload data in unencrypted packet
|
||||||
|
|
||||||
// Header Types
|
|
||||||
HeaderType1 = 0 // Two byte header, one 16 byte address field
|
|
||||||
HeaderType2 = 1 // Two byte header, two 16 byte address fields
|
|
||||||
|
|
||||||
// Propagation Types
|
// Propagation Types
|
||||||
PropagationBroadcast = 0
|
PropagationBroadcast = 0
|
||||||
@@ -19,9 +15,7 @@ const (
|
|||||||
DestinationPlain = 2
|
DestinationPlain = 2
|
||||||
DestinationLink = 3
|
DestinationLink = 3
|
||||||
|
|
||||||
// Packet Types
|
// Minimum packet sizes
|
||||||
PacketData = 0
|
MinAnnounceSize = 169 // header(2) + desthash(16) + enckey(32) + signkey(32) +
|
||||||
PacketAnnounce = 1
|
// namehash(10) + randomhash(10) + signature(64) + min appdata(3)
|
||||||
PacketLinkRequest = 2
|
)
|
||||||
PacketProof = 3
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,171 +1,250 @@
|
|||||||
package packet
|
package packet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"crypto/sha256"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
HeaderSize = 2
|
// Packet Types
|
||||||
AddressSize = 16
|
PacketTypeData = 0x00
|
||||||
ContextSize = 1
|
PacketTypeAnnounce = 0x01
|
||||||
MaxDataSize = 465 // Maximum size of payload data
|
PacketTypeLinkReq = 0x02
|
||||||
)
|
PacketTypeProof = 0x03
|
||||||
|
|
||||||
// Header flags and types
|
// Header Types
|
||||||
const (
|
HeaderType1 = 0x00
|
||||||
// First byte flags
|
HeaderType2 = 0x01
|
||||||
IFACFlag = 0x80 // Interface authentication code flag
|
|
||||||
HeaderTypeFlag = 0x40 // Header type flag
|
|
||||||
ContextFlag = 0x20 // Context flag
|
|
||||||
PropagationFlags = 0x18 // Propagation type flags (bits 3-4)
|
|
||||||
DestinationFlags = 0x06 // Destination type flags (bits 1-2)
|
|
||||||
PacketTypeFlags = 0x01 // Packet type flags (bit 0)
|
|
||||||
|
|
||||||
// Second byte
|
// Context Types
|
||||||
HopsField = 0xFF // Number of hops (entire byte)
|
ContextNone = 0x00
|
||||||
|
ContextResource = 0x01
|
||||||
|
ContextResourceAdv = 0x02
|
||||||
|
ContextResourceReq = 0x03
|
||||||
|
ContextResourceHMU = 0x04
|
||||||
|
ContextResourcePRF = 0x05
|
||||||
|
ContextResourceICL = 0x06
|
||||||
|
ContextResourceRCL = 0x07
|
||||||
|
ContextCacheReq = 0x08
|
||||||
|
ContextRequest = 0x09
|
||||||
|
ContextResponse = 0x0A
|
||||||
|
ContextPathResponse = 0x0B
|
||||||
|
ContextCommand = 0x0C
|
||||||
|
ContextCmdStatus = 0x0D
|
||||||
|
ContextChannel = 0x0E
|
||||||
|
ContextKeepalive = 0xFA
|
||||||
|
ContextLinkIdentify = 0xFB
|
||||||
|
ContextLinkClose = 0xFC
|
||||||
|
ContextLinkProof = 0xFD
|
||||||
|
ContextLRRTT = 0xFE
|
||||||
|
ContextLRProof = 0xFF
|
||||||
|
|
||||||
|
// Flag Values
|
||||||
|
FlagSet = 0x01
|
||||||
|
FlagUnset = 0x00
|
||||||
|
|
||||||
|
// Header sizes
|
||||||
|
HeaderMaxSize = 64
|
||||||
|
MTU = 500
|
||||||
|
|
||||||
|
AddressSize = 32 // Size of address/hash fields in bytes
|
||||||
)
|
)
|
||||||
|
|
||||||
type Packet struct {
|
type Packet struct {
|
||||||
Header [2]byte
|
HeaderType byte
|
||||||
Addresses []byte // Either 16 or 32 bytes depending on header type
|
PacketType byte
|
||||||
Context byte
|
TransportType byte
|
||||||
Data []byte
|
Context byte
|
||||||
AccessCode []byte // Optional: Only present if IFAC flag is set
|
ContextFlag byte
|
||||||
|
Hops byte
|
||||||
|
|
||||||
|
DestinationType byte
|
||||||
|
DestinationHash []byte
|
||||||
|
TransportID []byte
|
||||||
|
Data []byte
|
||||||
|
|
||||||
|
Raw []byte
|
||||||
|
Packed bool
|
||||||
|
Sent bool
|
||||||
|
CreateReceipt bool
|
||||||
|
FromPacked bool
|
||||||
|
|
||||||
|
SentAt time.Time
|
||||||
|
PacketHash []byte
|
||||||
|
RatchetID []byte
|
||||||
|
|
||||||
|
RSSI *float64
|
||||||
|
SNR *float64
|
||||||
|
Q *float64
|
||||||
|
|
||||||
|
Addresses []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPacket(headerType, propagationType, destinationType, packetType byte, hops byte) *Packet {
|
func NewPacket(destType byte, data []byte, packetType byte, context byte,
|
||||||
p := &Packet{
|
transportType byte, headerType byte, transportID []byte, createReceipt bool,
|
||||||
Header: [2]byte{0, hops},
|
contextFlag byte) *Packet {
|
||||||
Addresses: make([]byte, 0),
|
|
||||||
Data: make([]byte, 0),
|
return &Packet{
|
||||||
|
HeaderType: headerType,
|
||||||
|
PacketType: packetType,
|
||||||
|
TransportType: transportType,
|
||||||
|
Context: context,
|
||||||
|
ContextFlag: contextFlag,
|
||||||
|
Hops: 0,
|
||||||
|
DestinationType: destType,
|
||||||
|
Data: data,
|
||||||
|
TransportID: transportID,
|
||||||
|
CreateReceipt: createReceipt,
|
||||||
|
Packed: false,
|
||||||
|
Sent: false,
|
||||||
|
FromPacked: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Packet) Pack() error {
|
||||||
|
if p.Packed {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set header type
|
log.Printf("[DEBUG-6] Packing packet: type=%d, header=%d", p.PacketType, p.HeaderType)
|
||||||
if headerType == HeaderType2 {
|
|
||||||
p.Header[0] |= HeaderTypeFlag
|
// Create header byte
|
||||||
p.Addresses = make([]byte, 2*AddressSize) // Two address fields
|
flags := byte(p.HeaderType<<6) | byte(p.ContextFlag<<5) |
|
||||||
|
byte(p.TransportType<<4) | byte(p.DestinationType<<2) | byte(p.PacketType)
|
||||||
|
|
||||||
|
header := []byte{flags, p.Hops}
|
||||||
|
log.Printf("[DEBUG-5] Created packet header: flags=%08b, hops=%d", flags, p.Hops)
|
||||||
|
|
||||||
|
if p.HeaderType == HeaderType2 {
|
||||||
|
if p.TransportID == nil {
|
||||||
|
return errors.New("transport ID required for header type 2")
|
||||||
|
}
|
||||||
|
header = append(header, p.TransportID...)
|
||||||
|
log.Printf("[DEBUG-7] Added transport ID to header: %x", p.TransportID)
|
||||||
|
}
|
||||||
|
|
||||||
|
header = append(header, p.DestinationHash...)
|
||||||
|
header = append(header, p.Context)
|
||||||
|
log.Printf("[DEBUG-6] Final header length: %d bytes", len(header))
|
||||||
|
|
||||||
|
p.Raw = append(header, p.Data...)
|
||||||
|
log.Printf("[DEBUG-5] Final packet size: %d bytes", len(p.Raw))
|
||||||
|
|
||||||
|
if len(p.Raw) > MTU {
|
||||||
|
return errors.New("packet size exceeds MTU")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Packed = true
|
||||||
|
p.updateHash()
|
||||||
|
log.Printf("[DEBUG-7] Packet hash: %x", p.PacketHash)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Packet) Unpack() error {
|
||||||
|
if len(p.Raw) < 3 {
|
||||||
|
return errors.New("packet too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := p.Raw[0]
|
||||||
|
p.Hops = p.Raw[1]
|
||||||
|
|
||||||
|
p.HeaderType = (flags & 0b01000000) >> 6
|
||||||
|
p.ContextFlag = (flags & 0b00100000) >> 5
|
||||||
|
p.TransportType = (flags & 0b00010000) >> 4
|
||||||
|
p.DestinationType = (flags & 0b00001100) >> 2
|
||||||
|
p.PacketType = flags & 0b00000011
|
||||||
|
|
||||||
|
dstLen := 16 // Truncated hash length
|
||||||
|
|
||||||
|
if p.HeaderType == HeaderType2 {
|
||||||
|
if len(p.Raw) < 2*dstLen+3 {
|
||||||
|
return errors.New("packet too short for header type 2")
|
||||||
|
}
|
||||||
|
p.TransportID = p.Raw[2 : dstLen+2]
|
||||||
|
p.DestinationHash = p.Raw[dstLen+2 : 2*dstLen+2]
|
||||||
|
p.Context = p.Raw[2*dstLen+2]
|
||||||
|
p.Data = p.Raw[2*dstLen+3:]
|
||||||
} else {
|
} else {
|
||||||
p.Addresses = make([]byte, AddressSize) // One address field
|
if len(p.Raw) < dstLen+3 {
|
||||||
|
return errors.New("packet too short for header type 1")
|
||||||
|
}
|
||||||
|
p.TransportID = nil
|
||||||
|
p.DestinationHash = p.Raw[2 : dstLen+2]
|
||||||
|
p.Context = p.Raw[dstLen+2]
|
||||||
|
p.Data = p.Raw[dstLen+3:]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set propagation type
|
p.Packed = false
|
||||||
p.Header[0] |= (propagationType << 3) & PropagationFlags
|
p.updateHash()
|
||||||
|
|
||||||
// Set destination type
|
|
||||||
p.Header[0] |= (destinationType << 1) & DestinationFlags
|
|
||||||
|
|
||||||
// Set packet type
|
|
||||||
p.Header[0] |= packetType & PacketTypeFlags
|
|
||||||
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Packet) SetAccessCode(code []byte) {
|
|
||||||
p.AccessCode = code
|
|
||||||
p.Header[0] |= IFACFlag
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Packet) SetContext(context byte) {
|
|
||||||
p.Context = context
|
|
||||||
p.Header[0] |= ContextFlag
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Packet) SetData(data []byte) error {
|
|
||||||
if len(data) > MaxDataSize {
|
|
||||||
return errors.New("data exceeds maximum allowed size")
|
|
||||||
}
|
|
||||||
p.Data = data
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Packet) SetAddress(index int, address []byte) error {
|
func (p *Packet) GetHash() []byte {
|
||||||
if len(address) != AddressSize {
|
hashable := p.getHashablePart()
|
||||||
return errors.New("invalid address size")
|
hash := sha256.Sum256(hashable)
|
||||||
|
return hash[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Packet) getHashablePart() []byte {
|
||||||
|
hashable := []byte{p.Raw[0] & 0b00001111}
|
||||||
|
if p.HeaderType == HeaderType2 {
|
||||||
|
hashable = append(hashable, p.Raw[18:]...)
|
||||||
|
} else {
|
||||||
|
hashable = append(hashable, p.Raw[2:]...)
|
||||||
}
|
}
|
||||||
|
return hashable
|
||||||
offset := index * AddressSize
|
}
|
||||||
if offset+AddressSize > len(p.Addresses) {
|
|
||||||
return errors.New("address index out of range")
|
func (p *Packet) updateHash() {
|
||||||
}
|
p.PacketHash = p.GetHash()
|
||||||
|
|
||||||
copy(p.Addresses[offset:], address)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Packet) Serialize() ([]byte, error) {
|
func (p *Packet) Serialize() ([]byte, error) {
|
||||||
totalSize := HeaderSize + len(p.Addresses) + ContextSize + len(p.Data)
|
if !p.Packed {
|
||||||
if p.AccessCode != nil {
|
if err := p.Pack(); err != nil {
|
||||||
totalSize += len(p.AccessCode)
|
return nil, fmt.Errorf("failed to pack packet: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer := make([]byte, totalSize)
|
p.Addresses = p.DestinationHash
|
||||||
offset := 0
|
|
||||||
|
|
||||||
// Write header
|
return p.Raw, nil
|
||||||
copy(buffer[offset:], p.Header[:])
|
|
||||||
offset += HeaderSize
|
|
||||||
|
|
||||||
// Write access code if present
|
|
||||||
if p.AccessCode != nil {
|
|
||||||
copy(buffer[offset:], p.AccessCode)
|
|
||||||
offset += len(p.AccessCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write addresses
|
|
||||||
copy(buffer[offset:], p.Addresses)
|
|
||||||
offset += len(p.Addresses)
|
|
||||||
|
|
||||||
// Write context
|
|
||||||
buffer[offset] = p.Context
|
|
||||||
offset += ContextSize
|
|
||||||
|
|
||||||
// Write data
|
|
||||||
copy(buffer[offset:], p.Data)
|
|
||||||
|
|
||||||
return buffer, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParsePacket(data []byte) (*Packet, error) {
|
func NewAnnouncePacket(destHash []byte, identity *identity.Identity, appData []byte, transportID []byte) (*Packet, error) {
|
||||||
if len(data) < HeaderSize {
|
log.Printf("[DEBUG-7] Creating new announce packet: destHash=%x, appData=%s", destHash, string(appData))
|
||||||
return nil, errors.New("packet data too short")
|
|
||||||
}
|
// Create combined public key
|
||||||
|
pubKey := identity.GetPublicKey()
|
||||||
|
log.Printf("[DEBUG-6] Using public key: %x", pubKey)
|
||||||
|
|
||||||
|
// Create signed data
|
||||||
|
signedData := append(destHash, pubKey...)
|
||||||
|
signedData = append(signedData, appData...)
|
||||||
|
log.Printf("[DEBUG-5] Created signed data (%d bytes)", len(signedData))
|
||||||
|
|
||||||
|
// Sign the data
|
||||||
|
signature := identity.Sign(signedData)
|
||||||
|
log.Printf("[DEBUG-6] Generated signature: %x", signature)
|
||||||
|
|
||||||
|
// Combine all data
|
||||||
|
data := append(pubKey, appData...)
|
||||||
|
data = append(data, signature...)
|
||||||
|
log.Printf("[DEBUG-5] Combined packet data (%d bytes)", len(data))
|
||||||
|
|
||||||
p := &Packet{
|
p := &Packet{
|
||||||
Header: [2]byte{data[0], data[1]},
|
HeaderType: HeaderType2,
|
||||||
|
PacketType: PacketTypeAnnounce,
|
||||||
|
TransportID: transportID,
|
||||||
|
DestinationHash: destHash,
|
||||||
|
Data: data,
|
||||||
}
|
}
|
||||||
|
|
||||||
offset := HeaderSize
|
log.Printf("[DEBUG-4] Created announce packet: type=%d, header=%d", p.PacketType, p.HeaderType)
|
||||||
|
|
||||||
// Handle access code if present
|
|
||||||
if p.Header[0]&IFACFlag != 0 {
|
|
||||||
// Access code handling would go here
|
|
||||||
// For now, we'll assume no access code
|
|
||||||
return nil, errors.New("access code handling not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine address size based on header type
|
|
||||||
addrLen := AddressSize
|
|
||||||
if p.Header[0]&HeaderTypeFlag != 0 {
|
|
||||||
addrLen = 2 * AddressSize
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data[offset:]) < addrLen+ContextSize {
|
|
||||||
return nil, errors.New("packet data too short for addresses and context")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy addresses
|
|
||||||
p.Addresses = make([]byte, addrLen)
|
|
||||||
copy(p.Addresses, data[offset:offset+addrLen])
|
|
||||||
offset += addrLen
|
|
||||||
|
|
||||||
// Copy context
|
|
||||||
p.Context = data[offset]
|
|
||||||
offset++
|
|
||||||
|
|
||||||
// Copy remaining data
|
|
||||||
p.Data = make([]byte, len(data)-offset)
|
|
||||||
copy(p.Data, data[offset:])
|
|
||||||
|
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|||||||
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 (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"path/filepath"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -19,88 +18,90 @@ const (
|
|||||||
STATUS_CANCELLED = 0x04
|
STATUS_CANCELLED = 0x04
|
||||||
|
|
||||||
DEFAULT_SEGMENT_SIZE = 384 // Based on ENCRYPTED_MDU
|
DEFAULT_SEGMENT_SIZE = 384 // Based on ENCRYPTED_MDU
|
||||||
MAX_SEGMENTS = 65535
|
MAX_SEGMENTS = 65535
|
||||||
CLEANUP_INTERVAL = 300 // 5 minutes
|
CLEANUP_INTERVAL = 300 // 5 minutes
|
||||||
|
|
||||||
// Window size constants
|
// Window size constants
|
||||||
WINDOW = 4
|
WINDOW = 4
|
||||||
WINDOW_MIN = 2
|
WINDOW_MIN = 2
|
||||||
WINDOW_MAX_SLOW = 10
|
WINDOW_MAX_SLOW = 10
|
||||||
WINDOW_MAX_VERY_SLOW = 4
|
WINDOW_MAX_VERY_SLOW = 4
|
||||||
WINDOW_MAX_FAST = 75
|
WINDOW_MAX_FAST = 75
|
||||||
WINDOW_MAX = WINDOW_MAX_FAST
|
WINDOW_MAX = WINDOW_MAX_FAST
|
||||||
|
|
||||||
// Rate thresholds
|
// Rate thresholds
|
||||||
FAST_RATE_THRESHOLD = WINDOW_MAX_SLOW - WINDOW - 2
|
FAST_RATE_THRESHOLD = WINDOW_MAX_SLOW - WINDOW - 2
|
||||||
VERY_SLOW_RATE_THRESHOLD = 2
|
VERY_SLOW_RATE_THRESHOLD = 2
|
||||||
|
|
||||||
// Transfer rates (bytes per second)
|
// Transfer rates (bytes per second)
|
||||||
RATE_FAST = (50 * 1000) / 8 // 50 Kbps
|
RATE_FAST = (50 * 1000) / 8 // 50 Kbps
|
||||||
RATE_VERY_SLOW = (2 * 1000) / 8 // 2 Kbps
|
RATE_VERY_SLOW = (2 * 1000) / 8 // 2 Kbps
|
||||||
|
|
||||||
// Window flexibility
|
// Window flexibility
|
||||||
WINDOW_FLEXIBILITY = 4
|
WINDOW_FLEXIBILITY = 4
|
||||||
|
|
||||||
// Hash and segment constants
|
// Hash and segment constants
|
||||||
MAPHASH_LEN = 4
|
MAPHASH_LEN = 4
|
||||||
RANDOM_HASH_SIZE = 4
|
RANDOM_HASH_SIZE = 4
|
||||||
|
|
||||||
// Size limits
|
// Size limits
|
||||||
MAX_EFFICIENT_SIZE = 16*1024*1024 - 1 // ~16MB
|
MAX_EFFICIENT_SIZE = 16*1024*1024 - 1 // ~16MB
|
||||||
AUTO_COMPRESS_MAX_SIZE = MAX_EFFICIENT_SIZE
|
AUTO_COMPRESS_MAX_SIZE = MAX_EFFICIENT_SIZE
|
||||||
|
|
||||||
// Timeouts and retries
|
// Timeouts and retries
|
||||||
PART_TIMEOUT_FACTOR = 4
|
PART_TIMEOUT_FACTOR = 4
|
||||||
PART_TIMEOUT_FACTOR_AFTER_RTT = 2
|
PART_TIMEOUT_FACTOR_AFTER_RTT = 2
|
||||||
PROOF_TIMEOUT_FACTOR = 3
|
PROOF_TIMEOUT_FACTOR = 3
|
||||||
MAX_RETRIES = 16
|
MAX_RETRIES = 16
|
||||||
MAX_ADV_RETRIES = 4
|
MAX_ADV_RETRIES = 4
|
||||||
SENDER_GRACE_TIME = 10.0
|
SENDER_GRACE_TIME = 10.0
|
||||||
PROCESSING_GRACE = 1.0
|
PROCESSING_GRACE = 1.0
|
||||||
RETRY_GRACE_TIME = 0.25
|
RETRY_GRACE_TIME = 0.25
|
||||||
PER_RETRY_DELAY = 0.5
|
PER_RETRY_DELAY = 0.5
|
||||||
)
|
)
|
||||||
|
|
||||||
type Resource struct {
|
type Resource struct {
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
data []byte
|
data []byte
|
||||||
fileHandle io.ReadWriteSeeker
|
fileHandle io.ReadWriteSeeker
|
||||||
|
fileName string
|
||||||
hash []byte
|
hash []byte
|
||||||
randomHash []byte
|
randomHash []byte
|
||||||
originalHash []byte
|
originalHash []byte
|
||||||
status byte
|
status byte
|
||||||
compressed bool
|
compressed bool
|
||||||
autoCompress bool
|
autoCompress bool
|
||||||
encrypted bool
|
encrypted bool
|
||||||
split bool
|
split bool
|
||||||
segments uint16
|
segments uint16
|
||||||
segmentIndex uint16
|
segmentIndex uint16
|
||||||
totalSegments uint16
|
totalSegments uint16
|
||||||
completedParts map[uint16]bool
|
completedParts map[uint16]bool
|
||||||
transferSize int64
|
transferSize int64
|
||||||
dataSize int64
|
dataSize int64
|
||||||
progress float64
|
progress float64
|
||||||
window int
|
window int
|
||||||
windowMax int
|
windowMax int
|
||||||
windowMin int
|
windowMin int
|
||||||
windowFlexibility int
|
windowFlexibility int
|
||||||
rtt float64
|
rtt float64
|
||||||
fastRateRounds int
|
fastRateRounds int
|
||||||
verySlowRateRounds int
|
verySlowRateRounds int
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
completedAt time.Time
|
completedAt time.Time
|
||||||
callback func(*Resource)
|
callback func(*Resource)
|
||||||
progressCallback func(*Resource)
|
progressCallback func(*Resource)
|
||||||
|
readOffset int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(data interface{}, autoCompress bool) (*Resource, error) {
|
func New(data interface{}, autoCompress bool) (*Resource, error) {
|
||||||
r := &Resource{
|
r := &Resource{
|
||||||
status: STATUS_PENDING,
|
status: STATUS_PENDING,
|
||||||
compressed: false,
|
compressed: false,
|
||||||
autoCompress: autoCompress,
|
autoCompress: autoCompress,
|
||||||
completedParts: make(map[uint16]bool),
|
completedParts: make(map[uint16]bool),
|
||||||
createdAt: time.Now(),
|
createdAt: time.Now(),
|
||||||
progress: 0.0,
|
progress: 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
switch v := data.(type) {
|
switch v := data.(type) {
|
||||||
@@ -118,6 +119,10 @@ func New(data interface{}, autoCompress bool) (*Resource, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if namer, ok := v.(interface{ Name() string }); ok {
|
||||||
|
r.fileName = namer.Name()
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("unsupported data type")
|
return nil, errors.New("unsupported data type")
|
||||||
}
|
}
|
||||||
@@ -138,10 +143,10 @@ func New(data interface{}, autoCompress bool) (*Resource, error) {
|
|||||||
r.transferSize = int64(float64(r.dataSize) * compressibility)
|
r.transferSize = int64(float64(r.dataSize) * compressibility)
|
||||||
} else if r.fileHandle != nil {
|
} else if r.fileHandle != nil {
|
||||||
// For file handles, use extension-based estimation
|
// For file handles, use extension-based estimation
|
||||||
ext := strings.ToLower(filepath.Ext(r.fileHandle.Name()))
|
ext := strings.ToLower(filepath.Ext(r.fileName))
|
||||||
r.transferSize = estimateFileCompression(r.dataSize, ext)
|
r.transferSize = estimateFileCompression(r.dataSize, ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure minimum size and add compression overhead
|
// Ensure minimum size and add compression overhead
|
||||||
if r.transferSize < r.dataSize/10 {
|
if r.transferSize < r.dataSize/10 {
|
||||||
r.transferSize = r.dataSize / 10
|
r.transferSize = r.dataSize / 10
|
||||||
@@ -223,7 +228,7 @@ func (r *Resource) IsCompressed() bool {
|
|||||||
func (r *Resource) Cancel() {
|
func (r *Resource) Cancel() {
|
||||||
r.mutex.Lock()
|
r.mutex.Lock()
|
||||||
defer r.mutex.Unlock()
|
defer r.mutex.Unlock()
|
||||||
|
|
||||||
if r.status == STATUS_PENDING || r.status == STATUS_ACTIVE {
|
if r.status == STATUS_PENDING || r.status == STATUS_ACTIVE {
|
||||||
r.status = STATUS_CANCELLED
|
r.status = STATUS_CANCELLED
|
||||||
r.completedAt = time.Now()
|
r.completedAt = time.Now()
|
||||||
@@ -342,13 +347,13 @@ func estimateCompressibility(data []byte) float64 {
|
|||||||
if len(data) < sampleSize {
|
if len(data) < sampleSize {
|
||||||
sampleSize = len(data)
|
sampleSize = len(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count unique bytes in sample
|
// Count unique bytes in sample
|
||||||
uniqueBytes := make(map[byte]struct{})
|
uniqueBytes := make(map[byte]struct{})
|
||||||
for i := 0; i < sampleSize; i++ {
|
for i := 0; i < sampleSize; i++ {
|
||||||
uniqueBytes[data[i]] = struct{}{}
|
uniqueBytes[data[i]] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate entropy-based compression estimate
|
// Calculate entropy-based compression estimate
|
||||||
uniqueRatio := float64(len(uniqueBytes)) / float64(sampleSize)
|
uniqueRatio := float64(len(uniqueBytes)) / float64(sampleSize)
|
||||||
return 0.3 + (0.7 * uniqueRatio) // Base compression ratio between 0.3 and 1.0
|
return 0.3 + (0.7 * uniqueRatio) // Base compression ratio between 0.3 and 1.0
|
||||||
@@ -357,13 +362,13 @@ func estimateCompressibility(data []byte) float64 {
|
|||||||
func estimateFileCompression(size int64, extension string) int64 {
|
func estimateFileCompression(size int64, extension string) int64 {
|
||||||
// Compression ratio estimates based on common file types
|
// Compression ratio estimates based on common file types
|
||||||
compressionRatios := map[string]float64{
|
compressionRatios := map[string]float64{
|
||||||
".txt": 0.4, // Text compresses well
|
".txt": 0.4, // Text compresses well
|
||||||
".log": 0.4,
|
".log": 0.4,
|
||||||
".json": 0.4,
|
".json": 0.4,
|
||||||
".xml": 0.4,
|
".xml": 0.4,
|
||||||
".html": 0.4,
|
".html": 0.4,
|
||||||
".csv": 0.5,
|
".csv": 0.5,
|
||||||
".doc": 0.8, // Already compressed
|
".doc": 0.8, // Already compressed
|
||||||
".docx": 0.95,
|
".docx": 0.95,
|
||||||
".pdf": 0.95,
|
".pdf": 0.95,
|
||||||
".jpg": 0.99, // Already compressed
|
".jpg": 0.99, // Already compressed
|
||||||
@@ -376,11 +381,43 @@ func estimateFileCompression(size int64, extension string) int64 {
|
|||||||
".gz": 0.99,
|
".gz": 0.99,
|
||||||
".rar": 0.99,
|
".rar": 0.99,
|
||||||
}
|
}
|
||||||
|
|
||||||
ratio, exists := compressionRatios[extension]
|
ratio, exists := compressionRatios[extension]
|
||||||
if !exists {
|
if !exists {
|
||||||
ratio = 0.7 // Default compression ratio for unknown types
|
ratio = 0.7 // Default compression ratio for unknown types
|
||||||
}
|
}
|
||||||
|
|
||||||
return int64(float64(size) * ratio)
|
return int64(float64(size) * ratio)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Resource) Read(p []byte) (n int, err error) {
|
||||||
|
r.mutex.Lock()
|
||||||
|
defer r.mutex.Unlock()
|
||||||
|
|
||||||
|
if r.data != nil {
|
||||||
|
if r.readOffset >= int64(len(r.data)) {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
n = copy(p, r.data[r.readOffset:])
|
||||||
|
r.readOffset += int64(n)
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.fileHandle != nil {
|
||||||
|
return r.fileHandle.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, errors.New("no data source available")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) GetName() string {
|
||||||
|
r.mutex.RLock()
|
||||||
|
defer r.mutex.RUnlock()
|
||||||
|
return r.fileName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) GetSize() int64 {
|
||||||
|
r.mutex.RLock()
|
||||||
|
defer r.mutex.RUnlock()
|
||||||
|
return r.dataSize
|
||||||
|
}
|
||||||
|
|||||||
170
pkg/transport/announce.go
Normal file
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