Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fed33aadff | |||
| d0c83ec1a2 | |||
| aa94bee606 | |||
| 745609423f | |||
| 16e1c7e4eb | |||
| aec3672228 | |||
| aace3abd6d | |||
| ca3fefaae8 | |||
| d4f89735f6 | |||
| b37d393286 | |||
| 5e0c829cf6 | |||
| a80f2bb2ac | |||
| 7de206447a | |||
| f740514e2b | |||
| b907dd93f1 | |||
| 011a6303eb | |||
| 12f487d937 | |||
| b9aebc8406 | |||
| ffb3c3d4f4 | |||
| f291ba74e9 | |||
| 6e87fc9bcd | |||
| cb402e2bb6 | |||
| fe5101340a | |||
| dfac66e8bc | |||
| bc05835dae | |||
|
|
26371cdb6a | ||
|
|
41db0500af | ||
|
|
8114c3bda4 | ||
|
|
3f141bf93b | ||
|
|
a9bf658b03 | ||
|
|
ae9a35e3bb | ||
|
|
32d32380d8 | ||
|
|
5e40f0bfe8 | ||
| 315b35fc81 | |||
| 54dec6aa89 | |||
| 92c8faec11 | |||
| 2aff4989e5 | |||
| f1d2a31be6 | |||
| f604d1a3c8 | |||
| 26a54436f7 | |||
| 2fd85a1034 | |||
| c8e81cd9f0 | |||
| 2f61ce9bf3 | |||
| b647e7c6c2 | |||
| 6b3990d399 | |||
| 041b439a66 | |||
| 534982b99d | |||
| 7379d07aba | |||
| 03345bc256 | |||
| e486923e8f | |||
| d7f41b785f | |||
| 15303a21dc | |||
| 4d4863aeeb | |||
| 76a4103a56 | |||
| 96348ce349 | |||
|
|
322711ba20 | ||
|
|
772248b31f | ||
|
|
fa1c80169e | ||
|
|
cb1e4a1115 | ||
|
|
836e97b17d | ||
|
|
87d3b4a58b | ||
|
|
77729e07e1 | ||
|
|
79e1caa815 | ||
|
|
a5b905bbaf | ||
|
|
c870406244 | ||
|
|
ea8daf6bb2 | ||
|
|
d79406e354 | ||
|
|
f9b8d29780 | ||
|
|
0cebfb2193 | ||
|
|
9e229287e8 | ||
|
|
9508e6e195 | ||
|
|
5acbef454f | ||
|
|
0862830431 | ||
|
|
6cdc02346f | ||
|
|
3ffd5b72a1 | ||
|
|
73af84e24f | ||
|
|
ae40d2879c | ||
|
|
a2499e4a15 | ||
|
|
30ea1dd0c7 | ||
|
|
785bc7d782 | ||
|
|
144f5bea6a | ||
|
|
a3c701e205 | ||
|
|
a8a7607eb6 | ||
|
|
a2947a3adb | ||
|
|
2cb37102fb | ||
|
|
54c401e2a5 | ||
|
|
8df4039b18 | ||
|
|
12156adae9 | ||
|
|
a34e3d274e | ||
|
|
f3d22dfcd4 | ||
|
|
99d8e44182 | ||
|
|
083991c997 | ||
|
|
9ca24d96ab | ||
|
|
b478ca346e | ||
|
|
20b532e005 | ||
|
|
80eac50632 | ||
|
|
f15d8f6a84 | ||
|
|
c523d6f542 | ||
|
|
8a175e3051 | ||
|
|
28d46921d3 | ||
|
|
613ceddb0b | ||
|
|
599dd91979 | ||
|
|
e724886578 | ||
|
|
3034c0b0b4 | ||
|
|
3ed2c67742 | ||
|
|
f2c146b7c5 | ||
|
|
59cef5e56a | ||
|
|
ef613cc873 | ||
|
|
7a7ce84778 | ||
|
|
7ef7e60a87 | ||
|
|
73349d4a28 | ||
|
|
31128a6758 | ||
|
|
566ce5da96 | ||
|
|
139926be05 | ||
|
|
decbd8f29a | ||
|
|
0f5f5cbb13 | ||
|
|
a2476c9551 | ||
|
|
bfc75a2290 | ||
|
|
2e01fa565d |
7
.deepsource.toml
Normal file
7
.deepsource.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
version = 1
|
||||
|
||||
[[analyzers]]
|
||||
name = "go"
|
||||
|
||||
[analyzers.meta]
|
||||
import_root = "github.com/Sudo-Ivan/Reticulum-Go"
|
||||
17
.github/workflows/bearer.yml
vendored
Normal file
17
.github/workflows/bearer.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Bearer
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
rule_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Bearer
|
||||
uses: bearer/bearer-action@v2
|
||||
87
.github/workflows/build.yml
vendored
Normal file
87
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
name: Go Build Multi-Platform
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux, windows, darwin, freebsd]
|
||||
goarch: [amd64, arm64, arm]
|
||||
exclude:
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
build_complete: ${{ steps.build_step.outcome == 'success' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
|
||||
- name: Build
|
||||
id: build_step
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOARM: ${{ matrix.goarch == 'arm' && '6' || '' }}
|
||||
run: |
|
||||
output_name="reticulum-go-${GOOS}-${GOARCH}"
|
||||
if [ "$GOOS" = "windows" ]; then
|
||||
output_name+=".exe"
|
||||
fi
|
||||
go build -v -ldflags="-s -w" -o "${output_name}" ./cmd/reticulum-go
|
||||
echo "Built: ${output_name}"
|
||||
|
||||
- name: Calculate SHA256 Checksum
|
||||
run: |
|
||||
output_name="reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}"
|
||||
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||||
output_name+=".exe"
|
||||
fi
|
||||
sha256sum "${output_name}" > "${output_name}.sha256"
|
||||
echo "Calculated SHA256 for ${output_name}"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}*
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download All Build Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ./release-assets
|
||||
|
||||
- name: List downloaded files (for debugging)
|
||||
run: ls -R ./release-assets
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ./release-assets/*/*
|
||||
27
.github/workflows/go-test.yml
vendored
Normal file
27
.github/workflows/go-test.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Go Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go 1.24
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.24'
|
||||
|
||||
- name: Run Go tests
|
||||
run: go test ./...
|
||||
22
.github/workflows/gosec.yml
vendored
Normal file
22
.github/workflows/gosec.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Run Gosec
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GO111MODULE: on
|
||||
steps:
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v3
|
||||
- name: Run Gosec Security Scanner
|
||||
uses: securego/gosec@master
|
||||
with:
|
||||
args: ./...
|
||||
29
.github/workflows/revive.yml
vendored
Normal file
29
.github/workflows/revive.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Go Revive Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
|
||||
- name: Install revive
|
||||
run: go install github.com/mgechev/revive@latest
|
||||
|
||||
- name: Run revive
|
||||
run: |
|
||||
revive -config revive.toml -formatter stylish ./...
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,8 +1,7 @@
|
||||
reticulum-client
|
||||
reticulum-server
|
||||
|
||||
bin/
|
||||
logs/
|
||||
*.log
|
||||
|
||||
.env
|
||||
.json
|
||||
|
||||
bin/
|
||||
|
||||
21
CONTRIBUTING.md
Normal file
21
CONTRIBUTING.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Contributing
|
||||
|
||||
Be good to each other.
|
||||
|
||||
## Development
|
||||
|
||||
By contributing to this project you agree to the following:
|
||||
|
||||
- All code must be tested using `gosec`.
|
||||
- All code must be formatted with `gofmt`.
|
||||
- All code must be documented.
|
||||
|
||||
## Communication
|
||||
|
||||
Feel free to join our seperate matrix channel for this implementation.
|
||||
|
||||
- [Matrix](https://matrix.to/#/#reticulum-go-dev:matrix.org)
|
||||
|
||||
## Usage of LLMs and other Generative AI tools
|
||||
|
||||
We would prefer if you did not use LLMs and other generative AI tools to write critical parts of the code.
|
||||
9
LICENSE
Normal file
9
LICENSE
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
Copyright 2024-2025 Sudo-Ivan / Quad4.io
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
106
Makefile
Normal file
106
Makefile
Normal file
@@ -0,0 +1,106 @@
|
||||
GOCMD=go
|
||||
GOBUILD=$(GOCMD) build
|
||||
GOCLEAN=$(GOCMD) clean
|
||||
GOTEST=$(GOCMD) test
|
||||
GOGET=$(GOCMD) get
|
||||
GOMOD=$(GOCMD) mod
|
||||
BINARY_NAME=reticulum-go
|
||||
BINARY_UNIX=$(BINARY_NAME)_unix
|
||||
|
||||
BUILD_DIR=bin
|
||||
|
||||
MAIN_PACKAGE=./cmd/reticulum-go
|
||||
|
||||
ALL_PACKAGES=$$(go list ./... | grep -v /vendor/)
|
||||
|
||||
.PHONY: all build clean test coverage deps help
|
||||
|
||||
all: clean deps build test
|
||||
|
||||
build:
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_PACKAGE)
|
||||
|
||||
clean:
|
||||
@rm -rf $(BUILD_DIR)
|
||||
$(GOCLEAN)
|
||||
|
||||
test:
|
||||
$(GOTEST) -v $(ALL_PACKAGES)
|
||||
|
||||
coverage:
|
||||
$(GOTEST) -coverprofile=coverage.out $(ALL_PACKAGES)
|
||||
$(GOCMD) tool cover -html=coverage.out
|
||||
|
||||
deps:
|
||||
$(GOMOD) download
|
||||
$(GOMOD) verify
|
||||
|
||||
build-linux:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm $(MAIN_PACKAGE)
|
||||
|
||||
build-windows:
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-arm64.exe $(MAIN_PACKAGE)
|
||||
|
||||
build-darwin:
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 $(MAIN_PACKAGE)
|
||||
|
||||
build-freebsd:
|
||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-amd64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=386 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-386 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-arm64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-arm $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=riscv64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-riscv64 $(MAIN_PACKAGE)
|
||||
|
||||
build-openbsd:
|
||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-amd64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=386 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-386 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-arm64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-arm $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=ppc64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-ppc64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=riscv64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-riscv64 $(MAIN_PACKAGE)
|
||||
|
||||
build-netbsd:
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-amd64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=386 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-386 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm $(MAIN_PACKAGE)
|
||||
|
||||
build-arm:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-arm $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-arm64 $(MAIN_PACKAGE)
|
||||
|
||||
build-riscv:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-riscv64 $(MAIN_PACKAGE)
|
||||
|
||||
build-all: build-linux build-windows build-darwin build-freebsd build-openbsd build-netbsd build-arm build-riscv
|
||||
|
||||
run:
|
||||
@./$(BUILD_DIR)/$(BINARY_NAME)
|
||||
|
||||
install:
|
||||
$(GOMOD) download
|
||||
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " all - Clean, download dependencies, build and test"
|
||||
@echo " build - Build binary"
|
||||
@echo " clean - Remove build artifacts"
|
||||
@echo " test - Run tests"
|
||||
@echo " coverage - Generate test coverage report"
|
||||
@echo " deps - Download dependencies"
|
||||
@echo " build-linux - Build for Linux (amd64, arm64, arm)"
|
||||
@echo " build-windows- Build for Windows (amd64, arm64)"
|
||||
@echo " build-darwin - Build for MacOS (amd64, arm64)"
|
||||
@echo " build-freebsd- Build for FreeBSD (amd64, 386, arm64, arm, riscv64)"
|
||||
@echo " build-openbsd- Build for OpenBSD (amd64, 386, arm64, arm, ppc64, riscv64)"
|
||||
@echo " build-netbsd - Build for NetBSD (amd64, 386, arm64, arm)"
|
||||
@echo " build-arm - Build for ARM architectures (arm, arm64)"
|
||||
@echo " build-riscv - Build for RISC-V architecture (riscv64)"
|
||||
@echo " build-all - Build for all platforms and architectures"
|
||||
@echo " run - Run reticulum binary"
|
||||
@echo " install - Install dependencies"
|
||||
35
README.md
35
README.md
@@ -1,4 +1,37 @@
|
||||
# Reticulum-Go
|
||||
|
||||
Reticulum Network Stack in Go.
|
||||
> [!WARNING]
|
||||
> This project is still work in progress. Currently not compatible with the Python version.
|
||||
|
||||
[](https://socket.dev/go/package/github.com/sudo-ivan/reticulum-go)
|
||||

|
||||

|
||||
[](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/bearer.yml)
|
||||
[](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/build.yml)
|
||||
[](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/revive.yml)
|
||||
|
||||
[Reticulum Network](https://github.com/markqvist/Reticulum) implementation in Go `1.24+`.
|
||||
|
||||
Aiming to be fully compatible with the Python version.
|
||||
|
||||
## Usage
|
||||
|
||||
Requires Go 1.24+
|
||||
|
||||
```
|
||||
make install
|
||||
make build
|
||||
make run
|
||||
```
|
||||
|
||||
## Linter
|
||||
|
||||
[Revive](https://github.com/mgechev/revive)
|
||||
|
||||
```bash
|
||||
revive -config revive.toml -formatter friendly ./pkg/* ./cmd/* ./internal/*
|
||||
```
|
||||
|
||||
## External Packages
|
||||
|
||||
- `golang.org/x/crypto` `v0.39.0` - Cryptographic primitives
|
||||
|
||||
25
SECURITY.md
Normal file
25
SECURITY.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Security Policy
|
||||
|
||||
We use [Socket](https://socket.dev/), [Deepsource](https://deepsource.com/) and [gosec](https://github.com/securego/gosec) for this project.
|
||||
|
||||
## Strict Verfication of Contributors and Code Quality
|
||||
|
||||
We are strict about the quality of the code and the contributors. Please read the [CONTRIBUTING.md](CONTRIBUTING.md) file for more information.
|
||||
|
||||
## Cryptography Dependencies
|
||||
|
||||
- golang.org/x/crypto for core cryptographic primitives
|
||||
- hkdf
|
||||
- curve25519
|
||||
|
||||
- go/crypto
|
||||
- ed25519
|
||||
- sha256
|
||||
- rand
|
||||
- aes
|
||||
- cipher
|
||||
- hmac
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report any security vulnerabilities using Github reporting tool or email to [rns@quad4.io](mailto:rns@quad4.io)
|
||||
167
TODO.md
Normal file
167
TODO.md
Normal file
@@ -0,0 +1,167 @@
|
||||
### Core Components (In Progress)
|
||||
|
||||
Last Updated: 2025-07-06
|
||||
|
||||
- [x] Basic Configuration System
|
||||
- [x] Basic config structure
|
||||
- [x] Default settings
|
||||
- [x] Config file loading/saving
|
||||
- [x] Path management
|
||||
|
||||
- [x] Constants Definition (Testing required)
|
||||
- [x] Packet constants
|
||||
- [x] MTU constants
|
||||
- [x] Header types
|
||||
- [x] Additional protocol constants
|
||||
|
||||
- [x] Identity Management (Testing required)
|
||||
- [x] Identity creation
|
||||
- [x] Key pair generation
|
||||
- [x] Identity storage/recall
|
||||
- [x] Public key handling
|
||||
- [x] Signature verification
|
||||
- [x] Hash functions
|
||||
|
||||
- [x] Cryptographic Primitives (Testing required)
|
||||
- [x] Ed25519
|
||||
- [x] Curve25519
|
||||
- [x] AES-128-CBC
|
||||
- [x] AES-256-CBC
|
||||
- [x] SHA-256
|
||||
- [x] HKDF
|
||||
- [x] Secure random number generation
|
||||
- [x] HMAC
|
||||
|
||||
- [x] Packet Handling (In Progress)
|
||||
- [x] Packet creation
|
||||
- [x] Packet validation
|
||||
- [x] Basic proof system
|
||||
- [x] Packet encryption/decryption
|
||||
- [x] Signature verification
|
||||
- [x] Announce packet structure
|
||||
- [ ] Testing of packet encrypt/decrypt/sign/proof
|
||||
- [ ] Cross-client packet compatibility
|
||||
|
||||
- [x] Transport Layer (In Progress)
|
||||
- [x] Path management
|
||||
- [x] Basic packet routing
|
||||
- [x] Announce handling
|
||||
- [x] Link management
|
||||
- [x] Resource cleanup
|
||||
- [x] Network layer integration
|
||||
- [x] Basic announce implementation
|
||||
- [ ] Testing announce from go client to python client
|
||||
- [ ] Testing path finding and caching
|
||||
- [ ] Announce propagation optimization
|
||||
|
||||
- [x] Channel System (Testing Required)
|
||||
- [x] Channel creation and management
|
||||
- [x] Message handling
|
||||
- [x] Channel encryption
|
||||
- [x] Channel authentication
|
||||
- [x] Channel callbacks
|
||||
- [x] Integration with Buffer system
|
||||
- [ ] Testing with real network conditions
|
||||
- [ ] Cross-client compatibility testing
|
||||
|
||||
- [x] Buffer System (Testing Required)
|
||||
- [x] Raw channel reader/writer
|
||||
- [x] Buffered stream implementation
|
||||
- [x] Compression support
|
||||
- [ ] Testing with Channel system
|
||||
- [ ] Cross-client compatibility testing
|
||||
|
||||
- [x] Resolver System (Testing Required)
|
||||
- [x] Name resolution
|
||||
- [x] Cache management
|
||||
- [x] Announce handling
|
||||
- [x] Path resolution
|
||||
- [x] Integration with Transport layer
|
||||
- [ ] Testing with live network
|
||||
- [ ] Cross-client compatibility testing
|
||||
|
||||
### Interface Implementation (In Progress)
|
||||
- [x] UDP Interface
|
||||
- [x] TCP Interface
|
||||
- [x] Auto Interface
|
||||
- [ ] Local Interface (In Progress)
|
||||
- [ ] I2P Interface
|
||||
- [ ] Pipe Interface
|
||||
- [ ] RNode Interface
|
||||
- [ ] RNode Multiinterface
|
||||
- [ ] Serial Interface
|
||||
- [ ] AX25KISS Interface
|
||||
- [ ] Interface Discovery
|
||||
- [ ] Interface Modes
|
||||
- [ ] Full mode
|
||||
- [ ] Gateway mode
|
||||
- [ ] Access point mode
|
||||
- [ ] Roaming mode
|
||||
- [ ] Boundary mode
|
||||
|
||||
- [ ] Hot reloading interfaces
|
||||
|
||||
### Destination System (Testing required)
|
||||
- [x] Destination creation
|
||||
- [x] Destination types (IN/OUT)
|
||||
- [x] Destination aspects
|
||||
- [ ] Announce implementation (Fixing)
|
||||
- [x] Ratchet support
|
||||
- [x] Request handlers
|
||||
|
||||
### Link System (Testing required)
|
||||
- [x] Link establishment
|
||||
- [x] Link teardown
|
||||
- [x] Basic packet transfer
|
||||
- [x] Encryption/Decryption
|
||||
- [x] Identity verification
|
||||
- [x] Request/Response handling
|
||||
- [x] Session key management
|
||||
- [x] Link state tracking
|
||||
|
||||
### Resource System (Testing required)
|
||||
- [x] Resource creation
|
||||
- [x] Resource transfer
|
||||
- [x] Compression
|
||||
- [x] Progress tracking
|
||||
- [x] Segmentation
|
||||
- [x] Cleanup routines
|
||||
|
||||
### Compatibility
|
||||
- [ ] RNS Utilities.
|
||||
- [ ] Reticulum config.
|
||||
|
||||
|
||||
### Testing & Validation (Priority)
|
||||
- [ ] Unit tests for all components
|
||||
- [ ] Identity tests
|
||||
- [ ] Packet tests
|
||||
- [ ] Transport tests
|
||||
- [ ] Interface tests
|
||||
- [ ] Announce tests
|
||||
- [ ] Channel tests
|
||||
- [ ] Buffer tests
|
||||
- [ ] Resolver tests
|
||||
- [ ] Link tests
|
||||
- [ ] Resource tests
|
||||
- [ ] Integration tests
|
||||
- [ ] Go client to Go client
|
||||
- [ ] Go client to Python client
|
||||
- [ ] Interface compatibility
|
||||
- [ ] Path finding and resolution
|
||||
- [ ] Channel system end-to-end
|
||||
- [ ] Buffer system performance
|
||||
- [ ] Cross-client compatibility tests
|
||||
- [ ] Performance benchmarks
|
||||
- [ ] Security auditing (When Reticulum is 1.0 / stable)
|
||||
|
||||
### Documentation
|
||||
- [ ] API documentation
|
||||
- [ ] Usage examples
|
||||
|
||||
### Cleanup
|
||||
- [ ] Separate Cryptography from identity.go to their own files
|
||||
- [ ] Move constants to their own files
|
||||
- [ ] Remove default community interfaces in default config creation after testing
|
||||
- [ ] Optimize announce packet creation and caching
|
||||
- [ ] Improve debug logging system
|
||||
102
To-Do
102
To-Do
@@ -1,102 +0,0 @@
|
||||
To-Do List
|
||||
|
||||
Core Components
|
||||
[âś“] Basic Configuration System
|
||||
[âś“] Basic config structure
|
||||
[âś“] Default settings
|
||||
[âś“] Config file loading/saving
|
||||
[âś“] Path management
|
||||
|
||||
[âś“] Constants Definition
|
||||
[âś“] Packet constants
|
||||
[âś“] MTU constants
|
||||
[âś“] Header types
|
||||
[âś“] Additional protocol constants
|
||||
|
||||
[âś“] Identity Management
|
||||
[âś“] Identity creation
|
||||
[âś“] Key pair generation
|
||||
[âś“] Identity storage/recall
|
||||
|
||||
[âś“] Packet Handling
|
||||
[âś“] Packet creation
|
||||
[âś“] Packet validation
|
||||
[âś“] Basic proof system
|
||||
|
||||
[âś“] Crypto Implementation
|
||||
[âś“] Basic encryption
|
||||
[âś“] Key exchange
|
||||
[âś“] Hash functions
|
||||
[âś“] Ratchet implementation
|
||||
|
||||
[âś“] Transport Layer
|
||||
[âś“] Path management
|
||||
[âś“] Basic packet routing
|
||||
[âś“] Announce handling
|
||||
[âś“] Link management
|
||||
[âś“] Resource cleanup
|
||||
[âś“] Network layer integration
|
||||
|
||||
[âś“] Destination System
|
||||
[âś“] Destination creation
|
||||
[âś“] Destination types (IN/OUT)
|
||||
[âś“] Destination aspects
|
||||
[âś“] Announce implementation
|
||||
[âś“] Ratchet support
|
||||
[âś“] Request handlers
|
||||
|
||||
[âś“] Link System
|
||||
[âś“] Link establishment
|
||||
[âś“] Link teardown
|
||||
[âś“] Basic packet transfer
|
||||
[âś“] Encryption/Decryption
|
||||
[âś“] Identity verification
|
||||
[âś“] Request/Response handling
|
||||
|
||||
[âś“] Resource System
|
||||
[âś“] Resource creation
|
||||
[âś“] Resource transfer
|
||||
[âś“] Compression
|
||||
[âś“] Progress tracking
|
||||
[âś“] Segmentation
|
||||
[âś“] Cleanup routines
|
||||
|
||||
Basic Features
|
||||
[âś“] Network Interface
|
||||
[âś“] Basic UDP transport
|
||||
[âś“] TCP transport
|
||||
[ ] Interface discovery
|
||||
[ ] Connection management
|
||||
[âś“] Packet framing
|
||||
[âś“] Transport integration
|
||||
|
||||
[âś“] Announce System
|
||||
[âś“] Announce creation
|
||||
[âś“] Announce propagation
|
||||
[âś“] Path requests
|
||||
|
||||
[âś“] Resource Management
|
||||
[âś“] Resource tracking
|
||||
[âś“] Memory management
|
||||
[âś“] Cleanup routines
|
||||
|
||||
[âś“] Client Implementation
|
||||
[âś“] Basic client structure
|
||||
[âś“] Configuration handling
|
||||
[âś“] Interactive mode
|
||||
[âś“] Link establishment
|
||||
[âś“] Message sending/receiving
|
||||
|
||||
Next Immediate Tasks:
|
||||
1. [âś“] Fix import cycles by creating common package
|
||||
2. [ ] Implement Interface discovery
|
||||
3. [ ] Implement Connection management
|
||||
4. [ ] Test network layer integration end-to-end
|
||||
5. [ ] Add error handling for network failures
|
||||
6. [ ] Implement interface auto-configuration
|
||||
7. [ ] Complete NetworkInterface implementation
|
||||
8. [ ] Add comprehensive interface tests
|
||||
9. [ ] Implement connection retry logic
|
||||
10. [ ] Add metrics collection for interfaces
|
||||
11. [ ] Add client reconnection handling
|
||||
12. [ ] Implement client-side path caching
|
||||
@@ -1,137 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/internal/config"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/destination"
|
||||
)
|
||||
|
||||
var (
|
||||
configPath = flag.String("config", "", "Path to config file")
|
||||
targetHash = flag.String("target", "", "Target destination hash")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var cfg *common.ReticulumConfig
|
||||
var err error
|
||||
|
||||
if *configPath == "" {
|
||||
cfg, err = config.InitConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize config: %v", err)
|
||||
}
|
||||
} else {
|
||||
cfg, err = config.LoadConfig(*configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Enable transport by default for client
|
||||
cfg.EnableTransport = true
|
||||
|
||||
// Initialize transport
|
||||
transport, err := transport.NewTransport(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize transport: %v", err)
|
||||
}
|
||||
defer transport.Close()
|
||||
|
||||
// If target specified, establish connection
|
||||
if *targetHash != "" {
|
||||
destHash, err := identity.HashFromHex(*targetHash)
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid destination hash: %v", err)
|
||||
}
|
||||
|
||||
// Request path if needed
|
||||
if !transport.HasPath(destHash) {
|
||||
fmt.Println("Requesting path to destination...")
|
||||
if err := transport.RequestPath(destHash, "", nil, true); err != nil {
|
||||
log.Fatalf("Failed to request path: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get destination identity
|
||||
destIdentity, err := identity.Recall(destHash)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to recall identity: %v", err)
|
||||
}
|
||||
|
||||
// Create destination
|
||||
dest, err := destination.New(
|
||||
destIdentity,
|
||||
destination.OUT,
|
||||
destination.SINGLE,
|
||||
"client",
|
||||
"direct",
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create destination: %v", err)
|
||||
}
|
||||
|
||||
// Enable and configure ratchets
|
||||
dest.SetRetainedRatchets(destination.RATCHET_COUNT)
|
||||
dest.SetRatchetInterval(destination.RATCHET_INTERVAL)
|
||||
dest.EnforceRatchets()
|
||||
|
||||
// Create link
|
||||
link := transport.NewLink(dest.Hash(), func() {
|
||||
fmt.Println("Link established")
|
||||
}, func() {
|
||||
fmt.Println("Link closed")
|
||||
})
|
||||
|
||||
defer link.Teardown()
|
||||
|
||||
// Set packet callback
|
||||
link.SetPacketCallback(func(data []byte) {
|
||||
fmt.Printf("Received: %s\n", string(data))
|
||||
})
|
||||
|
||||
// Start interactive loop
|
||||
go interactiveLoop(link)
|
||||
} else {
|
||||
fmt.Println("No target specified. Use -target <hash> to connect to a destination")
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for interrupt
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigChan
|
||||
}
|
||||
|
||||
func interactiveLoop(link *transport.Link) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
fmt.Print("> ")
|
||||
input, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading input: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "quit" || input == "exit" {
|
||||
return
|
||||
}
|
||||
|
||||
if err := link.Send([]byte(input)); err != nil {
|
||||
fmt.Printf("Failed to send: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
642
cmd/reticulum-go/main.go
Normal file
642
cmd/reticulum-go/main.go
Normal file
@@ -0,0 +1,642 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/internal/config"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/buffer"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/channel"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/destination"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/interfaces"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/packet"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
||||
)
|
||||
|
||||
var (
|
||||
debugLevel = flag.Int("debug", 7, "Debug level (0-7)")
|
||||
interceptPackets = flag.Bool("intercept-packets", false, "Enable packet interception")
|
||||
interceptOutput = flag.String("intercept-output", "packets.log", "Output file for intercepted packets")
|
||||
)
|
||||
|
||||
func debugLog(level int, format string, v ...interface{}) {
|
||||
if *debugLevel >= level {
|
||||
log.Printf("[DEBUG-%d] %s", level, fmt.Sprintf(format, v...))
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
ANNOUNCE_RATE_TARGET = 3600 // Default target time between announces (1 hour)
|
||||
ANNOUNCE_RATE_GRACE = 3 // Number of grace announces before enforcing rate
|
||||
ANNOUNCE_RATE_PENALTY = 7200 // Additional penalty time for rate violations
|
||||
MAX_ANNOUNCE_HOPS = 128 // Maximum number of hops for announces
|
||||
DEBUG_CRITICAL = 1 // Critical errors
|
||||
DEBUG_ERROR = 2 // Non-critical errors
|
||||
DEBUG_INFO = 3 // Important information
|
||||
DEBUG_VERBOSE = 4 // Detailed information
|
||||
DEBUG_TRACE = 5 // Very detailed tracing
|
||||
DEBUG_PACKETS = 6 // Packet-level details
|
||||
DEBUG_ALL = 7 // Everything including identity operations
|
||||
APP_NAME = "Go-Client"
|
||||
APP_ASPECT = "node" // Always use "node" for node announces
|
||||
)
|
||||
|
||||
type Reticulum struct {
|
||||
config *common.ReticulumConfig
|
||||
transport *transport.Transport
|
||||
interfaces []interfaces.Interface
|
||||
channels map[string]*channel.Channel
|
||||
buffers map[string]*buffer.Buffer
|
||||
pathRequests map[string]*common.PathRequest
|
||||
announceHistory map[string]announceRecord
|
||||
announceHistoryMu sync.RWMutex
|
||||
identity *identity.Identity
|
||||
destination *destination.Destination
|
||||
|
||||
// Node-specific information
|
||||
maxTransferSize int16 // Max transfer size in KB
|
||||
nodeEnabled bool // Whether this node is enabled
|
||||
nodeTimestamp int64 // Last node announcement timestamp
|
||||
}
|
||||
|
||||
type announceRecord struct {
|
||||
timestamp int64
|
||||
appData []byte
|
||||
}
|
||||
|
||||
func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
|
||||
if cfg == nil {
|
||||
cfg = config.DefaultConfig()
|
||||
}
|
||||
|
||||
// Set default app name and aspect if not provided
|
||||
if cfg.AppName == "" {
|
||||
cfg.AppName = APP_NAME
|
||||
}
|
||||
if cfg.AppAspect == "" {
|
||||
cfg.AppAspect = APP_ASPECT // Always use "node" for node announcements
|
||||
}
|
||||
|
||||
if err := initializeDirectories(); err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize directories: %v", err)
|
||||
}
|
||||
debugLog(3, "Directories initialized")
|
||||
|
||||
t := transport.NewTransport(cfg)
|
||||
debugLog(3, "Transport initialized")
|
||||
|
||||
identity, err := identity.NewIdentity()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create identity: %v", err)
|
||||
}
|
||||
debugLog(2, "Created new identity: %x", identity.Hash())
|
||||
|
||||
// Create destination
|
||||
debugLog(DEBUG_INFO, "Creating destination...")
|
||||
dest, err := destination.New(
|
||||
identity,
|
||||
destination.IN,
|
||||
destination.SINGLE,
|
||||
"reticulum",
|
||||
t,
|
||||
"node",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create destination: %v", err)
|
||||
}
|
||||
debugLog(DEBUG_INFO, "Created destination with hash: %x", dest.GetHash())
|
||||
|
||||
// Set node metadata
|
||||
nodeTimestamp := time.Now().Unix()
|
||||
|
||||
r := &Reticulum{
|
||||
config: cfg,
|
||||
transport: t,
|
||||
interfaces: make([]interfaces.Interface, 0),
|
||||
channels: make(map[string]*channel.Channel),
|
||||
buffers: make(map[string]*buffer.Buffer),
|
||||
pathRequests: make(map[string]*common.PathRequest),
|
||||
announceHistory: make(map[string]announceRecord),
|
||||
identity: identity,
|
||||
destination: dest,
|
||||
|
||||
// Node-specific information
|
||||
maxTransferSize: 500, // Default 500KB
|
||||
nodeEnabled: true, // Enabled by default
|
||||
nodeTimestamp: nodeTimestamp,
|
||||
}
|
||||
|
||||
// Enable destination features
|
||||
dest.AcceptsLinks(true)
|
||||
// Enable ratchets and point to a file for persistence.
|
||||
// The actual path should probably be configurable.
|
||||
ratchetPath := ".reticulum-go/storage/ratchets/" + r.identity.GetHexHash()
|
||||
dest.EnableRatchets(ratchetPath)
|
||||
dest.SetProofStrategy(destination.PROVE_APP)
|
||||
debugLog(DEBUG_VERBOSE, "Configured destination features")
|
||||
|
||||
// Initialize interfaces from config
|
||||
for name, ifaceConfig := range cfg.Interfaces {
|
||||
if !ifaceConfig.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
var iface interfaces.Interface
|
||||
var err error
|
||||
|
||||
switch ifaceConfig.Type {
|
||||
case "TCPClientInterface":
|
||||
iface, err = interfaces.NewTCPClientInterface(
|
||||
name,
|
||||
ifaceConfig.TargetHost,
|
||||
ifaceConfig.TargetPort,
|
||||
ifaceConfig.Enabled,
|
||||
true, // IN
|
||||
true, // OUT
|
||||
)
|
||||
case "UDPInterface":
|
||||
iface, err = interfaces.NewUDPInterface(
|
||||
name,
|
||||
ifaceConfig.Address,
|
||||
ifaceConfig.TargetHost,
|
||||
ifaceConfig.Enabled,
|
||||
)
|
||||
case "AutoInterface":
|
||||
iface, err = interfaces.NewAutoInterface(name, ifaceConfig)
|
||||
default:
|
||||
debugLog(1, "Unknown interface type: %s", ifaceConfig.Type)
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if cfg.PanicOnInterfaceErr {
|
||||
return nil, fmt.Errorf("failed to create interface %s: %v", name, err)
|
||||
}
|
||||
debugLog(1, "Error creating interface %s: %v", name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Set packet callback
|
||||
iface.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
|
||||
if r.transport != nil {
|
||||
r.transport.HandlePacket(data, ni)
|
||||
}
|
||||
})
|
||||
|
||||
debugLog(2, "Configuring interface %s (type=%s)...", name, ifaceConfig.Type)
|
||||
r.interfaces = append(r.interfaces, iface)
|
||||
debugLog(3, "Interface %s started successfully", name)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *Reticulum) handleInterface(iface common.NetworkInterface) {
|
||||
debugLog(DEBUG_INFO, "Setting up interface %s (type=%T)", iface.GetName(), iface)
|
||||
|
||||
ch := channel.NewChannel(&transportWrapper{r.transport})
|
||||
r.channels[iface.GetName()] = ch
|
||||
|
||||
rw := buffer.CreateBidirectionalBuffer(
|
||||
1,
|
||||
2,
|
||||
ch,
|
||||
func(size int) {
|
||||
data := make([]byte, size)
|
||||
debugLog(DEBUG_PACKETS, "Interface %s: Reading %d bytes from buffer", iface.GetName(), size)
|
||||
iface.ProcessIncoming(data)
|
||||
|
||||
if len(data) > 0 {
|
||||
debugLog(DEBUG_TRACE, "Interface %s: Received packet type 0x%02x", iface.GetName(), data[0])
|
||||
r.transport.HandlePacket(data, iface)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
r.buffers[iface.GetName()] = &buffer.Buffer{
|
||||
ReadWriter: rw,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reticulum) monitorInterfaces() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
for _, iface := range r.interfaces {
|
||||
if tcpClient, ok := iface.(*interfaces.TCPClientInterface); ok {
|
||||
stats := fmt.Sprintf("Interface %s status - Connected: %v, TX: %d bytes (%.2f Kbps), RX: %d bytes (%.2f Kbps)",
|
||||
iface.GetName(),
|
||||
tcpClient.IsConnected(),
|
||||
tcpClient.GetTxBytes(),
|
||||
float64(tcpClient.GetTxBytes()*8)/(5*1024),
|
||||
tcpClient.GetRxBytes(),
|
||||
float64(tcpClient.GetRxBytes()*8)/(5*1024),
|
||||
)
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
stats = fmt.Sprintf("%s, RTT: %v", stats, tcpClient.GetRTT())
|
||||
}
|
||||
|
||||
debugLog(DEBUG_VERBOSE, "%s", stats)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
debugLog(1, "Initializing Reticulum (Debug Level: %d)...", *debugLevel)
|
||||
|
||||
cfg, err := config.InitConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize config: %v", err)
|
||||
}
|
||||
debugLog(2, "Configuration loaded from: %s", cfg.ConfigPath)
|
||||
|
||||
// Add default TCP interfaces if none configured
|
||||
if len(cfg.Interfaces) == 0 {
|
||||
debugLog(2, "No interfaces configured, adding default TCP interfaces")
|
||||
cfg.Interfaces = make(map[string]*common.InterfaceConfig)
|
||||
|
||||
cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
|
||||
Type: "TCPClientInterface",
|
||||
Enabled: true,
|
||||
TargetHost: "127.0.0.1",
|
||||
TargetPort: 4242,
|
||||
Name: "Go-RNS-Testnet",
|
||||
}
|
||||
|
||||
cfg.Interfaces["Quad4 TCP"] = &common.InterfaceConfig{
|
||||
Type: "TCPClientInterface",
|
||||
Enabled: true,
|
||||
TargetHost: "rns.quad4.io",
|
||||
TargetPort: 4242,
|
||||
Name: "Quad4 TCP",
|
||||
}
|
||||
}
|
||||
|
||||
r, err := NewReticulum(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create Reticulum instance: %v", err)
|
||||
}
|
||||
|
||||
// Start monitoring interfaces
|
||||
go r.monitorInterfaces()
|
||||
|
||||
// Register announce handler
|
||||
handler := NewAnnounceHandler(r, []string{"*"})
|
||||
r.transport.RegisterAnnounceHandler(handler)
|
||||
|
||||
// Start Reticulum
|
||||
if err := r.Start(); err != nil {
|
||||
log.Fatalf("Failed to start Reticulum: %v", err)
|
||||
}
|
||||
|
||||
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, 0700); err != nil { // #nosec G301
|
||||
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
|
||||
debugLog(2, "Sending initial announce")
|
||||
if err := r.destination.Announce(r.createNodeAppData()); err != nil {
|
||||
debugLog(1, "Failed to send initial announce: %v", err)
|
||||
}
|
||||
|
||||
// Start periodic announce goroutine
|
||||
go func() {
|
||||
// Wait a bit before the first announce
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
for {
|
||||
debugLog(3, "Announcing destination...")
|
||||
err := r.destination.Announce(r.createNodeAppData())
|
||||
if err != nil {
|
||||
debugLog(1, "Could not send announce: %v", err)
|
||||
}
|
||||
|
||||
// Announce every 5 minutes
|
||||
time.Sleep(5 * time.Minute)
|
||||
}
|
||||
}()
|
||||
|
||||
go r.monitorInterfaces()
|
||||
|
||||
debugLog(2, "Reticulum started successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reticulum) Stop() error {
|
||||
debugLog(2, "Stopping Reticulum...")
|
||||
|
||||
for _, buf := range r.buffers {
|
||||
if err := buf.Close(); err != nil {
|
||||
debugLog(1, "Error closing buffer: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, ch := range r.channels {
|
||||
if err := ch.Close(); err != nil {
|
||||
debugLog(1, "Error closing channel: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, iface := range r.interfaces {
|
||||
if err := iface.Stop(); err != nil {
|
||||
debugLog(1, "Error stopping interface %s: %v", iface.GetName(), err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.transport.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close transport: %v", err)
|
||||
}
|
||||
|
||||
debugLog(2, "Reticulum stopped successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
type AnnounceHandler struct {
|
||||
aspectFilter []string
|
||||
reticulum *Reticulum
|
||||
}
|
||||
|
||||
func NewAnnounceHandler(r *Reticulum, aspectFilter []string) *AnnounceHandler {
|
||||
return &AnnounceHandler{
|
||||
aspectFilter: aspectFilter,
|
||||
reticulum: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AnnounceHandler) AspectFilter() []string {
|
||||
return h.aspectFilter
|
||||
}
|
||||
|
||||
func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appData []byte) error {
|
||||
debugLog(DEBUG_INFO, "Received announce from %x", destHash)
|
||||
debugLog(DEBUG_PACKETS, "Raw announce data: %x", appData)
|
||||
|
||||
var isNode bool
|
||||
var nodeEnabled bool
|
||||
var nodeTimestamp int64
|
||||
var nodeMaxSize int16
|
||||
|
||||
// Parse msgpack array
|
||||
if len(appData) > 0 {
|
||||
if appData[0] == 0x92 {
|
||||
// Format [name, ticket] for standard peers
|
||||
debugLog(DEBUG_VERBOSE, "Received standard peer announce")
|
||||
isNode = false
|
||||
var pos = 1
|
||||
|
||||
// Parse first element (NameBytes)
|
||||
if pos+1 < len(appData) && appData[pos] == 0xc4 {
|
||||
nameLen := int(appData[pos+1])
|
||||
if pos+2+nameLen <= len(appData) {
|
||||
nameBytes := appData[pos+2 : pos+2+nameLen]
|
||||
name := string(nameBytes)
|
||||
pos += 2 + nameLen
|
||||
debugLog(DEBUG_VERBOSE, "Peer name: %s (bytes: %x)", name, nameBytes)
|
||||
|
||||
// Parse second element (TicketValue)
|
||||
if pos < len(appData) {
|
||||
ticketValue := appData[pos] // Assuming fixint for now
|
||||
debugLog(DEBUG_VERBOSE, "Peer ticket value: %d", ticketValue)
|
||||
} else {
|
||||
debugLog(DEBUG_ERROR, "Could not parse ticket value from announce appData")
|
||||
}
|
||||
} else {
|
||||
debugLog(DEBUG_ERROR, "Could not parse name bytes from announce appData")
|
||||
}
|
||||
} else {
|
||||
debugLog(DEBUG_ERROR, "Announce appData name is not in expected bin 8 format")
|
||||
}
|
||||
} else if appData[0] == 0x93 {
|
||||
// Format [enable, timestamp, maxsize] for nodes
|
||||
debugLog(DEBUG_VERBOSE, "Received node announce")
|
||||
isNode = true
|
||||
var pos = 1
|
||||
|
||||
// Parse first element (Boolean enable/disable)
|
||||
if pos < len(appData) {
|
||||
if appData[pos] == 0xc3 {
|
||||
nodeEnabled = true
|
||||
} else if appData[pos] == 0xc2 {
|
||||
nodeEnabled = false
|
||||
} else {
|
||||
debugLog(DEBUG_ERROR, "Unexpected format for node enabled status: %x", appData[pos])
|
||||
}
|
||||
pos++
|
||||
debugLog(DEBUG_VERBOSE, "Node enabled: %v", nodeEnabled)
|
||||
|
||||
// Parse second element (Int32 timestamp)
|
||||
if pos+4 < len(appData) && appData[pos] == 0xd2 {
|
||||
pos++
|
||||
timestamp := binary.BigEndian.Uint32(appData[pos : pos+4])
|
||||
nodeTimestamp = int64(timestamp)
|
||||
pos += 4
|
||||
debugLog(DEBUG_VERBOSE, "Node timestamp: %d (%s)", timestamp, time.Unix(nodeTimestamp, 0))
|
||||
|
||||
// Parse third element (Int16 max transfer size)
|
||||
if pos+2 < len(appData) && appData[pos] == 0xd1 {
|
||||
pos++
|
||||
maxSize := binary.BigEndian.Uint16(appData[pos : pos+2])
|
||||
nodeMaxSize = int16(maxSize) // #nosec G115
|
||||
debugLog(DEBUG_VERBOSE, "Node max transfer size: %d KB", nodeMaxSize)
|
||||
} else {
|
||||
debugLog(DEBUG_ERROR, "Could not parse max transfer size from node announce")
|
||||
}
|
||||
} else {
|
||||
debugLog(DEBUG_ERROR, "Could not parse timestamp from node announce")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debugLog(DEBUG_VERBOSE, "Unknown announce data format: %x", appData)
|
||||
}
|
||||
}
|
||||
|
||||
// Type assert and log identity details
|
||||
if identity, ok := id.(*identity.Identity); ok {
|
||||
debugLog(DEBUG_ALL, "Identity details:")
|
||||
debugLog(DEBUG_ALL, " Hash: %s", identity.GetHexHash())
|
||||
debugLog(DEBUG_ALL, " Public Key: %x", identity.GetPublicKey())
|
||||
|
||||
ratchets := identity.GetRatchets()
|
||||
debugLog(DEBUG_ALL, " Active Ratchets: %d", len(ratchets))
|
||||
|
||||
if len(ratchets) > 0 {
|
||||
ratchetKey := identity.GetCurrentRatchetKey()
|
||||
if ratchetKey != nil {
|
||||
ratchetID := identity.GetRatchetID(ratchetKey)
|
||||
debugLog(DEBUG_ALL, " Current Ratchet ID: %x", ratchetID)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a better record with more info
|
||||
recordType := "peer"
|
||||
if isNode {
|
||||
recordType = "node"
|
||||
debugLog(DEBUG_INFO, "Storing node in announce history: enabled=%v, timestamp=%d, maxsize=%dKB",
|
||||
nodeEnabled, nodeTimestamp, nodeMaxSize)
|
||||
}
|
||||
|
||||
h.reticulum.announceHistoryMu.Lock()
|
||||
h.reticulum.announceHistory[identity.GetHexHash()] = announceRecord{
|
||||
timestamp: time.Now().Unix(),
|
||||
appData: appData,
|
||||
}
|
||||
h.reticulum.announceHistoryMu.Unlock()
|
||||
|
||||
debugLog(DEBUG_VERBOSE, "Stored %s announce in history for identity %s", recordType, identity.GetHexHash())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *AnnounceHandler) ReceivePathResponses() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Reticulum) GetDestination() *destination.Destination {
|
||||
return r.destination
|
||||
}
|
||||
|
||||
func (r *Reticulum) createNodeAppData() []byte {
|
||||
// Create a msgpack array with 3 elements
|
||||
// [Bool, Int32, Int16] for [enable, timestamp, max_transfer_size]
|
||||
appData := []byte{0x93} // Array with 3 elements
|
||||
|
||||
// Element 0: Boolean for enable/disable peer
|
||||
if r.nodeEnabled {
|
||||
appData = append(appData, 0xc3) // true
|
||||
} else {
|
||||
appData = append(appData, 0xc2) // false
|
||||
}
|
||||
|
||||
// Element 1: Int32 timestamp (current time)
|
||||
// Update the timestamp when creating new announcements
|
||||
r.nodeTimestamp = time.Now().Unix()
|
||||
appData = append(appData, 0xd2) // int32 format
|
||||
timeBytes := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(timeBytes, uint32(r.nodeTimestamp)) // #nosec G115
|
||||
appData = append(appData, timeBytes...)
|
||||
|
||||
// Element 2: Int16 max transfer size in KB
|
||||
appData = append(appData, 0xd1) // int16 format
|
||||
sizeBytes := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(sizeBytes, uint16(r.maxTransferSize)) // #nosec G115
|
||||
appData = append(appData, sizeBytes...)
|
||||
|
||||
log.Printf("[DEBUG-7] Created node appData (msgpack [enable=%v, timestamp=%d, maxsize=%d]): %x",
|
||||
r.nodeEnabled, r.nodeTimestamp, r.maxTransferSize, appData)
|
||||
return appData
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/internal/config"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/interfaces"
|
||||
)
|
||||
|
||||
type Reticulum struct {
|
||||
config *config.ReticulumConfig
|
||||
transport *transport.Transport
|
||||
}
|
||||
|
||||
func NewReticulum(cfg *config.ReticulumConfig) (*Reticulum, error) {
|
||||
if cfg == nil {
|
||||
cfg = config.DefaultConfig()
|
||||
}
|
||||
|
||||
// Initialize transport
|
||||
t, err := transport.NewTransport(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Reticulum{
|
||||
config: cfg,
|
||||
transport: t,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Reticulum) Start() error {
|
||||
// Initialize interfaces based on config
|
||||
for _, ifaceConfig := range r.config.Interfaces {
|
||||
var iface interfaces.Interface
|
||||
|
||||
switch ifaceConfig.Type {
|
||||
case "tcp":
|
||||
client, err := interfaces.NewTCPClient(
|
||||
ifaceConfig.Name,
|
||||
ifaceConfig.Address,
|
||||
ifaceConfig.Port,
|
||||
ifaceConfig.KISSFraming,
|
||||
ifaceConfig.I2PTunneled,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create TCP interface %s: %v", ifaceConfig.Name, err)
|
||||
continue
|
||||
}
|
||||
iface = client
|
||||
|
||||
case "tcpserver":
|
||||
server, err := interfaces.NewTCPServer(
|
||||
ifaceConfig.Name,
|
||||
ifaceConfig.Address,
|
||||
ifaceConfig.Port,
|
||||
ifaceConfig.PreferIPv6,
|
||||
ifaceConfig.I2PTunneled,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create TCP server interface %s: %v", ifaceConfig.Name, err)
|
||||
continue
|
||||
}
|
||||
iface = server
|
||||
|
||||
default:
|
||||
log.Printf("Unknown interface type: %s", ifaceConfig.Type)
|
||||
continue
|
||||
}
|
||||
|
||||
// Set packet callback to transport
|
||||
iface.SetPacketCallback(r.transport.HandlePacket)
|
||||
}
|
||||
|
||||
log.Printf("Reticulum initialized with config at: %s", r.config.ConfigPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reticulum) Stop() error {
|
||||
if err := r.transport.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Initialize configuration
|
||||
cfg, err := config.InitConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize config: %v", err)
|
||||
}
|
||||
|
||||
// Create new reticulum instance
|
||||
r, err := NewReticulum(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create Reticulum instance: %v", err)
|
||||
}
|
||||
|
||||
// Start reticulum
|
||||
if err := r.Start(); err != nil {
|
||||
log.Fatalf("Failed to start Reticulum: %v", err)
|
||||
}
|
||||
|
||||
// Wait for interrupt signal
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigChan
|
||||
|
||||
// Clean shutdown
|
||||
if err := r.Stop(); err != nil {
|
||||
log.Printf("Error during shutdown: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
enable_transport = true
|
||||
share_instance = true
|
||||
shared_instance_port = 37428
|
||||
instance_control_port = 37429
|
||||
panic_on_interface_error = false
|
||||
loglevel = 4
|
||||
|
||||
[interfaces]
|
||||
[interfaces."Local TCP"]
|
||||
type = "TCPClientInterface"
|
||||
enabled = true
|
||||
target_host = "127.0.0.1"
|
||||
target_port = 4242
|
||||
|
||||
[interfaces."Local UDP"]
|
||||
type = "UDPInterface"
|
||||
enabled = true
|
||||
interface = "lo"
|
||||
@@ -1,18 +0,0 @@
|
||||
enable_transport = true
|
||||
share_instance = true
|
||||
shared_instance_port = 37430
|
||||
instance_control_port = 37431
|
||||
panic_on_interface_error = false
|
||||
loglevel = 4
|
||||
|
||||
[interfaces]
|
||||
[interfaces."Local TCP"]
|
||||
type = "TCPClientInterface"
|
||||
enabled = true
|
||||
target_host = "127.0.0.1"
|
||||
target_port = 4243
|
||||
|
||||
[interfaces."Local UDP"]
|
||||
type = "UDPInterface"
|
||||
enabled = true
|
||||
interface = "lo"
|
||||
8
go.mod
8
go.mod
@@ -1,9 +1,5 @@
|
||||
module github.com/Sudo-Ivan/reticulum-go
|
||||
|
||||
go 1.23.4
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
golang.org/x/crypto v0.31.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
require golang.org/x/crypto v0.39.0
|
||||
|
||||
10
go.sum
10
go.sum
@@ -1,8 +1,2 @@
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pelletier/go-toml"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultSharedInstancePort = 37428
|
||||
DefaultInstanceControlPort = 37429
|
||||
DefaultLogLevel = 4
|
||||
DefaultLogLevel = 4
|
||||
)
|
||||
|
||||
func DefaultConfig() *common.ReticulumConfig {
|
||||
return &common.ReticulumConfig{
|
||||
EnableTransport: false,
|
||||
ShareInstance: true,
|
||||
SharedInstancePort: DefaultSharedInstancePort,
|
||||
InstanceControlPort: DefaultInstanceControlPort,
|
||||
EnableTransport: true,
|
||||
ShareInstance: true,
|
||||
SharedInstancePort: DefaultSharedInstancePort,
|
||||
InstanceControlPort: DefaultInstanceControlPort,
|
||||
PanicOnInterfaceErr: false,
|
||||
LogLevel: DefaultLogLevel,
|
||||
Interfaces: make(map[string]common.InterfaceConfig),
|
||||
Interfaces: make(map[string]*common.InterfaceConfig),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +34,7 @@ func GetConfigPath() (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(homeDir, ".reticulum", "config"), nil
|
||||
return filepath.Join(homeDir, ".reticulum-go", "config"), nil
|
||||
}
|
||||
|
||||
func EnsureConfigDir() error {
|
||||
@@ -40,65 +43,212 @@ func EnsureConfigDir() error {
|
||||
return err
|
||||
}
|
||||
|
||||
configDir := filepath.Join(homeDir, ".reticulum")
|
||||
return os.MkdirAll(configDir, 0755)
|
||||
configDir := filepath.Join(homeDir, ".reticulum-go")
|
||||
return os.MkdirAll(configDir, 0700) // #nosec G301
|
||||
}
|
||||
|
||||
// parseValue parses string values into appropriate types
|
||||
func parseValue(value string) interface{} {
|
||||
value = strings.TrimSpace(value)
|
||||
|
||||
// Try bool
|
||||
if value == "true" {
|
||||
return true
|
||||
}
|
||||
if value == "false" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Try int
|
||||
if i, err := strconv.Atoi(value); err == nil {
|
||||
return i
|
||||
}
|
||||
|
||||
// Return as string
|
||||
return value
|
||||
}
|
||||
|
||||
// LoadConfig loads the configuration from the specified path
|
||||
func LoadConfig(path string) (*common.ReticulumConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
file, err := os.Open(path) // #nosec G304
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
cfg := DefaultConfig()
|
||||
if err := toml.Unmarshal(data, cfg); err != nil {
|
||||
return nil, err
|
||||
cfg.ConfigPath = path
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
var currentInterface *common.InterfaceConfig
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
// Skip comments and empty lines
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle interface sections
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
name := strings.Trim(line, "[]")
|
||||
currentInterface = &common.InterfaceConfig{Name: name}
|
||||
cfg.Interfaces[name] = currentInterface
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
if currentInterface != nil {
|
||||
// Parse interface config
|
||||
switch key {
|
||||
case "type":
|
||||
currentInterface.Type = value
|
||||
case "enabled":
|
||||
currentInterface.Enabled = value == "true"
|
||||
case "address":
|
||||
currentInterface.Address = value
|
||||
case "port":
|
||||
currentInterface.Port, _ = strconv.Atoi(value)
|
||||
case "target_host":
|
||||
currentInterface.TargetHost = value
|
||||
case "target_port":
|
||||
currentInterface.TargetPort, _ = strconv.Atoi(value)
|
||||
case "discovery_port":
|
||||
currentInterface.DiscoveryPort, _ = strconv.Atoi(value)
|
||||
case "data_port":
|
||||
currentInterface.DataPort, _ = strconv.Atoi(value)
|
||||
case "discovery_scope":
|
||||
currentInterface.DiscoveryScope = value
|
||||
case "group_id":
|
||||
currentInterface.GroupID = value
|
||||
}
|
||||
} else {
|
||||
// Parse global config
|
||||
switch key {
|
||||
case "enable_transport":
|
||||
cfg.EnableTransport = value == "true"
|
||||
case "share_instance":
|
||||
cfg.ShareInstance = value == "true"
|
||||
case "shared_instance_port":
|
||||
cfg.SharedInstancePort, _ = strconv.Atoi(value)
|
||||
case "instance_control_port":
|
||||
cfg.InstanceControlPort, _ = strconv.Atoi(value)
|
||||
case "panic_on_interface_error":
|
||||
cfg.PanicOnInterfaceErr = value == "true"
|
||||
case "loglevel":
|
||||
cfg.LogLevel, _ = strconv.Atoi(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg.ConfigPath = path
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// SaveConfig saves the configuration to the specified path
|
||||
func SaveConfig(cfg *common.ReticulumConfig) error {
|
||||
data, err := toml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
if cfg.ConfigPath == "" {
|
||||
return fmt.Errorf("config path not set")
|
||||
}
|
||||
|
||||
return os.WriteFile(cfg.ConfigPath, data, 0644)
|
||||
var builder strings.Builder
|
||||
|
||||
// Write global config
|
||||
builder.WriteString("# Reticulum Configuration\n")
|
||||
builder.WriteString(fmt.Sprintf("enable_transport = %v\n", cfg.EnableTransport))
|
||||
builder.WriteString(fmt.Sprintf("share_instance = %v\n", cfg.ShareInstance))
|
||||
builder.WriteString(fmt.Sprintf("shared_instance_port = %d\n", cfg.SharedInstancePort))
|
||||
builder.WriteString(fmt.Sprintf("instance_control_port = %d\n", cfg.InstanceControlPort))
|
||||
builder.WriteString(fmt.Sprintf("panic_on_interface_error = %v\n", cfg.PanicOnInterfaceErr))
|
||||
builder.WriteString(fmt.Sprintf("loglevel = %d\n\n", cfg.LogLevel))
|
||||
|
||||
// Write interface configs
|
||||
for name, iface := range cfg.Interfaces {
|
||||
builder.WriteString(fmt.Sprintf("[%s]\n", name))
|
||||
builder.WriteString(fmt.Sprintf("type = %s\n", iface.Type))
|
||||
builder.WriteString(fmt.Sprintf("enabled = %v\n", iface.Enabled))
|
||||
|
||||
if iface.Address != "" {
|
||||
builder.WriteString(fmt.Sprintf("address = %s\n", iface.Address))
|
||||
}
|
||||
if iface.Port != 0 {
|
||||
builder.WriteString(fmt.Sprintf("port = %d\n", iface.Port))
|
||||
}
|
||||
if iface.TargetHost != "" {
|
||||
builder.WriteString(fmt.Sprintf("target_host = %s\n", iface.TargetHost))
|
||||
}
|
||||
if iface.TargetPort != 0 {
|
||||
builder.WriteString(fmt.Sprintf("target_port = %d\n", iface.TargetPort))
|
||||
}
|
||||
if iface.DiscoveryPort != 0 {
|
||||
builder.WriteString(fmt.Sprintf("discovery_port = %d\n", iface.DiscoveryPort))
|
||||
}
|
||||
if iface.DataPort != 0 {
|
||||
builder.WriteString(fmt.Sprintf("data_port = %d\n", iface.DataPort))
|
||||
}
|
||||
if iface.DiscoveryScope != "" {
|
||||
builder.WriteString(fmt.Sprintf("discovery_scope = %s\n", iface.DiscoveryScope))
|
||||
}
|
||||
if iface.GroupID != "" {
|
||||
builder.WriteString(fmt.Sprintf("group_id = %s\n", iface.GroupID))
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
return os.WriteFile(cfg.ConfigPath, []byte(builder.String()), 0600) // #nosec G306
|
||||
}
|
||||
|
||||
// CreateDefaultConfig creates a default configuration file
|
||||
func CreateDefaultConfig(path string) error {
|
||||
cfg := DefaultConfig()
|
||||
cfg.ConfigPath = path
|
||||
|
||||
// Add default interface
|
||||
cfg.Interfaces["Default Interface"] = common.InterfaceConfig{
|
||||
Type: "AutoInterface",
|
||||
Enabled: false,
|
||||
// Add Auto Interface
|
||||
cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{
|
||||
Type: "AutoInterface",
|
||||
Enabled: true,
|
||||
GroupID: "reticulum",
|
||||
DiscoveryScope: "link",
|
||||
DiscoveryPort: 29716,
|
||||
DataPort: 42671,
|
||||
}
|
||||
|
||||
// Add default quad4net interface
|
||||
cfg.Interfaces["quad4net tcp"] = common.InterfaceConfig{
|
||||
// Add default interfaces
|
||||
cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
|
||||
Type: "TCPClientInterface",
|
||||
Enabled: true,
|
||||
TargetHost: "127.0.0.1",
|
||||
TargetPort: 4242,
|
||||
Name: "Go-RNS-Testnet",
|
||||
}
|
||||
|
||||
cfg.Interfaces["Quad4 TCP"] = &common.InterfaceConfig{
|
||||
Type: "TCPClientInterface",
|
||||
Enabled: true,
|
||||
TargetHost: "rns.quad4.io",
|
||||
TargetPort: 4242,
|
||||
Name: "Quad4 TCP",
|
||||
}
|
||||
|
||||
data, err := toml.Marshal(cfg)
|
||||
if err != nil {
|
||||
cfg.Interfaces["Local UDP"] = &common.InterfaceConfig{
|
||||
Type: "UDPInterface",
|
||||
Enabled: false,
|
||||
Address: "0.0.0.0",
|
||||
Port: 37696,
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { // #nosec G301
|
||||
return err
|
||||
}
|
||||
|
||||
// Create config directory if it doesn't exist
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(path, data, 0644)
|
||||
return SaveConfig(cfg)
|
||||
}
|
||||
|
||||
// InitConfig initializes the configuration system
|
||||
@@ -118,4 +268,4 @@ func InitConfig() (*common.ReticulumConfig, error) {
|
||||
|
||||
// Load config
|
||||
return LoadConfig(configPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,48 @@
|
||||
package announce
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
const (
|
||||
PACKET_TYPE_DATA = 0x00
|
||||
PACKET_TYPE_ANNOUNCE = 0x01
|
||||
PACKET_TYPE_LINK = 0x02
|
||||
PACKET_TYPE_PROOF = 0x03
|
||||
|
||||
// Announce Types
|
||||
ANNOUNCE_NONE = 0x00
|
||||
ANNOUNCE_PATH = 0x01
|
||||
ANNOUNCE_IDENTITY = 0x02
|
||||
|
||||
// Header Types
|
||||
HEADER_TYPE_1 = 0x00 // One address field
|
||||
HEADER_TYPE_2 = 0x01 // Two address fields
|
||||
|
||||
// Propagation Types
|
||||
PROP_TYPE_BROADCAST = 0x00
|
||||
PROP_TYPE_TRANSPORT = 0x01
|
||||
|
||||
DEST_TYPE_SINGLE = 0x00
|
||||
DEST_TYPE_GROUP = 0x01
|
||||
DEST_TYPE_PLAIN = 0x02
|
||||
DEST_TYPE_LINK = 0x03
|
||||
|
||||
// IFAC Flag
|
||||
IFAC_NONE = 0x00
|
||||
IFAC_AUTH = 0x80
|
||||
|
||||
MAX_HOPS = 128
|
||||
PROPAGATION_RATE = 0.02 // 2% of interface bandwidth
|
||||
RETRY_INTERVAL = 300 // 5 minutes
|
||||
@@ -24,27 +51,37 @@ const (
|
||||
|
||||
type AnnounceHandler interface {
|
||||
AspectFilter() []string
|
||||
ReceivedAnnounce(destinationHash []byte, announcedIdentity *identity.Identity, appData []byte) error
|
||||
ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte) error
|
||||
ReceivePathResponses() bool
|
||||
}
|
||||
|
||||
type Announce struct {
|
||||
mutex sync.RWMutex
|
||||
mutex *sync.RWMutex
|
||||
destinationHash []byte
|
||||
identity *identity.Identity
|
||||
appData []byte
|
||||
hops uint8
|
||||
timestamp int64
|
||||
signature []byte
|
||||
pathResponse bool
|
||||
retries int
|
||||
handlers []AnnounceHandler
|
||||
identity *identity.Identity
|
||||
appData []byte
|
||||
config *common.ReticulumConfig
|
||||
hops uint8
|
||||
timestamp int64
|
||||
signature []byte
|
||||
pathResponse bool
|
||||
retries int
|
||||
handlers []AnnounceHandler
|
||||
ratchetID []byte
|
||||
packet []byte
|
||||
hash []byte
|
||||
}
|
||||
|
||||
func New(dest *identity.Identity, appData []byte, pathResponse bool) (*Announce, error) {
|
||||
func New(dest *identity.Identity, appData []byte, pathResponse bool, config *common.ReticulumConfig) (*Announce, error) {
|
||||
if dest == nil {
|
||||
return nil, errors.New("destination identity required")
|
||||
}
|
||||
|
||||
a := &Announce{
|
||||
mutex: &sync.RWMutex{},
|
||||
identity: dest,
|
||||
appData: appData,
|
||||
config: config,
|
||||
hops: 0,
|
||||
timestamp: time.Now().Unix(),
|
||||
pathResponse: pathResponse,
|
||||
@@ -52,46 +89,62 @@ func New(dest *identity.Identity, appData []byte, pathResponse bool) (*Announce,
|
||||
handlers: make([]AnnounceHandler, 0),
|
||||
}
|
||||
|
||||
// Generate destination hash
|
||||
hash := sha256.New()
|
||||
hash.Write(dest.GetPublicKey())
|
||||
a.destinationHash = hash.Sum(nil)[:16] // Truncated hash
|
||||
// Generate truncated hash from public key
|
||||
pubKey := dest.GetPublicKey()
|
||||
hash := sha256.Sum256(pubKey)
|
||||
a.destinationHash = hash[:identity.TRUNCATED_HASHLENGTH/8]
|
||||
|
||||
// Sign the announce
|
||||
// Get current ratchet ID if enabled
|
||||
currentRatchet := dest.GetCurrentRatchetKey()
|
||||
if currentRatchet != nil {
|
||||
ratchetPub, err := curve25519.X25519(currentRatchet, curve25519.Basepoint)
|
||||
if err == nil {
|
||||
a.ratchetID = dest.GetRatchetID(ratchetPub)
|
||||
}
|
||||
}
|
||||
|
||||
// Sign announce data
|
||||
signData := append(a.destinationHash, a.appData...)
|
||||
if a.ratchetID != nil {
|
||||
signData = append(signData, a.ratchetID...)
|
||||
}
|
||||
a.signature = dest.Sign(signData)
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a *Announce) Propagate(interfaces []transport.Interface) error {
|
||||
a.mutex.Lock()
|
||||
defer a.mutex.Unlock()
|
||||
func (a *Announce) Propagate(interfaces []common.NetworkInterface) error {
|
||||
a.mutex.RLock()
|
||||
defer a.mutex.RUnlock()
|
||||
|
||||
if a.hops >= MAX_HOPS {
|
||||
return errors.New("maximum hop count reached")
|
||||
log.Printf("[DEBUG-7] Propagating announce across %d interfaces", len(interfaces))
|
||||
|
||||
var packet []byte
|
||||
if a.packet != nil {
|
||||
log.Printf("[DEBUG-7] Using cached packet (%d bytes)", len(a.packet))
|
||||
packet = a.packet
|
||||
} else {
|
||||
log.Printf("[DEBUG-7] Creating new packet")
|
||||
packet = a.CreatePacket()
|
||||
a.packet = packet
|
||||
}
|
||||
|
||||
// Increment hop count
|
||||
a.hops++
|
||||
|
||||
// Create announce packet
|
||||
packet := make([]byte, 0)
|
||||
packet = append(packet, a.destinationHash...)
|
||||
packet = append(packet, a.identity.GetPublicKey()...)
|
||||
packet = append(packet, byte(a.hops))
|
||||
|
||||
if a.appData != nil {
|
||||
packet = append(packet, a.appData...)
|
||||
}
|
||||
|
||||
packet = append(packet, a.signature...)
|
||||
|
||||
// Propagate to all interfaces
|
||||
for _, iface := range interfaces {
|
||||
if err := iface.SendAnnounce(packet, a.pathResponse); err != nil {
|
||||
return err
|
||||
if !iface.IsEnabled() {
|
||||
log.Printf("[DEBUG-7] Skipping disabled interface: %s", iface.GetName())
|
||||
continue
|
||||
}
|
||||
if !iface.GetBandwidthAvailable() {
|
||||
log.Printf("[DEBUG-7] Skipping interface with insufficient bandwidth: %s", iface.GetName())
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-7] Sending announce on interface %s", iface.GetName())
|
||||
if err := iface.Send(packet, ""); err != nil {
|
||||
log.Printf("[DEBUG-7] Failed to send on interface %s: %v", iface.GetName(), err)
|
||||
return fmt.Errorf("failed to propagate on interface %s: %w", iface.GetName(), err)
|
||||
}
|
||||
log.Printf("[DEBUG-7] Successfully sent announce on interface %s", iface.GetName())
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -118,28 +171,117 @@ func (a *Announce) HandleAnnounce(data []byte) error {
|
||||
a.mutex.Lock()
|
||||
defer a.mutex.Unlock()
|
||||
|
||||
// Validate announce data
|
||||
if len(data) < 16+32+1 { // Min size: hash + pubkey + hops
|
||||
return errors.New("invalid announce data")
|
||||
log.Printf("[DEBUG-7] Handling announce packet of %d bytes", len(data))
|
||||
|
||||
// Minimum packet size validation
|
||||
// header(2) + desthash(16) + context(1) + enckey(32) + signkey(32) + namehash(10) +
|
||||
// randomhash(10) + signature(64) + min app data(3)
|
||||
if len(data) < 170 {
|
||||
log.Printf("[DEBUG-7] Invalid announce data length: %d bytes (minimum 170)", len(data))
|
||||
return errors.New("invalid announce data length")
|
||||
}
|
||||
|
||||
// Extract fields
|
||||
destHash := data[:16]
|
||||
pubKey := data[16:48]
|
||||
hops := data[48]
|
||||
appData := data[49 : len(data)-64]
|
||||
signature := data[len(data)-64:]
|
||||
// Extract header and check packet type
|
||||
header := data[:2]
|
||||
if header[0]&0x03 != PACKET_TYPE_ANNOUNCE {
|
||||
return errors.New("not an announce packet")
|
||||
}
|
||||
|
||||
// Get hop count
|
||||
hopCount := header[1]
|
||||
if hopCount > MAX_HOPS {
|
||||
log.Printf("[DEBUG-7] Announce exceeded max hops: %d", hopCount)
|
||||
return errors.New("announce exceeded maximum hop count")
|
||||
}
|
||||
|
||||
// Parse the packet based on header type
|
||||
headerType := (header[0] & 0b01000000) >> 6
|
||||
var contextByte byte
|
||||
var packetData []byte
|
||||
|
||||
if headerType == HEADER_TYPE_2 {
|
||||
// Header type 2 format: header(2) + desthash(16) + transportid(16) + context(1) + data
|
||||
if len(data) < 35 {
|
||||
return errors.New("header type 2 packet too short")
|
||||
}
|
||||
destHash := data[2:18]
|
||||
transportID := data[18:34]
|
||||
contextByte = data[34]
|
||||
packetData = data[35:]
|
||||
|
||||
log.Printf("[DEBUG-7] Header type 2 announce: destHash=%x, transportID=%x, context=%d",
|
||||
destHash, transportID, contextByte)
|
||||
} else {
|
||||
// Header type 1 format: header(2) + desthash(16) + context(1) + data
|
||||
if len(data) < 19 {
|
||||
return errors.New("header type 1 packet too short")
|
||||
}
|
||||
destHash := data[2:18]
|
||||
contextByte = data[18]
|
||||
packetData = data[19:]
|
||||
|
||||
log.Printf("[DEBUG-7] Header type 1 announce: destHash=%x, context=%d",
|
||||
destHash, contextByte)
|
||||
}
|
||||
|
||||
// Now parse the data portion according to the spec
|
||||
// Public Key (32) + Signing Key (32) + Name Hash (10) + Random Hash (10) + [Ratchet] + Signature (64) + App Data
|
||||
|
||||
if len(packetData) < 148 { // 32 + 32 + 10 + 10 + 64
|
||||
return errors.New("announce data too short")
|
||||
}
|
||||
|
||||
// Extract the components
|
||||
encKey := packetData[:32]
|
||||
signKey := packetData[32:64]
|
||||
nameHash := packetData[64:74]
|
||||
randomHash := packetData[74:84]
|
||||
|
||||
// The next field could be a ratchet (32 bytes) or signature (64 bytes)
|
||||
// We need to detect this somehow or use a flag
|
||||
// For now, assume no ratchet
|
||||
|
||||
signature := packetData[84:148]
|
||||
appData := packetData[148:]
|
||||
|
||||
log.Printf("[DEBUG-7] Announce fields: encKey=%x, signKey=%x", encKey, signKey)
|
||||
log.Printf("[DEBUG-7] Name hash=%x, random hash=%x", nameHash, randomHash)
|
||||
log.Printf("[DEBUG-7] Signature=%x, appDataLen=%d", signature[:8], len(appData))
|
||||
|
||||
// Get the destination hash from header
|
||||
var destHash []byte
|
||||
if headerType == HEADER_TYPE_2 {
|
||||
destHash = data[2:18]
|
||||
} else {
|
||||
destHash = data[2:18]
|
||||
}
|
||||
|
||||
// Combine public keys
|
||||
pubKey := append(encKey, signKey...)
|
||||
|
||||
// Create announced identity from public keys
|
||||
announcedIdentity := identity.FromPublicKey(pubKey)
|
||||
if announcedIdentity == nil {
|
||||
return errors.New("invalid identity public key")
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
signData := append(destHash, appData...)
|
||||
if !a.identity.Verify(signData, signature) {
|
||||
signedData := make([]byte, 0)
|
||||
signedData = append(signedData, destHash...)
|
||||
signedData = append(signedData, encKey...)
|
||||
signedData = append(signedData, signKey...)
|
||||
signedData = append(signedData, nameHash...)
|
||||
signedData = append(signedData, randomHash...)
|
||||
signedData = append(signedData, appData...)
|
||||
|
||||
if !announcedIdentity.Verify(signedData, signature) {
|
||||
return errors.New("invalid announce signature")
|
||||
}
|
||||
|
||||
// Process announce with registered handlers
|
||||
// Process with handlers
|
||||
for _, handler := range a.handlers {
|
||||
if handler.ReceivePathResponses() || !a.pathResponse {
|
||||
if err := handler.ReceivedAnnounce(destHash, a.identity, appData); err != nil {
|
||||
if err := handler.ReceivedAnnounce(destHash, announcedIdentity, appData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -148,7 +290,7 @@ func (a *Announce) HandleAnnounce(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Announce) RequestPath(destHash []byte, onInterface transport.Interface) error {
|
||||
func (a *Announce) RequestPath(destHash []byte, onInterface common.NetworkInterface) error {
|
||||
a.mutex.Lock()
|
||||
defer a.mutex.Unlock()
|
||||
|
||||
@@ -158,9 +300,207 @@ func (a *Announce) RequestPath(destHash []byte, onInterface transport.Interface)
|
||||
packet = append(packet, byte(0)) // Initial hop count
|
||||
|
||||
// Send path request
|
||||
if err := onInterface.SendPathRequest(packet); err != nil {
|
||||
if err := onInterface.Send(packet, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// CreateHeader creates a Reticulum packet header according to spec
|
||||
func CreateHeader(ifacFlag byte, headerType byte, contextFlag byte, propType byte, destType byte, packetType byte, hops byte) []byte {
|
||||
header := make([]byte, 2)
|
||||
|
||||
// First byte: [IFAC Flag], [Header Type], [Context Flag], [Propagation Type], [Destination Type] and [Packet Type]
|
||||
header[0] = ifacFlag | (headerType << 6) | (contextFlag << 5) |
|
||||
(propType << 4) | (destType << 2) | packetType
|
||||
|
||||
// Second byte: Number of hops
|
||||
header[1] = hops
|
||||
|
||||
return header
|
||||
}
|
||||
|
||||
func (a *Announce) CreatePacket() []byte {
|
||||
// This function creates the complete announce packet according to the Reticulum specification.
|
||||
// Announce Packet Structure:
|
||||
// [Header (2 bytes)][Dest Hash (16 bytes)][Transport ID (16 bytes)][Context (1 byte)][Announce Data]
|
||||
// Announce Data Structure:
|
||||
// [Public Key (32 bytes)][Signing Key (32 bytes)][Name Hash (10 bytes)][Random Hash (10 bytes)][Ratchet (32 bytes)][Signature (64 bytes)][App Data]
|
||||
|
||||
// 1. Create Header
|
||||
header := CreateHeader(
|
||||
IFAC_NONE,
|
||||
HEADER_TYPE_2,
|
||||
0, // No context flag for announce
|
||||
PROP_TYPE_BROADCAST,
|
||||
DEST_TYPE_SINGLE,
|
||||
PACKET_TYPE_ANNOUNCE,
|
||||
a.hops,
|
||||
)
|
||||
|
||||
// 2. Destination Hash
|
||||
destHash := a.identity.Hash()
|
||||
|
||||
// 3. Transport ID (zeros for broadcast announce)
|
||||
transportID := make([]byte, 16)
|
||||
|
||||
// 4. Context Byte (zero for announce)
|
||||
contextByte := byte(0)
|
||||
|
||||
// 5. Announce Data
|
||||
// 5.1 Public Keys
|
||||
pubKey := a.identity.GetPublicKey()
|
||||
encKey := pubKey[:32]
|
||||
signKey := pubKey[32:]
|
||||
|
||||
// 5.2 Name Hash
|
||||
appName := fmt.Sprintf("%s.%s", a.config.AppName, a.config.AppAspect)
|
||||
nameHash := sha256.Sum256([]byte(appName))
|
||||
nameHash10 := nameHash[:10]
|
||||
|
||||
// 5.3 Random Hash
|
||||
randomHash := make([]byte, 10)
|
||||
_, err := rand.Read(randomHash)
|
||||
if err != nil {
|
||||
log.Printf("Error reading random bytes for announce: %v", err)
|
||||
}
|
||||
|
||||
// 5.4 Ratchet
|
||||
ratchetData := make([]byte, 32)
|
||||
currentRatchetKey := a.identity.GetCurrentRatchetKey()
|
||||
if currentRatchetKey != nil {
|
||||
ratchetPub, err := curve25519.X25519(currentRatchetKey, curve25519.Basepoint)
|
||||
if err == nil {
|
||||
copy(ratchetData, ratchetPub)
|
||||
}
|
||||
}
|
||||
|
||||
// 5.5 Signature
|
||||
// The signature is calculated over: Dest Hash + Public Keys + Name Hash + Random Hash + Ratchet + App Data
|
||||
validationData := make([]byte, 0)
|
||||
validationData = append(validationData, destHash...)
|
||||
validationData = append(validationData, encKey...)
|
||||
validationData = append(validationData, signKey...)
|
||||
validationData = append(validationData, nameHash10...)
|
||||
validationData = append(validationData, randomHash...)
|
||||
validationData = append(validationData, ratchetData...)
|
||||
validationData = append(validationData, a.appData...)
|
||||
signature := a.identity.Sign(validationData)
|
||||
|
||||
// 6. Assemble the packet
|
||||
packet := make([]byte, 0)
|
||||
packet = append(packet, header...)
|
||||
packet = append(packet, destHash...)
|
||||
packet = append(packet, transportID...)
|
||||
packet = append(packet, contextByte)
|
||||
packet = append(packet, encKey...)
|
||||
packet = append(packet, signKey...)
|
||||
packet = append(packet, nameHash10...)
|
||||
packet = append(packet, randomHash...)
|
||||
packet = append(packet, ratchetData...)
|
||||
packet = append(packet, signature...)
|
||||
packet = append(packet, a.appData...)
|
||||
|
||||
return packet
|
||||
}
|
||||
|
||||
type AnnouncePacket struct {
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func NewAnnouncePacket(pubKey []byte, appData []byte, announceID []byte) *AnnouncePacket {
|
||||
packet := &AnnouncePacket{}
|
||||
|
||||
// Build packet data
|
||||
packet.Data = make([]byte, 0, len(pubKey)+len(appData)+len(announceID)+4)
|
||||
|
||||
// Add header
|
||||
packet.Data = append(packet.Data, PACKET_TYPE_ANNOUNCE)
|
||||
packet.Data = append(packet.Data, ANNOUNCE_IDENTITY)
|
||||
|
||||
// Add public key
|
||||
packet.Data = append(packet.Data, pubKey...)
|
||||
|
||||
// Add app data length and content
|
||||
appDataLen := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(appDataLen, uint16(len(appData))) // #nosec G115
|
||||
packet.Data = append(packet.Data, appDataLen...)
|
||||
packet.Data = append(packet.Data, appData...)
|
||||
|
||||
// Add announce ID
|
||||
packet.Data = append(packet.Data, announceID...)
|
||||
|
||||
return packet
|
||||
}
|
||||
|
||||
// NewAnnounce creates a new announce packet for a destination
|
||||
func NewAnnounce(identity *identity.Identity, appData []byte, ratchetID []byte, pathResponse bool, config *common.ReticulumConfig) (*Announce, error) {
|
||||
log.Printf("[DEBUG-7] Creating new announce: appDataLen=%d, hasRatchet=%v, pathResponse=%v",
|
||||
len(appData), ratchetID != nil, pathResponse)
|
||||
|
||||
if identity == nil {
|
||||
log.Printf("[DEBUG-7] Error: nil identity provided")
|
||||
return nil, errors.New("identity cannot be nil")
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
return nil, errors.New("config cannot be nil")
|
||||
}
|
||||
|
||||
destHash := identity.Hash()
|
||||
log.Printf("[DEBUG-7] Generated destination hash: %x", destHash)
|
||||
|
||||
a := &Announce{
|
||||
identity: identity,
|
||||
appData: appData,
|
||||
ratchetID: ratchetID,
|
||||
pathResponse: pathResponse,
|
||||
destinationHash: destHash,
|
||||
hops: 0,
|
||||
mutex: &sync.RWMutex{},
|
||||
handlers: make([]AnnounceHandler, 0),
|
||||
config: config,
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-7] Created announce object: destHash=%x, hops=%d",
|
||||
a.destinationHash, a.hops)
|
||||
|
||||
// Create initial packet
|
||||
packet := a.CreatePacket()
|
||||
a.packet = packet
|
||||
|
||||
// Generate hash
|
||||
hash := a.Hash()
|
||||
log.Printf("[DEBUG-7] Generated announce hash: %x", hash)
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a *Announce) Hash() []byte {
|
||||
if a.hash == nil {
|
||||
// Generate hash from announce data
|
||||
h := sha256.New()
|
||||
h.Write(a.destinationHash)
|
||||
h.Write(a.identity.GetPublicKey())
|
||||
h.Write([]byte{a.hops})
|
||||
h.Write(a.appData)
|
||||
if a.ratchetID != nil {
|
||||
h.Write(a.ratchetID)
|
||||
}
|
||||
a.hash = h.Sum(nil)
|
||||
}
|
||||
return a.hash
|
||||
}
|
||||
|
||||
func (a *Announce) GetPacket() []byte {
|
||||
a.mutex.Lock()
|
||||
defer a.mutex.Unlock()
|
||||
|
||||
if a.packet == nil {
|
||||
// Use CreatePacket to generate the packet
|
||||
a.packet = a.CreatePacket()
|
||||
}
|
||||
|
||||
return a.packet
|
||||
}
|
||||
|
||||
7
pkg/announce/handler.go
Normal file
7
pkg/announce/handler.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package announce
|
||||
|
||||
type Handler interface {
|
||||
AspectFilter() []string
|
||||
ReceivedAnnounce(destHash []byte, identity interface{}, appData []byte) error
|
||||
ReceivePathResponses() bool
|
||||
}
|
||||
252
pkg/buffer/buffer.go
Normal file
252
pkg/buffer/buffer.go
Normal file
@@ -0,0 +1,252 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/bzip2"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/channel"
|
||||
)
|
||||
|
||||
const (
|
||||
StreamIDMax = 0x3fff // 16383
|
||||
MaxChunkLen = 16 * 1024
|
||||
MaxDataLen = 457 // MDU - 2 - 6 (2 for stream header, 6 for channel envelope)
|
||||
CompressTries = 4
|
||||
)
|
||||
|
||||
type StreamDataMessage struct {
|
||||
StreamID uint16
|
||||
Data []byte
|
||||
EOF bool
|
||||
Compressed bool
|
||||
}
|
||||
|
||||
func (m *StreamDataMessage) Pack() ([]byte, error) {
|
||||
headerVal := uint16(m.StreamID & StreamIDMax)
|
||||
if m.EOF {
|
||||
headerVal |= 0x8000
|
||||
}
|
||||
if m.Compressed {
|
||||
headerVal |= 0x4000
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := binary.Write(buf, binary.BigEndian, headerVal); err != nil { // #nosec G104
|
||||
return nil, err // Or handle the error appropriately
|
||||
}
|
||||
buf.Write(m.Data)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (m *StreamDataMessage) GetType() uint16 {
|
||||
return 0x01 // Assign appropriate message type constant
|
||||
}
|
||||
|
||||
func (m *StreamDataMessage) Unpack(data []byte) error {
|
||||
if len(data) < 2 {
|
||||
return io.ErrShortBuffer
|
||||
}
|
||||
|
||||
header := binary.BigEndian.Uint16(data[:2])
|
||||
m.StreamID = header & StreamIDMax
|
||||
m.EOF = (header & 0x8000) != 0
|
||||
m.Compressed = (header & 0x4000) != 0
|
||||
m.Data = data[2:]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type RawChannelReader struct {
|
||||
streamID int
|
||||
channel *channel.Channel
|
||||
buffer *bytes.Buffer
|
||||
eof bool
|
||||
callbacks []func(int)
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewRawChannelReader(streamID int, ch *channel.Channel) *RawChannelReader {
|
||||
reader := &RawChannelReader{
|
||||
streamID: streamID,
|
||||
channel: ch,
|
||||
buffer: bytes.NewBuffer(nil),
|
||||
callbacks: make([]func(int), 0),
|
||||
}
|
||||
|
||||
ch.AddMessageHandler(reader.HandleMessage)
|
||||
return reader
|
||||
}
|
||||
|
||||
func (r *RawChannelReader) AddReadyCallback(cb func(int)) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
r.callbacks = append(r.callbacks, cb)
|
||||
}
|
||||
|
||||
func (r *RawChannelReader) RemoveReadyCallback(cb func(int)) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
for i, fn := range r.callbacks {
|
||||
if &fn == &cb {
|
||||
r.callbacks = append(r.callbacks[:i], r.callbacks[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RawChannelReader) Read(p []byte) (n int, err error) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
if r.buffer.Len() == 0 && r.eof {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
n, err = r.buffer.Read(p)
|
||||
if err == io.EOF && !r.eof {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *RawChannelReader) HandleMessage(msg channel.MessageBase) bool { // #nosec G115
|
||||
if streamMsg, ok := msg.(*StreamDataMessage); ok && streamMsg.StreamID == uint16(r.streamID) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
if streamMsg.Compressed {
|
||||
decompressed := decompressData(streamMsg.Data)
|
||||
r.buffer.Write(decompressed)
|
||||
} else {
|
||||
r.buffer.Write(streamMsg.Data)
|
||||
}
|
||||
|
||||
if streamMsg.EOF {
|
||||
r.eof = true
|
||||
}
|
||||
|
||||
// Notify callbacks
|
||||
for _, cb := range r.callbacks {
|
||||
cb(r.buffer.Len())
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type RawChannelWriter struct {
|
||||
streamID int
|
||||
channel *channel.Channel
|
||||
eof bool
|
||||
}
|
||||
|
||||
func NewRawChannelWriter(streamID int, ch *channel.Channel) *RawChannelWriter {
|
||||
return &RawChannelWriter{
|
||||
streamID: streamID,
|
||||
channel: ch,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *RawChannelWriter) Write(p []byte) (n int, err error) {
|
||||
if len(p) > MaxChunkLen {
|
||||
p = p[:MaxChunkLen]
|
||||
}
|
||||
|
||||
msg := &StreamDataMessage{
|
||||
StreamID: uint16(w.streamID), // #nosec G115
|
||||
Data: p,
|
||||
EOF: w.eof,
|
||||
}
|
||||
|
||||
if len(p) > 32 {
|
||||
for try := 1; try < CompressTries; try++ {
|
||||
chunkLen := len(p) / try
|
||||
compressed := compressData(p[:chunkLen])
|
||||
if len(compressed) < MaxDataLen && len(compressed) < chunkLen {
|
||||
msg.Data = compressed
|
||||
msg.Compressed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.channel.Send(msg); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *RawChannelWriter) Close() error {
|
||||
w.eof = true
|
||||
_, err := w.Write(nil)
|
||||
return err
|
||||
}
|
||||
|
||||
type Buffer struct {
|
||||
ReadWriter *bufio.ReadWriter
|
||||
}
|
||||
|
||||
func (b *Buffer) Write(p []byte) (n int, err error) {
|
||||
return b.ReadWriter.Write(p)
|
||||
}
|
||||
|
||||
func (b *Buffer) Read(p []byte) (n int, err error) {
|
||||
return b.ReadWriter.Read(p)
|
||||
}
|
||||
|
||||
func (b *Buffer) Close() error {
|
||||
if err := b.ReadWriter.Writer.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateReader(streamID int, ch *channel.Channel, readyCallback func(int)) *bufio.Reader {
|
||||
raw := NewRawChannelReader(streamID, ch)
|
||||
if readyCallback != nil {
|
||||
raw.AddReadyCallback(readyCallback)
|
||||
}
|
||||
return bufio.NewReader(raw)
|
||||
}
|
||||
|
||||
func CreateWriter(streamID int, ch *channel.Channel) *bufio.Writer {
|
||||
raw := NewRawChannelWriter(streamID, ch)
|
||||
return bufio.NewWriter(raw)
|
||||
}
|
||||
|
||||
func CreateBidirectionalBuffer(receiveStreamID, sendStreamID int, ch *channel.Channel, readyCallback func(int)) *bufio.ReadWriter {
|
||||
reader := CreateReader(receiveStreamID, ch, readyCallback)
|
||||
writer := CreateWriter(sendStreamID, ch)
|
||||
return bufio.NewReadWriter(reader, writer)
|
||||
}
|
||||
|
||||
func compressData(data []byte) []byte {
|
||||
var compressed bytes.Buffer
|
||||
w := bytes.NewBuffer(data)
|
||||
r := bzip2.NewReader(w)
|
||||
_, err := io.Copy(&compressed, r) // #nosec G104 #nosec G110
|
||||
if err != nil {
|
||||
// Handle error, e.g., log it or return an error
|
||||
return nil
|
||||
}
|
||||
return compressed.Bytes()
|
||||
}
|
||||
|
||||
func decompressData(data []byte) []byte {
|
||||
reader := bzip2.NewReader(bytes.NewReader(data))
|
||||
var decompressed bytes.Buffer
|
||||
// Limit the amount of data read to prevent decompression bombs
|
||||
limitedReader := io.LimitReader(reader, MaxChunkLen) // #nosec G110
|
||||
_, err := io.Copy(&decompressed, limitedReader)
|
||||
if err != nil {
|
||||
// Handle error, e.g., log it or return an error
|
||||
return nil
|
||||
}
|
||||
return decompressed.Bytes()
|
||||
}
|
||||
226
pkg/channel/channel.go
Normal file
226
pkg/channel/channel.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package channel
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
||||
)
|
||||
|
||||
const (
|
||||
// Window sizes and thresholds
|
||||
WindowInitial = 2
|
||||
WindowMin = 2
|
||||
WindowMinSlow = 2
|
||||
WindowMinMedium = 5
|
||||
WindowMinFast = 16
|
||||
WindowMaxSlow = 5
|
||||
WindowMaxMedium = 12
|
||||
WindowMaxFast = 48
|
||||
WindowMax = WindowMaxFast
|
||||
WindowFlexibility = 4
|
||||
|
||||
// RTT thresholds
|
||||
RTTFast = 0.18
|
||||
RTTMedium = 0.75
|
||||
RTTSlow = 1.45
|
||||
|
||||
// Sequence numbers
|
||||
SeqMax uint16 = 0xFFFF
|
||||
SeqModulus uint16 = SeqMax
|
||||
|
||||
FastRateThreshold = 10
|
||||
)
|
||||
|
||||
// MessageState represents the state of a message
|
||||
type MessageState int
|
||||
|
||||
const (
|
||||
MsgStateNew MessageState = iota
|
||||
MsgStateSent
|
||||
MsgStateDelivered
|
||||
MsgStateFailed
|
||||
)
|
||||
|
||||
// MessageBase defines the interface for messages that can be sent over a channel
|
||||
type MessageBase interface {
|
||||
Pack() ([]byte, error)
|
||||
Unpack([]byte) error
|
||||
GetType() uint16
|
||||
}
|
||||
|
||||
// Channel manages reliable message delivery over a transport link
|
||||
type Channel struct {
|
||||
link transport.LinkInterface
|
||||
mutex sync.RWMutex
|
||||
txRing []*Envelope
|
||||
rxRing []*Envelope
|
||||
window int
|
||||
windowMax int
|
||||
windowMin int
|
||||
windowFlex int
|
||||
nextSequence uint16
|
||||
nextRxSequence uint16
|
||||
maxTries int
|
||||
fastRateRounds int
|
||||
medRateRounds int
|
||||
messageHandlers []func(MessageBase) bool
|
||||
}
|
||||
|
||||
// Envelope wraps a message with metadata for transmission
|
||||
type Envelope struct {
|
||||
Sequence uint16
|
||||
Message MessageBase
|
||||
Raw []byte
|
||||
Packet interface{}
|
||||
Tries int
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// NewChannel creates a new Channel instance
|
||||
func NewChannel(link transport.LinkInterface) *Channel {
|
||||
return &Channel{
|
||||
link: link,
|
||||
messageHandlers: make([]func(MessageBase) bool, 0),
|
||||
mutex: sync.RWMutex{},
|
||||
windowMax: WindowMaxSlow,
|
||||
windowMin: WindowMinSlow,
|
||||
window: WindowInitial,
|
||||
maxTries: 3,
|
||||
}
|
||||
}
|
||||
|
||||
// Send transmits a message over the channel
|
||||
func (c *Channel) Send(msg MessageBase) error {
|
||||
if c.link.GetStatus() != transport.STATUS_ACTIVE {
|
||||
return errors.New("link not ready")
|
||||
}
|
||||
|
||||
env := &Envelope{
|
||||
Sequence: c.nextSequence,
|
||||
Message: msg,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
c.nextSequence = (c.nextSequence + 1) % SeqModulus
|
||||
c.txRing = append(c.txRing, env)
|
||||
c.mutex.Unlock()
|
||||
|
||||
data, err := msg.Pack()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
env.Raw = data
|
||||
packet := c.link.Send(data)
|
||||
env.Packet = packet
|
||||
env.Tries++
|
||||
|
||||
timeout := c.getPacketTimeout(env.Tries)
|
||||
c.link.SetPacketTimeout(packet, c.handleTimeout, timeout)
|
||||
c.link.SetPacketDelivered(packet, c.handleDelivered)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleTimeout handles packet timeout events
|
||||
func (c *Channel) handleTimeout(packet interface{}) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
for _, env := range c.txRing {
|
||||
if env.Packet == packet {
|
||||
if env.Tries >= c.maxTries {
|
||||
// Remove from ring and notify failure
|
||||
return
|
||||
}
|
||||
env.Tries++
|
||||
if err := c.link.Resend(packet); err != nil { // #nosec G104
|
||||
// Handle resend error, e.g., log it or mark envelope as failed
|
||||
log.Printf("Failed to resend packet: %v", err)
|
||||
// Optionally, mark the envelope as failed or remove it from txRing
|
||||
// env.State = MsgStateFailed
|
||||
// c.txRing = append(c.txRing[:i], c.txRing[i+1:]...)
|
||||
return
|
||||
}
|
||||
timeout := c.getPacketTimeout(env.Tries)
|
||||
c.link.SetPacketTimeout(packet, c.handleTimeout, timeout)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleDelivered handles packet delivery confirmations
|
||||
func (c *Channel) handleDelivered(packet interface{}) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
for i, env := range c.txRing {
|
||||
if env.Packet == packet {
|
||||
c.txRing = append(c.txRing[:i], c.txRing[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Channel) getPacketTimeout(tries int) time.Duration {
|
||||
rtt := c.link.GetRTT()
|
||||
if rtt < 0.025 {
|
||||
rtt = 0.025
|
||||
}
|
||||
|
||||
timeout := math.Pow(1.5, float64(tries-1)) * rtt * 2.5 * float64(len(c.txRing)+2)
|
||||
return time.Duration(timeout * float64(time.Second))
|
||||
}
|
||||
|
||||
func (c *Channel) AddMessageHandler(handler func(MessageBase) bool) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
c.messageHandlers = append(c.messageHandlers, handler)
|
||||
}
|
||||
|
||||
func (c *Channel) RemoveMessageHandler(handler func(MessageBase) bool) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
for i, h := range c.messageHandlers {
|
||||
if &h == &handler {
|
||||
c.messageHandlers = append(c.messageHandlers[:i], c.messageHandlers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Channel) updateRateThresholds() {
|
||||
rtt := c.link.RTT()
|
||||
|
||||
if rtt > RTTFast {
|
||||
c.fastRateRounds = 0
|
||||
|
||||
if rtt > RTTMedium {
|
||||
c.medRateRounds = 0
|
||||
} else {
|
||||
c.medRateRounds++
|
||||
if c.windowMax < WindowMaxMedium && c.medRateRounds == FastRateThreshold {
|
||||
c.windowMax = WindowMaxMedium
|
||||
c.windowMin = WindowMinMedium
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.fastRateRounds++
|
||||
if c.windowMax < WindowMaxFast && c.fastRateRounds == FastRateThreshold {
|
||||
c.windowMax = WindowMaxFast
|
||||
c.windowMin = WindowMinFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Channel) Close() error {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
// Cleanup resources
|
||||
return nil
|
||||
}
|
||||
@@ -1,29 +1,93 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
DEFAULT_SHARED_INSTANCE_PORT = 37428
|
||||
DEFAULT_INSTANCE_CONTROL_PORT = 37429
|
||||
DEFAULT_LOG_LEVEL = 20
|
||||
)
|
||||
|
||||
// ConfigProvider interface for accessing configuration
|
||||
type ConfigProvider interface {
|
||||
GetConfigPath() string
|
||||
GetLogLevel() int
|
||||
GetInterfaces() map[string]InterfaceConfig
|
||||
GetConfigPath() string
|
||||
GetLogLevel() int
|
||||
GetInterfaces() map[string]InterfaceConfig
|
||||
}
|
||||
|
||||
// InterfaceConfig represents interface configuration
|
||||
type InterfaceConfig struct {
|
||||
Type string `toml:"type"`
|
||||
Enabled bool `toml:"enabled"`
|
||||
TargetHost string `toml:"target_host,omitempty"`
|
||||
TargetPort int `toml:"target_port,omitempty"`
|
||||
Interface string `toml:"interface,omitempty"`
|
||||
Name string
|
||||
Type string
|
||||
Enabled bool
|
||||
Address string
|
||||
Port int
|
||||
TargetHost string
|
||||
TargetPort int
|
||||
TargetAddress string
|
||||
Interface string
|
||||
KISSFraming bool
|
||||
I2PTunneled bool
|
||||
PreferIPv6 bool
|
||||
MaxReconnTries int
|
||||
Bitrate int64
|
||||
MTU int
|
||||
GroupID string
|
||||
DiscoveryScope string
|
||||
DiscoveryPort int
|
||||
DataPort int
|
||||
}
|
||||
|
||||
// ReticulumConfig represents the main configuration structure
|
||||
type ReticulumConfig struct {
|
||||
EnableTransport bool `toml:"enable_transport"`
|
||||
ShareInstance bool `toml:"share_instance"`
|
||||
SharedInstancePort int `toml:"shared_instance_port"`
|
||||
InstanceControlPort int `toml:"instance_control_port"`
|
||||
PanicOnInterfaceErr bool `toml:"panic_on_interface_error"`
|
||||
LogLevel int `toml:"loglevel"`
|
||||
ConfigPath string `toml:"-"`
|
||||
Interfaces map[string]InterfaceConfig
|
||||
}
|
||||
ConfigPath string
|
||||
EnableTransport bool
|
||||
ShareInstance bool
|
||||
SharedInstancePort int
|
||||
InstanceControlPort int
|
||||
PanicOnInterfaceErr bool
|
||||
LogLevel int
|
||||
Interfaces map[string]*InterfaceConfig
|
||||
AppName string
|
||||
AppAspect string
|
||||
}
|
||||
|
||||
// NewReticulumConfig creates a new ReticulumConfig with default values
|
||||
func NewReticulumConfig() *ReticulumConfig {
|
||||
return &ReticulumConfig{
|
||||
EnableTransport: true,
|
||||
ShareInstance: false,
|
||||
SharedInstancePort: DEFAULT_SHARED_INSTANCE_PORT,
|
||||
InstanceControlPort: DEFAULT_INSTANCE_CONTROL_PORT,
|
||||
PanicOnInterfaceErr: false,
|
||||
LogLevel: DEFAULT_LOG_LEVEL,
|
||||
Interfaces: make(map[string]*InterfaceConfig),
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks if the configuration is valid
|
||||
func (c *ReticulumConfig) Validate() error {
|
||||
if c.SharedInstancePort < 1 || c.SharedInstancePort > 65535 {
|
||||
return fmt.Errorf("invalid shared instance port: %d", c.SharedInstancePort)
|
||||
}
|
||||
if c.InstanceControlPort < 1 || c.InstanceControlPort > 65535 {
|
||||
return fmt.Errorf("invalid instance control port: %d", c.InstanceControlPort)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DefaultConfig() *ReticulumConfig {
|
||||
return &ReticulumConfig{
|
||||
EnableTransport: true,
|
||||
ShareInstance: false,
|
||||
SharedInstancePort: DEFAULT_SHARED_INSTANCE_PORT,
|
||||
InstanceControlPort: DEFAULT_INSTANCE_CONTROL_PORT,
|
||||
PanicOnInterfaceErr: false,
|
||||
LogLevel: DEFAULT_LOG_LEVEL,
|
||||
Interfaces: make(map[string]*InterfaceConfig),
|
||||
AppName: "Go Client",
|
||||
AppAspect: "node",
|
||||
}
|
||||
}
|
||||
|
||||
94
pkg/common/config_test.go
Normal file
94
pkg/common/config_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewReticulumConfig(t *testing.T) {
|
||||
cfg := NewReticulumConfig()
|
||||
|
||||
if !cfg.EnableTransport {
|
||||
t.Errorf("NewReticulumConfig() EnableTransport = %v; want true", cfg.EnableTransport)
|
||||
}
|
||||
if cfg.ShareInstance {
|
||||
t.Errorf("NewReticulumConfig() ShareInstance = %v; want false", cfg.ShareInstance)
|
||||
}
|
||||
if cfg.SharedInstancePort != DEFAULT_SHARED_INSTANCE_PORT {
|
||||
t.Errorf("NewReticulumConfig() SharedInstancePort = %d; want %d", cfg.SharedInstancePort, DEFAULT_SHARED_INSTANCE_PORT)
|
||||
}
|
||||
if cfg.InstanceControlPort != DEFAULT_INSTANCE_CONTROL_PORT {
|
||||
t.Errorf("NewReticulumConfig() InstanceControlPort = %d; want %d", cfg.InstanceControlPort, DEFAULT_INSTANCE_CONTROL_PORT)
|
||||
}
|
||||
if cfg.PanicOnInterfaceErr {
|
||||
t.Errorf("NewReticulumConfig() PanicOnInterfaceErr = %v; want false", cfg.PanicOnInterfaceErr)
|
||||
}
|
||||
if cfg.LogLevel != DEFAULT_LOG_LEVEL {
|
||||
t.Errorf("NewReticulumConfig() LogLevel = %d; want %d", cfg.LogLevel, DEFAULT_LOG_LEVEL)
|
||||
}
|
||||
if len(cfg.Interfaces) != 0 {
|
||||
t.Errorf("NewReticulumConfig() Interfaces length = %d; want 0", len(cfg.Interfaces))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
if !cfg.EnableTransport {
|
||||
t.Errorf("DefaultConfig() EnableTransport = %v; want true", cfg.EnableTransport)
|
||||
}
|
||||
if cfg.ShareInstance {
|
||||
t.Errorf("DefaultConfig() ShareInstance = %v; want false", cfg.ShareInstance)
|
||||
}
|
||||
if cfg.SharedInstancePort != DEFAULT_SHARED_INSTANCE_PORT {
|
||||
t.Errorf("DefaultConfig() SharedInstancePort = %d; want %d", cfg.SharedInstancePort, DEFAULT_SHARED_INSTANCE_PORT)
|
||||
}
|
||||
if cfg.InstanceControlPort != DEFAULT_INSTANCE_CONTROL_PORT {
|
||||
t.Errorf("DefaultConfig() InstanceControlPort = %d; want %d", cfg.InstanceControlPort, DEFAULT_INSTANCE_CONTROL_PORT)
|
||||
}
|
||||
if cfg.PanicOnInterfaceErr {
|
||||
t.Errorf("DefaultConfig() PanicOnInterfaceErr = %v; want false", cfg.PanicOnInterfaceErr)
|
||||
}
|
||||
if cfg.LogLevel != DEFAULT_LOG_LEVEL {
|
||||
t.Errorf("DefaultConfig() LogLevel = %d; want %d", cfg.LogLevel, DEFAULT_LOG_LEVEL)
|
||||
}
|
||||
if len(cfg.Interfaces) != 0 {
|
||||
t.Errorf("DefaultConfig() Interfaces length = %d; want 0", len(cfg.Interfaces))
|
||||
}
|
||||
if cfg.AppName != "Go Client" {
|
||||
t.Errorf("DefaultConfig() AppName = %q; want %q", cfg.AppName, "Go Client")
|
||||
}
|
||||
if cfg.AppAspect != "node" {
|
||||
t.Errorf("DefaultConfig() AppAspect = %q; want %q", cfg.AppAspect, "node")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReticulumConfig_Validate(t *testing.T) {
|
||||
validConfig := DefaultConfig()
|
||||
if err := validConfig.Validate(); err != nil {
|
||||
t.Errorf("Validate() on default config failed: %v", err)
|
||||
}
|
||||
|
||||
invalidPortConfig1 := DefaultConfig()
|
||||
invalidPortConfig1.SharedInstancePort = 0
|
||||
if err := invalidPortConfig1.Validate(); err == nil {
|
||||
t.Errorf("Validate() did not return error for invalid SharedInstancePort 0")
|
||||
}
|
||||
|
||||
invalidPortConfig2 := DefaultConfig()
|
||||
invalidPortConfig2.SharedInstancePort = 65536
|
||||
if err := invalidPortConfig2.Validate(); err == nil {
|
||||
t.Errorf("Validate() did not return error for invalid SharedInstancePort 65536")
|
||||
}
|
||||
|
||||
invalidPortConfig3 := DefaultConfig()
|
||||
invalidPortConfig3.InstanceControlPort = 0
|
||||
if err := invalidPortConfig3.Validate(); err == nil {
|
||||
t.Errorf("Validate() did not return error for invalid InstanceControlPort 0")
|
||||
}
|
||||
|
||||
invalidPortConfig4 := DefaultConfig()
|
||||
invalidPortConfig4.InstanceControlPort = 65536
|
||||
if err := invalidPortConfig4.Validate(); err == nil {
|
||||
t.Errorf("Validate() did not return error for invalid InstanceControlPort 65536")
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,61 @@
|
||||
package common
|
||||
|
||||
const (
|
||||
// Interface Types
|
||||
IF_TYPE_UDP InterfaceType = iota
|
||||
IF_TYPE_TCP
|
||||
IF_TYPE_UNIX
|
||||
// Interface Types
|
||||
IF_TYPE_NONE InterfaceType = iota
|
||||
IF_TYPE_UDP
|
||||
IF_TYPE_TCP
|
||||
IF_TYPE_UNIX
|
||||
IF_TYPE_I2P
|
||||
IF_TYPE_BLUETOOTH
|
||||
IF_TYPE_SERIAL
|
||||
IF_TYPE_AUTO
|
||||
|
||||
// Interface Modes
|
||||
IF_MODE_FULL InterfaceMode = iota
|
||||
IF_MODE_POINT
|
||||
IF_MODE_GATEWAY
|
||||
// Interface Modes
|
||||
IF_MODE_FULL InterfaceMode = iota
|
||||
IF_MODE_POINT
|
||||
IF_MODE_GATEWAY
|
||||
IF_MODE_ACCESS_POINT
|
||||
IF_MODE_ROAMING
|
||||
IF_MODE_BOUNDARY
|
||||
|
||||
// Transport Modes
|
||||
TRANSPORT_MODE_DIRECT TransportMode = iota
|
||||
TRANSPORT_MODE_RELAY
|
||||
TRANSPORT_MODE_GATEWAY
|
||||
// Transport Modes
|
||||
TRANSPORT_MODE_DIRECT TransportMode = iota
|
||||
TRANSPORT_MODE_RELAY
|
||||
TRANSPORT_MODE_GATEWAY
|
||||
|
||||
// Path Status
|
||||
PATH_STATUS_UNKNOWN PathStatus = iota
|
||||
PATH_STATUS_DIRECT
|
||||
PATH_STATUS_RELAY
|
||||
PATH_STATUS_FAILED
|
||||
// Path Status
|
||||
PATH_STATUS_UNKNOWN PathStatus = iota
|
||||
PATH_STATUS_DIRECT
|
||||
PATH_STATUS_RELAY
|
||||
PATH_STATUS_FAILED
|
||||
|
||||
// Common Constants
|
||||
DEFAULT_MTU = 1500
|
||||
MAX_PACKET_SIZE = 65535
|
||||
)
|
||||
// Resource Status
|
||||
RESOURCE_STATUS_PENDING = 0x00
|
||||
RESOURCE_STATUS_ACTIVE = 0x01
|
||||
RESOURCE_STATUS_COMPLETE = 0x02
|
||||
RESOURCE_STATUS_FAILED = 0x03
|
||||
RESOURCE_STATUS_CANCELLED = 0x04
|
||||
|
||||
// Link Status
|
||||
LINK_STATUS_PENDING = 0x00
|
||||
LINK_STATUS_ACTIVE = 0x01
|
||||
LINK_STATUS_CLOSED = 0x02
|
||||
LINK_STATUS_FAILED = 0x03
|
||||
|
||||
// Direction Constants
|
||||
IN = 0x01
|
||||
OUT = 0x02
|
||||
|
||||
// Common Constants
|
||||
DEFAULT_MTU = 1500
|
||||
MAX_PACKET_SIZE = 65535
|
||||
BITRATE_MINIMUM = 5
|
||||
|
||||
// Timeouts and Intervals
|
||||
ESTABLISH_TIMEOUT = 6
|
||||
KEEPALIVE_INTERVAL = 360
|
||||
STALE_TIME = 720
|
||||
PATH_REQUEST_TTL = 300
|
||||
ANNOUNCE_TIMEOUT = 15
|
||||
)
|
||||
|
||||
@@ -1,57 +1,211 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NetworkInterface combines both low-level and high-level interface requirements
|
||||
// NetworkInterface defines the interface for all network communication methods
|
||||
type NetworkInterface interface {
|
||||
// Low-level network operations
|
||||
Start() error
|
||||
Stop() error
|
||||
Send(data []byte, address string) error
|
||||
Receive() ([]byte, string, error)
|
||||
GetType() InterfaceType
|
||||
GetMode() InterfaceMode
|
||||
GetMTU() int
|
||||
|
||||
// High-level packet operations
|
||||
ProcessIncoming([]byte)
|
||||
ProcessOutgoing([]byte) error
|
||||
SendPathRequest([]byte) error
|
||||
SendLinkPacket([]byte, []byte, time.Time) error
|
||||
Detach()
|
||||
SetPacketCallback(PacketCallback)
|
||||
|
||||
// Additional required fields
|
||||
GetName() string
|
||||
GetConn() net.Conn
|
||||
IsEnabled() bool
|
||||
// Core interface operations
|
||||
Start() error
|
||||
Stop() error
|
||||
Enable()
|
||||
Disable()
|
||||
Detach()
|
||||
|
||||
// Network operations
|
||||
Send(data []byte, address string) error
|
||||
GetConn() net.Conn
|
||||
GetMTU() int
|
||||
GetName() string
|
||||
|
||||
// Interface properties
|
||||
GetType() InterfaceType
|
||||
GetMode() InterfaceMode
|
||||
IsEnabled() bool
|
||||
IsOnline() bool
|
||||
IsDetached() bool
|
||||
GetBandwidthAvailable() bool
|
||||
|
||||
// Packet handling
|
||||
ProcessIncoming([]byte)
|
||||
ProcessOutgoing([]byte) error
|
||||
SendPathRequest([]byte) error
|
||||
SendLinkPacket([]byte, []byte, time.Time) error
|
||||
SetPacketCallback(PacketCallback)
|
||||
GetPacketCallback() PacketCallback
|
||||
}
|
||||
|
||||
type PacketCallback func([]byte, interface{})
|
||||
|
||||
// BaseInterface provides common implementation
|
||||
// BaseInterface provides common implementation for network interfaces
|
||||
type BaseInterface struct {
|
||||
Name string
|
||||
Mode InterfaceMode
|
||||
Type InterfaceType
|
||||
|
||||
Online bool
|
||||
Detached bool
|
||||
|
||||
IN bool
|
||||
OUT bool
|
||||
|
||||
MTU int
|
||||
Bitrate int64
|
||||
|
||||
TxBytes uint64
|
||||
RxBytes uint64
|
||||
|
||||
mutex sync.RWMutex
|
||||
owner interface{}
|
||||
packetCallback PacketCallback
|
||||
}
|
||||
Name string
|
||||
Mode InterfaceMode
|
||||
Type InterfaceType
|
||||
Online bool
|
||||
Enabled bool
|
||||
Detached bool
|
||||
|
||||
IN bool
|
||||
OUT bool
|
||||
|
||||
MTU int
|
||||
Bitrate int64
|
||||
|
||||
TxBytes uint64
|
||||
RxBytes uint64
|
||||
lastTx time.Time
|
||||
|
||||
Mutex sync.RWMutex
|
||||
Owner interface{}
|
||||
PacketCallback PacketCallback
|
||||
}
|
||||
|
||||
// NewBaseInterface creates a new BaseInterface instance
|
||||
func NewBaseInterface(name string, ifaceType InterfaceType, enabled bool) BaseInterface {
|
||||
return BaseInterface{
|
||||
Name: name,
|
||||
Type: ifaceType,
|
||||
Mode: IF_MODE_FULL,
|
||||
Enabled: enabled,
|
||||
MTU: DEFAULT_MTU,
|
||||
Bitrate: BITRATE_MINIMUM,
|
||||
lastTx: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Default implementations for BaseInterface
|
||||
func (i *BaseInterface) GetType() InterfaceType {
|
||||
return i.Type
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetMode() InterfaceMode {
|
||||
return i.Mode
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetMTU() int {
|
||||
return i.MTU
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetName() string {
|
||||
return i.Name
|
||||
}
|
||||
|
||||
func (i *BaseInterface) IsEnabled() bool {
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
return i.Enabled && i.Online && !i.Detached
|
||||
}
|
||||
|
||||
func (i *BaseInterface) IsOnline() bool {
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
return i.Online
|
||||
}
|
||||
|
||||
func (i *BaseInterface) IsDetached() bool {
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
return i.Detached
|
||||
}
|
||||
|
||||
func (i *BaseInterface) SetPacketCallback(callback PacketCallback) {
|
||||
i.Mutex.Lock()
|
||||
defer i.Mutex.Unlock()
|
||||
i.PacketCallback = callback
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetPacketCallback() PacketCallback {
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
return i.PacketCallback
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Detach() {
|
||||
i.Mutex.Lock()
|
||||
defer i.Mutex.Unlock()
|
||||
i.Detached = true
|
||||
i.Online = false
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Enable() {
|
||||
i.Mutex.Lock()
|
||||
defer i.Mutex.Unlock()
|
||||
i.Enabled = true
|
||||
i.Online = true
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Disable() {
|
||||
i.Mutex.Lock()
|
||||
defer i.Mutex.Unlock()
|
||||
i.Enabled = false
|
||||
i.Online = false
|
||||
}
|
||||
|
||||
// Default implementations that should be overridden by specific interfaces
|
||||
func (i *BaseInterface) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetConn() net.Conn {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Send(data []byte, address string) error {
|
||||
return i.ProcessOutgoing(data)
|
||||
}
|
||||
|
||||
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
||||
if i.PacketCallback != nil {
|
||||
i.PacketCallback(data, i)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *BaseInterface) ProcessOutgoing(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *BaseInterface) SendPathRequest(data []byte) error {
|
||||
return i.Send(data, "")
|
||||
}
|
||||
|
||||
func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.Time) error {
|
||||
// Create link packet
|
||||
packet := make([]byte, 0, len(dest)+len(data)+9) // 1 byte type + dest + 8 byte timestamp
|
||||
packet = append(packet, 0x02) // Link packet type
|
||||
packet = append(packet, dest...)
|
||||
|
||||
// Add timestamp
|
||||
ts := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) // #nosec G115
|
||||
packet = append(packet, ts...)
|
||||
|
||||
// Add data
|
||||
packet = append(packet, data...)
|
||||
|
||||
return i.Send(packet, "")
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetBandwidthAvailable() bool {
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
|
||||
// If no transmission in last second, bandwidth is available
|
||||
if time.Since(i.lastTx) > time.Second {
|
||||
return true
|
||||
}
|
||||
|
||||
// Calculate current bandwidth usage
|
||||
bytesPerSec := float64(i.TxBytes) / time.Since(i.lastTx).Seconds()
|
||||
currentUsage := bytesPerSec * 8 // Convert to bits/sec
|
||||
|
||||
// Check if usage is below threshold (2% of total bitrate)
|
||||
maxUsage := float64(i.Bitrate) * 0.02 // 2% propagation rate
|
||||
return currentUsage < maxUsage
|
||||
}
|
||||
|
||||
@@ -4,33 +4,72 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Interface related types
|
||||
type InterfaceMode byte
|
||||
type InterfaceType byte
|
||||
|
||||
// Transport related types
|
||||
type TransportMode byte
|
||||
type PathStatus byte
|
||||
|
||||
// Common structs
|
||||
// Path represents routing information for a destination
|
||||
type Path struct {
|
||||
Interface NetworkInterface
|
||||
Address string
|
||||
Status PathStatus
|
||||
LastSeen time.Time
|
||||
NextHop []byte
|
||||
Hops uint8
|
||||
LastUpdated time.Time
|
||||
Interface NetworkInterface
|
||||
LastSeen time.Time
|
||||
NextHop []byte
|
||||
Hops uint8
|
||||
LastUpdated time.Time
|
||||
HopCount uint8
|
||||
}
|
||||
|
||||
// Common callbacks
|
||||
type ProofRequestedCallback func(interface{}) bool
|
||||
type ProofRequestedCallback func([]byte, []byte)
|
||||
type LinkEstablishedCallback func(interface{})
|
||||
type PacketCallback func([]byte, NetworkInterface)
|
||||
|
||||
// Request handler
|
||||
// RequestHandler manages path requests and responses
|
||||
type RequestHandler struct {
|
||||
Path string
|
||||
ResponseGenerator func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity interface{}, requestedAt int64) []byte
|
||||
AllowMode byte
|
||||
AllowedList [][]byte
|
||||
}
|
||||
}
|
||||
|
||||
// Interface types
|
||||
type InterfaceMode byte
|
||||
type InterfaceType byte
|
||||
|
||||
// RatchetIDReceiver holds ratchet ID information
|
||||
type RatchetIDReceiver struct {
|
||||
LatestRatchetID []byte
|
||||
}
|
||||
|
||||
// NetworkStats holds interface statistics
|
||||
type NetworkStats struct {
|
||||
BytesSent uint64
|
||||
BytesReceived uint64
|
||||
PacketsSent uint64
|
||||
PacketsReceived uint64
|
||||
LastUpdated time.Time
|
||||
}
|
||||
|
||||
// LinkStatus represents the current state of a link
|
||||
type LinkStatus struct {
|
||||
Established bool
|
||||
LastSeen time.Time
|
||||
RTT time.Duration
|
||||
Quality float64
|
||||
Hops uint8
|
||||
}
|
||||
|
||||
// PathRequest represents a path discovery request
|
||||
type PathRequest struct {
|
||||
DestinationHash []byte
|
||||
Tag []byte
|
||||
TTL int
|
||||
Recursive bool
|
||||
}
|
||||
|
||||
// PathResponse represents a path discovery response
|
||||
type PathResponse struct {
|
||||
DestinationHash []byte
|
||||
NextHop []byte
|
||||
Hops uint8
|
||||
Tag []byte
|
||||
}
|
||||
|
||||
@@ -1,49 +1,270 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"gopkg.in/yaml.v3"
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Identity struct {
|
||||
Name string `yaml:"name"`
|
||||
StoragePath string `yaml:"storage_path"`
|
||||
} `yaml:"identity"`
|
||||
Identity struct {
|
||||
Name string
|
||||
StoragePath string
|
||||
}
|
||||
|
||||
Interfaces []struct {
|
||||
Name string `yaml:"name"`
|
||||
Type string `yaml:"type"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
ListenPort int `yaml:"listen_port"`
|
||||
ListenIP string `yaml:"listen_ip"`
|
||||
KissFraming bool `yaml:"kiss_framing"`
|
||||
I2PTunneled bool `yaml:"i2p_tunneled"`
|
||||
} `yaml:"interfaces"`
|
||||
Interfaces []struct {
|
||||
Name string
|
||||
Type string
|
||||
Enabled bool
|
||||
ListenPort int
|
||||
ListenIP string
|
||||
KissFraming bool
|
||||
I2PTunneled bool
|
||||
}
|
||||
|
||||
Transport struct {
|
||||
AnnounceInterval int `yaml:"announce_interval"`
|
||||
PathRequestTimeout int `yaml:"path_request_timeout"`
|
||||
MaxHops int `yaml:"max_hops"`
|
||||
BitrateLimit int64 `yaml:"bitrate_limit"`
|
||||
} `yaml:"transport"`
|
||||
Transport struct {
|
||||
AnnounceInterval int
|
||||
PathRequestTimeout int
|
||||
MaxHops int
|
||||
BitrateLimit int64
|
||||
}
|
||||
|
||||
Logging struct {
|
||||
Level string `yaml:"level"`
|
||||
File string `yaml:"file"`
|
||||
} `yaml:"logging"`
|
||||
Logging struct {
|
||||
Level string
|
||||
File string
|
||||
}
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file, err := os.Open(path) // #nosec G304
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg := &Config{}
|
||||
scanner := bufio.NewScanner(file)
|
||||
var currentSection string
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
// Skip comments and empty lines
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle sections
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
currentSection = strings.Trim(line, "[]")
|
||||
|
||||
// If this is an interface section, append new interface
|
||||
if strings.HasPrefix(currentSection, "interface ") {
|
||||
cfg.Interfaces = append(cfg.Interfaces, struct {
|
||||
Name string
|
||||
Type string
|
||||
Enabled bool
|
||||
ListenPort int
|
||||
ListenIP string
|
||||
KissFraming bool
|
||||
I2PTunneled bool
|
||||
}{})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse key-value pairs
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
switch currentSection {
|
||||
case "identity":
|
||||
switch key {
|
||||
case "name":
|
||||
cfg.Identity.Name = value
|
||||
case "storage_path":
|
||||
cfg.Identity.StoragePath = value
|
||||
}
|
||||
|
||||
case "transport":
|
||||
switch key {
|
||||
case "announce_interval":
|
||||
cfg.Transport.AnnounceInterval, _ = strconv.Atoi(value)
|
||||
case "path_request_timeout":
|
||||
cfg.Transport.PathRequestTimeout, _ = strconv.Atoi(value)
|
||||
case "max_hops":
|
||||
cfg.Transport.MaxHops, _ = strconv.Atoi(value)
|
||||
case "bitrate_limit":
|
||||
cfg.Transport.BitrateLimit, _ = strconv.ParseInt(value, 10, 64)
|
||||
}
|
||||
|
||||
case "logging":
|
||||
switch key {
|
||||
case "level":
|
||||
cfg.Logging.Level = value
|
||||
case "file":
|
||||
cfg.Logging.File = value
|
||||
}
|
||||
|
||||
default:
|
||||
// Handle interface sections
|
||||
if strings.HasPrefix(currentSection, "interface ") && len(cfg.Interfaces) > 0 {
|
||||
iface := &cfg.Interfaces[len(cfg.Interfaces)-1]
|
||||
switch key {
|
||||
case "name":
|
||||
iface.Name = value
|
||||
case "type":
|
||||
iface.Type = value
|
||||
case "enabled":
|
||||
iface.Enabled = value == "true"
|
||||
case "listen_port":
|
||||
iface.ListenPort, _ = strconv.Atoi(value)
|
||||
case "listen_ip":
|
||||
iface.ListenIP = value
|
||||
case "kiss_framing":
|
||||
iface.KissFraming = value == "true"
|
||||
case "i2p_tunneled":
|
||||
iface.I2PTunneled = value == "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func SaveConfig(cfg *Config, path string) error {
|
||||
var builder strings.Builder
|
||||
|
||||
// Write Identity section
|
||||
builder.WriteString("[identity]\n")
|
||||
builder.WriteString(fmt.Sprintf("name = %s\n", cfg.Identity.Name))
|
||||
builder.WriteString(fmt.Sprintf("storage_path = %s\n\n", cfg.Identity.StoragePath))
|
||||
|
||||
// Write Transport section
|
||||
builder.WriteString("[transport]\n")
|
||||
builder.WriteString(fmt.Sprintf("announce_interval = %d\n", cfg.Transport.AnnounceInterval))
|
||||
builder.WriteString(fmt.Sprintf("path_request_timeout = %d\n", cfg.Transport.PathRequestTimeout))
|
||||
builder.WriteString(fmt.Sprintf("max_hops = %d\n", cfg.Transport.MaxHops))
|
||||
builder.WriteString(fmt.Sprintf("bitrate_limit = %d\n\n", cfg.Transport.BitrateLimit))
|
||||
|
||||
// Write Logging section
|
||||
builder.WriteString("[logging]\n")
|
||||
builder.WriteString(fmt.Sprintf("level = %s\n", cfg.Logging.Level))
|
||||
builder.WriteString(fmt.Sprintf("file = %s\n\n", cfg.Logging.File))
|
||||
|
||||
// Write Interface sections
|
||||
for _, iface := range cfg.Interfaces {
|
||||
builder.WriteString(fmt.Sprintf("[interface %s]\n", iface.Name))
|
||||
builder.WriteString(fmt.Sprintf("type = %s\n", iface.Type))
|
||||
builder.WriteString(fmt.Sprintf("enabled = %v\n", iface.Enabled))
|
||||
builder.WriteString(fmt.Sprintf("listen_port = %d\n", iface.ListenPort))
|
||||
builder.WriteString(fmt.Sprintf("listen_ip = %s\n", iface.ListenIP))
|
||||
builder.WriteString(fmt.Sprintf("kiss_framing = %v\n", iface.KissFraming))
|
||||
builder.WriteString(fmt.Sprintf("i2p_tunneled = %v\n\n", iface.I2PTunneled))
|
||||
}
|
||||
|
||||
return os.WriteFile(path, []byte(builder.String()), 0600) // #nosec G306
|
||||
}
|
||||
|
||||
func GetConfigDir() string {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
// Fallback to current directory if home directory cannot be determined
|
||||
return ".reticulum-go"
|
||||
}
|
||||
return filepath.Join(homeDir, ".reticulum-go")
|
||||
}
|
||||
|
||||
func GetDefaultConfigPath() string {
|
||||
return filepath.Join(GetConfigDir(), "config")
|
||||
}
|
||||
|
||||
func EnsureConfigDir() error {
|
||||
configDir := GetConfigDir()
|
||||
return os.MkdirAll(configDir, 0700) // #nosec G301
|
||||
}
|
||||
|
||||
func InitConfig() (*Config, error) {
|
||||
// Ensure config directory exists
|
||||
if err := EnsureConfigDir(); err != nil {
|
||||
return nil, fmt.Errorf("failed to create config directory: %v", err)
|
||||
}
|
||||
|
||||
configPath := GetDefaultConfigPath()
|
||||
|
||||
// Check if config file exists
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
// Create default config
|
||||
cfg := &Config{}
|
||||
|
||||
// Set default values
|
||||
cfg.Identity.Name = "reticulum-node"
|
||||
cfg.Identity.StoragePath = filepath.Join(GetConfigDir(), "storage")
|
||||
|
||||
cfg.Transport.AnnounceInterval = 300
|
||||
cfg.Transport.PathRequestTimeout = 15
|
||||
cfg.Transport.MaxHops = 8
|
||||
cfg.Transport.BitrateLimit = 1000000
|
||||
|
||||
cfg.Logging.Level = "info"
|
||||
cfg.Logging.File = filepath.Join(GetConfigDir(), "reticulum.log")
|
||||
|
||||
// Add default interfaces
|
||||
cfg.Interfaces = append(cfg.Interfaces, struct {
|
||||
Name string
|
||||
Type string
|
||||
Enabled bool
|
||||
ListenPort int
|
||||
ListenIP string
|
||||
KissFraming bool
|
||||
I2PTunneled bool
|
||||
}{
|
||||
Name: "Local UDP",
|
||||
Type: "UDPInterface",
|
||||
Enabled: true,
|
||||
ListenPort: 37697,
|
||||
ListenIP: "0.0.0.0",
|
||||
})
|
||||
|
||||
cfg.Interfaces = append(cfg.Interfaces, struct {
|
||||
Name string
|
||||
Type string
|
||||
Enabled bool
|
||||
ListenPort int
|
||||
ListenIP string
|
||||
KissFraming bool
|
||||
I2PTunneled bool
|
||||
}{
|
||||
Name: "Auto Discovery",
|
||||
Type: "AutoInterface",
|
||||
Enabled: true,
|
||||
ListenPort: 29717,
|
||||
})
|
||||
|
||||
// Save default config
|
||||
if err := SaveConfig(cfg, configPath); err != nil {
|
||||
return nil, fmt.Errorf("failed to save default config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Load config
|
||||
cfg, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
110
pkg/cryptography/aes.go
Normal file
110
pkg/cryptography/aes.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package cryptography
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
// AES256KeySize is the size of an AES-256 key in bytes.
|
||||
AES256KeySize = 32 // 256 bits
|
||||
)
|
||||
|
||||
// GenerateAES256Key generates a random AES-256 key.
|
||||
func GenerateAES256Key() ([]byte, error) {
|
||||
key := make([]byte, AES256KeySize)
|
||||
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// EncryptAES256CBC encrypts data using AES-256 in CBC mode.
|
||||
// The IV is prepended to the ciphertext.
|
||||
func EncryptAES256CBC(key, plaintext []byte) ([]byte, error) {
|
||||
if len(key) != AES256KeySize {
|
||||
return nil, errors.New("invalid key size: must be 32 bytes for AES-256")
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate a random IV.
|
||||
iv := make([]byte, aes.BlockSize)
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add PKCS7 padding.
|
||||
padding := aes.BlockSize - len(plaintext)%aes.BlockSize
|
||||
padtext := make([]byte, len(plaintext)+padding)
|
||||
copy(padtext, plaintext)
|
||||
for i := len(plaintext); i < len(padtext); i++ {
|
||||
padtext[i] = byte(padding)
|
||||
}
|
||||
|
||||
// Encrypt the data.
|
||||
mode := cipher.NewCBCEncrypter(block, iv) // #nosec G407
|
||||
ciphertext := make([]byte, len(padtext))
|
||||
mode.CryptBlocks(ciphertext, padtext)
|
||||
|
||||
// Prepend the IV to the ciphertext.
|
||||
return append(iv, ciphertext...), nil
|
||||
}
|
||||
|
||||
// DecryptAES256CBC decrypts data using AES-256 in CBC mode.
|
||||
// It assumes the IV is prepended to the ciphertext.
|
||||
func DecryptAES256CBC(key, ciphertext []byte) ([]byte, error) {
|
||||
if len(key) != AES256KeySize {
|
||||
return nil, errors.New("invalid key size: must be 32 bytes for AES-256")
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(ciphertext) < aes.BlockSize {
|
||||
return nil, errors.New("ciphertext is too short")
|
||||
}
|
||||
|
||||
// Extract the IV from the beginning of the ciphertext.
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
ciphertext = ciphertext[aes.BlockSize:]
|
||||
|
||||
if len(ciphertext)%aes.BlockSize != 0 {
|
||||
return nil, errors.New("ciphertext is not a multiple of the block size")
|
||||
}
|
||||
|
||||
// Decrypt the data.
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
plaintext := make([]byte, len(ciphertext))
|
||||
mode.CryptBlocks(plaintext, ciphertext)
|
||||
|
||||
// Remove PKCS7 padding.
|
||||
if len(plaintext) == 0 {
|
||||
return nil, errors.New("invalid padding: plaintext is empty")
|
||||
}
|
||||
|
||||
padding := int(plaintext[len(plaintext)-1])
|
||||
if padding > aes.BlockSize || padding == 0 {
|
||||
return nil, errors.New("invalid padding size")
|
||||
}
|
||||
if len(plaintext) < padding {
|
||||
return nil, errors.New("invalid padding: padding size is larger than plaintext")
|
||||
}
|
||||
|
||||
// Verify the padding bytes.
|
||||
for i := len(plaintext) - padding; i < len(plaintext); i++ {
|
||||
if plaintext[i] != byte(padding) {
|
||||
return nil, errors.New("invalid padding bytes")
|
||||
}
|
||||
}
|
||||
|
||||
return plaintext[:len(plaintext)-padding], nil
|
||||
}
|
||||
194
pkg/cryptography/aes_test.go
Normal file
194
pkg/cryptography/aes_test.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package cryptography
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateAES256Key(t *testing.T) {
|
||||
key, err := GenerateAES256Key()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateAES256Key failed: %v", err)
|
||||
}
|
||||
if len(key) != AES256KeySize {
|
||||
t.Errorf("Expected key size %d, got %d", AES256KeySize, len(key))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAES256CBCEncryptionDecryption(t *testing.T) {
|
||||
key, err := GenerateAES256Key()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate AES-256 key: %v", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
plaintext []byte
|
||||
}{
|
||||
{"ShortMessage", []byte("Hello")},
|
||||
{"BlockSizeMessage", []byte("This is 16 bytes")},
|
||||
{"LongMessage", []byte("This is a longer message that spans multiple AES blocks and tests the padding.")},
|
||||
{"EmptyMessage", []byte("")},
|
||||
{"SingleByte", []byte("A")},
|
||||
{"ExactlyTwoBlocks", []byte("This is exactly 32 bytes long!!!")},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ciphertext, err := EncryptAES256CBC(key, tc.plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("EncryptAES256CBC failed: %v", err)
|
||||
}
|
||||
|
||||
decrypted, err := DecryptAES256CBC(key, ciphertext)
|
||||
if err != nil {
|
||||
t.Fatalf("DecryptAES256CBC failed: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(tc.plaintext, decrypted) {
|
||||
t.Errorf("Decrypted text does not match original plaintext.\nGot: %q (%x)\nWant: %q (%x)",
|
||||
decrypted, decrypted, tc.plaintext, tc.plaintext)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAES256CBC_InvalidKeySize(t *testing.T) {
|
||||
plaintext := []byte("test message")
|
||||
|
||||
invalidKeys := [][]byte{
|
||||
make([]byte, 16), // AES-128
|
||||
make([]byte, 24), // AES-192
|
||||
make([]byte, 15), // Too short
|
||||
make([]byte, 33), // Too long
|
||||
nil, // Nil key
|
||||
}
|
||||
|
||||
for i, key := range invalidKeys {
|
||||
t.Run(fmt.Sprintf("InvalidKey_%d", i), func(t *testing.T) {
|
||||
_, err := EncryptAES256CBC(key, plaintext)
|
||||
if err == nil {
|
||||
t.Error("EncryptAES256CBC should have failed with invalid key size")
|
||||
}
|
||||
|
||||
// Test with some dummy ciphertext
|
||||
dummyCiphertext := make([]byte, 32) // Just enough for IV + one block
|
||||
rand.Read(dummyCiphertext)
|
||||
_, err = DecryptAES256CBC(key, dummyCiphertext)
|
||||
if err == nil {
|
||||
t.Error("DecryptAES256CBC should have failed with invalid key size")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func TestDecryptAES256CBCErrorCases(t *testing.T) {
|
||||
key, err := GenerateAES256Key()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate key: %v", err)
|
||||
}
|
||||
|
||||
t.Run("CiphertextTooShort", func(t *testing.T) {
|
||||
shortCiphertext := []byte{0x01, 0x02, 0x03} // Less than AES block size
|
||||
_, err := DecryptAES256CBC(key, shortCiphertext)
|
||||
if err == nil {
|
||||
t.Error("DecryptAES256CBC should have failed for ciphertext shorter than block size")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CiphertextNotMultipleOfBlockSize", func(t *testing.T) {
|
||||
iv := make([]byte, aes.BlockSize)
|
||||
rand.Read(iv)
|
||||
invalidCiphertext := append(iv, []byte{0x01, 0x02, 0x03}...) // IV + data not multiple of block size
|
||||
_, err := DecryptAES256CBC(key, invalidCiphertext)
|
||||
if err == nil {
|
||||
t.Error("DecryptAES256CBC should have failed for ciphertext not multiple of block size")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidPadding", func(t *testing.T) {
|
||||
// Create a valid ciphertext first
|
||||
plaintext := []byte("valid data")
|
||||
ciphertext, err := EncryptAES256CBC(key, plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test ciphertext: %v", err)
|
||||
}
|
||||
|
||||
// Corrupt the last byte (which affects padding)
|
||||
corruptedCiphertext := make([]byte, len(ciphertext))
|
||||
copy(corruptedCiphertext, ciphertext)
|
||||
corruptedCiphertext[len(corruptedCiphertext)-1] ^= 0xFF
|
||||
|
||||
_, err = DecryptAES256CBC(key, corruptedCiphertext)
|
||||
if err == nil {
|
||||
t.Error("DecryptAES256CBC should have failed for corrupted padding")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmptyPlaintextAfterDecryption", func(t *testing.T) {
|
||||
// This creates a ciphertext that decrypts to just padding
|
||||
key, _ := GenerateAES256Key()
|
||||
iv := make([]byte, aes.BlockSize)
|
||||
// A block of padding bytes
|
||||
paddedBlock := bytes.Repeat([]byte{byte(aes.BlockSize)}, aes.BlockSize)
|
||||
|
||||
block, _ := aes.NewCipher(key)
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
ciphertext := make([]byte, len(paddedBlock))
|
||||
mode.CryptBlocks(ciphertext, paddedBlock)
|
||||
|
||||
// Prepend IV
|
||||
fullCiphertext := append(iv, ciphertext...)
|
||||
|
||||
// This should decrypt to an empty slice, which is valid
|
||||
decrypted, err := DecryptAES256CBC(key, fullCiphertext)
|
||||
if err != nil {
|
||||
t.Errorf("DecryptAES256CBC failed for empty plaintext case: %v", err)
|
||||
}
|
||||
if len(decrypted) != 0 {
|
||||
t.Errorf("Expected empty plaintext, got %q", decrypted)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConstants(t *testing.T) {
|
||||
if AES256KeySize != 32 {
|
||||
t.Errorf("AES256KeySize should be 32, got %d", AES256KeySize)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAES256CBC(b *testing.B) {
|
||||
key, err := GenerateAES256Key()
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to generate key: %v", err)
|
||||
}
|
||||
|
||||
data := make([]byte, 1024) // 1KB of data
|
||||
rand.Read(data)
|
||||
|
||||
b.Run("Encrypt", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := EncryptAES256CBC(key, data)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ciphertext, _ := EncryptAES256CBC(key, data)
|
||||
b.Run("Decrypt", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := DecryptAES256CBC(key, ciphertext)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
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)
|
||||
}
|
||||
63
pkg/cryptography/curve25519_test.go
Normal file
63
pkg/cryptography/curve25519_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package cryptography
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
func TestGenerateKeyPair(t *testing.T) {
|
||||
priv1, pub1, err := GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyPair failed: %v", err)
|
||||
}
|
||||
|
||||
if len(priv1) != curve25519.ScalarSize {
|
||||
t.Errorf("Private key length is %d, want %d", len(priv1), curve25519.ScalarSize)
|
||||
}
|
||||
if len(pub1) != curve25519.PointSize {
|
||||
t.Errorf("Public key length is %d, want %d", len(pub1), curve25519.PointSize)
|
||||
}
|
||||
|
||||
// Generate another pair, should be different
|
||||
priv2, pub2, err := GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("Second GenerateKeyPair failed: %v", err)
|
||||
}
|
||||
if bytes.Equal(priv1, priv2) {
|
||||
t.Error("Generated private keys are identical")
|
||||
}
|
||||
if bytes.Equal(pub1, pub2) {
|
||||
t.Error("Generated public keys are identical")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveSharedSecret(t *testing.T) {
|
||||
privA, pubA, err := GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyPair A failed: %v", err)
|
||||
}
|
||||
privB, pubB, err := GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyPair B failed: %v", err)
|
||||
}
|
||||
|
||||
secretA, err := DeriveSharedSecret(privA, pubB)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveSharedSecret (A perspective) failed: %v", err)
|
||||
}
|
||||
|
||||
secretB, err := DeriveSharedSecret(privB, pubA)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveSharedSecret (B perspective) failed: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(secretA, secretB) {
|
||||
t.Errorf("Derived shared secrets do not match:\nSecret A: %x\nSecret B: %x", secretA, secretB)
|
||||
}
|
||||
|
||||
if len(secretA) != curve25519.PointSize { // Shared secret length
|
||||
t.Errorf("Shared secret length is %d, want %d", len(secretA), curve25519.PointSize)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
79
pkg/cryptography/ed25519_test.go
Normal file
79
pkg/cryptography/ed25519_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package cryptography
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateSigningKeyPair(t *testing.T) {
|
||||
pub1, priv1, err := GenerateSigningKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateSigningKeyPair failed: %v", err)
|
||||
}
|
||||
|
||||
if len(pub1) != ed25519.PublicKeySize {
|
||||
t.Errorf("Public key length is %d, want %d", len(pub1), ed25519.PublicKeySize)
|
||||
}
|
||||
if len(priv1) != ed25519.PrivateKeySize {
|
||||
t.Errorf("Private key length is %d, want %d", len(priv1), ed25519.PrivateKeySize)
|
||||
}
|
||||
|
||||
// Generate another pair, should be different
|
||||
pub2, priv2, err := GenerateSigningKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("Second GenerateSigningKeyPair failed: %v", err)
|
||||
}
|
||||
if pub1.Equal(pub2) {
|
||||
t.Error("Generated public keys are identical")
|
||||
}
|
||||
if priv1.Equal(priv2) {
|
||||
t.Error("Generated private keys are identical")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignAndVerify(t *testing.T) {
|
||||
pub, priv, err := GenerateSigningKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateSigningKeyPair failed: %v", err)
|
||||
}
|
||||
|
||||
message := []byte("This message needs to be signed.")
|
||||
|
||||
signature := Sign(priv, message)
|
||||
if len(signature) != ed25519.SignatureSize {
|
||||
t.Errorf("Signature length is %d, want %d", len(signature), ed25519.SignatureSize)
|
||||
}
|
||||
|
||||
// Verify correct signature
|
||||
if !Verify(pub, message, signature) {
|
||||
t.Errorf("Verify failed for a valid signature")
|
||||
}
|
||||
|
||||
// Verify with tampered message
|
||||
tamperedMessage := append(message, '!')
|
||||
if Verify(pub, tamperedMessage, signature) {
|
||||
t.Errorf("Verify succeeded for a tampered message")
|
||||
}
|
||||
|
||||
// Verify with tampered signature
|
||||
tamperedSignature := append(signature[:len(signature)-1], ^signature[len(signature)-1])
|
||||
if Verify(pub, message, tamperedSignature) {
|
||||
t.Errorf("Verify succeeded for a tampered signature")
|
||||
}
|
||||
|
||||
// Verify with wrong public key
|
||||
wrongPub, _, _ := GenerateSigningKeyPair()
|
||||
if Verify(wrongPub, message, signature) {
|
||||
t.Errorf("Verify succeeded with the wrong public key")
|
||||
}
|
||||
|
||||
// Verify empty message
|
||||
emptyMessage := []byte("")
|
||||
emptySig := Sign(priv, emptyMessage)
|
||||
if !Verify(pub, emptyMessage, emptySig) {
|
||||
t.Errorf("Verify failed for an empty message")
|
||||
}
|
||||
if Verify(pub, message, emptySig) {
|
||||
t.Errorf("Verify succeeded comparing non-empty message with empty signature")
|
||||
}
|
||||
}
|
||||
17
pkg/cryptography/hkdf.go
Normal file
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
|
||||
}
|
||||
108
pkg/cryptography/hkdf_test.go
Normal file
108
pkg/cryptography/hkdf_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package cryptography
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeriveKey(t *testing.T) {
|
||||
secret := []byte("test-secret")
|
||||
salt := []byte("test-salt")
|
||||
info := []byte("test-info")
|
||||
length := 32 // Desired key length
|
||||
|
||||
key1, err := DeriveKey(secret, salt, info, length)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKey failed: %v", err)
|
||||
}
|
||||
|
||||
if len(key1) != length {
|
||||
t.Errorf("DeriveKey returned key of length %d; want %d", len(key1), length)
|
||||
}
|
||||
|
||||
// Derive another key with the same parameters, should be identical
|
||||
key2, err := DeriveKey(secret, salt, info, length)
|
||||
if err != nil {
|
||||
t.Fatalf("Second DeriveKey failed: %v", err)
|
||||
}
|
||||
if !bytes.Equal(key1, key2) {
|
||||
t.Errorf("DeriveKey is not deterministic. Got %x and %x for the same inputs", key1, key2)
|
||||
}
|
||||
|
||||
// Derive a key with different info, should be different
|
||||
differentInfo := []byte("different-info")
|
||||
key3, err := DeriveKey(secret, salt, differentInfo, length)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKey with different info failed: %v", err)
|
||||
}
|
||||
if bytes.Equal(key1, key3) {
|
||||
t.Errorf("DeriveKey produced the same key for different info strings")
|
||||
}
|
||||
|
||||
// Derive a key with different salt, should be different
|
||||
differentSalt := []byte("different-salt")
|
||||
key4, err := DeriveKey(secret, differentSalt, info, length)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKey with different salt failed: %v", err)
|
||||
}
|
||||
if bytes.Equal(key1, key4) {
|
||||
t.Errorf("DeriveKey produced the same key for different salts")
|
||||
}
|
||||
|
||||
// Derive a key with different secret, should be different
|
||||
differentSecret := []byte("different-secret")
|
||||
key5, err := DeriveKey(differentSecret, salt, info, length)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKey with different secret failed: %v", err)
|
||||
}
|
||||
if bytes.Equal(key1, key5) {
|
||||
t.Errorf("DeriveKey produced the same key for different secrets")
|
||||
}
|
||||
|
||||
// Derive a key with different length
|
||||
differentLength := 64
|
||||
key6, err := DeriveKey(secret, salt, info, differentLength)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKey with different length failed: %v", err)
|
||||
}
|
||||
if len(key6) != differentLength {
|
||||
t.Errorf("DeriveKey returned key of length %d; want %d", len(key6), differentLength)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveKeyEdgeCases(t *testing.T) {
|
||||
secret := []byte("test-secret")
|
||||
salt := []byte("test-salt")
|
||||
info := []byte("test-info")
|
||||
|
||||
t.Run("EmptySecret", func(t *testing.T) {
|
||||
_, err := DeriveKey([]byte{}, salt, info, 32)
|
||||
if err != nil {
|
||||
t.Errorf("DeriveKey failed with empty secret: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmptySalt", func(t *testing.T) {
|
||||
_, err := DeriveKey(secret, []byte{}, info, 32)
|
||||
if err != nil {
|
||||
t.Errorf("DeriveKey failed with empty salt: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmptyInfo", func(t *testing.T) {
|
||||
_, err := DeriveKey(secret, salt, []byte{}, 32)
|
||||
if err != nil {
|
||||
t.Errorf("DeriveKey failed with empty info: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ZeroLength", func(t *testing.T) {
|
||||
key, err := DeriveKey(secret, salt, info, 0)
|
||||
if err != nil {
|
||||
t.Errorf("DeriveKey failed with zero length: %v", err)
|
||||
}
|
||||
if len(key) != 0 {
|
||||
t.Errorf("DeriveKey with zero length returned non-empty key: %x", key)
|
||||
}
|
||||
})
|
||||
}
|
||||
26
pkg/cryptography/hmac.go
Normal file
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)
|
||||
}
|
||||
80
pkg/cryptography/hmac_test.go
Normal file
80
pkg/cryptography/hmac_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package cryptography
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateHMACKey(t *testing.T) {
|
||||
testSizes := []int{16, 32, 64}
|
||||
for _, size := range testSizes {
|
||||
t.Run("Size"+string(rune(size)), func(t *testing.T) { // Simple name conversion
|
||||
key, err := GenerateHMACKey(size)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateHMACKey(%d) failed: %v", size, err)
|
||||
}
|
||||
if len(key) != size {
|
||||
t.Errorf("GenerateHMACKey(%d) returned key of length %d; want %d", size, len(key), size)
|
||||
}
|
||||
|
||||
// Check if key is not all zeros (basic check for randomness)
|
||||
isZero := true
|
||||
for _, b := range key {
|
||||
if b != 0 {
|
||||
isZero = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if isZero {
|
||||
t.Errorf("GenerateHMACKey(%d) returned an all-zero key", size)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeAndValidateHMAC(t *testing.T) {
|
||||
key, err := GenerateHMACKey(32) // Use SHA256 key size
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate HMAC key: %v", err)
|
||||
}
|
||||
|
||||
message := []byte("This is a test message.")
|
||||
|
||||
// Compute HMAC
|
||||
computedHMAC := ComputeHMAC(key, message)
|
||||
if len(computedHMAC) != 32 { // SHA256 output size
|
||||
t.Errorf("ComputeHMAC returned HMAC of length %d; want 32", len(computedHMAC))
|
||||
}
|
||||
|
||||
// Validate correct HMAC
|
||||
if !ValidateHMAC(key, message, computedHMAC) {
|
||||
t.Errorf("ValidateHMAC failed for correctly computed HMAC")
|
||||
}
|
||||
|
||||
// Validate incorrect HMAC (tampered message)
|
||||
tamperedMessage := append(message, byte('!'))
|
||||
if ValidateHMAC(key, tamperedMessage, computedHMAC) {
|
||||
t.Errorf("ValidateHMAC succeeded for tampered message")
|
||||
}
|
||||
|
||||
// Validate incorrect HMAC (tampered key)
|
||||
wrongKey, _ := GenerateHMACKey(32)
|
||||
if ValidateHMAC(wrongKey, message, computedHMAC) {
|
||||
t.Errorf("ValidateHMAC succeeded for incorrect key")
|
||||
}
|
||||
|
||||
// Validate incorrect HMAC (tampered HMAC)
|
||||
tamperedHMAC := append(computedHMAC[:len(computedHMAC)-1], ^computedHMAC[len(computedHMAC)-1])
|
||||
if ValidateHMAC(key, message, tamperedHMAC) {
|
||||
t.Errorf("ValidateHMAC succeeded for tampered HMAC")
|
||||
}
|
||||
|
||||
// Validate empty message
|
||||
emptyMessage := []byte("")
|
||||
emptyHMAC := ComputeHMAC(key, emptyMessage)
|
||||
if !ValidateHMAC(key, emptyMessage, emptyHMAC) {
|
||||
t.Errorf("ValidateHMAC failed for empty message")
|
||||
}
|
||||
if ValidateHMAC(key, message, emptyHMAC) {
|
||||
t.Errorf("ValidateHMAC succeeded comparing non-empty message with empty HMAC")
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,12 @@ package destination
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/announce"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
||||
@@ -30,6 +31,15 @@ const (
|
||||
|
||||
RATCHET_COUNT = 512 // Default number of retained ratchet keys
|
||||
RATCHET_INTERVAL = 1800 // Minimum interval between ratchet rotations in seconds
|
||||
|
||||
// Debug levels
|
||||
DEBUG_CRITICAL = 1 // Critical errors
|
||||
DEBUG_ERROR = 2 // Non-critical errors
|
||||
DEBUG_INFO = 3 // Important information
|
||||
DEBUG_VERBOSE = 4 // Detailed information
|
||||
DEBUG_TRACE = 5 // Very detailed tracing
|
||||
DEBUG_PACKETS = 6 // Packet-level details
|
||||
DEBUG_ALL = 7 // Everything
|
||||
)
|
||||
|
||||
type PacketCallback = common.PacketCallback
|
||||
@@ -44,39 +54,42 @@ type RequestHandler struct {
|
||||
}
|
||||
|
||||
type Destination struct {
|
||||
identity *identity.Identity
|
||||
direction byte
|
||||
destType byte
|
||||
appName string
|
||||
aspects []string
|
||||
hash []byte
|
||||
|
||||
acceptsLinks bool
|
||||
proofStrategy byte
|
||||
|
||||
packetCallback PacketCallback
|
||||
proofCallback ProofRequestedCallback
|
||||
linkCallback LinkEstablishedCallback
|
||||
|
||||
identity *identity.Identity
|
||||
direction byte
|
||||
destType byte
|
||||
appName string
|
||||
aspects []string
|
||||
hashValue []byte
|
||||
transport *transport.Transport
|
||||
|
||||
acceptsLinks bool
|
||||
proofStrategy byte
|
||||
|
||||
packetCallback PacketCallback
|
||||
proofCallback ProofRequestedCallback
|
||||
linkCallback LinkEstablishedCallback
|
||||
|
||||
ratchetsEnabled bool
|
||||
ratchetPath string
|
||||
ratchetCount int
|
||||
ratchetInterval int
|
||||
enforceRatchets bool
|
||||
|
||||
defaultAppData []byte
|
||||
|
||||
defaultAppData []byte
|
||||
mutex sync.RWMutex
|
||||
|
||||
requestHandlers map[string]*RequestHandler
|
||||
callbacks struct {
|
||||
packetReceived common.PacketCallback
|
||||
proofRequested common.ProofRequestedCallback
|
||||
linkEstablished common.LinkEstablishedCallback
|
||||
}
|
||||
}
|
||||
|
||||
func New(id *identity.Identity, direction byte, destType byte, appName string, aspects ...string) (*Destination, error) {
|
||||
func debugLog(level int, format string, v ...interface{}) {
|
||||
log.Printf("[DEBUG-%d] %s", level, fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func New(id *identity.Identity, direction byte, destType byte, appName string, transport *transport.Transport, aspects ...string) (*Destination, error) {
|
||||
debugLog(DEBUG_INFO, "Creating new destination: app=%s type=%d direction=%d", appName, destType, direction)
|
||||
|
||||
if id == nil {
|
||||
debugLog(DEBUG_ERROR, "Cannot create destination: identity is nil")
|
||||
return nil, errors.New("identity cannot be nil")
|
||||
}
|
||||
|
||||
@@ -86,6 +99,7 @@ func New(id *identity.Identity, direction byte, destType byte, appName string, a
|
||||
destType: destType,
|
||||
appName: appName,
|
||||
aspects: aspects,
|
||||
transport: transport,
|
||||
acceptsLinks: false,
|
||||
proofStrategy: PROVE_NONE,
|
||||
ratchetCount: RATCHET_COUNT,
|
||||
@@ -94,19 +108,28 @@ func New(id *identity.Identity, direction byte, destType byte, appName string, a
|
||||
}
|
||||
|
||||
// Generate destination hash
|
||||
d.hash = d.Hash()
|
||||
|
||||
d.hashValue = d.calculateHash()
|
||||
debugLog(DEBUG_VERBOSE, "Created destination with hash: %x", d.hashValue)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *Destination) Hash() []byte {
|
||||
func (d *Destination) calculateHash() []byte {
|
||||
debugLog(DEBUG_TRACE, "Calculating hash for destination %s", d.ExpandName())
|
||||
|
||||
nameHash := sha256.Sum256([]byte(d.ExpandName()))
|
||||
identityHash := sha256.Sum256(d.identity.GetPublicKey())
|
||||
|
||||
|
||||
debugLog(DEBUG_ALL, "Name hash: %x", nameHash)
|
||||
debugLog(DEBUG_ALL, "Identity hash: %x", identityHash)
|
||||
|
||||
combined := append(nameHash[:], identityHash[:]...)
|
||||
finalHash := sha256.Sum256(combined)
|
||||
|
||||
return finalHash[:16] // Truncated to 128 bits
|
||||
|
||||
truncated := finalHash[:16]
|
||||
debugLog(DEBUG_VERBOSE, "Calculated destination hash: %x", truncated)
|
||||
|
||||
return truncated
|
||||
}
|
||||
|
||||
func (d *Destination) ExpandName() string {
|
||||
@@ -121,70 +144,42 @@ func (d *Destination) Announce(appData []byte) error {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
// If no specific appData provided, use default
|
||||
log.Printf("[DEBUG-4] Announcing destination %s", d.ExpandName())
|
||||
|
||||
if appData == nil {
|
||||
appData = d.defaultAppData
|
||||
}
|
||||
|
||||
// Create announce packet
|
||||
packet := make([]byte, 0)
|
||||
|
||||
// Add destination hash
|
||||
packet = append(packet, d.hash...)
|
||||
|
||||
// Add identity public key
|
||||
packet = append(packet, d.identity.GetPublicKey()...)
|
||||
|
||||
// Add flags byte
|
||||
flags := byte(0)
|
||||
if d.acceptsLinks {
|
||||
flags |= 0x01
|
||||
}
|
||||
if d.ratchetsEnabled {
|
||||
flags |= 0x02
|
||||
}
|
||||
packet = append(packet, flags)
|
||||
|
||||
// Add proof strategy
|
||||
packet = append(packet, d.proofStrategy)
|
||||
|
||||
// Add app data length and data if present
|
||||
if appData != nil {
|
||||
appDataLen := uint16(len(appData))
|
||||
lenBytes := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(lenBytes, appDataLen)
|
||||
packet = append(packet, lenBytes...)
|
||||
packet = append(packet, appData...)
|
||||
} else {
|
||||
// No app data
|
||||
packet = append(packet, 0x00, 0x00)
|
||||
}
|
||||
|
||||
// Add ratchet data if enabled
|
||||
if d.ratchetsEnabled {
|
||||
// Add ratchet interval
|
||||
intervalBytes := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(intervalBytes, uint32(d.ratchetInterval))
|
||||
packet = append(packet, intervalBytes...)
|
||||
|
||||
// Add current ratchet key
|
||||
ratchetKey := d.identity.GetCurrentRatchetKey()
|
||||
if ratchetKey == nil {
|
||||
return errors.New("failed to get current ratchet key")
|
||||
}
|
||||
packet = append(packet, ratchetKey...)
|
||||
}
|
||||
|
||||
// Sign the announce packet
|
||||
signature, err := d.Sign(packet)
|
||||
// Create a new Announce instance
|
||||
announce, err := announce.New(d.identity, appData, false, d.transport.GetConfig())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sign announce packet: %w", err)
|
||||
return fmt.Errorf("failed to create announce: %w", err)
|
||||
}
|
||||
packet = append(packet, signature...)
|
||||
|
||||
// Send announce packet through transport layer
|
||||
// This will need to be implemented in the transport package
|
||||
return transport.SendAnnounce(packet)
|
||||
// Get the packet from the announce instance
|
||||
packet := announce.GetPacket()
|
||||
if packet == nil {
|
||||
return errors.New("failed to create announce packet")
|
||||
}
|
||||
|
||||
// Send announce packet to all interfaces
|
||||
log.Printf("[DEBUG-4] Sending announce packet to all interfaces")
|
||||
if d.transport == nil {
|
||||
return errors.New("transport not initialized")
|
||||
}
|
||||
|
||||
interfaces := d.transport.GetInterfaces()
|
||||
var lastErr error
|
||||
for _, iface := range interfaces {
|
||||
if iface.IsEnabled() && iface.IsOnline() {
|
||||
if err := iface.Send(packet, ""); err != nil {
|
||||
log.Printf("[ERROR] Failed to send announce on interface %s: %v", iface.GetName(), err)
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (d *Destination) AcceptsLinks(accepts bool) {
|
||||
@@ -220,7 +215,7 @@ func (d *Destination) SetProofStrategy(strategy byte) {
|
||||
func (d *Destination) EnableRatchets(path string) bool {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
|
||||
d.ratchetsEnabled = true
|
||||
d.ratchetPath = path
|
||||
return true
|
||||
@@ -236,7 +231,7 @@ func (d *Destination) SetRetainedRatchets(count int) bool {
|
||||
if count < 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
d.ratchetCount = count
|
||||
@@ -247,7 +242,7 @@ func (d *Destination) SetRatchetInterval(interval int) bool {
|
||||
if interval < 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
d.ratchetInterval = interval
|
||||
@@ -275,7 +270,7 @@ func (d *Destination) RegisterRequestHandler(path string, responseGen func(strin
|
||||
return errors.New("invalid allow mode")
|
||||
}
|
||||
|
||||
if allow == ALLOW_LIST && (allowedList == nil || len(allowedList) == 0) {
|
||||
if allow == ALLOW_LIST && len(allowedList) == 0 {
|
||||
return errors.New("allowed list required for ALLOW_LIST mode")
|
||||
}
|
||||
|
||||
@@ -305,19 +300,32 @@ func (d *Destination) DeregisterRequestHandler(path string) bool {
|
||||
|
||||
func (d *Destination) Encrypt(plaintext []byte) ([]byte, error) {
|
||||
if d.destType == PLAIN {
|
||||
log.Printf("[DEBUG-4] Using plaintext transmission for PLAIN destination")
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
if d.identity == nil {
|
||||
log.Printf("[DEBUG-3] Cannot encrypt: no identity available")
|
||||
return nil, errors.New("no identity available for encryption")
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-4] Encrypting %d bytes for destination type %d", len(plaintext), d.destType)
|
||||
|
||||
switch d.destType {
|
||||
case SINGLE:
|
||||
return d.identity.Encrypt(plaintext, nil)
|
||||
recipientKey := d.identity.GetPublicKey()
|
||||
log.Printf("[DEBUG-4] Encrypting for single recipient with key %x", recipientKey[:8])
|
||||
return d.identity.Encrypt(plaintext, recipientKey)
|
||||
case GROUP:
|
||||
return d.identity.EncryptSymmetric(plaintext)
|
||||
key := d.identity.GetCurrentRatchetKey()
|
||||
if key == nil {
|
||||
log.Printf("[DEBUG-3] Cannot encrypt: no ratchet key available")
|
||||
return nil, errors.New("no ratchet key available")
|
||||
}
|
||||
log.Printf("[DEBUG-4] Encrypting for group with ratchet key %x", key[:8])
|
||||
return d.identity.EncryptWithHMAC(plaintext, key)
|
||||
default:
|
||||
log.Printf("[DEBUG-3] Unsupported destination type %d for encryption", d.destType)
|
||||
return nil, errors.New("unsupported destination type for encryption")
|
||||
}
|
||||
}
|
||||
@@ -331,14 +339,15 @@ func (d *Destination) Decrypt(ciphertext []byte) ([]byte, error) {
|
||||
return nil, errors.New("no identity available for decryption")
|
||||
}
|
||||
|
||||
switch d.destType {
|
||||
case SINGLE:
|
||||
return d.identity.Decrypt(ciphertext, nil)
|
||||
case GROUP:
|
||||
return d.identity.DecryptSymmetric(ciphertext)
|
||||
default:
|
||||
return nil, errors.New("unsupported destination type for decryption")
|
||||
}
|
||||
// Create empty ratchet receiver to get latest ratchet ID if available
|
||||
ratchetReceiver := &common.RatchetIDReceiver{}
|
||||
|
||||
// Call Decrypt with full parameter list:
|
||||
// - ciphertext: the encrypted data
|
||||
// - ratchets: nil since we're not providing specific ratchets
|
||||
// - enforceRatchets: false to allow fallback to normal decryption
|
||||
// - ratchetIDReceiver: to receive the latest ratchet ID used
|
||||
return d.identity.Decrypt(ciphertext, nil, false, ratchetReceiver)
|
||||
}
|
||||
|
||||
func (d *Destination) Sign(data []byte) ([]byte, error) {
|
||||
@@ -347,4 +356,33 @@ func (d *Destination) Sign(data []byte) ([]byte, error) {
|
||||
}
|
||||
signature := d.identity.Sign(data)
|
||||
return signature, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Destination) GetPublicKey() []byte {
|
||||
if d.identity == nil {
|
||||
return nil
|
||||
}
|
||||
return d.identity.GetPublicKey()
|
||||
}
|
||||
|
||||
func (d *Destination) GetIdentity() *identity.Identity {
|
||||
return d.identity
|
||||
}
|
||||
|
||||
func (d *Destination) GetType() byte {
|
||||
return d.destType
|
||||
}
|
||||
|
||||
func (d *Destination) GetHash() []byte {
|
||||
d.mutex.RLock()
|
||||
defer d.mutex.RUnlock()
|
||||
if d.hashValue == nil {
|
||||
d.mutex.RUnlock()
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
if d.hashValue == nil {
|
||||
d.hashValue = d.calculateHash()
|
||||
}
|
||||
}
|
||||
return d.hashValue
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
275
pkg/interfaces/auto.go
Normal file
275
pkg/interfaces/auto.go
Normal file
@@ -0,0 +1,275 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
)
|
||||
|
||||
const (
|
||||
DEFAULT_DISCOVERY_PORT = 29716
|
||||
DEFAULT_DATA_PORT = 42671
|
||||
BITRATE_GUESS = 10 * 1000 * 1000
|
||||
PEERING_TIMEOUT = 7500 * time.Millisecond
|
||||
SCOPE_LINK = "2"
|
||||
SCOPE_ADMIN = "4"
|
||||
SCOPE_SITE = "5"
|
||||
SCOPE_ORGANISATION = "8"
|
||||
SCOPE_GLOBAL = "e"
|
||||
)
|
||||
|
||||
type AutoInterface struct {
|
||||
BaseInterface
|
||||
groupID []byte
|
||||
discoveryPort int
|
||||
dataPort int
|
||||
discoveryScope string
|
||||
peers map[string]*Peer
|
||||
linkLocalAddrs []string
|
||||
adoptedInterfaces map[string]string
|
||||
interfaceServers map[string]*net.UDPConn
|
||||
multicastEchoes map[string]time.Time
|
||||
mutex sync.RWMutex
|
||||
outboundConn *net.UDPConn
|
||||
}
|
||||
|
||||
type Peer struct {
|
||||
ifaceName string
|
||||
lastHeard time.Time
|
||||
conn *net.UDPConn
|
||||
}
|
||||
|
||||
func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterface, error) {
|
||||
ai := &AutoInterface{
|
||||
BaseInterface: BaseInterface{
|
||||
Name: name,
|
||||
Mode: common.IF_MODE_FULL,
|
||||
Type: common.IF_TYPE_AUTO,
|
||||
Online: false,
|
||||
Enabled: config.Enabled,
|
||||
Detached: false,
|
||||
IN: false,
|
||||
OUT: false,
|
||||
MTU: common.DEFAULT_MTU,
|
||||
Bitrate: BITRATE_MINIMUM,
|
||||
},
|
||||
discoveryPort: DEFAULT_DISCOVERY_PORT,
|
||||
dataPort: DEFAULT_DATA_PORT,
|
||||
discoveryScope: SCOPE_LINK,
|
||||
peers: make(map[string]*Peer),
|
||||
linkLocalAddrs: make([]string, 0),
|
||||
adoptedInterfaces: make(map[string]string),
|
||||
interfaceServers: make(map[string]*net.UDPConn),
|
||||
multicastEchoes: make(map[string]time.Time),
|
||||
}
|
||||
|
||||
if config.Port != 0 {
|
||||
ai.discoveryPort = config.Port
|
||||
}
|
||||
|
||||
if config.GroupID != "" {
|
||||
ai.groupID = []byte(config.GroupID)
|
||||
} else {
|
||||
ai.groupID = []byte("reticulum")
|
||||
}
|
||||
|
||||
return ai, nil
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) Start() error {
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list interfaces: %v", err)
|
||||
}
|
||||
|
||||
for _, iface := range interfaces {
|
||||
if err := ai.configureInterface(&iface); err != nil {
|
||||
log.Printf("Failed to configure interface %s: %v", iface.Name, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(ai.adoptedInterfaces) == 0 {
|
||||
return fmt.Errorf("no suitable interfaces found")
|
||||
}
|
||||
|
||||
go ai.peerJobs()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) configureInterface(iface *net.Interface) error {
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.IsLinkLocalUnicast() {
|
||||
ai.adoptedInterfaces[iface.Name] = ipnet.IP.String()
|
||||
ai.multicastEchoes[iface.Name] = time.Now()
|
||||
|
||||
if err := ai.startDiscoveryListener(iface); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ai.startDataListener(iface); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) startDiscoveryListener(iface *net.Interface) error {
|
||||
addr := &net.UDPAddr{
|
||||
IP: net.ParseIP(fmt.Sprintf("ff%s%s::1", ai.discoveryScope, SCOPE_LINK)),
|
||||
Port: ai.discoveryPort,
|
||||
Zone: iface.Name,
|
||||
}
|
||||
|
||||
conn, err := net.ListenMulticastUDP("udp6", iface, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go ai.handleDiscovery(conn, iface.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) startDataListener(iface *net.Interface) error {
|
||||
addr := &net.UDPAddr{
|
||||
IP: net.IPv6zero,
|
||||
Port: ai.dataPort,
|
||||
Zone: iface.Name,
|
||||
}
|
||||
|
||||
conn, err := net.ListenUDP("udp6", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ai.interfaceServers[iface.Name] = conn
|
||||
go ai.handleData(conn)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) {
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
_, remoteAddr, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
log.Printf("Discovery read error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
ai.handlePeerAnnounce(remoteAddr, ifaceName)
|
||||
}
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) handleData(conn *net.UDPConn) {
|
||||
buf := make([]byte, ai.GetMTU())
|
||||
for {
|
||||
n, _, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
if !ai.IsDetached() {
|
||||
log.Printf("Data read error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if callback := ai.GetPacketCallback(); callback != nil {
|
||||
callback(buf[:n], ai)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) handlePeerAnnounce(addr *net.UDPAddr, ifaceName string) {
|
||||
ai.mutex.Lock()
|
||||
defer ai.mutex.Unlock()
|
||||
|
||||
peerAddr := addr.IP.String()
|
||||
|
||||
for _, localAddr := range ai.linkLocalAddrs {
|
||||
if peerAddr == localAddr {
|
||||
ai.multicastEchoes[ifaceName] = time.Now()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, exists := ai.peers[peerAddr]; !exists {
|
||||
ai.peers[peerAddr] = &Peer{
|
||||
ifaceName: ifaceName,
|
||||
lastHeard: time.Now(),
|
||||
}
|
||||
log.Printf("Added peer %s on %s", peerAddr, ifaceName)
|
||||
} else {
|
||||
ai.peers[peerAddr].lastHeard = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) peerJobs() {
|
||||
ticker := time.NewTicker(PEERING_TIMEOUT)
|
||||
for range ticker.C {
|
||||
ai.mutex.Lock()
|
||||
now := time.Now()
|
||||
|
||||
for addr, peer := range ai.peers {
|
||||
if now.Sub(peer.lastHeard) > PEERING_TIMEOUT {
|
||||
delete(ai.peers, addr)
|
||||
log.Printf("Removed timed out peer %s", addr)
|
||||
}
|
||||
}
|
||||
|
||||
ai.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) Send(data []byte, address string) error {
|
||||
ai.mutex.RLock()
|
||||
defer ai.mutex.RUnlock()
|
||||
|
||||
for _, peer := range ai.peers {
|
||||
addr := &net.UDPAddr{
|
||||
IP: net.ParseIP(address),
|
||||
Port: ai.dataPort,
|
||||
Zone: peer.ifaceName,
|
||||
}
|
||||
|
||||
if ai.outboundConn == nil {
|
||||
var err error
|
||||
ai.outboundConn, err = net.ListenUDP("udp6", &net.UDPAddr{Port: 0})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := ai.outboundConn.WriteToUDP(data, addr); err != nil {
|
||||
log.Printf("Failed to send to peer %s: %v", address, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) Stop() error {
|
||||
ai.mutex.Lock()
|
||||
defer ai.mutex.Unlock()
|
||||
|
||||
for _, server := range ai.interfaceServers {
|
||||
server.Close() // #nosec G104
|
||||
}
|
||||
|
||||
if ai.outboundConn != nil {
|
||||
ai.outboundConn.Close() // #nosec G104
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
290
pkg/interfaces/auto_test.go
Normal file
290
pkg/interfaces/auto_test.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
)
|
||||
|
||||
func TestNewAutoInterface(t *testing.T) {
|
||||
t.Run("DefaultConfig", func(t *testing.T) {
|
||||
config := &common.InterfaceConfig{Enabled: true}
|
||||
ai, err := NewAutoInterface("autoDefault", config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAutoInterface failed with default config: %v", err)
|
||||
}
|
||||
if ai == nil {
|
||||
t.Fatal("NewAutoInterface returned nil with default config")
|
||||
}
|
||||
|
||||
if ai.GetName() != "autoDefault" {
|
||||
t.Errorf("GetName() = %s; want autoDefault", ai.GetName())
|
||||
}
|
||||
if ai.GetType() != common.IF_TYPE_AUTO {
|
||||
t.Errorf("GetType() = %v; want %v", ai.GetType(), common.IF_TYPE_AUTO)
|
||||
}
|
||||
if ai.discoveryPort != DEFAULT_DISCOVERY_PORT {
|
||||
t.Errorf("discoveryPort = %d; want %d", ai.discoveryPort, DEFAULT_DISCOVERY_PORT)
|
||||
}
|
||||
if ai.dataPort != DEFAULT_DATA_PORT {
|
||||
t.Errorf("dataPort = %d; want %d", ai.dataPort, DEFAULT_DATA_PORT)
|
||||
}
|
||||
if string(ai.groupID) != "reticulum" {
|
||||
t.Errorf("groupID = %s; want reticulum", string(ai.groupID))
|
||||
}
|
||||
if ai.discoveryScope != SCOPE_LINK {
|
||||
t.Errorf("discoveryScope = %s; want %s", ai.discoveryScope, SCOPE_LINK)
|
||||
}
|
||||
if len(ai.peers) != 0 {
|
||||
t.Errorf("peers map not empty initially")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CustomConfig", func(t *testing.T) {
|
||||
config := &common.InterfaceConfig{
|
||||
Enabled: true,
|
||||
Port: 12345, // Custom discovery port
|
||||
GroupID: "customGroup",
|
||||
}
|
||||
ai, err := NewAutoInterface("autoCustom", config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAutoInterface failed with custom config: %v", err)
|
||||
}
|
||||
if ai == nil {
|
||||
t.Fatal("NewAutoInterface returned nil with custom config")
|
||||
}
|
||||
|
||||
if ai.discoveryPort != 12345 {
|
||||
t.Errorf("discoveryPort = %d; want 12345", ai.discoveryPort)
|
||||
}
|
||||
if string(ai.groupID) != "customGroup" {
|
||||
t.Errorf("groupID = %s; want customGroup", string(ai.groupID))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// mockAutoInterface embeds AutoInterface but overrides methods that start goroutines
|
||||
type mockAutoInterface struct {
|
||||
*AutoInterface
|
||||
}
|
||||
|
||||
func newMockAutoInterface(name string, config *common.InterfaceConfig) (*mockAutoInterface, error) {
|
||||
ai, err := NewAutoInterface(name, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize maps that would normally be initialized in Start()
|
||||
ai.peers = make(map[string]*Peer)
|
||||
ai.linkLocalAddrs = make([]string, 0)
|
||||
ai.adoptedInterfaces = make(map[string]string)
|
||||
ai.interfaceServers = make(map[string]*net.UDPConn)
|
||||
ai.multicastEchoes = make(map[string]time.Time)
|
||||
|
||||
return &mockAutoInterface{AutoInterface: ai}, nil
|
||||
}
|
||||
|
||||
func (m *mockAutoInterface) Start() error {
|
||||
// Don't start any goroutines
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAutoInterface) Stop() error {
|
||||
// Don't try to close connections that were never opened
|
||||
return nil
|
||||
}
|
||||
|
||||
// mockHandlePeerAnnounce is a test-only method that doesn't handle its own locking
|
||||
func (m *mockAutoInterface) mockHandlePeerAnnounce(addr *net.UDPAddr, ifaceName string) {
|
||||
peerAddr := addr.IP.String() + "%" + addr.Zone
|
||||
|
||||
for _, localAddr := range m.linkLocalAddrs {
|
||||
if peerAddr == localAddr {
|
||||
m.multicastEchoes[ifaceName] = time.Now()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, exists := m.peers[peerAddr]; !exists {
|
||||
m.peers[peerAddr] = &Peer{
|
||||
ifaceName: ifaceName,
|
||||
lastHeard: time.Now(),
|
||||
}
|
||||
} else {
|
||||
m.peers[peerAddr].lastHeard = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoInterfacePeerManagement(t *testing.T) {
|
||||
// Use a shorter timeout for testing
|
||||
testTimeout := 100 * time.Millisecond
|
||||
|
||||
config := &common.InterfaceConfig{Enabled: true}
|
||||
ai, err := newMockAutoInterface("autoPeerTest", config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create mock interface: %v", err)
|
||||
}
|
||||
|
||||
// Create a done channel to signal goroutine cleanup
|
||||
done := make(chan struct{})
|
||||
|
||||
// Start peer management with done channel
|
||||
go func() {
|
||||
ticker := time.NewTicker(testTimeout)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
ai.mutex.Lock()
|
||||
now := time.Now()
|
||||
for addr, peer := range ai.peers {
|
||||
if now.Sub(peer.lastHeard) > testTimeout {
|
||||
delete(ai.peers, addr)
|
||||
}
|
||||
}
|
||||
ai.mutex.Unlock()
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Ensure cleanup
|
||||
defer func() {
|
||||
close(done)
|
||||
ai.Stop()
|
||||
}()
|
||||
|
||||
// Simulate receiving peer announces
|
||||
peer1AddrStr := "fe80::1%eth0"
|
||||
peer2AddrStr := "fe80::2%eth0"
|
||||
localAddrStr := "fe80::aaaa%eth0" // Simulate a local address
|
||||
|
||||
peer1Addr := &net.UDPAddr{IP: net.ParseIP("fe80::1"), Zone: "eth0"}
|
||||
peer2Addr := &net.UDPAddr{IP: net.ParseIP("fe80::2"), Zone: "eth0"}
|
||||
localAddr := &net.UDPAddr{IP: net.ParseIP("fe80::aaaa"), Zone: "eth0"}
|
||||
|
||||
// Add a simulated local address to avoid adding it as a peer
|
||||
ai.mutex.Lock()
|
||||
ai.linkLocalAddrs = append(ai.linkLocalAddrs, localAddrStr)
|
||||
ai.mutex.Unlock()
|
||||
|
||||
t.Run("AddPeer1", func(t *testing.T) {
|
||||
ai.mutex.Lock()
|
||||
ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
|
||||
ai.mutex.Unlock()
|
||||
|
||||
// Give a small amount of time for the peer to be processed
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
ai.mutex.RLock()
|
||||
count := len(ai.peers)
|
||||
peer, exists := ai.peers[peer1AddrStr]
|
||||
var ifaceName string
|
||||
if exists {
|
||||
ifaceName = peer.ifaceName
|
||||
}
|
||||
ai.mutex.RUnlock()
|
||||
|
||||
if count != 1 {
|
||||
t.Fatalf("Expected 1 peer, got %d", count)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("Peer %s not found in map", peer1AddrStr)
|
||||
}
|
||||
if ifaceName != "eth0" {
|
||||
t.Errorf("Peer %s interface name = %s; want eth0", peer1AddrStr, ifaceName)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AddPeer2", func(t *testing.T) {
|
||||
ai.mutex.Lock()
|
||||
ai.mockHandlePeerAnnounce(peer2Addr, "eth0")
|
||||
ai.mutex.Unlock()
|
||||
|
||||
// Give a small amount of time for the peer to be processed
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
ai.mutex.RLock()
|
||||
count := len(ai.peers)
|
||||
_, exists := ai.peers[peer2AddrStr]
|
||||
ai.mutex.RUnlock()
|
||||
|
||||
if count != 2 {
|
||||
t.Fatalf("Expected 2 peers, got %d", count)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("Peer %s not found in map", peer2AddrStr)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("IgnoreLocalAnnounce", func(t *testing.T) {
|
||||
ai.mutex.Lock()
|
||||
ai.mockHandlePeerAnnounce(localAddr, "eth0")
|
||||
ai.mutex.Unlock()
|
||||
|
||||
// Give a small amount of time for the peer to be processed
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
ai.mutex.RLock()
|
||||
count := len(ai.peers)
|
||||
ai.mutex.RUnlock()
|
||||
|
||||
if count != 2 {
|
||||
t.Fatalf("Expected 2 peers after local announce, got %d", count)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UpdatePeerTimestamp", func(t *testing.T) {
|
||||
ai.mutex.RLock()
|
||||
peer, exists := ai.peers[peer1AddrStr]
|
||||
var initialTime time.Time
|
||||
if exists {
|
||||
initialTime = peer.lastHeard
|
||||
}
|
||||
ai.mutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
t.Fatalf("Peer %s not found before timestamp update", peer1AddrStr)
|
||||
}
|
||||
|
||||
ai.mutex.Lock()
|
||||
ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
|
||||
ai.mutex.Unlock()
|
||||
|
||||
// Give a small amount of time for the peer to be processed
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
ai.mutex.RLock()
|
||||
peer, exists = ai.peers[peer1AddrStr]
|
||||
var updatedTime time.Time
|
||||
if exists {
|
||||
updatedTime = peer.lastHeard
|
||||
}
|
||||
ai.mutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
t.Fatalf("Peer %s not found after timestamp update", peer1AddrStr)
|
||||
}
|
||||
|
||||
if !updatedTime.After(initialTime) {
|
||||
t.Errorf("Peer timestamp was not updated after receiving another announce")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PeerTimeout", func(t *testing.T) {
|
||||
// Wait for peer timeout
|
||||
time.Sleep(testTimeout * 2)
|
||||
|
||||
ai.mutex.RLock()
|
||||
count := len(ai.peers)
|
||||
ai.mutex.RUnlock()
|
||||
|
||||
if count != 0 {
|
||||
t.Errorf("Expected all peers to timeout, got %d peers", count)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,21 +1,102 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
)
|
||||
|
||||
const (
|
||||
BITRATE_MINIMUM = 5 // Minimum required bitrate in bits/sec
|
||||
BITRATE_MINIMUM = 1200 // Minimum bitrate in bits/second
|
||||
MODE_FULL = 0x01
|
||||
|
||||
// Interface modes
|
||||
MODE_GATEWAY = 0x02
|
||||
MODE_ACCESS_POINT = 0x03
|
||||
MODE_ROAMING = 0x04
|
||||
MODE_BOUNDARY = 0x05
|
||||
|
||||
// Interface types
|
||||
TYPE_UDP = 0x01
|
||||
TYPE_TCP = 0x02
|
||||
|
||||
PROPAGATION_RATE = 0.02 // 2% of interface bandwidth
|
||||
|
||||
DEBUG_LEVEL = 4 // Default debug level for interface logging
|
||||
|
||||
// Debug levels
|
||||
DEBUG_CRITICAL = 1
|
||||
DEBUG_ERROR = 2
|
||||
DEBUG_INFO = 3
|
||||
DEBUG_VERBOSE = 4
|
||||
DEBUG_TRACE = 5
|
||||
DEBUG_PACKETS = 6
|
||||
DEBUG_ALL = 7
|
||||
)
|
||||
|
||||
// BaseInterface embeds common.BaseInterface and implements common.Interface
|
||||
type Interface interface {
|
||||
GetName() string
|
||||
GetType() common.InterfaceType
|
||||
GetMode() common.InterfaceMode
|
||||
IsOnline() bool
|
||||
IsDetached() bool
|
||||
IsEnabled() bool
|
||||
Detach()
|
||||
Enable()
|
||||
Disable()
|
||||
Send(data []byte, addr string) error
|
||||
SetPacketCallback(common.PacketCallback)
|
||||
GetPacketCallback() common.PacketCallback
|
||||
ProcessIncoming([]byte)
|
||||
ProcessOutgoing([]byte) error
|
||||
SendPathRequest([]byte) error
|
||||
SendLinkPacket([]byte, []byte, time.Time) error
|
||||
Start() error
|
||||
Stop() error
|
||||
GetMTU() int
|
||||
GetConn() net.Conn
|
||||
GetBandwidthAvailable() bool
|
||||
common.NetworkInterface
|
||||
}
|
||||
|
||||
type BaseInterface struct {
|
||||
common.BaseInterface
|
||||
Name string
|
||||
Mode common.InterfaceMode
|
||||
Type common.InterfaceType
|
||||
Online bool
|
||||
Enabled bool
|
||||
Detached bool
|
||||
IN bool
|
||||
OUT bool
|
||||
MTU int
|
||||
Bitrate int64
|
||||
TxBytes uint64
|
||||
RxBytes uint64
|
||||
lastTx time.Time
|
||||
|
||||
mutex sync.RWMutex
|
||||
packetCallback common.PacketCallback
|
||||
}
|
||||
|
||||
func NewBaseInterface(name string, ifType common.InterfaceType, enabled bool) BaseInterface {
|
||||
return BaseInterface{
|
||||
Name: name,
|
||||
Mode: common.IF_MODE_FULL,
|
||||
Type: ifType,
|
||||
Online: false,
|
||||
Enabled: enabled,
|
||||
Detached: false,
|
||||
IN: false,
|
||||
OUT: false,
|
||||
MTU: common.DEFAULT_MTU,
|
||||
Bitrate: BITRATE_MINIMUM,
|
||||
lastTx: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||
@@ -24,28 +105,38 @@ func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||
i.packetCallback = callback
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetPacketCallback() common.PacketCallback {
|
||||
i.mutex.RLock()
|
||||
defer i.mutex.RUnlock()
|
||||
return i.packetCallback
|
||||
}
|
||||
|
||||
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
||||
i.mutex.Lock()
|
||||
i.RxBytes += uint64(len(data))
|
||||
i.mutex.Unlock()
|
||||
|
||||
i.mutex.RLock()
|
||||
callback := i.packetCallback
|
||||
i.mutex.RUnlock()
|
||||
|
||||
|
||||
if callback != nil {
|
||||
callback(data, i)
|
||||
}
|
||||
|
||||
i.RxBytes += uint64(len(data))
|
||||
}
|
||||
|
||||
func (i *BaseInterface) ProcessOutgoing(data []byte) error {
|
||||
i.TxBytes += uint64(len(data))
|
||||
return nil
|
||||
}
|
||||
if !i.Online || i.Detached {
|
||||
log.Printf("[DEBUG-1] Interface %s: Cannot process outgoing packet - interface offline or detached", i.Name)
|
||||
return fmt.Errorf("interface offline or detached")
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Detach() {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
i.Detached = true
|
||||
i.Online = false
|
||||
i.TxBytes += uint64(len(data))
|
||||
i.mutex.Unlock()
|
||||
|
||||
log.Printf("[DEBUG-%d] Interface %s: Processed outgoing packet of %d bytes, total TX: %d", DEBUG_LEVEL, i.Name, len(data), i.TxBytes)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *BaseInterface) SendPathRequest(packet []byte) error {
|
||||
@@ -53,7 +144,7 @@ func (i *BaseInterface) SendPathRequest(packet []byte) error {
|
||||
return fmt.Errorf("interface offline or detached")
|
||||
}
|
||||
|
||||
frame := make([]byte, 0, len(packet)+2)
|
||||
frame := make([]byte, 0, len(packet)+1)
|
||||
frame = append(frame, 0x01)
|
||||
frame = append(frame, packet...)
|
||||
|
||||
@@ -68,12 +159,156 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
|
||||
frame := make([]byte, 0, len(dest)+len(data)+9)
|
||||
frame = append(frame, 0x02)
|
||||
frame = append(frame, dest...)
|
||||
|
||||
|
||||
ts := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix()))
|
||||
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) // #nosec G115
|
||||
frame = append(frame, ts...)
|
||||
|
||||
frame = append(frame, data...)
|
||||
|
||||
return i.ProcessOutgoing(frame)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Detach() {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
i.Detached = true
|
||||
i.Online = false
|
||||
}
|
||||
|
||||
func (i *BaseInterface) IsEnabled() bool {
|
||||
i.mutex.RLock()
|
||||
defer i.mutex.RUnlock()
|
||||
return i.Enabled && i.Online && !i.Detached
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Enable() {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
|
||||
prevState := i.Enabled
|
||||
i.Enabled = true
|
||||
i.Online = true
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
230
pkg/interfaces/interface_test.go
Normal file
230
pkg/interfaces/interface_test.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
)
|
||||
|
||||
func TestBaseInterfaceStateChanges(t *testing.T) {
|
||||
bi := NewBaseInterface("test", common.IF_TYPE_TCP, false) // Start disabled
|
||||
|
||||
if bi.IsEnabled() {
|
||||
t.Error("Newly created disabled interface reports IsEnabled() == true")
|
||||
}
|
||||
if bi.IsOnline() {
|
||||
t.Error("Newly created disabled interface reports IsOnline() == true")
|
||||
}
|
||||
if bi.IsDetached() {
|
||||
t.Error("Newly created interface reports IsDetached() == true")
|
||||
}
|
||||
|
||||
bi.Enable()
|
||||
if !bi.IsEnabled() {
|
||||
t.Error("After Enable(), IsEnabled() == false")
|
||||
}
|
||||
if !bi.IsOnline() {
|
||||
t.Error("After Enable(), IsOnline() == false")
|
||||
}
|
||||
if bi.IsDetached() {
|
||||
t.Error("After Enable(), IsDetached() == true")
|
||||
}
|
||||
|
||||
bi.Detach()
|
||||
if bi.IsEnabled() {
|
||||
t.Error("After Detach(), IsEnabled() == true")
|
||||
}
|
||||
if bi.IsOnline() {
|
||||
t.Error("After Detach(), IsOnline() == true")
|
||||
}
|
||||
if !bi.IsDetached() {
|
||||
t.Error("After Detach(), IsDetached() == false")
|
||||
}
|
||||
|
||||
// Reset for Disable test
|
||||
bi = NewBaseInterface("test2", common.IF_TYPE_UDP, true) // Start enabled
|
||||
if !bi.Enabled { // Check the Enabled field directly first
|
||||
t.Error("Newly created enabled interface reports Enabled == false")
|
||||
}
|
||||
if bi.IsEnabled() { // IsEnabled should still be false because Online is false
|
||||
t.Error("Newly created enabled interface reports IsEnabled() == true before Enable() is called")
|
||||
}
|
||||
|
||||
bi.Enable() // Explicitly enable to set Online = true
|
||||
if !bi.IsEnabled() { // Now IsEnabled should be true
|
||||
t.Error("After Enable() on initially enabled interface, IsEnabled() == false")
|
||||
}
|
||||
|
||||
bi.Disable()
|
||||
if bi.Enabled { // Check Enabled field after Disable()
|
||||
t.Error("After Disable(), Enabled == true")
|
||||
}
|
||||
if bi.IsOnline() {
|
||||
t.Error("After Disable(), IsOnline() == true")
|
||||
}
|
||||
if bi.IsDetached() { // Disable doesn't detach
|
||||
t.Error("After Disable(), IsDetached() == true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterfaceGetters(t *testing.T) {
|
||||
bi := NewBaseInterface("getterTest", common.IF_TYPE_AUTO, true)
|
||||
|
||||
if bi.GetName() != "getterTest" {
|
||||
t.Errorf("GetName() = %s; want getterTest", bi.GetName())
|
||||
}
|
||||
if bi.GetType() != common.IF_TYPE_AUTO {
|
||||
t.Errorf("GetType() = %v; want %v", bi.GetType(), common.IF_TYPE_AUTO)
|
||||
}
|
||||
if bi.GetMode() != common.IF_MODE_FULL {
|
||||
t.Errorf("GetMode() = %v; want %v", bi.GetMode(), common.IF_MODE_FULL)
|
||||
}
|
||||
if bi.GetMTU() != common.DEFAULT_MTU { // Assuming default MTU
|
||||
t.Errorf("GetMTU() = %d; want %d", bi.GetMTU(), common.DEFAULT_MTU)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterfaceCallbacks(t *testing.T) {
|
||||
bi := NewBaseInterface("callbackTest", common.IF_TYPE_TCP, true)
|
||||
var wg sync.WaitGroup
|
||||
var callbackCalled bool
|
||||
|
||||
callback := func(data []byte, iface common.NetworkInterface) {
|
||||
if len(data) != 5 {
|
||||
t.Errorf("Callback received data length %d; want 5", len(data))
|
||||
}
|
||||
if iface.GetName() != "callbackTest" {
|
||||
t.Errorf("Callback received interface name %s; want callbackTest", iface.GetName())
|
||||
}
|
||||
callbackCalled = true
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
bi.SetPacketCallback(callback)
|
||||
if bi.GetPacketCallback() == nil { // Cannot directly compare functions
|
||||
t.Error("GetPacketCallback() returned nil after SetPacketCallback()")
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go bi.ProcessIncoming([]byte{1, 2, 3, 4, 5}) // Run in goroutine as callback might block
|
||||
|
||||
// Wait for callback or timeout
|
||||
waitTimeout(&wg, 1*time.Second, t)
|
||||
|
||||
if !callbackCalled {
|
||||
t.Error("Packet callback was not called after ProcessIncoming")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterfaceStats(t *testing.T) {
|
||||
bi := NewBaseInterface("statsTest", common.IF_TYPE_UDP, true)
|
||||
bi.Enable() // Need to be Online for ProcessOutgoing
|
||||
|
||||
data1 := []byte{1, 2, 3}
|
||||
data2 := []byte{4, 5, 6, 7, 8}
|
||||
|
||||
bi.ProcessIncoming(data1)
|
||||
if bi.RxBytes != uint64(len(data1)) {
|
||||
t.Errorf("RxBytes = %d; want %d after first ProcessIncoming", bi.RxBytes, len(data1))
|
||||
}
|
||||
|
||||
bi.ProcessIncoming(data2)
|
||||
if bi.RxBytes != uint64(len(data1)+len(data2)) {
|
||||
t.Errorf("RxBytes = %d; want %d after second ProcessIncoming", bi.RxBytes, len(data1)+len(data2))
|
||||
}
|
||||
|
||||
// ProcessOutgoing only updates TxBytes in BaseInterface
|
||||
err := bi.ProcessOutgoing(data1)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessOutgoing failed: %v", err)
|
||||
}
|
||||
if bi.TxBytes != uint64(len(data1)) {
|
||||
t.Errorf("TxBytes = %d; want %d after first ProcessOutgoing", bi.TxBytes, len(data1))
|
||||
}
|
||||
|
||||
err = bi.ProcessOutgoing(data2)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessOutgoing failed: %v", err)
|
||||
}
|
||||
if bi.TxBytes != uint64(len(data1)+len(data2)) {
|
||||
t.Errorf("TxBytes = %d; want %d after second ProcessOutgoing", bi.TxBytes, len(data1)+len(data2))
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to wait for a WaitGroup with a timeout
|
||||
func waitTimeout(wg *sync.WaitGroup, timeout time.Duration, t *testing.T) {
|
||||
c := make(chan struct{})
|
||||
go func() {
|
||||
defer close(c)
|
||||
wg.Wait()
|
||||
}()
|
||||
select {
|
||||
case <-c:
|
||||
// Completed normally
|
||||
case <-time.After(timeout):
|
||||
t.Fatal("Timed out waiting for WaitGroup")
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal mock interface for InterceptedInterface test
|
||||
type mockInterface struct {
|
||||
BaseInterface
|
||||
sendCalled bool
|
||||
sendData []byte
|
||||
}
|
||||
|
||||
func (m *mockInterface) Send(data []byte, addr string) error {
|
||||
m.sendCalled = true
|
||||
m.sendData = data
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add other methods to satisfy the Interface interface (can be minimal/panic)
|
||||
func (m *mockInterface) GetType() common.InterfaceType { return common.IF_TYPE_NONE }
|
||||
func (m *mockInterface) GetMode() common.InterfaceMode { return common.IF_MODE_FULL }
|
||||
func (m *mockInterface) ProcessIncoming(data []byte) {}
|
||||
func (m *mockInterface) ProcessOutgoing(data []byte) error { return nil }
|
||||
func (m *mockInterface) SendPathRequest([]byte) error { return nil }
|
||||
func (m *mockInterface) SendLinkPacket([]byte, []byte, time.Time) error { return nil }
|
||||
func (m *mockInterface) Start() error { return nil }
|
||||
func (m *mockInterface) Stop() error { return nil }
|
||||
func (m *mockInterface) GetConn() net.Conn { return nil }
|
||||
func (m *mockInterface) GetBandwidthAvailable() bool { return true }
|
||||
|
||||
func TestInterceptedInterface(t *testing.T) {
|
||||
mockBase := &mockInterface{}
|
||||
var interceptorCalled bool
|
||||
var interceptedData []byte
|
||||
|
||||
interceptor := func(data []byte, iface common.NetworkInterface) error {
|
||||
interceptorCalled = true
|
||||
interceptedData = data
|
||||
return nil
|
||||
}
|
||||
|
||||
intercepted := NewInterceptedInterface(mockBase, interceptor)
|
||||
|
||||
testData := []byte("intercept me")
|
||||
err := intercepted.Send(testData, "dummy_addr")
|
||||
if err != nil {
|
||||
t.Fatalf("Intercepted Send failed: %v", err)
|
||||
}
|
||||
|
||||
if !interceptorCalled {
|
||||
t.Error("Interceptor function was not called")
|
||||
}
|
||||
if !bytes.Equal(interceptedData, testData) {
|
||||
t.Errorf("Interceptor received data %x; want %x", interceptedData, testData)
|
||||
}
|
||||
|
||||
if !mockBase.sendCalled {
|
||||
t.Error("Original Send function was not called")
|
||||
}
|
||||
if !bytes.Equal(mockBase.sendData, testData) {
|
||||
t.Errorf("Original Send received data %x; want %x", mockBase.sendData, testData)
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,13 @@ package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -17,103 +21,102 @@ const (
|
||||
KISS_TFEND = 0xDC
|
||||
KISS_TFESC = 0xDD
|
||||
|
||||
TCP_USER_TIMEOUT = 24
|
||||
TCP_PROBE_AFTER = 5
|
||||
TCP_PROBE_INTERVAL = 2
|
||||
TCP_USER_TIMEOUT = 24
|
||||
TCP_PROBE_AFTER = 5
|
||||
TCP_PROBE_INTERVAL = 2
|
||||
TCP_PROBES = 12
|
||||
RECONNECT_WAIT = 5
|
||||
INITIAL_TIMEOUT = 5
|
||||
INITIAL_BACKOFF = time.Second
|
||||
MAX_BACKOFF = time.Minute * 5
|
||||
)
|
||||
|
||||
type TCPClientInterface struct {
|
||||
Interface
|
||||
conn net.Conn
|
||||
targetAddr string
|
||||
targetPort int
|
||||
kissFraming bool
|
||||
i2pTunneled bool
|
||||
initiator bool
|
||||
reconnecting bool
|
||||
neverConnected bool
|
||||
writing bool
|
||||
BaseInterface
|
||||
conn net.Conn
|
||||
targetAddr string
|
||||
targetPort int
|
||||
kissFraming bool
|
||||
i2pTunneled bool
|
||||
initiator bool
|
||||
reconnecting bool
|
||||
neverConnected bool
|
||||
writing bool
|
||||
maxReconnectTries int
|
||||
packetBuffer []byte
|
||||
packetType byte
|
||||
packetBuffer []byte
|
||||
packetType byte
|
||||
mutex sync.RWMutex
|
||||
enabled bool
|
||||
TxBytes uint64
|
||||
RxBytes uint64
|
||||
lastTx time.Time
|
||||
lastRx time.Time
|
||||
}
|
||||
|
||||
func NewTCPClient(name string, targetAddr string, targetPort int, kissFraming bool, i2pTunneled bool) (*TCPClientInterface, error) {
|
||||
func NewTCPClientInterface(name string, targetHost string, targetPort int, kissFraming bool, i2pTunneled bool, enabled bool) (*TCPClientInterface, error) {
|
||||
tc := &TCPClientInterface{
|
||||
Interface: Interface{
|
||||
Name: name,
|
||||
Mode: MODE_FULL,
|
||||
MTU: 1064,
|
||||
Bitrate: 10000000, // 10Mbps estimate
|
||||
},
|
||||
targetAddr: targetAddr,
|
||||
targetPort: targetPort,
|
||||
kissFraming: kissFraming,
|
||||
i2pTunneled: i2pTunneled,
|
||||
initiator: true,
|
||||
BaseInterface: NewBaseInterface(name, common.IF_TYPE_TCP, enabled),
|
||||
targetAddr: targetHost,
|
||||
targetPort: targetPort,
|
||||
kissFraming: kissFraming,
|
||||
i2pTunneled: i2pTunneled,
|
||||
initiator: true,
|
||||
enabled: enabled,
|
||||
maxReconnectTries: TCP_PROBES,
|
||||
packetBuffer: make([]byte, 0),
|
||||
neverConnected: true,
|
||||
}
|
||||
|
||||
if err := tc.connect(true); err != nil {
|
||||
go tc.reconnect()
|
||||
} else {
|
||||
if enabled {
|
||||
addr := net.JoinHostPort(targetHost, fmt.Sprintf("%d", targetPort))
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tc.conn = conn
|
||||
tc.Online = true
|
||||
go tc.readLoop()
|
||||
}
|
||||
|
||||
return tc, nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) connect(initial bool) error {
|
||||
addr := fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort)
|
||||
conn, err := net.DialTimeout("tcp", addr, time.Second*INITIAL_TIMEOUT)
|
||||
func (tc *TCPClientInterface) Start() error {
|
||||
tc.mutex.Lock()
|
||||
defer tc.mutex.Unlock()
|
||||
|
||||
if !tc.Enabled {
|
||||
return fmt.Errorf("interface not enabled")
|
||||
}
|
||||
|
||||
if tc.conn != nil {
|
||||
tc.Online = true
|
||||
go tc.readLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
addr := net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort))
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
if initial {
|
||||
return fmt.Errorf("initial connection failed: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
tc.conn = conn
|
||||
tc.Online = true
|
||||
tc.writing = false
|
||||
tc.neverConnected = false
|
||||
|
||||
// Set TCP options
|
||||
if tcpConn, ok := conn.(*net.TCPConn); ok {
|
||||
tcpConn.SetNoDelay(true)
|
||||
tcpConn.SetKeepAlive(true)
|
||||
tcpConn.SetKeepAlivePeriod(time.Second * TCP_PROBE_INTERVAL)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) reconnect() {
|
||||
if tc.initiator && !tc.reconnecting {
|
||||
tc.reconnecting = true
|
||||
attempts := 0
|
||||
|
||||
for !tc.Online {
|
||||
time.Sleep(time.Second * RECONNECT_WAIT)
|
||||
attempts++
|
||||
|
||||
if tc.maxReconnectTries > 0 && attempts > tc.maxReconnectTries {
|
||||
tc.teardown()
|
||||
break
|
||||
}
|
||||
|
||||
if err := tc.connect(false); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
go tc.readLoop()
|
||||
break
|
||||
// Set platform-specific timeouts
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
if err := tc.setTimeoutsLinux(); err != nil {
|
||||
log.Printf("[DEBUG-2] Failed to set Linux TCP timeouts: %v", err)
|
||||
}
|
||||
case "darwin":
|
||||
if err := tc.setTimeoutsOSX(); err != nil {
|
||||
log.Printf("[DEBUG-2] Failed to set OSX TCP timeouts: %v", err)
|
||||
}
|
||||
|
||||
tc.reconnecting = false
|
||||
}
|
||||
|
||||
tc.Online = true
|
||||
go tc.readLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) readLoop() {
|
||||
@@ -134,26 +137,31 @@ func (tc *TCPClientInterface) readLoop() {
|
||||
return
|
||||
}
|
||||
|
||||
// Update RX bytes for raw received data
|
||||
tc.UpdateStats(uint64(n), true) // #nosec G115
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
b := buffer[i]
|
||||
|
||||
if tc.kissFraming {
|
||||
// KISS framing logic
|
||||
if inFrame && b == KISS_FEND {
|
||||
inFrame = false
|
||||
tc.handlePacket(dataBuffer)
|
||||
dataBuffer = dataBuffer[:0]
|
||||
} else if b == KISS_FEND {
|
||||
inFrame = true
|
||||
} else if inFrame {
|
||||
if b == KISS_FEND {
|
||||
if inFrame && len(dataBuffer) > 0 {
|
||||
tc.handlePacket(dataBuffer)
|
||||
dataBuffer = dataBuffer[:0]
|
||||
}
|
||||
inFrame = !inFrame
|
||||
continue
|
||||
}
|
||||
|
||||
if inFrame {
|
||||
if b == KISS_FESC {
|
||||
escape = true
|
||||
} else {
|
||||
if escape {
|
||||
if b == KISS_TFEND {
|
||||
b = KISS_FEND
|
||||
}
|
||||
if b == KISS_TFESC {
|
||||
} else if b == KISS_TFESC {
|
||||
b = KISS_FESC
|
||||
}
|
||||
escape = false
|
||||
@@ -163,13 +171,16 @@ func (tc *TCPClientInterface) readLoop() {
|
||||
}
|
||||
} else {
|
||||
// HDLC framing logic
|
||||
if inFrame && b == HDLC_FLAG {
|
||||
inFrame = false
|
||||
tc.handlePacket(dataBuffer)
|
||||
dataBuffer = dataBuffer[:0]
|
||||
} else if b == HDLC_FLAG {
|
||||
inFrame = true
|
||||
} else if inFrame {
|
||||
if b == HDLC_FLAG {
|
||||
if inFrame && len(dataBuffer) > 0 {
|
||||
tc.handlePacket(dataBuffer)
|
||||
dataBuffer = dataBuffer[:0]
|
||||
}
|
||||
inFrame = !inFrame
|
||||
continue
|
||||
}
|
||||
|
||||
if inFrame {
|
||||
if b == HDLC_ESC {
|
||||
escape = true
|
||||
} else {
|
||||
@@ -187,20 +198,40 @@ func (tc *TCPClientInterface) readLoop() {
|
||||
|
||||
func (tc *TCPClientInterface) handlePacket(data []byte) {
|
||||
if len(data) < 1 {
|
||||
log.Printf("[DEBUG-7] Received invalid packet: empty")
|
||||
return
|
||||
}
|
||||
|
||||
packetType := data[0]
|
||||
tc.mutex.Lock()
|
||||
tc.packetType = data[0]
|
||||
tc.RxBytes += uint64(len(data))
|
||||
lastRx := time.Now()
|
||||
tc.lastRx = lastRx
|
||||
tc.mutex.Unlock()
|
||||
|
||||
log.Printf("[DEBUG-7] Received packet: type=0x%02x, size=%d bytes", tc.packetType, len(data))
|
||||
|
||||
payload := data[1:]
|
||||
|
||||
switch packetType {
|
||||
case 0x01: // Path request
|
||||
tc.Interface.ProcessIncoming(payload)
|
||||
switch tc.packetType {
|
||||
case 0x01: // Announce packet
|
||||
log.Printf("[DEBUG-7] Processing announce packet: payload=%d bytes", len(payload))
|
||||
if len(payload) >= 53 {
|
||||
tc.BaseInterface.ProcessIncoming(payload)
|
||||
} else {
|
||||
log.Printf("[DEBUG-7] Announce packet too small: %d bytes", len(payload))
|
||||
}
|
||||
case 0x02: // Link packet
|
||||
if len(payload) < 40 { // minimum size for link packet
|
||||
log.Printf("[DEBUG-7] Processing link packet: payload=%d bytes", len(payload))
|
||||
if len(payload) < 40 {
|
||||
log.Printf("[DEBUG-7] Link packet too small: %d bytes", len(payload))
|
||||
return
|
||||
}
|
||||
tc.Interface.ProcessIncoming(payload)
|
||||
tc.BaseInterface.ProcessIncoming(payload)
|
||||
case 0x03: // Announce packet
|
||||
tc.BaseInterface.ProcessIncoming(payload)
|
||||
case 0x04: // Transport packet
|
||||
tc.BaseInterface.ProcessIncoming(payload)
|
||||
default:
|
||||
// Unknown packet type
|
||||
return
|
||||
@@ -224,13 +255,11 @@ func (tc *TCPClientInterface) ProcessOutgoing(data []byte) error {
|
||||
frame = append(frame, HDLC_FLAG)
|
||||
}
|
||||
|
||||
if _, err := tc.conn.Write(frame); err != nil {
|
||||
tc.teardown()
|
||||
return fmt.Errorf("write failed: %v", err)
|
||||
}
|
||||
// Update TX stats before sending
|
||||
tc.UpdateStats(uint64(len(frame)), false)
|
||||
|
||||
tc.Interface.ProcessOutgoing(data)
|
||||
return nil
|
||||
_, err := tc.conn.Write(frame)
|
||||
return err
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) teardown() {
|
||||
@@ -238,7 +267,7 @@ func (tc *TCPClientInterface) teardown() {
|
||||
tc.IN = false
|
||||
tc.OUT = false
|
||||
if tc.conn != nil {
|
||||
tc.conn.Close()
|
||||
tc.conn.Close() // #nosec G104
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,130 +298,239 @@ func escapeKISS(data []byte) []byte {
|
||||
return escaped
|
||||
}
|
||||
|
||||
type TCPServerInterface struct {
|
||||
Interface
|
||||
server net.Listener
|
||||
bindAddr string
|
||||
bindPort int
|
||||
i2pTunneled bool
|
||||
preferIPv6 bool
|
||||
spawned []*TCPClientInterface
|
||||
spawnedMutex sync.RWMutex
|
||||
func (tc *TCPClientInterface) SetPacketCallback(cb common.PacketCallback) {
|
||||
tc.packetCallback = cb
|
||||
}
|
||||
|
||||
func NewTCPServer(name string, bindAddr string, bindPort int, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) {
|
||||
ts := &TCPServerInterface{
|
||||
Interface: Interface{
|
||||
Name: name,
|
||||
Mode: MODE_FULL,
|
||||
MTU: 1064,
|
||||
Bitrate: 10000000, // 10Mbps estimate
|
||||
},
|
||||
bindAddr: bindAddr,
|
||||
bindPort: bindPort,
|
||||
i2pTunneled: i2pTunneled,
|
||||
preferIPv6: preferIPv6,
|
||||
spawned: make([]*TCPClientInterface, 0),
|
||||
}
|
||||
|
||||
// Resolve bind address
|
||||
var addr string
|
||||
if ts.bindAddr == "" {
|
||||
if ts.preferIPv6 {
|
||||
addr = fmt.Sprintf("[::0]:%d", ts.bindPort)
|
||||
} else {
|
||||
addr = fmt.Sprintf("0.0.0.0:%d", ts.bindPort)
|
||||
}
|
||||
} else {
|
||||
addr = fmt.Sprintf("%s:%d", ts.bindAddr, ts.bindPort)
|
||||
}
|
||||
|
||||
// Create listener
|
||||
var err error
|
||||
ts.server, err = net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create TCP listener: %v", err)
|
||||
}
|
||||
|
||||
ts.Online = true
|
||||
ts.IN = true
|
||||
|
||||
// Start accept loop
|
||||
go ts.acceptLoop()
|
||||
|
||||
return ts, nil
|
||||
func (tc *TCPClientInterface) IsEnabled() bool {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
return tc.enabled && tc.Online && !tc.Detached
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) acceptLoop() {
|
||||
for {
|
||||
conn, err := ts.server.Accept()
|
||||
if err != nil {
|
||||
if !ts.Detached {
|
||||
// Log error and continue accepting
|
||||
continue
|
||||
}
|
||||
func (tc *TCPClientInterface) GetName() string {
|
||||
return tc.Name
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) GetPacketCallback() common.PacketCallback {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
return tc.packetCallback
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) IsDetached() bool {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
return tc.Detached
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) IsOnline() bool {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
return tc.Online
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) reconnect() {
|
||||
tc.mutex.Lock()
|
||||
if tc.reconnecting {
|
||||
tc.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
tc.reconnecting = true
|
||||
tc.mutex.Unlock()
|
||||
|
||||
backoff := time.Second
|
||||
maxBackoff := time.Minute * 5
|
||||
retries := 0
|
||||
|
||||
for retries < tc.maxReconnectTries {
|
||||
tc.teardown()
|
||||
|
||||
addr := net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort))
|
||||
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err == nil {
|
||||
tc.mutex.Lock()
|
||||
tc.conn = conn
|
||||
tc.Online = true
|
||||
|
||||
tc.neverConnected = false
|
||||
tc.reconnecting = false
|
||||
tc.mutex.Unlock()
|
||||
|
||||
go tc.readLoop()
|
||||
return
|
||||
}
|
||||
|
||||
// Create new client interface for this connection
|
||||
client := &TCPClientInterface{
|
||||
Interface: Interface{
|
||||
Name: fmt.Sprintf("Client-%s-%s", ts.Name, conn.RemoteAddr()),
|
||||
Mode: ts.Mode,
|
||||
MTU: ts.MTU,
|
||||
},
|
||||
conn: conn,
|
||||
i2pTunneled: ts.i2pTunneled,
|
||||
// Log reconnection attempt
|
||||
fmt.Printf("Failed to reconnect to %s (attempt %d/%d): %v\n",
|
||||
addr, retries+1, tc.maxReconnectTries, err)
|
||||
|
||||
// Wait with exponential backoff
|
||||
time.Sleep(backoff)
|
||||
|
||||
// Increase backoff time exponentially
|
||||
backoff *= 2
|
||||
if backoff > maxBackoff {
|
||||
backoff = maxBackoff
|
||||
}
|
||||
|
||||
// Configure TCP options
|
||||
if tcpConn, ok := conn.(*net.TCPConn); ok {
|
||||
tcpConn.SetNoDelay(true)
|
||||
tcpConn.SetKeepAlive(true)
|
||||
tcpConn.SetKeepAlivePeriod(time.Duration(TCP_PROBE_INTERVAL) * time.Second)
|
||||
retries++
|
||||
}
|
||||
|
||||
tc.mutex.Lock()
|
||||
tc.reconnecting = false
|
||||
tc.mutex.Unlock()
|
||||
|
||||
// If we've exhausted all retries, perform final teardown
|
||||
tc.teardown()
|
||||
fmt.Printf("Failed to reconnect to %s after %d attempts\n",
|
||||
fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort), tc.maxReconnectTries)
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) Enable() {
|
||||
tc.mutex.Lock()
|
||||
defer tc.mutex.Unlock()
|
||||
tc.Online = true
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) Disable() {
|
||||
tc.mutex.Lock()
|
||||
defer tc.mutex.Unlock()
|
||||
tc.Online = false
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) IsConnected() bool {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
return tc.conn != nil && tc.Online && !tc.reconnecting
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) GetRTT() time.Duration {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
|
||||
if !tc.IsConnected() {
|
||||
return 0
|
||||
}
|
||||
|
||||
if tcpConn, ok := tc.conn.(*net.TCPConn); ok {
|
||||
var rtt time.Duration = 0
|
||||
if runtime.GOOS == "linux" {
|
||||
if info, err := tcpConn.SyscallConn(); err == nil {
|
||||
if err := info.Control(func(fd uintptr) { // #nosec G104
|
||||
rtt = platformGetRTT(fd)
|
||||
}); err != nil {
|
||||
log.Printf("[DEBUG-2] Error in SyscallConn Control: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return rtt
|
||||
}
|
||||
|
||||
client.Online = true
|
||||
client.IN = ts.IN
|
||||
client.OUT = ts.OUT
|
||||
return 0
|
||||
}
|
||||
|
||||
// Add to spawned interfaces
|
||||
ts.spawnedMutex.Lock()
|
||||
ts.spawned = append(ts.spawned, client)
|
||||
ts.spawnedMutex.Unlock()
|
||||
func (tc *TCPClientInterface) GetTxBytes() uint64 {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
return tc.TxBytes
|
||||
}
|
||||
|
||||
// Start client read loop
|
||||
go client.readLoop()
|
||||
func (tc *TCPClientInterface) GetRxBytes() uint64 {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
return tc.RxBytes
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) UpdateStats(bytes uint64, isRx bool) {
|
||||
tc.mutex.Lock()
|
||||
defer tc.mutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
if isRx {
|
||||
tc.RxBytes += bytes
|
||||
tc.lastRx = now
|
||||
log.Printf("[DEBUG-5] Interface %s RX stats: bytes=%d total=%d last=%v",
|
||||
tc.Name, bytes, tc.RxBytes, tc.lastRx)
|
||||
} else {
|
||||
tc.TxBytes += bytes
|
||||
tc.lastTx = now
|
||||
log.Printf("[DEBUG-5] Interface %s TX stats: bytes=%d total=%d last=%v",
|
||||
tc.Name, bytes, tc.TxBytes, tc.lastTx)
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) Detach() {
|
||||
ts.Interface.Detach()
|
||||
|
||||
if ts.server != nil {
|
||||
ts.server.Close()
|
||||
}
|
||||
|
||||
ts.spawnedMutex.Lock()
|
||||
for _, client := range ts.spawned {
|
||||
client.Detach()
|
||||
}
|
||||
ts.spawned = nil
|
||||
ts.spawnedMutex.Unlock()
|
||||
func (tc *TCPClientInterface) GetStats() (tx uint64, rx uint64, lastTx time.Time, lastRx time.Time) {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
return tc.TxBytes, tc.RxBytes, tc.lastTx, tc.lastRx
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
|
||||
ts.spawnedMutex.RLock()
|
||||
defer ts.spawnedMutex.RUnlock()
|
||||
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, client := range ts.spawned {
|
||||
if err := client.ProcessOutgoing(data); err != nil {
|
||||
lastErr = err
|
||||
if !tc.i2pTunneled {
|
||||
if err := tcpConn.SetKeepAlive(true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tcpConn.SetKeepAlivePeriod(time.Duration(TCP_PROBE_INTERVAL) * time.Second); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return lastErr
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlive(true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type TCPServerInterface struct {
|
||||
BaseInterface
|
||||
connections map[string]net.Conn
|
||||
mutex sync.RWMutex
|
||||
bindAddr string
|
||||
bindPort int
|
||||
preferIPv6 bool
|
||||
kissFraming bool
|
||||
i2pTunneled bool
|
||||
packetCallback common.PacketCallback
|
||||
TxBytes uint64
|
||||
RxBytes uint64
|
||||
}
|
||||
|
||||
func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFraming bool, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) {
|
||||
ts := &TCPServerInterface{
|
||||
BaseInterface: BaseInterface{
|
||||
Name: name,
|
||||
Mode: common.IF_MODE_FULL,
|
||||
Type: common.IF_TYPE_TCP,
|
||||
Online: false,
|
||||
MTU: common.DEFAULT_MTU,
|
||||
Detached: false,
|
||||
},
|
||||
connections: make(map[string]net.Conn),
|
||||
bindAddr: bindAddr,
|
||||
bindPort: bindPort,
|
||||
preferIPv6: preferIPv6,
|
||||
kissFraming: kissFraming,
|
||||
i2pTunneled: i2pTunneled,
|
||||
}
|
||||
|
||||
return ts, nil
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) String() string {
|
||||
@@ -401,8 +539,165 @@ func (ts *TCPServerInterface) String() string {
|
||||
if ts.preferIPv6 {
|
||||
addr = "[::0]"
|
||||
} else {
|
||||
addr = "0.0.0.0"
|
||||
addr = "0.0.0.0"
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("TCPServerInterface[%s/%s:%d]", ts.Name, addr, ts.bindPort)
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
ts.packetCallback = callback
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) GetPacketCallback() common.PacketCallback {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
return ts.packetCallback
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) IsEnabled() bool {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
return ts.BaseInterface.Enabled && ts.BaseInterface.Online && !ts.BaseInterface.Detached
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) GetName() string {
|
||||
return ts.Name
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) IsDetached() bool {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
return ts.BaseInterface.Detached
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) IsOnline() bool {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
return ts.Online
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) Enable() {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
ts.Online = true
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) Disable() {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
ts.Online = false
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) Start() error {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", ts.bindAddr, ts.bindPort)
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start TCP server: %w", err)
|
||||
}
|
||||
|
||||
ts.Online = true
|
||||
|
||||
// Accept connections in a goroutine
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
if !ts.Online {
|
||||
return // Normal shutdown
|
||||
}
|
||||
log.Printf("[DEBUG-2] Error accepting connection: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle each connection in a separate goroutine
|
||||
go ts.handleConnection(conn)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) Stop() error {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
|
||||
ts.Online = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) GetTxBytes() uint64 {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
return ts.TxBytes
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) GetRxBytes() uint64 {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
return ts.RxBytes
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
|
||||
addr := conn.RemoteAddr().String()
|
||||
ts.mutex.Lock()
|
||||
ts.connections[addr] = conn
|
||||
ts.mutex.Unlock()
|
||||
|
||||
defer func() {
|
||||
ts.mutex.Lock()
|
||||
delete(ts.connections, addr)
|
||||
ts.mutex.Unlock()
|
||||
conn.Close() // #nosec G104
|
||||
}()
|
||||
|
||||
buffer := make([]byte, ts.MTU)
|
||||
for {
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ts.mutex.Lock()
|
||||
ts.RxBytes += uint64(n) // #nosec G115
|
||||
ts.mutex.Unlock()
|
||||
|
||||
if ts.packetCallback != nil {
|
||||
ts.packetCallback(buffer[:n], ts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
|
||||
if !ts.Online {
|
||||
return fmt.Errorf("interface offline")
|
||||
}
|
||||
|
||||
var frame []byte
|
||||
if ts.kissFraming {
|
||||
frame = append([]byte{KISS_FEND}, escapeKISS(data)...)
|
||||
frame = append(frame, KISS_FEND)
|
||||
} else {
|
||||
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
|
||||
frame = append(frame, HDLC_FLAG)
|
||||
}
|
||||
|
||||
ts.TxBytes += uint64(len(frame))
|
||||
|
||||
for _, conn := range ts.connections {
|
||||
if _, err := conn.Write(frame); err != nil {
|
||||
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)), // #nosec G103
|
||||
uintptr(unsafe.Pointer(&size)), // #nosec G103
|
||||
0,
|
||||
)
|
||||
|
||||
if err != 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// RTT is in microseconds, convert to Duration
|
||||
return time.Duration(info.Rtt) * time.Microsecond
|
||||
}
|
||||
52
pkg/interfaces/tcp_test.go
Normal file
52
pkg/interfaces/tcp_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEscapeHDLC(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input []byte
|
||||
expected []byte
|
||||
}{
|
||||
{"NoEscape", []byte{0x01, 0x02, 0x03}, []byte{0x01, 0x02, 0x03}},
|
||||
{"EscapeFlag", []byte{0x01, HDLC_FLAG, 0x03}, []byte{0x01, HDLC_ESC, HDLC_FLAG ^ HDLC_ESC_MASK, 0x03}},
|
||||
{"EscapeEsc", []byte{0x01, HDLC_ESC, 0x03}, []byte{0x01, HDLC_ESC, HDLC_ESC ^ HDLC_ESC_MASK, 0x03}},
|
||||
{"EscapeBoth", []byte{HDLC_FLAG, HDLC_ESC}, []byte{HDLC_ESC, HDLC_FLAG ^ HDLC_ESC_MASK, HDLC_ESC, HDLC_ESC ^ HDLC_ESC_MASK}},
|
||||
{"Empty", []byte{}, []byte{}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := escapeHDLC(tc.input)
|
||||
if !bytes.Equal(result, tc.expected) {
|
||||
t.Errorf("escapeHDLC(%x) = %x; want %x", tc.input, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscapeKISS(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input []byte
|
||||
expected []byte
|
||||
}{
|
||||
{"NoEscape", []byte{0x01, 0x02, 0x03}, []byte{0x01, 0x02, 0x03}},
|
||||
{"EscapeFEND", []byte{0x01, KISS_FEND, 0x03}, []byte{0x01, KISS_FESC, KISS_TFEND, 0x03}},
|
||||
{"EscapeFESC", []byte{0x01, KISS_FESC, 0x03}, []byte{0x01, KISS_FESC, KISS_TFESC, 0x03}},
|
||||
{"EscapeBoth", []byte{KISS_FEND, KISS_FESC}, []byte{KISS_FESC, KISS_TFEND, KISS_FESC, KISS_TFESC}},
|
||||
{"Empty", []byte{}, []byte{}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := escapeKISS(tc.input)
|
||||
if !bytes.Equal(result, tc.expected) {
|
||||
t.Errorf("escapeKISS(%x) = %x; want %x", tc.input, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,85 +4,114 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
)
|
||||
|
||||
type UDPInterface struct {
|
||||
Interface
|
||||
conn *net.UDPConn
|
||||
listenAddr *net.UDPAddr
|
||||
BaseInterface
|
||||
conn *net.UDPConn
|
||||
addr *net.UDPAddr
|
||||
targetAddr *net.UDPAddr
|
||||
mutex sync.RWMutex
|
||||
readBuffer []byte
|
||||
}
|
||||
|
||||
func NewUDPInterface(name string, listenAddr string, targetAddr string) (*UDPInterface, error) {
|
||||
ui := &UDPInterface{
|
||||
Interface: Interface{
|
||||
Name: name,
|
||||
Mode: MODE_FULL,
|
||||
MTU: 1500,
|
||||
Bitrate: 100000000, // 100Mbps estimate for UDP
|
||||
},
|
||||
readBuffer: make([]byte, 65535),
|
||||
}
|
||||
|
||||
// Parse listen address
|
||||
laddr, err := net.ResolveUDPAddr("udp", listenAddr)
|
||||
func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) {
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid listen address: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
ui.listenAddr = laddr
|
||||
|
||||
// Parse target address if provided
|
||||
if targetAddr != "" {
|
||||
taddr, err := net.ResolveUDPAddr("udp", targetAddr)
|
||||
var targetAddr *net.UDPAddr
|
||||
if target != "" {
|
||||
targetAddr, err = net.ResolveUDPAddr("udp", target)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid target address: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
ui.targetAddr = taddr
|
||||
ui.OUT = true
|
||||
}
|
||||
|
||||
// Create UDP connection
|
||||
conn, err := net.ListenUDP("udp", ui.listenAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to listen on UDP: %v", err)
|
||||
ui := &UDPInterface{
|
||||
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
|
||||
addr: udpAddr,
|
||||
targetAddr: targetAddr,
|
||||
readBuffer: make([]byte, common.DEFAULT_MTU),
|
||||
}
|
||||
ui.conn = conn
|
||||
ui.IN = true
|
||||
ui.Online = true
|
||||
|
||||
// Start read loop
|
||||
go ui.readLoop()
|
||||
|
||||
return ui, nil
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) readLoop() {
|
||||
for {
|
||||
if !ui.Online {
|
||||
return
|
||||
}
|
||||
func (ui *UDPInterface) GetName() string {
|
||||
return ui.Name
|
||||
}
|
||||
|
||||
n, addr, err := ui.conn.ReadFromUDP(ui.readBuffer)
|
||||
if err != nil {
|
||||
if !ui.Detached {
|
||||
// Log error
|
||||
}
|
||||
continue
|
||||
}
|
||||
func (ui *UDPInterface) GetType() common.InterfaceType {
|
||||
return ui.Type
|
||||
}
|
||||
|
||||
// Copy received data
|
||||
data := make([]byte, n)
|
||||
copy(data, ui.readBuffer[:n])
|
||||
func (ui *UDPInterface) GetMode() common.InterfaceMode {
|
||||
return ui.Mode
|
||||
}
|
||||
|
||||
// Process packet
|
||||
ui.ProcessIncoming(data)
|
||||
func (ui *UDPInterface) IsOnline() bool {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
return ui.Online
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) IsDetached() bool {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
return ui.Detached
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Detach() {
|
||||
ui.mutex.Lock()
|
||||
defer ui.mutex.Unlock()
|
||||
ui.Detached = true
|
||||
if ui.conn != nil {
|
||||
ui.conn.Close() // #nosec G104
|
||||
}
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Send(data []byte, addr string) error {
|
||||
if !ui.IsEnabled() {
|
||||
return fmt.Errorf("interface not enabled")
|
||||
}
|
||||
|
||||
if ui.targetAddr == nil {
|
||||
return fmt.Errorf("no target address configured")
|
||||
}
|
||||
|
||||
_, err := ui.conn.WriteTo(data, ui.targetAddr)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||
ui.mutex.Lock()
|
||||
defer ui.mutex.Unlock()
|
||||
ui.packetCallback = callback
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetPacketCallback() common.PacketCallback {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
return ui.packetCallback
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) ProcessIncoming(data []byte) {
|
||||
if callback := ui.GetPacketCallback(); callback != nil {
|
||||
callback(data, ui)
|
||||
}
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
|
||||
if !ui.Online || ui.targetAddr == nil {
|
||||
return fmt.Errorf("interface offline or no target address configured")
|
||||
if !ui.IsOnline() {
|
||||
return fmt.Errorf("interface offline")
|
||||
}
|
||||
|
||||
if ui.targetAddr == nil {
|
||||
return fmt.Errorf("no target address configured")
|
||||
}
|
||||
|
||||
_, err := ui.conn.WriteToUDP(data, ui.targetAddr)
|
||||
@@ -90,13 +119,80 @@ func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
|
||||
return fmt.Errorf("UDP write failed: %v", err)
|
||||
}
|
||||
|
||||
ui.Interface.ProcessOutgoing(data)
|
||||
ui.mutex.Lock()
|
||||
ui.TxBytes += uint64(len(data))
|
||||
ui.mutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Detach() {
|
||||
ui.Interface.Detach()
|
||||
if ui.conn != nil {
|
||||
ui.conn.Close()
|
||||
func (ui *UDPInterface) GetConn() net.Conn {
|
||||
return ui.conn
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetTxBytes() uint64 {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
return ui.TxBytes
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetRxBytes() uint64 {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
return ui.RxBytes
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetMTU() int {
|
||||
return ui.MTU
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetBitrate() int {
|
||||
return int(ui.Bitrate)
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Enable() {
|
||||
ui.mutex.Lock()
|
||||
defer ui.mutex.Unlock()
|
||||
ui.Online = true
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Disable() {
|
||||
ui.mutex.Lock()
|
||||
defer ui.mutex.Unlock()
|
||||
ui.Online = false
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Start() error {
|
||||
conn, err := net.ListenUDP("udp", ui.addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ui.conn = conn
|
||||
ui.Online = true
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
func (ui *UDPInterface) readLoop() {
|
||||
buffer := make([]byte, ui.MTU)
|
||||
for {
|
||||
n, _, err := ui.conn.ReadFromUDP(buffer)
|
||||
if err != nil {
|
||||
if ui.Online {
|
||||
log.Printf("Error reading from UDP interface %s: %v", ui.Name, err)
|
||||
ui.Stop() // Consider if stopping is the right action or just log and continue
|
||||
}
|
||||
return
|
||||
}
|
||||
if ui.packetCallback != nil {
|
||||
ui.packetCallback(buffer[:n], ui)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func (ui *UDPInterface) IsEnabled() bool {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
return ui.Enabled && ui.Online && !ui.Detached
|
||||
}
|
||||
|
||||
93
pkg/interfaces/udp_test.go
Normal file
93
pkg/interfaces/udp_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
)
|
||||
|
||||
func TestNewUDPInterface(t *testing.T) {
|
||||
validAddr := "127.0.0.1:0" // Use port 0 for OS to assign a free port
|
||||
validTarget := "127.0.0.1:8080"
|
||||
invalidAddr := "invalid-address"
|
||||
|
||||
t.Run("ValidConfig", func(t *testing.T) {
|
||||
ui, err := NewUDPInterface("udpValid", validAddr, validTarget, true)
|
||||
if err != nil {
|
||||
t.Fatalf("NewUDPInterface failed with valid config: %v", err)
|
||||
}
|
||||
if ui == nil {
|
||||
t.Fatal("NewUDPInterface returned nil interface with valid config")
|
||||
}
|
||||
if ui.GetName() != "udpValid" {
|
||||
t.Errorf("GetName() = %s; want udpValid", ui.GetName())
|
||||
}
|
||||
if ui.GetType() != common.IF_TYPE_UDP {
|
||||
t.Errorf("GetType() = %v; want %v", ui.GetType(), common.IF_TYPE_UDP)
|
||||
}
|
||||
if ui.addr.String() != validAddr && ui.addr.Port == 0 { // Check if address resolved, port 0 is special
|
||||
// Allow OS-assigned port if 0 was specified
|
||||
} else if ui.addr.String() != validAddr {
|
||||
// t.Errorf("Resolved addr = %s; want %s", ui.addr.String(), validAddr) //This check is flaky with port 0
|
||||
}
|
||||
if ui.targetAddr.String() != validTarget {
|
||||
t.Errorf("Resolved targetAddr = %s; want %s", ui.targetAddr.String(), validTarget)
|
||||
}
|
||||
if !ui.Enabled { // BaseInterface field
|
||||
t.Error("Interface not enabled by default when requested")
|
||||
}
|
||||
if ui.IsOnline() { // Should be offline initially
|
||||
t.Error("Interface online initially")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ValidConfigNoTarget", func(t *testing.T) {
|
||||
ui, err := NewUDPInterface("udpNoTarget", validAddr, "", true)
|
||||
if err != nil {
|
||||
t.Fatalf("NewUDPInterface failed with valid config (no target): %v", err)
|
||||
}
|
||||
if ui == nil {
|
||||
t.Fatal("NewUDPInterface returned nil interface with valid config (no target)")
|
||||
}
|
||||
if ui.targetAddr != nil {
|
||||
t.Errorf("targetAddr = %v; want nil", ui.targetAddr)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidAddress", func(t *testing.T) {
|
||||
_, err := NewUDPInterface("udpInvalidAddr", invalidAddr, validTarget, true)
|
||||
if err == nil {
|
||||
t.Error("NewUDPInterface succeeded with invalid address")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidTarget", func(t *testing.T) {
|
||||
_, err := NewUDPInterface("udpInvalidTarget", validAddr, invalidAddr, true)
|
||||
if err == nil {
|
||||
t.Error("NewUDPInterface succeeded with invalid target address")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUDPInterfaceState(t *testing.T) {
|
||||
// Basic state tests are covered by BaseInterface tests
|
||||
// Add specific UDP ones if needed, e.g., involving the conn
|
||||
addr := "127.0.0.1:0"
|
||||
ui, _ := NewUDPInterface("udpState", addr, "", true)
|
||||
|
||||
if ui.conn != nil {
|
||||
t.Error("conn field is not nil before Start()")
|
||||
}
|
||||
|
||||
// We don't call Start() here because it requires actual network binding
|
||||
// Testing Send requires Start() and a listener, which is too complex for unit tests here
|
||||
|
||||
// Test Detach
|
||||
ui.Detach()
|
||||
if !ui.IsDetached() {
|
||||
t.Error("IsDetached() is false after Detach()")
|
||||
}
|
||||
|
||||
// Further tests on Send/ProcessOutgoing/readLoop would require mocking net.UDPConn
|
||||
// or setting up a local listener.
|
||||
}
|
||||
661
pkg/link/link.go
661
pkg/link/link.go
@@ -5,104 +5,183 @@ import (
|
||||
"crypto/cipher"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/destination"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/packet"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/pathfinder"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/resolver"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/resource"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
||||
)
|
||||
|
||||
const (
|
||||
CURVE = "Curve25519"
|
||||
|
||||
ESTABLISHMENT_TIMEOUT_PER_HOP = 6
|
||||
KEEPALIVE_TIMEOUT_FACTOR = 4
|
||||
STALE_GRACE = 2
|
||||
KEEPALIVE = 360
|
||||
STALE_TIME = 720
|
||||
KEEPALIVE_TIMEOUT_FACTOR = 4
|
||||
STALE_GRACE = 2
|
||||
KEEPALIVE = 360
|
||||
STALE_TIME = 720
|
||||
|
||||
ACCEPT_NONE = 0x00
|
||||
ACCEPT_ALL = 0x01
|
||||
ACCEPT_APP = 0x02
|
||||
|
||||
STATUS_PENDING = 0x00
|
||||
STATUS_ACTIVE = 0x01
|
||||
STATUS_CLOSED = 0x02
|
||||
STATUS_FAILED = 0x03
|
||||
STATUS_PENDING = 0x00
|
||||
STATUS_ACTIVE = 0x01
|
||||
STATUS_CLOSED = 0x02
|
||||
STATUS_FAILED = 0x03
|
||||
|
||||
PROVE_NONE = 0x00
|
||||
PROVE_ALL = 0x01
|
||||
PROVE_APP = 0x02
|
||||
|
||||
WATCHDOG_MIN_SLEEP = 0.025
|
||||
WATCHDOG_INTERVAL = 0.1
|
||||
)
|
||||
|
||||
type Link struct {
|
||||
mutex sync.RWMutex
|
||||
destination interface{}
|
||||
status byte
|
||||
establishedAt time.Time
|
||||
lastInbound time.Time
|
||||
lastOutbound time.Time
|
||||
lastDataReceived time.Time
|
||||
lastDataSent time.Time
|
||||
|
||||
remoteIdentity *identity.Identity
|
||||
sessionKey []byte
|
||||
linkID []byte
|
||||
|
||||
mutex sync.RWMutex
|
||||
destination *destination.Destination
|
||||
status byte
|
||||
networkInterface common.NetworkInterface
|
||||
establishedAt time.Time
|
||||
lastInbound time.Time
|
||||
lastOutbound time.Time
|
||||
lastDataReceived time.Time
|
||||
lastDataSent time.Time
|
||||
pathFinder *pathfinder.PathFinder
|
||||
|
||||
remoteIdentity *identity.Identity
|
||||
sessionKey []byte
|
||||
linkID []byte
|
||||
|
||||
rtt float64
|
||||
establishmentRate float64
|
||||
|
||||
trackPhyStats bool
|
||||
rssi float64
|
||||
snr float64
|
||||
q float64
|
||||
|
||||
resourceStrategy byte
|
||||
|
||||
|
||||
establishedCallback func(*Link)
|
||||
closedCallback func(*Link)
|
||||
packetCallback func([]byte, *packet.Packet)
|
||||
resourceCallback func(interface{}) bool
|
||||
resourceStartedCallback func(interface{})
|
||||
closedCallback func(*Link)
|
||||
packetCallback func([]byte, *packet.Packet)
|
||||
identifiedCallback func(*Link, *identity.Identity)
|
||||
|
||||
teardownReason byte
|
||||
hmacKey []byte
|
||||
transport *transport.Transport
|
||||
|
||||
rssi float64
|
||||
snr float64
|
||||
q float64
|
||||
resourceCallback func(interface{}) bool
|
||||
resourceStartedCallback func(interface{})
|
||||
resourceConcludedCallback func(interface{})
|
||||
remoteIdentifiedCallback func(*Link, *identity.Identity)
|
||||
resourceStrategy byte
|
||||
proofStrategy byte
|
||||
proofCallback func(*packet.Packet) bool
|
||||
trackPhyStats bool
|
||||
|
||||
watchdogLock bool
|
||||
watchdogActive bool
|
||||
establishmentTimeout time.Duration
|
||||
keepalive time.Duration
|
||||
staleTime time.Duration
|
||||
initiator bool
|
||||
}
|
||||
|
||||
func New(dest interface{}, establishedCb func(*Link), closedCb func(*Link)) *Link {
|
||||
l := &Link{
|
||||
destination: dest,
|
||||
status: STATUS_PENDING,
|
||||
establishedAt: time.Time{},
|
||||
lastInbound: time.Time{},
|
||||
lastOutbound: time.Time{},
|
||||
lastDataReceived: time.Time{},
|
||||
lastDataSent: time.Time{},
|
||||
resourceStrategy: ACCEPT_NONE,
|
||||
establishedCallback: establishedCb,
|
||||
closedCallback: closedCb,
|
||||
func NewLink(dest *destination.Destination, transport *transport.Transport, networkIface common.NetworkInterface, establishedCallback func(*Link), closedCallback func(*Link)) *Link {
|
||||
return &Link{
|
||||
destination: dest,
|
||||
status: STATUS_PENDING,
|
||||
transport: transport,
|
||||
networkInterface: networkIface,
|
||||
establishedCallback: establishedCallback,
|
||||
closedCallback: closedCallback,
|
||||
establishedAt: time.Time{}, // Zero time until established
|
||||
lastInbound: time.Time{},
|
||||
lastOutbound: time.Time{},
|
||||
lastDataReceived: time.Time{},
|
||||
lastDataSent: time.Time{},
|
||||
pathFinder: pathfinder.NewPathFinder(),
|
||||
|
||||
watchdogLock: false,
|
||||
watchdogActive: false,
|
||||
establishmentTimeout: time.Duration(ESTABLISHMENT_TIMEOUT_PER_HOP * float64(time.Second)),
|
||||
keepalive: time.Duration(KEEPALIVE * float64(time.Second)),
|
||||
staleTime: time.Duration(STALE_TIME * float64(time.Second)),
|
||||
initiator: false,
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Link) Identify(id *identity.Identity) error {
|
||||
func (l *Link) Establish() error {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
if l.status != STATUS_ACTIVE {
|
||||
return errors.New("link not active")
|
||||
if l.status != STATUS_PENDING {
|
||||
log.Printf("[DEBUG-3] Cannot establish link: invalid status %d", l.status)
|
||||
return errors.New("link already established or failed")
|
||||
}
|
||||
|
||||
// Create identification message
|
||||
idMsg := append(id.GetPublicKey(), id.Sign(l.linkID)...)
|
||||
|
||||
// Encrypt and send identification
|
||||
err := l.SendPacket(idMsg)
|
||||
if err != nil {
|
||||
destPublicKey := l.destination.GetPublicKey()
|
||||
if destPublicKey == nil {
|
||||
log.Printf("[DEBUG-3] Cannot establish link: destination has no public key")
|
||||
return errors.New("destination has no public key")
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-4] Creating link request packet for destination %x", destPublicKey[:8])
|
||||
|
||||
p := &packet.Packet{
|
||||
HeaderType: packet.HeaderType1,
|
||||
PacketType: packet.PacketTypeLinkReq,
|
||||
TransportType: 0,
|
||||
Context: packet.ContextLinkIdentify,
|
||||
ContextFlag: packet.FlagUnset,
|
||||
Hops: 0,
|
||||
DestinationType: l.destination.GetType(),
|
||||
DestinationHash: l.destination.GetHash(),
|
||||
Data: l.linkID,
|
||||
CreateReceipt: true,
|
||||
}
|
||||
|
||||
if err := p.Pack(); err != nil {
|
||||
log.Printf("[DEBUG-3] Failed to pack link request packet: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
log.Printf("[DEBUG-4] Sending link request packet with ID %x", l.linkID[:8])
|
||||
return l.transport.SendPacket(p)
|
||||
}
|
||||
|
||||
func (l *Link) Identify(id *identity.Identity) error {
|
||||
if !l.IsActive() {
|
||||
return errors.New("link not active")
|
||||
}
|
||||
|
||||
p := &packet.Packet{
|
||||
HeaderType: packet.HeaderType1,
|
||||
PacketType: packet.PacketTypeData,
|
||||
TransportType: 0,
|
||||
Context: packet.ContextLinkIdentify,
|
||||
ContextFlag: packet.FlagUnset,
|
||||
Hops: 0,
|
||||
DestinationType: l.destination.GetType(),
|
||||
DestinationHash: l.destination.GetHash(),
|
||||
Data: id.GetPublicKey(),
|
||||
CreateReceipt: true,
|
||||
}
|
||||
|
||||
if err := p.Pack(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.transport.SendPacket(p)
|
||||
}
|
||||
|
||||
func (l *Link) HandleIdentification(data []byte) error {
|
||||
@@ -110,26 +189,33 @@ func (l *Link) HandleIdentification(data []byte) error {
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
if len(data) < ed25519.PublicKeySize+ed25519.SignatureSize {
|
||||
return errors.New("invalid identification data")
|
||||
log.Printf("[DEBUG-3] Invalid identification data length: %d bytes", len(data))
|
||||
return errors.New("invalid identification data length")
|
||||
}
|
||||
|
||||
pubKey := data[:ed25519.PublicKeySize]
|
||||
signature := data[ed25519.PublicKeySize:]
|
||||
|
||||
remoteIdentity := &identity.Identity{}
|
||||
if !remoteIdentity.LoadPublicKey(pubKey) {
|
||||
return errors.New("invalid remote public key")
|
||||
log.Printf("[DEBUG-4] Processing identification from public key %x", pubKey[:8])
|
||||
|
||||
remoteIdentity := identity.FromPublicKey(pubKey)
|
||||
if remoteIdentity == nil {
|
||||
log.Printf("[DEBUG-3] Invalid remote identity from public key %x", pubKey[:8])
|
||||
return errors.New("invalid remote identity")
|
||||
}
|
||||
|
||||
// Verify signature of link ID
|
||||
if !remoteIdentity.Verify(l.linkID, signature) {
|
||||
return errors.New("invalid identification signature")
|
||||
signData := append(l.linkID, pubKey...)
|
||||
if !remoteIdentity.Verify(signData, signature) {
|
||||
log.Printf("[DEBUG-3] Invalid signature from remote identity %x", pubKey[:8])
|
||||
return errors.New("invalid signature")
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-4] Remote identity verified successfully: %x", pubKey[:8])
|
||||
l.remoteIdentity = remoteIdentity
|
||||
|
||||
if l.remoteIdentifiedCallback != nil {
|
||||
l.remoteIdentifiedCallback(l, remoteIdentity)
|
||||
if l.identifiedCallback != nil {
|
||||
log.Printf("[DEBUG-4] Executing identified callback for remote identity %x", pubKey[:8])
|
||||
l.identifiedCallback(l, remoteIdentity)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -158,8 +244,8 @@ func (l *Link) Request(path string, data []byte, timeout time.Duration) (*Reques
|
||||
|
||||
receipt := &RequestReceipt{
|
||||
requestID: requestID,
|
||||
status: STATUS_PENDING,
|
||||
sentAt: time.Now(),
|
||||
status: STATUS_PENDING,
|
||||
sentAt: time.Now(),
|
||||
}
|
||||
|
||||
// Send request
|
||||
@@ -184,12 +270,12 @@ func (l *Link) Request(path string, data []byte, timeout time.Duration) (*Reques
|
||||
}
|
||||
|
||||
type RequestReceipt struct {
|
||||
mutex sync.RWMutex
|
||||
requestID []byte
|
||||
status byte
|
||||
sentAt time.Time
|
||||
receivedAt time.Time
|
||||
response []byte
|
||||
mutex sync.RWMutex
|
||||
requestID []byte
|
||||
status byte
|
||||
sentAt time.Time
|
||||
receivedAt time.Time
|
||||
response []byte
|
||||
}
|
||||
|
||||
func (r *RequestReceipt) GetRequestID() []byte {
|
||||
@@ -233,21 +319,40 @@ func (l *Link) TrackPhyStats(track bool) {
|
||||
l.trackPhyStats = track
|
||||
}
|
||||
|
||||
func (l *Link) UpdatePhyStats(rssi, snr, q float64) {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
if l.trackPhyStats {
|
||||
l.rssi = rssi
|
||||
l.snr = snr
|
||||
l.q = q
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Link) GetRSSI() float64 {
|
||||
l.mutex.RLock()
|
||||
defer l.mutex.RUnlock()
|
||||
if !l.trackPhyStats {
|
||||
return 0
|
||||
}
|
||||
return l.rssi
|
||||
}
|
||||
|
||||
func (l *Link) GetSNR() float64 {
|
||||
l.mutex.RLock()
|
||||
defer l.mutex.RUnlock()
|
||||
if !l.trackPhyStats {
|
||||
return 0
|
||||
}
|
||||
return l.snr
|
||||
}
|
||||
|
||||
func (l *Link) GetQ() float64 {
|
||||
l.mutex.RLock()
|
||||
defer l.mutex.RUnlock()
|
||||
if !l.trackPhyStats {
|
||||
return 0
|
||||
}
|
||||
return l.q
|
||||
}
|
||||
|
||||
@@ -319,7 +424,7 @@ func (l *Link) GetRemoteIdentity() *identity.Identity {
|
||||
func (l *Link) Teardown() {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
|
||||
if l.status == STATUS_ACTIVE {
|
||||
l.status = STATUS_CLOSED
|
||||
if l.closedCallback != nil {
|
||||
@@ -361,85 +466,58 @@ func (l *Link) SetResourceConcludedCallback(callback func(interface{})) {
|
||||
func (l *Link) SetRemoteIdentifiedCallback(callback func(*Link, *identity.Identity)) {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
l.remoteIdentifiedCallback = callback
|
||||
l.identifiedCallback = callback
|
||||
}
|
||||
|
||||
func (l *Link) SetResourceStrategy(strategy byte) error {
|
||||
if strategy != ACCEPT_NONE && strategy != ACCEPT_ALL && strategy != ACCEPT_APP {
|
||||
return errors.New("unsupported resource strategy")
|
||||
}
|
||||
|
||||
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
l.resourceStrategy = strategy
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewLink(destination interface{}, establishedCallback func(*Link), closedCallback func(*Link)) *Link {
|
||||
l := &Link{
|
||||
destination: destination,
|
||||
status: STATUS_PENDING,
|
||||
establishedAt: time.Time{},
|
||||
lastInbound: time.Time{},
|
||||
lastOutbound: time.Time{},
|
||||
lastDataReceived: time.Time{},
|
||||
lastDataSent: time.Time{},
|
||||
establishedCallback: establishedCallback,
|
||||
closedCallback: closedCallback,
|
||||
resourceStrategy: ACCEPT_NONE,
|
||||
trackPhyStats: false,
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Link) Establish() error {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
if l.status != STATUS_PENDING {
|
||||
return errors.New("link already established or failed")
|
||||
}
|
||||
|
||||
// Generate session key using ECDH
|
||||
ephemeralKey := make([]byte, 32)
|
||||
if _, err := rand.Read(ephemeralKey); err != nil {
|
||||
return err
|
||||
}
|
||||
l.sessionKey = ephemeralKey
|
||||
|
||||
l.establishedAt = time.Now()
|
||||
l.status = STATUS_ACTIVE
|
||||
|
||||
if l.establishedCallback != nil {
|
||||
l.establishedCallback(l)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Link) SendPacket(data []byte) error {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
if l.status != STATUS_ACTIVE {
|
||||
log.Printf("[DEBUG-3] Cannot send packet: link not active (status: %d)", l.status)
|
||||
return errors.New("link not active")
|
||||
}
|
||||
|
||||
// Encrypt data using session key
|
||||
encryptedData, err := l.encrypt(data)
|
||||
log.Printf("[DEBUG-4] Encrypting packet of %d bytes", len(data))
|
||||
encrypted, err := l.encrypt(data)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-3] Failed to encrypt packet: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
p := &packet.Packet{
|
||||
HeaderType: packet.HeaderType1,
|
||||
PacketType: packet.PacketTypeData,
|
||||
TransportType: 0,
|
||||
Context: packet.ContextNone,
|
||||
ContextFlag: packet.FlagUnset,
|
||||
Hops: 0,
|
||||
DestinationType: l.destination.GetType(),
|
||||
DestinationHash: l.destination.GetHash(),
|
||||
Data: encrypted,
|
||||
CreateReceipt: false,
|
||||
}
|
||||
|
||||
if err := p.Pack(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-4] Sending encrypted packet of %d bytes", len(encrypted))
|
||||
l.lastOutbound = time.Now()
|
||||
l.lastDataSent = time.Now()
|
||||
|
||||
if l.packetCallback != nil {
|
||||
l.packetCallback(encryptedData, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
return l.transport.SendPacket(p)
|
||||
}
|
||||
|
||||
func (l *Link) HandleInbound(data []byte) error {
|
||||
@@ -447,20 +525,28 @@ func (l *Link) HandleInbound(data []byte) error {
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
if l.status != STATUS_ACTIVE {
|
||||
log.Printf("[DEBUG-3] Dropping inbound packet: link not active (status: %d)", l.status)
|
||||
return errors.New("link not active")
|
||||
}
|
||||
|
||||
// Decrypt data using session key
|
||||
decryptedData, err := l.decrypt(data)
|
||||
if err != nil {
|
||||
return err
|
||||
// Decode and log packet details
|
||||
l.decodePacket(data)
|
||||
|
||||
// Decrypt if we have a session key
|
||||
if l.sessionKey != nil {
|
||||
decrypted, err := l.decrypt(data)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-3] Failed to decrypt packet: %v", err)
|
||||
return err
|
||||
}
|
||||
data = decrypted
|
||||
}
|
||||
|
||||
l.lastInbound = time.Now()
|
||||
l.lastDataReceived = time.Now()
|
||||
|
||||
if l.packetCallback != nil {
|
||||
l.packetCallback(decryptedData, nil)
|
||||
l.packetCallback(data, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -476,17 +562,27 @@ func (l *Link) encrypt(data []byte) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
// Generate IV
|
||||
iv := make([]byte, aes.BlockSize)
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, err
|
||||
// Add PKCS7 padding
|
||||
padding := aes.BlockSize - len(data)%aes.BlockSize
|
||||
padtext := make([]byte, len(data)+padding)
|
||||
copy(padtext, data)
|
||||
for i := len(data); i < len(padtext); i++ {
|
||||
padtext[i] = byte(padding)
|
||||
}
|
||||
|
||||
return gcm.Seal(nonce, nonce, data, nil), nil
|
||||
// Encrypt
|
||||
mode := cipher.NewCBCEncrypter(block, iv) // #nosec G407
|
||||
ciphertext := make([]byte, len(padtext))
|
||||
mode.CryptBlocks(ciphertext, padtext)
|
||||
|
||||
// Prepend IV to ciphertext
|
||||
return append(iv, ciphertext...), nil
|
||||
}
|
||||
|
||||
func (l *Link) decrypt(data []byte) ([]byte, error) {
|
||||
@@ -499,31 +595,34 @@ func (l *Link) decrypt(data []byte) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
if len(data) < aes.BlockSize {
|
||||
return nil, errors.New("ciphertext too short")
|
||||
}
|
||||
|
||||
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
|
||||
return gcm.Open(nil, nonce, ciphertext, nil)
|
||||
}
|
||||
iv := data[:aes.BlockSize]
|
||||
ciphertext := data[aes.BlockSize:]
|
||||
|
||||
func (l *Link) UpdatePhyStats(rssi float64, snr float64, q float64) {
|
||||
if !l.trackPhyStats {
|
||||
return
|
||||
if len(ciphertext)%aes.BlockSize != 0 {
|
||||
return nil, errors.New("ciphertext is not a multiple of block size")
|
||||
}
|
||||
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
l.rssi = rssi
|
||||
l.snr = snr
|
||||
l.q = q
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
plaintext := make([]byte, len(ciphertext))
|
||||
mode.CryptBlocks(plaintext, ciphertext)
|
||||
|
||||
// Remove PKCS7 padding
|
||||
padding := int(plaintext[len(plaintext)-1])
|
||||
if padding > aes.BlockSize || padding == 0 {
|
||||
return nil, errors.New("invalid padding")
|
||||
}
|
||||
|
||||
for i := len(plaintext) - padding; i < len(plaintext); i++ {
|
||||
if plaintext[i] != byte(padding) {
|
||||
return nil, errors.New("invalid padding")
|
||||
}
|
||||
}
|
||||
|
||||
return plaintext[:len(plaintext)-padding], nil
|
||||
}
|
||||
|
||||
func (l *Link) GetRTT() float64 {
|
||||
@@ -546,4 +645,242 @@ func (l *Link) GetStatus() byte {
|
||||
|
||||
func (l *Link) IsActive() bool {
|
||||
return l.GetStatus() == STATUS_ACTIVE
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Link) SendResource(res *resource.Resource) error {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
if l.status != STATUS_ACTIVE {
|
||||
l.teardownReason = STATUS_FAILED
|
||||
return errors.New("link not active")
|
||||
}
|
||||
|
||||
// Activate the resource
|
||||
res.Activate()
|
||||
|
||||
// Send the resource data as packets
|
||||
buffer := make([]byte, resource.DEFAULT_SEGMENT_SIZE)
|
||||
for {
|
||||
n, err := res.Read(buffer)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
l.teardownReason = STATUS_FAILED
|
||||
return fmt.Errorf("error reading resource: %v", err)
|
||||
}
|
||||
|
||||
if err := l.SendPacket(buffer[:n]); err != nil {
|
||||
l.teardownReason = STATUS_FAILED
|
||||
return fmt.Errorf("error sending resource packet: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Link) maintainLink() {
|
||||
ticker := time.NewTicker(time.Second * KEEPALIVE)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if l.status != STATUS_ACTIVE {
|
||||
return
|
||||
}
|
||||
|
||||
inactiveTime := l.InactiveFor()
|
||||
if inactiveTime > float64(STALE_TIME) {
|
||||
l.mutex.Lock()
|
||||
l.teardownReason = STATUS_FAILED
|
||||
l.mutex.Unlock()
|
||||
l.Teardown()
|
||||
return
|
||||
}
|
||||
|
||||
noDataTime := l.NoDataFor()
|
||||
if noDataTime > float64(KEEPALIVE) {
|
||||
l.mutex.Lock()
|
||||
err := l.SendPacket([]byte{})
|
||||
if err != nil {
|
||||
l.teardownReason = STATUS_FAILED
|
||||
l.mutex.Unlock()
|
||||
l.Teardown()
|
||||
return
|
||||
}
|
||||
l.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Link) Start() {
|
||||
go l.maintainLink()
|
||||
}
|
||||
|
||||
func (l *Link) SetProofStrategy(strategy byte) error {
|
||||
if strategy != PROVE_NONE && strategy != PROVE_ALL && strategy != PROVE_APP {
|
||||
return errors.New("invalid proof strategy")
|
||||
}
|
||||
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
l.proofStrategy = strategy
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Link) SetProofCallback(callback func(*packet.Packet) bool) {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
l.proofCallback = callback
|
||||
}
|
||||
|
||||
func (l *Link) HandleProofRequest(packet *packet.Packet) bool {
|
||||
l.mutex.RLock()
|
||||
defer l.mutex.RUnlock()
|
||||
|
||||
switch l.proofStrategy {
|
||||
case PROVE_NONE:
|
||||
return false
|
||||
case PROVE_ALL:
|
||||
return true
|
||||
case PROVE_APP:
|
||||
if l.proofCallback != nil {
|
||||
return l.proofCallback(packet)
|
||||
}
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Link) decodePacket(data []byte) {
|
||||
if len(data) < 1 {
|
||||
log.Printf("[DEBUG-7] Invalid packet: zero length")
|
||||
return
|
||||
}
|
||||
|
||||
packetType := data[0]
|
||||
log.Printf("[DEBUG-7] Packet Analysis:")
|
||||
log.Printf("[DEBUG-7] - Size: %d bytes", len(data))
|
||||
log.Printf("[DEBUG-7] - Type: 0x%02x", packetType)
|
||||
|
||||
switch packetType {
|
||||
case packet.PacketTypeData:
|
||||
log.Printf("[DEBUG-7] - Type Description: Data Packet")
|
||||
if len(data) > 1 {
|
||||
log.Printf("[DEBUG-7] - Payload Size: %d bytes", len(data)-1)
|
||||
}
|
||||
|
||||
case packet.PacketTypeLinkReq:
|
||||
log.Printf("[DEBUG-7] - Type Description: Link Management")
|
||||
if len(data) > 32 {
|
||||
log.Printf("[DEBUG-7] - Link ID: %x", data[1:33])
|
||||
}
|
||||
|
||||
case packet.PacketTypeAnnounce:
|
||||
log.Printf("[DEBUG-7] Received announce packet (%d bytes)", len(data))
|
||||
if len(data) < packet.MinAnnounceSize {
|
||||
log.Printf("[DEBUG-3] Announce packet too short: %d bytes", len(data))
|
||||
return
|
||||
}
|
||||
|
||||
destHash := data[2:18]
|
||||
encKey := data[18:50]
|
||||
signKey := data[50:82]
|
||||
nameHash := data[82:92]
|
||||
randomHash := data[92:102]
|
||||
signature := data[102:166]
|
||||
appData := data[166:]
|
||||
|
||||
pubKey := append(encKey, signKey...)
|
||||
|
||||
validationData := make([]byte, 0, 164)
|
||||
validationData = append(validationData, destHash...)
|
||||
validationData = append(validationData, encKey...)
|
||||
validationData = append(validationData, signKey...)
|
||||
validationData = append(validationData, nameHash...)
|
||||
validationData = append(validationData, randomHash...)
|
||||
|
||||
if identity.ValidateAnnounce(validationData, destHash, pubKey, signature, appData) {
|
||||
log.Printf("[DEBUG-4] Valid announce from %x", pubKey[:8])
|
||||
if err := l.transport.HandleAnnounce(destHash, l.networkInterface); err != nil {
|
||||
log.Printf("[DEBUG-3] Failed to handle announce: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[DEBUG-3] Invalid announce signature from %x", pubKey[:8])
|
||||
}
|
||||
|
||||
case packet.PacketTypeProof:
|
||||
log.Printf("[DEBUG-7] - Type Description: RNS Discovery")
|
||||
if len(data) > 17 {
|
||||
searchHash := data[1:17]
|
||||
log.Printf("[DEBUG-7] - Searching for Hash: %x", searchHash)
|
||||
|
||||
if id, err := resolver.ResolveIdentity(hex.EncodeToString(searchHash)); err == nil {
|
||||
log.Printf("[DEBUG-7] - Found matching identity: %s", id.GetHexHash())
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
log.Printf("[DEBUG-7] - Type Description: Unknown (0x%02x)", packetType)
|
||||
log.Printf("[DEBUG-7] - Raw Hex: %x", data)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function for min of two ints
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (l *Link) startWatchdog() {
|
||||
if l.watchdogActive {
|
||||
return
|
||||
}
|
||||
|
||||
l.watchdogActive = true
|
||||
go l.watchdog()
|
||||
}
|
||||
|
||||
func (l *Link) watchdog() {
|
||||
for l.status != STATUS_CLOSED {
|
||||
l.mutex.Lock()
|
||||
if l.watchdogLock {
|
||||
l.mutex.Unlock()
|
||||
time.Sleep(time.Duration(WATCHDOG_MIN_SLEEP * float64(time.Second)))
|
||||
continue
|
||||
}
|
||||
|
||||
var sleepTime float64 = WATCHDOG_INTERVAL
|
||||
|
||||
switch l.status {
|
||||
case STATUS_ACTIVE:
|
||||
lastActivity := l.lastInbound
|
||||
if l.lastOutbound.After(lastActivity) {
|
||||
lastActivity = l.lastOutbound
|
||||
}
|
||||
|
||||
if time.Since(lastActivity) > l.keepalive {
|
||||
if l.initiator {
|
||||
if err := l.SendPacket([]byte{}); err != nil { // #nosec G104
|
||||
log.Printf("[DEBUG-3] Failed to send keepalive packet: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if time.Since(lastActivity) > l.staleTime {
|
||||
l.status = STATUS_CLOSED
|
||||
l.teardownReason = STATUS_FAILED
|
||||
if l.closedCallback != nil {
|
||||
l.closedCallback(l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
l.mutex.Unlock()
|
||||
time.Sleep(time.Duration(sleepTime * float64(time.Second)))
|
||||
}
|
||||
l.watchdogActive = false
|
||||
}
|
||||
|
||||
@@ -3,11 +3,7 @@ package packet
|
||||
const (
|
||||
// MTU constants
|
||||
EncryptedMDU = 383 // Maximum size of payload data in encrypted packet
|
||||
PlainMDU = 464 // Maximum size of payload data in unencrypted packet
|
||||
|
||||
// Header Types
|
||||
HeaderType1 = 0 // Two byte header, one 16 byte address field
|
||||
HeaderType2 = 1 // Two byte header, two 16 byte address fields
|
||||
PlainMDU = 464 // Maximum size of payload data in unencrypted packet
|
||||
|
||||
// Propagation Types
|
||||
PropagationBroadcast = 0
|
||||
@@ -19,9 +15,7 @@ const (
|
||||
DestinationPlain = 2
|
||||
DestinationLink = 3
|
||||
|
||||
// Packet Types
|
||||
PacketData = 0
|
||||
PacketAnnounce = 1
|
||||
PacketLinkRequest = 2
|
||||
PacketProof = 3
|
||||
)
|
||||
// Minimum packet sizes
|
||||
MinAnnounceSize = 170 // header(2) + desthash(16) + context(1) + enckey(32) + signkey(32) +
|
||||
// namehash(10) + randomhash(10) + signature(64) + min appdata(3)
|
||||
)
|
||||
|
||||
@@ -1,171 +1,323 @@
|
||||
package packet
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderSize = 2
|
||||
AddressSize = 16
|
||||
ContextSize = 1
|
||||
MaxDataSize = 465 // Maximum size of payload data
|
||||
)
|
||||
// Packet Types
|
||||
PacketTypeData = 0x00
|
||||
PacketTypeAnnounce = 0x01
|
||||
PacketTypeLinkReq = 0x02
|
||||
PacketTypeProof = 0x03
|
||||
|
||||
// Header flags and types
|
||||
const (
|
||||
// First byte flags
|
||||
IFACFlag = 0x80 // Interface authentication code flag
|
||||
HeaderTypeFlag = 0x40 // Header type flag
|
||||
ContextFlag = 0x20 // Context flag
|
||||
PropagationFlags = 0x18 // Propagation type flags (bits 3-4)
|
||||
DestinationFlags = 0x06 // Destination type flags (bits 1-2)
|
||||
PacketTypeFlags = 0x01 // Packet type flags (bit 0)
|
||||
// Header Types
|
||||
HeaderType1 = 0x00
|
||||
HeaderType2 = 0x01
|
||||
|
||||
// Second byte
|
||||
HopsField = 0xFF // Number of hops (entire byte)
|
||||
// Context Types
|
||||
ContextNone = 0x00
|
||||
ContextResource = 0x01
|
||||
ContextResourceAdv = 0x02
|
||||
ContextResourceReq = 0x03
|
||||
ContextResourceHMU = 0x04
|
||||
ContextResourcePRF = 0x05
|
||||
ContextResourceICL = 0x06
|
||||
ContextResourceRCL = 0x07
|
||||
ContextCacheReq = 0x08
|
||||
ContextRequest = 0x09
|
||||
ContextResponse = 0x0A
|
||||
ContextPathResponse = 0x0B
|
||||
ContextCommand = 0x0C
|
||||
ContextCmdStatus = 0x0D
|
||||
ContextChannel = 0x0E
|
||||
ContextKeepalive = 0xFA
|
||||
ContextLinkIdentify = 0xFB
|
||||
ContextLinkClose = 0xFC
|
||||
ContextLinkProof = 0xFD
|
||||
ContextLRRTT = 0xFE
|
||||
ContextLRProof = 0xFF
|
||||
|
||||
// Flag Values
|
||||
FlagSet = 0x01
|
||||
FlagUnset = 0x00
|
||||
|
||||
// Header sizes
|
||||
HeaderMaxSize = 64
|
||||
MTU = 500
|
||||
|
||||
AddressSize = 32 // Size of address/hash fields in bytes
|
||||
)
|
||||
|
||||
type Packet struct {
|
||||
Header [2]byte
|
||||
Addresses []byte // Either 16 or 32 bytes depending on header type
|
||||
Context byte
|
||||
Data []byte
|
||||
AccessCode []byte // Optional: Only present if IFAC flag is set
|
||||
HeaderType byte
|
||||
PacketType byte
|
||||
TransportType byte
|
||||
Context byte
|
||||
ContextFlag byte
|
||||
Hops byte
|
||||
|
||||
DestinationType byte
|
||||
DestinationHash []byte
|
||||
TransportID []byte
|
||||
Data []byte
|
||||
|
||||
Raw []byte
|
||||
Packed bool
|
||||
Sent bool
|
||||
CreateReceipt bool
|
||||
FromPacked bool
|
||||
|
||||
SentAt time.Time
|
||||
PacketHash []byte
|
||||
RatchetID []byte
|
||||
|
||||
RSSI *float64
|
||||
SNR *float64
|
||||
Q *float64
|
||||
|
||||
Addresses []byte
|
||||
}
|
||||
|
||||
func NewPacket(headerType, propagationType, destinationType, packetType byte, hops byte) *Packet {
|
||||
p := &Packet{
|
||||
Header: [2]byte{0, hops},
|
||||
Addresses: make([]byte, 0),
|
||||
Data: make([]byte, 0),
|
||||
func NewPacket(destType byte, data []byte, packetType byte, context byte,
|
||||
transportType byte, headerType byte, transportID []byte, createReceipt bool,
|
||||
contextFlag byte) *Packet {
|
||||
|
||||
return &Packet{
|
||||
HeaderType: headerType,
|
||||
PacketType: packetType,
|
||||
TransportType: transportType,
|
||||
Context: context,
|
||||
ContextFlag: contextFlag,
|
||||
Hops: 0,
|
||||
DestinationType: destType,
|
||||
Data: data,
|
||||
TransportID: transportID,
|
||||
CreateReceipt: createReceipt,
|
||||
Packed: false,
|
||||
Sent: false,
|
||||
FromPacked: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Packet) Pack() error {
|
||||
if p.Packed {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set header type
|
||||
if headerType == HeaderType2 {
|
||||
p.Header[0] |= HeaderTypeFlag
|
||||
p.Addresses = make([]byte, 2*AddressSize) // Two address fields
|
||||
log.Printf("[DEBUG-6] Packing packet: type=%d, header=%d", p.PacketType, p.HeaderType)
|
||||
|
||||
// Create header byte (Corrected order)
|
||||
flags := byte(0)
|
||||
flags |= (p.HeaderType << 6) & 0b01000000
|
||||
flags |= (p.ContextFlag << 5) & 0b00100000
|
||||
flags |= (p.TransportType << 4) & 0b00010000
|
||||
flags |= (p.DestinationType << 2) & 0b00001100
|
||||
flags |= p.PacketType & 0b00000011
|
||||
|
||||
header := []byte{flags, p.Hops}
|
||||
log.Printf("[DEBUG-5] Created packet header: flags=%08b, hops=%d", flags, p.Hops)
|
||||
|
||||
if p.HeaderType == HeaderType2 {
|
||||
if p.TransportID == nil {
|
||||
return errors.New("transport ID required for header type 2")
|
||||
}
|
||||
header = append(header, p.TransportID...)
|
||||
log.Printf("[DEBUG-7] Added transport ID to header: %x", p.TransportID)
|
||||
}
|
||||
|
||||
header = append(header, p.DestinationHash...)
|
||||
header = append(header, p.Context)
|
||||
log.Printf("[DEBUG-6] Final header length: %d bytes", len(header))
|
||||
|
||||
p.Raw = append(header, p.Data...)
|
||||
log.Printf("[DEBUG-5] Final packet size: %d bytes", len(p.Raw))
|
||||
|
||||
if len(p.Raw) > MTU {
|
||||
return errors.New("packet size exceeds MTU")
|
||||
}
|
||||
|
||||
p.Packed = true
|
||||
p.updateHash()
|
||||
log.Printf("[DEBUG-7] Packet hash: %x", p.PacketHash)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Packet) Unpack() error {
|
||||
if len(p.Raw) < 3 {
|
||||
return errors.New("packet too short")
|
||||
}
|
||||
|
||||
flags := p.Raw[0]
|
||||
p.Hops = p.Raw[1]
|
||||
|
||||
p.HeaderType = (flags & 0b01000000) >> 6
|
||||
p.ContextFlag = (flags & 0b00100000) >> 5
|
||||
p.TransportType = (flags & 0b00010000) >> 4
|
||||
p.DestinationType = (flags & 0b00001100) >> 2
|
||||
p.PacketType = flags & 0b00000011
|
||||
|
||||
dstLen := 16 // Truncated hash length
|
||||
|
||||
if p.HeaderType == HeaderType2 {
|
||||
if len(p.Raw) < 2*dstLen+3 {
|
||||
return errors.New("packet too short for header type 2")
|
||||
}
|
||||
p.TransportID = p.Raw[2 : dstLen+2]
|
||||
p.DestinationHash = p.Raw[dstLen+2 : 2*dstLen+2]
|
||||
p.Context = p.Raw[2*dstLen+2]
|
||||
p.Data = p.Raw[2*dstLen+3:]
|
||||
} else {
|
||||
p.Addresses = make([]byte, AddressSize) // One address field
|
||||
if len(p.Raw) < dstLen+3 {
|
||||
return errors.New("packet too short for header type 1")
|
||||
}
|
||||
p.TransportID = nil
|
||||
p.DestinationHash = p.Raw[2 : dstLen+2]
|
||||
p.Context = p.Raw[dstLen+2]
|
||||
p.Data = p.Raw[dstLen+3:]
|
||||
}
|
||||
|
||||
// Set propagation type
|
||||
p.Header[0] |= (propagationType << 3) & PropagationFlags
|
||||
|
||||
// Set destination type
|
||||
p.Header[0] |= (destinationType << 1) & DestinationFlags
|
||||
|
||||
// Set packet type
|
||||
p.Header[0] |= packetType & PacketTypeFlags
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Packet) SetAccessCode(code []byte) {
|
||||
p.AccessCode = code
|
||||
p.Header[0] |= IFACFlag
|
||||
}
|
||||
|
||||
func (p *Packet) SetContext(context byte) {
|
||||
p.Context = context
|
||||
p.Header[0] |= ContextFlag
|
||||
}
|
||||
|
||||
func (p *Packet) SetData(data []byte) error {
|
||||
if len(data) > MaxDataSize {
|
||||
return errors.New("data exceeds maximum allowed size")
|
||||
}
|
||||
p.Data = data
|
||||
p.Packed = false
|
||||
p.updateHash()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Packet) SetAddress(index int, address []byte) error {
|
||||
if len(address) != AddressSize {
|
||||
return errors.New("invalid address size")
|
||||
func (p *Packet) GetHash() []byte {
|
||||
hashable := p.getHashablePart()
|
||||
hash := sha256.Sum256(hashable)
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
func (p *Packet) getHashablePart() []byte {
|
||||
hashable := []byte{p.Raw[0] & 0b00001111} // Lower 4 bits of flags
|
||||
if p.HeaderType == HeaderType2 {
|
||||
// Match Python: Start hash from DestHash (index 18), skipping TransportID
|
||||
dstLen := 16 // RNS.Identity.TRUNCATED_HASHLENGTH / 8
|
||||
startIndex := dstLen + 2
|
||||
if len(p.Raw) > startIndex {
|
||||
hashable = append(hashable, p.Raw[startIndex:]...)
|
||||
}
|
||||
} else {
|
||||
// Match Python: Start hash from DestHash (index 2)
|
||||
if len(p.Raw) > 2 {
|
||||
hashable = append(hashable, p.Raw[2:]...)
|
||||
}
|
||||
}
|
||||
|
||||
offset := index * AddressSize
|
||||
if offset+AddressSize > len(p.Addresses) {
|
||||
return errors.New("address index out of range")
|
||||
}
|
||||
|
||||
copy(p.Addresses[offset:], address)
|
||||
return nil
|
||||
return hashable
|
||||
}
|
||||
|
||||
func (p *Packet) updateHash() {
|
||||
p.PacketHash = p.GetHash()
|
||||
}
|
||||
|
||||
func (p *Packet) Serialize() ([]byte, error) {
|
||||
totalSize := HeaderSize + len(p.Addresses) + ContextSize + len(p.Data)
|
||||
if p.AccessCode != nil {
|
||||
totalSize += len(p.AccessCode)
|
||||
if !p.Packed {
|
||||
if err := p.Pack(); err != nil {
|
||||
return nil, fmt.Errorf("failed to pack packet: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
buffer := make([]byte, totalSize)
|
||||
offset := 0
|
||||
p.Addresses = p.DestinationHash
|
||||
|
||||
// Write header
|
||||
copy(buffer[offset:], p.Header[:])
|
||||
offset += HeaderSize
|
||||
|
||||
// Write access code if present
|
||||
if p.AccessCode != nil {
|
||||
copy(buffer[offset:], p.AccessCode)
|
||||
offset += len(p.AccessCode)
|
||||
}
|
||||
|
||||
// Write addresses
|
||||
copy(buffer[offset:], p.Addresses)
|
||||
offset += len(p.Addresses)
|
||||
|
||||
// Write context
|
||||
buffer[offset] = p.Context
|
||||
offset += ContextSize
|
||||
|
||||
// Write data
|
||||
copy(buffer[offset:], p.Data)
|
||||
|
||||
return buffer, nil
|
||||
return p.Raw, nil
|
||||
}
|
||||
|
||||
func ParsePacket(data []byte) (*Packet, error) {
|
||||
if len(data) < HeaderSize {
|
||||
return nil, errors.New("packet data too short")
|
||||
func NewAnnouncePacket(destHash []byte, identity *identity.Identity, appData []byte, transportID []byte) (*Packet, error) {
|
||||
log.Printf("[DEBUG-7] Creating new announce packet: destHash=%x, appData=%s", destHash, fmt.Sprintf("%x", appData))
|
||||
|
||||
// Get public key separated into encryption and signing keys
|
||||
pubKey := identity.GetPublicKey()
|
||||
encKey := pubKey[:32]
|
||||
signKey := pubKey[32:]
|
||||
log.Printf("[DEBUG-6] Using public keys: encKey=%x, signKey=%x", encKey, signKey)
|
||||
|
||||
// Parse app name from first msgpack element if possible
|
||||
// For nodes, we'll use "reticulum.node" as the name hash
|
||||
var appName string
|
||||
if len(appData) > 2 && appData[0] == 0x93 {
|
||||
// This is a node announce, use standard node name
|
||||
appName = "reticulum.node"
|
||||
} else if len(appData) > 3 && appData[0] == 0x92 && appData[1] == 0xc4 {
|
||||
// Try to extract name from peer announce appData
|
||||
nameLen := int(appData[2])
|
||||
if 3+nameLen <= len(appData) {
|
||||
appName = string(appData[3 : 3+nameLen])
|
||||
} else {
|
||||
// Default fallback
|
||||
appName = "reticulum-go.node"
|
||||
}
|
||||
} else {
|
||||
// Default fallback
|
||||
appName = "reticulum-go.node"
|
||||
}
|
||||
|
||||
// Create name hash (10 bytes)
|
||||
nameHash := sha256.Sum256([]byte(appName))
|
||||
nameHash10 := nameHash[:10]
|
||||
log.Printf("[DEBUG-6] Using name hash for '%s': %x", appName, nameHash10)
|
||||
|
||||
// Create random hash (10 bytes) - 5 bytes random + 5 bytes time
|
||||
randomHash := make([]byte, 10)
|
||||
_, err := rand.Read(randomHash[:5]) // #nosec G104
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-6] Failed to read random bytes for hash: %v", err)
|
||||
return nil, err // Or handle the error appropriately
|
||||
}
|
||||
timeBytes := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(timeBytes, uint64(time.Now().Unix())) // #nosec G115
|
||||
copy(randomHash[5:], timeBytes[:5])
|
||||
log.Printf("[DEBUG-6] Generated random hash: %x", randomHash)
|
||||
|
||||
// Prepare ratchet ID if available (not yet implemented)
|
||||
var ratchetID []byte
|
||||
|
||||
// Prepare data for signature
|
||||
// Signature consists of destination hash, public keys, name hash, random hash, and app data
|
||||
signedData := make([]byte, 0, len(destHash)+len(encKey)+len(signKey)+len(nameHash10)+len(randomHash)+len(appData))
|
||||
signedData = append(signedData, destHash...)
|
||||
signedData = append(signedData, encKey...)
|
||||
signedData = append(signedData, signKey...)
|
||||
signedData = append(signedData, nameHash10...)
|
||||
signedData = append(signedData, randomHash...)
|
||||
signedData = append(signedData, appData...)
|
||||
log.Printf("[DEBUG-5] Created signed data (%d bytes)", len(signedData))
|
||||
|
||||
// Sign the data
|
||||
signature := identity.Sign(signedData)
|
||||
log.Printf("[DEBUG-6] Generated signature: %x", signature)
|
||||
|
||||
// Combine all fields according to spec
|
||||
// Data structure: Public Key (32) + Signing Key (32) + Name Hash (10) + Random Hash (10) + Ratchet (optional) + Signature (64) + App Data
|
||||
data := make([]byte, 0, 32+32+10+10+64+len(appData))
|
||||
data = append(data, encKey...) // Encryption key (32 bytes)
|
||||
data = append(data, signKey...) // Signing key (32 bytes)
|
||||
data = append(data, nameHash10...) // Name hash (10 bytes)
|
||||
data = append(data, randomHash...) // Random hash (10 bytes)
|
||||
if ratchetID != nil {
|
||||
data = append(data, ratchetID...) // Ratchet ID (32 bytes if present)
|
||||
}
|
||||
data = append(data, signature...) // Signature (64 bytes)
|
||||
data = append(data, appData...) // Application data (variable)
|
||||
|
||||
log.Printf("[DEBUG-5] Combined packet data (%d bytes)", len(data))
|
||||
|
||||
// Create the packet with header type 2 (two address fields)
|
||||
p := &Packet{
|
||||
Header: [2]byte{data[0], data[1]},
|
||||
HeaderType: HeaderType2,
|
||||
PacketType: PacketTypeAnnounce,
|
||||
TransportID: transportID,
|
||||
DestinationHash: destHash,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
offset := HeaderSize
|
||||
|
||||
// Handle access code if present
|
||||
if p.Header[0]&IFACFlag != 0 {
|
||||
// Access code handling would go here
|
||||
// For now, we'll assume no access code
|
||||
return nil, errors.New("access code handling not implemented")
|
||||
}
|
||||
|
||||
// Determine address size based on header type
|
||||
addrLen := AddressSize
|
||||
if p.Header[0]&HeaderTypeFlag != 0 {
|
||||
addrLen = 2 * AddressSize
|
||||
}
|
||||
|
||||
if len(data[offset:]) < addrLen+ContextSize {
|
||||
return nil, errors.New("packet data too short for addresses and context")
|
||||
}
|
||||
|
||||
// Copy addresses
|
||||
p.Addresses = make([]byte, addrLen)
|
||||
copy(p.Addresses, data[offset:offset+addrLen])
|
||||
offset += addrLen
|
||||
|
||||
// Copy context
|
||||
p.Context = data[offset]
|
||||
offset++
|
||||
|
||||
// Copy remaining data
|
||||
p.Data = make([]byte, len(data)-offset)
|
||||
copy(p.Data, data[offset:])
|
||||
|
||||
log.Printf("[DEBUG-4] Created announce packet: type=%d, header=%d", p.PacketType, p.HeaderType)
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
276
pkg/packet/packet_test.go
Normal file
276
pkg/packet/packet_test.go
Normal file
@@ -0,0 +1,276 @@
|
||||
package packet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func randomBytes(n int) []byte {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
panic("Failed to generate random bytes: " + err.Error())
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func TestPacketPackUnpack(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
headerType byte
|
||||
packetType byte
|
||||
transportType byte
|
||||
destType byte
|
||||
context byte
|
||||
contextFlag byte
|
||||
dataSize int
|
||||
needsTransportID bool
|
||||
}{
|
||||
{
|
||||
name: "HeaderType1_Data_NoContextFlag",
|
||||
headerType: HeaderType1,
|
||||
packetType: PacketTypeData,
|
||||
transportType: 0x01, // Example
|
||||
destType: 0x02, // Example
|
||||
context: ContextNone,
|
||||
contextFlag: FlagUnset,
|
||||
dataSize: 100,
|
||||
needsTransportID: false,
|
||||
},
|
||||
{
|
||||
name: "HeaderType2_Announce_ContextFlagSet",
|
||||
headerType: HeaderType2,
|
||||
packetType: PacketTypeAnnounce,
|
||||
transportType: 0x01, // Changed from 0x0F (15) to 1 (valid 1-bit value)
|
||||
destType: 0x01, // Example
|
||||
context: ContextResourceAdv,
|
||||
contextFlag: FlagSet,
|
||||
dataSize: 50,
|
||||
needsTransportID: true,
|
||||
},
|
||||
{
|
||||
name: "HeaderType1_EmptyData",
|
||||
headerType: HeaderType1,
|
||||
packetType: PacketTypeProof,
|
||||
transportType: 0x00,
|
||||
destType: 0x00,
|
||||
context: ContextLRProof,
|
||||
contextFlag: FlagSet,
|
||||
dataSize: 0,
|
||||
needsTransportID: false,
|
||||
},
|
||||
{
|
||||
name: "HeaderType2_MaxHops", // Hops are set manually before pack
|
||||
headerType: HeaderType2,
|
||||
packetType: PacketTypeLinkReq,
|
||||
transportType: 0x01, // Changed from 0x05 (5) to 1 (valid 1-bit value)
|
||||
destType: 0x03,
|
||||
context: ContextLinkIdentify,
|
||||
contextFlag: FlagUnset,
|
||||
dataSize: 200,
|
||||
needsTransportID: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
originalData := randomBytes(tc.dataSize)
|
||||
originalDestHash := randomBytes(16) // Truncated dest hash
|
||||
var originalTransportID []byte
|
||||
if tc.needsTransportID {
|
||||
originalTransportID = randomBytes(16)
|
||||
}
|
||||
|
||||
p := &Packet{
|
||||
HeaderType: tc.headerType,
|
||||
PacketType: tc.packetType,
|
||||
TransportType: tc.transportType,
|
||||
Context: tc.context,
|
||||
ContextFlag: tc.contextFlag,
|
||||
Hops: 5, // Example hops
|
||||
DestinationType: tc.destType,
|
||||
DestinationHash: originalDestHash,
|
||||
TransportID: originalTransportID,
|
||||
Data: originalData,
|
||||
Packed: false,
|
||||
}
|
||||
|
||||
// Test Pack
|
||||
err := p.Pack()
|
||||
if err != nil {
|
||||
t.Fatalf("Pack() failed: %v", err)
|
||||
}
|
||||
if !p.Packed {
|
||||
t.Error("Pack() did not set Packed flag to true")
|
||||
}
|
||||
if len(p.Raw) == 0 {
|
||||
t.Error("Pack() resulted in empty Raw data")
|
||||
}
|
||||
|
||||
// Create a new packet from the raw data for unpacking
|
||||
unpackTarget := &Packet{Raw: p.Raw}
|
||||
|
||||
// Test Unpack
|
||||
err = unpackTarget.Unpack()
|
||||
if err != nil {
|
||||
t.Fatalf("Unpack() failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify unpacked fields match original
|
||||
if unpackTarget.HeaderType != tc.headerType {
|
||||
t.Errorf("Unpacked HeaderType = %d; want %d", unpackTarget.HeaderType, tc.headerType)
|
||||
}
|
||||
if unpackTarget.PacketType != tc.packetType {
|
||||
t.Errorf("Unpacked PacketType = %d; want %d", unpackTarget.PacketType, tc.packetType)
|
||||
}
|
||||
if unpackTarget.TransportType != tc.transportType {
|
||||
t.Errorf("Unpacked TransportType = %d; want %d", unpackTarget.TransportType, tc.transportType)
|
||||
}
|
||||
if unpackTarget.Context != tc.context {
|
||||
t.Errorf("Unpacked Context = %d; want %d", unpackTarget.Context, tc.context)
|
||||
}
|
||||
if unpackTarget.ContextFlag != tc.contextFlag {
|
||||
t.Errorf("Unpacked ContextFlag = %d; want %d", unpackTarget.ContextFlag, tc.contextFlag)
|
||||
}
|
||||
if unpackTarget.Hops != 5 { // Should match the Hops set before packing
|
||||
t.Errorf("Unpacked Hops = %d; want %d", unpackTarget.Hops, 5)
|
||||
}
|
||||
if unpackTarget.DestinationType != tc.destType {
|
||||
t.Errorf("Unpacked DestinationType = %d; want %d", unpackTarget.DestinationType, tc.destType)
|
||||
}
|
||||
if !bytes.Equal(unpackTarget.DestinationHash, originalDestHash) {
|
||||
t.Errorf("Unpacked DestinationHash = %x; want %x", unpackTarget.DestinationHash, originalDestHash)
|
||||
}
|
||||
if !bytes.Equal(unpackTarget.Data, originalData) {
|
||||
t.Errorf("Unpacked Data = %x; want %x", unpackTarget.Data, originalData)
|
||||
}
|
||||
|
||||
if tc.needsTransportID {
|
||||
if !bytes.Equal(unpackTarget.TransportID, originalTransportID) {
|
||||
t.Errorf("Unpacked TransportID = %x; want %x", unpackTarget.TransportID, originalTransportID)
|
||||
}
|
||||
} else {
|
||||
if unpackTarget.TransportID != nil {
|
||||
t.Errorf("Unpacked TransportID = %x; want nil", unpackTarget.TransportID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackMTUExceeded(t *testing.T) {
|
||||
p := &Packet{
|
||||
HeaderType: HeaderType1,
|
||||
PacketType: PacketTypeData,
|
||||
DestinationHash: randomBytes(16),
|
||||
Context: ContextNone,
|
||||
Data: randomBytes(MTU + 10), // Exceed MTU
|
||||
}
|
||||
err := p.Pack()
|
||||
if err == nil {
|
||||
t.Errorf("Pack() should have failed due to exceeding MTU, but it didn't")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnpackTooShort(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
raw []byte
|
||||
}{
|
||||
{"VeryShort", []byte{0x01}},
|
||||
{"HeaderType1MinShort", []byte{0x00, 0x05, 0x01, 0x02}}, // Missing parts of dest hash
|
||||
{"HeaderType2MinShort", []byte{0x40, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}}, // Missing dest hash
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p := &Packet{Raw: tc.raw}
|
||||
err := p.Unpack()
|
||||
if err == nil {
|
||||
t.Errorf("Unpack() should have failed for short packet, but it didn't")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketHashing(t *testing.T) {
|
||||
// Create two identical packets
|
||||
data := randomBytes(50)
|
||||
destHash := randomBytes(16)
|
||||
p1 := &Packet{
|
||||
HeaderType: HeaderType1,
|
||||
PacketType: PacketTypeData,
|
||||
TransportType: 0x01,
|
||||
Context: ContextNone,
|
||||
ContextFlag: FlagUnset,
|
||||
Hops: 2,
|
||||
DestinationType: 0x02,
|
||||
DestinationHash: destHash,
|
||||
Data: data,
|
||||
}
|
||||
p2 := &Packet{
|
||||
HeaderType: HeaderType1,
|
||||
PacketType: PacketTypeData,
|
||||
TransportType: 0x01,
|
||||
Context: ContextNone,
|
||||
ContextFlag: FlagUnset,
|
||||
Hops: 2,
|
||||
DestinationType: 0x02,
|
||||
DestinationHash: destHash,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
// Pack both
|
||||
if err := p1.Pack(); err != nil {
|
||||
t.Fatalf("p1.Pack() failed: %v", err)
|
||||
}
|
||||
if err := p2.Pack(); err != nil {
|
||||
t.Fatalf("p2.Pack() failed: %v", err)
|
||||
}
|
||||
|
||||
// Hashes should be identical
|
||||
hash1 := p1.GetHash()
|
||||
hash2 := p2.GetHash()
|
||||
if !bytes.Equal(hash1, hash2) {
|
||||
t.Errorf("Hashes of identical packets differ:\nHash1: %x\nHash2: %x", hash1, hash2)
|
||||
}
|
||||
if !bytes.Equal(p1.PacketHash, hash1) {
|
||||
t.Errorf("p1.PacketHash (%x) does not match GetHash() (%x)", p1.PacketHash, hash1)
|
||||
}
|
||||
|
||||
// Change a non-hashable field (hops) in p2
|
||||
p2.Hops = 3
|
||||
p2.Raw[1] = 3 // Need to modify Raw as Pack isn't called again
|
||||
hash3 := p2.GetHash()
|
||||
if !bytes.Equal(hash1, hash3) {
|
||||
t.Errorf("Hash changed after modifying non-hashable Hops field:\nHash1: %x\nHash3: %x", hash1, hash3)
|
||||
}
|
||||
|
||||
// Change a hashable field (data) in p2
|
||||
p2.Data = append(p2.Data, 0x99)
|
||||
p2.Raw = append(p2.Raw, 0x99) // Modify Raw to reflect data change
|
||||
hash4 := p2.GetHash()
|
||||
if bytes.Equal(hash1, hash4) {
|
||||
t.Errorf("Hash did not change after modifying hashable Data field")
|
||||
}
|
||||
|
||||
// Test HeaderType2 hashing difference
|
||||
p3 := &Packet{
|
||||
HeaderType: HeaderType2,
|
||||
PacketType: PacketTypeData,
|
||||
TransportType: 0x01,
|
||||
Context: ContextNone,
|
||||
ContextFlag: FlagUnset,
|
||||
Hops: 2,
|
||||
DestinationType: 0x02,
|
||||
DestinationHash: destHash,
|
||||
TransportID: randomBytes(16),
|
||||
Data: data,
|
||||
}
|
||||
if err := p3.Pack(); err != nil {
|
||||
t.Fatalf("p3.Pack() failed: %v", err)
|
||||
}
|
||||
hash5 := p3.GetHash()
|
||||
_ = hash5 // Use hash5 to avoid unused variable error
|
||||
}
|
||||
34
pkg/pathfinder/pathfinder.go
Normal file
34
pkg/pathfinder/pathfinder.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package pathfinder
|
||||
|
||||
import "time"
|
||||
|
||||
type PathFinder struct {
|
||||
paths map[string]Path
|
||||
}
|
||||
|
||||
type Path struct {
|
||||
NextHop []byte
|
||||
Interface string
|
||||
HopCount byte
|
||||
LastUpdated int64
|
||||
}
|
||||
|
||||
func NewPathFinder() *PathFinder {
|
||||
return &PathFinder{
|
||||
paths: make(map[string]Path),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PathFinder) AddPath(destHash string, nextHop []byte, iface string, hops byte) {
|
||||
p.paths[destHash] = Path{
|
||||
NextHop: nextHop,
|
||||
Interface: iface,
|
||||
HopCount: hops,
|
||||
LastUpdated: time.Now().Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PathFinder) GetPath(destHash string) (Path, bool) {
|
||||
path, exists := p.paths[destHash]
|
||||
return path, exists
|
||||
}
|
||||
193
pkg/rate/rate.go
Normal file
193
pkg/rate/rate.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package rate
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultAnnounceRateTarget = 3600.0 // Default 1 hour between announces
|
||||
DefaultAnnounceRateGrace = 3 // Default number of grace announces
|
||||
DefaultAnnounceRatePenalty = 7200.0 // Default 2 hour penalty
|
||||
DefaultBurstFreqNew = 3.5 // Default announces/sec for new interfaces
|
||||
DefaultBurstFreq = 12.0 // Default announces/sec for established interfaces
|
||||
DefaultBurstHold = 60 // Default seconds to hold after burst
|
||||
DefaultBurstPenalty = 300 // Default seconds penalty after burst
|
||||
DefaultMaxHeldAnnounces = 256 // Default max announces in hold queue
|
||||
DefaultHeldReleaseInterval = 30 // Default seconds between releasing held announces
|
||||
)
|
||||
|
||||
type Limiter struct {
|
||||
rate float64
|
||||
interval time.Duration
|
||||
lastUpdate time.Time
|
||||
allowance float64
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func NewLimiter(rate float64, interval time.Duration) *Limiter {
|
||||
return &Limiter{
|
||||
rate: rate,
|
||||
interval: interval,
|
||||
lastUpdate: time.Now(),
|
||||
allowance: rate,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Limiter) Allow() bool {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
elapsed := now.Sub(l.lastUpdate)
|
||||
l.lastUpdate = now
|
||||
|
||||
l.allowance += elapsed.Seconds() * l.rate
|
||||
if l.allowance > l.rate {
|
||||
l.allowance = l.rate
|
||||
}
|
||||
|
||||
if l.allowance < 1.0 {
|
||||
return false
|
||||
}
|
||||
|
||||
l.allowance -= 1.0
|
||||
return true
|
||||
}
|
||||
|
||||
// AnnounceRateControl handles per-destination announce rate limiting
|
||||
type AnnounceRateControl struct {
|
||||
rateTarget float64
|
||||
rateGrace int
|
||||
ratePenalty float64
|
||||
|
||||
announceHistory map[string][]time.Time // Maps dest hash to announce times
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewAnnounceRateControl(target float64, grace int, penalty float64) *AnnounceRateControl {
|
||||
return &AnnounceRateControl{
|
||||
rateTarget: target,
|
||||
rateGrace: grace,
|
||||
ratePenalty: penalty,
|
||||
announceHistory: make(map[string][]time.Time),
|
||||
}
|
||||
}
|
||||
|
||||
func (arc *AnnounceRateControl) AllowAnnounce(destHash string) bool {
|
||||
arc.mutex.Lock()
|
||||
defer arc.mutex.Unlock()
|
||||
|
||||
history := arc.announceHistory[destHash]
|
||||
now := time.Now()
|
||||
|
||||
// Cleanup old history entries
|
||||
cutoff := now.Add(-24 * time.Hour)
|
||||
newHistory := []time.Time{}
|
||||
for _, t := range history {
|
||||
if t.After(cutoff) {
|
||||
newHistory = append(newHistory, t)
|
||||
}
|
||||
}
|
||||
history = newHistory
|
||||
|
||||
// Allow if within grace period
|
||||
if len(history) < arc.rateGrace {
|
||||
arc.announceHistory[destHash] = append(history, now)
|
||||
return true
|
||||
}
|
||||
|
||||
// Check rate
|
||||
lastAnnounce := history[len(history)-1]
|
||||
waitTime := arc.rateTarget
|
||||
if len(history) > arc.rateGrace {
|
||||
waitTime += arc.ratePenalty
|
||||
}
|
||||
|
||||
if now.Sub(lastAnnounce).Seconds() < waitTime {
|
||||
return false
|
||||
}
|
||||
|
||||
arc.announceHistory[destHash] = append(history, now)
|
||||
return true
|
||||
}
|
||||
|
||||
// IngressControl handles new destination announce rate limiting
|
||||
type IngressControl struct {
|
||||
enabled bool
|
||||
burstFreqNew float64
|
||||
burstFreq float64
|
||||
burstHold time.Duration
|
||||
burstPenalty time.Duration
|
||||
maxHeldAnnounces int
|
||||
heldReleaseInterval time.Duration
|
||||
|
||||
heldAnnounces map[string][]byte // Maps announce hash to announce data
|
||||
lastBurst time.Time
|
||||
announceCount int
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewIngressControl(enabled bool) *IngressControl {
|
||||
return &IngressControl{
|
||||
enabled: enabled,
|
||||
burstFreqNew: DefaultBurstFreqNew,
|
||||
burstFreq: DefaultBurstFreq,
|
||||
burstHold: time.Duration(DefaultBurstHold) * time.Second,
|
||||
burstPenalty: time.Duration(DefaultBurstPenalty) * time.Second,
|
||||
maxHeldAnnounces: DefaultMaxHeldAnnounces,
|
||||
heldReleaseInterval: time.Duration(DefaultHeldReleaseInterval) * time.Second,
|
||||
heldAnnounces: make(map[string][]byte),
|
||||
lastBurst: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (ic *IngressControl) ProcessAnnounce(announceHash string, announceData []byte, isNewDest bool) bool {
|
||||
if !ic.enabled {
|
||||
return true
|
||||
}
|
||||
|
||||
ic.mutex.Lock()
|
||||
defer ic.mutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
elapsed := now.Sub(ic.lastBurst)
|
||||
|
||||
// Reset counter if enough time has passed
|
||||
if elapsed > ic.burstHold+ic.burstPenalty {
|
||||
ic.announceCount = 0
|
||||
ic.lastBurst = now
|
||||
}
|
||||
|
||||
// Check burst frequency
|
||||
maxFreq := ic.burstFreq
|
||||
if isNewDest {
|
||||
maxFreq = ic.burstFreqNew
|
||||
}
|
||||
|
||||
ic.announceCount++
|
||||
burstFreq := float64(ic.announceCount) / elapsed.Seconds()
|
||||
|
||||
// Hold announce if burst frequency exceeded
|
||||
if burstFreq > maxFreq {
|
||||
if len(ic.heldAnnounces) < ic.maxHeldAnnounces {
|
||||
ic.heldAnnounces[announceHash] = announceData
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (ic *IngressControl) ReleaseHeldAnnounce() (string, []byte, bool) {
|
||||
ic.mutex.Lock()
|
||||
defer ic.mutex.Unlock()
|
||||
|
||||
// Return first held announce if any exist
|
||||
for hash, data := range ic.heldAnnounces {
|
||||
delete(ic.heldAnnounces, hash)
|
||||
return hash, data, true
|
||||
}
|
||||
|
||||
return "", nil, false
|
||||
}
|
||||
74
pkg/resolver/resolver.go
Normal file
74
pkg/resolver/resolver.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||
)
|
||||
|
||||
type Resolver struct {
|
||||
cache map[string]*identity.Identity
|
||||
cacheLock sync.RWMutex
|
||||
}
|
||||
|
||||
func New() *Resolver {
|
||||
return &Resolver{
|
||||
cache: make(map[string]*identity.Identity),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resolver) ResolveIdentity(fullName string) (*identity.Identity, error) {
|
||||
if fullName == "" {
|
||||
return nil, errors.New("empty identity name")
|
||||
}
|
||||
|
||||
r.cacheLock.RLock()
|
||||
if cachedIdentity, exists := r.cache[fullName]; exists {
|
||||
r.cacheLock.RUnlock()
|
||||
return cachedIdentity, nil
|
||||
}
|
||||
r.cacheLock.RUnlock()
|
||||
|
||||
// Hash the full name to create a deterministic identity
|
||||
h := sha256.New()
|
||||
h.Write([]byte(fullName))
|
||||
nameHash := h.Sum(nil)[:identity.NAME_HASH_LENGTH/8]
|
||||
hashStr := hex.EncodeToString(nameHash)
|
||||
|
||||
// Check if this identity is known
|
||||
if knownData, exists := identity.GetKnownDestination(hashStr); exists {
|
||||
if id, ok := knownData[2].(*identity.Identity); ok {
|
||||
r.cacheLock.Lock()
|
||||
r.cache[fullName] = id
|
||||
r.cacheLock.Unlock()
|
||||
return id, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Split name into parts for hierarchical resolution
|
||||
parts := strings.Split(fullName, ".")
|
||||
if len(parts) < 2 {
|
||||
return nil, errors.New("invalid identity name format")
|
||||
}
|
||||
|
||||
// Create new identity if not found
|
||||
id, err := identity.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.cacheLock.Lock()
|
||||
r.cache[fullName] = id
|
||||
r.cacheLock.Unlock()
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func ResolveIdentity(fullName string) (*identity.Identity, error) {
|
||||
r := New()
|
||||
return r.ResolveIdentity(fullName)
|
||||
}
|
||||
@@ -2,13 +2,12 @@ package resource
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -19,88 +18,90 @@ const (
|
||||
STATUS_CANCELLED = 0x04
|
||||
|
||||
DEFAULT_SEGMENT_SIZE = 384 // Based on ENCRYPTED_MDU
|
||||
MAX_SEGMENTS = 65535
|
||||
CLEANUP_INTERVAL = 300 // 5 minutes
|
||||
MAX_SEGMENTS = 65535
|
||||
CLEANUP_INTERVAL = 300 // 5 minutes
|
||||
|
||||
// Window size constants
|
||||
WINDOW = 4
|
||||
WINDOW_MIN = 2
|
||||
WINDOW_MAX_SLOW = 10
|
||||
WINDOW_MIN = 2
|
||||
WINDOW_MAX_SLOW = 10
|
||||
WINDOW_MAX_VERY_SLOW = 4
|
||||
WINDOW_MAX_FAST = 75
|
||||
WINDOW_MAX = WINDOW_MAX_FAST
|
||||
|
||||
WINDOW_MAX_FAST = 75
|
||||
WINDOW_MAX = WINDOW_MAX_FAST
|
||||
|
||||
// Rate thresholds
|
||||
FAST_RATE_THRESHOLD = WINDOW_MAX_SLOW - WINDOW - 2
|
||||
FAST_RATE_THRESHOLD = WINDOW_MAX_SLOW - WINDOW - 2
|
||||
VERY_SLOW_RATE_THRESHOLD = 2
|
||||
|
||||
|
||||
// Transfer rates (bytes per second)
|
||||
RATE_FAST = (50 * 1000) / 8 // 50 Kbps
|
||||
RATE_VERY_SLOW = (2 * 1000) / 8 // 2 Kbps
|
||||
|
||||
RATE_FAST = (50 * 1000) / 8 // 50 Kbps
|
||||
RATE_VERY_SLOW = (2 * 1000) / 8 // 2 Kbps
|
||||
|
||||
// Window flexibility
|
||||
WINDOW_FLEXIBILITY = 4
|
||||
|
||||
|
||||
// Hash and segment constants
|
||||
MAPHASH_LEN = 4
|
||||
MAPHASH_LEN = 4
|
||||
RANDOM_HASH_SIZE = 4
|
||||
|
||||
|
||||
// Size limits
|
||||
MAX_EFFICIENT_SIZE = 16*1024*1024 - 1 // ~16MB
|
||||
MAX_EFFICIENT_SIZE = 16*1024*1024 - 1 // ~16MB
|
||||
AUTO_COMPRESS_MAX_SIZE = MAX_EFFICIENT_SIZE
|
||||
|
||||
|
||||
// Timeouts and retries
|
||||
PART_TIMEOUT_FACTOR = 4
|
||||
PART_TIMEOUT_FACTOR = 4
|
||||
PART_TIMEOUT_FACTOR_AFTER_RTT = 2
|
||||
PROOF_TIMEOUT_FACTOR = 3
|
||||
MAX_RETRIES = 16
|
||||
MAX_ADV_RETRIES = 4
|
||||
SENDER_GRACE_TIME = 10.0
|
||||
PROCESSING_GRACE = 1.0
|
||||
RETRY_GRACE_TIME = 0.25
|
||||
PER_RETRY_DELAY = 0.5
|
||||
PROOF_TIMEOUT_FACTOR = 3
|
||||
MAX_RETRIES = 16
|
||||
MAX_ADV_RETRIES = 4
|
||||
SENDER_GRACE_TIME = 10.0
|
||||
PROCESSING_GRACE = 1.0
|
||||
RETRY_GRACE_TIME = 0.25
|
||||
PER_RETRY_DELAY = 0.5
|
||||
)
|
||||
|
||||
type Resource struct {
|
||||
mutex sync.RWMutex
|
||||
mutex sync.RWMutex
|
||||
data []byte
|
||||
fileHandle io.ReadWriteSeeker
|
||||
fileName string
|
||||
hash []byte
|
||||
randomHash []byte
|
||||
originalHash []byte
|
||||
status byte
|
||||
compressed bool
|
||||
autoCompress bool
|
||||
encrypted bool
|
||||
split bool
|
||||
segments uint16
|
||||
segmentIndex uint16
|
||||
totalSegments uint16
|
||||
completedParts map[uint16]bool
|
||||
transferSize int64
|
||||
dataSize int64
|
||||
progress float64
|
||||
window int
|
||||
windowMax int
|
||||
windowMin int
|
||||
windowFlexibility int
|
||||
rtt float64
|
||||
fastRateRounds int
|
||||
status byte
|
||||
compressed bool
|
||||
autoCompress bool
|
||||
encrypted bool
|
||||
split bool
|
||||
segments uint16
|
||||
segmentIndex uint16
|
||||
totalSegments uint16
|
||||
completedParts map[uint16]bool
|
||||
transferSize int64
|
||||
dataSize int64
|
||||
progress float64
|
||||
window int
|
||||
windowMax int
|
||||
windowMin int
|
||||
windowFlexibility int
|
||||
rtt float64
|
||||
fastRateRounds int
|
||||
verySlowRateRounds int
|
||||
createdAt time.Time
|
||||
completedAt time.Time
|
||||
callback func(*Resource)
|
||||
progressCallback func(*Resource)
|
||||
createdAt time.Time
|
||||
completedAt time.Time
|
||||
callback func(*Resource)
|
||||
progressCallback func(*Resource)
|
||||
readOffset int64
|
||||
}
|
||||
|
||||
func New(data interface{}, autoCompress bool) (*Resource, error) {
|
||||
r := &Resource{
|
||||
status: STATUS_PENDING,
|
||||
compressed: false,
|
||||
autoCompress: autoCompress,
|
||||
autoCompress: autoCompress,
|
||||
completedParts: make(map[uint16]bool),
|
||||
createdAt: time.Now(),
|
||||
progress: 0.0,
|
||||
createdAt: time.Now(),
|
||||
progress: 0.0,
|
||||
}
|
||||
|
||||
switch v := data.(type) {
|
||||
@@ -118,12 +119,16 @@ func New(data interface{}, autoCompress bool) (*Resource, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if namer, ok := v.(interface{ Name() string }); ok {
|
||||
r.fileName = namer.Name()
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unsupported data type")
|
||||
}
|
||||
|
||||
// Calculate segments needed
|
||||
r.segments = uint16((r.dataSize + DEFAULT_SEGMENT_SIZE - 1) / DEFAULT_SEGMENT_SIZE)
|
||||
r.segments = uint16((r.dataSize + DEFAULT_SEGMENT_SIZE - 1) / DEFAULT_SEGMENT_SIZE) // #nosec G115
|
||||
if r.segments > MAX_SEGMENTS {
|
||||
return nil, errors.New("resource too large")
|
||||
}
|
||||
@@ -138,10 +143,10 @@ func New(data interface{}, autoCompress bool) (*Resource, error) {
|
||||
r.transferSize = int64(float64(r.dataSize) * compressibility)
|
||||
} else if r.fileHandle != nil {
|
||||
// For file handles, use extension-based estimation
|
||||
ext := strings.ToLower(filepath.Ext(r.fileHandle.Name()))
|
||||
ext := strings.ToLower(filepath.Ext(r.fileName))
|
||||
r.transferSize = estimateFileCompression(r.dataSize, ext)
|
||||
}
|
||||
|
||||
|
||||
// Ensure minimum size and add compression overhead
|
||||
if r.transferSize < r.dataSize/10 {
|
||||
r.transferSize = r.dataSize / 10
|
||||
@@ -223,7 +228,7 @@ func (r *Resource) IsCompressed() bool {
|
||||
func (r *Resource) Cancel() {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
|
||||
if r.status == STATUS_PENDING || r.status == STATUS_ACTIVE {
|
||||
r.status = STATUS_CANCELLED
|
||||
r.completedAt = time.Now()
|
||||
@@ -342,13 +347,13 @@ func estimateCompressibility(data []byte) float64 {
|
||||
if len(data) < sampleSize {
|
||||
sampleSize = len(data)
|
||||
}
|
||||
|
||||
|
||||
// Count unique bytes in sample
|
||||
uniqueBytes := make(map[byte]struct{})
|
||||
for i := 0; i < sampleSize; i++ {
|
||||
uniqueBytes[data[i]] = struct{}{}
|
||||
}
|
||||
|
||||
|
||||
// Calculate entropy-based compression estimate
|
||||
uniqueRatio := float64(len(uniqueBytes)) / float64(sampleSize)
|
||||
return 0.3 + (0.7 * uniqueRatio) // Base compression ratio between 0.3 and 1.0
|
||||
@@ -357,13 +362,13 @@ func estimateCompressibility(data []byte) float64 {
|
||||
func estimateFileCompression(size int64, extension string) int64 {
|
||||
// Compression ratio estimates based on common file types
|
||||
compressionRatios := map[string]float64{
|
||||
".txt": 0.4, // Text compresses well
|
||||
".txt": 0.4, // Text compresses well
|
||||
".log": 0.4,
|
||||
".json": 0.4,
|
||||
".xml": 0.4,
|
||||
".html": 0.4,
|
||||
".csv": 0.5,
|
||||
".doc": 0.8, // Already compressed
|
||||
".doc": 0.8, // Already compressed
|
||||
".docx": 0.95,
|
||||
".pdf": 0.95,
|
||||
".jpg": 0.99, // Already compressed
|
||||
@@ -376,11 +381,43 @@ func estimateFileCompression(size int64, extension string) int64 {
|
||||
".gz": 0.99,
|
||||
".rar": 0.99,
|
||||
}
|
||||
|
||||
|
||||
ratio, exists := compressionRatios[extension]
|
||||
if !exists {
|
||||
ratio = 0.7 // Default compression ratio for unknown types
|
||||
}
|
||||
|
||||
|
||||
return int64(float64(size) * ratio)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resource) Read(p []byte) (n int, err error) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
if r.data != nil {
|
||||
if r.readOffset >= int64(len(r.data)) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n = copy(p, r.data[r.readOffset:])
|
||||
r.readOffset += int64(n)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
if r.fileHandle != nil {
|
||||
return r.fileHandle.Read(p)
|
||||
}
|
||||
|
||||
return 0, errors.New("no data source available")
|
||||
}
|
||||
|
||||
func (r *Resource) GetName() string {
|
||||
r.mutex.RLock()
|
||||
defer r.mutex.RUnlock()
|
||||
return r.fileName
|
||||
}
|
||||
|
||||
func (r *Resource) GetSize() int64 {
|
||||
r.mutex.RLock()
|
||||
defer r.mutex.RUnlock()
|
||||
return r.dataSize
|
||||
}
|
||||
|
||||
170
pkg/transport/announce.go
Normal file
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