Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fed33aadff | |||
| d0c83ec1a2 | |||
| aa94bee606 | |||
| 745609423f | |||
| 16e1c7e4eb | |||
| aec3672228 | |||
| aace3abd6d | |||
| ca3fefaae8 | |||
| d4f89735f6 | |||
| b37d393286 | |||
| 5e0c829cf6 | |||
| a80f2bb2ac | |||
| 7de206447a | |||
| f740514e2b | |||
| b907dd93f1 | |||
| 011a6303eb | |||
| 12f487d937 | |||
| b9aebc8406 | |||
| ffb3c3d4f4 | |||
| f291ba74e9 | |||
| 6e87fc9bcd | |||
| cb402e2bb6 | |||
| fe5101340a | |||
| dfac66e8bc | |||
| bc05835dae | |||
|
|
26371cdb6a | ||
|
|
41db0500af | ||
|
|
8114c3bda4 | ||
|
|
3f141bf93b | ||
|
|
a9bf658b03 | ||
|
|
ae9a35e3bb | ||
|
|
32d32380d8 | ||
|
|
5e40f0bfe8 | ||
| 315b35fc81 | |||
| 54dec6aa89 | |||
| 92c8faec11 | |||
| 2aff4989e5 | |||
| f1d2a31be6 | |||
| f604d1a3c8 | |||
| 26a54436f7 | |||
| 2fd85a1034 | |||
| c8e81cd9f0 | |||
| 2f61ce9bf3 |
17
.github/workflows/bearer.yml
vendored
Normal file
17
.github/workflows/bearer.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Bearer
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
rule_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Bearer
|
||||
uses: bearer/bearer-action@v2
|
||||
87
.github/workflows/build.yml
vendored
Normal file
87
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
name: Go Build Multi-Platform
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux, windows, darwin, freebsd]
|
||||
goarch: [amd64, arm64, arm]
|
||||
exclude:
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
build_complete: ${{ steps.build_step.outcome == 'success' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
|
||||
- name: Build
|
||||
id: build_step
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOARM: ${{ matrix.goarch == 'arm' && '6' || '' }}
|
||||
run: |
|
||||
output_name="reticulum-go-${GOOS}-${GOARCH}"
|
||||
if [ "$GOOS" = "windows" ]; then
|
||||
output_name+=".exe"
|
||||
fi
|
||||
go build -v -ldflags="-s -w" -o "${output_name}" ./cmd/reticulum-go
|
||||
echo "Built: ${output_name}"
|
||||
|
||||
- name: Calculate SHA256 Checksum
|
||||
run: |
|
||||
output_name="reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}"
|
||||
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||||
output_name+=".exe"
|
||||
fi
|
||||
sha256sum "${output_name}" > "${output_name}.sha256"
|
||||
echo "Calculated SHA256 for ${output_name}"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}*
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download All Build Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ./release-assets
|
||||
|
||||
- name: List downloaded files (for debugging)
|
||||
run: ls -R ./release-assets
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ./release-assets/*/*
|
||||
27
.github/workflows/go-test.yml
vendored
Normal file
27
.github/workflows/go-test.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Go Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go 1.24
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.24'
|
||||
|
||||
- name: Run Go tests
|
||||
run: go test ./...
|
||||
24
.github/workflows/gosec.yml
vendored
24
.github/workflows/gosec.yml
vendored
@@ -1,28 +1,22 @@
|
||||
name: "Security Scan"
|
||||
|
||||
name: Run Gosec
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
branches:
|
||||
- main
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
env:
|
||||
GO111MODULE: on
|
||||
steps:
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
- name: Run Gosec Security Scanner
|
||||
uses: securego/gosec@master
|
||||
with:
|
||||
args: '-no-fail -fmt sarif -out results.sarif ./...'
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
args: ./...
|
||||
|
||||
29
.github/workflows/revive.yml
vendored
Normal file
29
.github/workflows/revive.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Go Revive Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
|
||||
- name: Install revive
|
||||
run: go install github.com/mgechev/revive@latest
|
||||
|
||||
- name: Run revive
|
||||
run: |
|
||||
revive -config revive.toml -formatter stylish ./...
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -5,12 +5,3 @@ logs/
|
||||
.json
|
||||
|
||||
bin/
|
||||
|
||||
RNS/
|
||||
|
||||
test-network/go-rns-network/reticulum/storage/
|
||||
test-network/go-rns-network/reticulum/interfaces/
|
||||
test-network/go-rns-network/nomadnetwork
|
||||
|
||||
/test-network/go-rns-network/
|
||||
/test-network/go-rns-network-client/
|
||||
21
CONTRIBUTING.md
Normal file
21
CONTRIBUTING.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Contributing
|
||||
|
||||
Be good to each other.
|
||||
|
||||
## Development
|
||||
|
||||
By contributing to this project you agree to the following:
|
||||
|
||||
- All code must be tested using `gosec`.
|
||||
- All code must be formatted with `gofmt`.
|
||||
- All code must be documented.
|
||||
|
||||
## Communication
|
||||
|
||||
Feel free to join our seperate matrix channel for this implementation.
|
||||
|
||||
- [Matrix](https://matrix.to/#/#reticulum-go-dev:matrix.org)
|
||||
|
||||
## Usage of LLMs and other Generative AI tools
|
||||
|
||||
We would prefer if you did not use LLMs and other generative AI tools to write critical parts of the code.
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
Copyright 2024 Sudo-Ivan / Quad4.io
|
||||
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:
|
||||
|
||||
|
||||
11
Makefile
11
Makefile
@@ -70,7 +70,14 @@ build-netbsd:
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm $(MAIN_PACKAGE)
|
||||
|
||||
build-all: build-linux build-windows build-darwin build-freebsd build-openbsd build-netbsd
|
||||
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)
|
||||
@@ -92,6 +99,8 @@ help:
|
||||
@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"
|
||||
186
README.md
186
README.md
@@ -1,10 +1,22 @@
|
||||
# Reticulum-Go
|
||||
|
||||
[Reticulum Network](https://github.com/markqvist/Reticulum) implementation in Go `1.24`.
|
||||
> [!WARNING]
|
||||
> This project is still work in progress. Currently not compatible with the Python version.
|
||||
|
||||
Aiming for full spec compatibility with the Python version 0.9.2.
|
||||
[](https://socket.dev/go/package/github.com/sudo-ivan/reticulum-go)
|
||||

|
||||

|
||||
[](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/bearer.yml)
|
||||
[](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/build.yml)
|
||||
[](https://github.com/Sudo-Ivan/Reticulum-Go/actions/workflows/revive.yml)
|
||||
|
||||
# Testing
|
||||
[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
|
||||
@@ -22,170 +34,4 @@ revive -config revive.toml -formatter friendly ./pkg/* ./cmd/* ./internal/*
|
||||
|
||||
## External Packages
|
||||
|
||||
- `golang.org/x/crypto` - Cryptographic primitives
|
||||
|
||||
## To-Do List
|
||||
|
||||
### Core Components (In Progress)
|
||||
- [x] Basic Configuration System
|
||||
- [x] Basic config structure
|
||||
- [x] Default settings
|
||||
- [x] Config file loading/saving
|
||||
- [x] Path management
|
||||
|
||||
- [x] Constants Definition (Testing required)
|
||||
- [x] Packet constants
|
||||
- [x] MTU constants
|
||||
- [x] Header types
|
||||
- [x] Additional protocol constants
|
||||
|
||||
- [x] Identity Management (Testing required)
|
||||
- [x] Identity creation
|
||||
- [x] Key pair generation
|
||||
- [x] Identity storage/recall
|
||||
- [x] Public key handling
|
||||
- [x] Signature verification
|
||||
- [x] Hash functions
|
||||
|
||||
- [x] Cryptographic Primitives (Testing required)
|
||||
- [x] Ed25519
|
||||
- [x] Curve25519
|
||||
- [x] AES-CBC
|
||||
- [x] SHA-256
|
||||
- [x] HKDF
|
||||
- [x] Secure random number generation
|
||||
- [x] HMAC
|
||||
|
||||
- [x] Packet Handling (In Progress)
|
||||
- [x] Packet creation
|
||||
- [x] Packet validation
|
||||
- [x] Basic proof system
|
||||
- [x] Packet encryption/decryption
|
||||
- [x] Signature verification
|
||||
- [x] Announce packet structure
|
||||
- [ ] Testing of packet encrypt/decrypt/sign/proof
|
||||
- [ ] Cross-client packet compatibility
|
||||
|
||||
- [x] Transport Layer (In Progress)
|
||||
- [x] Path management
|
||||
- [x] Basic packet routing
|
||||
- [x] Announce handling
|
||||
- [x] Link management
|
||||
- [x] Resource cleanup
|
||||
- [x] Network layer integration
|
||||
- [x] Basic announce implementation
|
||||
- [ ] Testing announce from go client to python client
|
||||
- [ ] Testing path finding and caching
|
||||
- [ ] Announce propagation optimization
|
||||
|
||||
- [x] Channel System (Testing Required)
|
||||
- [x] Channel creation and management
|
||||
- [x] Message handling
|
||||
- [x] Channel encryption
|
||||
- [x] Channel authentication
|
||||
- [x] Channel callbacks
|
||||
- [x] Integration with Buffer system
|
||||
- [ ] Testing with real network conditions
|
||||
- [ ] Cross-client compatibility testing
|
||||
|
||||
- [x] Buffer System (Testing Required)
|
||||
- [x] Raw channel reader/writer
|
||||
- [x] Buffered stream implementation
|
||||
- [x] Compression support
|
||||
- [ ] Testing with Channel system
|
||||
- [ ] Cross-client compatibility testing
|
||||
|
||||
- [x] Resolver System (Testing Required)
|
||||
- [x] Name resolution
|
||||
- [x] Cache management
|
||||
- [x] Announce handling
|
||||
- [x] Path resolution
|
||||
- [x] Integration with Transport layer
|
||||
- [ ] Testing with live network
|
||||
- [ ] Cross-client compatibility testing
|
||||
|
||||
### Interface Implementation (In Progress)
|
||||
- [x] UDP Interface
|
||||
- [x] TCP Interface
|
||||
- [x] Auto Interface
|
||||
- [ ] Local Interface (In Progress)
|
||||
- [ ] I2P Interface
|
||||
- [ ] Pipe Interface
|
||||
- [ ] RNode Interface
|
||||
- [ ] RNode Multiinterface
|
||||
- [ ] Serial Interface
|
||||
- [ ] AX25KISS Interface
|
||||
- [ ] Interface Discovery
|
||||
- [ ] Interface Modes
|
||||
- [ ] Full mode
|
||||
- [ ] Gateway mode
|
||||
- [ ] Access point mode
|
||||
- [ ] Roaming mode
|
||||
- [ ] Boundary mode
|
||||
|
||||
- [ ] Hot reloading interfaces
|
||||
|
||||
### Destination System (Testing required)
|
||||
- [x] Destination creation
|
||||
- [x] Destination types (IN/OUT)
|
||||
- [x] Destination aspects
|
||||
- [x] Announce implementation
|
||||
- [x] Ratchet support
|
||||
- [x] Request handlers
|
||||
|
||||
### Link System (Testing required)
|
||||
- [x] Link establishment
|
||||
- [x] Link teardown
|
||||
- [x] Basic packet transfer
|
||||
- [x] Encryption/Decryption
|
||||
- [x] Identity verification
|
||||
- [x] Request/Response handling
|
||||
- [x] Session key management
|
||||
- [x] Link state tracking
|
||||
|
||||
### Resource System (Testing required)
|
||||
- [x] Resource creation
|
||||
- [x] Resource transfer
|
||||
- [x] Compression
|
||||
- [x] Progress tracking
|
||||
- [x] Segmentation
|
||||
- [x] Cleanup routines
|
||||
|
||||
### Compatibility
|
||||
- [ ] RNS Utilities.
|
||||
- [ ] Reticulum config.
|
||||
|
||||
|
||||
### Testing & Validation (Priority)
|
||||
- [ ] Unit tests for all components
|
||||
- [ ] Identity tests
|
||||
- [ ] Packet tests
|
||||
- [ ] Transport tests
|
||||
- [ ] Interface tests
|
||||
- [ ] Announce tests
|
||||
- [ ] Channel tests
|
||||
- [ ] Buffer tests
|
||||
- [ ] Resolver tests
|
||||
- [ ] Link tests
|
||||
- [ ] Resource tests
|
||||
- [ ] Integration tests
|
||||
- [ ] Go client to Go client
|
||||
- [ ] Go client to Python client
|
||||
- [ ] Interface compatibility
|
||||
- [ ] Path finding and resolution
|
||||
- [ ] Channel system end-to-end
|
||||
- [ ] Buffer system performance
|
||||
- [ ] Cross-client compatibility tests
|
||||
- [ ] Performance benchmarks
|
||||
- [ ] Security auditing (When Reticulum is 1.0 / stable)
|
||||
|
||||
### Documentation
|
||||
- [ ] API documentation
|
||||
- [ ] Usage examples
|
||||
|
||||
### Cleanup
|
||||
- [ ] Separate Cryptography from identity.go to their own files
|
||||
- [ ] Move constants to their own files
|
||||
- [ ] Remove default community interfaces in default config creation after testing
|
||||
- [ ] Optimize announce packet creation and caching
|
||||
- [ ] Improve debug logging system
|
||||
- `golang.org/x/crypto` `v0.39.0` - Cryptographic primitives
|
||||
|
||||
34
SECURITY.md
34
SECURITY.md
@@ -1,6 +1,10 @@
|
||||
# Security Policy
|
||||
|
||||
I use [Socket](https://socket.dev/), [Deepsource](https://deepsource.com/) and [gosec](https://github.com/securego/gosec) for this project.
|
||||
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
|
||||
|
||||
@@ -18,30 +22,4 @@ I use [Socket](https://socket.dev/), [Deepsource](https://deepsource.com/) and [
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report any security vulnerabilities to [rns@quad4.io](mailto:rns@quad4.io)
|
||||
|
||||
**PGP Key:**
|
||||
|
||||
```
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xjMEZ3RaxBYJKwYBBAHaRw8BAQdAcW8OFXyQ6KuqoTWKVbULYgakD/CeW50y
|
||||
W0KFou8WwJTNG3Juc0BxdWFkNC5pbyA8cm5zQHF1YWQ0LmlvPsLAEQQTFgoA
|
||||
gwWCZ3RaxAMLCQcJkJm7qyNLc8pmRRQAAAAAABwAIHNhbHRAbm90YXRpb25z
|
||||
Lm9wZW5wZ3Bqcy5vcmdVRY9jqwrIm+oRWRFnnBjKUcqvkG/kwkQZ3T74Xz3K
|
||||
QQMVCggEFgACAQIZAQKbAwIeARYhBG62BFzXpfHCy0yV95m7qyNLc8pmAACS
|
||||
oQD+K8oIaGx3tOlQbBV5AT3pHCaqXpRoL4W0V4JWc3VCi+MA/iiW6peitoae
|
||||
+YhKE5lnkiU1jP47VuItQDNt+fNyqNAOzjgEZ3RaxBIKKwYBBAGXVQEFAQEH
|
||||
QOBQyIb3gXV0Uih/V9Yx5JsFavxSenCtncNXx5KM6cB8AwEIB8K+BBgWCgBw
|
||||
BYJndFrECZCZu6sjS3PKZkUUAAAAAAAcACBzYWx0QG5vdGF0aW9ucy5vcGVu
|
||||
cGdwanMub3Jnpqm3qWGYB50CM/kuv+byGwQ3wxIGIpRlK8pwT4l+wXICmwwW
|
||||
IQRutgRc16XxwstMlfeZu6sjS3PKZgAAzm0BAIKHfL9G+IzCX9B1gVGcG9an
|
||||
j+gC4y9FrEsmFEBpvGeXAP93FfhO447jWijmxsImTtHTyvhpfeR3a7huFFyi
|
||||
lh60DA==
|
||||
=Nm9f
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
```
|
||||
|
||||
## Gosec Command
|
||||
|
||||
`gosec ./cmd/* ./pkg/* ./internal/*`
|
||||
Please report any security vulnerabilities using Github reporting tool or email to [rns@quad4.io](mailto:rns@quad4.io)
|
||||
167
TODO.md
Normal file
167
TODO.md
Normal file
@@ -0,0 +1,167 @@
|
||||
### Core Components (In Progress)
|
||||
|
||||
Last Updated: 2025-07-06
|
||||
|
||||
- [x] Basic Configuration System
|
||||
- [x] Basic config structure
|
||||
- [x] Default settings
|
||||
- [x] Config file loading/saving
|
||||
- [x] Path management
|
||||
|
||||
- [x] Constants Definition (Testing required)
|
||||
- [x] Packet constants
|
||||
- [x] MTU constants
|
||||
- [x] Header types
|
||||
- [x] Additional protocol constants
|
||||
|
||||
- [x] Identity Management (Testing required)
|
||||
- [x] Identity creation
|
||||
- [x] Key pair generation
|
||||
- [x] Identity storage/recall
|
||||
- [x] Public key handling
|
||||
- [x] Signature verification
|
||||
- [x] Hash functions
|
||||
|
||||
- [x] Cryptographic Primitives (Testing required)
|
||||
- [x] Ed25519
|
||||
- [x] Curve25519
|
||||
- [x] AES-128-CBC
|
||||
- [x] AES-256-CBC
|
||||
- [x] SHA-256
|
||||
- [x] HKDF
|
||||
- [x] Secure random number generation
|
||||
- [x] HMAC
|
||||
|
||||
- [x] Packet Handling (In Progress)
|
||||
- [x] Packet creation
|
||||
- [x] Packet validation
|
||||
- [x] Basic proof system
|
||||
- [x] Packet encryption/decryption
|
||||
- [x] Signature verification
|
||||
- [x] Announce packet structure
|
||||
- [ ] Testing of packet encrypt/decrypt/sign/proof
|
||||
- [ ] Cross-client packet compatibility
|
||||
|
||||
- [x] Transport Layer (In Progress)
|
||||
- [x] Path management
|
||||
- [x] Basic packet routing
|
||||
- [x] Announce handling
|
||||
- [x] Link management
|
||||
- [x] Resource cleanup
|
||||
- [x] Network layer integration
|
||||
- [x] Basic announce implementation
|
||||
- [ ] Testing announce from go client to python client
|
||||
- [ ] Testing path finding and caching
|
||||
- [ ] Announce propagation optimization
|
||||
|
||||
- [x] Channel System (Testing Required)
|
||||
- [x] Channel creation and management
|
||||
- [x] Message handling
|
||||
- [x] Channel encryption
|
||||
- [x] Channel authentication
|
||||
- [x] Channel callbacks
|
||||
- [x] Integration with Buffer system
|
||||
- [ ] Testing with real network conditions
|
||||
- [ ] Cross-client compatibility testing
|
||||
|
||||
- [x] Buffer System (Testing Required)
|
||||
- [x] Raw channel reader/writer
|
||||
- [x] Buffered stream implementation
|
||||
- [x] Compression support
|
||||
- [ ] Testing with Channel system
|
||||
- [ ] Cross-client compatibility testing
|
||||
|
||||
- [x] Resolver System (Testing Required)
|
||||
- [x] Name resolution
|
||||
- [x] Cache management
|
||||
- [x] Announce handling
|
||||
- [x] Path resolution
|
||||
- [x] Integration with Transport layer
|
||||
- [ ] Testing with live network
|
||||
- [ ] Cross-client compatibility testing
|
||||
|
||||
### Interface Implementation (In Progress)
|
||||
- [x] UDP Interface
|
||||
- [x] TCP Interface
|
||||
- [x] Auto Interface
|
||||
- [ ] Local Interface (In Progress)
|
||||
- [ ] I2P Interface
|
||||
- [ ] Pipe Interface
|
||||
- [ ] RNode Interface
|
||||
- [ ] RNode Multiinterface
|
||||
- [ ] Serial Interface
|
||||
- [ ] AX25KISS Interface
|
||||
- [ ] Interface Discovery
|
||||
- [ ] Interface Modes
|
||||
- [ ] Full mode
|
||||
- [ ] Gateway mode
|
||||
- [ ] Access point mode
|
||||
- [ ] Roaming mode
|
||||
- [ ] Boundary mode
|
||||
|
||||
- [ ] Hot reloading interfaces
|
||||
|
||||
### Destination System (Testing required)
|
||||
- [x] Destination creation
|
||||
- [x] Destination types (IN/OUT)
|
||||
- [x] Destination aspects
|
||||
- [ ] Announce implementation (Fixing)
|
||||
- [x] Ratchet support
|
||||
- [x] Request handlers
|
||||
|
||||
### Link System (Testing required)
|
||||
- [x] Link establishment
|
||||
- [x] Link teardown
|
||||
- [x] Basic packet transfer
|
||||
- [x] Encryption/Decryption
|
||||
- [x] Identity verification
|
||||
- [x] Request/Response handling
|
||||
- [x] Session key management
|
||||
- [x] Link state tracking
|
||||
|
||||
### Resource System (Testing required)
|
||||
- [x] Resource creation
|
||||
- [x] Resource transfer
|
||||
- [x] Compression
|
||||
- [x] Progress tracking
|
||||
- [x] Segmentation
|
||||
- [x] Cleanup routines
|
||||
|
||||
### Compatibility
|
||||
- [ ] RNS Utilities.
|
||||
- [ ] Reticulum config.
|
||||
|
||||
|
||||
### Testing & Validation (Priority)
|
||||
- [ ] Unit tests for all components
|
||||
- [ ] Identity tests
|
||||
- [ ] Packet tests
|
||||
- [ ] Transport tests
|
||||
- [ ] Interface tests
|
||||
- [ ] Announce tests
|
||||
- [ ] Channel tests
|
||||
- [ ] Buffer tests
|
||||
- [ ] Resolver tests
|
||||
- [ ] Link tests
|
||||
- [ ] Resource tests
|
||||
- [ ] Integration tests
|
||||
- [ ] Go client to Go client
|
||||
- [ ] Go client to Python client
|
||||
- [ ] Interface compatibility
|
||||
- [ ] Path finding and resolution
|
||||
- [ ] Channel system end-to-end
|
||||
- [ ] Buffer system performance
|
||||
- [ ] Cross-client compatibility tests
|
||||
- [ ] Performance benchmarks
|
||||
- [ ] Security auditing (When Reticulum is 1.0 / stable)
|
||||
|
||||
### Documentation
|
||||
- [ ] API documentation
|
||||
- [ ] Usage examples
|
||||
|
||||
### Cleanup
|
||||
- [ ] Separate Cryptography from identity.go to their own files
|
||||
- [ ] Move constants to their own files
|
||||
- [ ] Remove default community interfaces in default config creation after testing
|
||||
- [ ] Optimize announce packet creation and caching
|
||||
- [ ] Improve debug logging system
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 315 KiB |
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -12,7 +13,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/internal/config"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/announce"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/buffer"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/channel"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
@@ -48,7 +48,7 @@ const (
|
||||
DEBUG_PACKETS = 6 // Packet-level details
|
||||
DEBUG_ALL = 7 // Everything including identity operations
|
||||
APP_NAME = "Go-Client"
|
||||
APP_ASPECT = "node"
|
||||
APP_ASPECT = "node" // Always use "node" for node announces
|
||||
)
|
||||
|
||||
type Reticulum struct {
|
||||
@@ -62,9 +62,16 @@ type Reticulum struct {
|
||||
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) {
|
||||
@@ -74,10 +81,10 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
|
||||
|
||||
// Set default app name and aspect if not provided
|
||||
if cfg.AppName == "" {
|
||||
cfg.AppName = "Go Client"
|
||||
cfg.AppName = APP_NAME
|
||||
}
|
||||
if cfg.AppAspect == "" {
|
||||
cfg.AppAspect = "node"
|
||||
cfg.AppAspect = APP_ASPECT // Always use "node" for node announcements
|
||||
}
|
||||
|
||||
if err := initializeDirectories(); err != nil {
|
||||
@@ -100,23 +107,17 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
|
||||
identity,
|
||||
destination.IN,
|
||||
destination.SINGLE,
|
||||
APP_NAME,
|
||||
APP_ASPECT,
|
||||
"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 default app data for announces
|
||||
appData := []byte(fmt.Sprintf(`{"app":"%s","aspect":"%s","version":"0.1.0"}`, APP_NAME, APP_ASPECT))
|
||||
dest.SetDefaultAppData(appData)
|
||||
|
||||
// Enable destination features
|
||||
dest.AcceptsLinks(true)
|
||||
dest.EnableRatchets("") // Empty string for default path
|
||||
dest.SetProofStrategy(destination.PROVE_APP)
|
||||
debugLog(DEBUG_VERBOSE, "Configured destination features")
|
||||
// Set node metadata
|
||||
nodeTimestamp := time.Now().Unix()
|
||||
|
||||
r := &Reticulum{
|
||||
config: cfg,
|
||||
@@ -128,8 +129,22 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
|
||||
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 {
|
||||
@@ -233,7 +248,7 @@ func (r *Reticulum) monitorInterfaces() {
|
||||
stats = fmt.Sprintf("%s, RTT: %v", stats, tcpClient.GetRTT())
|
||||
}
|
||||
|
||||
debugLog(DEBUG_VERBOSE, stats)
|
||||
debugLog(DEBUG_VERBOSE, "%s", stats)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,18 +291,6 @@ func main() {
|
||||
log.Fatalf("Failed to create Reticulum instance: %v", err)
|
||||
}
|
||||
|
||||
// Create announce using r.identity
|
||||
announce, err := announce.NewAnnounce(
|
||||
r.identity,
|
||||
[]byte("HELLO WORLD"),
|
||||
nil,
|
||||
false,
|
||||
r.config,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create announce: %v", err)
|
||||
}
|
||||
|
||||
// Start monitoring interfaces
|
||||
go r.monitorInterfaces()
|
||||
|
||||
@@ -300,39 +303,6 @@ func main() {
|
||||
log.Fatalf("Failed to start Reticulum: %v", err)
|
||||
}
|
||||
|
||||
// Send initial announces after interfaces are ready
|
||||
time.Sleep(2 * time.Second) // Give interfaces time to connect
|
||||
for _, iface := range r.interfaces {
|
||||
if netIface, ok := iface.(common.NetworkInterface); ok {
|
||||
if netIface.IsEnabled() && netIface.IsOnline() {
|
||||
debugLog(2, "Sending initial announce on interface %s", netIface.GetName())
|
||||
if err := announce.Propagate([]common.NetworkInterface{netIface}); err != nil {
|
||||
debugLog(1, "Failed to propagate initial announce: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start periodic announces
|
||||
go func() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
debugLog(3, "Starting periodic announce cycle")
|
||||
for _, iface := range r.interfaces {
|
||||
if netIface, ok := iface.(common.NetworkInterface); ok {
|
||||
if netIface.IsEnabled() && netIface.IsOnline() {
|
||||
debugLog(2, "Sending periodic announce on interface %s", netIface.GetName())
|
||||
if err := announce.Propagate([]common.NetworkInterface{netIface}); err != nil {
|
||||
debugLog(1, "Failed to propagate periodic announce: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigChan
|
||||
@@ -402,7 +372,7 @@ func initializeDirectories() error {
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
if err := os.MkdirAll(dir, 0700); err != nil { // #nosec G301
|
||||
return fmt.Errorf("failed to create directory %s: %v", dir, err)
|
||||
}
|
||||
}
|
||||
@@ -437,70 +407,26 @@ func (r *Reticulum) Start() error {
|
||||
// Wait for interfaces to initialize
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Send initial announce once per interface
|
||||
initialAnnounce, err := announce.NewAnnounce(
|
||||
r.identity,
|
||||
createAppData(r.config.AppName, r.config.AppAspect),
|
||||
nil,
|
||||
false,
|
||||
r.config,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create announce: %v", err)
|
||||
// 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)
|
||||
}
|
||||
|
||||
for _, iface := range r.interfaces {
|
||||
if netIface, ok := iface.(common.NetworkInterface); ok {
|
||||
if netIface.IsEnabled() && netIface.IsOnline() {
|
||||
debugLog(2, "Sending initial announce on interface %s", netIface.GetName())
|
||||
if err := initialAnnounce.Propagate([]common.NetworkInterface{netIface}); err != nil {
|
||||
debugLog(1, "Failed to send initial announce on interface %s: %v", netIface.GetName(), err)
|
||||
}
|
||||
// Add delay between interfaces
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start periodic announce goroutine with rate limiting
|
||||
// Start periodic announce goroutine
|
||||
go func() {
|
||||
ticker := time.NewTicker(ANNOUNCE_RATE_TARGET * time.Second)
|
||||
defer ticker.Stop()
|
||||
// Wait a bit before the first announce
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
announceCount := 0
|
||||
for range ticker.C {
|
||||
announceCount++
|
||||
debugLog(3, "Starting periodic announce cycle #%d", announceCount)
|
||||
|
||||
periodicAnnounce, err := announce.NewAnnounce(
|
||||
r.identity,
|
||||
createAppData(r.config.AppName, r.config.AppAspect),
|
||||
nil,
|
||||
false,
|
||||
r.config,
|
||||
)
|
||||
for {
|
||||
debugLog(3, "Announcing destination...")
|
||||
err := r.destination.Announce(r.createNodeAppData())
|
||||
if err != nil {
|
||||
debugLog(1, "Failed to create periodic announce: %v", err)
|
||||
continue
|
||||
debugLog(1, "Could not send announce: %v", err)
|
||||
}
|
||||
|
||||
// Send to each interface with rate limiting
|
||||
for _, iface := range r.interfaces {
|
||||
if netIface, ok := iface.(common.NetworkInterface); ok {
|
||||
if netIface.IsEnabled() && netIface.IsOnline() {
|
||||
// Apply rate limiting after grace period
|
||||
if announceCount > ANNOUNCE_RATE_GRACE {
|
||||
time.Sleep(time.Duration(ANNOUNCE_RATE_PENALTY) * time.Second)
|
||||
}
|
||||
|
||||
debugLog(2, "Sending periodic announce on interface %s", netIface.GetName())
|
||||
if err := periodicAnnounce.Propagate([]common.NetworkInterface{netIface}); err != nil {
|
||||
debugLog(1, "Failed to send periodic announce on interface %s: %v", netIface.GetName(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Announce every 5 minutes
|
||||
time.Sleep(5 * time.Minute)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -559,25 +485,82 @@ func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appD
|
||||
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 { // msgpack array of 2 elements
|
||||
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 (name)
|
||||
if appData[pos] == 0xc4 { // bin 8 format
|
||||
// Parse first element (NameBytes)
|
||||
if pos+1 < len(appData) && appData[pos] == 0xc4 {
|
||||
nameLen := int(appData[pos+1])
|
||||
name := string(appData[pos+2 : pos+2+nameLen])
|
||||
pos += 2 + nameLen
|
||||
debugLog(DEBUG_VERBOSE, "Announce name: %s", name)
|
||||
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 (app data)
|
||||
if pos < len(appData) && appData[pos] == 0xc4 { // bin 8 format
|
||||
dataLen := int(appData[pos+1])
|
||||
data := appData[pos+2 : pos+2+dataLen]
|
||||
debugLog(DEBUG_VERBOSE, "Announce app data: %s", string(data))
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,14 +581,22 @@ func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appD
|
||||
}
|
||||
}
|
||||
|
||||
// Store announce in history
|
||||
// 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{
|
||||
// You can add fields here to store relevant announce data
|
||||
timestamp: time.Now().Unix(),
|
||||
appData: appData,
|
||||
}
|
||||
h.reticulum.announceHistoryMu.Unlock()
|
||||
|
||||
debugLog(DEBUG_VERBOSE, "Stored announce in history for identity %s", identity.GetHexHash())
|
||||
debugLog(DEBUG_VERBOSE, "Stored %s announce in history for identity %s", recordType, identity.GetHexHash())
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -619,24 +610,33 @@ func (r *Reticulum) GetDestination() *destination.Destination {
|
||||
return r.destination
|
||||
}
|
||||
|
||||
func createAppData(appName, appAspect string) []byte {
|
||||
nameString := fmt.Sprintf("%s.%s", appName, appAspect)
|
||||
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
|
||||
|
||||
// Create MessagePack array with 2 elements
|
||||
appData := []byte{0x92} // Fix array with 2 elements
|
||||
// Element 0: Boolean for enable/disable peer
|
||||
if r.nodeEnabled {
|
||||
appData = append(appData, 0xc3) // true
|
||||
} else {
|
||||
appData = append(appData, 0xc2) // false
|
||||
}
|
||||
|
||||
// First element: name string (always use str 8 format for consistency)
|
||||
nameBytes := []byte(nameString)
|
||||
appData = append(appData, 0xd9) // str 8 format
|
||||
appData = append(appData, byte(len(nameBytes))) // length
|
||||
appData = append(appData, nameBytes...) // string data
|
||||
// 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...)
|
||||
|
||||
// Second element: version string (always use str 8 format for consistency)
|
||||
version := "0.1.0"
|
||||
versionBytes := []byte(version)
|
||||
appData = append(appData, 0xd9) // str 8 format
|
||||
appData = append(appData, byte(len(versionBytes))) // length
|
||||
appData = append(appData, versionBytes...) // string data
|
||||
// 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
|
||||
}
|
||||
|
||||
4
go.mod
4
go.mod
@@ -1,5 +1,5 @@
|
||||
module github.com/Sudo-Ivan/reticulum-go
|
||||
|
||||
go 1.24.0
|
||||
go 1.24.4
|
||||
|
||||
require golang.org/x/crypto v0.31.0
|
||||
require golang.org/x/crypto v0.39.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1,2 +1,2 @@
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
|
||||
@@ -44,7 +44,7 @@ func EnsureConfigDir() error {
|
||||
}
|
||||
|
||||
configDir := filepath.Join(homeDir, ".reticulum-go")
|
||||
return os.MkdirAll(configDir, 0755)
|
||||
return os.MkdirAll(configDir, 0700) // #nosec G301
|
||||
}
|
||||
|
||||
// parseValue parses string values into appropriate types
|
||||
@@ -70,7 +70,7 @@ func parseValue(value string) interface{} {
|
||||
|
||||
// LoadConfig loads the configuration from the specified path
|
||||
func LoadConfig(path string) (*common.ReticulumConfig, error) {
|
||||
file, err := os.Open(path)
|
||||
file, err := os.Open(path) // #nosec G304
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -202,7 +202,7 @@ func SaveConfig(cfg *common.ReticulumConfig) error {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
return os.WriteFile(cfg.ConfigPath, []byte(builder.String()), 0644)
|
||||
return os.WriteFile(cfg.ConfigPath, []byte(builder.String()), 0600) // #nosec G306
|
||||
}
|
||||
|
||||
// CreateDefaultConfig creates a default configuration file
|
||||
@@ -244,7 +244,7 @@ func CreateDefaultConfig(path string) error {
|
||||
Port: 37696,
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { // #nosec G301
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -96,7 +97,10 @@ func New(dest *identity.Identity, appData []byte, pathResponse bool, config *com
|
||||
// Get current ratchet ID if enabled
|
||||
currentRatchet := dest.GetCurrentRatchetKey()
|
||||
if currentRatchet != nil {
|
||||
a.ratchetID = dest.GetRatchetID(currentRatchet)
|
||||
ratchetPub, err := curve25519.X25519(currentRatchet, curve25519.Basepoint)
|
||||
if err == nil {
|
||||
a.ratchetID = dest.GetRatchetID(ratchetPub)
|
||||
}
|
||||
}
|
||||
|
||||
// Sign announce data
|
||||
@@ -169,44 +173,108 @@ func (a *Announce) HandleAnnounce(data []byte) error {
|
||||
|
||||
log.Printf("[DEBUG-7] Handling announce packet of %d bytes", len(data))
|
||||
|
||||
// Minimum packet size validation (header(2) + desthash(16) + enckey(32) + signkey(32) + namehash(10) +
|
||||
// randomhash(10) + signature(64) + min app data(3))
|
||||
if len(data) < 169 {
|
||||
log.Printf("[DEBUG-7] Invalid announce data length: %d bytes", len(data))
|
||||
// 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")
|
||||
}
|
||||
|
||||
// Parse fields
|
||||
// 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]
|
||||
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:]
|
||||
|
||||
log.Printf("[DEBUG-7] Announce fields: destHash=%x, encKey=%x, signKey=%x",
|
||||
destHash, encKey, signKey)
|
||||
log.Printf("[DEBUG-7] Name hash=%x, random hash=%x", nameHash, randomHash)
|
||||
|
||||
// Validate hop count
|
||||
if hopCount > MAX_HOPS {
|
||||
log.Printf("[DEBUG-7] Announce exceeded max hops: %d", hopCount)
|
||||
return errors.New("announce exceeded maximum hop count")
|
||||
}
|
||||
|
||||
// Create announced identity from public keys
|
||||
// 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 !announcedIdentity.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")
|
||||
}
|
||||
|
||||
@@ -254,54 +322,86 @@ func CreateHeader(ifacFlag byte, headerType byte, contextFlag byte, propType byt
|
||||
}
|
||||
|
||||
func (a *Announce) CreatePacket() []byte {
|
||||
// Create header
|
||||
// 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_1,
|
||||
0, // No context flag
|
||||
HEADER_TYPE_2,
|
||||
0, // No context flag for announce
|
||||
PROP_TYPE_BROADCAST,
|
||||
DEST_TYPE_SINGLE,
|
||||
PACKET_TYPE_ANNOUNCE,
|
||||
a.hops,
|
||||
)
|
||||
|
||||
packet := header
|
||||
// 2. Destination Hash
|
||||
destHash := a.identity.Hash()
|
||||
|
||||
// Add destination hash (16 bytes)
|
||||
packet = append(packet, a.destinationHash...)
|
||||
// 3. Transport ID (zeros for broadcast announce)
|
||||
transportID := make([]byte, 16)
|
||||
|
||||
// Add public key parts (32 bytes each)
|
||||
// 4. Context Byte (zero for announce)
|
||||
contextByte := byte(0)
|
||||
|
||||
// 5. Announce Data
|
||||
// 5.1 Public Keys
|
||||
pubKey := a.identity.GetPublicKey()
|
||||
packet = append(packet, pubKey[:32]...) // Encryption key
|
||||
packet = append(packet, pubKey[32:]...) // Signing key
|
||||
encKey := pubKey[:32]
|
||||
signKey := pubKey[32:]
|
||||
|
||||
// Add name hash (10 bytes)
|
||||
nameHash := sha256.Sum256([]byte(fmt.Sprintf("%s.%s", a.config.AppName, a.config.AppAspect)))
|
||||
packet = append(packet, nameHash[:10]...)
|
||||
// 5.2 Name Hash
|
||||
appName := fmt.Sprintf("%s.%s", a.config.AppName, a.config.AppAspect)
|
||||
nameHash := sha256.Sum256([]byte(appName))
|
||||
nameHash10 := nameHash[:10]
|
||||
|
||||
// Add random hash (10 bytes)
|
||||
randomBytes := make([]byte, 10)
|
||||
rand.Read(randomBytes)
|
||||
packet = append(packet, randomBytes...)
|
||||
|
||||
// Create validation data for signature
|
||||
validationData := make([]byte, 0)
|
||||
validationData = append(validationData, a.destinationHash...)
|
||||
validationData = append(validationData, pubKey[:32]...) // Encryption key
|
||||
validationData = append(validationData, pubKey[32:]...) // Signing key
|
||||
validationData = append(validationData, nameHash[:10]...)
|
||||
validationData = append(validationData, randomBytes...)
|
||||
validationData = append(validationData, a.appData...)
|
||||
|
||||
// Add signature (64 bytes)
|
||||
signature := a.identity.Sign(validationData)
|
||||
packet = append(packet, signature...)
|
||||
|
||||
// Add app data
|
||||
if len(a.appData) > 0 {
|
||||
packet = append(packet, a.appData...)
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -324,7 +424,7 @@ func NewAnnouncePacket(pubKey []byte, appData []byte, announceID []byte) *Announ
|
||||
|
||||
// Add app data length and content
|
||||
appDataLen := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(appDataLen, uint16(len(appData)))
|
||||
binary.BigEndian.PutUint16(appDataLen, uint16(len(appData))) // #nosec G115
|
||||
packet.Data = append(packet.Data, appDataLen...)
|
||||
packet.Data = append(packet.Data, appData...)
|
||||
|
||||
@@ -398,36 +498,8 @@ func (a *Announce) GetPacket() []byte {
|
||||
defer a.mutex.Unlock()
|
||||
|
||||
if a.packet == nil {
|
||||
// Generate hash from announce data
|
||||
h := sha256.New()
|
||||
h.Write(a.destinationHash)
|
||||
h.Write(a.identity.GetPublicKey())
|
||||
h.Write([]byte{a.hops})
|
||||
h.Write(a.appData)
|
||||
if a.ratchetID != nil {
|
||||
h.Write(a.ratchetID)
|
||||
}
|
||||
|
||||
// Construct packet
|
||||
packet := make([]byte, 0)
|
||||
packet = append(packet, PACKET_TYPE_ANNOUNCE)
|
||||
packet = append(packet, a.destinationHash...)
|
||||
packet = append(packet, a.identity.GetPublicKey()...)
|
||||
packet = append(packet, a.hops)
|
||||
packet = append(packet, a.appData...)
|
||||
if a.ratchetID != nil {
|
||||
packet = append(packet, a.ratchetID...)
|
||||
}
|
||||
|
||||
// Add signature
|
||||
signData := append(a.destinationHash, a.appData...)
|
||||
if a.ratchetID != nil {
|
||||
signData = append(signData, a.ratchetID...)
|
||||
}
|
||||
signature := a.identity.Sign(signData)
|
||||
packet = append(packet, signature...)
|
||||
|
||||
a.packet = packet
|
||||
// Use CreatePacket to generate the packet
|
||||
a.packet = a.CreatePacket()
|
||||
}
|
||||
|
||||
return a.packet
|
||||
|
||||
@@ -35,7 +35,9 @@ func (m *StreamDataMessage) Pack() ([]byte, error) {
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
binary.Write(buf, binary.BigEndian, headerVal)
|
||||
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
|
||||
}
|
||||
@@ -111,8 +113,8 @@ func (r *RawChannelReader) Read(p []byte) (n int, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (r *RawChannelReader) HandleMessage(msg channel.MessageBase) bool {
|
||||
if streamMsg, ok := msg.(*StreamDataMessage); ok && streamMsg.StreamID == uint16(r.streamID) {
|
||||
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()
|
||||
|
||||
@@ -156,7 +158,7 @@ func (w *RawChannelWriter) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
msg := &StreamDataMessage{
|
||||
StreamID: uint16(w.streamID),
|
||||
StreamID: uint16(w.streamID), // #nosec G115
|
||||
Data: p,
|
||||
EOF: w.eof,
|
||||
}
|
||||
@@ -228,13 +230,23 @@ func compressData(data []byte) []byte {
|
||||
var compressed bytes.Buffer
|
||||
w := bytes.NewBuffer(data)
|
||||
r := bzip2.NewReader(w)
|
||||
io.Copy(&compressed, r)
|
||||
_, 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
|
||||
io.Copy(&decompressed, reader)
|
||||
// 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()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package channel
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -138,7 +139,14 @@ func (c *Channel) handleTimeout(packet interface{}) {
|
||||
return
|
||||
}
|
||||
env.Tries++
|
||||
c.link.Resend(packet)
|
||||
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
|
||||
|
||||
94
pkg/common/config_test.go
Normal file
94
pkg/common/config_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewReticulumConfig(t *testing.T) {
|
||||
cfg := NewReticulumConfig()
|
||||
|
||||
if !cfg.EnableTransport {
|
||||
t.Errorf("NewReticulumConfig() EnableTransport = %v; want true", cfg.EnableTransport)
|
||||
}
|
||||
if cfg.ShareInstance {
|
||||
t.Errorf("NewReticulumConfig() ShareInstance = %v; want false", cfg.ShareInstance)
|
||||
}
|
||||
if cfg.SharedInstancePort != DEFAULT_SHARED_INSTANCE_PORT {
|
||||
t.Errorf("NewReticulumConfig() SharedInstancePort = %d; want %d", cfg.SharedInstancePort, DEFAULT_SHARED_INSTANCE_PORT)
|
||||
}
|
||||
if cfg.InstanceControlPort != DEFAULT_INSTANCE_CONTROL_PORT {
|
||||
t.Errorf("NewReticulumConfig() InstanceControlPort = %d; want %d", cfg.InstanceControlPort, DEFAULT_INSTANCE_CONTROL_PORT)
|
||||
}
|
||||
if cfg.PanicOnInterfaceErr {
|
||||
t.Errorf("NewReticulumConfig() PanicOnInterfaceErr = %v; want false", cfg.PanicOnInterfaceErr)
|
||||
}
|
||||
if cfg.LogLevel != DEFAULT_LOG_LEVEL {
|
||||
t.Errorf("NewReticulumConfig() LogLevel = %d; want %d", cfg.LogLevel, DEFAULT_LOG_LEVEL)
|
||||
}
|
||||
if len(cfg.Interfaces) != 0 {
|
||||
t.Errorf("NewReticulumConfig() Interfaces length = %d; want 0", len(cfg.Interfaces))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
if !cfg.EnableTransport {
|
||||
t.Errorf("DefaultConfig() EnableTransport = %v; want true", cfg.EnableTransport)
|
||||
}
|
||||
if cfg.ShareInstance {
|
||||
t.Errorf("DefaultConfig() ShareInstance = %v; want false", cfg.ShareInstance)
|
||||
}
|
||||
if cfg.SharedInstancePort != DEFAULT_SHARED_INSTANCE_PORT {
|
||||
t.Errorf("DefaultConfig() SharedInstancePort = %d; want %d", cfg.SharedInstancePort, DEFAULT_SHARED_INSTANCE_PORT)
|
||||
}
|
||||
if cfg.InstanceControlPort != DEFAULT_INSTANCE_CONTROL_PORT {
|
||||
t.Errorf("DefaultConfig() InstanceControlPort = %d; want %d", cfg.InstanceControlPort, DEFAULT_INSTANCE_CONTROL_PORT)
|
||||
}
|
||||
if cfg.PanicOnInterfaceErr {
|
||||
t.Errorf("DefaultConfig() PanicOnInterfaceErr = %v; want false", cfg.PanicOnInterfaceErr)
|
||||
}
|
||||
if cfg.LogLevel != DEFAULT_LOG_LEVEL {
|
||||
t.Errorf("DefaultConfig() LogLevel = %d; want %d", cfg.LogLevel, DEFAULT_LOG_LEVEL)
|
||||
}
|
||||
if len(cfg.Interfaces) != 0 {
|
||||
t.Errorf("DefaultConfig() Interfaces length = %d; want 0", len(cfg.Interfaces))
|
||||
}
|
||||
if cfg.AppName != "Go Client" {
|
||||
t.Errorf("DefaultConfig() AppName = %q; want %q", cfg.AppName, "Go Client")
|
||||
}
|
||||
if cfg.AppAspect != "node" {
|
||||
t.Errorf("DefaultConfig() AppAspect = %q; want %q", cfg.AppAspect, "node")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReticulumConfig_Validate(t *testing.T) {
|
||||
validConfig := DefaultConfig()
|
||||
if err := validConfig.Validate(); err != nil {
|
||||
t.Errorf("Validate() on default config failed: %v", err)
|
||||
}
|
||||
|
||||
invalidPortConfig1 := DefaultConfig()
|
||||
invalidPortConfig1.SharedInstancePort = 0
|
||||
if err := invalidPortConfig1.Validate(); err == nil {
|
||||
t.Errorf("Validate() did not return error for invalid SharedInstancePort 0")
|
||||
}
|
||||
|
||||
invalidPortConfig2 := DefaultConfig()
|
||||
invalidPortConfig2.SharedInstancePort = 65536
|
||||
if err := invalidPortConfig2.Validate(); err == nil {
|
||||
t.Errorf("Validate() did not return error for invalid SharedInstancePort 65536")
|
||||
}
|
||||
|
||||
invalidPortConfig3 := DefaultConfig()
|
||||
invalidPortConfig3.InstanceControlPort = 0
|
||||
if err := invalidPortConfig3.Validate(); err == nil {
|
||||
t.Errorf("Validate() did not return error for invalid InstanceControlPort 0")
|
||||
}
|
||||
|
||||
invalidPortConfig4 := DefaultConfig()
|
||||
invalidPortConfig4.InstanceControlPort = 65536
|
||||
if err := invalidPortConfig4.Validate(); err == nil {
|
||||
t.Errorf("Validate() did not return error for invalid InstanceControlPort 65536")
|
||||
}
|
||||
}
|
||||
@@ -183,7 +183,7 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
|
||||
|
||||
// Add timestamp
|
||||
ts := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix()))
|
||||
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) // #nosec G115
|
||||
packet = append(packet, ts...)
|
||||
|
||||
// Add data
|
||||
|
||||
@@ -39,7 +39,7 @@ type Config struct {
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
file, err := os.Open(path)
|
||||
file, err := os.Open(path) // #nosec G304
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -176,7 +176,7 @@ func SaveConfig(cfg *Config, path string) error {
|
||||
builder.WriteString(fmt.Sprintf("i2p_tunneled = %v\n\n", iface.I2PTunneled))
|
||||
}
|
||||
|
||||
return os.WriteFile(path, []byte(builder.String()), 0644)
|
||||
return os.WriteFile(path, []byte(builder.String()), 0600) // #nosec G306
|
||||
}
|
||||
|
||||
func GetConfigDir() string {
|
||||
@@ -194,7 +194,7 @@ func GetDefaultConfigPath() string {
|
||||
|
||||
func EnsureConfigDir() error {
|
||||
configDir := GetConfigDir()
|
||||
return os.MkdirAll(configDir, 0755)
|
||||
return os.MkdirAll(configDir, 0700) // #nosec G301
|
||||
}
|
||||
|
||||
func InitConfig() (*Config, error) {
|
||||
|
||||
@@ -8,19 +8,39 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
func EncryptAESCBC(key, plaintext []byte) ([]byte, error) {
|
||||
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 IV
|
||||
// Generate a random IV.
|
||||
iv := make([]byte, aes.BlockSize)
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add PKCS7 padding
|
||||
// Add PKCS7 padding.
|
||||
padding := aes.BlockSize - len(plaintext)%aes.BlockSize
|
||||
padtext := make([]byte, len(plaintext)+padding)
|
||||
copy(padtext, plaintext)
|
||||
@@ -28,36 +48,63 @@ func EncryptAESCBC(key, plaintext []byte) ([]byte, error) {
|
||||
padtext[i] = byte(padding)
|
||||
}
|
||||
|
||||
// Encrypt
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
// 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
|
||||
}
|
||||
|
||||
func DecryptAESCBC(key, ciphertext []byte) ([]byte, error) {
|
||||
// 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 too short")
|
||||
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 block size")
|
||||
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
|
||||
// Remove PKCS7 padding.
|
||||
if len(plaintext) == 0 {
|
||||
return nil, errors.New("invalid padding: plaintext is empty")
|
||||
}
|
||||
|
||||
padding := int(plaintext[len(plaintext)-1])
|
||||
if padding > aes.BlockSize || padding == 0 {
|
||||
return nil, errors.New("invalid padding size")
|
||||
}
|
||||
if len(plaintext) < padding {
|
||||
return nil, errors.New("invalid padding: padding size is larger than plaintext")
|
||||
}
|
||||
|
||||
// Verify the padding bytes.
|
||||
for i := len(plaintext) - padding; i < len(plaintext); i++ {
|
||||
if plaintext[i] != byte(padding) {
|
||||
return nil, errors.New("invalid padding bytes")
|
||||
}
|
||||
}
|
||||
|
||||
return plaintext[:len(plaintext)-padding], nil
|
||||
}
|
||||
|
||||
194
pkg/cryptography/aes_test.go
Normal file
194
pkg/cryptography/aes_test.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package cryptography
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateAES256Key(t *testing.T) {
|
||||
key, err := GenerateAES256Key()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateAES256Key failed: %v", err)
|
||||
}
|
||||
if len(key) != AES256KeySize {
|
||||
t.Errorf("Expected key size %d, got %d", AES256KeySize, len(key))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAES256CBCEncryptionDecryption(t *testing.T) {
|
||||
key, err := GenerateAES256Key()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate AES-256 key: %v", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
plaintext []byte
|
||||
}{
|
||||
{"ShortMessage", []byte("Hello")},
|
||||
{"BlockSizeMessage", []byte("This is 16 bytes")},
|
||||
{"LongMessage", []byte("This is a longer message that spans multiple AES blocks and tests the padding.")},
|
||||
{"EmptyMessage", []byte("")},
|
||||
{"SingleByte", []byte("A")},
|
||||
{"ExactlyTwoBlocks", []byte("This is exactly 32 bytes long!!!")},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ciphertext, err := EncryptAES256CBC(key, tc.plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("EncryptAES256CBC failed: %v", err)
|
||||
}
|
||||
|
||||
decrypted, err := DecryptAES256CBC(key, ciphertext)
|
||||
if err != nil {
|
||||
t.Fatalf("DecryptAES256CBC failed: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(tc.plaintext, decrypted) {
|
||||
t.Errorf("Decrypted text does not match original plaintext.\nGot: %q (%x)\nWant: %q (%x)",
|
||||
decrypted, decrypted, tc.plaintext, tc.plaintext)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAES256CBC_InvalidKeySize(t *testing.T) {
|
||||
plaintext := []byte("test message")
|
||||
|
||||
invalidKeys := [][]byte{
|
||||
make([]byte, 16), // AES-128
|
||||
make([]byte, 24), // AES-192
|
||||
make([]byte, 15), // Too short
|
||||
make([]byte, 33), // Too long
|
||||
nil, // Nil key
|
||||
}
|
||||
|
||||
for i, key := range invalidKeys {
|
||||
t.Run(fmt.Sprintf("InvalidKey_%d", i), func(t *testing.T) {
|
||||
_, err := EncryptAES256CBC(key, plaintext)
|
||||
if err == nil {
|
||||
t.Error("EncryptAES256CBC should have failed with invalid key size")
|
||||
}
|
||||
|
||||
// Test with some dummy ciphertext
|
||||
dummyCiphertext := make([]byte, 32) // Just enough for IV + one block
|
||||
rand.Read(dummyCiphertext)
|
||||
_, err = DecryptAES256CBC(key, dummyCiphertext)
|
||||
if err == nil {
|
||||
t.Error("DecryptAES256CBC should have failed with invalid key size")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func TestDecryptAES256CBCErrorCases(t *testing.T) {
|
||||
key, err := GenerateAES256Key()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate key: %v", err)
|
||||
}
|
||||
|
||||
t.Run("CiphertextTooShort", func(t *testing.T) {
|
||||
shortCiphertext := []byte{0x01, 0x02, 0x03} // Less than AES block size
|
||||
_, err := DecryptAES256CBC(key, shortCiphertext)
|
||||
if err == nil {
|
||||
t.Error("DecryptAES256CBC should have failed for ciphertext shorter than block size")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CiphertextNotMultipleOfBlockSize", func(t *testing.T) {
|
||||
iv := make([]byte, aes.BlockSize)
|
||||
rand.Read(iv)
|
||||
invalidCiphertext := append(iv, []byte{0x01, 0x02, 0x03}...) // IV + data not multiple of block size
|
||||
_, err := DecryptAES256CBC(key, invalidCiphertext)
|
||||
if err == nil {
|
||||
t.Error("DecryptAES256CBC should have failed for ciphertext not multiple of block size")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidPadding", func(t *testing.T) {
|
||||
// Create a valid ciphertext first
|
||||
plaintext := []byte("valid data")
|
||||
ciphertext, err := EncryptAES256CBC(key, plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test ciphertext: %v", err)
|
||||
}
|
||||
|
||||
// Corrupt the last byte (which affects padding)
|
||||
corruptedCiphertext := make([]byte, len(ciphertext))
|
||||
copy(corruptedCiphertext, ciphertext)
|
||||
corruptedCiphertext[len(corruptedCiphertext)-1] ^= 0xFF
|
||||
|
||||
_, err = DecryptAES256CBC(key, corruptedCiphertext)
|
||||
if err == nil {
|
||||
t.Error("DecryptAES256CBC should have failed for corrupted padding")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmptyPlaintextAfterDecryption", func(t *testing.T) {
|
||||
// This creates a ciphertext that decrypts to just padding
|
||||
key, _ := GenerateAES256Key()
|
||||
iv := make([]byte, aes.BlockSize)
|
||||
// A block of padding bytes
|
||||
paddedBlock := bytes.Repeat([]byte{byte(aes.BlockSize)}, aes.BlockSize)
|
||||
|
||||
block, _ := aes.NewCipher(key)
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
ciphertext := make([]byte, len(paddedBlock))
|
||||
mode.CryptBlocks(ciphertext, paddedBlock)
|
||||
|
||||
// Prepend IV
|
||||
fullCiphertext := append(iv, ciphertext...)
|
||||
|
||||
// This should decrypt to an empty slice, which is valid
|
||||
decrypted, err := DecryptAES256CBC(key, fullCiphertext)
|
||||
if err != nil {
|
||||
t.Errorf("DecryptAES256CBC failed for empty plaintext case: %v", err)
|
||||
}
|
||||
if len(decrypted) != 0 {
|
||||
t.Errorf("Expected empty plaintext, got %q", decrypted)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConstants(t *testing.T) {
|
||||
if AES256KeySize != 32 {
|
||||
t.Errorf("AES256KeySize should be 32, got %d", AES256KeySize)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAES256CBC(b *testing.B) {
|
||||
key, err := GenerateAES256Key()
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to generate key: %v", err)
|
||||
}
|
||||
|
||||
data := make([]byte, 1024) // 1KB of data
|
||||
rand.Read(data)
|
||||
|
||||
b.Run("Encrypt", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := EncryptAES256CBC(key, data)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ciphertext, _ := EncryptAES256CBC(key, data)
|
||||
b.Run("Decrypt", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := DecryptAES256CBC(key, ciphertext)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
63
pkg/cryptography/curve25519_test.go
Normal file
63
pkg/cryptography/curve25519_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package cryptography
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
func TestGenerateKeyPair(t *testing.T) {
|
||||
priv1, pub1, err := GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyPair failed: %v", err)
|
||||
}
|
||||
|
||||
if len(priv1) != curve25519.ScalarSize {
|
||||
t.Errorf("Private key length is %d, want %d", len(priv1), curve25519.ScalarSize)
|
||||
}
|
||||
if len(pub1) != curve25519.PointSize {
|
||||
t.Errorf("Public key length is %d, want %d", len(pub1), curve25519.PointSize)
|
||||
}
|
||||
|
||||
// Generate another pair, should be different
|
||||
priv2, pub2, err := GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("Second GenerateKeyPair failed: %v", err)
|
||||
}
|
||||
if bytes.Equal(priv1, priv2) {
|
||||
t.Error("Generated private keys are identical")
|
||||
}
|
||||
if bytes.Equal(pub1, pub2) {
|
||||
t.Error("Generated public keys are identical")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveSharedSecret(t *testing.T) {
|
||||
privA, pubA, err := GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyPair A failed: %v", err)
|
||||
}
|
||||
privB, pubB, err := GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyPair B failed: %v", err)
|
||||
}
|
||||
|
||||
secretA, err := DeriveSharedSecret(privA, pubB)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveSharedSecret (A perspective) failed: %v", err)
|
||||
}
|
||||
|
||||
secretB, err := DeriveSharedSecret(privB, pubA)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveSharedSecret (B perspective) failed: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(secretA, secretB) {
|
||||
t.Errorf("Derived shared secrets do not match:\nSecret A: %x\nSecret B: %x", secretA, secretB)
|
||||
}
|
||||
|
||||
if len(secretA) != curve25519.PointSize { // Shared secret length
|
||||
t.Errorf("Shared secret length is %d, want %d", len(secretA), curve25519.PointSize)
|
||||
}
|
||||
}
|
||||
79
pkg/cryptography/ed25519_test.go
Normal file
79
pkg/cryptography/ed25519_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package cryptography
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateSigningKeyPair(t *testing.T) {
|
||||
pub1, priv1, err := GenerateSigningKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateSigningKeyPair failed: %v", err)
|
||||
}
|
||||
|
||||
if len(pub1) != ed25519.PublicKeySize {
|
||||
t.Errorf("Public key length is %d, want %d", len(pub1), ed25519.PublicKeySize)
|
||||
}
|
||||
if len(priv1) != ed25519.PrivateKeySize {
|
||||
t.Errorf("Private key length is %d, want %d", len(priv1), ed25519.PrivateKeySize)
|
||||
}
|
||||
|
||||
// Generate another pair, should be different
|
||||
pub2, priv2, err := GenerateSigningKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("Second GenerateSigningKeyPair failed: %v", err)
|
||||
}
|
||||
if pub1.Equal(pub2) {
|
||||
t.Error("Generated public keys are identical")
|
||||
}
|
||||
if priv1.Equal(priv2) {
|
||||
t.Error("Generated private keys are identical")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignAndVerify(t *testing.T) {
|
||||
pub, priv, err := GenerateSigningKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateSigningKeyPair failed: %v", err)
|
||||
}
|
||||
|
||||
message := []byte("This message needs to be signed.")
|
||||
|
||||
signature := Sign(priv, message)
|
||||
if len(signature) != ed25519.SignatureSize {
|
||||
t.Errorf("Signature length is %d, want %d", len(signature), ed25519.SignatureSize)
|
||||
}
|
||||
|
||||
// Verify correct signature
|
||||
if !Verify(pub, message, signature) {
|
||||
t.Errorf("Verify failed for a valid signature")
|
||||
}
|
||||
|
||||
// Verify with tampered message
|
||||
tamperedMessage := append(message, '!')
|
||||
if Verify(pub, tamperedMessage, signature) {
|
||||
t.Errorf("Verify succeeded for a tampered message")
|
||||
}
|
||||
|
||||
// Verify with tampered signature
|
||||
tamperedSignature := append(signature[:len(signature)-1], ^signature[len(signature)-1])
|
||||
if Verify(pub, message, tamperedSignature) {
|
||||
t.Errorf("Verify succeeded for a tampered signature")
|
||||
}
|
||||
|
||||
// Verify with wrong public key
|
||||
wrongPub, _, _ := GenerateSigningKeyPair()
|
||||
if Verify(wrongPub, message, signature) {
|
||||
t.Errorf("Verify succeeded with the wrong public key")
|
||||
}
|
||||
|
||||
// Verify empty message
|
||||
emptyMessage := []byte("")
|
||||
emptySig := Sign(priv, emptyMessage)
|
||||
if !Verify(pub, emptyMessage, emptySig) {
|
||||
t.Errorf("Verify failed for an empty message")
|
||||
}
|
||||
if Verify(pub, message, emptySig) {
|
||||
t.Errorf("Verify succeeded comparing non-empty message with empty signature")
|
||||
}
|
||||
}
|
||||
108
pkg/cryptography/hkdf_test.go
Normal file
108
pkg/cryptography/hkdf_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package cryptography
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeriveKey(t *testing.T) {
|
||||
secret := []byte("test-secret")
|
||||
salt := []byte("test-salt")
|
||||
info := []byte("test-info")
|
||||
length := 32 // Desired key length
|
||||
|
||||
key1, err := DeriveKey(secret, salt, info, length)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKey failed: %v", err)
|
||||
}
|
||||
|
||||
if len(key1) != length {
|
||||
t.Errorf("DeriveKey returned key of length %d; want %d", len(key1), length)
|
||||
}
|
||||
|
||||
// Derive another key with the same parameters, should be identical
|
||||
key2, err := DeriveKey(secret, salt, info, length)
|
||||
if err != nil {
|
||||
t.Fatalf("Second DeriveKey failed: %v", err)
|
||||
}
|
||||
if !bytes.Equal(key1, key2) {
|
||||
t.Errorf("DeriveKey is not deterministic. Got %x and %x for the same inputs", key1, key2)
|
||||
}
|
||||
|
||||
// Derive a key with different info, should be different
|
||||
differentInfo := []byte("different-info")
|
||||
key3, err := DeriveKey(secret, salt, differentInfo, length)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKey with different info failed: %v", err)
|
||||
}
|
||||
if bytes.Equal(key1, key3) {
|
||||
t.Errorf("DeriveKey produced the same key for different info strings")
|
||||
}
|
||||
|
||||
// Derive a key with different salt, should be different
|
||||
differentSalt := []byte("different-salt")
|
||||
key4, err := DeriveKey(secret, differentSalt, info, length)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKey with different salt failed: %v", err)
|
||||
}
|
||||
if bytes.Equal(key1, key4) {
|
||||
t.Errorf("DeriveKey produced the same key for different salts")
|
||||
}
|
||||
|
||||
// Derive a key with different secret, should be different
|
||||
differentSecret := []byte("different-secret")
|
||||
key5, err := DeriveKey(differentSecret, salt, info, length)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKey with different secret failed: %v", err)
|
||||
}
|
||||
if bytes.Equal(key1, key5) {
|
||||
t.Errorf("DeriveKey produced the same key for different secrets")
|
||||
}
|
||||
|
||||
// Derive a key with different length
|
||||
differentLength := 64
|
||||
key6, err := DeriveKey(secret, salt, info, differentLength)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKey with different length failed: %v", err)
|
||||
}
|
||||
if len(key6) != differentLength {
|
||||
t.Errorf("DeriveKey returned key of length %d; want %d", len(key6), differentLength)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveKeyEdgeCases(t *testing.T) {
|
||||
secret := []byte("test-secret")
|
||||
salt := []byte("test-salt")
|
||||
info := []byte("test-info")
|
||||
|
||||
t.Run("EmptySecret", func(t *testing.T) {
|
||||
_, err := DeriveKey([]byte{}, salt, info, 32)
|
||||
if err != nil {
|
||||
t.Errorf("DeriveKey failed with empty secret: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmptySalt", func(t *testing.T) {
|
||||
_, err := DeriveKey(secret, []byte{}, info, 32)
|
||||
if err != nil {
|
||||
t.Errorf("DeriveKey failed with empty salt: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmptyInfo", func(t *testing.T) {
|
||||
_, err := DeriveKey(secret, salt, []byte{}, 32)
|
||||
if err != nil {
|
||||
t.Errorf("DeriveKey failed with empty info: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ZeroLength", func(t *testing.T) {
|
||||
key, err := DeriveKey(secret, salt, info, 0)
|
||||
if err != nil {
|
||||
t.Errorf("DeriveKey failed with zero length: %v", err)
|
||||
}
|
||||
if len(key) != 0 {
|
||||
t.Errorf("DeriveKey with zero length returned non-empty key: %x", key)
|
||||
}
|
||||
})
|
||||
}
|
||||
80
pkg/cryptography/hmac_test.go
Normal file
80
pkg/cryptography/hmac_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package cryptography
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateHMACKey(t *testing.T) {
|
||||
testSizes := []int{16, 32, 64}
|
||||
for _, size := range testSizes {
|
||||
t.Run("Size"+string(rune(size)), func(t *testing.T) { // Simple name conversion
|
||||
key, err := GenerateHMACKey(size)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateHMACKey(%d) failed: %v", size, err)
|
||||
}
|
||||
if len(key) != size {
|
||||
t.Errorf("GenerateHMACKey(%d) returned key of length %d; want %d", size, len(key), size)
|
||||
}
|
||||
|
||||
// Check if key is not all zeros (basic check for randomness)
|
||||
isZero := true
|
||||
for _, b := range key {
|
||||
if b != 0 {
|
||||
isZero = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if isZero {
|
||||
t.Errorf("GenerateHMACKey(%d) returned an all-zero key", size)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeAndValidateHMAC(t *testing.T) {
|
||||
key, err := GenerateHMACKey(32) // Use SHA256 key size
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate HMAC key: %v", err)
|
||||
}
|
||||
|
||||
message := []byte("This is a test message.")
|
||||
|
||||
// Compute HMAC
|
||||
computedHMAC := ComputeHMAC(key, message)
|
||||
if len(computedHMAC) != 32 { // SHA256 output size
|
||||
t.Errorf("ComputeHMAC returned HMAC of length %d; want 32", len(computedHMAC))
|
||||
}
|
||||
|
||||
// Validate correct HMAC
|
||||
if !ValidateHMAC(key, message, computedHMAC) {
|
||||
t.Errorf("ValidateHMAC failed for correctly computed HMAC")
|
||||
}
|
||||
|
||||
// Validate incorrect HMAC (tampered message)
|
||||
tamperedMessage := append(message, byte('!'))
|
||||
if ValidateHMAC(key, tamperedMessage, computedHMAC) {
|
||||
t.Errorf("ValidateHMAC succeeded for tampered message")
|
||||
}
|
||||
|
||||
// Validate incorrect HMAC (tampered key)
|
||||
wrongKey, _ := GenerateHMACKey(32)
|
||||
if ValidateHMAC(wrongKey, message, computedHMAC) {
|
||||
t.Errorf("ValidateHMAC succeeded for incorrect key")
|
||||
}
|
||||
|
||||
// Validate incorrect HMAC (tampered HMAC)
|
||||
tamperedHMAC := append(computedHMAC[:len(computedHMAC)-1], ^computedHMAC[len(computedHMAC)-1])
|
||||
if ValidateHMAC(key, message, tamperedHMAC) {
|
||||
t.Errorf("ValidateHMAC succeeded for tampered HMAC")
|
||||
}
|
||||
|
||||
// Validate empty message
|
||||
emptyMessage := []byte("")
|
||||
emptyHMAC := ComputeHMAC(key, emptyMessage)
|
||||
if !ValidateHMAC(key, emptyMessage, emptyHMAC) {
|
||||
t.Errorf("ValidateHMAC failed for empty message")
|
||||
}
|
||||
if ValidateHMAC(key, message, emptyHMAC) {
|
||||
t.Errorf("ValidateHMAC succeeded comparing non-empty message with empty HMAC")
|
||||
}
|
||||
}
|
||||
@@ -2,12 +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"
|
||||
@@ -60,6 +60,7 @@ type Destination struct {
|
||||
appName string
|
||||
aspects []string
|
||||
hashValue []byte
|
||||
transport *transport.Transport
|
||||
|
||||
acceptsLinks bool
|
||||
proofStrategy byte
|
||||
@@ -84,7 +85,7 @@ func debugLog(level int, format string, v ...interface{}) {
|
||||
log.Printf("[DEBUG-%d] %s", level, fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func New(id *identity.Identity, direction byte, destType byte, appName string, aspects ...string) (*Destination, error) {
|
||||
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 {
|
||||
@@ -98,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,
|
||||
@@ -142,61 +144,42 @@ func (d *Destination) Announce(appData []byte) error {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
log.Printf("[DEBUG-4] Creating announce packet for destination %s", d.ExpandName())
|
||||
log.Printf("[DEBUG-4] Announcing destination %s", d.ExpandName())
|
||||
|
||||
// If no specific appData provided, use default
|
||||
if appData == nil {
|
||||
log.Printf("[DEBUG-4] Using default app data for announce")
|
||||
appData = d.defaultAppData
|
||||
}
|
||||
|
||||
// Create announce packet
|
||||
packet := make([]byte, 0, 256) // Pre-allocate reasonable size
|
||||
// Create a new Announce instance
|
||||
announce, err := announce.New(d.identity, appData, false, d.transport.GetConfig())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create announce: %w", err)
|
||||
}
|
||||
|
||||
// Add packet type and header
|
||||
packet = append(packet, 0x01) // PACKET_TYPE_ANNOUNCE
|
||||
packet = append(packet, 0x00) // Initial hop count
|
||||
// Get the packet from the announce instance
|
||||
packet := announce.GetPacket()
|
||||
if packet == nil {
|
||||
return errors.New("failed to create announce packet")
|
||||
}
|
||||
|
||||
// Add destination hash (16 bytes)
|
||||
packet = append(packet, d.hashValue...)
|
||||
log.Printf("[DEBUG-4] Added destination hash %x to announce", d.hashValue[:8])
|
||||
// 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")
|
||||
}
|
||||
|
||||
// Add identity public key (32 bytes)
|
||||
pubKey := d.identity.GetPublicKey()
|
||||
packet = append(packet, pubKey...)
|
||||
log.Printf("[DEBUG-4] Added public key %x to announce", pubKey[:8])
|
||||
|
||||
// Add app data with length prefix
|
||||
appDataLen := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(appDataLen, uint16(len(appData)))
|
||||
packet = append(packet, appDataLen...)
|
||||
packet = append(packet, appData...)
|
||||
log.Printf("[DEBUG-4] Added %d bytes of app data to announce", len(appData))
|
||||
|
||||
// Add ratchet data if enabled
|
||||
if d.ratchetsEnabled {
|
||||
log.Printf("[DEBUG-4] Adding ratchet data to announce")
|
||||
ratchetKey := d.identity.GetCurrentRatchetKey()
|
||||
if ratchetKey == nil {
|
||||
log.Printf("[DEBUG-3] Failed to get current ratchet key")
|
||||
return errors.New("failed to get current ratchet key")
|
||||
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
|
||||
}
|
||||
}
|
||||
packet = append(packet, ratchetKey...)
|
||||
log.Printf("[DEBUG-4] Added ratchet key %x to announce", ratchetKey[:8])
|
||||
}
|
||||
|
||||
// Sign the announce packet (64 bytes)
|
||||
signData := append(d.hashValue, appData...)
|
||||
if d.ratchetsEnabled {
|
||||
signData = append(signData, d.identity.GetCurrentRatchetKey()...)
|
||||
}
|
||||
signature := d.identity.Sign(signData)
|
||||
packet = append(packet, signature...)
|
||||
log.Printf("[DEBUG-4] Added signature to announce packet (total size: %d bytes)", len(packet))
|
||||
|
||||
// Send announce packet through transport
|
||||
log.Printf("[DEBUG-4] Sending announce packet through transport layer")
|
||||
return transport.SendAnnounce(packet)
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (d *Destination) AcceptsLinks(accepts bool) {
|
||||
|
||||
@@ -133,7 +133,7 @@ func (i *Identity) Encrypt(plaintext []byte, ratchet []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
// Encrypt data
|
||||
ciphertext, err := cryptography.EncryptAESCBC(key[:16], plaintext)
|
||||
ciphertext, err := cryptography.EncryptAES256CBC(key[:32], plaintext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -164,7 +164,11 @@ func TruncatedHash(data []byte) []byte {
|
||||
|
||||
func GetRandomHash() []byte {
|
||||
randomData := make([]byte, TRUNCATED_HASHLENGTH/8)
|
||||
rand.Read(randomData)
|
||||
_, err := rand.Read(randomData) // #nosec G104
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-1] Failed to read random data for hash: %v", err)
|
||||
return nil // Or handle the error appropriately
|
||||
}
|
||||
return TruncatedHash(randomData)
|
||||
}
|
||||
|
||||
@@ -256,26 +260,35 @@ func (i *Identity) GetCurrentRatchetKey() []byte {
|
||||
i.mutex.RLock()
|
||||
defer i.mutex.RUnlock()
|
||||
|
||||
// Generate new ratchet key if none exists
|
||||
if len(i.ratchets) == 0 {
|
||||
key := make([]byte, RATCHETSIZE/8)
|
||||
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||
// If no ratchets exist, generate one.
|
||||
// This should ideally be handled by an explicit setup process.
|
||||
log.Println("[DEBUG-5] No ratchets found, generating a new one on-the-fly.")
|
||||
// Temporarily unlock to call RotateRatchet, which locks internally.
|
||||
i.mutex.RUnlock()
|
||||
newRatchet, err := i.RotateRatchet()
|
||||
i.mutex.RLock()
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-1] Failed to generate initial ratchet key: %v", err)
|
||||
return nil
|
||||
}
|
||||
i.ratchets[string(key)] = key
|
||||
i.ratchetExpiry[string(key)] = time.Now().Unix() + RATCHET_EXPIRY
|
||||
return key
|
||||
return newRatchet
|
||||
}
|
||||
|
||||
// Return most recent ratchet key
|
||||
// Return the most recently generated ratchet key
|
||||
var latestKey []byte
|
||||
var latestTime int64
|
||||
for key, expiry := range i.ratchetExpiry {
|
||||
var latestTime int64 = 0
|
||||
for id, expiry := range i.ratchetExpiry {
|
||||
if expiry > latestTime {
|
||||
latestTime = expiry
|
||||
latestKey = i.ratchets[key]
|
||||
latestKey = i.ratchets[id]
|
||||
}
|
||||
}
|
||||
|
||||
if latestKey == nil {
|
||||
log.Printf("[DEBUG-2] Could not determine the latest ratchet key from %d ratchets.", len(i.ratchets))
|
||||
}
|
||||
|
||||
return latestKey
|
||||
}
|
||||
|
||||
@@ -395,7 +408,7 @@ func (i *Identity) tryRatchetDecryption(peerPubBytes, ciphertext, ratchet []byte
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
plaintext, err := cryptography.DecryptAESCBC(key, ciphertext)
|
||||
plaintext, err := cryptography.DecryptAES256CBC(key, ciphertext)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -404,7 +417,7 @@ func (i *Identity) tryRatchetDecryption(peerPubBytes, ciphertext, ratchet []byte
|
||||
}
|
||||
|
||||
func (i *Identity) EncryptWithHMAC(plaintext []byte, key []byte) ([]byte, error) {
|
||||
ciphertext, err := cryptography.EncryptAESCBC(key, plaintext)
|
||||
ciphertext, err := cryptography.EncryptAES256CBC(key, plaintext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -426,12 +439,19 @@ func (i *Identity) DecryptWithHMAC(data []byte, key []byte) ([]byte, error) {
|
||||
return nil, errors.New("invalid HMAC")
|
||||
}
|
||||
|
||||
return cryptography.DecryptAESCBC(key, ciphertext)
|
||||
return cryptography.DecryptAES256CBC(key, ciphertext)
|
||||
}
|
||||
|
||||
func (i *Identity) ToFile(path string) error {
|
||||
log.Printf("[DEBUG-7] Saving identity %s to file: %s", i.GetHexHash(), path)
|
||||
|
||||
// Persist ratchets to a separate file
|
||||
ratchetPath := path + ".ratchets"
|
||||
if err := i.saveRatchets(ratchetPath); err != nil {
|
||||
log.Printf("[DEBUG-1] Failed to save ratchets: %v", err)
|
||||
// Continue saving the main identity file even if ratchets fail
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"private_key": i.privateKey,
|
||||
"public_key": i.publicKey,
|
||||
@@ -440,7 +460,7 @@ func (i *Identity) ToFile(path string) error {
|
||||
"app_data": i.appData,
|
||||
}
|
||||
|
||||
file, err := os.Create(path)
|
||||
file, err := os.Create(path) // #nosec G304
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-1] Failed to create identity file: %v", err)
|
||||
return err
|
||||
@@ -456,10 +476,33 @@ func (i *Identity) ToFile(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Identity) saveRatchets(path string) error {
|
||||
i.mutex.RLock()
|
||||
defer i.mutex.RUnlock()
|
||||
|
||||
if len(i.ratchets) == 0 {
|
||||
return nil // Nothing to save
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-6] Saving %d ratchets to %s", len(i.ratchets), path)
|
||||
data := map[string]interface{}{
|
||||
"ratchets": i.ratchets,
|
||||
"ratchet_expiry": i.ratchetExpiry,
|
||||
}
|
||||
|
||||
file, err := os.Create(path) // #nosec G304
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create ratchet file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
return json.NewEncoder(file).Encode(data)
|
||||
}
|
||||
|
||||
func RecallIdentity(path string) (*Identity, error) {
|
||||
log.Printf("[DEBUG-7] Attempting to recall identity from: %s", path)
|
||||
|
||||
file, err := os.Open(path)
|
||||
file, err := os.Open(path) // #nosec G304
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-1] Failed to open identity file: %v", err)
|
||||
return nil, err
|
||||
@@ -483,10 +526,56 @@ func RecallIdentity(path string) (*Identity, error) {
|
||||
mutex: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
// Load ratchets if they exist
|
||||
ratchetPath := path + ".ratchets"
|
||||
if err := id.loadRatchets(ratchetPath); err != nil {
|
||||
log.Printf("[DEBUG-2] Could not load ratchets for identity %s: %v", id.GetHexHash(), err)
|
||||
// This is not a fatal error, the identity can still function
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-7] Successfully recalled identity with hash: %s", id.GetHexHash())
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (i *Identity) loadRatchets(path string) error {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
|
||||
file, err := os.Open(path) // #nosec G304
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Printf("[DEBUG-6] No ratchet file found at %s, skipping.", path)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to open ratchet file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var data map[string]interface{}
|
||||
if err := json.NewDecoder(file).Decode(&data); err != nil {
|
||||
return fmt.Errorf("failed to decode ratchet data: %w", err)
|
||||
}
|
||||
|
||||
if ratchets, ok := data["ratchets"].(map[string]interface{}); ok {
|
||||
for id, key := range ratchets {
|
||||
if keyStr, ok := key.(string); ok {
|
||||
i.ratchets[id] = []byte(keyStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if expiry, ok := data["ratchet_expiry"].(map[string]interface{}); ok {
|
||||
for id, timeVal := range expiry {
|
||||
if timeFloat, ok := timeVal.(float64); ok {
|
||||
i.ratchetExpiry[id] = int64(timeFloat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-6] Loaded %d ratchets from %s", len(i.ratchets), path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func HashFromString(hash string) ([]byte, error) {
|
||||
if len(hash) != 32 {
|
||||
return nil, fmt.Errorf("invalid hash length: expected 32, got %d", len(hash))
|
||||
|
||||
@@ -44,21 +44,19 @@ type Peer struct {
|
||||
}
|
||||
|
||||
func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterface, error) {
|
||||
base := &BaseInterface{
|
||||
Name: name,
|
||||
Mode: common.IF_MODE_FULL,
|
||||
Type: common.IF_TYPE_AUTO,
|
||||
Online: false,
|
||||
Enabled: config.Enabled,
|
||||
Detached: false,
|
||||
IN: false,
|
||||
OUT: false,
|
||||
MTU: common.DEFAULT_MTU,
|
||||
Bitrate: BITRATE_MINIMUM,
|
||||
}
|
||||
|
||||
ai := &AutoInterface{
|
||||
BaseInterface: *base,
|
||||
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,
|
||||
@@ -165,13 +163,13 @@ func (ai *AutoInterface) startDataListener(iface *net.Interface) error {
|
||||
func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) {
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, remoteAddr, err := conn.ReadFromUDP(buf)
|
||||
_, remoteAddr, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
log.Printf("Discovery read error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
ai.handlePeerAnnounce(remoteAddr, buf[:n], ifaceName)
|
||||
ai.handlePeerAnnounce(remoteAddr, ifaceName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +190,7 @@ func (ai *AutoInterface) handleData(conn *net.UDPConn) {
|
||||
}
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) handlePeerAnnounce(addr *net.UDPAddr, data []byte, ifaceName string) {
|
||||
func (ai *AutoInterface) handlePeerAnnounce(addr *net.UDPAddr, ifaceName string) {
|
||||
ai.mutex.Lock()
|
||||
defer ai.mutex.Unlock()
|
||||
|
||||
@@ -266,11 +264,11 @@ func (ai *AutoInterface) Stop() error {
|
||||
defer ai.mutex.Unlock()
|
||||
|
||||
for _, server := range ai.interfaceServers {
|
||||
server.Close()
|
||||
server.Close() // #nosec G104
|
||||
}
|
||||
|
||||
if ai.outboundConn != nil {
|
||||
ai.outboundConn.Close()
|
||||
ai.outboundConn.Close() // #nosec G104
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
290
pkg/interfaces/auto_test.go
Normal file
290
pkg/interfaces/auto_test.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
)
|
||||
|
||||
func TestNewAutoInterface(t *testing.T) {
|
||||
t.Run("DefaultConfig", func(t *testing.T) {
|
||||
config := &common.InterfaceConfig{Enabled: true}
|
||||
ai, err := NewAutoInterface("autoDefault", config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAutoInterface failed with default config: %v", err)
|
||||
}
|
||||
if ai == nil {
|
||||
t.Fatal("NewAutoInterface returned nil with default config")
|
||||
}
|
||||
|
||||
if ai.GetName() != "autoDefault" {
|
||||
t.Errorf("GetName() = %s; want autoDefault", ai.GetName())
|
||||
}
|
||||
if ai.GetType() != common.IF_TYPE_AUTO {
|
||||
t.Errorf("GetType() = %v; want %v", ai.GetType(), common.IF_TYPE_AUTO)
|
||||
}
|
||||
if ai.discoveryPort != DEFAULT_DISCOVERY_PORT {
|
||||
t.Errorf("discoveryPort = %d; want %d", ai.discoveryPort, DEFAULT_DISCOVERY_PORT)
|
||||
}
|
||||
if ai.dataPort != DEFAULT_DATA_PORT {
|
||||
t.Errorf("dataPort = %d; want %d", ai.dataPort, DEFAULT_DATA_PORT)
|
||||
}
|
||||
if string(ai.groupID) != "reticulum" {
|
||||
t.Errorf("groupID = %s; want reticulum", string(ai.groupID))
|
||||
}
|
||||
if ai.discoveryScope != SCOPE_LINK {
|
||||
t.Errorf("discoveryScope = %s; want %s", ai.discoveryScope, SCOPE_LINK)
|
||||
}
|
||||
if len(ai.peers) != 0 {
|
||||
t.Errorf("peers map not empty initially")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CustomConfig", func(t *testing.T) {
|
||||
config := &common.InterfaceConfig{
|
||||
Enabled: true,
|
||||
Port: 12345, // Custom discovery port
|
||||
GroupID: "customGroup",
|
||||
}
|
||||
ai, err := NewAutoInterface("autoCustom", config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAutoInterface failed with custom config: %v", err)
|
||||
}
|
||||
if ai == nil {
|
||||
t.Fatal("NewAutoInterface returned nil with custom config")
|
||||
}
|
||||
|
||||
if ai.discoveryPort != 12345 {
|
||||
t.Errorf("discoveryPort = %d; want 12345", ai.discoveryPort)
|
||||
}
|
||||
if string(ai.groupID) != "customGroup" {
|
||||
t.Errorf("groupID = %s; want customGroup", string(ai.groupID))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// mockAutoInterface embeds AutoInterface but overrides methods that start goroutines
|
||||
type mockAutoInterface struct {
|
||||
*AutoInterface
|
||||
}
|
||||
|
||||
func newMockAutoInterface(name string, config *common.InterfaceConfig) (*mockAutoInterface, error) {
|
||||
ai, err := NewAutoInterface(name, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize maps that would normally be initialized in Start()
|
||||
ai.peers = make(map[string]*Peer)
|
||||
ai.linkLocalAddrs = make([]string, 0)
|
||||
ai.adoptedInterfaces = make(map[string]string)
|
||||
ai.interfaceServers = make(map[string]*net.UDPConn)
|
||||
ai.multicastEchoes = make(map[string]time.Time)
|
||||
|
||||
return &mockAutoInterface{AutoInterface: ai}, nil
|
||||
}
|
||||
|
||||
func (m *mockAutoInterface) Start() error {
|
||||
// Don't start any goroutines
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAutoInterface) Stop() error {
|
||||
// Don't try to close connections that were never opened
|
||||
return nil
|
||||
}
|
||||
|
||||
// mockHandlePeerAnnounce is a test-only method that doesn't handle its own locking
|
||||
func (m *mockAutoInterface) mockHandlePeerAnnounce(addr *net.UDPAddr, ifaceName string) {
|
||||
peerAddr := addr.IP.String() + "%" + addr.Zone
|
||||
|
||||
for _, localAddr := range m.linkLocalAddrs {
|
||||
if peerAddr == localAddr {
|
||||
m.multicastEchoes[ifaceName] = time.Now()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, exists := m.peers[peerAddr]; !exists {
|
||||
m.peers[peerAddr] = &Peer{
|
||||
ifaceName: ifaceName,
|
||||
lastHeard: time.Now(),
|
||||
}
|
||||
} else {
|
||||
m.peers[peerAddr].lastHeard = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoInterfacePeerManagement(t *testing.T) {
|
||||
// Use a shorter timeout for testing
|
||||
testTimeout := 100 * time.Millisecond
|
||||
|
||||
config := &common.InterfaceConfig{Enabled: true}
|
||||
ai, err := newMockAutoInterface("autoPeerTest", config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create mock interface: %v", err)
|
||||
}
|
||||
|
||||
// Create a done channel to signal goroutine cleanup
|
||||
done := make(chan struct{})
|
||||
|
||||
// Start peer management with done channel
|
||||
go func() {
|
||||
ticker := time.NewTicker(testTimeout)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
ai.mutex.Lock()
|
||||
now := time.Now()
|
||||
for addr, peer := range ai.peers {
|
||||
if now.Sub(peer.lastHeard) > testTimeout {
|
||||
delete(ai.peers, addr)
|
||||
}
|
||||
}
|
||||
ai.mutex.Unlock()
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Ensure cleanup
|
||||
defer func() {
|
||||
close(done)
|
||||
ai.Stop()
|
||||
}()
|
||||
|
||||
// Simulate receiving peer announces
|
||||
peer1AddrStr := "fe80::1%eth0"
|
||||
peer2AddrStr := "fe80::2%eth0"
|
||||
localAddrStr := "fe80::aaaa%eth0" // Simulate a local address
|
||||
|
||||
peer1Addr := &net.UDPAddr{IP: net.ParseIP("fe80::1"), Zone: "eth0"}
|
||||
peer2Addr := &net.UDPAddr{IP: net.ParseIP("fe80::2"), Zone: "eth0"}
|
||||
localAddr := &net.UDPAddr{IP: net.ParseIP("fe80::aaaa"), Zone: "eth0"}
|
||||
|
||||
// Add a simulated local address to avoid adding it as a peer
|
||||
ai.mutex.Lock()
|
||||
ai.linkLocalAddrs = append(ai.linkLocalAddrs, localAddrStr)
|
||||
ai.mutex.Unlock()
|
||||
|
||||
t.Run("AddPeer1", func(t *testing.T) {
|
||||
ai.mutex.Lock()
|
||||
ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
|
||||
ai.mutex.Unlock()
|
||||
|
||||
// Give a small amount of time for the peer to be processed
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
ai.mutex.RLock()
|
||||
count := len(ai.peers)
|
||||
peer, exists := ai.peers[peer1AddrStr]
|
||||
var ifaceName string
|
||||
if exists {
|
||||
ifaceName = peer.ifaceName
|
||||
}
|
||||
ai.mutex.RUnlock()
|
||||
|
||||
if count != 1 {
|
||||
t.Fatalf("Expected 1 peer, got %d", count)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("Peer %s not found in map", peer1AddrStr)
|
||||
}
|
||||
if ifaceName != "eth0" {
|
||||
t.Errorf("Peer %s interface name = %s; want eth0", peer1AddrStr, ifaceName)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AddPeer2", func(t *testing.T) {
|
||||
ai.mutex.Lock()
|
||||
ai.mockHandlePeerAnnounce(peer2Addr, "eth0")
|
||||
ai.mutex.Unlock()
|
||||
|
||||
// Give a small amount of time for the peer to be processed
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
ai.mutex.RLock()
|
||||
count := len(ai.peers)
|
||||
_, exists := ai.peers[peer2AddrStr]
|
||||
ai.mutex.RUnlock()
|
||||
|
||||
if count != 2 {
|
||||
t.Fatalf("Expected 2 peers, got %d", count)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("Peer %s not found in map", peer2AddrStr)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("IgnoreLocalAnnounce", func(t *testing.T) {
|
||||
ai.mutex.Lock()
|
||||
ai.mockHandlePeerAnnounce(localAddr, "eth0")
|
||||
ai.mutex.Unlock()
|
||||
|
||||
// Give a small amount of time for the peer to be processed
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
ai.mutex.RLock()
|
||||
count := len(ai.peers)
|
||||
ai.mutex.RUnlock()
|
||||
|
||||
if count != 2 {
|
||||
t.Fatalf("Expected 2 peers after local announce, got %d", count)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UpdatePeerTimestamp", func(t *testing.T) {
|
||||
ai.mutex.RLock()
|
||||
peer, exists := ai.peers[peer1AddrStr]
|
||||
var initialTime time.Time
|
||||
if exists {
|
||||
initialTime = peer.lastHeard
|
||||
}
|
||||
ai.mutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
t.Fatalf("Peer %s not found before timestamp update", peer1AddrStr)
|
||||
}
|
||||
|
||||
ai.mutex.Lock()
|
||||
ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
|
||||
ai.mutex.Unlock()
|
||||
|
||||
// Give a small amount of time for the peer to be processed
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
ai.mutex.RLock()
|
||||
peer, exists = ai.peers[peer1AddrStr]
|
||||
var updatedTime time.Time
|
||||
if exists {
|
||||
updatedTime = peer.lastHeard
|
||||
}
|
||||
ai.mutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
t.Fatalf("Peer %s not found after timestamp update", peer1AddrStr)
|
||||
}
|
||||
|
||||
if !updatedTime.After(initialTime) {
|
||||
t.Errorf("Peer timestamp was not updated after receiving another announce")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PeerTimeout", func(t *testing.T) {
|
||||
// Wait for peer timeout
|
||||
time.Sleep(testTimeout * 2)
|
||||
|
||||
ai.mutex.RLock()
|
||||
count := len(ai.peers)
|
||||
ai.mutex.RUnlock()
|
||||
|
||||
if count != 0 {
|
||||
t.Errorf("Expected all peers to timeout, got %d peers", count)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -161,7 +161,7 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
|
||||
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...)
|
||||
|
||||
|
||||
230
pkg/interfaces/interface_test.go
Normal file
230
pkg/interfaces/interface_test.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
)
|
||||
|
||||
func TestBaseInterfaceStateChanges(t *testing.T) {
|
||||
bi := NewBaseInterface("test", common.IF_TYPE_TCP, false) // Start disabled
|
||||
|
||||
if bi.IsEnabled() {
|
||||
t.Error("Newly created disabled interface reports IsEnabled() == true")
|
||||
}
|
||||
if bi.IsOnline() {
|
||||
t.Error("Newly created disabled interface reports IsOnline() == true")
|
||||
}
|
||||
if bi.IsDetached() {
|
||||
t.Error("Newly created interface reports IsDetached() == true")
|
||||
}
|
||||
|
||||
bi.Enable()
|
||||
if !bi.IsEnabled() {
|
||||
t.Error("After Enable(), IsEnabled() == false")
|
||||
}
|
||||
if !bi.IsOnline() {
|
||||
t.Error("After Enable(), IsOnline() == false")
|
||||
}
|
||||
if bi.IsDetached() {
|
||||
t.Error("After Enable(), IsDetached() == true")
|
||||
}
|
||||
|
||||
bi.Detach()
|
||||
if bi.IsEnabled() {
|
||||
t.Error("After Detach(), IsEnabled() == true")
|
||||
}
|
||||
if bi.IsOnline() {
|
||||
t.Error("After Detach(), IsOnline() == true")
|
||||
}
|
||||
if !bi.IsDetached() {
|
||||
t.Error("After Detach(), IsDetached() == false")
|
||||
}
|
||||
|
||||
// Reset for Disable test
|
||||
bi = NewBaseInterface("test2", common.IF_TYPE_UDP, true) // Start enabled
|
||||
if !bi.Enabled { // Check the Enabled field directly first
|
||||
t.Error("Newly created enabled interface reports Enabled == false")
|
||||
}
|
||||
if bi.IsEnabled() { // IsEnabled should still be false because Online is false
|
||||
t.Error("Newly created enabled interface reports IsEnabled() == true before Enable() is called")
|
||||
}
|
||||
|
||||
bi.Enable() // Explicitly enable to set Online = true
|
||||
if !bi.IsEnabled() { // Now IsEnabled should be true
|
||||
t.Error("After Enable() on initially enabled interface, IsEnabled() == false")
|
||||
}
|
||||
|
||||
bi.Disable()
|
||||
if bi.Enabled { // Check Enabled field after Disable()
|
||||
t.Error("After Disable(), Enabled == true")
|
||||
}
|
||||
if bi.IsOnline() {
|
||||
t.Error("After Disable(), IsOnline() == true")
|
||||
}
|
||||
if bi.IsDetached() { // Disable doesn't detach
|
||||
t.Error("After Disable(), IsDetached() == true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterfaceGetters(t *testing.T) {
|
||||
bi := NewBaseInterface("getterTest", common.IF_TYPE_AUTO, true)
|
||||
|
||||
if bi.GetName() != "getterTest" {
|
||||
t.Errorf("GetName() = %s; want getterTest", bi.GetName())
|
||||
}
|
||||
if bi.GetType() != common.IF_TYPE_AUTO {
|
||||
t.Errorf("GetType() = %v; want %v", bi.GetType(), common.IF_TYPE_AUTO)
|
||||
}
|
||||
if bi.GetMode() != common.IF_MODE_FULL {
|
||||
t.Errorf("GetMode() = %v; want %v", bi.GetMode(), common.IF_MODE_FULL)
|
||||
}
|
||||
if bi.GetMTU() != common.DEFAULT_MTU { // Assuming default MTU
|
||||
t.Errorf("GetMTU() = %d; want %d", bi.GetMTU(), common.DEFAULT_MTU)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterfaceCallbacks(t *testing.T) {
|
||||
bi := NewBaseInterface("callbackTest", common.IF_TYPE_TCP, true)
|
||||
var wg sync.WaitGroup
|
||||
var callbackCalled bool
|
||||
|
||||
callback := func(data []byte, iface common.NetworkInterface) {
|
||||
if len(data) != 5 {
|
||||
t.Errorf("Callback received data length %d; want 5", len(data))
|
||||
}
|
||||
if iface.GetName() != "callbackTest" {
|
||||
t.Errorf("Callback received interface name %s; want callbackTest", iface.GetName())
|
||||
}
|
||||
callbackCalled = true
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
bi.SetPacketCallback(callback)
|
||||
if bi.GetPacketCallback() == nil { // Cannot directly compare functions
|
||||
t.Error("GetPacketCallback() returned nil after SetPacketCallback()")
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go bi.ProcessIncoming([]byte{1, 2, 3, 4, 5}) // Run in goroutine as callback might block
|
||||
|
||||
// Wait for callback or timeout
|
||||
waitTimeout(&wg, 1*time.Second, t)
|
||||
|
||||
if !callbackCalled {
|
||||
t.Error("Packet callback was not called after ProcessIncoming")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterfaceStats(t *testing.T) {
|
||||
bi := NewBaseInterface("statsTest", common.IF_TYPE_UDP, true)
|
||||
bi.Enable() // Need to be Online for ProcessOutgoing
|
||||
|
||||
data1 := []byte{1, 2, 3}
|
||||
data2 := []byte{4, 5, 6, 7, 8}
|
||||
|
||||
bi.ProcessIncoming(data1)
|
||||
if bi.RxBytes != uint64(len(data1)) {
|
||||
t.Errorf("RxBytes = %d; want %d after first ProcessIncoming", bi.RxBytes, len(data1))
|
||||
}
|
||||
|
||||
bi.ProcessIncoming(data2)
|
||||
if bi.RxBytes != uint64(len(data1)+len(data2)) {
|
||||
t.Errorf("RxBytes = %d; want %d after second ProcessIncoming", bi.RxBytes, len(data1)+len(data2))
|
||||
}
|
||||
|
||||
// ProcessOutgoing only updates TxBytes in BaseInterface
|
||||
err := bi.ProcessOutgoing(data1)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessOutgoing failed: %v", err)
|
||||
}
|
||||
if bi.TxBytes != uint64(len(data1)) {
|
||||
t.Errorf("TxBytes = %d; want %d after first ProcessOutgoing", bi.TxBytes, len(data1))
|
||||
}
|
||||
|
||||
err = bi.ProcessOutgoing(data2)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessOutgoing failed: %v", err)
|
||||
}
|
||||
if bi.TxBytes != uint64(len(data1)+len(data2)) {
|
||||
t.Errorf("TxBytes = %d; want %d after second ProcessOutgoing", bi.TxBytes, len(data1)+len(data2))
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to wait for a WaitGroup with a timeout
|
||||
func waitTimeout(wg *sync.WaitGroup, timeout time.Duration, t *testing.T) {
|
||||
c := make(chan struct{})
|
||||
go func() {
|
||||
defer close(c)
|
||||
wg.Wait()
|
||||
}()
|
||||
select {
|
||||
case <-c:
|
||||
// Completed normally
|
||||
case <-time.After(timeout):
|
||||
t.Fatal("Timed out waiting for WaitGroup")
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal mock interface for InterceptedInterface test
|
||||
type mockInterface struct {
|
||||
BaseInterface
|
||||
sendCalled bool
|
||||
sendData []byte
|
||||
}
|
||||
|
||||
func (m *mockInterface) Send(data []byte, addr string) error {
|
||||
m.sendCalled = true
|
||||
m.sendData = data
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add other methods to satisfy the Interface interface (can be minimal/panic)
|
||||
func (m *mockInterface) GetType() common.InterfaceType { return common.IF_TYPE_NONE }
|
||||
func (m *mockInterface) GetMode() common.InterfaceMode { return common.IF_MODE_FULL }
|
||||
func (m *mockInterface) ProcessIncoming(data []byte) {}
|
||||
func (m *mockInterface) ProcessOutgoing(data []byte) error { return nil }
|
||||
func (m *mockInterface) SendPathRequest([]byte) error { return nil }
|
||||
func (m *mockInterface) SendLinkPacket([]byte, []byte, time.Time) error { return nil }
|
||||
func (m *mockInterface) Start() error { return nil }
|
||||
func (m *mockInterface) Stop() error { return nil }
|
||||
func (m *mockInterface) GetConn() net.Conn { return nil }
|
||||
func (m *mockInterface) GetBandwidthAvailable() bool { return true }
|
||||
|
||||
func TestInterceptedInterface(t *testing.T) {
|
||||
mockBase := &mockInterface{}
|
||||
var interceptorCalled bool
|
||||
var interceptedData []byte
|
||||
|
||||
interceptor := func(data []byte, iface common.NetworkInterface) error {
|
||||
interceptorCalled = true
|
||||
interceptedData = data
|
||||
return nil
|
||||
}
|
||||
|
||||
intercepted := NewInterceptedInterface(mockBase, interceptor)
|
||||
|
||||
testData := []byte("intercept me")
|
||||
err := intercepted.Send(testData, "dummy_addr")
|
||||
if err != nil {
|
||||
t.Fatalf("Intercepted Send failed: %v", err)
|
||||
}
|
||||
|
||||
if !interceptorCalled {
|
||||
t.Error("Interceptor function was not called")
|
||||
}
|
||||
if !bytes.Equal(interceptedData, testData) {
|
||||
t.Errorf("Interceptor received data %x; want %x", interceptedData, testData)
|
||||
}
|
||||
|
||||
if !mockBase.sendCalled {
|
||||
t.Error("Original Send function was not called")
|
||||
}
|
||||
if !bytes.Equal(mockBase.sendData, testData) {
|
||||
t.Errorf("Original Send received data %x; want %x", mockBase.sendData, testData)
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func NewTCPClientInterface(name string, targetHost string, targetPort int, kissF
|
||||
}
|
||||
|
||||
if enabled {
|
||||
addr := fmt.Sprintf("%s:%d", targetHost, targetPort)
|
||||
addr := net.JoinHostPort(targetHost, fmt.Sprintf("%d", targetPort))
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -95,7 +95,7 @@ func (tc *TCPClientInterface) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort)
|
||||
addr := net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort))
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -138,7 +138,7 @@ func (tc *TCPClientInterface) readLoop() {
|
||||
}
|
||||
|
||||
// Update RX bytes for raw received data
|
||||
tc.UpdateStats(uint64(n), true)
|
||||
tc.UpdateStats(uint64(n), true) // #nosec G115
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
b := buffer[i]
|
||||
@@ -267,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,7 +346,7 @@ func (tc *TCPClientInterface) reconnect() {
|
||||
for retries < tc.maxReconnectTries {
|
||||
tc.teardown()
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort)
|
||||
addr := net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort))
|
||||
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err == nil {
|
||||
@@ -418,9 +418,11 @@ func (tc *TCPClientInterface) GetRTT() time.Duration {
|
||||
var rtt time.Duration = 0
|
||||
if runtime.GOOS == "linux" {
|
||||
if info, err := tcpConn.SyscallConn(); err == nil {
|
||||
info.Control(func(fd uintptr) {
|
||||
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
|
||||
@@ -651,7 +653,7 @@ func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
|
||||
ts.mutex.Lock()
|
||||
delete(ts.connections, addr)
|
||||
ts.mutex.Unlock()
|
||||
conn.Close()
|
||||
conn.Close() // #nosec G104
|
||||
}()
|
||||
|
||||
buffer := make([]byte, ts.MTU)
|
||||
@@ -662,7 +664,7 @@ func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
|
||||
}
|
||||
|
||||
ts.mutex.Lock()
|
||||
ts.RxBytes += uint64(n)
|
||||
ts.RxBytes += uint64(n) // #nosec G115
|
||||
ts.mutex.Unlock()
|
||||
|
||||
if ts.packetCallback != nil {
|
||||
|
||||
@@ -11,4 +11,4 @@ import (
|
||||
// Default implementation for non-Linux platforms
|
||||
func platformGetRTT(fd uintptr) time.Duration {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ func platformGetRTT(fd uintptr) time.Duration {
|
||||
fd,
|
||||
syscall.SOL_TCP,
|
||||
syscall.TCP_INFO,
|
||||
uintptr(unsafe.Pointer(&info)),
|
||||
uintptr(unsafe.Pointer(&size)),
|
||||
uintptr(unsafe.Pointer(&info)), // #nosec G103
|
||||
uintptr(unsafe.Pointer(&size)), // #nosec G103
|
||||
0,
|
||||
)
|
||||
|
||||
@@ -29,4 +29,4 @@ func platformGetRTT(fd uintptr) time.Duration {
|
||||
|
||||
// RTT is in microseconds, convert to Duration
|
||||
return time.Duration(info.Rtt) * time.Microsecond
|
||||
}
|
||||
}
|
||||
|
||||
52
pkg/interfaces/tcp_test.go
Normal file
52
pkg/interfaces/tcp_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEscapeHDLC(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input []byte
|
||||
expected []byte
|
||||
}{
|
||||
{"NoEscape", []byte{0x01, 0x02, 0x03}, []byte{0x01, 0x02, 0x03}},
|
||||
{"EscapeFlag", []byte{0x01, HDLC_FLAG, 0x03}, []byte{0x01, HDLC_ESC, HDLC_FLAG ^ HDLC_ESC_MASK, 0x03}},
|
||||
{"EscapeEsc", []byte{0x01, HDLC_ESC, 0x03}, []byte{0x01, HDLC_ESC, HDLC_ESC ^ HDLC_ESC_MASK, 0x03}},
|
||||
{"EscapeBoth", []byte{HDLC_FLAG, HDLC_ESC}, []byte{HDLC_ESC, HDLC_FLAG ^ HDLC_ESC_MASK, HDLC_ESC, HDLC_ESC ^ HDLC_ESC_MASK}},
|
||||
{"Empty", []byte{}, []byte{}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := escapeHDLC(tc.input)
|
||||
if !bytes.Equal(result, tc.expected) {
|
||||
t.Errorf("escapeHDLC(%x) = %x; want %x", tc.input, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscapeKISS(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input []byte
|
||||
expected []byte
|
||||
}{
|
||||
{"NoEscape", []byte{0x01, 0x02, 0x03}, []byte{0x01, 0x02, 0x03}},
|
||||
{"EscapeFEND", []byte{0x01, KISS_FEND, 0x03}, []byte{0x01, KISS_FESC, KISS_TFEND, 0x03}},
|
||||
{"EscapeFESC", []byte{0x01, KISS_FESC, 0x03}, []byte{0x01, KISS_FESC, KISS_TFESC, 0x03}},
|
||||
{"EscapeBoth", []byte{KISS_FEND, KISS_FESC}, []byte{KISS_FESC, KISS_TFEND, KISS_FESC, KISS_TFESC}},
|
||||
{"Empty", []byte{}, []byte{}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := escapeKISS(tc.input)
|
||||
if !bytes.Equal(result, tc.expected) {
|
||||
t.Errorf("escapeKISS(%x) = %x; want %x", tc.input, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
@@ -71,7 +70,7 @@ func (ui *UDPInterface) Detach() {
|
||||
defer ui.mutex.Unlock()
|
||||
ui.Detached = true
|
||||
if ui.conn != nil {
|
||||
ui.conn.Close()
|
||||
ui.conn.Close() // #nosec G104
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,32 +172,24 @@ func (ui *UDPInterface) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
func (ui *UDPInterface) readLoop() {
|
||||
buffer := make([]byte, ui.MTU)
|
||||
for {
|
||||
if ui.IsDetached() {
|
||||
return
|
||||
}
|
||||
|
||||
n, addr, err := ui.conn.ReadFromUDP(buffer)
|
||||
n, _, err := ui.conn.ReadFromUDP(buffer)
|
||||
if err != nil {
|
||||
if !ui.IsDetached() {
|
||||
log.Printf("UDP read error: %v", err)
|
||||
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
|
||||
}
|
||||
|
||||
ui.mutex.Lock()
|
||||
ui.RxBytes += uint64(n)
|
||||
ui.mutex.Unlock()
|
||||
|
||||
log.Printf("Received %d bytes from %s", n, addr.String())
|
||||
|
||||
if callback := ui.GetPacketCallback(); callback != nil {
|
||||
callback(buffer[:n], ui)
|
||||
if ui.packetCallback != nil {
|
||||
ui.packetCallback(buffer[:n], ui)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func (ui *UDPInterface) IsEnabled() bool {
|
||||
ui.mutex.RLock()
|
||||
|
||||
93
pkg/interfaces/udp_test.go
Normal file
93
pkg/interfaces/udp_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
)
|
||||
|
||||
func TestNewUDPInterface(t *testing.T) {
|
||||
validAddr := "127.0.0.1:0" // Use port 0 for OS to assign a free port
|
||||
validTarget := "127.0.0.1:8080"
|
||||
invalidAddr := "invalid-address"
|
||||
|
||||
t.Run("ValidConfig", func(t *testing.T) {
|
||||
ui, err := NewUDPInterface("udpValid", validAddr, validTarget, true)
|
||||
if err != nil {
|
||||
t.Fatalf("NewUDPInterface failed with valid config: %v", err)
|
||||
}
|
||||
if ui == nil {
|
||||
t.Fatal("NewUDPInterface returned nil interface with valid config")
|
||||
}
|
||||
if ui.GetName() != "udpValid" {
|
||||
t.Errorf("GetName() = %s; want udpValid", ui.GetName())
|
||||
}
|
||||
if ui.GetType() != common.IF_TYPE_UDP {
|
||||
t.Errorf("GetType() = %v; want %v", ui.GetType(), common.IF_TYPE_UDP)
|
||||
}
|
||||
if ui.addr.String() != validAddr && ui.addr.Port == 0 { // Check if address resolved, port 0 is special
|
||||
// Allow OS-assigned port if 0 was specified
|
||||
} else if ui.addr.String() != validAddr {
|
||||
// t.Errorf("Resolved addr = %s; want %s", ui.addr.String(), validAddr) //This check is flaky with port 0
|
||||
}
|
||||
if ui.targetAddr.String() != validTarget {
|
||||
t.Errorf("Resolved targetAddr = %s; want %s", ui.targetAddr.String(), validTarget)
|
||||
}
|
||||
if !ui.Enabled { // BaseInterface field
|
||||
t.Error("Interface not enabled by default when requested")
|
||||
}
|
||||
if ui.IsOnline() { // Should be offline initially
|
||||
t.Error("Interface online initially")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ValidConfigNoTarget", func(t *testing.T) {
|
||||
ui, err := NewUDPInterface("udpNoTarget", validAddr, "", true)
|
||||
if err != nil {
|
||||
t.Fatalf("NewUDPInterface failed with valid config (no target): %v", err)
|
||||
}
|
||||
if ui == nil {
|
||||
t.Fatal("NewUDPInterface returned nil interface with valid config (no target)")
|
||||
}
|
||||
if ui.targetAddr != nil {
|
||||
t.Errorf("targetAddr = %v; want nil", ui.targetAddr)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidAddress", func(t *testing.T) {
|
||||
_, err := NewUDPInterface("udpInvalidAddr", invalidAddr, validTarget, true)
|
||||
if err == nil {
|
||||
t.Error("NewUDPInterface succeeded with invalid address")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidTarget", func(t *testing.T) {
|
||||
_, err := NewUDPInterface("udpInvalidTarget", validAddr, invalidAddr, true)
|
||||
if err == nil {
|
||||
t.Error("NewUDPInterface succeeded with invalid target address")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUDPInterfaceState(t *testing.T) {
|
||||
// Basic state tests are covered by BaseInterface tests
|
||||
// Add specific UDP ones if needed, e.g., involving the conn
|
||||
addr := "127.0.0.1:0"
|
||||
ui, _ := NewUDPInterface("udpState", addr, "", true)
|
||||
|
||||
if ui.conn != nil {
|
||||
t.Error("conn field is not nil before Start()")
|
||||
}
|
||||
|
||||
// We don't call Start() here because it requires actual network binding
|
||||
// Testing Send requires Start() and a listener, which is too complex for unit tests here
|
||||
|
||||
// Test Detach
|
||||
ui.Detach()
|
||||
if !ui.IsDetached() {
|
||||
t.Error("IsDetached() is false after Detach()")
|
||||
}
|
||||
|
||||
// Further tests on Send/ProcessOutgoing/readLoop would require mocking net.UDPConn
|
||||
// or setting up a local listener.
|
||||
}
|
||||
@@ -577,7 +577,7 @@ func (l *Link) encrypt(data []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
// Encrypt
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
mode := cipher.NewCBCEncrypter(block, iv) // #nosec G407
|
||||
ciphertext := make([]byte, len(padtext))
|
||||
mode.CryptBlocks(ciphertext, padtext)
|
||||
|
||||
@@ -864,7 +864,9 @@ func (l *Link) watchdog() {
|
||||
|
||||
if time.Since(lastActivity) > l.keepalive {
|
||||
if l.initiator {
|
||||
l.SendPacket([]byte{}) // Keepalive packet
|
||||
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 {
|
||||
|
||||
@@ -16,6 +16,6 @@ const (
|
||||
DestinationLink = 3
|
||||
|
||||
// Minimum packet sizes
|
||||
MinAnnounceSize = 169 // header(2) + desthash(16) + enckey(32) + signkey(32) +
|
||||
MinAnnounceSize = 170 // header(2) + desthash(16) + context(1) + enckey(32) + signkey(32) +
|
||||
// namehash(10) + randomhash(10) + signature(64) + min appdata(3)
|
||||
)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package packet
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -113,9 +115,13 @@ func (p *Packet) Pack() error {
|
||||
|
||||
log.Printf("[DEBUG-6] Packing packet: type=%d, header=%d", p.PacketType, p.HeaderType)
|
||||
|
||||
// Create header byte
|
||||
flags := byte(p.HeaderType<<6) | byte(p.ContextFlag<<5) |
|
||||
byte(p.TransportType<<4) | byte(p.DestinationType<<2) | byte(p.PacketType)
|
||||
// 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)
|
||||
@@ -191,11 +197,19 @@ func (p *Packet) GetHash() []byte {
|
||||
}
|
||||
|
||||
func (p *Packet) getHashablePart() []byte {
|
||||
hashable := []byte{p.Raw[0] & 0b00001111}
|
||||
hashable := []byte{p.Raw[0] & 0b00001111} // Lower 4 bits of flags
|
||||
if p.HeaderType == HeaderType2 {
|
||||
hashable = append(hashable, p.Raw[18:]...)
|
||||
// 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 {
|
||||
hashable = append(hashable, p.Raw[2:]...)
|
||||
// Match Python: Start hash from DestHash (index 2)
|
||||
if len(p.Raw) > 2 {
|
||||
hashable = append(hashable, p.Raw[2:]...)
|
||||
}
|
||||
}
|
||||
return hashable
|
||||
}
|
||||
@@ -217,14 +231,62 @@ func (p *Packet) Serialize() ([]byte, error) {
|
||||
}
|
||||
|
||||
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, string(appData))
|
||||
log.Printf("[DEBUG-7] Creating new announce packet: destHash=%x, appData=%s", destHash, fmt.Sprintf("%x", appData))
|
||||
|
||||
// Create combined public key
|
||||
// Get public key separated into encryption and signing keys
|
||||
pubKey := identity.GetPublicKey()
|
||||
log.Printf("[DEBUG-6] Using public key: %x", pubKey)
|
||||
encKey := pubKey[:32]
|
||||
signKey := pubKey[32:]
|
||||
log.Printf("[DEBUG-6] Using public keys: encKey=%x, signKey=%x", encKey, signKey)
|
||||
|
||||
// Create signed data
|
||||
signedData := append(destHash, pubKey...)
|
||||
// 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))
|
||||
|
||||
@@ -232,11 +294,22 @@ func NewAnnouncePacket(destHash []byte, identity *identity.Identity, appData []b
|
||||
signature := identity.Sign(signedData)
|
||||
log.Printf("[DEBUG-6] Generated signature: %x", signature)
|
||||
|
||||
// Combine all data
|
||||
data := append(pubKey, appData...)
|
||||
data = append(data, 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{
|
||||
HeaderType: HeaderType2,
|
||||
PacketType: PacketTypeAnnounce,
|
||||
|
||||
276
pkg/packet/packet_test.go
Normal file
276
pkg/packet/packet_test.go
Normal file
@@ -0,0 +1,276 @@
|
||||
package packet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func randomBytes(n int) []byte {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
panic("Failed to generate random bytes: " + err.Error())
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func TestPacketPackUnpack(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
headerType byte
|
||||
packetType byte
|
||||
transportType byte
|
||||
destType byte
|
||||
context byte
|
||||
contextFlag byte
|
||||
dataSize int
|
||||
needsTransportID bool
|
||||
}{
|
||||
{
|
||||
name: "HeaderType1_Data_NoContextFlag",
|
||||
headerType: HeaderType1,
|
||||
packetType: PacketTypeData,
|
||||
transportType: 0x01, // Example
|
||||
destType: 0x02, // Example
|
||||
context: ContextNone,
|
||||
contextFlag: FlagUnset,
|
||||
dataSize: 100,
|
||||
needsTransportID: false,
|
||||
},
|
||||
{
|
||||
name: "HeaderType2_Announce_ContextFlagSet",
|
||||
headerType: HeaderType2,
|
||||
packetType: PacketTypeAnnounce,
|
||||
transportType: 0x01, // Changed from 0x0F (15) to 1 (valid 1-bit value)
|
||||
destType: 0x01, // Example
|
||||
context: ContextResourceAdv,
|
||||
contextFlag: FlagSet,
|
||||
dataSize: 50,
|
||||
needsTransportID: true,
|
||||
},
|
||||
{
|
||||
name: "HeaderType1_EmptyData",
|
||||
headerType: HeaderType1,
|
||||
packetType: PacketTypeProof,
|
||||
transportType: 0x00,
|
||||
destType: 0x00,
|
||||
context: ContextLRProof,
|
||||
contextFlag: FlagSet,
|
||||
dataSize: 0,
|
||||
needsTransportID: false,
|
||||
},
|
||||
{
|
||||
name: "HeaderType2_MaxHops", // Hops are set manually before pack
|
||||
headerType: HeaderType2,
|
||||
packetType: PacketTypeLinkReq,
|
||||
transportType: 0x01, // Changed from 0x05 (5) to 1 (valid 1-bit value)
|
||||
destType: 0x03,
|
||||
context: ContextLinkIdentify,
|
||||
contextFlag: FlagUnset,
|
||||
dataSize: 200,
|
||||
needsTransportID: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
originalData := randomBytes(tc.dataSize)
|
||||
originalDestHash := randomBytes(16) // Truncated dest hash
|
||||
var originalTransportID []byte
|
||||
if tc.needsTransportID {
|
||||
originalTransportID = randomBytes(16)
|
||||
}
|
||||
|
||||
p := &Packet{
|
||||
HeaderType: tc.headerType,
|
||||
PacketType: tc.packetType,
|
||||
TransportType: tc.transportType,
|
||||
Context: tc.context,
|
||||
ContextFlag: tc.contextFlag,
|
||||
Hops: 5, // Example hops
|
||||
DestinationType: tc.destType,
|
||||
DestinationHash: originalDestHash,
|
||||
TransportID: originalTransportID,
|
||||
Data: originalData,
|
||||
Packed: false,
|
||||
}
|
||||
|
||||
// Test Pack
|
||||
err := p.Pack()
|
||||
if err != nil {
|
||||
t.Fatalf("Pack() failed: %v", err)
|
||||
}
|
||||
if !p.Packed {
|
||||
t.Error("Pack() did not set Packed flag to true")
|
||||
}
|
||||
if len(p.Raw) == 0 {
|
||||
t.Error("Pack() resulted in empty Raw data")
|
||||
}
|
||||
|
||||
// Create a new packet from the raw data for unpacking
|
||||
unpackTarget := &Packet{Raw: p.Raw}
|
||||
|
||||
// Test Unpack
|
||||
err = unpackTarget.Unpack()
|
||||
if err != nil {
|
||||
t.Fatalf("Unpack() failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify unpacked fields match original
|
||||
if unpackTarget.HeaderType != tc.headerType {
|
||||
t.Errorf("Unpacked HeaderType = %d; want %d", unpackTarget.HeaderType, tc.headerType)
|
||||
}
|
||||
if unpackTarget.PacketType != tc.packetType {
|
||||
t.Errorf("Unpacked PacketType = %d; want %d", unpackTarget.PacketType, tc.packetType)
|
||||
}
|
||||
if unpackTarget.TransportType != tc.transportType {
|
||||
t.Errorf("Unpacked TransportType = %d; want %d", unpackTarget.TransportType, tc.transportType)
|
||||
}
|
||||
if unpackTarget.Context != tc.context {
|
||||
t.Errorf("Unpacked Context = %d; want %d", unpackTarget.Context, tc.context)
|
||||
}
|
||||
if unpackTarget.ContextFlag != tc.contextFlag {
|
||||
t.Errorf("Unpacked ContextFlag = %d; want %d", unpackTarget.ContextFlag, tc.contextFlag)
|
||||
}
|
||||
if unpackTarget.Hops != 5 { // Should match the Hops set before packing
|
||||
t.Errorf("Unpacked Hops = %d; want %d", unpackTarget.Hops, 5)
|
||||
}
|
||||
if unpackTarget.DestinationType != tc.destType {
|
||||
t.Errorf("Unpacked DestinationType = %d; want %d", unpackTarget.DestinationType, tc.destType)
|
||||
}
|
||||
if !bytes.Equal(unpackTarget.DestinationHash, originalDestHash) {
|
||||
t.Errorf("Unpacked DestinationHash = %x; want %x", unpackTarget.DestinationHash, originalDestHash)
|
||||
}
|
||||
if !bytes.Equal(unpackTarget.Data, originalData) {
|
||||
t.Errorf("Unpacked Data = %x; want %x", unpackTarget.Data, originalData)
|
||||
}
|
||||
|
||||
if tc.needsTransportID {
|
||||
if !bytes.Equal(unpackTarget.TransportID, originalTransportID) {
|
||||
t.Errorf("Unpacked TransportID = %x; want %x", unpackTarget.TransportID, originalTransportID)
|
||||
}
|
||||
} else {
|
||||
if unpackTarget.TransportID != nil {
|
||||
t.Errorf("Unpacked TransportID = %x; want nil", unpackTarget.TransportID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackMTUExceeded(t *testing.T) {
|
||||
p := &Packet{
|
||||
HeaderType: HeaderType1,
|
||||
PacketType: PacketTypeData,
|
||||
DestinationHash: randomBytes(16),
|
||||
Context: ContextNone,
|
||||
Data: randomBytes(MTU + 10), // Exceed MTU
|
||||
}
|
||||
err := p.Pack()
|
||||
if err == nil {
|
||||
t.Errorf("Pack() should have failed due to exceeding MTU, but it didn't")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnpackTooShort(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
raw []byte
|
||||
}{
|
||||
{"VeryShort", []byte{0x01}},
|
||||
{"HeaderType1MinShort", []byte{0x00, 0x05, 0x01, 0x02}}, // Missing parts of dest hash
|
||||
{"HeaderType2MinShort", []byte{0x40, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}}, // Missing dest hash
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p := &Packet{Raw: tc.raw}
|
||||
err := p.Unpack()
|
||||
if err == nil {
|
||||
t.Errorf("Unpack() should have failed for short packet, but it didn't")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketHashing(t *testing.T) {
|
||||
// Create two identical packets
|
||||
data := randomBytes(50)
|
||||
destHash := randomBytes(16)
|
||||
p1 := &Packet{
|
||||
HeaderType: HeaderType1,
|
||||
PacketType: PacketTypeData,
|
||||
TransportType: 0x01,
|
||||
Context: ContextNone,
|
||||
ContextFlag: FlagUnset,
|
||||
Hops: 2,
|
||||
DestinationType: 0x02,
|
||||
DestinationHash: destHash,
|
||||
Data: data,
|
||||
}
|
||||
p2 := &Packet{
|
||||
HeaderType: HeaderType1,
|
||||
PacketType: PacketTypeData,
|
||||
TransportType: 0x01,
|
||||
Context: ContextNone,
|
||||
ContextFlag: FlagUnset,
|
||||
Hops: 2,
|
||||
DestinationType: 0x02,
|
||||
DestinationHash: destHash,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
// Pack both
|
||||
if err := p1.Pack(); err != nil {
|
||||
t.Fatalf("p1.Pack() failed: %v", err)
|
||||
}
|
||||
if err := p2.Pack(); err != nil {
|
||||
t.Fatalf("p2.Pack() failed: %v", err)
|
||||
}
|
||||
|
||||
// Hashes should be identical
|
||||
hash1 := p1.GetHash()
|
||||
hash2 := p2.GetHash()
|
||||
if !bytes.Equal(hash1, hash2) {
|
||||
t.Errorf("Hashes of identical packets differ:\nHash1: %x\nHash2: %x", hash1, hash2)
|
||||
}
|
||||
if !bytes.Equal(p1.PacketHash, hash1) {
|
||||
t.Errorf("p1.PacketHash (%x) does not match GetHash() (%x)", p1.PacketHash, hash1)
|
||||
}
|
||||
|
||||
// Change a non-hashable field (hops) in p2
|
||||
p2.Hops = 3
|
||||
p2.Raw[1] = 3 // Need to modify Raw as Pack isn't called again
|
||||
hash3 := p2.GetHash()
|
||||
if !bytes.Equal(hash1, hash3) {
|
||||
t.Errorf("Hash changed after modifying non-hashable Hops field:\nHash1: %x\nHash3: %x", hash1, hash3)
|
||||
}
|
||||
|
||||
// Change a hashable field (data) in p2
|
||||
p2.Data = append(p2.Data, 0x99)
|
||||
p2.Raw = append(p2.Raw, 0x99) // Modify Raw to reflect data change
|
||||
hash4 := p2.GetHash()
|
||||
if bytes.Equal(hash1, hash4) {
|
||||
t.Errorf("Hash did not change after modifying hashable Data field")
|
||||
}
|
||||
|
||||
// Test HeaderType2 hashing difference
|
||||
p3 := &Packet{
|
||||
HeaderType: HeaderType2,
|
||||
PacketType: PacketTypeData,
|
||||
TransportType: 0x01,
|
||||
Context: ContextNone,
|
||||
ContextFlag: FlagUnset,
|
||||
Hops: 2,
|
||||
DestinationType: 0x02,
|
||||
DestinationHash: destHash,
|
||||
TransportID: randomBytes(16),
|
||||
Data: data,
|
||||
}
|
||||
if err := p3.Pack(); err != nil {
|
||||
t.Fatalf("p3.Pack() failed: %v", err)
|
||||
}
|
||||
hash5 := p3.GetHash()
|
||||
_ = hash5 // Use hash5 to avoid unused variable error
|
||||
}
|
||||
@@ -128,7 +128,7 @@ func New(data interface{}, autoCompress bool) (*Resource, error) {
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
mathrand "math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -121,9 +120,6 @@ type Path struct {
|
||||
HopCount byte
|
||||
}
|
||||
|
||||
var randSource = mathrand.NewSource(time.Now().UnixNano())
|
||||
var rng = mathrand.New(randSource)
|
||||
|
||||
func NewTransport(cfg *common.ReticulumConfig) *Transport {
|
||||
t := &Transport{
|
||||
interfaces: make(map[string]common.NetworkInterface),
|
||||
@@ -445,7 +441,15 @@ func (t *Transport) HandleAnnounce(data []byte, sourceIface common.NetworkInterf
|
||||
}
|
||||
|
||||
// Add random delay before retransmission (0-2 seconds)
|
||||
delay := time.Duration(rng.Float64() * 2 * float64(time.Second))
|
||||
var delay time.Duration
|
||||
b := make([]byte, 8)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-7] Failed to generate random delay: %v", err)
|
||||
delay = time.Duration(0) // Default to no delay on error
|
||||
} else {
|
||||
delay = time.Duration(binary.BigEndian.Uint64(b)%2000) * time.Millisecond // #nosec G115
|
||||
}
|
||||
time.Sleep(delay)
|
||||
|
||||
// Check bandwidth allocation for announces
|
||||
@@ -515,7 +519,7 @@ func (p *LinkPacket) send() error {
|
||||
|
||||
// Add timestamp
|
||||
ts := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(ts, uint64(p.Timestamp.Unix()))
|
||||
binary.BigEndian.PutUint64(ts, uint64(p.Timestamp.Unix())) // #nosec G115
|
||||
header = append(header, ts...)
|
||||
|
||||
// Combine header and data
|
||||
@@ -738,7 +742,15 @@ func (t *Transport) handleAnnouncePacket(data []byte, iface common.NetworkInterf
|
||||
}
|
||||
|
||||
// Add random delay before retransmission (0-2 seconds)
|
||||
delay := time.Duration(rng.Float64() * 2 * float64(time.Second))
|
||||
var delay time.Duration
|
||||
b := make([]byte, 8)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-7] Failed to generate random delay: %v", err)
|
||||
delay = time.Duration(0) // Default to no delay on error
|
||||
} else {
|
||||
delay = time.Duration(binary.BigEndian.Uint64(b)%2000) * time.Millisecond // #nosec G115
|
||||
}
|
||||
time.Sleep(delay)
|
||||
|
||||
// Check bandwidth allocation for announces
|
||||
@@ -791,14 +803,16 @@ func (t *Transport) handleLinkPacket(data []byte, iface common.NetworkInterface)
|
||||
if nextIfaceName != iface.GetName() {
|
||||
if nextIface, ok := t.interfaces[nextIfaceName]; ok {
|
||||
log.Printf("[DEBUG-7] Forwarding link packet to %s", nextIfaceName)
|
||||
nextIface.Send(data, string(nextHop))
|
||||
if err := nextIface.Send(data, string(nextHop)); err != nil { // #nosec G104
|
||||
log.Printf("[DEBUG-7] Failed to forward link packet: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if link := t.findLink(dest); link != nil {
|
||||
log.Printf("[DEBUG-6] Updating link timing - Last inbound: %v", time.Unix(int64(timestamp), 0))
|
||||
link.lastInbound = time.Unix(int64(timestamp), 0)
|
||||
log.Printf("[DEBUG-6] Updating link timing - Last inbound: %v", time.Unix(int64(timestamp), 0)) // #nosec G115
|
||||
link.lastInbound = time.Unix(int64(timestamp), 0) // #nosec G115
|
||||
if link.packetCb != nil {
|
||||
log.Printf("[DEBUG-7] Executing packet callback with %d bytes", len(payload))
|
||||
p := &packet.Packet{Data: payload}
|
||||
@@ -1090,9 +1104,13 @@ func CreateAnnouncePacket(destHash []byte, identity *identity.Identity, appData
|
||||
|
||||
// Add random hash (10 bytes)
|
||||
randomBytes := make([]byte, 5)
|
||||
rand.Read(randomBytes)
|
||||
_, err := rand.Read(randomBytes) // #nosec G104
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-7] Failed to read random bytes: %v", err)
|
||||
return nil // Or handle the error appropriately
|
||||
}
|
||||
timeBytes := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(timeBytes, uint64(time.Now().Unix()))
|
||||
binary.BigEndian.PutUint64(timeBytes, uint64(time.Now().Unix())) // #nosec G115
|
||||
log.Printf("[DEBUG-7] Adding random hash (10 bytes): %x%x", randomBytes, timeBytes[:5])
|
||||
packet = append(packet, randomBytes...)
|
||||
packet = append(packet, timeBytes[:5]...)
|
||||
@@ -1138,3 +1156,7 @@ func (t *Transport) GetInterfaces() map[string]common.NetworkInterface {
|
||||
|
||||
return interfaces
|
||||
}
|
||||
|
||||
func (t *Transport) GetConfig() *common.ReticulumConfig {
|
||||
return t.config
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user