119 Commits

Author SHA1 Message Date
fed33aadff add badge 2025-07-15 14:08:22 -05:00
d0c83ec1a2 update revive workflow 2025-07-15 14:06:18 -05:00
aa94bee606 fix workflow permissions 2025-07-15 14:02:22 -05:00
745609423f update 2025-07-15 14:01:10 -05:00
16e1c7e4eb add revive workflow 2025-07-15 13:59:27 -05:00
aec3672228 update 2025-07-15 13:55:04 -05:00
aace3abd6d update build workflow 2025-07-15 13:53:26 -05:00
ca3fefaae8 Add workflow permissions 2025-07-15 13:51:32 -05:00
d4f89735f6 add bearer 2025-07-15 13:51:13 -05:00
b37d393286 Update SECURITY.md to simplify vulnerability reporting instructions. 2025-07-15 13:51:07 -05:00
5e0c829cf6 Fix: Address various static analysis warnings
- **pkg/announce/announce.go**: Added error handling for `rand.Read` to log potential issues when generating random hashes.
- **pkg/buffer/buffer.go**: Removed a redundant `#nosec G115` comment as the line no longer triggers the warning.
- **pkg/cryptography/aes.go**: Added `#nosec G407` to explicitly acknowledge the use of `cipher.NewCBCEncrypter` which is acceptable in this context.
- **pkg/transport/transport.go**: Removed redundant `#nosec G115` comments as the lines no longer trigger the warning.
2025-07-15 13:45:48 -05:00
a80f2bb2ac Add a GetConfig method to the Transport struct. 2025-07-15 13:40:28 -05:00
7de206447a Migrate all AES encryption to AES-256-CBC and implement persistent ratchet storage. 2025-07-15 13:40:20 -05:00
f740514e2b Fix Destination announcing to use a dedicated announce package and improve transport integration. 2025-07-15 13:40:11 -05:00
b907dd93f1 Announce packet creation to strictly follow Reticulum specification. 2025-07-15 13:39:49 -05:00
011a6303eb Use destination-based announcing and consolidate ratchet path handling. 2025-07-15 13:39:39 -05:00
12f487d937 use AES-256-CBC only 2025-07-15 13:31:19 -05:00
b9aebc8406 gosec fixes and added #nosec where necassary 2025-07-06 00:33:50 -05:00
ffb3c3d4f4 Update Go version and x/crypto dependency to latest stable versions. 2025-07-06 00:09:53 -05:00
f291ba74e9 update 2025-07-06 00:09:41 -05:00
6e87fc9bcd go fmt 2025-07-06 00:09:14 -05:00
cb402e2bb6 add badges 2025-07-06 00:07:19 -05:00
fe5101340a Update TODO with AES 256 completion 2025-07-06 00:05:34 -05:00
dfac66e8bc add workflows 2025-07-06 00:05:11 -05:00
bc05835dae Add AES 256 and update AES test 2025-07-05 23:59:59 -05:00
Ivan
26371cdb6a Code cleanup of unused functions/variables 2025-05-07 18:35:45 -05:00
Ivan
41db0500af update x/crypto v0.37.0 > v0.38.0 2025-05-07 18:28:29 -05:00
Ivan
8114c3bda4 Add unit tests for configuration, cryptography, interfaces, and packet handling. 2025-05-07 18:24:52 -05:00
Ivan
3f141bf93b update 2025-05-07 18:24:07 -05:00
Ivan
a9bf658b03 update with badge 2025-05-07 18:23:50 -05:00
Ivan
ae9a35e3bb update 2025-05-04 03:47:13 -05:00
Ivan
32d32380d8 update license 2025-05-04 03:47:09 -05:00
Ivan
5e40f0bfe8 fix 2025-04-18 22:54:43 -05:00
315b35fc81 update 2025-04-18 22:53:45 -05:00
54dec6aa89 add 2025-04-18 22:53:42 -05:00
92c8faec11 update 2025-04-18 22:53:38 -05:00
2aff4989e5 Updated 2025-04-18 22:42:21 -05:00
f1d2a31be6 Updated 2025-04-18 22:42:12 -05:00
f604d1a3c8 create TODO 2025-04-18 22:41:54 -05:00
26a54436f7 remove 2025-04-18 22:41:48 -05:00
2fd85a1034 update x/crypto 2025-04-15 12:48:44 -05:00
c8e81cd9f0 Enhance node announcement handling and packet structure. Introduce node-specific metadata in the Reticulum struct, update announce packet creation to support new formats, and improve validation checks for announce data. Adjust minimum packet size requirements and refactor related functions for clarity and consistency. 2025-03-29 18:12:47 -05:00
2f61ce9bf3 remove github actions for now 2025-03-16 21:20:58 -05:00
b647e7c6c2 update 2025-03-12 00:25:39 -05:00
6b3990d399 more platforms 2025-03-11 23:06:56 -05:00
041b439a66 fix RTT for specific platforms 2025-03-11 23:06:26 -05:00
534982b99d remove old code 2025-03-11 22:51:38 -05:00
7379d07aba add revive config 2025-03-11 22:51:31 -05:00
03345bc256 update 2025-03-11 22:51:25 -05:00
e486923e8f update 2025-03-11 22:32:36 -05:00
d7f41b785f update 2025-03-11 22:22:55 -05:00
15303a21dc update 2025-03-11 22:22:51 -05:00
4d4863aeeb format 2025-03-11 22:18:40 -05:00
76a4103a56 cleanup 2025-03-11 22:18:09 -05:00
96348ce349 add: gosec 2025-03-11 22:18:02 -05:00
Sudo-Ivan
322711ba20 update 2025-02-14 13:06:11 -06:00
Sudo-Ivan
772248b31f update Go to 1.24 2025-02-14 13:05:47 -06:00
Ivan
fa1c80169e Update README.md 2025-02-14 16:04:46 +00:00
Sudo-Ivan
cb1e4a1115 update 2025-01-25 13:55:19 -06:00
Sudo-Ivan
836e97b17d add 2025-01-25 13:47:32 -06:00
Sudo-Ivan
87d3b4a58b update interfaces 2025-01-05 16:24:58 -06:00
Sudo-Ivan
77729e07e1 packet interceptor 2025-01-04 18:37:53 -06:00
Sudo-Ivan
79e1caa815 0.3.7 - announce packet improvements, cryptgraphy pkg, cleanup 2025-01-04 18:20:36 -06:00
Sudo-Ivan
a5b905bbaf cleanup/sort 2025-01-04 18:17:33 -06:00
Sudo-Ivan
c870406244 move cryptography to its own folder/files 2025-01-04 18:17:13 -06:00
Sudo-Ivan
ea8daf6bb2 0.3.6 - announce packet creation 2025-01-02 14:10:31 -06:00
Sudo-Ivan
d79406e354 update 2025-01-02 13:51:21 -06:00
Sudo-Ivan
f9b8d29780 update 2025-01-02 13:45:57 -06:00
Sudo-Ivan
0cebfb2193 update 2025-01-01 19:14:36 -06:00
Sudo-Ivan
9e229287e8 update 2025-01-01 19:12:40 -06:00
Sudo-Ivan
9508e6e195 update packet creation 2025-01-01 19:12:32 -06:00
Sudo-Ivan
5acbef454f 0.3.5 2025-01-01 18:31:58 -06:00
Sudo-Ivan
0862830431 0.3.4 2025-01-01 17:00:11 -06:00
Sudo-Ivan
6cdc02346f update 2025-01-01 03:12:26 -06:00
Sudo-Ivan
3ffd5b72a1 update 2025-01-01 02:03:12 -06:00
Sudo-Ivan
73af84e24f 0.3.3 2025-01-01 01:41:16 -06:00
Sudo-Ivan
ae40d2879c update and format 2025-01-01 00:58:37 -06:00
Sudo-Ivan
a2499e4a15 update 2025-01-01 00:46:47 -06:00
Sudo-Ivan
30ea1dd0c7 0.3.2 2025-01-01 00:40:25 -06:00
Sudo-Ivan
785bc7d782 update clients 2024-12-31 19:26:30 -06:00
Sudo-Ivan
144f5bea6a update 2024-12-31 19:26:21 -06:00
Sudo-Ivan
a3c701e205 add to-do item 2024-12-31 19:25:31 -06:00
Sudo-Ivan
a8a7607eb6 update binary name 2024-12-31 19:25:23 -06:00
Sudo-Ivan
a2947a3adb update announce.go 2024-12-31 17:07:23 -06:00
Sudo-Ivan
2cb37102fb update 2024-12-31 17:02:57 -06:00
Sudo-Ivan
54c401e2a5 buffer, channel, more transport constants 2024-12-31 17:02:51 -06:00
Sudo-Ivan
8df4039b18 update 2024-12-31 15:30:39 -06:00
Sudo-Ivan
12156adae9 update 2024-12-31 15:22:31 -06:00
Sudo-Ivan
a34e3d274e fix PGP key 2024-12-31 15:20:14 -06:00
Sudo-Ivan
f3d22dfcd4 0.3.1 2024-12-31 15:15:06 -06:00
Sudo-Ivan
99d8e44182 update 2024-12-31 14:41:05 -06:00
Sudo-Ivan
083991c997 add Makefile 2024-12-31 14:34:04 -06:00
Sudo-Ivan
9ca24d96ab move 2024-12-31 14:33:58 -06:00
Sudo-Ivan
b478ca346e update 2024-12-31 14:30:38 -06:00
Sudo-Ivan
20b532e005 update 2024-12-31 14:19:49 -06:00
Sudo-Ivan
80eac50632 update 2024-12-31 14:12:55 -06:00
Sudo-Ivan
f15d8f6a84 move To-Do to README 2024-12-31 14:12:17 -06:00
Sudo-Ivan
c523d6f542 update 2024-12-31 14:11:44 -06:00
Sudo-Ivan
8a175e3051 update 2024-12-31 14:00:10 -06:00
Sudo-Ivan
28d46921d3 0.3.0 2024-12-31 13:49:05 -06:00
Sudo-Ivan
613ceddb0b update common packages 2024-12-31 13:48:22 -06:00
Sudo-Ivan
599dd91979 announce: ratchet support, trunacted hash fix 2024-12-31 13:48:22 -06:00
deepsource-io[bot]
e724886578 ci: add .deepsource.toml 2024-12-31 17:51:22 +00:00
Sudo-Ivan
3034c0b0b4 link physical stats 2024-12-31 11:30:30 -06:00
Sudo-Ivan
3ed2c67742 interface: add interface mods, types and more 2024-12-31 11:22:37 -06:00
Sudo-Ivan
f2c146b7c5 transport: update constants, functions 2024-12-31 11:21:55 -06:00
Sudo-Ivan
59cef5e56a ephermeral keypair, ratchets, shared secret and more 2024-12-31 10:39:31 -06:00
Sudo-Ivan
ef613cc873 0.2.9 - rachets and cryptography 2024-12-30 23:41:18 -06:00
Sudo-Ivan
7a7ce84778 0.2.8 2024-12-30 12:58:43 -06:00
Sudo-Ivan
7ef7e60a87 0.2.7 2024-12-30 11:35:11 -06:00
Sudo-Ivan
73349d4a28 add: AES-CBC 2024-12-30 11:25:45 -06:00
Sudo-Ivan
31128a6758 0.2.6 2024-12-30 11:09:25 -06:00
Sudo-Ivan
566ce5da96 update 2024-12-30 04:22:01 -06:00
Sudo-Ivan
139926be05 0.2.5 2024-12-30 04:00:52 -06:00
Sudo-Ivan
decbd8f29a 0.2.4 2024-12-30 03:50:52 -06:00
Sudo-Ivan
0f5f5cbb13 0.2.3 2024-12-30 02:54:49 -06:00
Sudo-Ivan
a2476c9551 0.2.2 2024-12-30 02:43:35 -06:00
Sudo-Ivan
bfc75a2290 0.2.1 2024-12-30 02:34:38 -06:00
Sudo-Ivan
2e01fa565d update 0.2.0 2024-12-30 02:26:51 -06:00
67 changed files with 8995 additions and 1895 deletions

7
.deepsource.toml Normal file
View File

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

17
.github/workflows/bearer.yml vendored Normal file
View 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
View 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
View 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
View 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
View 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
View File

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

21
CONTRIBUTING.md Normal file
View 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
View File

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

106
Makefile Normal file
View 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"

View File

@@ -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.
[![Socket Badge](https://socket.dev/api/badge/go/package/github.com/sudo-ivan/reticulum-go?version=v0.3.9)](https://socket.dev/go/package/github.com/sudo-ivan/reticulum-go)
![Go Test](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/go-test.yml/badge.svg)
![Run Gosec](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/gosec.yml/badge.svg)
[![Bearer](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/bearer.yml/badge.svg)](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/bearer.yml)
[![Go Build Multi-Platform](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/build.yml/badge.svg)](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/build.yml)
[![Go Revive Lint](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/revive.yml/badge.svg)](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/revive.yml)
[Reticulum 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
View File

@@ -0,0 +1,25 @@
# Security Policy
We use [Socket](https://socket.dev/), [Deepsource](https://deepsource.com/) and [gosec](https://github.com/securego/gosec) for this project.
## 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
View 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
View File

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

View File

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

642
cmd/reticulum-go/main.go Normal file
View 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
}

View File

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

View File

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

View File

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

8
go.mod
View File

@@ -1,9 +1,5 @@
module github.com/Sudo-Ivan/reticulum-go
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
View File

@@ -1,8 +1,2 @@
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/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=

View File

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

View File

@@ -1,21 +1,48 @@
package announce
import (
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"log"
"sync"
"time"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
"golang.org/x/crypto/curve25519"
)
const (
PACKET_TYPE_DATA = 0x00
PACKET_TYPE_ANNOUNCE = 0x01
PACKET_TYPE_LINK = 0x02
PACKET_TYPE_PROOF = 0x03
// Announce Types
ANNOUNCE_NONE = 0x00
ANNOUNCE_PATH = 0x01
ANNOUNCE_IDENTITY = 0x02
// Header Types
HEADER_TYPE_1 = 0x00 // One address field
HEADER_TYPE_2 = 0x01 // Two address fields
// Propagation Types
PROP_TYPE_BROADCAST = 0x00
PROP_TYPE_TRANSPORT = 0x01
DEST_TYPE_SINGLE = 0x00
DEST_TYPE_GROUP = 0x01
DEST_TYPE_PLAIN = 0x02
DEST_TYPE_LINK = 0x03
// IFAC Flag
IFAC_NONE = 0x00
IFAC_AUTH = 0x80
MAX_HOPS = 128
PROPAGATION_RATE = 0.02 // 2% of interface bandwidth
RETRY_INTERVAL = 300 // 5 minutes
@@ -24,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
View File

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

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

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

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

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

View File

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

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

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

View File

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

View File

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

View File

@@ -4,33 +4,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
}

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

View File

@@ -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
}

View File

File diff suppressed because it is too large Load Diff

275
pkg/interfaces/auto.go Normal file
View 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
View File

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

View File

@@ -1,21 +1,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)
}

View File

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

View File

@@ -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
}

View File

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

View File

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

View File

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

View File

@@ -4,85 +4,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
}

View File

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

View File

@@ -5,104 +5,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
}

View File

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

View File

@@ -1,171 +1,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
View 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
}

View File

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

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

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

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

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

View File

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

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

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

View File

File diff suppressed because it is too large Load Diff

52
revive.toml Normal file
View File

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

View File

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