Compare commits
321 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
97353d430b
|
|||
|
1d3a969742
|
|||
|
bad92193a3
|
|||
|
6560949ec4
|
|||
|
d6152ccd85
|
|||
|
e6fd7188d2
|
|||
|
bc44ed2aaa
|
|||
|
e5ac206e5c
|
|||
|
b74012099c
|
|||
|
ae1d290fa7
|
|||
|
562f850b8f
|
|||
|
ff0088644e
|
|||
|
aee52bf56c
|
|||
|
fd951a10f8
|
|||
|
11d4c6407e
|
|||
|
1be94dc0ba
|
|||
|
b30a1ba3eb
|
|||
|
9f755aec21
|
|||
|
1a579bc716
|
|||
|
8124d95192
|
|||
|
a59dca45a7
|
|||
|
1106215241
|
|||
|
ee61747e20
|
|||
|
4c1c819e42
|
|||
|
078fa0f17d
|
|||
|
876476cff5
|
|||
|
899b08e92e
|
|||
|
eec73d2d93
|
|||
|
6888eccc62
|
|||
|
6fa0187ae1
|
|||
|
2ba3f059a1
|
|||
|
63454b3bbb
|
|||
|
9f36e37f94
|
|||
|
73b982c6e0
|
|||
|
22c54f2252
|
|||
|
d68a6cfb9c
|
|||
|
8267123fb5
|
|||
|
a540b64331
|
|||
|
b0d2d4778f
|
|||
|
6cb90d3c4b
|
|||
|
009755c981
|
|||
|
595430c808
|
|||
|
e9b647d5a7
|
|||
|
7d57888696
|
|||
|
cbe2df02ad
|
|||
|
ff893945e9
|
|||
|
b705427bc9
|
|||
|
88083be84e
|
|||
|
4ea1fd1f28
|
|||
|
1ad1f3cfd2
|
|||
|
1281731a81
|
|||
|
8d97c29b19
|
|||
|
f8712b35b8
|
|||
|
0051405033
|
|||
|
3a14394640
|
|||
|
2faf1fb5a2
|
|||
|
e51baa8673
|
|||
|
21a0dafae6
|
|||
|
156d642fe6
|
|||
|
20a1da6a56
|
|||
|
0a3d0c9440
|
|||
|
04bd23e753
|
|||
|
a01b253473
|
|||
|
53b981cb21
|
|||
|
6cc82159cf
|
|||
|
5a9bfd74d3
|
|||
|
1c3de51913
|
|||
|
25e0719954
|
|||
|
cbff6e4bc4
|
|||
|
4d6eda36c0
|
|||
|
18200213f0
|
|||
|
3cb375e7f2
|
|||
|
53d418d753
|
|||
|
ac0407c954
|
|||
|
83abefcf43
|
|||
|
3d4b77bee3
|
|||
|
71b6032ea4
|
|||
|
3675d63418
|
|||
|
37652b2d33
|
|||
|
5c19b76367
|
|||
|
f96af89269
|
|||
|
6704c620c8
|
|||
|
738aa9528a
|
|||
|
ff1589216b
|
|||
|
3af2be27d8
|
|||
|
8e777bef03
|
|||
|
220912989e
|
|||
|
177dc3a099
|
|||
|
df3497b725
|
|||
|
e3d65525b8
|
|||
|
474bb2ff33
|
|||
|
5707c230e2
|
|||
|
4ae59c9716
|
|||
|
59060d6002
|
|||
|
ed413b62a1
|
|||
|
b60d91aa17
|
|||
|
d465f103ec
|
|||
|
fcfd04c0c2
|
|||
|
b25f2c2bdc
|
|||
|
7bb127526c
|
|||
|
48f8288577
|
|||
|
7086926839
|
|||
|
6c3bdaa743
|
|||
|
d8d38fdfe4
|
|||
|
82dad74ba8
|
|||
|
b630122d78
|
|||
|
99e4d92e8e
|
|||
|
9fa712c0b1
|
|||
|
87fc514f32
|
|||
|
1f6f8580a8
|
|||
|
5a3fed8f8b
|
|||
|
5e4b21a431
|
|||
|
aea98d4cae
|
|||
|
8aa30a39e4
|
|||
|
c3893eb33d
|
|||
|
53e98c73af
|
|||
|
2fc9446687
|
|||
|
49aee4818b
|
|||
|
430290deaf
|
|||
|
2ae1143b19
|
|||
|
a34c211872
|
|||
|
fda77ba10d
|
|||
|
9a93a08b85
|
|||
|
25fa49ffe2
|
|||
|
96c68f2aff
|
|||
|
30210d0714
|
|||
|
e8fc291a01
|
|||
|
ea0797de68
|
|||
|
3cacfadf27
|
|||
|
45878f0666
|
|||
|
483234eee0
|
|||
|
82523cc7df
|
|||
|
634ff693de
|
|||
|
ea36ba7a65
|
|||
|
8e243a7c8b
|
|||
|
dd4383a526
|
|||
|
75ddddd537
|
|||
|
6b3fae179f
|
|||
|
a1834f35f7
|
|||
|
804e8ed997
|
|||
|
c46d15fc90
|
|||
|
06ac57c677
|
|||
|
ea5c9147da
|
|||
|
49bc7fe7f6
|
|||
|
9b1f45ff77
|
|||
|
0efd83f0e8
|
|||
|
7dc9f94771
|
|||
|
ae729c2ca7
|
|||
|
3b3389118f
|
|||
|
66b11b4f39
|
|||
|
afc0779edf
|
|||
|
a6546d3566
|
|||
|
e59fee8e60
|
|||
| 395b180757 | |||
| b743f57690 | |||
| bd0c94109e | |||
| 6896672562 | |||
| fb69af9bf4 | |||
|
dd87da4a51
|
|||
|
c8d231556c
|
|||
|
b489135c5b
|
|||
|
1d83e7f539
|
|||
|
565b13a0eb
|
|||
|
52f8e21da0
|
|||
|
ad5b6ed83a
|
|||
|
cced3f5092
|
|||
|
3b2a8591a7
|
|||
|
f513c6abcd
|
|||
|
e83bc31ccc
|
|||
|
1e67bc56b6
|
|||
|
b0e6ce93f9
|
|||
|
0d2239be83
|
|||
|
cfcdb62168
|
|||
|
d14692b19b
|
|||
|
ca3bef0635
|
|||
|
59486330ec
|
|||
|
1133e9755d
|
|||
|
b450aa8569
|
|||
|
6aef9e9337
|
|||
|
678ab32ee6
|
|||
|
4e37112aee
|
|||
|
af046370db
|
|||
|
eb40fd6451
|
|||
|
763fb4a962
|
|||
|
673f19b1ff
|
|||
|
4a20551e9a
|
|||
|
1ac5696c80
|
|||
|
5c2ea259b8
|
|||
|
7573e942f1
|
|||
|
d3cf775394
|
|||
|
5e19f6f802
|
|||
|
72d70b2141
|
|||
|
06da42a148
|
|||
|
ec8b843cd4
|
|||
|
9e7e9a71ca
|
|||
|
b0669954a4
|
|||
|
ded5853026
|
|||
|
426422413c
|
|||
|
50c8546344
|
|||
|
b0fad14504
|
|||
|
24aa4fa88b
|
|||
|
189645940c
|
|||
|
4f37df2fc0
|
|||
|
4859f5513a
|
|||
|
2a2d6d6515
|
|||
|
d0c111d2f5
|
|||
|
3d943aaaef
|
|||
|
d5d7fbdb79
|
|||
|
fdfe895d2d
|
|||
|
01e639133b
|
|||
|
6c6953e664
|
|||
|
b4039dc148
|
|||
|
ba18fba43e
|
|||
|
f4a929ce3a
|
|||
|
fe66163ef7
|
|||
|
2cc34172c8
|
|||
|
9331c4edbd
|
|||
|
f097bb3241
|
|||
|
22fc5093db
|
|||
|
fc95e54b2e
|
|||
|
636d400f1e
|
|||
| fd5eb65bc0 | |||
| 4e13fe523b | |||
| dd2cc3e3d9 | |||
| 353e9c6d9b | |||
| 088ba3337d | |||
| 4cd2338095 | |||
| c6cc1d8ca8 | |||
| 0afb0e9ade | |||
| feeaa72102 | |||
| bb964445f3 | |||
| 5369037a74 | |||
| bb98248830 | |||
| 575657bbc5 | |||
| 8da4a759f5 | |||
| dff1489ee5 | |||
| 30c97bc9dd | |||
| 005e2566aa | |||
| cc10830df3 | |||
| b548e5711e | |||
| cc89bfef6e | |||
| 45a3ac1e87 | |||
| e39936ac30 | |||
| b601ae1c51 | |||
| 7d7a022736 | |||
| 0ac2a8d200 | |||
| f3808a73e1 | |||
| cb908fb143 | |||
| f53194be25 | |||
| ad732d1465 | |||
| b70a7d03af | |||
| 911fe3ea8e | |||
| b59bb349dc | |||
| 08cbacd69f | |||
| 9a70a92261 | |||
| be34168a1b | |||
| cebab6b2f3 | |||
| fdcb371582 | |||
| f01b1f8bac | |||
| a0eca36884 | |||
| 972d00df92 | |||
| 483b6e562b | |||
| cbb5ffa970 | |||
| b7cc0c39b4 | |||
| 982c173760 | |||
| 49ca73ab3a | |||
| 43b224b4d7 | |||
| 456a95d569 | |||
| 53b2d18a79 | |||
| 8d7f86e15a | |||
| 40213eeac9 | |||
| 5cb8b12a0f | |||
| 2f165186d1 | |||
| 6cd3b15d78 | |||
| 98c8d35f1e | |||
| 064b2b10b8 | |||
| a8d78d2784 | |||
| 5a0c70190f | |||
| d5bf7dc720 | |||
| 8b4bca7939 | |||
| c004ff1a97 | |||
| 38323da57d | |||
| 2ffd12b3e1 | |||
| 069d4163eb | |||
| 93e1317789 | |||
| 3b270e05c4 | |||
| a05818b3a7 | |||
| df2b0a0079 | |||
| c507e9125b | |||
| 767110f3d0 | |||
|
|
8e5f193caf | ||
| 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 |
@@ -1,7 +0,0 @@
|
||||
version = 1
|
||||
|
||||
[[analyzers]]
|
||||
name = "go"
|
||||
|
||||
[analyzers.meta]
|
||||
import_root = "github.com/Sudo-Ivan/Reticulum-Go"
|
||||
36
.dockerignore
Normal file
36
.dockerignore
Normal file
@@ -0,0 +1,36 @@
|
||||
# Binaries and build folders
|
||||
bin/
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Go modules' cache
|
||||
vendor/
|
||||
|
||||
# Local test/coverage/log artifacts
|
||||
*.test
|
||||
*.out
|
||||
*.log
|
||||
logs/
|
||||
coverage.out
|
||||
|
||||
# Environment and secret files
|
||||
.env
|
||||
|
||||
# User/IDE/Editor config
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Example and generated files
|
||||
examples/
|
||||
*.json
|
||||
|
||||
# SBOM and analysis artifacts
|
||||
bom.json
|
||||
dependency-results.sbom.json
|
||||
*.sbom.json
|
||||
|
||||
27
.gitea/workflows/bearer.yml
Normal file
27
.gitea/workflows/bearer.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Bearer
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
scan:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Source
|
||||
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Run Bearer Security Scanner
|
||||
uses: https://git.quad4.io/actions/bearer-action@828eeb928ce2f4a7ca5ed57fb8b59508cb8c79bc # v2
|
||||
with:
|
||||
path: ./
|
||||
|
||||
105
.gitea/workflows/build.yml
Normal file
105
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,105 @@
|
||||
name: Go Build Multi-Platform
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main", "master" ]
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches: [ "main", "master" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux, windows, darwin, freebsd]
|
||||
goarch: [amd64, arm64, arm]
|
||||
include:
|
||||
- goos: js
|
||||
goarch: wasm
|
||||
exclude:
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
build_complete: ${{ steps.build_step.outcome == 'success' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up Go
|
||||
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: '1.25'
|
||||
|
||||
- name: Setup Task
|
||||
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
|
||||
with:
|
||||
version: '3.46.3'
|
||||
|
||||
- name: Build
|
||||
id: build_step
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOARM: ${{ matrix.goarch == 'arm' && '6' || '' }}
|
||||
CGO_ENABLED: '0'
|
||||
run: |
|
||||
output_name="reticulum-go-${GOOS}-${GOARCH}"
|
||||
if [ "$GOOS" = "js" ] && [ "$GOARCH" = "wasm" ]; then
|
||||
task build-wasm
|
||||
output_name+=".wasm"
|
||||
mv bin/reticulum-go.wasm "${output_name}"
|
||||
else
|
||||
task build
|
||||
if [ "$GOOS" = "windows" ]; then
|
||||
output_name+=".exe"
|
||||
fi
|
||||
mv bin/reticulum-go "${output_name}"
|
||||
fi
|
||||
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"
|
||||
elif [ "${{ matrix.goos }}" = "js" ] && [ "${{ matrix.goarch }}" = "wasm" ]; then
|
||||
output_name+=".wasm"
|
||||
fi
|
||||
BINARY_PATH="${output_name}" task checksum
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: https://git.quad4.io/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3.2.1
|
||||
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: https://git.quad4.io/actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a
|
||||
with:
|
||||
path: ./release-assets
|
||||
|
||||
- name: List downloaded files (for debugging)
|
||||
run: ls -R ./release-assets
|
||||
|
||||
- name: Create Gitea Release
|
||||
uses: https://git.quad4.io/actions/gitea-release-action@4875285c0950474efb7ca2df55233c51333eeb74
|
||||
with:
|
||||
files: ./release-assets/*/*
|
||||
105
.gitea/workflows/go-test.yml
Normal file
105
.gitea/workflows/go-test.yml
Normal file
@@ -0,0 +1,105 @@
|
||||
name: Go Test Multi-Platform
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test (${{ matrix.os }}, ${{ matrix.goarch }})
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# AMD64 testing on Linux
|
||||
- os: ubuntu-latest
|
||||
goarch: amd64
|
||||
# ARM64 testing on Linux
|
||||
- os: ubuntu-latest
|
||||
goarch: arm64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout Source
|
||||
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up Go 1.25
|
||||
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: '1.25'
|
||||
|
||||
- name: Setup Task
|
||||
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
|
||||
with:
|
||||
version: '3.46.3'
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: https://git.quad4.io/actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-${{ matrix.goarch }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ matrix.goarch }}-
|
||||
|
||||
- name: Set up Node.js
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
||||
uses: https://git.quad4.io/actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Run tests
|
||||
run: task test
|
||||
|
||||
- name: Run tests with race detector (Linux AMD64 only)
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
||||
run: task test-race
|
||||
|
||||
- name: Run WebAssembly tests (Linux AMD64 only)
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
||||
run: |
|
||||
chmod +x misc/wasm/go_js_wasm_exec
|
||||
task test-wasm
|
||||
|
||||
- name: Test build (ensure compilation works)
|
||||
run: |
|
||||
echo "Testing build for current platform (${{ matrix.os }}, ${{ matrix.goarch }})..."
|
||||
task build
|
||||
|
||||
- name: Test WebAssembly build (Linux AMD64 only)
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
||||
run: task build-wasm
|
||||
|
||||
- name: Test binary execution
|
||||
run: |
|
||||
echo "Testing binary execution on (${{ matrix.os }}, ${{ matrix.goarch }})..."
|
||||
timeout 5s ./bin/reticulum-go || echo "Binary started successfully (timeout expected)"
|
||||
|
||||
- name: Test cross-compilation (AMD64 runners only)
|
||||
if: matrix.goarch == 'amd64'
|
||||
run: |
|
||||
echo "Testing ARM64 cross-compilation from AMD64..."
|
||||
GOOS=linux GOARCH=arm64 task build
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: arm64
|
||||
|
||||
- name: Test ARMv6 cross-compilation (AMD64 runners only)
|
||||
if: matrix.goarch == 'amd64'
|
||||
run: |
|
||||
echo "Testing ARMv6 cross-compilation from AMD64..."
|
||||
GOOS=linux GOARCH=arm GOARM=6 task build
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: arm
|
||||
GOARM: 6
|
||||
27
.gitea/workflows/gosec.yml
Normal file
27
.gitea/workflows/gosec.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Run Gosec
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GO111MODULE: on
|
||||
steps:
|
||||
- name: Checkout Source
|
||||
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Run Gosec Security Scanner
|
||||
uses: https://git.quad4.io/actions/gosec@c073629009897d89e03229bc81232c7375892086
|
||||
with:
|
||||
args: ./...
|
||||
33
.gitea/workflows/revive.yml
Normal file
33
.gitea/workflows/revive.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Go Revive Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main", "master" ]
|
||||
pull_request:
|
||||
branches: [ "main", "master" ]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up Go
|
||||
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: '1.25'
|
||||
|
||||
- name: Setup Task
|
||||
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
|
||||
with:
|
||||
version: '3.46.3'
|
||||
|
||||
- name: Install revive
|
||||
run: go install github.com/mgechev/revive@latest
|
||||
|
||||
- name: Run lint
|
||||
run: task lint
|
||||
53
.gitea/workflows/sbom.yml
Normal file
53
.gitea/workflows/sbom.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Generate SBOM
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
generate-sbom:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
with:
|
||||
go-version: '1.25.5'
|
||||
|
||||
- name: Setup Task
|
||||
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
|
||||
with:
|
||||
version: '3.46.3'
|
||||
|
||||
- name: Install dependencies
|
||||
run: task deps
|
||||
|
||||
- name: Install Trivy
|
||||
run: task trivy:install
|
||||
|
||||
- name: Generate SBOM
|
||||
run: task sbom
|
||||
|
||||
- name: Commit and Push Changes
|
||||
run: |
|
||||
git config --global user.name "Gitea Action"
|
||||
git config --global user.email "actions@noreply.quad4.io"
|
||||
git remote set-url origin https://${{ secrets.GITEA_TOKEN }}@git.quad4.io/${{ github.repository }}.git
|
||||
git fetch origin main || git fetch origin master
|
||||
git checkout main || git checkout master
|
||||
git add sbom/
|
||||
if ! git diff --quiet || ! git diff --staged --quiet; then
|
||||
git commit -m "Auto-update SBOM [skip ci]"
|
||||
git push origin main || git push origin master
|
||||
fi
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
|
||||
64
.gitea/workflows/tinygo.yml
Normal file
64
.gitea/workflows/tinygo.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
name: TinyGo Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "tinygo" ]
|
||||
pull_request:
|
||||
branches: [ "tinygo" ]
|
||||
|
||||
jobs:
|
||||
tinygo-build:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: tinygo-default
|
||||
target: ""
|
||||
output: reticulum-go-tinygo
|
||||
make_target: tinygo-build
|
||||
- name: tinygo-wasm
|
||||
target: wasm
|
||||
output: reticulum-go.wasm
|
||||
make_target: tinygo-wasm
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
build_complete: ${{ steps.build_step.outcome == 'success' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up Go
|
||||
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
|
||||
with:
|
||||
go-version: '1.24'
|
||||
|
||||
- name: Install TinyGo
|
||||
run: |
|
||||
wget https://github.com/tinygo-org/tinygo/releases/download/v0.37.0/tinygo_0.37.0_amd64.deb
|
||||
sudo dpkg -i tinygo_0.37.0_amd64.deb
|
||||
|
||||
- name: Build with TinyGo
|
||||
id: build_step
|
||||
run: |
|
||||
make ${{ matrix.make_target }}
|
||||
output_name="${{ matrix.output }}"
|
||||
if [ -f "bin/${output_name}" ]; then
|
||||
sha256sum "bin/${output_name}" | cut -d' ' -f1 > "bin/${output_name}.sha256"
|
||||
echo "Built: ${output_name}"
|
||||
echo "Generated checksum: bin/${output_name}.sha256"
|
||||
else
|
||||
echo "Build output not found: bin/${output_name}"
|
||||
ls -la bin/
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: https://git.quad4.io/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5
|
||||
with:
|
||||
name: ${{ matrix.name }}
|
||||
path: bin/${{ matrix.output }}*
|
||||
36
.gitignore
vendored
36
.gitignore
vendored
@@ -1,7 +1,31 @@
|
||||
logs/
|
||||
*.log
|
||||
|
||||
.env
|
||||
.json
|
||||
|
||||
# Build artifacts
|
||||
bin/
|
||||
|
||||
# Test coverage reports
|
||||
coverage.out
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Local environment variables
|
||||
.env
|
||||
|
||||
# JSON assets and auto-generated exports
|
||||
*.json
|
||||
|
||||
# Example files, not adding them just yet.
|
||||
/examples/*
|
||||
!/examples/wasm/
|
||||
|
||||
# OS / Editor files
|
||||
.DS_Store # macOS Finder metadata
|
||||
Thumbs.db # Windows Explorer thumbnail cache
|
||||
|
||||
# IDE / Editor config directories
|
||||
.idea/ # JetBrains IDEs
|
||||
.vscode/ # Visual Studio Code
|
||||
|
||||
# Swap and test binaries
|
||||
*.swp # Swap files (e.g. vim)
|
||||
*.test # Go test binaries
|
||||
@@ -1,21 +1,7 @@
|
||||
# Contributing
|
||||
|
||||
Be good to each other.
|
||||
Send issues, suggestions, `.patch` files, or any feedback to one of the preferred methods:
|
||||
|
||||
## 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.
|
||||
1. Reticulum LXMF: `7cc8d66b4f6a0e0e49d34af7f6077b5a` - Ivan (main developer)
|
||||
2. XMPP: `ivan@chat.quad4.io` - Ivan (main developer)
|
||||
3. Email: `team@quad4.io` - Quad4 Team
|
||||
14
CONTRIBUTORS
Normal file
14
CONTRIBUTORS
Normal file
@@ -0,0 +1,14 @@
|
||||
CONTRIBUTORS
|
||||
|
||||
This file lists all contributors to the Reticulum-Go project.
|
||||
|
||||
Sudo-Ivan
|
||||
Total commits: 442
|
||||
First contribution: 2024-12-30
|
||||
Last contribution: 2026-01-01
|
||||
|
||||
|
||||
Mike Coles
|
||||
Total commits: 1
|
||||
First contribution: 2025-08-07
|
||||
Last contribution: 2025-08-07
|
||||
17
LICENSE
17
LICENSE
@@ -1,9 +1,12 @@
|
||||
Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
Copyright 2024-2025 Sudo-Ivan / Quad4.io
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
106
Makefile
106
Makefile
@@ -1,106 +0,0 @@
|
||||
GOCMD=go
|
||||
GOBUILD=$(GOCMD) build
|
||||
GOCLEAN=$(GOCMD) clean
|
||||
GOTEST=$(GOCMD) test
|
||||
GOGET=$(GOCMD) get
|
||||
GOMOD=$(GOCMD) mod
|
||||
BINARY_NAME=reticulum-go
|
||||
BINARY_UNIX=$(BINARY_NAME)_unix
|
||||
|
||||
BUILD_DIR=bin
|
||||
|
||||
MAIN_PACKAGE=./cmd/reticulum-go
|
||||
|
||||
ALL_PACKAGES=$$(go list ./... | grep -v /vendor/)
|
||||
|
||||
.PHONY: all build clean test coverage deps help
|
||||
|
||||
all: clean deps build test
|
||||
|
||||
build:
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_PACKAGE)
|
||||
|
||||
clean:
|
||||
@rm -rf $(BUILD_DIR)
|
||||
$(GOCLEAN)
|
||||
|
||||
test:
|
||||
$(GOTEST) -v $(ALL_PACKAGES)
|
||||
|
||||
coverage:
|
||||
$(GOTEST) -coverprofile=coverage.out $(ALL_PACKAGES)
|
||||
$(GOCMD) tool cover -html=coverage.out
|
||||
|
||||
deps:
|
||||
$(GOMOD) download
|
||||
$(GOMOD) verify
|
||||
|
||||
build-linux:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm $(MAIN_PACKAGE)
|
||||
|
||||
build-windows:
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-arm64.exe $(MAIN_PACKAGE)
|
||||
|
||||
build-darwin:
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 $(MAIN_PACKAGE)
|
||||
|
||||
build-freebsd:
|
||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-amd64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=386 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-386 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-arm64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-arm $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=riscv64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-riscv64 $(MAIN_PACKAGE)
|
||||
|
||||
build-openbsd:
|
||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-amd64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=386 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-386 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-arm64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-arm $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=ppc64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-ppc64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=riscv64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-openbsd-riscv64 $(MAIN_PACKAGE)
|
||||
|
||||
build-netbsd:
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-amd64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=386 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-386 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm64 $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm $(MAIN_PACKAGE)
|
||||
|
||||
build-arm:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-arm $(MAIN_PACKAGE)
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-arm64 $(MAIN_PACKAGE)
|
||||
|
||||
build-riscv:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-riscv64 $(MAIN_PACKAGE)
|
||||
|
||||
build-all: build-linux build-windows build-darwin build-freebsd build-openbsd build-netbsd build-arm build-riscv
|
||||
|
||||
run:
|
||||
@./$(BUILD_DIR)/$(BINARY_NAME)
|
||||
|
||||
install:
|
||||
$(GOMOD) download
|
||||
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " all - Clean, download dependencies, build and test"
|
||||
@echo " build - Build binary"
|
||||
@echo " clean - Remove build artifacts"
|
||||
@echo " test - Run tests"
|
||||
@echo " coverage - Generate test coverage report"
|
||||
@echo " deps - Download dependencies"
|
||||
@echo " build-linux - Build for Linux (amd64, arm64, arm)"
|
||||
@echo " build-windows- Build for Windows (amd64, arm64)"
|
||||
@echo " build-darwin - Build for MacOS (amd64, arm64)"
|
||||
@echo " build-freebsd- Build for FreeBSD (amd64, 386, arm64, arm, riscv64)"
|
||||
@echo " build-openbsd- Build for OpenBSD (amd64, 386, arm64, arm, ppc64, riscv64)"
|
||||
@echo " build-netbsd - Build for NetBSD (amd64, 386, arm64, arm)"
|
||||
@echo " build-arm - Build for ARM architectures (arm, arm64)"
|
||||
@echo " build-riscv - Build for RISC-V architecture (riscv64)"
|
||||
@echo " build-all - Build for all platforms and architectures"
|
||||
@echo " run - Run reticulum binary"
|
||||
@echo " install - Install dependencies"
|
||||
217
README.md
217
README.md
@@ -1,28 +1,215 @@
|
||||
# Reticulum-Go
|
||||
|
||||
> [!WARNING]
|
||||
> This project is still work in progress. Currently not compatible with the Python version.
|
||||
[](https://git.quad4.io/Networks/Reticulum-Go/actions/workflows/revive.yml)
|
||||
[](https://git.quad4.io/Networks/Reticulum-Go/actions/workflows/build.yml)
|
||||
[](https://git.quad4.io/Networks/Reticulum-Go/actions/workflows/go-test.yml)
|
||||
[](https://git.quad4.io/Networks/Reticulum-Go/actions/workflows/gosec.yml)
|
||||
[](https://git.quad4.io/Networks/Reticulum-Go/actions/workflows/bearer.yml)
|
||||
|
||||
[Reticulum Network](https://github.com/markqvist/Reticulum) implementation in Go `1.24+`.
|
||||
A high-performance Go implementation of the [Reticulum Network Stack](https://github.com/markqvist/Reticulum)
|
||||
|
||||
Aiming to be fully compatible with the Python version.
|
||||
## Project Goals:
|
||||
|
||||
# Testing
|
||||
- **Full Protocol Compatibility**: Maintain complete interoperability with the Python reference implementation
|
||||
- **Cross-Platform Support**: Support for legacy and modern platforms across multiple architectures
|
||||
- **Performance**: Leverage Go's concurrency model and runtime for improved throughput and latency
|
||||
- **More Privacy and Security**: Additional privacy and security features beyond the base specification
|
||||
|
||||
```
|
||||
make install
|
||||
make build
|
||||
make run
|
||||
```
|
||||
## Prerequisites
|
||||
|
||||
## Linter
|
||||
- Go 1.24 or later
|
||||
- [Task](https://taskfile.dev/) for build automation
|
||||
|
||||
[Revive](https://github.com/mgechev/revive)
|
||||
Note: You may need to set `alias task='go-task'` in your shell configuration to use `task` instead of `go-task`.
|
||||
|
||||
### Nix
|
||||
|
||||
If you have Nix installed, you can use the development shell which automatically provides all dependencies including Task:
|
||||
|
||||
```bash
|
||||
revive -config revive.toml -formatter friendly ./pkg/* ./cmd/* ./internal/*
|
||||
nix develop
|
||||
```
|
||||
|
||||
## External Packages
|
||||
This will enter a development environment with Go and Task pre-configured.
|
||||
|
||||
- `golang.org/x/crypto` `v0.37.0` - Cryptographic primitives
|
||||
## Quick Start
|
||||
|
||||
### Building the Binary
|
||||
|
||||
```bash
|
||||
task build
|
||||
```
|
||||
|
||||
The compiled binary will be located in `bin/reticulum-go`.
|
||||
|
||||
### Running the Application
|
||||
|
||||
```bash
|
||||
task run
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
task test
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Code Quality
|
||||
|
||||
Format code:
|
||||
|
||||
```bash
|
||||
task fmt
|
||||
```
|
||||
|
||||
Run static analysis checks (formatting, vet, linting):
|
||||
|
||||
```bash
|
||||
task check
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
Run all tests:
|
||||
|
||||
```bash
|
||||
task test
|
||||
```
|
||||
|
||||
Run short tests only:
|
||||
|
||||
```bash
|
||||
task test-short
|
||||
```
|
||||
|
||||
Generate coverage report:
|
||||
|
||||
```bash
|
||||
task coverage
|
||||
```
|
||||
|
||||
### Benchmarking
|
||||
|
||||
Run benchmarks with standard GC:
|
||||
|
||||
```bash
|
||||
task bench
|
||||
```
|
||||
|
||||
Run benchmarks with experimental Green Tea GC:
|
||||
|
||||
```bash
|
||||
task bench-experimental
|
||||
```
|
||||
|
||||
Compare both GC implementations:
|
||||
|
||||
```bash
|
||||
task bench-compare
|
||||
```
|
||||
|
||||
## Tasks
|
||||
|
||||
The project uses [Task](https://taskfile.dev/) for all development and build operations.
|
||||
|
||||
```
|
||||
| Task | Description |
|
||||
|---------------------|------------------------------------------------------|
|
||||
| default | Show available tasks |
|
||||
| all | Clean, download dependencies, build and test |
|
||||
| build | Build release binary (stripped, static) |
|
||||
| debug | Build debug binary |
|
||||
| build-experimental | Build with experimental Green Tea GC (Go 1.25+) |
|
||||
| experimental | Alias for build-experimental |
|
||||
| release | Build stripped static binary for release |
|
||||
| fmt | Format Go code |
|
||||
| fmt-check | Check if code is formatted (CI-friendly) |
|
||||
| vet | Run go vet |
|
||||
| lint | Run revive linter |
|
||||
| scan | Run gosec security scanner |
|
||||
| check | Run fmt-check, vet, and lint |
|
||||
| bench | Run benchmarks with standard GC |
|
||||
| bench-experimental | Run benchmarks with experimental GC |
|
||||
| bench-compare | Run benchmarks with both GC settings |
|
||||
| clean | Remove build artifacts |
|
||||
| test | Run all tests |
|
||||
| test-short | Run short tests only |
|
||||
| test-race | Run tests with race detector |
|
||||
| coverage | Generate test coverage report |
|
||||
| checksum | Generate SHA256 checksum for binary |
|
||||
| deps | Download and verify dependencies |
|
||||
| mod-tidy | Tidy go.mod file |
|
||||
| mod-verify | Verify dependencies |
|
||||
| build-linux | Build for Linux (amd64, arm64, arm, riscv64) |
|
||||
| build-all | Build for all Linux architectures |
|
||||
| build-wasm | Build WebAssembly binary with standard Go compiler |
|
||||
| test-wasm | Run WebAssembly tests using Node.js |
|
||||
| run | Run with go run |
|
||||
| tinygo-build | Build binary with TinyGo compiler |
|
||||
| tinygo-wasm | Build WebAssembly binary with TinyGo |
|
||||
| install | Install dependencies |
|
||||
|
||||
example: task build
|
||||
```
|
||||
|
||||
## Cross-Platform Builds
|
||||
|
||||
### Linux Builds
|
||||
|
||||
Build for all Linux architectures:
|
||||
|
||||
```bash
|
||||
task build-all
|
||||
```
|
||||
|
||||
Build for specific Linux architecture:
|
||||
|
||||
```bash
|
||||
task build-linux
|
||||
```
|
||||
|
||||
## Embedded Systems and WebAssembly
|
||||
|
||||
For building for embedded systems, see the [tinygo branch](https://git.quad4.io/Networks/Reticulum-Go/src/branch/tinygo/). Requires TinyGo 0.37.0+.
|
||||
|
||||
Build WebAssembly binary with standard Go compiler:
|
||||
|
||||
```bash
|
||||
task build-wasm
|
||||
```
|
||||
|
||||
Run WebAssembly unit tests (requires Node.js):
|
||||
|
||||
```bash
|
||||
task test-wasm
|
||||
```
|
||||
|
||||
Build with TinyGo:
|
||||
|
||||
```bash
|
||||
task tinygo-build
|
||||
```
|
||||
|
||||
Build WebAssembly binary with TinyGo:
|
||||
|
||||
```bash
|
||||
task tinygo-wasm
|
||||
```
|
||||
|
||||
## Experimental Features
|
||||
|
||||
### Green Tea Garbage Collector
|
||||
|
||||
Build with experimental Green Tea GC (requires Go 1.25+):
|
||||
|
||||
```bash
|
||||
task build-experimental
|
||||
```
|
||||
|
||||
This enables the experimental garbage collector for performance evaluation and testing.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [0BSD](LICENSE) license.
|
||||
37
SECURITY.md
37
SECURITY.md
@@ -1,14 +1,13 @@
|
||||
# Security Policy
|
||||
|
||||
We use [Socket](https://socket.dev/), [Deepsource](https://deepsource.com/) and [gosec](https://github.com/securego/gosec) for this project.
|
||||
## Supply Chain Security
|
||||
|
||||
## 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.
|
||||
- All actions are pinned to a full-length commit hash and have been forked to my Gitea instance in https://git.quad4.io/actions
|
||||
- BOM generation using CycloneDX
|
||||
|
||||
## Cryptography Dependencies
|
||||
|
||||
- golang.org/x/crypto for core cryptographic primitives
|
||||
- golang.org/x/crypto `v0.46.0` for core cryptographic primitives
|
||||
- hkdf
|
||||
- curve25519
|
||||
|
||||
@@ -22,30 +21,4 @@ We are strict about the quality of the code and the contributors. Please read th
|
||||
|
||||
## 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/*`
|
||||
Refer to [https://quad4.io/security](https://quad4.io/security) for how to report vulnerabilities.
|
||||
174
TODO.md
174
TODO.md
@@ -1,167 +1,11 @@
|
||||
### Core Components (In Progress)
|
||||
Working on creating a project and issues to better track things. Check out https://git.quad4.io/Networks/Reticulum-Go/projects/2
|
||||
|
||||
Last Updated: 2025-04-18
|
||||
## Todo
|
||||
|
||||
- [x] Basic Configuration System
|
||||
- [x] Basic config structure
|
||||
- [x] Default settings
|
||||
- [x] Config file loading/saving
|
||||
- [x] Path management
|
||||
|
||||
- [x] Constants Definition (Testing required)
|
||||
- [x] Packet constants
|
||||
- [x] MTU constants
|
||||
- [x] Header types
|
||||
- [x] Additional protocol constants
|
||||
|
||||
- [x] Identity Management (Testing required)
|
||||
- [x] Identity creation
|
||||
- [x] Key pair generation
|
||||
- [x] Identity storage/recall
|
||||
- [x] Public key handling
|
||||
- [x] Signature verification
|
||||
- [x] Hash functions
|
||||
|
||||
- [x] Cryptographic Primitives (Testing required)
|
||||
- [x] Ed25519
|
||||
- [x] Curve25519
|
||||
- [x] AES-128-CBC
|
||||
- [ ] AES-256-CBC
|
||||
- [x] SHA-256
|
||||
- [x] HKDF
|
||||
- [x] Secure random number generation
|
||||
- [x] HMAC
|
||||
|
||||
- [x] Packet Handling (In Progress)
|
||||
- [x] Packet creation
|
||||
- [x] Packet validation
|
||||
- [x] Basic proof system
|
||||
- [x] Packet encryption/decryption
|
||||
- [x] Signature verification
|
||||
- [x] Announce packet structure
|
||||
- [ ] Testing of packet encrypt/decrypt/sign/proof
|
||||
- [ ] Cross-client packet compatibility
|
||||
|
||||
- [x] Transport Layer (In Progress)
|
||||
- [x] Path management
|
||||
- [x] Basic packet routing
|
||||
- [x] Announce handling
|
||||
- [x] Link management
|
||||
- [x] Resource cleanup
|
||||
- [x] Network layer integration
|
||||
- [x] Basic announce implementation
|
||||
- [ ] Testing announce from go client to python client
|
||||
- [ ] Testing path finding and caching
|
||||
- [ ] Announce propagation optimization
|
||||
|
||||
- [x] Channel System (Testing Required)
|
||||
- [x] Channel creation and management
|
||||
- [x] Message handling
|
||||
- [x] Channel encryption
|
||||
- [x] Channel authentication
|
||||
- [x] Channel callbacks
|
||||
- [x] Integration with Buffer system
|
||||
- [ ] Testing with real network conditions
|
||||
- [ ] Cross-client compatibility testing
|
||||
|
||||
- [x] Buffer System (Testing Required)
|
||||
- [x] Raw channel reader/writer
|
||||
- [x] Buffered stream implementation
|
||||
- [x] Compression support
|
||||
- [ ] Testing with Channel system
|
||||
- [ ] Cross-client compatibility testing
|
||||
|
||||
- [x] Resolver System (Testing Required)
|
||||
- [x] Name resolution
|
||||
- [x] Cache management
|
||||
- [x] Announce handling
|
||||
- [x] Path resolution
|
||||
- [x] Integration with Transport layer
|
||||
- [ ] Testing with live network
|
||||
- [ ] Cross-client compatibility testing
|
||||
|
||||
### Interface Implementation (In Progress)
|
||||
- [x] UDP Interface
|
||||
- [x] TCP Interface
|
||||
- [x] Auto Interface
|
||||
- [ ] Local Interface (In Progress)
|
||||
- [ ] I2P Interface
|
||||
- [ ] Pipe Interface
|
||||
- [ ] RNode Interface
|
||||
- [ ] RNode Multiinterface
|
||||
- [ ] Serial Interface
|
||||
- [ ] AX25KISS Interface
|
||||
- [ ] Interface Discovery
|
||||
- [ ] Interface Modes
|
||||
- [ ] Full mode
|
||||
- [ ] Gateway mode
|
||||
- [ ] Access point mode
|
||||
- [ ] Roaming mode
|
||||
- [ ] Boundary mode
|
||||
|
||||
- [ ] Hot reloading interfaces
|
||||
|
||||
### Destination System (Testing required)
|
||||
- [x] Destination creation
|
||||
- [x] Destination types (IN/OUT)
|
||||
- [x] Destination aspects
|
||||
- [ ] Announce implementation (Fixing)
|
||||
- [x] Ratchet support
|
||||
- [x] Request handlers
|
||||
|
||||
### Link System (Testing required)
|
||||
- [x] Link establishment
|
||||
- [x] Link teardown
|
||||
- [x] Basic packet transfer
|
||||
- [x] Encryption/Decryption
|
||||
- [x] Identity verification
|
||||
- [x] Request/Response handling
|
||||
- [x] Session key management
|
||||
- [x] Link state tracking
|
||||
|
||||
### Resource System (Testing required)
|
||||
- [x] Resource creation
|
||||
- [x] Resource transfer
|
||||
- [x] Compression
|
||||
- [x] Progress tracking
|
||||
- [x] Segmentation
|
||||
- [x] Cleanup routines
|
||||
|
||||
### Compatibility
|
||||
- [ ] RNS Utilities.
|
||||
- [ ] Reticulum config.
|
||||
|
||||
|
||||
### Testing & Validation (Priority)
|
||||
- [ ] Unit tests for all components
|
||||
- [ ] Identity tests
|
||||
- [ ] Packet tests
|
||||
- [ ] Transport tests
|
||||
- [ ] Interface tests
|
||||
- [ ] Announce tests
|
||||
- [ ] Channel tests
|
||||
- [ ] Buffer tests
|
||||
- [ ] Resolver tests
|
||||
- [ ] Link tests
|
||||
- [ ] Resource tests
|
||||
- [ ] Integration tests
|
||||
- [ ] Go client to Go client
|
||||
- [ ] Go client to Python client
|
||||
- [ ] Interface compatibility
|
||||
- [ ] Path finding and resolution
|
||||
- [ ] Channel system end-to-end
|
||||
- [ ] Buffer system performance
|
||||
- [ ] Cross-client compatibility tests
|
||||
- [ ] Performance benchmarks
|
||||
- [ ] Security auditing (When Reticulum is 1.0 / stable)
|
||||
|
||||
### Documentation
|
||||
- [ ] API documentation
|
||||
- [ ] Usage examples
|
||||
|
||||
### Cleanup
|
||||
- [ ] Separate Cryptography from identity.go to their own files
|
||||
- [ ] Move constants to their own files
|
||||
- [ ] Remove default community interfaces in default config creation after testing
|
||||
- [ ] Optimize announce packet creation and caching
|
||||
- [ ] Improve debug logging system
|
||||
- Created dedicated constants.go for each section.
|
||||
- Link Request/Response System (in-progress)
|
||||
- Resource Transfer System (in-progress)
|
||||
- Link Keep-Alive & Timeout (in-progress)
|
||||
- Examples (in-progress)
|
||||
- Tests
|
||||
- Documentation
|
||||
683
Taskfile.yml
Normal file
683
Taskfile.yml
Normal file
@@ -0,0 +1,683 @@
|
||||
version: '3'
|
||||
env:
|
||||
GOPRIVATE: git.quad4.io
|
||||
|
||||
vars:
|
||||
GOCMD: go
|
||||
BINARY_NAME: reticulum-go
|
||||
BUILD_DIR: bin
|
||||
MAIN_PACKAGE: ./cmd/reticulum-go
|
||||
|
||||
tasks:
|
||||
default:
|
||||
desc: Show available tasks
|
||||
cmds:
|
||||
- task --list
|
||||
|
||||
all:
|
||||
desc: Clean, download dependencies, build and test
|
||||
deps: [clean, deps, build, test]
|
||||
|
||||
build:
|
||||
desc: Build release binary (no debug symbols, static)
|
||||
env:
|
||||
CGO_ENABLED: '0'
|
||||
cmds:
|
||||
- mkdir -p {{.BUILD_DIR}}
|
||||
- '{{.GOCMD}} build -ldflags="-s -w" -o {{.BUILD_DIR}}/{{.BINARY_NAME}} {{.MAIN_PACKAGE}}'
|
||||
|
||||
debug:
|
||||
desc: Build debug binary
|
||||
cmds:
|
||||
- mkdir -p {{.BUILD_DIR}}
|
||||
- '{{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}} {{.MAIN_PACKAGE}}'
|
||||
|
||||
build-experimental:
|
||||
desc: Build binary with experimental features (GOEXPERIMENT=greenteagc)
|
||||
env:
|
||||
GOEXPERIMENT: greenteagc
|
||||
cmds:
|
||||
- mkdir -p {{.BUILD_DIR}}
|
||||
- '{{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-experimental {{.MAIN_PACKAGE}}'
|
||||
|
||||
experimental:
|
||||
desc: Alias for build-experimental
|
||||
cmds:
|
||||
- task: build-experimental
|
||||
|
||||
release:
|
||||
desc: Build stripped static binary for release (alias for build)
|
||||
cmds:
|
||||
- task: build
|
||||
|
||||
fmt:
|
||||
desc: Format Go code
|
||||
cmds:
|
||||
- '{{.GOCMD}} fmt ./...'
|
||||
|
||||
fmt-check:
|
||||
desc: Check if code is formatted (useful for CI)
|
||||
cmds:
|
||||
- '{{.GOCMD}} fmt -d ./... > fmt.diff 2>&1 || true'
|
||||
- 'test -s fmt.diff && (echo "Code is not formatted. Run ''task fmt'' to fix." && cat fmt.diff && rm -f fmt.diff && exit 1) || (rm -f fmt.diff && exit 0)'
|
||||
|
||||
vet:
|
||||
desc: Run go vet
|
||||
cmds:
|
||||
- '{{.GOCMD}} vet ./...'
|
||||
|
||||
lint:
|
||||
desc: Run revive linter
|
||||
cmds:
|
||||
- revive -config revive.toml -formatter friendly ./pkg/* ./cmd/* ./internal/*
|
||||
|
||||
scan:
|
||||
desc: Run gosec security scanner
|
||||
cmds:
|
||||
- gosec ./...
|
||||
|
||||
check:
|
||||
desc: Run fmt-check, vet, lint, test-short, and scan with summary
|
||||
cmds:
|
||||
- |
|
||||
FAILED_TASKS=""
|
||||
FAIL_COUNT=0
|
||||
TOTAL_TASKS=5
|
||||
|
||||
echo "--- Running all checks ---"
|
||||
task fmt-check || { FAILED_TASKS="$FAILED_TASKS fmt-check"; FAIL_COUNT=$((FAIL_COUNT + 1)); }
|
||||
task vet || { FAILED_TASKS="$FAILED_TASKS vet"; FAIL_COUNT=$((FAIL_COUNT + 1)); }
|
||||
task lint || { FAILED_TASKS="$FAILED_TASKS lint"; FAIL_COUNT=$((FAIL_COUNT + 1)); }
|
||||
task test-short || { FAILED_TASKS="$FAILED_TASKS test-short"; FAIL_COUNT=$((FAIL_COUNT + 1)); }
|
||||
task scan || { FAILED_TASKS="$FAILED_TASKS scan"; FAIL_COUNT=$((FAIL_COUNT + 1)); }
|
||||
|
||||
echo "------------------------------------------"
|
||||
if [ $FAIL_COUNT -eq 0 ]; then
|
||||
echo "OK: All checks passed!"
|
||||
elif [ $FAIL_COUNT -eq $TOTAL_TASKS ]; then
|
||||
echo "ERROR: All tasks failed!"
|
||||
echo "Failed tasks:$FAILED_TASKS"
|
||||
exit 1
|
||||
else
|
||||
echo "ERROR: $FAIL_COUNT task(s) failed out of $TOTAL_TASKS!"
|
||||
echo "Failed tasks:$FAILED_TASKS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bench:
|
||||
desc: Run benchmarks with standard GC
|
||||
cmds:
|
||||
- '{{.GOCMD}} test -bench=. -benchmem ./...'
|
||||
|
||||
bench-experimental:
|
||||
desc: Run benchmarks with experimental GC
|
||||
env:
|
||||
GOEXPERIMENT: greenteagc
|
||||
cmds:
|
||||
- '{{.GOCMD}} test -bench=. -benchmem ./...'
|
||||
|
||||
bench-compare:
|
||||
desc: Run benchmarks with both GC settings
|
||||
deps: [bench, bench-experimental]
|
||||
|
||||
clean:
|
||||
desc: Remove build artifacts
|
||||
cmds:
|
||||
- '{{.GOCMD}} clean'
|
||||
- rm -rf {{.BUILD_DIR}}
|
||||
|
||||
test:
|
||||
desc: Run tests
|
||||
cmds:
|
||||
- '{{.GOCMD}} test -v ./...'
|
||||
|
||||
test-short:
|
||||
desc: Run short tests
|
||||
cmds:
|
||||
- '{{.GOCMD}} test -short -v ./...'
|
||||
|
||||
test-race:
|
||||
desc: Run tests with race detector
|
||||
cmds:
|
||||
- '{{.GOCMD}} test -race -v ./...'
|
||||
|
||||
coverage:
|
||||
desc: Generate test coverage report
|
||||
cmds:
|
||||
- '{{.GOCMD}} test -coverprofile=coverage.out ./...'
|
||||
- '{{.GOCMD}} tool cover -html=coverage.out'
|
||||
|
||||
deps:
|
||||
desc: Download and verify dependencies
|
||||
env:
|
||||
GOPROXY: '{{.GOPROXY | default "https://proxy.golang.org,direct"}}'
|
||||
cmds:
|
||||
- '{{.GOCMD}} mod download'
|
||||
- '{{.GOCMD}} mod verify'
|
||||
|
||||
mod-tidy:
|
||||
desc: Tidy go.mod file
|
||||
cmds:
|
||||
- '{{.GOCMD}} mod tidy'
|
||||
|
||||
mod-verify:
|
||||
desc: Verify dependencies
|
||||
cmds:
|
||||
- '{{.GOCMD}} mod verify'
|
||||
|
||||
build-linux:
|
||||
desc: Build for Linux (amd64, arm64, arm, riscv64)
|
||||
env:
|
||||
CGO_ENABLED: '0'
|
||||
cmds:
|
||||
- mkdir -p {{.BUILD_DIR}}
|
||||
- 'GOOS=linux GOARCH=amd64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-linux-amd64 {{.MAIN_PACKAGE}}'
|
||||
- 'GOOS=linux GOARCH=arm64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-linux-arm64 {{.MAIN_PACKAGE}}'
|
||||
- 'GOOS=linux GOARCH=arm {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-linux-arm {{.MAIN_PACKAGE}}'
|
||||
- 'GOOS=linux GOARCH=riscv64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-linux-riscv64 {{.MAIN_PACKAGE}}'
|
||||
|
||||
build-windows:
|
||||
desc: Build for Windows (amd64, arm64)
|
||||
env:
|
||||
CGO_ENABLED: '0'
|
||||
cmds:
|
||||
- mkdir -p {{.BUILD_DIR}}
|
||||
- 'GOOS=windows GOARCH=amd64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-windows-amd64.exe {{.MAIN_PACKAGE}}'
|
||||
- 'GOOS=windows GOARCH=arm64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-windows-arm64.exe {{.MAIN_PACKAGE}}'
|
||||
|
||||
build-darwin:
|
||||
desc: Build for MacOS (amd64, arm64)
|
||||
env:
|
||||
CGO_ENABLED: '0'
|
||||
cmds:
|
||||
- mkdir -p {{.BUILD_DIR}}
|
||||
- 'GOOS=darwin GOARCH=amd64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-darwin-amd64 {{.MAIN_PACKAGE}}'
|
||||
- 'GOOS=darwin GOARCH=arm64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-darwin-arm64 {{.MAIN_PACKAGE}}'
|
||||
|
||||
build-freebsd:
|
||||
desc: Build for FreeBSD (amd64, 386, arm64, arm, riscv64)
|
||||
env:
|
||||
CGO_ENABLED: '0'
|
||||
cmds:
|
||||
- mkdir -p {{.BUILD_DIR}}
|
||||
- 'GOOS=freebsd GOARCH=amd64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-freebsd-amd64 {{.MAIN_PACKAGE}}'
|
||||
- 'GOOS=freebsd GOARCH=386 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-freebsd-386 {{.MAIN_PACKAGE}}'
|
||||
- 'GOOS=freebsd GOARCH=arm64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-freebsd-arm64 {{.MAIN_PACKAGE}}'
|
||||
- 'GOOS=freebsd GOARCH=arm {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-freebsd-arm {{.MAIN_PACKAGE}}'
|
||||
- 'GOOS=freebsd GOARCH=riscv64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-freebsd-riscv64 {{.MAIN_PACKAGE}}'
|
||||
|
||||
build-openbsd:
|
||||
desc: Build for OpenBSD (amd64, 386, arm64, arm, ppc64, riscv64)
|
||||
env:
|
||||
CGO_ENABLED: '0'
|
||||
cmds:
|
||||
- mkdir -p {{.BUILD_DIR}}
|
||||
- 'GOOS=openbsd GOARCH=amd64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-openbsd-amd64 {{.MAIN_PACKAGE}}'
|
||||
- 'GOOS=openbsd GOARCH=386 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-openbsd-386 {{.MAIN_PACKAGE}}'
|
||||
- 'GOOS=openbsd GOARCH=arm64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-openbsd-arm64 {{.MAIN_PACKAGE}}'
|
||||
- 'GOOS=openbsd GOARCH=arm {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-openbsd-arm {{.MAIN_PACKAGE}}'
|
||||
- 'GOOS=openbsd GOARCH=ppc64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-openbsd-ppc64 {{.MAIN_PACKAGE}}'
|
||||
- 'GOOS=openbsd GOARCH=riscv64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-openbsd-riscv64 {{.MAIN_PACKAGE}}'
|
||||
|
||||
build-netbsd:
|
||||
desc: Build for NetBSD (amd64, 386, arm64, arm)
|
||||
env:
|
||||
CGO_ENABLED: '0'
|
||||
cmds:
|
||||
- mkdir -p {{.BUILD_DIR}}
|
||||
- 'GOOS=netbsd GOARCH=amd64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-netbsd-amd64 {{.MAIN_PACKAGE}}'
|
||||
- 'GOOS=netbsd GOARCH=386 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-netbsd-386 {{.MAIN_PACKAGE}}'
|
||||
- 'GOOS=netbsd GOARCH=arm64 {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-netbsd-arm64 {{.MAIN_PACKAGE}}'
|
||||
- 'GOOS=netbsd GOARCH=arm {{.GOCMD}} build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-netbsd-arm {{.MAIN_PACKAGE}}'
|
||||
|
||||
build-all:
|
||||
desc: Build for all platforms and architectures
|
||||
deps: [build-linux, build-windows, build-darwin, build-freebsd, build-openbsd, build-netbsd]
|
||||
|
||||
run:
|
||||
desc: Run with go run
|
||||
cmds:
|
||||
- '{{.GOCMD}} run {{.MAIN_PACKAGE}}'
|
||||
|
||||
tinygo-build:
|
||||
desc: Build binary with TinyGo compiler
|
||||
cmds:
|
||||
- mkdir -p {{.BUILD_DIR}}
|
||||
- tinygo build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-tinygo -size short {{.MAIN_PACKAGE}}
|
||||
|
||||
tinygo-wasm:
|
||||
desc: Build WebAssembly binary with TinyGo compiler
|
||||
cmds:
|
||||
- mkdir -p {{.BUILD_DIR}}
|
||||
- tinygo build -target wasm -o {{.BUILD_DIR}}/{{.BINARY_NAME}}.wasm ./cmd/reticulum-wasm
|
||||
|
||||
test-wasm:
|
||||
desc: Run WebAssembly tests using Node.js
|
||||
vars:
|
||||
ROOT_DIR:
|
||||
sh: pwd
|
||||
env:
|
||||
GOOS: js
|
||||
GOARCH: wasm
|
||||
cmds:
|
||||
- chmod +x {{.ROOT_DIR}}/misc/wasm/go_js_wasm_exec
|
||||
- PATH="$PATH:{{.ROOT_DIR}}/misc/wasm" {{.GOCMD}} test -v ./pkg/wasm/ ./cmd/reticulum-wasm/
|
||||
- |
|
||||
export PATH="$PATH:{{.ROOT_DIR}}/misc/wasm"
|
||||
cd examples/wasm && {{.GOCMD}} test -v .
|
||||
|
||||
build-wasm:
|
||||
desc: Build WebAssembly binary with standard Go compiler
|
||||
env:
|
||||
CGO_ENABLED: '0'
|
||||
GOOS: js
|
||||
GOARCH: wasm
|
||||
cmds:
|
||||
- mkdir -p {{.BUILD_DIR}}
|
||||
- '{{.GOCMD}} build -ldflags="-s -w" -o {{.BUILD_DIR}}/{{.BINARY_NAME}}.wasm ./cmd/reticulum-wasm'
|
||||
|
||||
example:wasm:build:
|
||||
desc: Build WebAssembly example
|
||||
env:
|
||||
CGO_ENABLED: '0'
|
||||
cmds:
|
||||
- mkdir -p examples/wasm/public/static examples/wasm/public/js
|
||||
- 'cd examples/wasm && GOOS=js GOARCH=wasm {{.GOCMD}} build -o public/static/reticulum-go.wasm .'
|
||||
- |
|
||||
GOROOT=$({{.GOCMD}} env GOROOT)
|
||||
if [ -f "$GOROOT/lib/wasm/wasm_exec.js" ]; then
|
||||
cp "$GOROOT/lib/wasm/wasm_exec.js" examples/wasm/public/js/
|
||||
echo "wasm_exec.js copied successfully from $GOROOT/lib/wasm/"
|
||||
elif [ -f "$GOROOT/misc/wasm/wasm_exec.js" ]; then
|
||||
cp "$GOROOT/misc/wasm/wasm_exec.js" examples/wasm/public/js/
|
||||
echo "wasm_exec.js copied successfully from $GOROOT/misc/wasm/"
|
||||
else
|
||||
echo "Warning: wasm_exec.js not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
example:wasm:run:
|
||||
desc: Run WebAssembly example using a simple HTTP server
|
||||
deps: [example:wasm:build]
|
||||
cmds:
|
||||
- echo "Starting server at http://localhost:8080"
|
||||
- echo "Press Ctrl+C to stop"
|
||||
- 'cd examples/wasm/public && python3 -m http.server 8080'
|
||||
|
||||
example:wasm:test:
|
||||
desc: Run tests for WASM example
|
||||
cmds:
|
||||
- task: test-wasm
|
||||
|
||||
install:
|
||||
desc: Install dependencies
|
||||
cmds:
|
||||
- '{{.GOCMD}} mod download'
|
||||
|
||||
checksum:
|
||||
desc: Generate SHA256 checksum for binary (uses BINARY_PATH env var if set, otherwise defaults to bin/reticulum-go)
|
||||
cmds:
|
||||
- |
|
||||
BINARY_PATH="${BINARY_PATH:-{{.BUILD_DIR}}/{{.BINARY_NAME}}}"
|
||||
if [ -f "$BINARY_PATH" ]; then
|
||||
sha256sum "$BINARY_PATH" > "${BINARY_PATH}.sha256"
|
||||
echo "Generated checksum: ${BINARY_PATH}.sha256"
|
||||
else
|
||||
echo "Error: Binary not found at $BINARY_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
example:announce:
|
||||
desc: Run announce example
|
||||
cmds:
|
||||
- 'cd examples/announce && {{.GOCMD}} run .'
|
||||
|
||||
example:minimal:
|
||||
desc: Run minimal example
|
||||
cmds:
|
||||
- 'cd examples/minimal && {{.GOCMD}} run .'
|
||||
|
||||
example:pageserver:
|
||||
desc: Run pageserver example
|
||||
cmds:
|
||||
- 'cd examples/pageserver && {{.GOCMD}} run .'
|
||||
|
||||
example:echo-listen:
|
||||
desc: Run echo example (waits for incoming connections, P2P peer)
|
||||
cmds:
|
||||
- 'cd examples/echo && {{.GOCMD}} run . --server'
|
||||
|
||||
example:echo-connect:
|
||||
desc: Run echo example (initiates connection to peer, requires DESTINATION env var)
|
||||
cmds:
|
||||
- |
|
||||
if [ -z "${DESTINATION}" ]; then
|
||||
echo "Error: DESTINATION environment variable required (hexadecimal hash of peer)"
|
||||
echo "Example: DESTINATION=abc123... task example:echo-connect"
|
||||
exit 1
|
||||
fi
|
||||
cd examples/echo && {{.GOCMD}} run . --destination="${DESTINATION}"
|
||||
|
||||
example:link-listen:
|
||||
desc: Run link example (waits for incoming link requests, P2P peer)
|
||||
cmds:
|
||||
- 'cd examples/link && {{.GOCMD}} run . --server'
|
||||
|
||||
example:link-connect:
|
||||
desc: Run link example (initiates link to peer, requires DESTINATION env var)
|
||||
cmds:
|
||||
- |
|
||||
if [ -z "${DESTINATION}" ]; then
|
||||
echo "Error: DESTINATION environment variable required (hexadecimal hash of peer)"
|
||||
echo "Example: DESTINATION=abc123... task example:link-connect"
|
||||
exit 1
|
||||
fi
|
||||
cd examples/link && {{.GOCMD}} run . --destination="${DESTINATION}"
|
||||
|
||||
example:filetransfer-share:
|
||||
desc: Run filetransfer example (shares files from directory, P2P peer)
|
||||
cmds:
|
||||
- |
|
||||
if [ -z "${SERVE_PATH}" ]; then
|
||||
echo "Error: SERVE_PATH environment variable required (directory to share)"
|
||||
echo "Example: SERVE_PATH=/path/to/files task example:filetransfer-share"
|
||||
exit 1
|
||||
fi
|
||||
cd examples/filetransfer && {{.GOCMD}} run . --server --serve="${SERVE_PATH}"
|
||||
|
||||
example:filetransfer-fetch:
|
||||
desc: Run filetransfer example (fetches files from peer, requires DESTINATION env var)
|
||||
cmds:
|
||||
- |
|
||||
if [ -z "${DESTINATION}" ]; then
|
||||
echo "Error: DESTINATION environment variable required (hexadecimal hash of peer)"
|
||||
echo "Example: DESTINATION=abc123... task example:filetransfer-fetch"
|
||||
exit 1
|
||||
fi
|
||||
cd examples/filetransfer && {{.GOCMD}} run . --destination="${DESTINATION}"
|
||||
|
||||
trivy:install:
|
||||
desc: Install Trivy scanner
|
||||
cmds:
|
||||
- |
|
||||
if ! command -v trivy &> /dev/null; then
|
||||
curl -L -o /tmp/trivy.deb https://git.quad4.io/Quad4-Extra/assets/raw/commit/90fdcea1bb71d91df2de6ff2e3897f278413f300/bin/trivy_0.68.2_Linux-64bit.deb
|
||||
sudo dpkg -i /tmp/trivy.deb || sudo apt-get install -f -y
|
||||
else
|
||||
echo "Trivy is already installed: $(trivy --version)"
|
||||
fi
|
||||
|
||||
trivy:scan:
|
||||
desc: Run Trivy vulnerability scan
|
||||
cmds:
|
||||
- |
|
||||
if ! command -v trivy &> /dev/null; then
|
||||
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||
exit 1
|
||||
fi
|
||||
trivy fs --scanners vuln --severity HIGH,CRITICAL --timeout 90m .
|
||||
|
||||
trivy:scan-all:
|
||||
desc: Run Trivy full scan (vulnerabilities, secrets, misconfig)
|
||||
cmds:
|
||||
- |
|
||||
if ! command -v trivy &> /dev/null; then
|
||||
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||
exit 1
|
||||
fi
|
||||
trivy fs --scanners vuln,secret,misconfig .
|
||||
|
||||
sbom:
|
||||
desc: Generate SBOM files (SPDX and CycloneDX formats)
|
||||
cmds:
|
||||
- |
|
||||
if ! command -v trivy &> /dev/null; then
|
||||
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p sbom
|
||||
trivy fs --format spdx-json --include-dev-deps --output sbom/sbom.spdx.json .
|
||||
trivy fs --format cyclonedx --include-dev-deps --output sbom/sbom.cyclonedx.json .
|
||||
echo "SBOM files generated in sbom/ directory"
|
||||
|
||||
sbom:spdx:
|
||||
desc: Generate SPDX JSON SBOM
|
||||
cmds:
|
||||
- |
|
||||
if ! command -v trivy &> /dev/null; then
|
||||
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p sbom
|
||||
trivy fs --format spdx-json --include-dev-deps --output sbom/sbom.spdx.json .
|
||||
echo "SPDX SBOM generated: sbom/sbom.spdx.json"
|
||||
|
||||
sbom:cyclonedx:
|
||||
desc: Generate CycloneDX SBOM
|
||||
cmds:
|
||||
- |
|
||||
if ! command -v trivy &> /dev/null; then
|
||||
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p sbom
|
||||
trivy fs --format cyclonedx --include-dev-deps --output sbom/sbom.cyclonedx.json .
|
||||
echo "CycloneDX SBOM generated: sbom/sbom.cyclonedx.json"
|
||||
|
||||
trivy:scan:json:
|
||||
desc: Run Trivy vulnerability scan with JSON output
|
||||
cmds:
|
||||
- |
|
||||
if ! command -v trivy &> /dev/null; then
|
||||
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p reports
|
||||
trivy fs --scanners vuln --format json --output reports/trivy-vuln.json --timeout 90m .
|
||||
|
||||
trivy:scan:sarif:
|
||||
desc: Run Trivy scan with SARIF output (for GitHub/GitLab integration)
|
||||
cmds:
|
||||
- |
|
||||
if ! command -v trivy &> /dev/null; then
|
||||
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p reports
|
||||
trivy fs --scanners vuln,secret --format sarif --output reports/trivy.sarif --timeout 90m .
|
||||
|
||||
trivy:scan:secrets:
|
||||
desc: Scan for hardcoded secrets
|
||||
cmds:
|
||||
- |
|
||||
if ! command -v trivy &> /dev/null; then
|
||||
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||
exit 1
|
||||
fi
|
||||
trivy fs --scanners secret .
|
||||
|
||||
trivy:scan:licenses:
|
||||
desc: Scan for licenses in dependencies
|
||||
cmds:
|
||||
- |
|
||||
if ! command -v trivy &> /dev/null; then
|
||||
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||
exit 1
|
||||
fi
|
||||
trivy fs --scanners license .
|
||||
|
||||
trivy:scan:misconfig:
|
||||
desc: Scan for misconfigurations in config files
|
||||
cmds:
|
||||
- |
|
||||
if ! command -v trivy &> /dev/null; then
|
||||
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||
exit 1
|
||||
fi
|
||||
trivy fs --scanners misconfig .
|
||||
|
||||
trivy:db-update:
|
||||
desc: Update Trivy vulnerability database
|
||||
cmds:
|
||||
- |
|
||||
if ! command -v trivy &> /dev/null; then
|
||||
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||
exit 1
|
||||
fi
|
||||
trivy image --download-db-only
|
||||
|
||||
trivy:cache-clean:
|
||||
desc: Clean Trivy cache
|
||||
cmds:
|
||||
- |
|
||||
if ! command -v trivy &> /dev/null; then
|
||||
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||
exit 1
|
||||
fi
|
||||
trivy clean --cache
|
||||
|
||||
trivy:compliance:
|
||||
desc: "Generate compliance report (specify COMPLIANCE env var: docker-bench-cis, k8s-nsa, etc.)"
|
||||
cmds:
|
||||
- |
|
||||
if ! command -v trivy &> /dev/null; then
|
||||
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${COMPLIANCE}" ]; then
|
||||
echo "Error: COMPLIANCE environment variable required"
|
||||
echo "Example: COMPLIANCE=docker-bench-cis task trivy:compliance"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p reports
|
||||
trivy fs --compliance "${COMPLIANCE}" --format json --output "reports/compliance-${COMPLIANCE}.json" .
|
||||
|
||||
trivy:ci:
|
||||
desc: Run Trivy scan for CI (exits with non-zero code on findings)
|
||||
cmds:
|
||||
- |
|
||||
if ! command -v trivy &> /dev/null; then
|
||||
echo "Error: Trivy not found. Run 'task trivy:install' first."
|
||||
exit 1
|
||||
fi
|
||||
trivy fs --scanners vuln --severity HIGH,CRITICAL --exit-code 1 --timeout 90m .
|
||||
|
||||
docker:build:
|
||||
desc: Build Docker image (runtime image)
|
||||
vars:
|
||||
IMAGE_NAME: reticulum-go
|
||||
IMAGE_TAG: latest
|
||||
cmds:
|
||||
- docker build -f docker/Dockerfile -t {{.IMAGE_NAME}}:{{.IMAGE_TAG}} .
|
||||
|
||||
docker:build:tag:
|
||||
desc: Build Docker image with custom tag (use IMAGE_TAG env var)
|
||||
vars:
|
||||
IMAGE_NAME: reticulum-go
|
||||
IMAGE_TAG: ${IMAGE_TAG:-latest}
|
||||
cmds:
|
||||
- docker build -f docker/Dockerfile -t {{.IMAGE_NAME}}:{{.IMAGE_TAG}} .
|
||||
|
||||
docker:build:build:
|
||||
desc: Build Docker image for building binaries only
|
||||
vars:
|
||||
IMAGE_NAME: reticulum-go-build
|
||||
IMAGE_TAG: latest
|
||||
cmds:
|
||||
- docker build -f docker/Dockerfile.build -t {{.IMAGE_NAME}}:{{.IMAGE_TAG}} .
|
||||
|
||||
docker:run:
|
||||
desc: Run Docker container (runtime image)
|
||||
vars:
|
||||
IMAGE_NAME: reticulum-go
|
||||
IMAGE_TAG: latest
|
||||
CONTAINER_NAME: reticulum-go
|
||||
cmds:
|
||||
- |
|
||||
docker run --rm -it \
|
||||
--name {{.CONTAINER_NAME}} \
|
||||
-p 4242:4242 \
|
||||
{{.IMAGE_NAME}}:{{.IMAGE_TAG}}
|
||||
|
||||
docker:run:detached:
|
||||
desc: Run Docker container in detached mode
|
||||
vars:
|
||||
IMAGE_NAME: reticulum-go
|
||||
IMAGE_TAG: latest
|
||||
CONTAINER_NAME: reticulum-go
|
||||
cmds:
|
||||
- |
|
||||
docker run -d \
|
||||
--name {{.CONTAINER_NAME}} \
|
||||
-p 4242:4242 \
|
||||
{{.IMAGE_NAME}}:{{.IMAGE_TAG}}
|
||||
|
||||
docker:stop:
|
||||
desc: Stop running Docker container
|
||||
vars:
|
||||
CONTAINER_NAME: reticulum-go
|
||||
cmds:
|
||||
- docker stop {{.CONTAINER_NAME}} || true
|
||||
- docker rm {{.CONTAINER_NAME}} || true
|
||||
|
||||
docker:extract:
|
||||
desc: Extract binary from build container
|
||||
vars:
|
||||
IMAGE_NAME: reticulum-go-build
|
||||
IMAGE_TAG: latest
|
||||
BINARY_NAME: reticulum-go
|
||||
cmds:
|
||||
- |
|
||||
CONTAINER_ID=$(docker create {{.IMAGE_NAME}}:{{.IMAGE_TAG}})
|
||||
docker cp $CONTAINER_ID:/dist/{{.BINARY_NAME}} {{.BUILD_DIR}}/{{.BINARY_NAME}}
|
||||
docker rm $CONTAINER_ID
|
||||
echo "Binary extracted to {{.BUILD_DIR}}/{{.BINARY_NAME}}"
|
||||
|
||||
docker:buildx:setup:
|
||||
desc: Setup Docker buildx for multi-platform builds
|
||||
cmds:
|
||||
- docker buildx create --name reticulum-builder --use || docker buildx use reticulum-builder
|
||||
- docker buildx inspect --bootstrap
|
||||
|
||||
docker:buildx:build:
|
||||
desc: Build multi-platform Docker image
|
||||
vars:
|
||||
IMAGE_NAME: reticulum-go
|
||||
IMAGE_TAG: latest
|
||||
PLATFORMS: linux/amd64,linux/arm64,linux/arm/v7
|
||||
cmds:
|
||||
- |
|
||||
docker buildx build \
|
||||
--platform {{.PLATFORMS}} \
|
||||
-f docker/Dockerfile \
|
||||
-t {{.IMAGE_NAME}}:{{.IMAGE_TAG}} \
|
||||
--load \
|
||||
.
|
||||
|
||||
docker:buildx:build:push:
|
||||
desc: Build and push multi-platform Docker image
|
||||
vars:
|
||||
IMAGE_NAME: reticulum-go
|
||||
IMAGE_TAG: latest
|
||||
PLATFORMS: linux/amd64,linux/arm64,linux/arm/v7
|
||||
cmds:
|
||||
- |
|
||||
if [ -z "${DOCKER_REGISTRY}" ]; then
|
||||
echo "Error: DOCKER_REGISTRY environment variable required"
|
||||
echo "Example: DOCKER_REGISTRY=registry.example.com task docker:buildx:build:push"
|
||||
exit 1
|
||||
fi
|
||||
docker buildx build \
|
||||
--platform {{.PLATFORMS}} \
|
||||
-f docker/Dockerfile \
|
||||
-t ${DOCKER_REGISTRY}/{{.IMAGE_NAME}}:{{.IMAGE_TAG}} \
|
||||
--push \
|
||||
.
|
||||
|
||||
docker:clean:
|
||||
desc: Clean Docker images and containers
|
||||
cmds:
|
||||
- docker stop reticulum-go || true
|
||||
- docker rm reticulum-go || true
|
||||
- docker rmi reticulum-go:latest || true
|
||||
- docker rmi reticulum-go-build:latest || true
|
||||
@@ -1,54 +1,43 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/internal/config"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/announce"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/buffer"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/channel"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/destination"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/interfaces"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/packet"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
||||
"git.quad4.io/Networks/Reticulum-Go/internal/config"
|
||||
"git.quad4.io/Networks/Reticulum-Go/internal/storage"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/buffer"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/channel"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/destination"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/interfaces"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/transport"
|
||||
)
|
||||
|
||||
var (
|
||||
debugLevel = flag.Int("debug", 7, "Debug level (0-7)")
|
||||
interceptPackets = flag.Bool("intercept-packets", false, "Enable packet interception")
|
||||
interceptOutput = flag.String("intercept-output", "packets.log", "Output file for intercepted packets")
|
||||
)
|
||||
|
||||
func debugLog(level int, format string, v ...interface{}) {
|
||||
if *debugLevel >= level {
|
||||
log.Printf("[DEBUG-%d] %s", level, fmt.Sprintf(format, v...))
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
ANNOUNCE_RATE_TARGET = 3600 // Default target time between announces (1 hour)
|
||||
ANNOUNCE_RATE_GRACE = 3 // Number of grace announces before enforcing rate
|
||||
ANNOUNCE_RATE_PENALTY = 7200 // Additional penalty time for rate violations
|
||||
MAX_ANNOUNCE_HOPS = 128 // Maximum number of hops for announces
|
||||
DEBUG_CRITICAL = 1 // Critical errors
|
||||
DEBUG_ERROR = 2 // Non-critical errors
|
||||
DEBUG_INFO = 3 // Important information
|
||||
DEBUG_VERBOSE = 4 // Detailed information
|
||||
DEBUG_TRACE = 5 // Very detailed tracing
|
||||
DEBUG_PACKETS = 6 // Packet-level details
|
||||
DEBUG_ALL = 7 // Everything including identity operations
|
||||
APP_NAME = "Go-Client"
|
||||
APP_NAME = "Reticulum-Go Test Node"
|
||||
APP_ASPECT = "node" // Always use "node" for node announces
|
||||
)
|
||||
|
||||
@@ -63,6 +52,7 @@ type Reticulum struct {
|
||||
announceHistoryMu sync.RWMutex
|
||||
identity *identity.Identity
|
||||
destination *destination.Destination
|
||||
storage *storage.Manager
|
||||
|
||||
// Node-specific information
|
||||
maxTransferSize int16 // Max transfer size in KB
|
||||
@@ -91,30 +81,60 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
|
||||
if err := initializeDirectories(); err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize directories: %v", err)
|
||||
}
|
||||
debugLog(3, "Directories initialized")
|
||||
debug.Log(debug.DEBUG_INFO, "Directories initialized")
|
||||
|
||||
// Initialize storage manager
|
||||
storageMgr, err := storage.NewManager()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize storage manager: %v", err)
|
||||
}
|
||||
debug.Log(debug.DEBUG_INFO, "Storage manager initialized")
|
||||
|
||||
t := transport.NewTransport(cfg)
|
||||
debugLog(3, "Transport initialized")
|
||||
debug.Log(debug.DEBUG_INFO, "Transport initialized")
|
||||
|
||||
identity, err := identity.NewIdentity()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create identity: %v", err)
|
||||
// Load or create identity
|
||||
identityPath := storageMgr.GetIdentityPath()
|
||||
|
||||
var ident *identity.Identity
|
||||
|
||||
if _, err := os.Stat(identityPath); err == nil {
|
||||
// Identity file exists, load it
|
||||
ident, err = identity.FromFile(identityPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load identity: %v", err)
|
||||
}
|
||||
debug.Log(debug.DEBUG_ERROR, "Loaded existing identity", common.STR_HASH, fmt.Sprintf(common.STR_FMT_HEX_LOW, ident.Hash()))
|
||||
} else {
|
||||
// Create new identity
|
||||
ident, err = identity.NewIdentity()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create identity: %v", err)
|
||||
}
|
||||
debug.Log(debug.DEBUG_ERROR, "Created new identity", common.STR_HASH, fmt.Sprintf(common.STR_FMT_HEX_LOW, ident.Hash()))
|
||||
|
||||
// Save it to disk
|
||||
if err := ident.ToFile(identityPath); err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to save identity to file", common.STR_ERROR, err)
|
||||
} else {
|
||||
debug.Log(debug.DEBUG_INFO, "Identity saved to file", "path", identityPath)
|
||||
}
|
||||
}
|
||||
debugLog(2, "Created new identity: %x", identity.Hash())
|
||||
|
||||
// Create destination
|
||||
debugLog(DEBUG_INFO, "Creating destination...")
|
||||
debug.Log(debug.DEBUG_INFO, "Creating destination...")
|
||||
dest, err := destination.New(
|
||||
identity,
|
||||
ident,
|
||||
destination.IN,
|
||||
destination.SINGLE,
|
||||
"reticulum",
|
||||
"nomadnetwork",
|
||||
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())
|
||||
debug.Log(debug.DEBUG_INFO, "Created destination with hash", common.STR_HASH, fmt.Sprintf(common.STR_FMT_HEX_LOW, dest.GetHash()))
|
||||
|
||||
// Set node metadata
|
||||
nodeTimestamp := time.Now().Unix()
|
||||
@@ -127,20 +147,25 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
|
||||
buffers: make(map[string]*buffer.Buffer),
|
||||
pathRequests: make(map[string]*common.PathRequest),
|
||||
announceHistory: make(map[string]announceRecord),
|
||||
identity: identity,
|
||||
identity: ident,
|
||||
destination: dest,
|
||||
storage: storageMgr,
|
||||
|
||||
// Node-specific information
|
||||
maxTransferSize: 500, // Default 500KB
|
||||
nodeEnabled: true, // Enabled by default
|
||||
maxTransferSize: common.NUM_500, // Default 500KB
|
||||
nodeEnabled: true, // Enabled by default
|
||||
nodeTimestamp: nodeTimestamp,
|
||||
}
|
||||
|
||||
// Enable destination features
|
||||
dest.AcceptsLinks(true)
|
||||
dest.EnableRatchets("") // Empty string for default path
|
||||
// Enable ratchets and point to a file for persistence.
|
||||
// The actual path should probably be configurable.
|
||||
ratchetPath := ".git.quad4.io/Networks/Reticulum-Go/storage/ratchets/" + r.identity.GetHexHash()
|
||||
dest.EnableRatchets(ratchetPath)
|
||||
dest.SetProofStrategy(destination.PROVE_APP)
|
||||
debugLog(DEBUG_VERBOSE, "Configured destination features")
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Configured destination features")
|
||||
|
||||
// Initialize interfaces from config
|
||||
for name, ifaceConfig := range cfg.Interfaces {
|
||||
@@ -152,14 +177,14 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
|
||||
var err error
|
||||
|
||||
switch ifaceConfig.Type {
|
||||
case "TCPClientInterface":
|
||||
case common.STR_TCP_CLIENT:
|
||||
iface, err = interfaces.NewTCPClientInterface(
|
||||
name,
|
||||
ifaceConfig.TargetHost,
|
||||
ifaceConfig.TargetPort,
|
||||
ifaceConfig.KISSFraming,
|
||||
ifaceConfig.I2PTunneled,
|
||||
ifaceConfig.Enabled,
|
||||
true, // IN
|
||||
true, // OUT
|
||||
)
|
||||
case "UDPInterface":
|
||||
iface, err = interfaces.NewUDPInterface(
|
||||
@@ -170,8 +195,20 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
|
||||
)
|
||||
case "AutoInterface":
|
||||
iface, err = interfaces.NewAutoInterface(name, ifaceConfig)
|
||||
case "WebSocketInterface":
|
||||
wsURL := ifaceConfig.Address
|
||||
if wsURL == "" {
|
||||
wsURL = ifaceConfig.TargetHost
|
||||
}
|
||||
debug.Log(debug.DEBUG_INFO, "Creating WebSocket interface", common.STR_NAME, name, "url", wsURL, "enabled", ifaceConfig.Enabled)
|
||||
iface, err = interfaces.NewWebSocketInterface(name, wsURL, ifaceConfig.Enabled)
|
||||
if err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to create WebSocket interface", common.STR_NAME, name, common.STR_ERROR, err)
|
||||
} else {
|
||||
debug.Log(debug.DEBUG_INFO, "WebSocket interface created successfully", common.STR_NAME, name)
|
||||
}
|
||||
default:
|
||||
debugLog(1, "Unknown interface type: %s", ifaceConfig.Type)
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Unknown interface type", common.STR_TYPE, ifaceConfig.Type)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -179,27 +216,30 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
|
||||
if cfg.PanicOnInterfaceErr {
|
||||
return nil, fmt.Errorf("failed to create interface %s: %v", name, err)
|
||||
}
|
||||
debugLog(1, "Error creating interface %s: %v", name, err)
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Error creating interface", common.STR_NAME, name, common.STR_ERROR, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Set packet callback
|
||||
iface.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
|
||||
debug.Log(debug.DEBUG_INFO, "Packet callback called for interface", common.STR_NAME, ni.GetName(), "data_len", len(data))
|
||||
if r.transport != nil {
|
||||
r.transport.HandlePacket(data, ni)
|
||||
} else {
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Transport is nil in packet callback")
|
||||
}
|
||||
})
|
||||
|
||||
debugLog(2, "Configuring interface %s (type=%s)...", name, ifaceConfig.Type)
|
||||
debug.Log(debug.DEBUG_ERROR, "Configuring interface", common.STR_NAME, name, common.STR_TYPE, ifaceConfig.Type)
|
||||
r.interfaces = append(r.interfaces, iface)
|
||||
debugLog(3, "Interface %s started successfully", name)
|
||||
debug.Log(debug.DEBUG_INFO, "Interface started successfully", common.STR_NAME, name)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *Reticulum) handleInterface(iface common.NetworkInterface) {
|
||||
debugLog(DEBUG_INFO, "Setting up interface %s (type=%T)", iface.GetName(), iface)
|
||||
debug.Log(debug.DEBUG_INFO, "Setting up interface", common.STR_NAME, iface.GetName(), common.STR_TYPE, fmt.Sprintf("%T", iface))
|
||||
|
||||
ch := channel.NewChannel(&transportWrapper{r.transport})
|
||||
r.channels[iface.GetName()] = ch
|
||||
@@ -210,11 +250,11 @@ func (r *Reticulum) handleInterface(iface common.NetworkInterface) {
|
||||
ch,
|
||||
func(size int) {
|
||||
data := make([]byte, size)
|
||||
debugLog(DEBUG_PACKETS, "Interface %s: Reading %d bytes from buffer", iface.GetName(), size)
|
||||
debug.Log(debug.DEBUG_PACKETS, "Interface reading bytes from buffer", common.STR_NAME, iface.GetName(), "size", size)
|
||||
iface.ProcessIncoming(data)
|
||||
|
||||
if len(data) > 0 {
|
||||
debugLog(DEBUG_TRACE, "Interface %s: Received packet type 0x%02x", iface.GetName(), data[0])
|
||||
if len(data) > common.ZERO {
|
||||
debug.Log(debug.DEBUG_TRACE, "Interface received packet type", common.STR_NAME, iface.GetName(), common.STR_TYPE, fmt.Sprintf("0x%02x", data[0]))
|
||||
r.transport.HandlePacket(data, iface)
|
||||
}
|
||||
},
|
||||
@@ -245,7 +285,7 @@ func (r *Reticulum) monitorInterfaces() {
|
||||
stats = fmt.Sprintf("%s, RTT: %v", stats, tcpClient.GetRTT())
|
||||
}
|
||||
|
||||
debugLog(DEBUG_VERBOSE, stats)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Interface status", "stats", stats)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,39 +293,20 @@ func (r *Reticulum) monitorInterfaces() {
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
debugLog(1, "Initializing Reticulum (Debug Level: %d)...", *debugLevel)
|
||||
debug.Init()
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Initializing Reticulum", "debug_level", debug.GetDebugLevel())
|
||||
|
||||
cfg, err := config.InitConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize config: %v", err)
|
||||
}
|
||||
debugLog(2, "Configuration loaded from: %s", cfg.ConfigPath)
|
||||
|
||||
// Add default TCP interfaces if none configured
|
||||
if len(cfg.Interfaces) == 0 {
|
||||
debugLog(2, "No interfaces configured, adding default TCP interfaces")
|
||||
cfg.Interfaces = make(map[string]*common.InterfaceConfig)
|
||||
|
||||
cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
|
||||
Type: "TCPClientInterface",
|
||||
Enabled: true,
|
||||
TargetHost: "127.0.0.1",
|
||||
TargetPort: 4242,
|
||||
Name: "Go-RNS-Testnet",
|
||||
}
|
||||
|
||||
cfg.Interfaces["Quad4 TCP"] = &common.InterfaceConfig{
|
||||
Type: "TCPClientInterface",
|
||||
Enabled: true,
|
||||
TargetHost: "rns.quad4.io",
|
||||
TargetPort: 4242,
|
||||
Name: "Quad4 TCP",
|
||||
}
|
||||
debug.GetLogger().Error("Failed to initialize config", common.STR_ERROR, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
debug.Log(debug.DEBUG_ERROR, "Configuration loaded", "path", cfg.ConfigPath)
|
||||
|
||||
r, err := NewReticulum(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create Reticulum instance: %v", err)
|
||||
debug.GetLogger().Error("Failed to create Reticulum instance", common.STR_ERROR, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Start monitoring interfaces
|
||||
@@ -297,60 +318,19 @@ func main() {
|
||||
|
||||
// Start Reticulum
|
||||
if err := r.Start(); err != nil {
|
||||
log.Fatalf("Failed to start Reticulum: %v", err)
|
||||
debug.GetLogger().Error("Failed to start Reticulum", common.STR_ERROR, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Start periodic announces
|
||||
go func() {
|
||||
ticker := time.NewTicker(5 * time.Minute) // Adjust interval as needed
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
debugLog(3, "Starting periodic announce cycle")
|
||||
|
||||
// Create a new announce packet for this cycle
|
||||
periodicAnnounce, err := announce.NewAnnounce(
|
||||
r.identity,
|
||||
r.createNodeAppData(),
|
||||
nil, // No ratchet ID for now
|
||||
false,
|
||||
r.config,
|
||||
)
|
||||
if err != nil {
|
||||
debugLog(1, "Failed to create periodic announce: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Propagate announce to all online interfaces
|
||||
var onlineInterfaces []common.NetworkInterface
|
||||
for _, iface := range r.interfaces {
|
||||
if netIface, ok := iface.(common.NetworkInterface); ok {
|
||||
if netIface.IsEnabled() && netIface.IsOnline() {
|
||||
onlineInterfaces = append(onlineInterfaces, netIface)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(onlineInterfaces) > 0 {
|
||||
debugLog(2, "Sending periodic announce on %d interfaces", len(onlineInterfaces))
|
||||
if err := periodicAnnounce.Propagate(onlineInterfaces); err != nil {
|
||||
debugLog(1, "Failed to propagate periodic announce: %v", err)
|
||||
}
|
||||
} else {
|
||||
debugLog(3, "No online interfaces for periodic announce")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigChan
|
||||
|
||||
debugLog(1, "Shutting down...")
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Shutting down...")
|
||||
if err := r.Stop(); err != nil {
|
||||
debugLog(1, "Error during shutdown: %v", err)
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Error during shutdown", common.STR_ERROR, err)
|
||||
}
|
||||
debugLog(1, "Goodbye!")
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Goodbye!")
|
||||
}
|
||||
|
||||
type transportWrapper struct {
|
||||
@@ -365,7 +345,7 @@ func (tw *transportWrapper) RTT() float64 {
|
||||
return tw.GetRTT()
|
||||
}
|
||||
|
||||
func (tw *transportWrapper) GetStatus() int {
|
||||
func (tw *transportWrapper) GetStatus() byte {
|
||||
return transport.STATUS_ACTIVE
|
||||
}
|
||||
|
||||
@@ -401,17 +381,38 @@ func (tw *transportWrapper) SetPacketDelivered(packet interface{}, callback func
|
||||
callback(packet)
|
||||
}
|
||||
|
||||
func (tw *transportWrapper) GetLinkID() []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tw *transportWrapper) HandleInbound(pkt *packet.Packet) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tw *transportWrapper) ValidateLinkProof(pkt *packet.Packet, networkIface common.NetworkInterface) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func initializeDirectories() error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get home directory: %v", err)
|
||||
}
|
||||
|
||||
basePath := filepath.Join(homeDir, ".reticulum-go")
|
||||
dirs := []string{
|
||||
".reticulum-go",
|
||||
".reticulum-go/storage",
|
||||
".reticulum-go/storage/destinations",
|
||||
".reticulum-go/storage/identities",
|
||||
".reticulum-go/storage/ratchets",
|
||||
basePath,
|
||||
filepath.Join(basePath, common.STR_STORAGE),
|
||||
filepath.Join(basePath, common.STR_STORAGE, "destinations"),
|
||||
filepath.Join(basePath, common.STR_STORAGE, "identities"),
|
||||
filepath.Join(basePath, common.STR_STORAGE, "ratchets"),
|
||||
filepath.Join(basePath, common.STR_STORAGE, "cache"),
|
||||
filepath.Join(basePath, common.STR_STORAGE, "cache", "announces"),
|
||||
filepath.Join(basePath, common.STR_STORAGE, "resources"),
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
if err := os.MkdirAll(dir, common.NUM_0700); err != nil { // #nosec G301
|
||||
return fmt.Errorf("failed to create directory %s: %v", dir, err)
|
||||
}
|
||||
}
|
||||
@@ -419,124 +420,87 @@ func initializeDirectories() error {
|
||||
}
|
||||
|
||||
func (r *Reticulum) Start() error {
|
||||
debugLog(2, "Starting Reticulum...")
|
||||
debug.Log(debug.DEBUG_ERROR, "Starting Reticulum...")
|
||||
|
||||
if err := r.transport.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start transport: %v", err)
|
||||
}
|
||||
debugLog(3, "Transport started successfully")
|
||||
debug.Log(debug.DEBUG_INFO, "Transport started successfully")
|
||||
|
||||
// Start interfaces
|
||||
for _, iface := range r.interfaces {
|
||||
debugLog(2, "Starting interface %s...", iface.GetName())
|
||||
debug.Log(debug.DEBUG_ERROR, "Starting interface", "name", iface.GetName())
|
||||
if err := iface.Start(); err != nil {
|
||||
if r.config.PanicOnInterfaceErr {
|
||||
return fmt.Errorf("failed to start interface %s: %v", iface.GetName(), err)
|
||||
}
|
||||
debugLog(1, "Error starting interface %s: %v", iface.GetName(), err)
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Error starting interface", "name", iface.GetName(), "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if netIface, ok := iface.(common.NetworkInterface); ok {
|
||||
// Register interface with transport
|
||||
if err := r.transport.RegisterInterface(iface.GetName(), netIface); err != nil {
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Failed to register interface with transport", "name", iface.GetName(), "error", err)
|
||||
} else {
|
||||
debug.Log(debug.DEBUG_INFO, "Registered interface with transport", "name", iface.GetName())
|
||||
}
|
||||
r.handleInterface(netIface)
|
||||
}
|
||||
debugLog(3, "Interface %s started successfully", iface.GetName())
|
||||
debug.Log(debug.DEBUG_INFO, "Interface started successfully", "name", iface.GetName())
|
||||
}
|
||||
|
||||
// Wait for interfaces to initialize
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Send initial announce once per interface
|
||||
initialAnnounce, err := announce.NewAnnounce(
|
||||
r.identity,
|
||||
r.createNodeAppData(),
|
||||
nil,
|
||||
false,
|
||||
r.config,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create announce: %v", err)
|
||||
// Send initial announce
|
||||
debug.Log(debug.DEBUG_ERROR, "Sending initial announce")
|
||||
nodeName := "Reticulum-Go Test Node"
|
||||
r.destination.SetDefaultAppData([]byte(nodeName))
|
||||
if err := r.destination.Announce(false, nil, nil); err != nil {
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Failed to send initial announce", "error", 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,
|
||||
r.createNodeAppData(),
|
||||
nil,
|
||||
false,
|
||||
r.config,
|
||||
)
|
||||
for {
|
||||
debug.Log(debug.DEBUG_INFO, "Announcing destination...")
|
||||
err := r.destination.Announce(false, nil, nil)
|
||||
if err != nil {
|
||||
debugLog(1, "Failed to create periodic announce: %v", err)
|
||||
continue
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Could not send announce", "error", 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
time.Sleep(60 * time.Second)
|
||||
}
|
||||
}()
|
||||
|
||||
go r.monitorInterfaces()
|
||||
|
||||
debugLog(2, "Reticulum started successfully")
|
||||
debug.Log(debug.DEBUG_ERROR, "Reticulum started successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reticulum) Stop() error {
|
||||
debugLog(2, "Stopping Reticulum...")
|
||||
debug.Log(debug.DEBUG_ERROR, "Stopping Reticulum...")
|
||||
|
||||
for _, buf := range r.buffers {
|
||||
if err := buf.Close(); err != nil {
|
||||
debugLog(1, "Error closing buffer: %v", err)
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Error closing buffer", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, ch := range r.channels {
|
||||
if err := ch.Close(); err != nil {
|
||||
debugLog(1, "Error closing channel: %v", err)
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Error closing channel", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, iface := range r.interfaces {
|
||||
if err := iface.Stop(); err != nil {
|
||||
debugLog(1, "Error stopping interface %s: %v", iface.GetName(), err)
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Error stopping interface", "name", iface.GetName(), "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,7 +508,7 @@ func (r *Reticulum) Stop() error {
|
||||
return fmt.Errorf("failed to close transport: %v", err)
|
||||
}
|
||||
|
||||
debugLog(2, "Reticulum stopped successfully")
|
||||
debug.Log(debug.DEBUG_ERROR, "Reticulum stopped successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -564,103 +528,59 @@ func (h *AnnounceHandler) AspectFilter() []string {
|
||||
return h.aspectFilter
|
||||
}
|
||||
|
||||
func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appData []byte) error {
|
||||
debugLog(DEBUG_INFO, "Received announce from %x", destHash)
|
||||
debugLog(DEBUG_PACKETS, "Raw announce data: %x", appData)
|
||||
func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appData []byte, hops uint8) error {
|
||||
debug.Log(debug.DEBUG_INFO, "Received announce", "hash", fmt.Sprintf("%x", destHash), "hops", hops)
|
||||
debug.Log(debug.DEBUG_PACKETS, "Raw announce data", "data", fmt.Sprintf("%x", appData))
|
||||
debug.Log(debug.DEBUG_INFO, "MAIN HANDLER: Received announce", "hash", fmt.Sprintf("%x", destHash), "appData_len", len(appData), "hops", hops)
|
||||
|
||||
var isNode bool
|
||||
var nodeEnabled bool
|
||||
var nodeTimestamp int64
|
||||
var nodeMaxSize int16
|
||||
|
||||
// Parse msgpack array
|
||||
if len(appData) > 0 {
|
||||
if appData[0] == 0x92 {
|
||||
// Format [name, ticket] for standard peers
|
||||
debugLog(DEBUG_VERBOSE, "Received standard peer announce")
|
||||
isNode = false
|
||||
var pos = 1
|
||||
|
||||
// Parse first element (NameBytes)
|
||||
if pos+1 < len(appData) && appData[pos] == 0xc4 {
|
||||
// Parse msgpack appData from transport announce format
|
||||
if len(appData) > common.ZERO {
|
||||
// appData is msgpack array [name, customData]
|
||||
if appData[0] == common.HEX_0x92 { // array of 2 elements
|
||||
// Skip array header and first element (name)
|
||||
pos := common.ONE
|
||||
if pos < len(appData) && appData[pos] == common.HEX_0xC4 { // bin 8
|
||||
nameLen := int(appData[pos+1])
|
||||
if pos+2+nameLen <= len(appData) {
|
||||
nameBytes := appData[pos+2 : pos+2+nameLen]
|
||||
name := string(nameBytes)
|
||||
pos += 2 + nameLen
|
||||
debugLog(DEBUG_VERBOSE, "Peer name: %s (bytes: %x)", name, nameBytes)
|
||||
|
||||
// Parse second element (TicketValue)
|
||||
if pos < len(appData) {
|
||||
ticketValue := appData[pos] // Assuming fixint for now
|
||||
debugLog(DEBUG_VERBOSE, "Peer ticket value: %d", ticketValue)
|
||||
} else {
|
||||
debugLog(DEBUG_ERROR, "Could not parse ticket value from announce appData")
|
||||
pos += common.TWO + nameLen
|
||||
if pos < len(appData) && appData[pos] == common.HEX_0xC4 { // bin 8
|
||||
dataLen := int(appData[pos+1])
|
||||
if pos+2+dataLen <= len(appData) {
|
||||
customData := appData[pos+2 : pos+2+dataLen]
|
||||
nodeName := string(customData)
|
||||
debug.Log(debug.DEBUG_INFO, "Parsed node name", "name", nodeName)
|
||||
debug.Log(debug.DEBUG_INFO, "Announced node", "name", nodeName)
|
||||
}
|
||||
} else {
|
||||
debugLog(DEBUG_ERROR, "Could not parse name bytes from announce appData")
|
||||
}
|
||||
} else {
|
||||
debugLog(DEBUG_ERROR, "Announce appData name is not in expected bin 8 format")
|
||||
}
|
||||
} else if appData[0] == 0x93 {
|
||||
// Format [enable, timestamp, maxsize] for nodes
|
||||
debugLog(DEBUG_VERBOSE, "Received node announce")
|
||||
isNode = true
|
||||
var pos = 1
|
||||
|
||||
// Parse first element (Boolean enable/disable)
|
||||
if pos < len(appData) {
|
||||
if appData[pos] == 0xc3 {
|
||||
nodeEnabled = true
|
||||
} else if appData[pos] == 0xc2 {
|
||||
nodeEnabled = false
|
||||
} else {
|
||||
debugLog(DEBUG_ERROR, "Unexpected format for node enabled status: %x", appData[pos])
|
||||
}
|
||||
pos++
|
||||
debugLog(DEBUG_VERBOSE, "Node enabled: %v", nodeEnabled)
|
||||
|
||||
// Parse second element (Int32 timestamp)
|
||||
if pos+4 < len(appData) && appData[pos] == 0xd2 {
|
||||
pos++
|
||||
timestamp := binary.BigEndian.Uint32(appData[pos : pos+4])
|
||||
nodeTimestamp = int64(timestamp)
|
||||
pos += 4
|
||||
debugLog(DEBUG_VERBOSE, "Node timestamp: %d (%s)", timestamp, time.Unix(nodeTimestamp, 0))
|
||||
|
||||
// Parse third element (Int16 max transfer size)
|
||||
if pos+2 < len(appData) && appData[pos] == 0xd1 {
|
||||
pos++
|
||||
maxSize := binary.BigEndian.Uint16(appData[pos : pos+2])
|
||||
nodeMaxSize = int16(maxSize)
|
||||
debugLog(DEBUG_VERBOSE, "Node max transfer size: %d KB", nodeMaxSize)
|
||||
} else {
|
||||
debugLog(DEBUG_ERROR, "Could not parse max transfer size from node announce")
|
||||
}
|
||||
} else {
|
||||
debugLog(DEBUG_ERROR, "Could not parse timestamp from node announce")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debugLog(DEBUG_VERBOSE, "Unknown announce data format: %x", appData)
|
||||
// Fallback: treat as raw node name
|
||||
nodeName := string(appData)
|
||||
debug.Log(debug.DEBUG_INFO, "Raw node name", "name", nodeName)
|
||||
debug.Log(debug.DEBUG_INFO, "Announced node", "name", nodeName)
|
||||
}
|
||||
} else {
|
||||
debug.Log(debug.DEBUG_INFO, "No appData (empty announce)")
|
||||
}
|
||||
|
||||
// Type assert and log identity details
|
||||
if identity, ok := id.(*identity.Identity); ok {
|
||||
debugLog(DEBUG_ALL, "Identity details:")
|
||||
debugLog(DEBUG_ALL, " Hash: %s", identity.GetHexHash())
|
||||
debugLog(DEBUG_ALL, " Public Key: %x", identity.GetPublicKey())
|
||||
debug.Log(debug.DEBUG_ALL, "Identity details")
|
||||
debug.Log(debug.DEBUG_ALL, "Identity hash", "hash", identity.GetHexHash())
|
||||
debug.Log(debug.DEBUG_ALL, "Identity public key", "key", fmt.Sprintf("%x", identity.GetPublicKey()))
|
||||
|
||||
ratchets := identity.GetRatchets()
|
||||
debugLog(DEBUG_ALL, " Active Ratchets: %d", len(ratchets))
|
||||
debug.Log(debug.DEBUG_ALL, "Active ratchets", "count", len(ratchets))
|
||||
|
||||
if len(ratchets) > 0 {
|
||||
ratchetKey := identity.GetCurrentRatchetKey()
|
||||
if ratchetKey != nil {
|
||||
ratchetID := identity.GetRatchetID(ratchetKey)
|
||||
debugLog(DEBUG_ALL, " Current Ratchet ID: %x", ratchetID)
|
||||
debug.Log(debug.DEBUG_ALL, "Current ratchet ID", "id", fmt.Sprintf("%x", ratchetID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,8 +588,7 @@ func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appD
|
||||
recordType := "peer"
|
||||
if isNode {
|
||||
recordType = "node"
|
||||
debugLog(DEBUG_INFO, "Storing node in announce history: enabled=%v, timestamp=%d, maxsize=%dKB",
|
||||
nodeEnabled, nodeTimestamp, nodeMaxSize)
|
||||
debug.Log(debug.DEBUG_INFO, "Storing node in announce history", "enabled", nodeEnabled, "timestamp", nodeTimestamp, "maxsize", fmt.Sprintf("%dKB", nodeMaxSize))
|
||||
}
|
||||
|
||||
h.reticulum.announceHistoryMu.Lock()
|
||||
@@ -679,7 +598,7 @@ func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appD
|
||||
}
|
||||
h.reticulum.announceHistoryMu.Unlock()
|
||||
|
||||
debugLog(DEBUG_VERBOSE, "Stored %s announce in history for identity %s", recordType, identity.GetHexHash())
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Stored announce in history", "type", recordType, "identity", identity.GetHexHash())
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -696,30 +615,28 @@ func (r *Reticulum) GetDestination() *destination.Destination {
|
||||
func (r *Reticulum) createNodeAppData() []byte {
|
||||
// Create a msgpack array with 3 elements
|
||||
// [Bool, Int32, Int16] for [enable, timestamp, max_transfer_size]
|
||||
appData := []byte{0x93} // Array with 3 elements
|
||||
appData := []byte{common.HEX_0x93} // Array with 3 elements
|
||||
|
||||
// Element 0: Boolean for enable/disable peer
|
||||
if r.nodeEnabled {
|
||||
appData = append(appData, 0xc3) // true
|
||||
appData = append(appData, common.HEX_0xC3) // true
|
||||
} else {
|
||||
appData = append(appData, 0xc2) // false
|
||||
appData = append(appData, common.HEX_0xC2) // false
|
||||
}
|
||||
|
||||
// Element 1: Int32 timestamp (current time)
|
||||
// Update the timestamp when creating new announcements
|
||||
r.nodeTimestamp = time.Now().Unix()
|
||||
appData = append(appData, 0xd2) // int32 format
|
||||
timeBytes := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(timeBytes, uint32(r.nodeTimestamp))
|
||||
appData = append(appData, common.HEX_0xD2) // int32 format
|
||||
timeBytes := make([]byte, common.FOUR)
|
||||
binary.BigEndian.PutUint32(timeBytes, uint32(r.nodeTimestamp)) // #nosec G115
|
||||
appData = append(appData, timeBytes...)
|
||||
|
||||
// Element 2: Int16 max transfer size in KB
|
||||
appData = append(appData, 0xd1) // int16 format
|
||||
sizeBytes := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(sizeBytes, uint16(r.maxTransferSize))
|
||||
appData = append(appData, common.HEX_0xD1) // int16 format
|
||||
sizeBytes := make([]byte, common.TWO)
|
||||
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)
|
||||
debug.Log(debug.DEBUG_ALL, "Created node appData", "enable", r.nodeEnabled, "timestamp", r.nodeTimestamp, "maxsize", r.maxTransferSize, "data", fmt.Sprintf("%x", appData))
|
||||
return appData
|
||||
}
|
||||
|
||||
61
cmd/reticulum-go/reticulum_test.go
Normal file
61
cmd/reticulum-go/reticulum_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/internal/config"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
)
|
||||
|
||||
func TestNewReticulum(t *testing.T) {
|
||||
// Set up a temporary home directory for testing
|
||||
tmpDir := t.TempDir()
|
||||
originalHome := os.Getenv(common.STR_HOME)
|
||||
os.Setenv(common.STR_HOME, tmpDir)
|
||||
defer os.Setenv(common.STR_HOME, originalHome)
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
// Disable interfaces for simple test
|
||||
cfg.Interfaces = make(map[string]*common.InterfaceConfig)
|
||||
|
||||
r, err := NewReticulum(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("NewReticulum failed: %v", err)
|
||||
}
|
||||
if r == nil {
|
||||
t.Fatal("NewReticulum returned nil")
|
||||
}
|
||||
|
||||
if r.identity == nil {
|
||||
t.Error("Reticulum identity should not be nil")
|
||||
}
|
||||
if r.destination == nil {
|
||||
t.Error("Reticulum destination should not be nil")
|
||||
}
|
||||
|
||||
// Verify directories were created
|
||||
basePath := filepath.Join(tmpDir, ".reticulum-go")
|
||||
if _, err := os.Stat(basePath); os.IsNotExist(err) {
|
||||
t.Error("Base directory not created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeAppData(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
os.Setenv(common.STR_HOME, tmpDir)
|
||||
|
||||
r := &Reticulum{
|
||||
nodeEnabled: true,
|
||||
maxTransferSize: common.NUM_500,
|
||||
}
|
||||
|
||||
data := r.createNodeAppData()
|
||||
if len(data) == common.ZERO {
|
||||
t.Error("createNodeAppData returned empty data")
|
||||
}
|
||||
if data[0] != common.HEX_0x93 {
|
||||
t.Errorf("Expected array header 0x93, got 0x%x", data[0])
|
||||
}
|
||||
}
|
||||
30
cmd/reticulum-wasm/main.go
Normal file
30
cmd/reticulum-wasm/main.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build js && wasm
|
||||
// +build js,wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"syscall/js"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/wasm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
run()
|
||||
// Keep the Go program running
|
||||
select {}
|
||||
}
|
||||
|
||||
func run() {
|
||||
debug.Init()
|
||||
debug.SetDebugLevel(debug.DEBUG_INFO)
|
||||
|
||||
wasm.RegisterJSFunctions()
|
||||
|
||||
// Notify JS that reticulum is ready
|
||||
js.Global().Call("reticulumReady")
|
||||
}
|
||||
|
||||
29
cmd/reticulum-wasm/main_test.go
Normal file
29
cmd/reticulum-wasm/main_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
//go:build js && wasm
|
||||
// +build js,wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"syscall/js"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
readyCalled := false
|
||||
js.Global().Set("reticulumReady", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
readyCalled = true
|
||||
return nil
|
||||
}))
|
||||
|
||||
run()
|
||||
|
||||
if !readyCalled {
|
||||
t.Error("reticulumReady was not called by run()")
|
||||
}
|
||||
|
||||
reticulum := js.Global().Get("reticulum")
|
||||
if reticulum.IsUndefined() {
|
||||
t.Error("reticulum functions were not registered")
|
||||
}
|
||||
}
|
||||
|
||||
39
docker/Dockerfile
Normal file
39
docker/Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
||||
ARG GO_VERSION=1.25
|
||||
FROM golang:${GO_VERSION}-alpine AS builder
|
||||
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOOS=linux
|
||||
ENV GOARCH=amd64
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY cmd/ cmd/
|
||||
COPY internal/ internal/
|
||||
COPY pkg/ pkg/
|
||||
|
||||
RUN go build \
|
||||
-ldflags='-w -s -extldflags "-static"' \
|
||||
-a -installsuffix cgo \
|
||||
-o reticulum-go \
|
||||
./cmd/reticulum-go
|
||||
|
||||
FROM busybox:1.37.0@sha256:870e815c3a50dd0f6b40efddb319c72c32c3ee340b5a3e8945904232ccd12f44
|
||||
|
||||
RUN adduser -D -s /bin/sh app
|
||||
|
||||
COPY --from=builder /build/reticulum-go /usr/local/bin/reticulum-go
|
||||
|
||||
RUN chmod +x /usr/local/bin/reticulum-go
|
||||
RUN mkdir -p /app && chown app:app /app
|
||||
|
||||
USER app
|
||||
WORKDIR /app
|
||||
|
||||
EXPOSE 4242
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/reticulum-go"]
|
||||
30
docker/Dockerfile.build
Normal file
30
docker/Dockerfile.build
Normal file
@@ -0,0 +1,30 @@
|
||||
ARG GO_VERSION=1.25
|
||||
FROM golang:${GO_VERSION}-alpine
|
||||
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOOS=linux
|
||||
ENV GOARCH=amd64
|
||||
|
||||
RUN apk add --no-cache git && \
|
||||
adduser -D -s /bin/sh builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
USER builder
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY cmd/ cmd/
|
||||
COPY internal/ internal/
|
||||
COPY pkg/ pkg/
|
||||
|
||||
ARG BINARY_NAME=reticulum-go
|
||||
ARG BUILD_PATH=./cmd/reticulum-go
|
||||
|
||||
RUN mkdir -p /dist && \
|
||||
go build \
|
||||
-ldflags='-w -s -extldflags "-static"' \
|
||||
-a -installsuffix cgo \
|
||||
-o /dist/${BINARY_NAME} \
|
||||
${BUILD_PATH}
|
||||
16
examples/wasm/go.mod
Normal file
16
examples/wasm/go.mod
Normal file
@@ -0,0 +1,16 @@
|
||||
module git.quad4.io/Networks/Reticulum-Go/examples/wasm
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
git.quad4.io/Networks/Reticulum-Go v0.6.0
|
||||
git.quad4.io/RNS-Things/reticulum-go-mf v0.0.0-20251231170406-60b810424de0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
)
|
||||
|
||||
replace git.quad4.io/Networks/Reticulum-Go => ../../
|
||||
16
examples/wasm/go.sum
Normal file
16
examples/wasm/go.sum
Normal file
@@ -0,0 +1,16 @@
|
||||
git.quad4.io/RNS-Things/reticulum-go-mf v0.0.0-20251231170406-60b810424de0 h1:Yne2IbESHud2fmsj9kjsTYR3QBj+vY9fTqvsEzaKfy8=
|
||||
git.quad4.io/RNS-Things/reticulum-go-mf v0.0.0-20251231170406-60b810424de0/go.mod h1:vhZm1vAMuWJtoFGGAHPlnFsVqTzHkBuYWDMGo6KjVPk=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
80
examples/wasm/main.go
Normal file
80
examples/wasm/main.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build js && wasm
|
||||
// +build js,wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"syscall/js"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/wasm"
|
||||
"git.quad4.io/RNS-Things/reticulum-go-mf/pkg/mf"
|
||||
)
|
||||
|
||||
var messenger *mf.Messenger
|
||||
|
||||
func main() {
|
||||
// Register the generic WASM bridge functions first
|
||||
wasm.RegisterJSFunctions()
|
||||
|
||||
// Add chat-specific functions to the "reticulum" JS object
|
||||
reticulum := js.Global().Get("reticulum")
|
||||
reticulum.Set("sendMessage", js.FuncOf(SendMessage))
|
||||
reticulum.Set("sendAnnounce", js.FuncOf(SendAnnounce))
|
||||
|
||||
// Keep the Go program running
|
||||
select {}
|
||||
}
|
||||
|
||||
func SendMessage(this js.Value, args []js.Value) interface{} {
|
||||
if len(args) < 2 {
|
||||
return js.ValueOf(map[string]interface{}{
|
||||
"error": "Destination hash and message required",
|
||||
})
|
||||
}
|
||||
|
||||
destHashHex := args[0].String()
|
||||
message := args[1].String()
|
||||
|
||||
destHash, err := hex.DecodeString(destHashHex)
|
||||
if err != nil {
|
||||
return js.ValueOf(map[string]interface{}{
|
||||
"error": fmt.Sprintf("Invalid destination hash: %v", err),
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize messenger if not already done
|
||||
if messenger == nil {
|
||||
t := wasm.GetTransport()
|
||||
d := wasm.GetDestinationPointer()
|
||||
if t == nil || d == nil {
|
||||
return js.ValueOf(map[string]interface{}{
|
||||
"error": "Reticulum not initialized",
|
||||
})
|
||||
}
|
||||
messenger = mf.NewMessenger(t, d)
|
||||
}
|
||||
|
||||
// Use the high-level Messenger from mf package
|
||||
if err := messenger.SendMessage(destHash, message); err != nil {
|
||||
return js.ValueOf(map[string]interface{}{
|
||||
"error": fmt.Sprintf("Send failed: %v", err),
|
||||
})
|
||||
}
|
||||
|
||||
return js.ValueOf(map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
||||
func SendAnnounce(this js.Value, args []js.Value) interface{} {
|
||||
var appData []byte
|
||||
if len(args) >= 1 && args[0].String() != "" {
|
||||
appData = []byte(args[0].String())
|
||||
}
|
||||
|
||||
return wasm.SendAnnounce(appData)
|
||||
}
|
||||
33
examples/wasm/main_test.go
Normal file
33
examples/wasm/main_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
//go:build js && wasm
|
||||
// +build js,wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"syscall/js"
|
||||
"testing"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/wasm"
|
||||
)
|
||||
|
||||
func TestRegisterFunctions(t *testing.T) {
|
||||
// Register functions
|
||||
wasm.RegisterJSFunctions()
|
||||
|
||||
reticulum := js.Global().Get("reticulum")
|
||||
if reticulum.IsUndefined() {
|
||||
t.Fatal("reticulum object not registered")
|
||||
}
|
||||
|
||||
// Manually register chat functions since main() has select{}
|
||||
reticulum.Set("sendMessage", js.FuncOf(SendMessage))
|
||||
reticulum.Set("sendAnnounce", js.FuncOf(SendAnnounce))
|
||||
|
||||
tests := []string{"sendMessage", "sendAnnounce", "init", "getIdentity", "sendData"}
|
||||
for _, name := range tests {
|
||||
if reticulum.Get(name).Type() != js.TypeFunction {
|
||||
t.Errorf("function %s not registered correctly", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
43
examples/wasm/public/index.html
Normal file
43
examples/wasm/public/index.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Reticulum WASM Chat Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="js/wasm_exec.js"></script>
|
||||
<script>
|
||||
if (!WebAssembly.instantiateStreaming) { // polyfill
|
||||
WebAssembly.instantiateStreaming = async (resp, importObject) => {
|
||||
const source = await (await resp).arrayBuffer();
|
||||
return await WebAssembly.instantiate(source, importObject);
|
||||
};
|
||||
}
|
||||
|
||||
const go = new Go();
|
||||
let mod, inst;
|
||||
WebAssembly.instantiateStreaming(fetch("static/reticulum-go.wasm"), go.importObject).then(async (result) => {
|
||||
mod = result.module;
|
||||
inst = result.instance;
|
||||
console.log("WASM loaded");
|
||||
await go.run(inst);
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
// Basic chat interface helper
|
||||
window.onChatMessage = (msg) => {
|
||||
console.log("Chat message received:", msg);
|
||||
};
|
||||
window.onPeerDiscovered = (peer) => {
|
||||
console.log("Peer discovered:", peer);
|
||||
};
|
||||
window.log = (msg, level) => {
|
||||
console.log(`[${level}] ${msg}`);
|
||||
};
|
||||
</script>
|
||||
<h1>Reticulum WASM Chat</h1>
|
||||
<p>Open console to see output.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
575
examples/wasm/public/js/wasm_exec.js
Normal file
575
examples/wasm/public/js/wasm_exec.js
Normal file
@@ -0,0 +1,575 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
"use strict";
|
||||
|
||||
(() => {
|
||||
const enosys = () => {
|
||||
const err = new Error("not implemented");
|
||||
err.code = "ENOSYS";
|
||||
return err;
|
||||
};
|
||||
|
||||
if (!globalThis.fs) {
|
||||
let outputBuf = "";
|
||||
globalThis.fs = {
|
||||
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused
|
||||
writeSync(fd, buf) {
|
||||
outputBuf += decoder.decode(buf);
|
||||
const nl = outputBuf.lastIndexOf("\n");
|
||||
if (nl != -1) {
|
||||
console.log(outputBuf.substring(0, nl));
|
||||
outputBuf = outputBuf.substring(nl + 1);
|
||||
}
|
||||
return buf.length;
|
||||
},
|
||||
write(fd, buf, offset, length, position, callback) {
|
||||
if (offset !== 0 || length !== buf.length || position !== null) {
|
||||
callback(enosys());
|
||||
return;
|
||||
}
|
||||
const n = this.writeSync(fd, buf);
|
||||
callback(null, n);
|
||||
},
|
||||
chmod(path, mode, callback) { callback(enosys()); },
|
||||
chown(path, uid, gid, callback) { callback(enosys()); },
|
||||
close(fd, callback) { callback(enosys()); },
|
||||
fchmod(fd, mode, callback) { callback(enosys()); },
|
||||
fchown(fd, uid, gid, callback) { callback(enosys()); },
|
||||
fstat(fd, callback) { callback(enosys()); },
|
||||
fsync(fd, callback) { callback(null); },
|
||||
ftruncate(fd, length, callback) { callback(enosys()); },
|
||||
lchown(path, uid, gid, callback) { callback(enosys()); },
|
||||
link(path, link, callback) { callback(enosys()); },
|
||||
lstat(path, callback) { callback(enosys()); },
|
||||
mkdir(path, perm, callback) { callback(enosys()); },
|
||||
open(path, flags, mode, callback) { callback(enosys()); },
|
||||
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
|
||||
readdir(path, callback) { callback(enosys()); },
|
||||
readlink(path, callback) { callback(enosys()); },
|
||||
rename(from, to, callback) { callback(enosys()); },
|
||||
rmdir(path, callback) { callback(enosys()); },
|
||||
stat(path, callback) { callback(enosys()); },
|
||||
symlink(path, link, callback) { callback(enosys()); },
|
||||
truncate(path, length, callback) { callback(enosys()); },
|
||||
unlink(path, callback) { callback(enosys()); },
|
||||
utimes(path, atime, mtime, callback) { callback(enosys()); },
|
||||
};
|
||||
}
|
||||
|
||||
if (!globalThis.process) {
|
||||
globalThis.process = {
|
||||
getuid() { return -1; },
|
||||
getgid() { return -1; },
|
||||
geteuid() { return -1; },
|
||||
getegid() { return -1; },
|
||||
getgroups() { throw enosys(); },
|
||||
pid: -1,
|
||||
ppid: -1,
|
||||
umask() { throw enosys(); },
|
||||
cwd() { throw enosys(); },
|
||||
chdir() { throw enosys(); },
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalThis.path) {
|
||||
globalThis.path = {
|
||||
resolve(...pathSegments) {
|
||||
return pathSegments.join("/");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalThis.crypto) {
|
||||
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
|
||||
}
|
||||
|
||||
if (!globalThis.performance) {
|
||||
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
|
||||
}
|
||||
|
||||
if (!globalThis.TextEncoder) {
|
||||
throw new Error("globalThis.TextEncoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
if (!globalThis.TextDecoder) {
|
||||
throw new Error("globalThis.TextDecoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder("utf-8");
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
|
||||
globalThis.Go = class {
|
||||
constructor() {
|
||||
this.argv = ["js"];
|
||||
this.env = {};
|
||||
this.exit = (code) => {
|
||||
if (code !== 0) {
|
||||
console.warn("exit code:", code);
|
||||
}
|
||||
};
|
||||
this._exitPromise = new Promise((resolve) => {
|
||||
this._resolveExitPromise = resolve;
|
||||
});
|
||||
this._pendingEvent = null;
|
||||
this._scheduledTimeouts = new Map();
|
||||
this._nextCallbackTimeoutID = 1;
|
||||
|
||||
const setInt64 = (addr, v) => {
|
||||
this.mem.setUint32(addr + 0, v, true);
|
||||
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
|
||||
}
|
||||
|
||||
const setInt32 = (addr, v) => {
|
||||
this.mem.setUint32(addr + 0, v, true);
|
||||
}
|
||||
|
||||
const getInt64 = (addr) => {
|
||||
const low = this.mem.getUint32(addr + 0, true);
|
||||
const high = this.mem.getInt32(addr + 4, true);
|
||||
return low + high * 4294967296;
|
||||
}
|
||||
|
||||
const loadValue = (addr) => {
|
||||
const f = this.mem.getFloat64(addr, true);
|
||||
if (f === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (!isNaN(f)) {
|
||||
return f;
|
||||
}
|
||||
|
||||
const id = this.mem.getUint32(addr, true);
|
||||
return this._values[id];
|
||||
}
|
||||
|
||||
const storeValue = (addr, v) => {
|
||||
const nanHead = 0x7FF80000;
|
||||
|
||||
if (typeof v === "number" && v !== 0) {
|
||||
if (isNaN(v)) {
|
||||
this.mem.setUint32(addr + 4, nanHead, true);
|
||||
this.mem.setUint32(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
this.mem.setFloat64(addr, v, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (v === undefined) {
|
||||
this.mem.setFloat64(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
|
||||
let id = this._ids.get(v);
|
||||
if (id === undefined) {
|
||||
id = this._idPool.pop();
|
||||
if (id === undefined) {
|
||||
id = this._values.length;
|
||||
}
|
||||
this._values[id] = v;
|
||||
this._goRefCounts[id] = 0;
|
||||
this._ids.set(v, id);
|
||||
}
|
||||
this._goRefCounts[id]++;
|
||||
let typeFlag = 0;
|
||||
switch (typeof v) {
|
||||
case "object":
|
||||
if (v !== null) {
|
||||
typeFlag = 1;
|
||||
}
|
||||
break;
|
||||
case "string":
|
||||
typeFlag = 2;
|
||||
break;
|
||||
case "symbol":
|
||||
typeFlag = 3;
|
||||
break;
|
||||
case "function":
|
||||
typeFlag = 4;
|
||||
break;
|
||||
}
|
||||
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
|
||||
this.mem.setUint32(addr, id, true);
|
||||
}
|
||||
|
||||
const loadSlice = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
|
||||
}
|
||||
|
||||
const loadSliceOfValues = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
const a = new Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
a[i] = loadValue(array + i * 8);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
const loadString = (addr) => {
|
||||
const saddr = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
|
||||
}
|
||||
|
||||
const testCallExport = (a, b) => {
|
||||
this._inst.exports.testExport0();
|
||||
return this._inst.exports.testExport(a, b);
|
||||
}
|
||||
|
||||
const timeOrigin = Date.now() - performance.now();
|
||||
this.importObject = {
|
||||
_gotest: {
|
||||
add: (a, b) => a + b,
|
||||
callExport: testCallExport,
|
||||
},
|
||||
gojs: {
|
||||
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
|
||||
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
|
||||
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
|
||||
// This changes the SP, thus we have to update the SP used by the imported function.
|
||||
|
||||
// func wasmExit(code int32)
|
||||
"runtime.wasmExit": (sp) => {
|
||||
sp >>>= 0;
|
||||
const code = this.mem.getInt32(sp + 8, true);
|
||||
this.exited = true;
|
||||
delete this._inst;
|
||||
delete this._values;
|
||||
delete this._goRefCounts;
|
||||
delete this._ids;
|
||||
delete this._idPool;
|
||||
this.exit(code);
|
||||
},
|
||||
|
||||
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
|
||||
"runtime.wasmWrite": (sp) => {
|
||||
sp >>>= 0;
|
||||
const fd = getInt64(sp + 8);
|
||||
const p = getInt64(sp + 16);
|
||||
const n = this.mem.getInt32(sp + 24, true);
|
||||
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
|
||||
},
|
||||
|
||||
// func resetMemoryDataView()
|
||||
"runtime.resetMemoryDataView": (sp) => {
|
||||
sp >>>= 0;
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
},
|
||||
|
||||
// func nanotime1() int64
|
||||
"runtime.nanotime1": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
|
||||
},
|
||||
|
||||
// func walltime() (sec int64, nsec int32)
|
||||
"runtime.walltime": (sp) => {
|
||||
sp >>>= 0;
|
||||
const msec = (new Date).getTime();
|
||||
setInt64(sp + 8, msec / 1000);
|
||||
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
|
||||
},
|
||||
|
||||
// func scheduleTimeoutEvent(delay int64) int32
|
||||
"runtime.scheduleTimeoutEvent": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this._nextCallbackTimeoutID;
|
||||
this._nextCallbackTimeoutID++;
|
||||
this._scheduledTimeouts.set(id, setTimeout(
|
||||
() => {
|
||||
this._resume();
|
||||
while (this._scheduledTimeouts.has(id)) {
|
||||
// for some reason Go failed to register the timeout event, log and try again
|
||||
// (temporary workaround for https://github.com/golang/go/issues/28975)
|
||||
console.warn("scheduleTimeoutEvent: missed timeout event");
|
||||
this._resume();
|
||||
}
|
||||
},
|
||||
getInt64(sp + 8),
|
||||
));
|
||||
this.mem.setInt32(sp + 16, id, true);
|
||||
},
|
||||
|
||||
// func clearTimeoutEvent(id int32)
|
||||
"runtime.clearTimeoutEvent": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this.mem.getInt32(sp + 8, true);
|
||||
clearTimeout(this._scheduledTimeouts.get(id));
|
||||
this._scheduledTimeouts.delete(id);
|
||||
},
|
||||
|
||||
// func getRandomData(r []byte)
|
||||
"runtime.getRandomData": (sp) => {
|
||||
sp >>>= 0;
|
||||
crypto.getRandomValues(loadSlice(sp + 8));
|
||||
},
|
||||
|
||||
// func finalizeRef(v ref)
|
||||
"syscall/js.finalizeRef": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this.mem.getUint32(sp + 8, true);
|
||||
this._goRefCounts[id]--;
|
||||
if (this._goRefCounts[id] === 0) {
|
||||
const v = this._values[id];
|
||||
this._values[id] = null;
|
||||
this._ids.delete(v);
|
||||
this._idPool.push(id);
|
||||
}
|
||||
},
|
||||
|
||||
// func stringVal(value string) ref
|
||||
"syscall/js.stringVal": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, loadString(sp + 8));
|
||||
},
|
||||
|
||||
// func valueGet(v ref, p string) ref
|
||||
"syscall/js.valueGet": (sp) => {
|
||||
sp >>>= 0;
|
||||
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 32, result);
|
||||
},
|
||||
|
||||
// func valueSet(v ref, p string, x ref)
|
||||
"syscall/js.valueSet": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
|
||||
},
|
||||
|
||||
// func valueDelete(v ref, p string)
|
||||
"syscall/js.valueDelete": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
|
||||
},
|
||||
|
||||
// func valueIndex(v ref, i int) ref
|
||||
"syscall/js.valueIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
|
||||
},
|
||||
|
||||
// valueSetIndex(v ref, i int, x ref)
|
||||
"syscall/js.valueSetIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
|
||||
},
|
||||
|
||||
// func valueCall(v ref, m string, args []ref) (ref, bool)
|
||||
"syscall/js.valueCall": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const m = Reflect.get(v, loadString(sp + 16));
|
||||
const args = loadSliceOfValues(sp + 32);
|
||||
const result = Reflect.apply(m, v, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, result);
|
||||
this.mem.setUint8(sp + 64, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, err);
|
||||
this.mem.setUint8(sp + 64, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueInvoke(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueInvoke": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.apply(v, undefined, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueNew(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueNew": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.construct(v, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueLength(v ref) int
|
||||
"syscall/js.valueLength": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
|
||||
},
|
||||
|
||||
// valuePrepareString(v ref) (ref, int)
|
||||
"syscall/js.valuePrepareString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = encoder.encode(String(loadValue(sp + 8)));
|
||||
storeValue(sp + 16, str);
|
||||
setInt64(sp + 24, str.length);
|
||||
},
|
||||
|
||||
// valueLoadString(v ref, b []byte)
|
||||
"syscall/js.valueLoadString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = loadValue(sp + 8);
|
||||
loadSlice(sp + 16).set(str);
|
||||
},
|
||||
|
||||
// func valueInstanceOf(v ref, t ref) bool
|
||||
"syscall/js.valueInstanceOf": (sp) => {
|
||||
sp >>>= 0;
|
||||
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
|
||||
},
|
||||
|
||||
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||
"syscall/js.copyBytesToGo": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadSlice(sp + 8);
|
||||
const src = loadValue(sp + 32);
|
||||
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
// func copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||
"syscall/js.copyBytesToJS": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadValue(sp + 8);
|
||||
const src = loadSlice(sp + 16);
|
||||
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
"debug": (value) => {
|
||||
console.log(value);
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async run(instance) {
|
||||
if (!(instance instanceof WebAssembly.Instance)) {
|
||||
throw new Error("Go.run: WebAssembly.Instance expected");
|
||||
}
|
||||
this._inst = instance;
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
this._values = [ // JS values that Go currently has references to, indexed by reference id
|
||||
NaN,
|
||||
0,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
globalThis,
|
||||
this,
|
||||
];
|
||||
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
|
||||
this._ids = new Map([ // mapping from JS values to reference ids
|
||||
[0, 1],
|
||||
[null, 2],
|
||||
[true, 3],
|
||||
[false, 4],
|
||||
[globalThis, 5],
|
||||
[this, 6],
|
||||
]);
|
||||
this._idPool = []; // unused ids that have been garbage collected
|
||||
this.exited = false; // whether the Go program has exited
|
||||
|
||||
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
|
||||
let offset = 4096;
|
||||
|
||||
const strPtr = (str) => {
|
||||
const ptr = offset;
|
||||
const bytes = encoder.encode(str + "\0");
|
||||
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
|
||||
offset += bytes.length;
|
||||
if (offset % 8 !== 0) {
|
||||
offset += 8 - (offset % 8);
|
||||
}
|
||||
return ptr;
|
||||
};
|
||||
|
||||
const argc = this.argv.length;
|
||||
|
||||
const argvPtrs = [];
|
||||
this.argv.forEach((arg) => {
|
||||
argvPtrs.push(strPtr(arg));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const keys = Object.keys(this.env).sort();
|
||||
keys.forEach((key) => {
|
||||
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const argv = offset;
|
||||
argvPtrs.forEach((ptr) => {
|
||||
this.mem.setUint32(offset, ptr, true);
|
||||
this.mem.setUint32(offset + 4, 0, true);
|
||||
offset += 8;
|
||||
});
|
||||
|
||||
// The linker guarantees global data starts from at least wasmMinDataAddr.
|
||||
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
|
||||
const wasmMinDataAddr = 4096 + 8192;
|
||||
if (offset >= wasmMinDataAddr) {
|
||||
throw new Error("total length of command line and environment variables exceeds limit");
|
||||
}
|
||||
|
||||
this._inst.exports.run(argc, argv);
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
await this._exitPromise;
|
||||
}
|
||||
|
||||
_resume() {
|
||||
if (this.exited) {
|
||||
throw new Error("Go program has already exited");
|
||||
}
|
||||
this._inst.exports.resume();
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
}
|
||||
|
||||
_makeFuncWrapper(id) {
|
||||
const go = this;
|
||||
return function () {
|
||||
const event = { id: id, this: this, args: arguments };
|
||||
go._pendingEvent = event;
|
||||
go._resume();
|
||||
return event.result;
|
||||
};
|
||||
}
|
||||
}
|
||||
})();
|
||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1766902085,
|
||||
"narHash": "sha256-coBu0ONtFzlwwVBzmjacUQwj3G+lybcZ1oeNSQkgC0M=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c0b0e0fddf73fd517c3471e546c0df87a42d53f4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
50
flake.nix
Normal file
50
flake.nix
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
description = "Reticulum-Go development environment";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
|
||||
go = pkgs.go_1_25;
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
go
|
||||
go-task
|
||||
revive
|
||||
gosec
|
||||
gnumake
|
||||
tinygo
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo "Reticulum-Go development environment"
|
||||
echo "Go version: $(go version)"
|
||||
echo "Task version: $(task --version 2>/dev/null || echo 'not available')"
|
||||
echo "Revive version: $(revive --version 2>/dev/null || echo 'not available')"
|
||||
echo "Gosec version: $(gosec --version 2>/dev/null || echo 'not available')"
|
||||
echo "TinyGo version: $(tinygo version 2>/dev/null || echo 'not available')"
|
||||
'';
|
||||
};
|
||||
|
||||
packages.default = pkgs.buildGoModule {
|
||||
pname = "reticulum-go";
|
||||
version = "dev";
|
||||
src = ./.;
|
||||
vendorHash = "";
|
||||
subPackages = [ "cmd/reticulum-go" ];
|
||||
ldflags = [ "-s" "-w" ];
|
||||
CGO_ENABLED = "0";
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
9
go.mod
9
go.mod
@@ -1,5 +1,10 @@
|
||||
module github.com/Sudo-Ivan/reticulum-go
|
||||
module git.quad4.io/Networks/Reticulum-Go
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require golang.org/x/crypto v0.37.0
|
||||
require (
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1
|
||||
golang.org/x/crypto v0.46.0
|
||||
)
|
||||
|
||||
require github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
|
||||
16
go.sum
16
go.sum
@@ -1,2 +1,14 @@
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package config
|
||||
|
||||
import (
|
||||
@@ -8,7 +10,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -44,7 +46,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 +72,8 @@ 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)
|
||||
// bearer:disable go_gosec_filesystem_filereadtaint
|
||||
file, err := os.Open(path) // #nosec G304
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -202,7 +205,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
|
||||
@@ -210,7 +213,6 @@ func CreateDefaultConfig(path string) error {
|
||||
cfg := DefaultConfig()
|
||||
cfg.ConfigPath = path
|
||||
|
||||
// Add Auto Interface
|
||||
cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{
|
||||
Type: "AutoInterface",
|
||||
Enabled: true,
|
||||
@@ -220,7 +222,6 @@ func CreateDefaultConfig(path string) error {
|
||||
DataPort: 42671,
|
||||
}
|
||||
|
||||
// Add default interfaces
|
||||
cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
|
||||
Type: "TCPClientInterface",
|
||||
Enabled: true,
|
||||
@@ -244,7 +245,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
|
||||
}
|
||||
|
||||
|
||||
136
internal/config/config_test.go
Normal file
136
internal/config/config_test.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
)
|
||||
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
if cfg == nil {
|
||||
t.Fatal("DefaultConfig() returned nil")
|
||||
}
|
||||
if !cfg.EnableTransport {
|
||||
t.Error("EnableTransport should be true by default")
|
||||
}
|
||||
if cfg.LogLevel != DefaultLogLevel {
|
||||
t.Errorf("LogLevel should be %d, got %d", DefaultLogLevel, cfg.LogLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected interface{}
|
||||
}{
|
||||
{"true", true},
|
||||
{"false", false},
|
||||
{"123", 123},
|
||||
{"hello", "hello"},
|
||||
{" 456 ", 456},
|
||||
{" world ", "world"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := parseValue(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("parseValue(%q) = %v; want %v", tt.input, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSaveConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config")
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.ConfigPath = configPath
|
||||
cfg.LogLevel = 1
|
||||
cfg.EnableTransport = false
|
||||
cfg.Interfaces["TestInterface"] = &common.InterfaceConfig{
|
||||
Name: "TestInterface",
|
||||
Type: "UDPInterface",
|
||||
Enabled: true,
|
||||
Address: "1.2.3.4",
|
||||
Port: 1234,
|
||||
}
|
||||
|
||||
err := SaveConfig(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("SaveConfig failed: %v", err)
|
||||
}
|
||||
|
||||
loadedCfg, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig failed: %v", err)
|
||||
}
|
||||
|
||||
if loadedCfg.LogLevel != 1 {
|
||||
t.Errorf("Expected LogLevel 1, got %d", loadedCfg.LogLevel)
|
||||
}
|
||||
if loadedCfg.EnableTransport {
|
||||
t.Error("Expected EnableTransport false")
|
||||
}
|
||||
|
||||
iface, ok := loadedCfg.Interfaces["TestInterface"]
|
||||
if !ok {
|
||||
t.Fatal("TestInterface not found in loaded config")
|
||||
}
|
||||
if iface.Type != "UDPInterface" {
|
||||
t.Errorf("Expected type UDPInterface, got %s", iface.Type)
|
||||
}
|
||||
if iface.Address != "1.2.3.4" {
|
||||
t.Errorf("Expected address 1.2.3.4, got %s", iface.Address)
|
||||
}
|
||||
if iface.Port != 1234 {
|
||||
t.Errorf("Expected port 1234, got %d", iface.Port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateDefaultConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config")
|
||||
|
||||
err := CreateDefaultConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateDefaultConfig failed: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
t.Fatal("Config file was not created")
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig failed: %v", err)
|
||||
}
|
||||
|
||||
if _, ok := cfg.Interfaces["Auto Discovery"]; !ok {
|
||||
t.Error("Auto Discovery interface missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConfigPath(t *testing.T) {
|
||||
path, err := GetConfigPath()
|
||||
if err != nil {
|
||||
t.Fatalf("GetConfigPath failed: %v", err)
|
||||
}
|
||||
if path == "" {
|
||||
t.Error("GetConfigPath returned empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureConfigDir(t *testing.T) {
|
||||
// This might modify the actual home directory if not careful,
|
||||
// but EnsureConfigDir uses os.UserHomeDir().
|
||||
// For testing purposes, we can't easily mock os.UserHomeDir() without
|
||||
// changing the code or environment variables.
|
||||
// Since we are in a sandbox, it should be fine.
|
||||
err := EnsureConfigDir()
|
||||
if err != nil {
|
||||
t.Errorf("EnsureConfigDir failed: %v", err)
|
||||
}
|
||||
}
|
||||
191
internal/storage/storage.go
Normal file
191
internal/storage/storage.go
Normal file
@@ -0,0 +1,191 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package storage
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
basePath string
|
||||
ratchetsPath string
|
||||
identitiesPath string
|
||||
destinationTable string
|
||||
knownDestinations string
|
||||
transportIdentity string
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
type RatchetData struct {
|
||||
RatchetKey []byte `msgpack:"ratchet_key"`
|
||||
Received int64 `msgpack:"received"`
|
||||
}
|
||||
|
||||
func NewManager() (*Manager, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get home directory: %w", err)
|
||||
}
|
||||
|
||||
basePath := filepath.Join(homeDir, ".reticulum-go", "storage")
|
||||
|
||||
m := &Manager{
|
||||
basePath: basePath,
|
||||
ratchetsPath: filepath.Join(basePath, "ratchets"),
|
||||
identitiesPath: filepath.Join(basePath, "identities"),
|
||||
destinationTable: filepath.Join(basePath, "destination_table"),
|
||||
knownDestinations: filepath.Join(basePath, "known_destinations"),
|
||||
transportIdentity: filepath.Join(basePath, "transport_identity"),
|
||||
}
|
||||
|
||||
if err := m.initializeDirectories(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Manager) initializeDirectories() error {
|
||||
dirs := []string{
|
||||
m.basePath,
|
||||
m.ratchetsPath,
|
||||
m.identitiesPath,
|
||||
filepath.Join(m.basePath, "cache"),
|
||||
filepath.Join(m.basePath, "cache", "announces"),
|
||||
filepath.Join(m.basePath, "resources"),
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return fmt.Errorf("failed to create directory %s: %w", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) SaveRatchet(identityHash []byte, ratchetKey []byte) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
hexHash := hex.EncodeToString(identityHash)
|
||||
ratchetDir := filepath.Join(m.ratchetsPath, hexHash)
|
||||
|
||||
if err := os.MkdirAll(ratchetDir, 0700); err != nil {
|
||||
return fmt.Errorf("failed to create ratchet directory: %w", err)
|
||||
}
|
||||
|
||||
ratchetData := RatchetData{
|
||||
RatchetKey: ratchetKey,
|
||||
Received: time.Now().Unix(),
|
||||
}
|
||||
|
||||
data, err := msgpack.Marshal(ratchetData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal ratchet data: %w", err)
|
||||
}
|
||||
|
||||
ratchetHash := hex.EncodeToString(ratchetKey[:16])
|
||||
outPath := filepath.Join(ratchetDir, ratchetHash+".out")
|
||||
finalPath := filepath.Join(ratchetDir, ratchetHash)
|
||||
|
||||
if err := os.WriteFile(outPath, data, 0600); err != nil {
|
||||
return fmt.Errorf("failed to write ratchet file: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Rename(outPath, finalPath); err != nil {
|
||||
_ = os.Remove(outPath)
|
||||
return fmt.Errorf("failed to move ratchet file: %w", err)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Saved ratchet to storage", "identity", hexHash, "ratchet", ratchetHash)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) LoadRatchets(identityHash []byte) (map[string][]byte, error) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
hexHash := hex.EncodeToString(identityHash)
|
||||
ratchetDir := filepath.Join(m.ratchetsPath, hexHash)
|
||||
|
||||
ratchets := make(map[string][]byte)
|
||||
|
||||
if _, err := os.Stat(ratchetDir); os.IsNotExist(err) {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "No ratchet directory found", "identity", hexHash)
|
||||
return ratchets, nil
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(ratchetDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read ratchet directory: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
expiry := int64(2592000) // 30 days
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := filepath.Join(ratchetDir, entry.Name())
|
||||
// bearer:disable go_gosec_filesystem_filereadtaint
|
||||
data, err := os.ReadFile(filePath) // #nosec G304 - reading from controlled directory
|
||||
if err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to read ratchet file", "file", entry.Name(), "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var ratchetData RatchetData
|
||||
if err := msgpack.Unmarshal(data, &ratchetData); err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Corrupted ratchet data", "file", entry.Name(), "error", err)
|
||||
_ = os.Remove(filePath)
|
||||
continue
|
||||
}
|
||||
|
||||
if now > ratchetData.Received+expiry {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Removing expired ratchet", "file", entry.Name())
|
||||
_ = os.Remove(filePath)
|
||||
continue
|
||||
}
|
||||
|
||||
ratchetHash := entry.Name()
|
||||
ratchets[ratchetHash] = ratchetData.RatchetKey
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Loaded ratchets from storage", "identity", hexHash, "count", len(ratchets))
|
||||
return ratchets, nil
|
||||
}
|
||||
|
||||
func (m *Manager) GetBasePath() string {
|
||||
return m.basePath
|
||||
}
|
||||
|
||||
func (m *Manager) GetRatchetsPath() string {
|
||||
return m.ratchetsPath
|
||||
}
|
||||
|
||||
func (m *Manager) GetIdentityPath() string {
|
||||
return filepath.Join(m.basePath, "identity")
|
||||
}
|
||||
|
||||
func (m *Manager) GetTransportIdentityPath() string {
|
||||
return m.transportIdentity
|
||||
}
|
||||
|
||||
func (m *Manager) GetDestinationTablePath() string {
|
||||
return m.destinationTable
|
||||
}
|
||||
|
||||
func (m *Manager) GetKnownDestinationsPath() string {
|
||||
return m.knownDestinations
|
||||
}
|
||||
117
internal/storage/storage_test.go
Normal file
117
internal/storage/storage_test.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewManager(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
originalHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", tmpDir)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
m, err := NewManager()
|
||||
if err != nil {
|
||||
t.Fatalf("NewManager failed: %v", err)
|
||||
}
|
||||
if m == nil {
|
||||
t.Fatal("NewManager returned nil")
|
||||
}
|
||||
|
||||
expectedBase := filepath.Join(tmpDir, ".reticulum-go", "storage")
|
||||
if m.basePath != expectedBase {
|
||||
t.Errorf("Expected basePath %s, got %s", expectedBase, m.basePath)
|
||||
}
|
||||
|
||||
// Verify directories were created
|
||||
dirs := []string{
|
||||
m.basePath,
|
||||
m.ratchetsPath,
|
||||
m.identitiesPath,
|
||||
filepath.Join(m.basePath, "cache"),
|
||||
filepath.Join(m.basePath, "cache", "announces"),
|
||||
filepath.Join(m.basePath, "resources"),
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
t.Errorf("Directory %s was not created", dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveLoadRatchets(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
originalHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", tmpDir)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
m, err := NewManager()
|
||||
if err != nil {
|
||||
t.Fatalf("NewManager failed: %v", err)
|
||||
}
|
||||
|
||||
identityHash := []byte("test-identity-hash")
|
||||
ratchetKey := make([]byte, 32)
|
||||
for i := range ratchetKey {
|
||||
ratchetKey[i] = byte(i)
|
||||
}
|
||||
|
||||
err = m.SaveRatchet(identityHash, ratchetKey)
|
||||
if err != nil {
|
||||
t.Fatalf("SaveRatchet failed: %v", err)
|
||||
}
|
||||
|
||||
ratchets, err := m.LoadRatchets(identityHash)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadRatchets failed: %v", err)
|
||||
}
|
||||
|
||||
if len(ratchets) != 1 {
|
||||
t.Errorf("Expected 1 ratchet, got %d", len(ratchets))
|
||||
}
|
||||
|
||||
// The key in the map is the hex of first 16 bytes of ratchetKey
|
||||
found := false
|
||||
for _, key := range ratchets {
|
||||
if bytes.Equal(key, ratchetKey) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Error("Saved ratchet key not found in loaded ratchets")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetters(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
originalHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", tmpDir)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
m, _ := NewManager()
|
||||
|
||||
if m.GetBasePath() == "" {
|
||||
t.Error("GetBasePath returned empty string")
|
||||
}
|
||||
if m.GetRatchetsPath() == "" {
|
||||
t.Error("GetRatchetsPath returned empty string")
|
||||
}
|
||||
if m.GetIdentityPath() == "" {
|
||||
t.Error("GetIdentityPath returned empty string")
|
||||
}
|
||||
if m.GetTransportIdentityPath() == "" {
|
||||
t.Error("GetTransportIdentityPath returned empty string")
|
||||
}
|
||||
if m.GetDestinationTablePath() == "" {
|
||||
t.Error("GetDestinationTablePath returned empty string")
|
||||
}
|
||||
if m.GetKnownDestinationsPath() == "" {
|
||||
t.Error("GetKnownDestinationsPath returned empty string")
|
||||
}
|
||||
}
|
||||
17
misc/wasm/go_js_wasm_exec
Executable file
17
misc/wasm/go_js_wasm_exec
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright 2018 The Go Authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the LICENSE file.
|
||||
|
||||
SOURCE="${BASH_SOURCE[0]}"
|
||||
while [ -h "$SOURCE" ]; do
|
||||
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||
SOURCE="$(readlink "$SOURCE")"
|
||||
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
|
||||
done
|
||||
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||
|
||||
# Increase the V8 stack size from the default of 984K
|
||||
# to 8192K to ensure all tests can pass without hitting
|
||||
# stack size limits.
|
||||
exec node --stack-size=8192 "$DIR/wasm_exec_node.js" "$@"
|
||||
575
misc/wasm/wasm_exec.js
Normal file
575
misc/wasm/wasm_exec.js
Normal file
@@ -0,0 +1,575 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
"use strict";
|
||||
|
||||
(() => {
|
||||
const enosys = () => {
|
||||
const err = new Error("not implemented");
|
||||
err.code = "ENOSYS";
|
||||
return err;
|
||||
};
|
||||
|
||||
if (!globalThis.fs) {
|
||||
let outputBuf = "";
|
||||
globalThis.fs = {
|
||||
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused
|
||||
writeSync(fd, buf) {
|
||||
outputBuf += decoder.decode(buf);
|
||||
const nl = outputBuf.lastIndexOf("\n");
|
||||
if (nl != -1) {
|
||||
console.log(outputBuf.substring(0, nl));
|
||||
outputBuf = outputBuf.substring(nl + 1);
|
||||
}
|
||||
return buf.length;
|
||||
},
|
||||
write(fd, buf, offset, length, position, callback) {
|
||||
if (offset !== 0 || length !== buf.length || position !== null) {
|
||||
callback(enosys());
|
||||
return;
|
||||
}
|
||||
const n = this.writeSync(fd, buf);
|
||||
callback(null, n);
|
||||
},
|
||||
chmod(path, mode, callback) { callback(enosys()); },
|
||||
chown(path, uid, gid, callback) { callback(enosys()); },
|
||||
close(fd, callback) { callback(enosys()); },
|
||||
fchmod(fd, mode, callback) { callback(enosys()); },
|
||||
fchown(fd, uid, gid, callback) { callback(enosys()); },
|
||||
fstat(fd, callback) { callback(enosys()); },
|
||||
fsync(fd, callback) { callback(null); },
|
||||
ftruncate(fd, length, callback) { callback(enosys()); },
|
||||
lchown(path, uid, gid, callback) { callback(enosys()); },
|
||||
link(path, link, callback) { callback(enosys()); },
|
||||
lstat(path, callback) { callback(enosys()); },
|
||||
mkdir(path, perm, callback) { callback(enosys()); },
|
||||
open(path, flags, mode, callback) { callback(enosys()); },
|
||||
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
|
||||
readdir(path, callback) { callback(enosys()); },
|
||||
readlink(path, callback) { callback(enosys()); },
|
||||
rename(from, to, callback) { callback(enosys()); },
|
||||
rmdir(path, callback) { callback(enosys()); },
|
||||
stat(path, callback) { callback(enosys()); },
|
||||
symlink(path, link, callback) { callback(enosys()); },
|
||||
truncate(path, length, callback) { callback(enosys()); },
|
||||
unlink(path, callback) { callback(enosys()); },
|
||||
utimes(path, atime, mtime, callback) { callback(enosys()); },
|
||||
};
|
||||
}
|
||||
|
||||
if (!globalThis.process) {
|
||||
globalThis.process = {
|
||||
getuid() { return -1; },
|
||||
getgid() { return -1; },
|
||||
geteuid() { return -1; },
|
||||
getegid() { return -1; },
|
||||
getgroups() { throw enosys(); },
|
||||
pid: -1,
|
||||
ppid: -1,
|
||||
umask() { throw enosys(); },
|
||||
cwd() { throw enosys(); },
|
||||
chdir() { throw enosys(); },
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalThis.path) {
|
||||
globalThis.path = {
|
||||
resolve(...pathSegments) {
|
||||
return pathSegments.join("/");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalThis.crypto) {
|
||||
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
|
||||
}
|
||||
|
||||
if (!globalThis.performance) {
|
||||
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
|
||||
}
|
||||
|
||||
if (!globalThis.TextEncoder) {
|
||||
throw new Error("globalThis.TextEncoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
if (!globalThis.TextDecoder) {
|
||||
throw new Error("globalThis.TextDecoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder("utf-8");
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
|
||||
globalThis.Go = class {
|
||||
constructor() {
|
||||
this.argv = ["js"];
|
||||
this.env = {};
|
||||
this.exit = (code) => {
|
||||
if (code !== 0) {
|
||||
console.warn("exit code:", code);
|
||||
}
|
||||
};
|
||||
this._exitPromise = new Promise((resolve) => {
|
||||
this._resolveExitPromise = resolve;
|
||||
});
|
||||
this._pendingEvent = null;
|
||||
this._scheduledTimeouts = new Map();
|
||||
this._nextCallbackTimeoutID = 1;
|
||||
|
||||
const setInt64 = (addr, v) => {
|
||||
this.mem.setUint32(addr + 0, v, true);
|
||||
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
|
||||
}
|
||||
|
||||
const setInt32 = (addr, v) => {
|
||||
this.mem.setUint32(addr + 0, v, true);
|
||||
}
|
||||
|
||||
const getInt64 = (addr) => {
|
||||
const low = this.mem.getUint32(addr + 0, true);
|
||||
const high = this.mem.getInt32(addr + 4, true);
|
||||
return low + high * 4294967296;
|
||||
}
|
||||
|
||||
const loadValue = (addr) => {
|
||||
const f = this.mem.getFloat64(addr, true);
|
||||
if (f === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (!isNaN(f)) {
|
||||
return f;
|
||||
}
|
||||
|
||||
const id = this.mem.getUint32(addr, true);
|
||||
return this._values[id];
|
||||
}
|
||||
|
||||
const storeValue = (addr, v) => {
|
||||
const nanHead = 0x7FF80000;
|
||||
|
||||
if (typeof v === "number" && v !== 0) {
|
||||
if (isNaN(v)) {
|
||||
this.mem.setUint32(addr + 4, nanHead, true);
|
||||
this.mem.setUint32(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
this.mem.setFloat64(addr, v, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (v === undefined) {
|
||||
this.mem.setFloat64(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
|
||||
let id = this._ids.get(v);
|
||||
if (id === undefined) {
|
||||
id = this._idPool.pop();
|
||||
if (id === undefined) {
|
||||
id = this._values.length;
|
||||
}
|
||||
this._values[id] = v;
|
||||
this._goRefCounts[id] = 0;
|
||||
this._ids.set(v, id);
|
||||
}
|
||||
this._goRefCounts[id]++;
|
||||
let typeFlag = 0;
|
||||
switch (typeof v) {
|
||||
case "object":
|
||||
if (v !== null) {
|
||||
typeFlag = 1;
|
||||
}
|
||||
break;
|
||||
case "string":
|
||||
typeFlag = 2;
|
||||
break;
|
||||
case "symbol":
|
||||
typeFlag = 3;
|
||||
break;
|
||||
case "function":
|
||||
typeFlag = 4;
|
||||
break;
|
||||
}
|
||||
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
|
||||
this.mem.setUint32(addr, id, true);
|
||||
}
|
||||
|
||||
const loadSlice = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
|
||||
}
|
||||
|
||||
const loadSliceOfValues = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
const a = new Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
a[i] = loadValue(array + i * 8);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
const loadString = (addr) => {
|
||||
const saddr = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
|
||||
}
|
||||
|
||||
const testCallExport = (a, b) => {
|
||||
this._inst.exports.testExport0();
|
||||
return this._inst.exports.testExport(a, b);
|
||||
}
|
||||
|
||||
const timeOrigin = Date.now() - performance.now();
|
||||
this.importObject = {
|
||||
_gotest: {
|
||||
add: (a, b) => a + b,
|
||||
callExport: testCallExport,
|
||||
},
|
||||
gojs: {
|
||||
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
|
||||
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
|
||||
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
|
||||
// This changes the SP, thus we have to update the SP used by the imported function.
|
||||
|
||||
// func wasmExit(code int32)
|
||||
"runtime.wasmExit": (sp) => {
|
||||
sp >>>= 0;
|
||||
const code = this.mem.getInt32(sp + 8, true);
|
||||
this.exited = true;
|
||||
delete this._inst;
|
||||
delete this._values;
|
||||
delete this._goRefCounts;
|
||||
delete this._ids;
|
||||
delete this._idPool;
|
||||
this.exit(code);
|
||||
},
|
||||
|
||||
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
|
||||
"runtime.wasmWrite": (sp) => {
|
||||
sp >>>= 0;
|
||||
const fd = getInt64(sp + 8);
|
||||
const p = getInt64(sp + 16);
|
||||
const n = this.mem.getInt32(sp + 24, true);
|
||||
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
|
||||
},
|
||||
|
||||
// func resetMemoryDataView()
|
||||
"runtime.resetMemoryDataView": (sp) => {
|
||||
sp >>>= 0;
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
},
|
||||
|
||||
// func nanotime1() int64
|
||||
"runtime.nanotime1": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
|
||||
},
|
||||
|
||||
// func walltime() (sec int64, nsec int32)
|
||||
"runtime.walltime": (sp) => {
|
||||
sp >>>= 0;
|
||||
const msec = (new Date).getTime();
|
||||
setInt64(sp + 8, msec / 1000);
|
||||
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
|
||||
},
|
||||
|
||||
// func scheduleTimeoutEvent(delay int64) int32
|
||||
"runtime.scheduleTimeoutEvent": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this._nextCallbackTimeoutID;
|
||||
this._nextCallbackTimeoutID++;
|
||||
this._scheduledTimeouts.set(id, setTimeout(
|
||||
() => {
|
||||
this._resume();
|
||||
while (this._scheduledTimeouts.has(id)) {
|
||||
// for some reason Go failed to register the timeout event, log and try again
|
||||
// (temporary workaround for https://github.com/golang/go/issues/28975)
|
||||
console.warn("scheduleTimeoutEvent: missed timeout event");
|
||||
this._resume();
|
||||
}
|
||||
},
|
||||
getInt64(sp + 8),
|
||||
));
|
||||
this.mem.setInt32(sp + 16, id, true);
|
||||
},
|
||||
|
||||
// func clearTimeoutEvent(id int32)
|
||||
"runtime.clearTimeoutEvent": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this.mem.getInt32(sp + 8, true);
|
||||
clearTimeout(this._scheduledTimeouts.get(id));
|
||||
this._scheduledTimeouts.delete(id);
|
||||
},
|
||||
|
||||
// func getRandomData(r []byte)
|
||||
"runtime.getRandomData": (sp) => {
|
||||
sp >>>= 0;
|
||||
crypto.getRandomValues(loadSlice(sp + 8));
|
||||
},
|
||||
|
||||
// func finalizeRef(v ref)
|
||||
"syscall/js.finalizeRef": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this.mem.getUint32(sp + 8, true);
|
||||
this._goRefCounts[id]--;
|
||||
if (this._goRefCounts[id] === 0) {
|
||||
const v = this._values[id];
|
||||
this._values[id] = null;
|
||||
this._ids.delete(v);
|
||||
this._idPool.push(id);
|
||||
}
|
||||
},
|
||||
|
||||
// func stringVal(value string) ref
|
||||
"syscall/js.stringVal": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, loadString(sp + 8));
|
||||
},
|
||||
|
||||
// func valueGet(v ref, p string) ref
|
||||
"syscall/js.valueGet": (sp) => {
|
||||
sp >>>= 0;
|
||||
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 32, result);
|
||||
},
|
||||
|
||||
// func valueSet(v ref, p string, x ref)
|
||||
"syscall/js.valueSet": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
|
||||
},
|
||||
|
||||
// func valueDelete(v ref, p string)
|
||||
"syscall/js.valueDelete": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
|
||||
},
|
||||
|
||||
// func valueIndex(v ref, i int) ref
|
||||
"syscall/js.valueIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
|
||||
},
|
||||
|
||||
// valueSetIndex(v ref, i int, x ref)
|
||||
"syscall/js.valueSetIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
|
||||
},
|
||||
|
||||
// func valueCall(v ref, m string, args []ref) (ref, bool)
|
||||
"syscall/js.valueCall": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const m = Reflect.get(v, loadString(sp + 16));
|
||||
const args = loadSliceOfValues(sp + 32);
|
||||
const result = Reflect.apply(m, v, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, result);
|
||||
this.mem.setUint8(sp + 64, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, err);
|
||||
this.mem.setUint8(sp + 64, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueInvoke(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueInvoke": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.apply(v, undefined, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueNew(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueNew": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.construct(v, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueLength(v ref) int
|
||||
"syscall/js.valueLength": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
|
||||
},
|
||||
|
||||
// valuePrepareString(v ref) (ref, int)
|
||||
"syscall/js.valuePrepareString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = encoder.encode(String(loadValue(sp + 8)));
|
||||
storeValue(sp + 16, str);
|
||||
setInt64(sp + 24, str.length);
|
||||
},
|
||||
|
||||
// valueLoadString(v ref, b []byte)
|
||||
"syscall/js.valueLoadString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = loadValue(sp + 8);
|
||||
loadSlice(sp + 16).set(str);
|
||||
},
|
||||
|
||||
// func valueInstanceOf(v ref, t ref) bool
|
||||
"syscall/js.valueInstanceOf": (sp) => {
|
||||
sp >>>= 0;
|
||||
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
|
||||
},
|
||||
|
||||
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||
"syscall/js.copyBytesToGo": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadSlice(sp + 8);
|
||||
const src = loadValue(sp + 32);
|
||||
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
// func copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||
"syscall/js.copyBytesToJS": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadValue(sp + 8);
|
||||
const src = loadSlice(sp + 16);
|
||||
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
"debug": (value) => {
|
||||
console.log(value);
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async run(instance) {
|
||||
if (!(instance instanceof WebAssembly.Instance)) {
|
||||
throw new Error("Go.run: WebAssembly.Instance expected");
|
||||
}
|
||||
this._inst = instance;
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
this._values = [ // JS values that Go currently has references to, indexed by reference id
|
||||
NaN,
|
||||
0,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
globalThis,
|
||||
this,
|
||||
];
|
||||
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
|
||||
this._ids = new Map([ // mapping from JS values to reference ids
|
||||
[0, 1],
|
||||
[null, 2],
|
||||
[true, 3],
|
||||
[false, 4],
|
||||
[globalThis, 5],
|
||||
[this, 6],
|
||||
]);
|
||||
this._idPool = []; // unused ids that have been garbage collected
|
||||
this.exited = false; // whether the Go program has exited
|
||||
|
||||
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
|
||||
let offset = 4096;
|
||||
|
||||
const strPtr = (str) => {
|
||||
const ptr = offset;
|
||||
const bytes = encoder.encode(str + "\0");
|
||||
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
|
||||
offset += bytes.length;
|
||||
if (offset % 8 !== 0) {
|
||||
offset += 8 - (offset % 8);
|
||||
}
|
||||
return ptr;
|
||||
};
|
||||
|
||||
const argc = this.argv.length;
|
||||
|
||||
const argvPtrs = [];
|
||||
this.argv.forEach((arg) => {
|
||||
argvPtrs.push(strPtr(arg));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const keys = Object.keys(this.env).sort();
|
||||
keys.forEach((key) => {
|
||||
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const argv = offset;
|
||||
argvPtrs.forEach((ptr) => {
|
||||
this.mem.setUint32(offset, ptr, true);
|
||||
this.mem.setUint32(offset + 4, 0, true);
|
||||
offset += 8;
|
||||
});
|
||||
|
||||
// The linker guarantees global data starts from at least wasmMinDataAddr.
|
||||
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
|
||||
const wasmMinDataAddr = 4096 + 8192;
|
||||
if (offset >= wasmMinDataAddr) {
|
||||
throw new Error("total length of command line and environment variables exceeds limit");
|
||||
}
|
||||
|
||||
this._inst.exports.run(argc, argv);
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
await this._exitPromise;
|
||||
}
|
||||
|
||||
_resume() {
|
||||
if (this.exited) {
|
||||
throw new Error("Go program has already exited");
|
||||
}
|
||||
this._inst.exports.resume();
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
}
|
||||
|
||||
_makeFuncWrapper(id) {
|
||||
const go = this;
|
||||
return function () {
|
||||
const event = { id: id, this: this, args: arguments };
|
||||
go._pendingEvent = event;
|
||||
go._resume();
|
||||
return event.result;
|
||||
};
|
||||
}
|
||||
}
|
||||
})();
|
||||
41
misc/wasm/wasm_exec_node.js
Normal file
41
misc/wasm/wasm_exec_node.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright 2021 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
"use strict";
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
globalThis.require = require;
|
||||
globalThis.fs = require("fs");
|
||||
globalThis.path = require("path");
|
||||
globalThis.TextEncoder = require("util").TextEncoder;
|
||||
globalThis.TextDecoder = require("util").TextDecoder;
|
||||
|
||||
globalThis.performance ??= require("performance");
|
||||
|
||||
globalThis.crypto ??= require("crypto");
|
||||
|
||||
require("./wasm_exec");
|
||||
|
||||
const go = new Go();
|
||||
go.argv = process.argv.slice(2);
|
||||
go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
|
||||
go.exit = process.exit;
|
||||
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
|
||||
process.on("exit", (code) => { // Node.js exits if no event handler is pending
|
||||
if (code === 0 && !go.exited) {
|
||||
// deadlock, make Go print error and stack traces
|
||||
go._pendingEvent = { id: 0 };
|
||||
go._resume();
|
||||
}
|
||||
});
|
||||
return go.run(result.instance);
|
||||
}).catch((err) => {
|
||||
// bearer:disable javascript_lang_logger_leak
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package announce
|
||||
|
||||
import (
|
||||
@@ -6,12 +8,13 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -48,15 +51,10 @@ const (
|
||||
MAX_RETRIES = 3
|
||||
)
|
||||
|
||||
type AnnounceHandler interface {
|
||||
AspectFilter() []string
|
||||
ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte) error
|
||||
ReceivePathResponses() bool
|
||||
}
|
||||
|
||||
type Announce struct {
|
||||
mutex *sync.RWMutex
|
||||
destinationHash []byte
|
||||
destinationName string
|
||||
identity *identity.Identity
|
||||
appData []byte
|
||||
config *common.ReticulumConfig
|
||||
@@ -65,38 +63,46 @@ type Announce struct {
|
||||
signature []byte
|
||||
pathResponse bool
|
||||
retries int
|
||||
handlers []AnnounceHandler
|
||||
handlers []Handler
|
||||
ratchetID []byte
|
||||
packet []byte
|
||||
hash []byte
|
||||
}
|
||||
|
||||
func New(dest *identity.Identity, appData []byte, pathResponse bool, config *common.ReticulumConfig) (*Announce, error) {
|
||||
func New(dest *identity.Identity, destinationHash []byte, destinationName string, appData []byte, pathResponse bool, config *common.ReticulumConfig) (*Announce, error) {
|
||||
if dest == nil {
|
||||
return nil, errors.New("destination identity required")
|
||||
}
|
||||
|
||||
a := &Announce{
|
||||
mutex: &sync.RWMutex{},
|
||||
identity: dest,
|
||||
appData: appData,
|
||||
config: config,
|
||||
hops: 0,
|
||||
timestamp: time.Now().Unix(),
|
||||
pathResponse: pathResponse,
|
||||
retries: 0,
|
||||
handlers: make([]AnnounceHandler, 0),
|
||||
if len(destinationHash) == 0 {
|
||||
return nil, errors.New("destination hash required")
|
||||
}
|
||||
|
||||
// Generate truncated hash from public key
|
||||
pubKey := dest.GetPublicKey()
|
||||
hash := sha256.Sum256(pubKey)
|
||||
a.destinationHash = hash[:identity.TRUNCATED_HASHLENGTH/8]
|
||||
if destinationName == "" {
|
||||
return nil, errors.New("destination name required")
|
||||
}
|
||||
|
||||
a := &Announce{
|
||||
mutex: &sync.RWMutex{},
|
||||
identity: dest,
|
||||
destinationHash: destinationHash,
|
||||
destinationName: destinationName,
|
||||
appData: appData,
|
||||
config: config,
|
||||
hops: 0,
|
||||
timestamp: time.Now().Unix(),
|
||||
pathResponse: pathResponse,
|
||||
retries: 0,
|
||||
handlers: make([]Handler, 0),
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -113,46 +119,46 @@ func (a *Announce) Propagate(interfaces []common.NetworkInterface) error {
|
||||
a.mutex.RLock()
|
||||
defer a.mutex.RUnlock()
|
||||
|
||||
log.Printf("[DEBUG-7] Propagating announce across %d interfaces", len(interfaces))
|
||||
debug.Log(debug.DEBUG_TRACE, "Propagating announce across interfaces", "count", len(interfaces))
|
||||
|
||||
var packet []byte
|
||||
if a.packet != nil {
|
||||
log.Printf("[DEBUG-7] Using cached packet (%d bytes)", len(a.packet))
|
||||
debug.Log(debug.DEBUG_TRACE, "Using cached packet", "bytes", len(a.packet))
|
||||
packet = a.packet
|
||||
} else {
|
||||
log.Printf("[DEBUG-7] Creating new packet")
|
||||
debug.Log(debug.DEBUG_TRACE, "Creating new packet")
|
||||
packet = a.CreatePacket()
|
||||
a.packet = packet
|
||||
}
|
||||
|
||||
for _, iface := range interfaces {
|
||||
if !iface.IsEnabled() {
|
||||
log.Printf("[DEBUG-7] Skipping disabled interface: %s", iface.GetName())
|
||||
debug.Log(debug.DEBUG_TRACE, "Skipping disabled interface", "name", iface.GetName())
|
||||
continue
|
||||
}
|
||||
if !iface.GetBandwidthAvailable() {
|
||||
log.Printf("[DEBUG-7] Skipping interface with insufficient bandwidth: %s", iface.GetName())
|
||||
debug.Log(debug.DEBUG_TRACE, "Skipping interface with insufficient bandwidth", "name", iface.GetName())
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-7] Sending announce on interface %s", iface.GetName())
|
||||
debug.Log(debug.DEBUG_TRACE, "Sending announce on interface", "name", iface.GetName())
|
||||
if err := iface.Send(packet, ""); err != nil {
|
||||
log.Printf("[DEBUG-7] Failed to send on interface %s: %v", iface.GetName(), err)
|
||||
debug.Log(debug.DEBUG_TRACE, "Failed to send on interface", "name", iface.GetName(), "error", err)
|
||||
return fmt.Errorf("failed to propagate on interface %s: %w", iface.GetName(), err)
|
||||
}
|
||||
log.Printf("[DEBUG-7] Successfully sent announce on interface %s", iface.GetName())
|
||||
debug.Log(debug.DEBUG_TRACE, "Successfully sent announce on interface", "name", iface.GetName())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Announce) RegisterHandler(handler AnnounceHandler) {
|
||||
func (a *Announce) RegisterHandler(handler Handler) {
|
||||
a.mutex.Lock()
|
||||
defer a.mutex.Unlock()
|
||||
a.handlers = append(a.handlers, handler)
|
||||
}
|
||||
|
||||
func (a *Announce) DeregisterHandler(handler AnnounceHandler) {
|
||||
func (a *Announce) DeregisterHandler(handler Handler) {
|
||||
a.mutex.Lock()
|
||||
defer a.mutex.Unlock()
|
||||
for i, h := range a.handlers {
|
||||
@@ -167,13 +173,13 @@ func (a *Announce) HandleAnnounce(data []byte) error {
|
||||
a.mutex.Lock()
|
||||
defer a.mutex.Unlock()
|
||||
|
||||
log.Printf("[DEBUG-7] Handling announce packet of %d bytes", len(data))
|
||||
debug.Log(debug.DEBUG_TRACE, "Handling announce packet", "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))
|
||||
debug.Log(debug.DEBUG_TRACE, "Invalid announce data length", "bytes", len(data), "minimum", 170)
|
||||
return errors.New("invalid announce data length")
|
||||
}
|
||||
|
||||
@@ -186,7 +192,7 @@ func (a *Announce) HandleAnnounce(data []byte) error {
|
||||
// Get hop count
|
||||
hopCount := header[1]
|
||||
if hopCount > MAX_HOPS {
|
||||
log.Printf("[DEBUG-7] Announce exceeded max hops: %d", hopCount)
|
||||
debug.Log(debug.DEBUG_TRACE, "Announce exceeded max hops", "hops", hopCount)
|
||||
return errors.New("announce exceeded maximum hop count")
|
||||
}
|
||||
|
||||
@@ -205,8 +211,7 @@ func (a *Announce) HandleAnnounce(data []byte) error {
|
||||
contextByte = data[34]
|
||||
packetData = data[35:]
|
||||
|
||||
log.Printf("[DEBUG-7] Header type 2 announce: destHash=%x, transportID=%x, context=%d",
|
||||
destHash, transportID, contextByte)
|
||||
debug.Log(debug.DEBUG_TRACE, "Header type 2 announce", "destHash", fmt.Sprintf("%x", destHash), "transportID", fmt.Sprintf("%x", transportID), "context", contextByte)
|
||||
} else {
|
||||
// Header type 1 format: header(2) + desthash(16) + context(1) + data
|
||||
if len(data) < 19 {
|
||||
@@ -216,14 +221,13 @@ func (a *Announce) HandleAnnounce(data []byte) error {
|
||||
contextByte = data[18]
|
||||
packetData = data[19:]
|
||||
|
||||
log.Printf("[DEBUG-7] Header type 1 announce: destHash=%x, context=%d",
|
||||
destHash, contextByte)
|
||||
debug.Log(debug.DEBUG_TRACE, "Header type 1 announce", "destHash", fmt.Sprintf("%x", destHash), "context", 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
|
||||
// Public Key (32) + Signing Key (32) + Name Hash (10) + Random Hash (10) + Ratchet (32) + Signature (64) + App Data
|
||||
|
||||
if len(packetData) < 148 { // 32 + 32 + 10 + 10 + 64
|
||||
if len(packetData) < 180 { // 32 + 32 + 10 + 10 + 32 + 64
|
||||
return errors.New("announce data too short")
|
||||
}
|
||||
|
||||
@@ -232,17 +236,14 @@ func (a *Announce) HandleAnnounce(data []byte) error {
|
||||
signKey := packetData[32:64]
|
||||
nameHash := packetData[64:74]
|
||||
randomHash := packetData[74:84]
|
||||
ratchetData := packetData[84:116]
|
||||
signature := packetData[116:180]
|
||||
appData := packetData[180:]
|
||||
|
||||
// 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))
|
||||
debug.Log(debug.DEBUG_TRACE, "Announce fields", "encKey", fmt.Sprintf("%x", encKey), "signKey", fmt.Sprintf("%x", signKey))
|
||||
debug.Log(debug.DEBUG_TRACE, "Name and random hash", "nameHash", fmt.Sprintf("%x", nameHash), "randomHash", fmt.Sprintf("%x", randomHash))
|
||||
debug.Log(debug.DEBUG_TRACE, "Ratchet data", "ratchet", fmt.Sprintf("%x", ratchetData[:8]))
|
||||
debug.Log(debug.DEBUG_TRACE, "Signature and app data", "signature", fmt.Sprintf("%x", signature[:8]), "appDataLen", len(appData))
|
||||
|
||||
// Get the destination hash from header
|
||||
var destHash []byte
|
||||
@@ -268,6 +269,7 @@ func (a *Announce) HandleAnnounce(data []byte) error {
|
||||
signedData = append(signedData, signKey...)
|
||||
signedData = append(signedData, nameHash...)
|
||||
signedData = append(signedData, randomHash...)
|
||||
signedData = append(signedData, ratchetData...)
|
||||
signedData = append(signedData, appData...)
|
||||
|
||||
if !announcedIdentity.Verify(signedData, signature) {
|
||||
@@ -277,7 +279,7 @@ func (a *Announce) HandleAnnounce(data []byte) error {
|
||||
// Process with handlers
|
||||
for _, handler := range a.handlers {
|
||||
if handler.ReceivePathResponses() || !a.pathResponse {
|
||||
if err := handler.ReceivedAnnounce(destHash, announcedIdentity, appData); err != nil {
|
||||
if err := handler.ReceivedAnnounce(destHash, announcedIdentity, appData, hopCount); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -296,11 +298,7 @@ func (a *Announce) RequestPath(destHash []byte, onInterface common.NetworkInterf
|
||||
packet = append(packet, byte(0)) // Initial hop count
|
||||
|
||||
// Send path request
|
||||
if err := onInterface.Send(packet, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return onInterface.Send(packet, "")
|
||||
}
|
||||
|
||||
// CreateHeader creates a Reticulum packet header according to spec
|
||||
@@ -318,100 +316,105 @@ 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)][Context (1 byte)][Announce Data]
|
||||
// Announce Data Structure:
|
||||
// [Public Key (64 bytes)][Name Hash (10 bytes)][Random Hash (10 bytes)][Ratchet (32 bytes optional)][Signature (64 bytes)][App Data]
|
||||
|
||||
// 2. Destination Hash
|
||||
destHash := a.destinationHash
|
||||
if len(destHash) > 16 {
|
||||
destHash = destHash[:16]
|
||||
}
|
||||
|
||||
// 3. Announce Data
|
||||
// 3.1 Public Key (full 64 bytes - not split into enc/sign keys in packet)
|
||||
pubKey := a.identity.GetPublicKey()
|
||||
if len(pubKey) != 64 {
|
||||
debug.Log(debug.DEBUG_TRACE, "Invalid public key length", "expected", 64, "got", len(pubKey))
|
||||
}
|
||||
|
||||
// 3.2 Name Hash
|
||||
nameHash := sha256.Sum256([]byte(a.destinationName))
|
||||
nameHash10 := nameHash[:10]
|
||||
|
||||
// 3.3 Random Hash (5 bytes random + 5 bytes timestamp)
|
||||
randomHash := make([]byte, 10)
|
||||
_, err := rand.Read(randomHash[:5])
|
||||
if err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to read random bytes for announce", "error", err)
|
||||
}
|
||||
// Add 5 bytes of timestamp
|
||||
timeBytes := make([]byte, 8)
|
||||
// #nosec G115 - Unix timestamp is always positive, no overflow risk
|
||||
binary.BigEndian.PutUint64(timeBytes, uint64(time.Now().Unix()))
|
||||
copy(randomHash[5:], timeBytes[:5])
|
||||
|
||||
// 3.4 Ratchet (only include if exists)
|
||||
var ratchetData []byte
|
||||
currentRatchetKey := a.identity.GetCurrentRatchetKey()
|
||||
if currentRatchetKey != nil {
|
||||
ratchetPub, err := curve25519.X25519(currentRatchetKey, curve25519.Basepoint)
|
||||
if err == nil {
|
||||
ratchetData = make([]byte, 32)
|
||||
copy(ratchetData, ratchetPub)
|
||||
}
|
||||
}
|
||||
|
||||
// Determine context flag based on whether ratchet exists
|
||||
contextFlag := byte(0)
|
||||
if len(ratchetData) > 0 {
|
||||
contextFlag = 1 // FLAG_SET
|
||||
}
|
||||
|
||||
// 1. Create Header - Use HEADER_TYPE_1
|
||||
header := CreateHeader(
|
||||
IFAC_NONE,
|
||||
HEADER_TYPE_2, // Use header type 2 for announces
|
||||
0, // No context flag
|
||||
HEADER_TYPE_1,
|
||||
contextFlag,
|
||||
PROP_TYPE_BROADCAST,
|
||||
DEST_TYPE_SINGLE,
|
||||
PACKET_TYPE_ANNOUNCE,
|
||||
a.hops,
|
||||
)
|
||||
|
||||
packet := header
|
||||
|
||||
// Add destination hash (16 bytes)
|
||||
packet = append(packet, a.destinationHash...)
|
||||
|
||||
// If using header type 2, add transport ID (16 bytes)
|
||||
// For broadcast announces, this is filled with zeroes
|
||||
transportID := make([]byte, 16)
|
||||
packet = append(packet, transportID...)
|
||||
|
||||
// Add context byte
|
||||
packet = append(packet, byte(0)) // Context byte, 0 for announces
|
||||
|
||||
// Add public key parts (32 bytes each)
|
||||
pubKey := a.identity.GetPublicKey()
|
||||
encKey := pubKey[:32] // Encryption key
|
||||
signKey := pubKey[32:] // Signing key
|
||||
|
||||
// Start building data portion according to spec
|
||||
data := make([]byte, 0, 32+32+10+10+32+64+len(a.appData))
|
||||
data = append(data, encKey...) // Encryption key (32 bytes)
|
||||
data = append(data, signKey...) // Signing key (32 bytes)
|
||||
|
||||
// Determine if this is a node announce based on appData format
|
||||
var appName string
|
||||
if len(a.appData) > 2 && a.appData[0] == 0x93 {
|
||||
// This is a node announcement
|
||||
appName = "reticulum.node"
|
||||
} else if len(a.appData) > 3 && a.appData[0] == 0x92 && a.appData[1] == 0xc4 {
|
||||
nameLen := int(a.appData[2])
|
||||
if 3+nameLen <= len(a.appData) {
|
||||
appName = string(a.appData[3 : 3+nameLen])
|
||||
} else {
|
||||
appName = fmt.Sprintf("%s.%s", a.config.AppName, a.config.AppAspect)
|
||||
}
|
||||
} else {
|
||||
// Default fallback using config values
|
||||
appName = fmt.Sprintf("%s.%s", a.config.AppName, a.config.AppAspect)
|
||||
// 4. Context Byte
|
||||
contextByte := byte(0)
|
||||
if a.pathResponse {
|
||||
contextByte = 0x0B // PATH_RESPONSE context
|
||||
}
|
||||
|
||||
// Add name hash (10 bytes)
|
||||
nameHash := sha256.Sum256([]byte(appName))
|
||||
nameHash10 := nameHash[:10]
|
||||
log.Printf("[DEBUG-6] Using name hash for '%s': %x", appName, nameHash10)
|
||||
data = append(data, nameHash10...)
|
||||
|
||||
// Add random hash (10 bytes) - 5 bytes random + 5 bytes time
|
||||
randomHash := make([]byte, 10)
|
||||
rand.Read(randomHash[:5])
|
||||
timeBytes := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(timeBytes, uint64(time.Now().Unix()))
|
||||
copy(randomHash[5:], timeBytes[:5])
|
||||
data = append(data, randomHash...)
|
||||
|
||||
// Add ratchet ID (32 bytes) - required in the packet format
|
||||
if a.ratchetID != nil {
|
||||
data = append(data, a.ratchetID...)
|
||||
} else {
|
||||
// If there's no ratchet, add 32 zero bytes as placeholder
|
||||
data = append(data, make([]byte, 32)...)
|
||||
}
|
||||
|
||||
// Create validation data for signature
|
||||
// Signature consists of destination hash, public keys, name hash, random hash, and app data
|
||||
// 3.5 Signature
|
||||
// The signature is calculated over: Dest Hash + Public Key (64 bytes) + Name Hash + Random Hash + Ratchet (if exists) + App Data
|
||||
validationData := make([]byte, 0)
|
||||
validationData = append(validationData, a.destinationHash...)
|
||||
validationData = append(validationData, encKey...)
|
||||
validationData = append(validationData, signKey...)
|
||||
validationData = append(validationData, destHash...)
|
||||
validationData = append(validationData, pubKey...)
|
||||
validationData = append(validationData, nameHash10...)
|
||||
validationData = append(validationData, randomHash...)
|
||||
validationData = append(validationData, a.appData...)
|
||||
|
||||
// Add signature (64 bytes)
|
||||
signature := a.identity.Sign(validationData)
|
||||
data = append(data, signature...)
|
||||
|
||||
// Add app data
|
||||
if len(a.appData) > 0 {
|
||||
data = append(data, a.appData...)
|
||||
if len(ratchetData) > 0 {
|
||||
validationData = append(validationData, ratchetData...)
|
||||
}
|
||||
validationData = append(validationData, a.appData...)
|
||||
signature := a.identity.Sign(validationData)
|
||||
|
||||
// Combine header and data
|
||||
packet = append(packet, data...)
|
||||
debug.Log(debug.DEBUG_TRACE, "Creating announce packet", "destHash", fmt.Sprintf("%x", destHash), "pubKeyLen", len(pubKey), "nameHash", fmt.Sprintf("%x", nameHash10), "randomHash", fmt.Sprintf("%x", randomHash), "ratchetLen", len(ratchetData), "sigLen", len(signature), "appDataLen", len(a.appData))
|
||||
|
||||
// 5. Assemble the packet (HEADER_TYPE_1 format)
|
||||
packet := make([]byte, 0)
|
||||
packet = append(packet, header...)
|
||||
packet = append(packet, destHash...)
|
||||
packet = append(packet, contextByte)
|
||||
packet = append(packet, pubKey...)
|
||||
packet = append(packet, nameHash10...)
|
||||
packet = append(packet, randomHash...)
|
||||
if len(ratchetData) > 0 {
|
||||
packet = append(packet, ratchetData...)
|
||||
}
|
||||
packet = append(packet, signature...)
|
||||
packet = append(packet, a.appData...)
|
||||
|
||||
debug.Log(debug.DEBUG_TRACE, "Final announce packet", "totalBytes", len(packet), "ratchetLen", len(ratchetData), "appDataLen", len(a.appData))
|
||||
|
||||
return packet
|
||||
}
|
||||
@@ -435,7 +438,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...)
|
||||
|
||||
@@ -446,12 +449,11 @@ func NewAnnouncePacket(pubKey []byte, appData []byte, announceID []byte) *Announ
|
||||
}
|
||||
|
||||
// NewAnnounce creates a new announce packet for a destination
|
||||
func NewAnnounce(identity *identity.Identity, appData []byte, ratchetID []byte, pathResponse bool, config *common.ReticulumConfig) (*Announce, error) {
|
||||
log.Printf("[DEBUG-7] Creating new announce: appDataLen=%d, hasRatchet=%v, pathResponse=%v",
|
||||
len(appData), ratchetID != nil, pathResponse)
|
||||
func NewAnnounce(identity *identity.Identity, destinationHash []byte, appData []byte, ratchetID []byte, pathResponse bool, config *common.ReticulumConfig) (*Announce, error) {
|
||||
debug.Log(debug.DEBUG_TRACE, "Creating new announce", "destHash", fmt.Sprintf("%x", destinationHash), "appDataLen", len(appData), "hasRatchet", ratchetID != nil, "pathResponse", pathResponse)
|
||||
|
||||
if identity == nil {
|
||||
log.Printf("[DEBUG-7] Error: nil identity provided")
|
||||
debug.Log(debug.DEBUG_ERROR, "Nil identity provided")
|
||||
return nil, errors.New("identity cannot be nil")
|
||||
}
|
||||
|
||||
@@ -459,8 +461,12 @@ func NewAnnounce(identity *identity.Identity, appData []byte, ratchetID []byte,
|
||||
return nil, errors.New("config cannot be nil")
|
||||
}
|
||||
|
||||
destHash := identity.Hash()
|
||||
log.Printf("[DEBUG-7] Generated destination hash: %x", destHash)
|
||||
if len(destinationHash) == 0 {
|
||||
return nil, errors.New("destination hash cannot be empty")
|
||||
}
|
||||
|
||||
destHash := destinationHash
|
||||
debug.Log(debug.DEBUG_TRACE, "Using provided destination hash", "destHash", fmt.Sprintf("%x", destHash))
|
||||
|
||||
a := &Announce{
|
||||
identity: identity,
|
||||
@@ -470,12 +476,11 @@ func NewAnnounce(identity *identity.Identity, appData []byte, ratchetID []byte,
|
||||
destinationHash: destHash,
|
||||
hops: 0,
|
||||
mutex: &sync.RWMutex{},
|
||||
handlers: make([]AnnounceHandler, 0),
|
||||
handlers: make([]Handler, 0),
|
||||
config: config,
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-7] Created announce object: destHash=%x, hops=%d",
|
||||
a.destinationHash, a.hops)
|
||||
debug.Log(debug.DEBUG_TRACE, "Created announce object", "destHash", fmt.Sprintf("%x", a.destinationHash), "hops", a.hops)
|
||||
|
||||
// Create initial packet
|
||||
packet := a.CreatePacket()
|
||||
@@ -483,7 +488,7 @@ func NewAnnounce(identity *identity.Identity, appData []byte, ratchetID []byte,
|
||||
|
||||
// Generate hash
|
||||
hash := a.Hash()
|
||||
log.Printf("[DEBUG-7] Generated announce hash: %x", hash)
|
||||
debug.Log(debug.DEBUG_TRACE, "Generated announce hash", "hash", fmt.Sprintf("%x", hash))
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
123
pkg/announce/announce_test.go
Normal file
123
pkg/announce/announce_test.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package announce
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
||||
)
|
||||
|
||||
type mockAnnounceHandler struct {
|
||||
received bool
|
||||
}
|
||||
|
||||
func (m *mockAnnounceHandler) AspectFilter() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAnnounceHandler) ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte, hops uint8) error {
|
||||
m.received = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAnnounceHandler) ReceivePathResponses() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type mockInterface struct {
|
||||
common.BaseInterface
|
||||
sent bool
|
||||
}
|
||||
|
||||
func (m *mockInterface) Send(data []byte, address string) error {
|
||||
m.sent = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockInterface) GetBandwidthAvailable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *mockInterface) IsEnabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func TestNewAnnounce(t *testing.T) {
|
||||
id, _ := identity.New()
|
||||
destHash := make([]byte, 16)
|
||||
config := &common.ReticulumConfig{}
|
||||
|
||||
ann, err := New(id, destHash, "testapp", []byte("appdata"), false, config)
|
||||
if err != nil {
|
||||
t.Fatalf("New failed: %v", err)
|
||||
}
|
||||
if ann == nil {
|
||||
t.Fatal("New returned nil")
|
||||
}
|
||||
|
||||
if !bytes.Equal(ann.destinationHash, destHash) {
|
||||
t.Error("Destination hash doesn't match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAndHandleAnnounce(t *testing.T) {
|
||||
id, _ := identity.New()
|
||||
destHash := make([]byte, 16)
|
||||
config := &common.ReticulumConfig{}
|
||||
|
||||
ann, _ := New(id, destHash, "testapp", []byte("appdata"), false, config)
|
||||
packet := ann.CreatePacket()
|
||||
|
||||
handler := &mockAnnounceHandler{}
|
||||
ann.RegisterHandler(handler)
|
||||
|
||||
err := ann.HandleAnnounce(packet)
|
||||
if err != nil {
|
||||
t.Fatalf("HandleAnnounce failed: %v", err)
|
||||
}
|
||||
|
||||
if !handler.received {
|
||||
t.Error("Handler did not receive announce")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPropagate(t *testing.T) {
|
||||
id, _ := identity.New()
|
||||
destHash := make([]byte, 16)
|
||||
config := &common.ReticulumConfig{}
|
||||
|
||||
ann, _ := New(id, destHash, "testapp", []byte("appdata"), false, config)
|
||||
|
||||
iface := &mockInterface{}
|
||||
iface.Name = "testiface"
|
||||
iface.Online = true
|
||||
iface.Enabled = true
|
||||
|
||||
err := ann.Propagate([]common.NetworkInterface{iface})
|
||||
if err != nil {
|
||||
t.Fatalf("Propagate failed: %v", err)
|
||||
}
|
||||
|
||||
if !iface.sent {
|
||||
t.Error("Packet was not sent on interface")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerRegistration(t *testing.T) {
|
||||
ann := &Announce{
|
||||
mutex: &sync.RWMutex{},
|
||||
}
|
||||
handler := &mockAnnounceHandler{}
|
||||
|
||||
ann.RegisterHandler(handler)
|
||||
if len(ann.handlers) != 1 {
|
||||
t.Errorf("Expected 1 handler, got %d", len(ann.handlers))
|
||||
}
|
||||
|
||||
ann.DeregisterHandler(handler)
|
||||
if len(ann.handlers) != 0 {
|
||||
t.Errorf("Expected 0 handlers, got %d", len(ann.handlers))
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package announce
|
||||
|
||||
type Handler interface {
|
||||
AspectFilter() []string
|
||||
ReceivedAnnounce(destHash []byte, identity interface{}, appData []byte) error
|
||||
ReceivedAnnounce(destHash []byte, identity interface{}, appData []byte, hops uint8) error
|
||||
ReceivePathResponses() bool
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package buffer
|
||||
|
||||
import (
|
||||
@@ -8,7 +10,7 @@ import (
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/channel"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/channel"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -16,6 +18,19 @@ const (
|
||||
MaxChunkLen = 16 * 1024
|
||||
MaxDataLen = 457 // MDU - 2 - 6 (2 for stream header, 6 for channel envelope)
|
||||
CompressTries = 4
|
||||
|
||||
// Stream header flags
|
||||
StreamHeaderEOF = 0x8000
|
||||
StreamHeaderCompressed = 0x4000
|
||||
|
||||
// Message type
|
||||
StreamDataMessageType = 0x01
|
||||
|
||||
// Header size
|
||||
StreamHeaderSize = 2
|
||||
|
||||
// Compression threshold
|
||||
CompressThreshold = 32
|
||||
)
|
||||
|
||||
type StreamDataMessage struct {
|
||||
@@ -28,43 +43,47 @@ type StreamDataMessage struct {
|
||||
func (m *StreamDataMessage) Pack() ([]byte, error) {
|
||||
headerVal := uint16(m.StreamID & StreamIDMax)
|
||||
if m.EOF {
|
||||
headerVal |= 0x8000
|
||||
headerVal |= StreamHeaderEOF
|
||||
}
|
||||
if m.Compressed {
|
||||
headerVal |= 0x4000
|
||||
headerVal |= StreamHeaderCompressed
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (m *StreamDataMessage) GetType() uint16 {
|
||||
return 0x01 // Assign appropriate message type constant
|
||||
return StreamDataMessageType
|
||||
}
|
||||
|
||||
func (m *StreamDataMessage) Unpack(data []byte) error {
|
||||
if len(data) < 2 {
|
||||
if len(data) < StreamHeaderSize {
|
||||
return io.ErrShortBuffer
|
||||
}
|
||||
|
||||
header := binary.BigEndian.Uint16(data[:2])
|
||||
header := binary.BigEndian.Uint16(data[:StreamHeaderSize])
|
||||
m.StreamID = header & StreamIDMax
|
||||
m.EOF = (header & 0x8000) != 0
|
||||
m.Compressed = (header & 0x4000) != 0
|
||||
m.Data = data[2:]
|
||||
m.EOF = (header & StreamHeaderEOF) != 0
|
||||
m.Compressed = (header & StreamHeaderCompressed) != 0
|
||||
m.Data = data[StreamHeaderSize:]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type RawChannelReader struct {
|
||||
streamID int
|
||||
channel *channel.Channel
|
||||
buffer *bytes.Buffer
|
||||
eof bool
|
||||
callbacks []func(int)
|
||||
mutex sync.RWMutex
|
||||
streamID int
|
||||
channel *channel.Channel
|
||||
buffer *bytes.Buffer
|
||||
eof bool
|
||||
callbacks map[int]func(int)
|
||||
nextCallbackID int
|
||||
messageHandlerID int
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewRawChannelReader(streamID int, ch *channel.Channel) *RawChannelReader {
|
||||
@@ -72,28 +91,26 @@ func NewRawChannelReader(streamID int, ch *channel.Channel) *RawChannelReader {
|
||||
streamID: streamID,
|
||||
channel: ch,
|
||||
buffer: bytes.NewBuffer(nil),
|
||||
callbacks: make([]func(int), 0),
|
||||
callbacks: make(map[int]func(int)),
|
||||
}
|
||||
|
||||
ch.AddMessageHandler(reader.HandleMessage)
|
||||
reader.messageHandlerID = ch.AddMessageHandler(reader.HandleMessage)
|
||||
return reader
|
||||
}
|
||||
|
||||
func (r *RawChannelReader) AddReadyCallback(cb func(int)) {
|
||||
func (r *RawChannelReader) AddReadyCallback(cb func(int)) int {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
r.callbacks = append(r.callbacks, cb)
|
||||
id := r.nextCallbackID
|
||||
r.nextCallbackID++
|
||||
r.callbacks[id] = cb
|
||||
return id
|
||||
}
|
||||
|
||||
func (r *RawChannelReader) RemoveReadyCallback(cb func(int)) {
|
||||
func (r *RawChannelReader) RemoveReadyCallback(id int) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
for i, fn := range r.callbacks {
|
||||
if &fn == &cb {
|
||||
r.callbacks = append(r.callbacks[:i], r.callbacks[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
delete(r.callbacks, id)
|
||||
}
|
||||
|
||||
func (r *RawChannelReader) Read(p []byte) (n int, err error) {
|
||||
@@ -108,10 +125,10 @@ func (r *RawChannelReader) Read(p []byte) (n int, err error) {
|
||||
if err == io.EOF && !r.eof {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *RawChannelReader) HandleMessage(msg channel.MessageBase) bool {
|
||||
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,12 +173,12 @@ 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,
|
||||
}
|
||||
|
||||
if len(p) > 32 {
|
||||
if len(p) > CompressThreshold {
|
||||
for try := 1; try < CompressTries; try++ {
|
||||
chunkLen := len(p) / try
|
||||
compressed := compressData(p[:chunkLen])
|
||||
@@ -199,10 +216,7 @@ func (b *Buffer) Read(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (b *Buffer) Close() error {
|
||||
if err := b.ReadWriter.Writer.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return b.ReadWriter.Writer.Flush()
|
||||
}
|
||||
|
||||
func CreateReader(streamID int, ch *channel.Channel, readyCallback func(int)) *bufio.Reader {
|
||||
@@ -228,13 +242,25 @@ func compressData(data []byte) []byte {
|
||||
var compressed bytes.Buffer
|
||||
w := bytes.NewBuffer(data)
|
||||
r := bzip2.NewReader(w)
|
||||
io.Copy(&compressed, r)
|
||||
// bearer:disable go_gosec_filesystem_decompression_bomb
|
||||
_, 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
|
||||
// bearer:disable go_gosec_filesystem_decompression_bomb
|
||||
_, err := io.Copy(&decompressed, limitedReader)
|
||||
if err != nil {
|
||||
// Handle error, e.g., log it or return an error
|
||||
return nil
|
||||
}
|
||||
return decompressed.Bytes()
|
||||
}
|
||||
|
||||
449
pkg/buffer/buffer_test.go
Normal file
449
pkg/buffer/buffer_test.go
Normal file
@@ -0,0 +1,449 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/channel"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/transport"
|
||||
)
|
||||
|
||||
func TestStreamDataMessage_Pack(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
streamID uint16
|
||||
data []byte
|
||||
eof bool
|
||||
compressed bool
|
||||
}{
|
||||
{
|
||||
name: "NormalMessage",
|
||||
streamID: 123,
|
||||
data: []byte("test data"),
|
||||
eof: false,
|
||||
compressed: false,
|
||||
},
|
||||
{
|
||||
name: "EOFMessage",
|
||||
streamID: 456,
|
||||
data: []byte("final data"),
|
||||
eof: true,
|
||||
compressed: false,
|
||||
},
|
||||
{
|
||||
name: "CompressedMessage",
|
||||
streamID: 789,
|
||||
data: []byte("compressed data"),
|
||||
eof: false,
|
||||
compressed: true,
|
||||
},
|
||||
{
|
||||
name: "EmptyData",
|
||||
streamID: 0,
|
||||
data: []byte{},
|
||||
eof: false,
|
||||
compressed: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
msg := &StreamDataMessage{
|
||||
StreamID: tt.streamID,
|
||||
Data: tt.data,
|
||||
EOF: tt.eof,
|
||||
Compressed: tt.compressed,
|
||||
}
|
||||
|
||||
packed, err := msg.Pack()
|
||||
if err != nil {
|
||||
t.Fatalf("Pack() failed: %v", err)
|
||||
}
|
||||
|
||||
if len(packed) < 2 {
|
||||
t.Error("Packed message too short")
|
||||
}
|
||||
|
||||
unpacked := &StreamDataMessage{}
|
||||
if err := unpacked.Unpack(packed); err != nil {
|
||||
t.Fatalf("Unpack() failed: %v", err)
|
||||
}
|
||||
|
||||
if unpacked.StreamID != tt.streamID {
|
||||
t.Errorf("StreamID = %d, want %d", unpacked.StreamID, tt.streamID)
|
||||
}
|
||||
if unpacked.EOF != tt.eof {
|
||||
t.Errorf("EOF = %v, want %v", unpacked.EOF, tt.eof)
|
||||
}
|
||||
if unpacked.Compressed != tt.compressed {
|
||||
t.Errorf("Compressed = %v, want %v", unpacked.Compressed, tt.compressed)
|
||||
}
|
||||
if !bytes.Equal(unpacked.Data, tt.data) {
|
||||
t.Errorf("Data = %v, want %v", unpacked.Data, tt.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamDataMessage_Unpack(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
name: "ValidMessage",
|
||||
data: []byte{0x00, 0x7B, 'h', 'e', 'l', 'l', 'o'},
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "TooShort",
|
||||
data: []byte{0x00},
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "Empty",
|
||||
data: []byte{},
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
msg := &StreamDataMessage{}
|
||||
err := msg.Unpack(tt.data)
|
||||
if (err != nil) != tt.wantError {
|
||||
t.Errorf("Unpack() error = %v, wantError %v", err, tt.wantError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamDataMessage_GetType(t *testing.T) {
|
||||
msg := &StreamDataMessage{}
|
||||
if msg.GetType() != 0x01 {
|
||||
t.Errorf("GetType() = %d, want 0x01", msg.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawChannelReader_AddCallback(t *testing.T) {
|
||||
reader := &RawChannelReader{
|
||||
streamID: 1,
|
||||
buffer: bytes.NewBuffer(nil),
|
||||
callbacks: make(map[int]func(int)),
|
||||
}
|
||||
|
||||
cb := func(int) {}
|
||||
|
||||
reader.AddReadyCallback(cb)
|
||||
if len(reader.callbacks) != 1 {
|
||||
t.Error("Callback should be added")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuffer_Write(t *testing.T) {
|
||||
buf := &Buffer{
|
||||
ReadWriter: bufio.NewReadWriter(bufio.NewReader(bytes.NewBuffer(nil)), bufio.NewWriter(bytes.NewBuffer(nil))),
|
||||
}
|
||||
|
||||
data := []byte("test")
|
||||
n, err := buf.Write(data)
|
||||
if err != nil {
|
||||
t.Errorf("Write() error = %v", err)
|
||||
}
|
||||
if n != len(data) {
|
||||
t.Errorf("Write() = %d bytes, want %d", n, len(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuffer_Read(t *testing.T) {
|
||||
buf := &Buffer{
|
||||
ReadWriter: bufio.NewReadWriter(bufio.NewReader(bytes.NewBuffer([]byte("test data"))), bufio.NewWriter(bytes.NewBuffer(nil))),
|
||||
}
|
||||
|
||||
data := make([]byte, 10)
|
||||
n, err := buf.Read(data)
|
||||
if err != nil && err != io.EOF {
|
||||
t.Errorf("Read() error = %v", err)
|
||||
}
|
||||
if n <= 0 {
|
||||
t.Errorf("Read() = %d bytes, want > 0", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuffer_Close(t *testing.T) {
|
||||
buf := &Buffer{
|
||||
ReadWriter: bufio.NewReadWriter(bufio.NewReader(bytes.NewBuffer(nil)), bufio.NewWriter(bytes.NewBuffer(nil))),
|
||||
}
|
||||
|
||||
if err := buf.Close(); err != nil {
|
||||
t.Errorf("Close() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamIDMax(t *testing.T) {
|
||||
if StreamIDMax != 0x3fff {
|
||||
t.Errorf("StreamIDMax = %d, want %d", StreamIDMax, 0x3fff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxChunkLen(t *testing.T) {
|
||||
if MaxChunkLen != 16*1024 {
|
||||
t.Errorf("MaxChunkLen = %d, want %d", MaxChunkLen, 16*1024)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxDataLen(t *testing.T) {
|
||||
if MaxDataLen != 457 {
|
||||
t.Errorf("MaxDataLen = %d, want %d", MaxDataLen, 457)
|
||||
}
|
||||
}
|
||||
|
||||
type mockLink struct {
|
||||
status byte
|
||||
rtt float64
|
||||
}
|
||||
|
||||
func (m *mockLink) GetStatus() byte { return m.status }
|
||||
func (m *mockLink) GetRTT() float64 { return m.rtt }
|
||||
func (m *mockLink) RTT() float64 { return m.rtt }
|
||||
func (m *mockLink) GetLinkID() []byte { return []byte("testlink") }
|
||||
func (m *mockLink) Send(data []byte) interface{} { return &packet.Packet{Raw: data} }
|
||||
func (m *mockLink) Resend(p interface{}) error { return nil }
|
||||
func (m *mockLink) SetPacketTimeout(p interface{}, cb func(interface{}), t time.Duration) {}
|
||||
func (m *mockLink) SetPacketDelivered(p interface{}, cb func(interface{})) {}
|
||||
func (m *mockLink) HandleInbound(pkt *packet.Packet) error { return nil }
|
||||
func (m *mockLink) ValidateLinkProof(pkt *packet.Packet, networkIface common.NetworkInterface) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestNewRawChannelReader(t *testing.T) {
|
||||
link := &mockLink{status: transport.STATUS_ACTIVE}
|
||||
ch := channel.NewChannel(link)
|
||||
reader := NewRawChannelReader(123, ch)
|
||||
|
||||
if reader.streamID != 123 {
|
||||
t.Errorf("streamID = %d, want %d", reader.streamID, 123)
|
||||
}
|
||||
if reader.channel != ch {
|
||||
t.Error("channel not set correctly")
|
||||
}
|
||||
if reader.buffer == nil {
|
||||
t.Error("buffer is nil")
|
||||
}
|
||||
if reader.callbacks == nil {
|
||||
t.Error("callbacks is nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawChannelReader_RemoveReadyCallback(t *testing.T) {
|
||||
reader := &RawChannelReader{
|
||||
streamID: 1,
|
||||
buffer: bytes.NewBuffer(nil),
|
||||
callbacks: make(map[int]func(int)),
|
||||
}
|
||||
|
||||
cb1 := func(int) {}
|
||||
cb2 := func(int) {}
|
||||
|
||||
id1 := reader.AddReadyCallback(cb1)
|
||||
reader.AddReadyCallback(cb2)
|
||||
|
||||
if len(reader.callbacks) != 2 {
|
||||
t.Errorf("callbacks length = %d, want 2", len(reader.callbacks))
|
||||
}
|
||||
|
||||
reader.RemoveReadyCallback(id1)
|
||||
|
||||
if len(reader.callbacks) != 1 {
|
||||
t.Errorf("RemoveReadyCallback did not remove callback, length = %d", len(reader.callbacks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawChannelReader_Read(t *testing.T) {
|
||||
reader := &RawChannelReader{
|
||||
streamID: 1,
|
||||
buffer: bytes.NewBuffer([]byte("test data")),
|
||||
eof: false,
|
||||
}
|
||||
|
||||
data := make([]byte, 10)
|
||||
n, err := reader.Read(data)
|
||||
if err != nil {
|
||||
t.Errorf("Read() error = %v", err)
|
||||
}
|
||||
if n == 0 {
|
||||
t.Error("Read() returned 0 bytes")
|
||||
}
|
||||
|
||||
reader.eof = true
|
||||
reader.buffer = bytes.NewBuffer(nil)
|
||||
n, err = reader.Read(data)
|
||||
if err != io.EOF {
|
||||
t.Errorf("Read() error = %v, want io.EOF", err)
|
||||
}
|
||||
if n != 0 {
|
||||
t.Errorf("Read() = %d bytes, want 0", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawChannelReader_HandleMessage(t *testing.T) {
|
||||
reader := &RawChannelReader{
|
||||
streamID: 1,
|
||||
buffer: bytes.NewBuffer(nil),
|
||||
callbacks: make(map[int]func(int)),
|
||||
}
|
||||
|
||||
msg := &StreamDataMessage{
|
||||
StreamID: 1,
|
||||
Data: []byte("test"),
|
||||
EOF: false,
|
||||
Compressed: false,
|
||||
}
|
||||
|
||||
called := false
|
||||
reader.AddReadyCallback(func(int) {
|
||||
called = true
|
||||
})
|
||||
|
||||
result := reader.HandleMessage(msg)
|
||||
if !result {
|
||||
t.Error("HandleMessage() = false, want true")
|
||||
}
|
||||
if !called {
|
||||
t.Error("callback was not called")
|
||||
}
|
||||
if reader.buffer.Len() == 0 {
|
||||
t.Error("buffer is empty after HandleMessage")
|
||||
}
|
||||
|
||||
msg.StreamID = 2
|
||||
result = reader.HandleMessage(msg)
|
||||
if result {
|
||||
t.Error("HandleMessage() = true, want false for different streamID")
|
||||
}
|
||||
|
||||
msg.StreamID = 1
|
||||
msg.EOF = true
|
||||
reader.HandleMessage(msg)
|
||||
if !reader.eof {
|
||||
t.Error("EOF not set after HandleMessage with EOF flag")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRawChannelWriter(t *testing.T) {
|
||||
link := &mockLink{status: transport.STATUS_ACTIVE}
|
||||
ch := channel.NewChannel(link)
|
||||
writer := NewRawChannelWriter(456, ch)
|
||||
|
||||
if writer.streamID != 456 {
|
||||
t.Errorf("streamID = %d, want %d", writer.streamID, 456)
|
||||
}
|
||||
if writer.channel != ch {
|
||||
t.Error("channel not set correctly")
|
||||
}
|
||||
if writer.eof {
|
||||
t.Error("eof should be false initially")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawChannelWriter_Write(t *testing.T) {
|
||||
link := &mockLink{status: transport.STATUS_ACTIVE}
|
||||
ch := channel.NewChannel(link)
|
||||
writer := NewRawChannelWriter(1, ch)
|
||||
|
||||
data := []byte("test data")
|
||||
n, err := writer.Write(data)
|
||||
if err != nil {
|
||||
t.Errorf("Write() error = %v", err)
|
||||
}
|
||||
if n != len(data) {
|
||||
t.Errorf("Write() = %d bytes, want %d", n, len(data))
|
||||
}
|
||||
|
||||
largeData := make([]byte, MaxChunkLen+100)
|
||||
n, err = writer.Write(largeData)
|
||||
if err != nil {
|
||||
t.Errorf("Write() error = %v", err)
|
||||
}
|
||||
if n != MaxChunkLen {
|
||||
t.Errorf("Write() = %d bytes, want %d", n, MaxChunkLen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawChannelWriter_Close(t *testing.T) {
|
||||
link := &mockLink{status: transport.STATUS_ACTIVE}
|
||||
ch := channel.NewChannel(link)
|
||||
writer := NewRawChannelWriter(1, ch)
|
||||
|
||||
if writer.eof {
|
||||
t.Error("EOF should be false before Close()")
|
||||
}
|
||||
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close() error = %v", err)
|
||||
}
|
||||
if !writer.eof {
|
||||
t.Error("EOF should be true after Close()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateReader(t *testing.T) {
|
||||
link := &mockLink{status: transport.STATUS_ACTIVE}
|
||||
ch := channel.NewChannel(link)
|
||||
callback := func(int) {}
|
||||
reader := CreateReader(789, ch, callback)
|
||||
|
||||
if reader == nil {
|
||||
t.Error("CreateReader() returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateWriter(t *testing.T) {
|
||||
link := &mockLink{status: transport.STATUS_ACTIVE}
|
||||
ch := channel.NewChannel(link)
|
||||
writer := CreateWriter(101, ch)
|
||||
|
||||
if writer == nil {
|
||||
t.Error("CreateWriter() returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateBidirectionalBuffer(t *testing.T) {
|
||||
link := &mockLink{status: transport.STATUS_ACTIVE}
|
||||
ch := channel.NewChannel(link)
|
||||
callback := func(int) {}
|
||||
buf := CreateBidirectionalBuffer(1, 2, ch, callback)
|
||||
|
||||
if buf == nil {
|
||||
t.Error("CreateBidirectionalBuffer() returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompressData(t *testing.T) {
|
||||
data := []byte("test data for compression")
|
||||
compressed := compressData(data)
|
||||
|
||||
if compressed == nil {
|
||||
t.Skip("compressData() returned nil (compression implementation may be incomplete)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecompressData(t *testing.T) {
|
||||
data := []byte("test data")
|
||||
compressed := compressData(data)
|
||||
if compressed == nil {
|
||||
t.Skip("compression not working, skipping decompression test")
|
||||
}
|
||||
|
||||
decompressed := decompressData(compressed)
|
||||
if decompressed == nil {
|
||||
t.Error("decompressData() returned nil")
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package channel
|
||||
|
||||
import (
|
||||
@@ -6,7 +8,9 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/transport"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,6 +36,19 @@ const (
|
||||
SeqModulus uint16 = SeqMax
|
||||
|
||||
FastRateThreshold = 10
|
||||
|
||||
// Timeout calculation constants
|
||||
RTTMinThreshold = 0.025
|
||||
TimeoutBaseMultiplier = 1.5
|
||||
TimeoutRingMultiplier = 2.5
|
||||
TimeoutRingOffset = 2
|
||||
|
||||
// Packet header constants
|
||||
ChannelHeaderSize = 6
|
||||
ChannelHeaderBits = 8
|
||||
|
||||
// Default retry count
|
||||
DefaultMaxTries = 3
|
||||
)
|
||||
|
||||
// MessageState represents the state of a message
|
||||
@@ -66,7 +83,13 @@ type Channel struct {
|
||||
maxTries int
|
||||
fastRateRounds int
|
||||
medRateRounds int
|
||||
messageHandlers []func(MessageBase) bool
|
||||
messageHandlers []messageHandlerEntry
|
||||
nextHandlerID int
|
||||
}
|
||||
|
||||
type messageHandlerEntry struct {
|
||||
id int
|
||||
handler func(MessageBase) bool
|
||||
}
|
||||
|
||||
// Envelope wraps a message with metadata for transmission
|
||||
@@ -83,12 +106,12 @@ type Envelope struct {
|
||||
func NewChannel(link transport.LinkInterface) *Channel {
|
||||
return &Channel{
|
||||
link: link,
|
||||
messageHandlers: make([]func(MessageBase) bool, 0),
|
||||
messageHandlers: make([]messageHandlerEntry, 0),
|
||||
mutex: sync.RWMutex{},
|
||||
windowMax: WindowMaxSlow,
|
||||
windowMin: WindowMinSlow,
|
||||
window: WindowInitial,
|
||||
maxTries: 3,
|
||||
maxTries: DefaultMaxTries,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +128,7 @@ func (c *Channel) Send(msg MessageBase) error {
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
c.nextSequence = (c.nextSequence + 1) % SeqModulus
|
||||
c.nextSequence = (c.nextSequence + common.ONE) % SeqModulus
|
||||
c.txRing = append(c.txRing, env)
|
||||
c.mutex.Unlock()
|
||||
|
||||
@@ -138,7 +161,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
|
||||
debug.Log(debug.DEBUG_INFO, "Failed to resend packet", "error", 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
|
||||
@@ -161,25 +191,28 @@ func (c *Channel) handleDelivered(packet interface{}) {
|
||||
|
||||
func (c *Channel) getPacketTimeout(tries int) time.Duration {
|
||||
rtt := c.link.GetRTT()
|
||||
if rtt < 0.025 {
|
||||
rtt = 0.025
|
||||
if rtt < RTTMinThreshold {
|
||||
rtt = RTTMinThreshold
|
||||
}
|
||||
|
||||
timeout := math.Pow(1.5, float64(tries-1)) * rtt * 2.5 * float64(len(c.txRing)+2)
|
||||
timeout := math.Pow(TimeoutBaseMultiplier, float64(tries-common.ONE)) * rtt * TimeoutRingMultiplier * float64(len(c.txRing)+TimeoutRingOffset)
|
||||
return time.Duration(timeout * float64(time.Second))
|
||||
}
|
||||
|
||||
func (c *Channel) AddMessageHandler(handler func(MessageBase) bool) {
|
||||
func (c *Channel) AddMessageHandler(handler func(MessageBase) bool) int {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
c.messageHandlers = append(c.messageHandlers, handler)
|
||||
id := c.nextHandlerID
|
||||
c.nextHandlerID++
|
||||
c.messageHandlers = append(c.messageHandlers, messageHandlerEntry{id: id, handler: handler})
|
||||
return id
|
||||
}
|
||||
|
||||
func (c *Channel) RemoveMessageHandler(handler func(MessageBase) bool) {
|
||||
func (c *Channel) RemoveMessageHandler(id int) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
for i, h := range c.messageHandlers {
|
||||
if &h == &handler {
|
||||
for i, entry := range c.messageHandlers {
|
||||
if entry.id == id {
|
||||
c.messageHandlers = append(c.messageHandlers[:i], c.messageHandlers[i+1:]...)
|
||||
break
|
||||
}
|
||||
@@ -190,10 +223,10 @@ func (c *Channel) updateRateThresholds() {
|
||||
rtt := c.link.RTT()
|
||||
|
||||
if rtt > RTTFast {
|
||||
c.fastRateRounds = 0
|
||||
c.fastRateRounds = common.ZERO
|
||||
|
||||
if rtt > RTTMedium {
|
||||
c.medRateRounds = 0
|
||||
c.medRateRounds = common.ZERO
|
||||
} else {
|
||||
c.medRateRounds++
|
||||
if c.windowMax < WindowMaxMedium && c.medRateRounds == FastRateThreshold {
|
||||
@@ -210,6 +243,59 @@ func (c *Channel) updateRateThresholds() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Channel) HandleInbound(data []byte) error {
|
||||
if len(data) < ChannelHeaderSize {
|
||||
return errors.New("channel packet too short")
|
||||
}
|
||||
|
||||
msgType := uint16(data[0])<<ChannelHeaderBits | uint16(data[1])
|
||||
sequence := uint16(data[2])<<ChannelHeaderBits | uint16(data[3])
|
||||
length := uint16(data[4])<<ChannelHeaderBits | uint16(data[5])
|
||||
|
||||
if len(data) < ChannelHeaderSize+int(length) {
|
||||
return errors.New("channel packet incomplete")
|
||||
}
|
||||
|
||||
msgData := data[ChannelHeaderSize : ChannelHeaderSize+length]
|
||||
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
for _, entry := range c.messageHandlers {
|
||||
if entry.handler != nil {
|
||||
msg := &GenericMessage{
|
||||
Type: msgType,
|
||||
Data: msgData,
|
||||
Seq: sequence,
|
||||
}
|
||||
if entry.handler(msg) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type GenericMessage struct {
|
||||
Type uint16
|
||||
Data []byte
|
||||
Seq uint16
|
||||
}
|
||||
|
||||
func (g *GenericMessage) Pack() ([]byte, error) {
|
||||
return g.Data, nil
|
||||
}
|
||||
|
||||
func (g *GenericMessage) Unpack(data []byte) error {
|
||||
g.Data = data
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GenericMessage) GetType() uint16 {
|
||||
return g.Type
|
||||
}
|
||||
|
||||
func (c *Channel) Close() error {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
130
pkg/channel/channel_test.go
Normal file
130
pkg/channel/channel_test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package channel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
|
||||
)
|
||||
|
||||
type mockLink struct {
|
||||
status byte
|
||||
rtt float64
|
||||
sent [][]byte
|
||||
timeouts map[interface{}]func(interface{})
|
||||
delivered map[interface{}]func(interface{})
|
||||
}
|
||||
|
||||
func (m *mockLink) GetStatus() byte { return m.status }
|
||||
func (m *mockLink) GetRTT() float64 { return m.rtt }
|
||||
func (m *mockLink) RTT() float64 { return m.rtt }
|
||||
func (m *mockLink) GetLinkID() []byte { return []byte("testlink") }
|
||||
func (m *mockLink) Send(data []byte) interface{} {
|
||||
m.sent = append(m.sent, data)
|
||||
p := &packet.Packet{Raw: data}
|
||||
return p
|
||||
}
|
||||
func (m *mockLink) Resend(p interface{}) error { return nil }
|
||||
func (m *mockLink) SetPacketTimeout(p interface{}, cb func(interface{}), t time.Duration) {
|
||||
if m.timeouts == nil {
|
||||
m.timeouts = make(map[interface{}]func(interface{}))
|
||||
}
|
||||
m.timeouts[p] = cb
|
||||
}
|
||||
func (m *mockLink) SetPacketDelivered(p interface{}, cb func(interface{})) {
|
||||
if m.delivered == nil {
|
||||
m.delivered = make(map[interface{}]func(interface{}))
|
||||
}
|
||||
m.delivered[p] = cb
|
||||
}
|
||||
func (m *mockLink) HandleInbound(pkt *packet.Packet) error { return nil }
|
||||
func (m *mockLink) ValidateLinkProof(pkt *packet.Packet, networkIface common.NetworkInterface) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type testMessage struct {
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (m *testMessage) Pack() ([]byte, error) { return m.data, nil }
|
||||
func (m *testMessage) Unpack(data []byte) error { m.data = data; return nil }
|
||||
func (m *testMessage) GetType() uint16 { return 1 }
|
||||
|
||||
func TestNewChannel(t *testing.T) {
|
||||
link := &mockLink{}
|
||||
c := NewChannel(link)
|
||||
if c == nil {
|
||||
t.Fatal("NewChannel returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelSend(t *testing.T) {
|
||||
link := &mockLink{status: 1} // STATUS_ACTIVE
|
||||
c := NewChannel(link)
|
||||
|
||||
msg := &testMessage{data: []byte("test")}
|
||||
err := c.Send(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("Send failed: %v", err)
|
||||
}
|
||||
|
||||
if len(link.sent) != 1 {
|
||||
t.Errorf("Expected 1 packet sent, got %d", len(link.sent))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleInbound(t *testing.T) {
|
||||
link := &mockLink{}
|
||||
c := NewChannel(link)
|
||||
|
||||
received := false
|
||||
c.AddMessageHandler(func(m MessageBase) bool {
|
||||
received = true
|
||||
return true
|
||||
})
|
||||
|
||||
// Packet format: [type 2][seq 2][len 2][data]
|
||||
data := []byte{0, 1, 0, 1, 0, 4, 't', 'e', 's', 't'}
|
||||
err := c.HandleInbound(data)
|
||||
if err != nil {
|
||||
t.Fatalf("HandleInbound failed: %v", err)
|
||||
}
|
||||
|
||||
if !received {
|
||||
t.Error("Message handler was not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageHandlers(t *testing.T) {
|
||||
c := &Channel{
|
||||
messageHandlers: make([]messageHandlerEntry, 0),
|
||||
}
|
||||
h := func(m MessageBase) bool { return true }
|
||||
|
||||
id := c.AddMessageHandler(h)
|
||||
if len(c.messageHandlers) != 1 {
|
||||
t.Errorf("Expected 1 handler, got %d", len(c.messageHandlers))
|
||||
}
|
||||
|
||||
c.RemoveMessageHandler(id)
|
||||
if len(c.messageHandlers) != 0 {
|
||||
t.Errorf("Expected 0 handlers, got %d", len(c.messageHandlers))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenericMessage(t *testing.T) {
|
||||
msg := &GenericMessage{Type: 1, Data: []byte("test")}
|
||||
if msg.GetType() != 1 {
|
||||
t.Error("Wrong type")
|
||||
}
|
||||
p, _ := msg.Pack()
|
||||
if !bytes.Equal(p, []byte("test")) {
|
||||
t.Error("Pack failed")
|
||||
}
|
||||
msg.Unpack([]byte("new"))
|
||||
if !bytes.Equal(msg.Data, []byte("new")) {
|
||||
t.Error("Unpack failed")
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package common
|
||||
|
||||
import (
|
||||
|
||||
94
pkg/common/config_test.go
Normal file
94
pkg/common/config_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewReticulumConfig(t *testing.T) {
|
||||
cfg := NewReticulumConfig()
|
||||
|
||||
if !cfg.EnableTransport {
|
||||
t.Errorf("NewReticulumConfig() EnableTransport = %v; want true", cfg.EnableTransport)
|
||||
}
|
||||
if cfg.ShareInstance {
|
||||
t.Errorf("NewReticulumConfig() ShareInstance = %v; want false", cfg.ShareInstance)
|
||||
}
|
||||
if cfg.SharedInstancePort != DEFAULT_SHARED_INSTANCE_PORT {
|
||||
t.Errorf("NewReticulumConfig() SharedInstancePort = %d; want %d", cfg.SharedInstancePort, DEFAULT_SHARED_INSTANCE_PORT)
|
||||
}
|
||||
if cfg.InstanceControlPort != DEFAULT_INSTANCE_CONTROL_PORT {
|
||||
t.Errorf("NewReticulumConfig() InstanceControlPort = %d; want %d", cfg.InstanceControlPort, DEFAULT_INSTANCE_CONTROL_PORT)
|
||||
}
|
||||
if cfg.PanicOnInterfaceErr {
|
||||
t.Errorf("NewReticulumConfig() PanicOnInterfaceErr = %v; want false", cfg.PanicOnInterfaceErr)
|
||||
}
|
||||
if cfg.LogLevel != DEFAULT_LOG_LEVEL {
|
||||
t.Errorf("NewReticulumConfig() LogLevel = %d; want %d", cfg.LogLevel, DEFAULT_LOG_LEVEL)
|
||||
}
|
||||
if len(cfg.Interfaces) != 0 {
|
||||
t.Errorf("NewReticulumConfig() Interfaces length = %d; want 0", len(cfg.Interfaces))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
if !cfg.EnableTransport {
|
||||
t.Errorf("DefaultConfig() EnableTransport = %v; want true", cfg.EnableTransport)
|
||||
}
|
||||
if cfg.ShareInstance {
|
||||
t.Errorf("DefaultConfig() ShareInstance = %v; want false", cfg.ShareInstance)
|
||||
}
|
||||
if cfg.SharedInstancePort != DEFAULT_SHARED_INSTANCE_PORT {
|
||||
t.Errorf("DefaultConfig() SharedInstancePort = %d; want %d", cfg.SharedInstancePort, DEFAULT_SHARED_INSTANCE_PORT)
|
||||
}
|
||||
if cfg.InstanceControlPort != DEFAULT_INSTANCE_CONTROL_PORT {
|
||||
t.Errorf("DefaultConfig() InstanceControlPort = %d; want %d", cfg.InstanceControlPort, DEFAULT_INSTANCE_CONTROL_PORT)
|
||||
}
|
||||
if cfg.PanicOnInterfaceErr {
|
||||
t.Errorf("DefaultConfig() PanicOnInterfaceErr = %v; want false", cfg.PanicOnInterfaceErr)
|
||||
}
|
||||
if cfg.LogLevel != DEFAULT_LOG_LEVEL {
|
||||
t.Errorf("DefaultConfig() LogLevel = %d; want %d", cfg.LogLevel, DEFAULT_LOG_LEVEL)
|
||||
}
|
||||
if len(cfg.Interfaces) != 0 {
|
||||
t.Errorf("DefaultConfig() Interfaces length = %d; want 0", len(cfg.Interfaces))
|
||||
}
|
||||
if cfg.AppName != "Go Client" {
|
||||
t.Errorf("DefaultConfig() AppName = %q; want %q", cfg.AppName, "Go Client")
|
||||
}
|
||||
if cfg.AppAspect != "node" {
|
||||
t.Errorf("DefaultConfig() AppAspect = %q; want %q", cfg.AppAspect, "node")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReticulumConfig_Validate(t *testing.T) {
|
||||
validConfig := DefaultConfig()
|
||||
if err := validConfig.Validate(); err != nil {
|
||||
t.Errorf("Validate() on default config failed: %v", err)
|
||||
}
|
||||
|
||||
invalidPortConfig1 := DefaultConfig()
|
||||
invalidPortConfig1.SharedInstancePort = 0
|
||||
if err := invalidPortConfig1.Validate(); err == nil {
|
||||
t.Errorf("Validate() did not return error for invalid SharedInstancePort 0")
|
||||
}
|
||||
|
||||
invalidPortConfig2 := DefaultConfig()
|
||||
invalidPortConfig2.SharedInstancePort = 65536
|
||||
if err := invalidPortConfig2.Validate(); err == nil {
|
||||
t.Errorf("Validate() did not return error for invalid SharedInstancePort 65536")
|
||||
}
|
||||
|
||||
invalidPortConfig3 := DefaultConfig()
|
||||
invalidPortConfig3.InstanceControlPort = 0
|
||||
if err := invalidPortConfig3.Validate(); err == nil {
|
||||
t.Errorf("Validate() did not return error for invalid InstanceControlPort 0")
|
||||
}
|
||||
|
||||
invalidPortConfig4 := DefaultConfig()
|
||||
invalidPortConfig4.InstanceControlPort = 65536
|
||||
if err := invalidPortConfig4.Validate(); err == nil {
|
||||
t.Errorf("Validate() did not return error for invalid InstanceControlPort 65536")
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package common
|
||||
|
||||
const (
|
||||
@@ -58,4 +60,87 @@ const (
|
||||
STALE_TIME = 720
|
||||
PATH_REQUEST_TTL = 300
|
||||
ANNOUNCE_TIMEOUT = 15
|
||||
|
||||
// Common Numeric Constants
|
||||
ZERO = 0
|
||||
ONE = 1
|
||||
TWO = 2
|
||||
THREE = 3
|
||||
FOUR = 4
|
||||
FIVE = 5
|
||||
SIX = 6
|
||||
SEVEN = 7
|
||||
EIGHT = 8
|
||||
FIFTEEN = 15
|
||||
|
||||
// Common Size Constants
|
||||
SIZE_16 = 16
|
||||
SIZE_32 = 32
|
||||
SIZE_48 = 48
|
||||
SIZE_64 = 64
|
||||
SIXTY_SEVEN = 67
|
||||
TOKEN_OVERHEAD = 48
|
||||
|
||||
// Common Hex Constants
|
||||
HEX_0x00 = 0x00
|
||||
HEX_0x01 = 0x01
|
||||
HEX_0x02 = 0x02
|
||||
HEX_0x03 = 0x03
|
||||
HEX_0x04 = 0x04
|
||||
HEX_0x92 = 0x92
|
||||
HEX_0x93 = 0x93
|
||||
HEX_0xC2 = 0xC2
|
||||
HEX_0xC3 = 0xC3
|
||||
HEX_0xC4 = 0xC4
|
||||
HEX_0xD1 = 0xD1
|
||||
HEX_0xD2 = 0xD2
|
||||
HEX_0xFE = 0xFE
|
||||
HEX_0xFF = 0xFF
|
||||
|
||||
// Common Numeric Constants
|
||||
NUM_11 = 11
|
||||
NUM_100 = 100
|
||||
NUM_500 = 500
|
||||
NUM_1024 = 1024
|
||||
NUM_1064 = 1064
|
||||
NUM_4242 = 4242
|
||||
NUM_0700 = 0700
|
||||
|
||||
// Common Float Constants
|
||||
FLOAT_ZERO = 0.0
|
||||
FLOAT_0_001 = 0.001
|
||||
FLOAT_0_025 = 0.025
|
||||
FLOAT_0_1 = 0.1
|
||||
FLOAT_1_0 = 1.0
|
||||
FLOAT_1_75 = 1.75
|
||||
FLOAT_5_0 = 5.0
|
||||
FLOAT_1E9 = 1e9
|
||||
|
||||
// Common String Constants
|
||||
STR_LINK_ID = "link_id"
|
||||
STR_BYTES = "bytes"
|
||||
STR_FMT_HEX = "0x%02x"
|
||||
STR_FMT_HEX_LOW = "%x"
|
||||
STR_FMT_DEC = "%d"
|
||||
STR_TEST = "test"
|
||||
STR_LINK = "link"
|
||||
STR_ERROR = "error"
|
||||
STR_HASH = "hash"
|
||||
STR_NAME = "name"
|
||||
STR_TYPE = "type"
|
||||
STR_STORAGE = "storage"
|
||||
STR_PATH = "path"
|
||||
STR_COUNT = "count"
|
||||
STR_HOME = "HOME"
|
||||
STR_PUBLIC_KEY = "public_key"
|
||||
STR_TCP_CLIENT = "TCPClientInterface"
|
||||
STR_UDP = "udp"
|
||||
STR_UDP6 = "udp6"
|
||||
STR_TCP = "tcp"
|
||||
STR_ETH0 = "eth0"
|
||||
STR_INTERFACE = "interface"
|
||||
STR_PEER = "peer"
|
||||
STR_ADDR = "addr"
|
||||
STR_LINK_NOT_ACTIVE = "link not active"
|
||||
STR_INTERFACE_OFFLINE = "interface offline or detached"
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package common
|
||||
|
||||
import (
|
||||
@@ -181,12 +183,10 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
|
||||
packet = append(packet, 0x02) // Link packet type
|
||||
packet = append(packet, dest...)
|
||||
|
||||
// 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
|
||||
packet = append(packet, data...)
|
||||
|
||||
return i.Send(packet, "")
|
||||
|
||||
288
pkg/common/interfaces_test.go
Normal file
288
pkg/common/interfaces_test.go
Normal file
@@ -0,0 +1,288 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewBaseInterface(t *testing.T) {
|
||||
iface := NewBaseInterface("test0", IF_TYPE_UDP, true)
|
||||
|
||||
if iface.Name != "test0" {
|
||||
t.Errorf("Name = %q, want %q", iface.Name, "test0")
|
||||
}
|
||||
if iface.Type != IF_TYPE_UDP {
|
||||
t.Errorf("Type = %v, want %v", iface.Type, IF_TYPE_UDP)
|
||||
}
|
||||
if iface.Mode != IF_MODE_FULL {
|
||||
t.Errorf("Mode = %v, want %v", iface.Mode, IF_MODE_FULL)
|
||||
}
|
||||
if !iface.Enabled {
|
||||
t.Errorf("Enabled = %v, want true", iface.Enabled)
|
||||
}
|
||||
if iface.MTU != DEFAULT_MTU {
|
||||
t.Errorf("MTU = %d, want %d", iface.MTU, DEFAULT_MTU)
|
||||
}
|
||||
if iface.Bitrate != BITRATE_MINIMUM {
|
||||
t.Errorf("Bitrate = %d, want %d", iface.Bitrate, BITRATE_MINIMUM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_GetType(t *testing.T) {
|
||||
iface := NewBaseInterface("test1", IF_TYPE_TCP, true)
|
||||
if iface.GetType() != IF_TYPE_TCP {
|
||||
t.Errorf("GetType() = %v, want %v", iface.GetType(), IF_TYPE_TCP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_GetMode(t *testing.T) {
|
||||
iface := NewBaseInterface("test2", IF_TYPE_UDP, true)
|
||||
if iface.GetMode() != IF_MODE_FULL {
|
||||
t.Errorf("GetMode() = %v, want %v", iface.GetMode(), IF_MODE_FULL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_GetMTU(t *testing.T) {
|
||||
iface := NewBaseInterface("test3", IF_TYPE_UDP, true)
|
||||
if iface.GetMTU() != DEFAULT_MTU {
|
||||
t.Errorf("GetMTU() = %d, want %d", iface.GetMTU(), DEFAULT_MTU)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_GetName(t *testing.T) {
|
||||
iface := NewBaseInterface("test4", IF_TYPE_UDP, true)
|
||||
if iface.GetName() != "test4" {
|
||||
t.Errorf("GetName() = %q, want %q", iface.GetName(), "test4")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_IsEnabled(t *testing.T) {
|
||||
iface := NewBaseInterface("test5", IF_TYPE_UDP, true)
|
||||
iface.Online = true
|
||||
iface.Detached = false
|
||||
|
||||
if !iface.IsEnabled() {
|
||||
t.Error("IsEnabled() = false, want true")
|
||||
}
|
||||
|
||||
iface.Enabled = false
|
||||
if iface.IsEnabled() {
|
||||
t.Error("IsEnabled() = true, want false when disabled")
|
||||
}
|
||||
|
||||
iface.Enabled = true
|
||||
iface.Online = false
|
||||
if iface.IsEnabled() {
|
||||
t.Error("IsEnabled() = true, want false when offline")
|
||||
}
|
||||
|
||||
iface.Online = true
|
||||
iface.Detached = true
|
||||
if iface.IsEnabled() {
|
||||
t.Error("IsEnabled() = true, want false when detached")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_IsOnline(t *testing.T) {
|
||||
iface := NewBaseInterface("test6", IF_TYPE_UDP, true)
|
||||
iface.Online = true
|
||||
|
||||
if !iface.IsOnline() {
|
||||
t.Error("IsOnline() = false, want true")
|
||||
}
|
||||
|
||||
iface.Online = false
|
||||
if iface.IsOnline() {
|
||||
t.Error("IsOnline() = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_IsDetached(t *testing.T) {
|
||||
iface := NewBaseInterface("test7", IF_TYPE_UDP, true)
|
||||
iface.Detached = true
|
||||
|
||||
if !iface.IsDetached() {
|
||||
t.Error("IsDetached() = false, want true")
|
||||
}
|
||||
|
||||
iface.Detached = false
|
||||
if iface.IsDetached() {
|
||||
t.Error("IsDetached() = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_SetPacketCallback(t *testing.T) {
|
||||
iface := NewBaseInterface("test8", IF_TYPE_UDP, true)
|
||||
|
||||
callback := func(data []byte, ni NetworkInterface) {}
|
||||
iface.SetPacketCallback(callback)
|
||||
|
||||
if iface.GetPacketCallback() == nil {
|
||||
t.Error("GetPacketCallback() = nil, want callback")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_GetPacketCallback(t *testing.T) {
|
||||
iface := NewBaseInterface("test9", IF_TYPE_UDP, true)
|
||||
|
||||
if iface.GetPacketCallback() != nil {
|
||||
t.Error("GetPacketCallback() != nil, want nil")
|
||||
}
|
||||
|
||||
callback := func(data []byte, ni NetworkInterface) {}
|
||||
iface.SetPacketCallback(callback)
|
||||
|
||||
if iface.GetPacketCallback() == nil {
|
||||
t.Error("GetPacketCallback() = nil, want callback")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_Detach(t *testing.T) {
|
||||
iface := NewBaseInterface("test10", IF_TYPE_UDP, true)
|
||||
iface.Online = true
|
||||
iface.Detached = false
|
||||
|
||||
iface.Detach()
|
||||
|
||||
if !iface.IsDetached() {
|
||||
t.Error("IsDetached() = false, want true after Detach()")
|
||||
}
|
||||
if iface.IsOnline() {
|
||||
t.Error("IsOnline() = true, want false after Detach()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_Enable(t *testing.T) {
|
||||
iface := NewBaseInterface("test11", IF_TYPE_UDP, false)
|
||||
iface.Online = false
|
||||
|
||||
iface.Enable()
|
||||
|
||||
if !iface.Enabled {
|
||||
t.Error("Enabled = false, want true after Enable()")
|
||||
}
|
||||
if !iface.IsOnline() {
|
||||
t.Error("IsOnline() = false, want true after Enable()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_Disable(t *testing.T) {
|
||||
iface := NewBaseInterface("test12", IF_TYPE_UDP, true)
|
||||
iface.Online = true
|
||||
|
||||
iface.Disable()
|
||||
|
||||
if iface.Enabled {
|
||||
t.Error("Enabled = true, want false after Disable()")
|
||||
}
|
||||
if iface.IsOnline() {
|
||||
t.Error("IsOnline() = true, want false after Disable()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_Start(t *testing.T) {
|
||||
iface := NewBaseInterface("test13", IF_TYPE_UDP, true)
|
||||
if err := iface.Start(); err != nil {
|
||||
t.Errorf("Start() error = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_Stop(t *testing.T) {
|
||||
iface := NewBaseInterface("test14", IF_TYPE_UDP, true)
|
||||
if err := iface.Stop(); err != nil {
|
||||
t.Errorf("Stop() error = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_GetConn(t *testing.T) {
|
||||
iface := NewBaseInterface("test15", IF_TYPE_UDP, true)
|
||||
if iface.GetConn() != nil {
|
||||
t.Error("GetConn() != nil, want nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_Send(t *testing.T) {
|
||||
iface := NewBaseInterface("test16", IF_TYPE_UDP, true)
|
||||
data := []byte("test data")
|
||||
|
||||
if err := iface.Send(data, ""); err != nil {
|
||||
t.Errorf("Send() error = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_ProcessIncoming(t *testing.T) {
|
||||
iface := NewBaseInterface("test17", IF_TYPE_UDP, true)
|
||||
|
||||
called := false
|
||||
callback := func(data []byte, ni NetworkInterface) {
|
||||
called = true
|
||||
}
|
||||
iface.SetPacketCallback(callback)
|
||||
|
||||
data := []byte("test")
|
||||
iface.ProcessIncoming(data)
|
||||
|
||||
if !called {
|
||||
t.Error("ProcessIncoming() did not call callback")
|
||||
}
|
||||
|
||||
iface.SetPacketCallback(nil)
|
||||
iface.ProcessIncoming(data)
|
||||
}
|
||||
|
||||
func TestBaseInterface_ProcessOutgoing(t *testing.T) {
|
||||
iface := NewBaseInterface("test18", IF_TYPE_UDP, true)
|
||||
data := []byte("test data")
|
||||
|
||||
if err := iface.ProcessOutgoing(data); err != nil {
|
||||
t.Errorf("ProcessOutgoing() error = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_SendPathRequest(t *testing.T) {
|
||||
iface := NewBaseInterface("test19", IF_TYPE_UDP, true)
|
||||
data := []byte("path request")
|
||||
|
||||
if err := iface.SendPathRequest(data); err != nil {
|
||||
t.Errorf("SendPathRequest() error = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_SendLinkPacket(t *testing.T) {
|
||||
iface := NewBaseInterface("test20", IF_TYPE_UDP, true)
|
||||
dest := []byte("destination")
|
||||
data := []byte("link data")
|
||||
timestamp := time.Now()
|
||||
|
||||
if err := iface.SendLinkPacket(dest, data, timestamp); err != nil {
|
||||
t.Errorf("SendLinkPacket() error = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseInterface_GetBandwidthAvailable(t *testing.T) {
|
||||
iface := NewBaseInterface("test21", IF_TYPE_UDP, true)
|
||||
|
||||
if !iface.GetBandwidthAvailable() {
|
||||
t.Error("GetBandwidthAvailable() = false, want true when no recent transmission")
|
||||
}
|
||||
|
||||
iface.lastTx = time.Now()
|
||||
iface.TxBytes = 0
|
||||
if !iface.GetBandwidthAvailable() {
|
||||
t.Error("GetBandwidthAvailable() = false, want true when TxBytes is 0")
|
||||
}
|
||||
|
||||
iface.lastTx = time.Now().Add(-500 * time.Millisecond)
|
||||
iface.TxBytes = 1000
|
||||
iface.Bitrate = 1000000
|
||||
|
||||
if !iface.GetBandwidthAvailable() {
|
||||
t.Error("GetBandwidthAvailable() = false, want true when usage is below threshold")
|
||||
}
|
||||
|
||||
iface.TxBytes = 10000000
|
||||
iface.Bitrate = 1000
|
||||
if iface.GetBandwidthAvailable() {
|
||||
t.Error("GetBandwidthAvailable() = true, want false when usage exceeds threshold")
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,18 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package common
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Destination type constants
|
||||
const (
|
||||
DESTINATION_SINGLE = 0x00
|
||||
DESTINATION_GROUP = 0x01
|
||||
DESTINATION_PLAIN = 0x02
|
||||
)
|
||||
|
||||
// Transport related types
|
||||
type TransportMode byte
|
||||
type PathStatus byte
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package config
|
||||
|
||||
import (
|
||||
@@ -39,7 +41,8 @@ type Config struct {
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
file, err := os.Open(path)
|
||||
// bearer:disable go_gosec_filesystem_filereadtaint
|
||||
file, err := os.Open(path) // #nosec G304
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -176,7 +179,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 +197,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) {
|
||||
@@ -222,7 +225,6 @@ func InitConfig() (*Config, error) {
|
||||
cfg.Logging.Level = "info"
|
||||
cfg.Logging.File = filepath.Join(GetConfigDir(), "reticulum.log")
|
||||
|
||||
// Add default interfaces
|
||||
cfg.Interfaces = append(cfg.Interfaces, struct {
|
||||
Name string
|
||||
Type string
|
||||
|
||||
192
pkg/config/config_test.go
Normal file
192
pkg/config/config_test.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "test_config")
|
||||
|
||||
configContent := `[identity]
|
||||
name = test-identity
|
||||
storage_path = /tmp/test-storage
|
||||
|
||||
[transport]
|
||||
announce_interval = 300
|
||||
path_request_timeout = 15
|
||||
max_hops = 8
|
||||
bitrate_limit = 1000000
|
||||
|
||||
[logging]
|
||||
level = info
|
||||
file = /tmp/test.log
|
||||
|
||||
[interface test-interface]
|
||||
type = UDPInterface
|
||||
enabled = true
|
||||
listen_ip = 127.0.0.1
|
||||
listen_port = 37696
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg == nil {
|
||||
t.Fatal("LoadConfig() returned nil")
|
||||
}
|
||||
|
||||
if len(cfg.Interfaces) == 0 {
|
||||
t.Error("No interfaces loaded")
|
||||
}
|
||||
|
||||
iface := cfg.Interfaces[0]
|
||||
if iface.Type != "UDPInterface" {
|
||||
t.Errorf("Interface type = %s, want UDPInterface", iface.Type)
|
||||
}
|
||||
if !iface.Enabled {
|
||||
t.Error("Interface should be enabled")
|
||||
}
|
||||
if iface.ListenIP != "127.0.0.1" {
|
||||
t.Errorf("Interface ListenIP = %s, want 127.0.0.1", iface.ListenIP)
|
||||
}
|
||||
if iface.ListenPort != 37696 {
|
||||
t.Errorf("Interface ListenPort = %d, want 37696", iface.ListenPort)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_NonexistentFile(t *testing.T) {
|
||||
_, err := LoadConfig("/nonexistent/path/config")
|
||||
if err == nil {
|
||||
t.Error("LoadConfig() should return error for nonexistent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_EmptyFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "empty_config")
|
||||
|
||||
if err := os.WriteFile(configPath, []byte(""), 0600); err != nil {
|
||||
t.Fatalf("Failed to write empty config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg == nil {
|
||||
t.Fatal("LoadConfig() returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_CommentsAndEmptyLines(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "test_config")
|
||||
|
||||
configContent := `# Comment line
|
||||
|
||||
[identity]
|
||||
name = test
|
||||
# Another comment
|
||||
|
||||
[interface test-interface]
|
||||
# Interface comment
|
||||
type = UDPInterface
|
||||
enabled = true
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg == nil {
|
||||
t.Fatal("LoadConfig() returned nil")
|
||||
}
|
||||
|
||||
if cfg.Identity.Name != "test" {
|
||||
t.Errorf("Identity.Name = %s, want test", cfg.Identity.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "test_config")
|
||||
|
||||
cfg := &Config{}
|
||||
cfg.Identity.Name = "test-identity"
|
||||
cfg.Identity.StoragePath = "/tmp/test"
|
||||
cfg.Transport.AnnounceInterval = 600
|
||||
cfg.Logging.Level = "debug"
|
||||
cfg.Logging.File = "/tmp/test.log"
|
||||
|
||||
if err := SaveConfig(cfg, configPath); err != nil {
|
||||
t.Fatalf("SaveConfig() error = %v", err)
|
||||
}
|
||||
|
||||
loaded, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
|
||||
if loaded.Identity.Name != "test-identity" {
|
||||
t.Errorf("Identity.Name = %s, want test-identity", loaded.Identity.Name)
|
||||
}
|
||||
if loaded.Transport.AnnounceInterval != 600 {
|
||||
t.Errorf("Transport.AnnounceInterval = %d, want 600", loaded.Transport.AnnounceInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConfigDir(t *testing.T) {
|
||||
dir := GetConfigDir()
|
||||
if dir == "" {
|
||||
t.Error("GetConfigDir() returned empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDefaultConfigPath(t *testing.T) {
|
||||
path := GetDefaultConfigPath()
|
||||
if path == "" {
|
||||
t.Error("GetDefaultConfigPath() returned empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureConfigDir(t *testing.T) {
|
||||
if err := EnsureConfigDir(); err != nil {
|
||||
t.Fatalf("EnsureConfigDir() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
originalHome := os.Getenv("HOME")
|
||||
defer func() {
|
||||
if originalHome != "" {
|
||||
os.Setenv("HOME", originalHome)
|
||||
}
|
||||
}()
|
||||
|
||||
os.Setenv("HOME", tmpDir)
|
||||
|
||||
cfg, err := InitConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("InitConfig() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg == nil {
|
||||
t.Fatal("InitConfig() returned nil")
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package cryptography
|
||||
|
||||
import (
|
||||
@@ -8,19 +10,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 +50,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
|
||||
}
|
||||
|
||||
199
pkg/cryptography/aes_test.go
Normal file
199
pkg/cryptography/aes_test.go
Normal file
@@ -0,0 +1,199 @@
|
||||
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 byte that XORs with the last padding byte.
|
||||
// In CBC, P[i] = D(C[i]) ^ C[i-1].
|
||||
// The last byte of plaintext P[len-1] depends on C[len-1] and C[len-1-BlockSize].
|
||||
// If we modify C[len-1-BlockSize], we flip the bits of P[len-1] predictably.
|
||||
// If we modify C[len-1] (the last byte of ciphertext), we scramble the whole block D(C[len-1]),
|
||||
// which might accidentally result in valid padding (e.g. 0x01).
|
||||
// So we corrupt the IV (or previous block) corresponding to the last byte.
|
||||
corruptedCiphertext := make([]byte, len(ciphertext))
|
||||
copy(corruptedCiphertext, ciphertext)
|
||||
corruptedCiphertext[len(ciphertext)-aes.BlockSize-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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package cryptography
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package cryptography
|
||||
|
||||
import (
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package cryptography
|
||||
|
||||
import (
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,50 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package cryptography
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/hkdf"
|
||||
"errors"
|
||||
"math"
|
||||
)
|
||||
|
||||
func DeriveKey(secret, salt, info []byte, length int) ([]byte, error) {
|
||||
hkdfReader := hkdf.New(sha256.New, secret, salt, info)
|
||||
key := make([]byte, length)
|
||||
if _, err := io.ReadFull(hkdfReader, key); err != nil {
|
||||
return nil, err
|
||||
hashLen := 32
|
||||
|
||||
if length < 1 {
|
||||
return nil, errors.New("invalid output key length")
|
||||
}
|
||||
return key, nil
|
||||
|
||||
if len(secret) == 0 {
|
||||
return nil, errors.New("cannot derive key from empty input material")
|
||||
}
|
||||
|
||||
if len(salt) == 0 {
|
||||
salt = make([]byte, hashLen)
|
||||
}
|
||||
|
||||
if info == nil {
|
||||
info = []byte{}
|
||||
}
|
||||
|
||||
pseudorandomKey := hmac.New(sha256.New, salt)
|
||||
pseudorandomKey.Write(secret)
|
||||
prk := pseudorandomKey.Sum(nil)
|
||||
|
||||
block := []byte{}
|
||||
derived := []byte{}
|
||||
|
||||
iterations := int(math.Ceil(float64(length) / float64(hashLen)))
|
||||
for i := 0; i < iterations; i++ {
|
||||
h := hmac.New(sha256.New, prk)
|
||||
h.Write(block)
|
||||
h.Write(info)
|
||||
counter := byte((i + 1) % (0xFF + 1))
|
||||
h.Write([]byte{counter})
|
||||
block = h.Sum(nil)
|
||||
derived = append(derived, block...)
|
||||
}
|
||||
|
||||
return derived[:length], nil
|
||||
}
|
||||
|
||||
105
pkg/cryptography/hkdf_test.go
Normal file
105
pkg/cryptography/hkdf_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
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 should fail with empty secret")
|
||||
}
|
||||
})
|
||||
|
||||
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) {
|
||||
_, err := DeriveKey(secret, salt, info, 0)
|
||||
if err == nil {
|
||||
t.Errorf("DeriveKey should fail with zero length")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package cryptography
|
||||
|
||||
import (
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
117
pkg/debug/debug.go
Normal file
117
pkg/debug/debug.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package debug
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log/slog"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
DEBUG_CRITICAL = 1
|
||||
DEBUG_ERROR = 2
|
||||
DEBUG_INFO = 3
|
||||
DEBUG_VERBOSE = 4
|
||||
DEBUG_TRACE = 5
|
||||
DEBUG_PACKETS = 6
|
||||
DEBUG_ALL = 7
|
||||
)
|
||||
|
||||
var (
|
||||
debugLevel = flag.Int("debug", 3, "debug level (1-7)")
|
||||
logger *slog.Logger
|
||||
initialized bool
|
||||
)
|
||||
|
||||
func Init() {
|
||||
if initialized {
|
||||
return
|
||||
}
|
||||
initialized = true
|
||||
|
||||
var level slog.Level
|
||||
switch {
|
||||
case *debugLevel >= DEBUG_ALL:
|
||||
level = slog.LevelDebug
|
||||
case *debugLevel >= DEBUG_PACKETS:
|
||||
level = slog.LevelDebug
|
||||
case *debugLevel >= DEBUG_TRACE:
|
||||
level = slog.LevelDebug
|
||||
case *debugLevel >= DEBUG_VERBOSE:
|
||||
level = slog.LevelDebug
|
||||
case *debugLevel >= DEBUG_INFO:
|
||||
level = slog.LevelInfo
|
||||
case *debugLevel >= DEBUG_ERROR:
|
||||
level = slog.LevelWarn
|
||||
case *debugLevel >= DEBUG_CRITICAL:
|
||||
level = slog.LevelError
|
||||
default:
|
||||
level = slog.LevelError
|
||||
}
|
||||
|
||||
opts := &slog.HandlerOptions{
|
||||
Level: level,
|
||||
}
|
||||
logger = slog.New(slog.NewTextHandler(os.Stderr, opts))
|
||||
slog.SetDefault(logger)
|
||||
}
|
||||
|
||||
func GetLogger() *slog.Logger {
|
||||
if !initialized {
|
||||
Init()
|
||||
}
|
||||
return logger
|
||||
}
|
||||
|
||||
func Log(level int, msg string, args ...interface{}) {
|
||||
if !initialized {
|
||||
Init()
|
||||
}
|
||||
|
||||
if *debugLevel < level {
|
||||
return
|
||||
}
|
||||
|
||||
var slogLevel slog.Level
|
||||
switch {
|
||||
case level >= DEBUG_ALL:
|
||||
slogLevel = slog.LevelDebug
|
||||
case level >= DEBUG_PACKETS:
|
||||
slogLevel = slog.LevelDebug
|
||||
case level >= DEBUG_TRACE:
|
||||
slogLevel = slog.LevelDebug
|
||||
case level >= DEBUG_VERBOSE:
|
||||
slogLevel = slog.LevelDebug
|
||||
case level >= DEBUG_INFO:
|
||||
slogLevel = slog.LevelInfo
|
||||
case level >= DEBUG_ERROR:
|
||||
slogLevel = slog.LevelWarn
|
||||
case level >= DEBUG_CRITICAL:
|
||||
slogLevel = slog.LevelError
|
||||
default:
|
||||
slogLevel = slog.LevelError
|
||||
}
|
||||
|
||||
if !logger.Enabled(context.TODO(), slogLevel) {
|
||||
return
|
||||
}
|
||||
|
||||
allArgs := make([]interface{}, len(args)+2)
|
||||
copy(allArgs, args)
|
||||
allArgs[len(args)] = "debug_level"
|
||||
allArgs[len(args)+1] = level
|
||||
logger.Log(context.TODO(), slogLevel, msg, allArgs...)
|
||||
}
|
||||
|
||||
func SetDebugLevel(level int) {
|
||||
*debugLevel = level
|
||||
if initialized {
|
||||
Init()
|
||||
}
|
||||
}
|
||||
|
||||
func GetDebugLevel() int {
|
||||
return *debugLevel
|
||||
}
|
||||
185
pkg/debug/debug_test.go
Normal file
185
pkg/debug/debug_test.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
originalFlag := flag.CommandLine
|
||||
defer func() {
|
||||
flag.CommandLine = originalFlag
|
||||
initialized = false
|
||||
}()
|
||||
|
||||
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
debugLevel = flag.Int("debug", 3, "debug level")
|
||||
|
||||
Init()
|
||||
|
||||
if !initialized {
|
||||
t.Error("Init() should set initialized to true")
|
||||
}
|
||||
|
||||
if GetLogger() == nil {
|
||||
t.Error("GetLogger() should return non-nil logger after Init()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLogger(t *testing.T) {
|
||||
originalFlag := flag.CommandLine
|
||||
defer func() {
|
||||
flag.CommandLine = originalFlag
|
||||
initialized = false
|
||||
}()
|
||||
|
||||
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
debugLevel = flag.Int("debug", 3, "debug level")
|
||||
initialized = false
|
||||
|
||||
logger := GetLogger()
|
||||
if logger == nil {
|
||||
t.Error("GetLogger() should return non-nil logger")
|
||||
}
|
||||
|
||||
if !initialized {
|
||||
t.Error("GetLogger() should initialize if not already initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLog(t *testing.T) {
|
||||
originalFlag := flag.CommandLine
|
||||
defer func() {
|
||||
flag.CommandLine = originalFlag
|
||||
initialized = false
|
||||
}()
|
||||
|
||||
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
debugLevel = flag.Int("debug", 7, "debug level")
|
||||
initialized = false
|
||||
|
||||
Log(DEBUG_INFO, "test message", "key", "value")
|
||||
}
|
||||
|
||||
func TestSetDebugLevel(t *testing.T) {
|
||||
originalFlag := flag.CommandLine
|
||||
defer func() {
|
||||
flag.CommandLine = originalFlag
|
||||
initialized = false
|
||||
}()
|
||||
|
||||
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
debugLevel = flag.Int("debug", 3, "debug level")
|
||||
initialized = false
|
||||
|
||||
SetDebugLevel(5)
|
||||
if GetDebugLevel() != 5 {
|
||||
t.Errorf("SetDebugLevel(5) did not set level correctly, got %d", GetDebugLevel())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDebugLevel(t *testing.T) {
|
||||
originalFlag := flag.CommandLine
|
||||
defer func() {
|
||||
flag.CommandLine = originalFlag
|
||||
initialized = false
|
||||
}()
|
||||
|
||||
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
debugLevel = flag.Int("debug", 4, "debug level")
|
||||
|
||||
level := GetDebugLevel()
|
||||
if level != 4 {
|
||||
t.Errorf("GetDebugLevel() = %d, want 4", level)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLog_LevelFiltering(t *testing.T) {
|
||||
originalFlag := flag.CommandLine
|
||||
defer func() {
|
||||
flag.CommandLine = originalFlag
|
||||
initialized = false
|
||||
}()
|
||||
|
||||
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
debugLevel = flag.Int("debug", 3, "debug level")
|
||||
initialized = false
|
||||
|
||||
Log(DEBUG_TRACE, "trace message")
|
||||
Log(DEBUG_INFO, "info message")
|
||||
Log(DEBUG_ERROR, "error message")
|
||||
}
|
||||
|
||||
func TestConstants(t *testing.T) {
|
||||
if DEBUG_CRITICAL != 1 {
|
||||
t.Errorf("DEBUG_CRITICAL = %d, want 1", DEBUG_CRITICAL)
|
||||
}
|
||||
if DEBUG_ERROR != 2 {
|
||||
t.Errorf("DEBUG_ERROR = %d, want 2", DEBUG_ERROR)
|
||||
}
|
||||
if DEBUG_INFO != 3 {
|
||||
t.Errorf("DEBUG_INFO = %d, want 3", DEBUG_INFO)
|
||||
}
|
||||
if DEBUG_VERBOSE != 4 {
|
||||
t.Errorf("DEBUG_VERBOSE = %d, want 4", DEBUG_VERBOSE)
|
||||
}
|
||||
if DEBUG_TRACE != 5 {
|
||||
t.Errorf("DEBUG_TRACE = %d, want 5", DEBUG_TRACE)
|
||||
}
|
||||
if DEBUG_PACKETS != 6 {
|
||||
t.Errorf("DEBUG_PACKETS = %d, want 6", DEBUG_PACKETS)
|
||||
}
|
||||
if DEBUG_ALL != 7 {
|
||||
t.Errorf("DEBUG_ALL = %d, want 7", DEBUG_ALL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLog_WithArgs(t *testing.T) {
|
||||
originalFlag := flag.CommandLine
|
||||
defer func() {
|
||||
flag.CommandLine = originalFlag
|
||||
initialized = false
|
||||
}()
|
||||
|
||||
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
debugLevel = flag.Int("debug", 7, "debug level")
|
||||
initialized = false
|
||||
|
||||
Log(DEBUG_INFO, "test message", "key1", "value1", "key2", "value2")
|
||||
}
|
||||
|
||||
func TestInit_MultipleCalls(t *testing.T) {
|
||||
originalFlag := flag.CommandLine
|
||||
defer func() {
|
||||
flag.CommandLine = originalFlag
|
||||
initialized = false
|
||||
}()
|
||||
|
||||
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
debugLevel = flag.Int("debug", 3, "debug level")
|
||||
initialized = false
|
||||
|
||||
Init()
|
||||
firstLogger := GetLogger()
|
||||
|
||||
Init()
|
||||
secondLogger := GetLogger()
|
||||
|
||||
if firstLogger != secondLogger {
|
||||
t.Error("Multiple Init() calls should not create new loggers")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLog_DisabledLevel(t *testing.T) {
|
||||
originalFlag := flag.CommandLine
|
||||
defer func() {
|
||||
flag.CommandLine = originalFlag
|
||||
initialized = false
|
||||
}()
|
||||
|
||||
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
debugLevel = flag.Int("debug", 1, "debug level")
|
||||
initialized = false
|
||||
|
||||
Log(DEBUG_TRACE, "this should be filtered")
|
||||
}
|
||||
@@ -1,22 +1,35 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package destination
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/announce"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
const (
|
||||
// Destination direction types
|
||||
// The IN bit specifies that the destination can receive traffic.
|
||||
// The OUT bit specifies that the destination can send traffic.
|
||||
// A destination can be both IN and OUT.
|
||||
IN = 0x01
|
||||
OUT = 0x02
|
||||
|
||||
// Destination types
|
||||
SINGLE = 0x00
|
||||
GROUP = 0x01
|
||||
PLAIN = 0x02
|
||||
@@ -31,15 +44,6 @@ const (
|
||||
|
||||
RATCHET_COUNT = 512 // Default number of retained ratchet keys
|
||||
RATCHET_INTERVAL = 1800 // Minimum interval between ratchet rotations in seconds
|
||||
|
||||
// Debug levels
|
||||
DEBUG_CRITICAL = 1 // Critical errors
|
||||
DEBUG_ERROR = 2 // Non-critical errors
|
||||
DEBUG_INFO = 3 // Important information
|
||||
DEBUG_VERBOSE = 4 // Detailed information
|
||||
DEBUG_TRACE = 5 // Very detailed tracing
|
||||
DEBUG_PACKETS = 6 // Packet-level details
|
||||
DEBUG_ALL = 7 // Everything
|
||||
)
|
||||
|
||||
type PacketCallback = common.PacketCallback
|
||||
@@ -51,6 +55,21 @@ type RequestHandler struct {
|
||||
ResponseGenerator func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity *identity.Identity, requestedAt int64) []byte
|
||||
AllowMode byte
|
||||
AllowedList [][]byte
|
||||
AutoCompress bool
|
||||
}
|
||||
|
||||
type Transport interface {
|
||||
GetConfig() *common.ReticulumConfig
|
||||
GetInterfaces() map[string]common.NetworkInterface
|
||||
RegisterDestination(hash []byte, dest interface{})
|
||||
}
|
||||
|
||||
type IncomingLinkHandler func(pkt *packet.Packet, dest *Destination, transport interface{}, networkIface common.NetworkInterface) (interface{}, error)
|
||||
|
||||
var incomingLinkHandler IncomingLinkHandler
|
||||
|
||||
func RegisterIncomingLinkHandler(handler IncomingLinkHandler) {
|
||||
incomingLinkHandler = handler
|
||||
}
|
||||
|
||||
type Destination struct {
|
||||
@@ -60,6 +79,7 @@ type Destination struct {
|
||||
appName string
|
||||
aspects []string
|
||||
hashValue []byte
|
||||
transport Transport
|
||||
|
||||
acceptsLinks bool
|
||||
proofStrategy byte
|
||||
@@ -68,11 +88,15 @@ type Destination struct {
|
||||
proofCallback ProofRequestedCallback
|
||||
linkCallback LinkEstablishedCallback
|
||||
|
||||
ratchetsEnabled bool
|
||||
ratchetPath string
|
||||
ratchetCount int
|
||||
ratchetInterval int
|
||||
enforceRatchets bool
|
||||
ratchetsEnabled bool
|
||||
ratchetPath string
|
||||
ratchetCount int
|
||||
ratchetInterval int
|
||||
enforceRatchets bool
|
||||
latestRatchetTime time.Time
|
||||
latestRatchetID []byte
|
||||
ratchets [][]byte
|
||||
ratchetFileLock sync.Mutex
|
||||
|
||||
defaultAppData []byte
|
||||
mutex sync.RWMutex
|
||||
@@ -80,15 +104,11 @@ type Destination struct {
|
||||
requestHandlers map[string]*RequestHandler
|
||||
}
|
||||
|
||||
func debugLog(level int, format string, v ...interface{}) {
|
||||
log.Printf("[DEBUG-%d] %s", level, fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func New(id *identity.Identity, direction byte, destType byte, appName string, aspects ...string) (*Destination, error) {
|
||||
debugLog(DEBUG_INFO, "Creating new destination: app=%s type=%d direction=%d", appName, destType, direction)
|
||||
func New(id *identity.Identity, direction byte, destType byte, appName string, transport Transport, aspects ...string) (*Destination, error) {
|
||||
debug.Log(debug.DEBUG_INFO, "Creating new destination", "app", appName, "type", destType, "direction", direction)
|
||||
|
||||
if id == nil {
|
||||
debugLog(DEBUG_ERROR, "Cannot create destination: identity is nil")
|
||||
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil")
|
||||
return nil, errors.New("identity cannot be nil")
|
||||
}
|
||||
|
||||
@@ -98,6 +118,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,
|
||||
@@ -107,27 +128,68 @@ func New(id *identity.Identity, direction byte, destType byte, appName string, a
|
||||
|
||||
// Generate destination hash
|
||||
d.hashValue = d.calculateHash()
|
||||
debugLog(DEBUG_VERBOSE, "Created destination with hash: %x", d.hashValue)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Created destination with hash", "hash", fmt.Sprintf("%x", d.hashValue))
|
||||
|
||||
// Auto-register with transport if direction is IN
|
||||
if (direction & IN) != 0 {
|
||||
transport.RegisterDestination(d.hashValue, d)
|
||||
debug.Log(debug.DEBUG_INFO, "Destination auto-registered with transport", "hash", fmt.Sprintf("%x", d.hashValue))
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// FromHash creates a destination from a known hash (e.g., from an announce).
|
||||
// This is used by clients to create destination objects for servers they've discovered.
|
||||
func FromHash(hash []byte, id *identity.Identity, destType byte, transport Transport) (*Destination, error) {
|
||||
debug.Log(debug.DEBUG_INFO, "Creating destination from hash", "hash", fmt.Sprintf("%x", hash))
|
||||
|
||||
if id == nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil")
|
||||
return nil, errors.New("identity cannot be nil")
|
||||
}
|
||||
|
||||
d := &Destination{
|
||||
identity: id,
|
||||
direction: OUT,
|
||||
destType: destType,
|
||||
hashValue: hash,
|
||||
transport: transport,
|
||||
acceptsLinks: false,
|
||||
proofStrategy: PROVE_NONE,
|
||||
ratchetCount: RATCHET_COUNT,
|
||||
ratchetInterval: RATCHET_INTERVAL,
|
||||
requestHandlers: make(map[string]*RequestHandler),
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Created destination from hash", "hash", fmt.Sprintf("%x", hash))
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *Destination) calculateHash() []byte {
|
||||
debugLog(DEBUG_TRACE, "Calculating hash for destination %s", d.ExpandName())
|
||||
debug.Log(debug.DEBUG_TRACE, "Calculating hash for destination", "name", d.ExpandName())
|
||||
|
||||
nameHash := sha256.Sum256([]byte(d.ExpandName()))
|
||||
identityHash := sha256.Sum256(d.identity.GetPublicKey())
|
||||
// destination_hash = SHA256(name_hash_10bytes + identity_hash_16bytes)[:16]
|
||||
// Identity hash is the truncated hash of the public key (16 bytes)
|
||||
identityHash := identity.TruncatedHash(d.identity.GetPublicKey())
|
||||
|
||||
debugLog(DEBUG_ALL, "Name hash: %x", nameHash)
|
||||
debugLog(DEBUG_ALL, "Identity hash: %x", identityHash)
|
||||
// Name hash is the FULL 32-byte SHA256, then we take first 10 bytes for concatenation
|
||||
nameHashFull := sha256.Sum256([]byte(d.ExpandName()))
|
||||
nameHash10 := nameHashFull[:10] // Only use 10 bytes
|
||||
|
||||
combined := append(nameHash[:], identityHash[:]...)
|
||||
finalHash := sha256.Sum256(combined)
|
||||
debug.Log(debug.DEBUG_ALL, "Identity hash", "hash", fmt.Sprintf("%x", identityHash))
|
||||
debug.Log(debug.DEBUG_ALL, "Name hash (10 bytes)", "hash", fmt.Sprintf("%x", nameHash10))
|
||||
|
||||
truncated := finalHash[:16]
|
||||
debugLog(DEBUG_VERBOSE, "Calculated destination hash: %x", truncated)
|
||||
// Concatenate name_hash (10 bytes) + identity_hash (16 bytes) = 26 bytes
|
||||
combined := append(nameHash10, identityHash...)
|
||||
|
||||
return truncated
|
||||
// Then hash again and truncate to 16 bytes
|
||||
finalHashFull := sha256.Sum256(combined)
|
||||
finalHash := finalHashFull[:16]
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Calculated destination hash", "hash", fmt.Sprintf("%x", finalHash))
|
||||
|
||||
return finalHash
|
||||
}
|
||||
|
||||
func (d *Destination) ExpandName() string {
|
||||
@@ -138,71 +200,68 @@ func (d *Destination) ExpandName() string {
|
||||
return name
|
||||
}
|
||||
|
||||
func (d *Destination) Announce(appData []byte) error {
|
||||
func (d *Destination) Announce(pathResponse bool, tag []byte, attachedInterface common.NetworkInterface) error {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
log.Printf("[DEBUG-4] Creating announce packet for destination %s", d.ExpandName())
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Announcing destination", "name", d.ExpandName(), "path_response", pathResponse)
|
||||
|
||||
// If no specific appData provided, use default
|
||||
if appData == nil {
|
||||
log.Printf("[DEBUG-4] Using default app data for announce")
|
||||
appData = d.defaultAppData
|
||||
appData := d.defaultAppData
|
||||
|
||||
// Create announce packet using announce package
|
||||
announceObj, err := announce.New(d.identity, d.hashValue, d.ExpandName(), appData, pathResponse, d.transport.GetConfig())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create announce: %w", err)
|
||||
}
|
||||
|
||||
// Create announce packet
|
||||
packet := make([]byte, 0, 256) // Pre-allocate reasonable size
|
||||
packet := announceObj.GetPacket()
|
||||
if packet == nil {
|
||||
return errors.New("failed to create announce packet")
|
||||
}
|
||||
|
||||
// Add packet type and header
|
||||
packet = append(packet, 0x01) // PACKET_TYPE_ANNOUNCE
|
||||
packet = append(packet, 0x00) // Initial hop count
|
||||
if pathResponse && tag != nil {
|
||||
debug.Log(debug.DEBUG_INFO, "Sending path response announce", "tag", fmt.Sprintf("%x", tag))
|
||||
}
|
||||
|
||||
// Add destination hash (16 bytes)
|
||||
packet = append(packet, d.hashValue...)
|
||||
log.Printf("[DEBUG-4] Added destination hash %x to announce", d.hashValue[:8])
|
||||
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")
|
||||
var lastErr error
|
||||
if attachedInterface != nil {
|
||||
if attachedInterface.IsEnabled() && attachedInterface.IsOnline() {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Sending announce to attached interface", "name", attachedInterface.GetName())
|
||||
if err := attachedInterface.Send(packet, ""); err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to send announce on attached interface", "error", err)
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
interfaces := d.transport.GetInterfaces()
|
||||
for name, iface := range interfaces {
|
||||
if iface.IsEnabled() && iface.IsOnline() {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Sending announce to interface", "name", name)
|
||||
if err := iface.Send(packet, ""); err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to send announce on interface", "name", name, "error", 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) {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
d.acceptsLinks = accepts
|
||||
|
||||
// Register with transport if accepting links
|
||||
if accepts && d.transport != nil {
|
||||
d.transport.RegisterDestination(d.hashValue, d)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Destination registered with transport for link requests", "hash", fmt.Sprintf("%x", d.hashValue))
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Destination) SetLinkEstablishedCallback(callback common.LinkEstablishedCallback) {
|
||||
@@ -211,12 +270,72 @@ func (d *Destination) SetLinkEstablishedCallback(callback common.LinkEstablished
|
||||
d.linkCallback = callback
|
||||
}
|
||||
|
||||
func (d *Destination) GetLinkCallback() common.LinkEstablishedCallback {
|
||||
d.mutex.RLock()
|
||||
defer d.mutex.RUnlock()
|
||||
return d.linkCallback
|
||||
}
|
||||
|
||||
func (d *Destination) HandleIncomingLinkRequest(pkt interface{}, transport interface{}, networkIface common.NetworkInterface) error {
|
||||
debug.Log(debug.DEBUG_INFO, "Handling incoming link request for destination", "hash", fmt.Sprintf("%x", d.GetHash()))
|
||||
|
||||
pktObj, ok := pkt.(*packet.Packet)
|
||||
if !ok {
|
||||
return errors.New("invalid packet type")
|
||||
}
|
||||
|
||||
if incomingLinkHandler == nil {
|
||||
return errors.New("no incoming link handler registered")
|
||||
}
|
||||
|
||||
linkIface, err := incomingLinkHandler(pktObj, d, transport, networkIface)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to handle link request: %w", err)
|
||||
}
|
||||
|
||||
if d.linkCallback != nil && linkIface != nil {
|
||||
debug.Log(debug.DEBUG_INFO, "Calling link established callback")
|
||||
d.linkCallback(linkIface)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Destination) SetPacketCallback(callback common.PacketCallback) {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
d.packetCallback = callback
|
||||
}
|
||||
|
||||
func (d *Destination) Receive(pkt *packet.Packet, iface common.NetworkInterface) {
|
||||
d.mutex.RLock()
|
||||
callback := d.packetCallback
|
||||
d.mutex.RUnlock()
|
||||
|
||||
if callback == nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "No packet callback set for destination")
|
||||
return
|
||||
}
|
||||
|
||||
if pkt.PacketType == packet.PacketTypeLinkReq {
|
||||
debug.Log(debug.DEBUG_INFO, "Received link request for destination")
|
||||
if err := d.HandleIncomingLinkRequest(pkt, d.transport, iface); err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to handle incoming link request", "error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
plaintext, err := d.Decrypt(pkt.Data)
|
||||
if err != nil {
|
||||
debug.Log(debug.DEBUG_INFO, "Failed to decrypt packet data", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "Destination received packet", "bytes", len(plaintext))
|
||||
|
||||
callback(plaintext, iface)
|
||||
}
|
||||
|
||||
func (d *Destination) SetProofRequestedCallback(callback common.ProofRequestedCallback) {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
@@ -233,8 +352,27 @@ func (d *Destination) EnableRatchets(path string) bool {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
if path == "" {
|
||||
debug.Log(debug.DEBUG_ERROR, "No ratchet file path specified")
|
||||
return false
|
||||
}
|
||||
|
||||
d.ratchetsEnabled = true
|
||||
d.ratchetPath = path
|
||||
d.latestRatchetTime = time.Time{} // Zero time to force rotation
|
||||
|
||||
// Load or initialize ratchets
|
||||
if err := d.reloadRatchets(); err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to load ratchets", "error", err)
|
||||
// Initialize empty ratchet list
|
||||
d.ratchets = make([][]byte, 0)
|
||||
if err := d.persistRatchets(); err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to create initial ratchet file", "error", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "Ratchets enabled", "path", path)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -315,34 +453,88 @@ func (d *Destination) DeregisterRequestHandler(path string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *Destination) GetRequestHandler(pathHash []byte) func([]byte, []byte, []byte, []byte, *identity.Identity, time.Time) interface{} {
|
||||
d.mutex.RLock()
|
||||
defer d.mutex.RUnlock()
|
||||
|
||||
for _, handler := range d.requestHandlers {
|
||||
handlerPathHash := identity.TruncatedHash([]byte(handler.Path))
|
||||
if string(handlerPathHash) == string(pathHash) {
|
||||
return func(pathHash []byte, data []byte, requestID []byte, linkID []byte, remoteIdentity *identity.Identity, requestedAt time.Time) interface{} {
|
||||
allowed := false
|
||||
if handler.AllowMode == ALLOW_ALL {
|
||||
allowed = true
|
||||
} else if handler.AllowMode == ALLOW_LIST && remoteIdentity != nil {
|
||||
remoteHash := remoteIdentity.Hash()
|
||||
for _, allowedHash := range handler.AllowedList {
|
||||
if string(remoteHash) == string(allowedHash) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := handler.ResponseGenerator(handler.Path, data, requestID, linkID, remoteIdentity, requestedAt.Unix())
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Destination) HandleRequest(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity *identity.Identity, requestedAt int64) []byte {
|
||||
d.mutex.RLock()
|
||||
handler, exists := d.requestHandlers[path]
|
||||
d.mutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
debug.Log(debug.DEBUG_INFO, "No handler registered for path", "path", path)
|
||||
return []byte(">Not Found\n\nThe requested resource was not found.")
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Calling request handler", "path", path)
|
||||
result := handler.ResponseGenerator(path, data, requestID, linkID, remoteIdentity, requestedAt)
|
||||
if result == nil {
|
||||
return []byte(">Not Found\n\nThe requested resource was not found.")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (d *Destination) Encrypt(plaintext []byte) ([]byte, error) {
|
||||
if d.destType == PLAIN {
|
||||
log.Printf("[DEBUG-4] Using plaintext transmission for PLAIN destination")
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Using plaintext transmission for PLAIN destination")
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
if d.identity == nil {
|
||||
log.Printf("[DEBUG-3] Cannot encrypt: no identity available")
|
||||
debug.Log(debug.DEBUG_INFO, "Cannot encrypt: no identity available")
|
||||
return nil, errors.New("no identity available for encryption")
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-4] Encrypting %d bytes for destination type %d", len(plaintext), d.destType)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Encrypting bytes for destination", "bytes", len(plaintext), "destType", d.destType)
|
||||
|
||||
switch d.destType {
|
||||
case SINGLE:
|
||||
recipientKey := d.identity.GetPublicKey()
|
||||
log.Printf("[DEBUG-4] Encrypting for single recipient with key %x", recipientKey[:8])
|
||||
recipientKey := d.identity.GetEncryptionKey()
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Encrypting for single recipient", "key", fmt.Sprintf("%x", recipientKey[:8]))
|
||||
return d.identity.Encrypt(plaintext, recipientKey)
|
||||
case GROUP:
|
||||
key := d.identity.GetCurrentRatchetKey()
|
||||
if key == nil {
|
||||
log.Printf("[DEBUG-3] Cannot encrypt: no ratchet key available")
|
||||
debug.Log(debug.DEBUG_INFO, "Cannot encrypt: no ratchet key available")
|
||||
return nil, errors.New("no ratchet key available")
|
||||
}
|
||||
log.Printf("[DEBUG-4] Encrypting for group with ratchet key %x", key[:8])
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Encrypting for group with ratchet key", "key", fmt.Sprintf("%x", key[:8]))
|
||||
return d.identity.EncryptWithHMAC(plaintext, key)
|
||||
default:
|
||||
log.Printf("[DEBUG-3] Unsupported destination type %d for encryption", d.destType)
|
||||
debug.Log(debug.DEBUG_INFO, "Unsupported destination type for encryption", "destType", d.destType)
|
||||
return nil, errors.New("unsupported destination type for encryption")
|
||||
}
|
||||
}
|
||||
@@ -403,3 +595,186 @@ func (d *Destination) GetHash() []byte {
|
||||
}
|
||||
return d.hashValue
|
||||
}
|
||||
|
||||
func (d *Destination) persistRatchets() error {
|
||||
d.ratchetFileLock.Lock()
|
||||
defer d.ratchetFileLock.Unlock()
|
||||
|
||||
if !d.ratchetsEnabled || d.ratchetPath == "" {
|
||||
return errors.New("ratchets not enabled or no path specified")
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_PACKETS, "Persisting ratchets", "count", len(d.ratchets), "path", d.ratchetPath)
|
||||
|
||||
// Pack ratchets using msgpack
|
||||
packedRatchets, err := msgpack.Marshal(d.ratchets)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to pack ratchets: %w", err)
|
||||
}
|
||||
|
||||
// Sign the packed ratchets
|
||||
signature, err := d.Sign(packedRatchets)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sign ratchets: %w", err)
|
||||
}
|
||||
|
||||
// Create structure
|
||||
persistedData := map[string][]byte{
|
||||
"signature": signature,
|
||||
"ratchets": packedRatchets,
|
||||
}
|
||||
|
||||
// Pack the entire structure
|
||||
finalData, err := msgpack.Marshal(persistedData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to pack ratchet data: %w", err)
|
||||
}
|
||||
|
||||
// Write to temporary file first, then rename (atomic operation)
|
||||
tempPath := d.ratchetPath + ".tmp"
|
||||
file, err := os.Create(tempPath) // #nosec G304
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp ratchet file: %w", err)
|
||||
}
|
||||
|
||||
if _, err := file.Write(finalData); err != nil {
|
||||
// #nosec G104 - Error already being handled, cleanup errors are non-critical
|
||||
file.Close()
|
||||
// #nosec G104 - Error already being handled, cleanup errors are non-critical
|
||||
os.Remove(tempPath)
|
||||
return fmt.Errorf("failed to write ratchet data: %w", err)
|
||||
}
|
||||
// #nosec G104 - File is being closed after successful write, error is non-critical
|
||||
file.Close()
|
||||
|
||||
// Remove old file if exists
|
||||
if _, err := os.Stat(d.ratchetPath); err == nil {
|
||||
// #nosec G104 - Removing old file, error is non-critical if it doesn't exist
|
||||
os.Remove(d.ratchetPath)
|
||||
}
|
||||
|
||||
// Atomic rename
|
||||
if err := os.Rename(tempPath, d.ratchetPath); err != nil {
|
||||
// #nosec G104 - Error already being handled, cleanup errors are non-critical
|
||||
os.Remove(tempPath)
|
||||
return fmt.Errorf("failed to rename ratchet file: %w", err)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_PACKETS, "Ratchets persisted successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Destination) reloadRatchets() error {
|
||||
d.ratchetFileLock.Lock()
|
||||
defer d.ratchetFileLock.Unlock()
|
||||
|
||||
if _, err := os.Stat(d.ratchetPath); os.IsNotExist(err) {
|
||||
debug.Log(debug.DEBUG_INFO, "No existing ratchet data found, initializing new ratchet file")
|
||||
d.ratchets = make([][]byte, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.Open(d.ratchetPath) // #nosec G304
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open ratchet file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Read all data
|
||||
fileData, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read ratchet file: %w", err)
|
||||
}
|
||||
|
||||
// Unpack outer structure
|
||||
var persistedData map[string][]byte
|
||||
if err := msgpack.Unmarshal(fileData, &persistedData); err != nil {
|
||||
return fmt.Errorf("failed to unpack ratchet data: %w", err)
|
||||
}
|
||||
|
||||
signature, hasSignature := persistedData["signature"]
|
||||
packedRatchets, hasRatchets := persistedData["ratchets"]
|
||||
|
||||
if !hasSignature || !hasRatchets {
|
||||
return fmt.Errorf("invalid ratchet file format")
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
if !d.identity.Verify(packedRatchets, signature) {
|
||||
return fmt.Errorf("invalid ratchet file signature")
|
||||
}
|
||||
|
||||
// Unpack ratchet list
|
||||
if err := msgpack.Unmarshal(packedRatchets, &d.ratchets); err != nil {
|
||||
return fmt.Errorf("failed to unpack ratchet list: %w", err)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "Ratchets reloaded successfully", "count", len(d.ratchets))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Destination) RotateRatchets() error {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
if !d.ratchetsEnabled {
|
||||
return errors.New("ratchets not enabled")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if !d.latestRatchetTime.IsZero() && now.Before(d.latestRatchetTime.Add(time.Duration(d.ratchetInterval)*time.Second)) {
|
||||
debug.Log(debug.DEBUG_TRACE, "Ratchet rotation interval not reached")
|
||||
return nil
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "Rotating ratchets", "destination", d.ExpandName())
|
||||
|
||||
// Generate new ratchet key (32 bytes for X25519 private key)
|
||||
newRatchet := make([]byte, 32)
|
||||
if _, err := io.ReadFull(rand.Reader, newRatchet); err != nil {
|
||||
return fmt.Errorf("failed to generate new ratchet: %w", err)
|
||||
}
|
||||
|
||||
// Insert at beginning (most recent first)
|
||||
d.ratchets = append([][]byte{newRatchet}, d.ratchets...)
|
||||
d.latestRatchetTime = now
|
||||
|
||||
// Get ratchet public key for ID
|
||||
ratchetPub, err := curve25519.X25519(newRatchet, curve25519.Basepoint)
|
||||
if err == nil {
|
||||
d.latestRatchetID = identity.TruncatedHash(ratchetPub)[:identity.NAME_HASH_LENGTH/8]
|
||||
}
|
||||
|
||||
// Clean old ratchets
|
||||
d.cleanRatchets()
|
||||
|
||||
// Persist to disk
|
||||
if err := d.persistRatchets(); err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to persist ratchets after rotation", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "Ratchet rotation completed", "total_ratchets", len(d.ratchets))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Destination) cleanRatchets() {
|
||||
if len(d.ratchets) > d.ratchetCount {
|
||||
debug.Log(debug.DEBUG_TRACE, "Cleaning old ratchets", "before", len(d.ratchets), "keeping", d.ratchetCount)
|
||||
d.ratchets = d.ratchets[:d.ratchetCount]
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Destination) GetRatchets() [][]byte {
|
||||
d.mutex.RLock()
|
||||
defer d.mutex.RUnlock()
|
||||
|
||||
if !d.ratchetsEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return copy to prevent external modification
|
||||
ratchetsCopy := make([][]byte, len(d.ratchets))
|
||||
copy(ratchetsCopy, d.ratchets)
|
||||
return ratchetsCopy
|
||||
}
|
||||
|
||||
152
pkg/destination/destination_test.go
Normal file
152
pkg/destination/destination_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package destination
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
||||
)
|
||||
|
||||
type mockTransport struct {
|
||||
config *common.ReticulumConfig
|
||||
interfaces map[string]common.NetworkInterface
|
||||
}
|
||||
|
||||
func (m *mockTransport) GetConfig() *common.ReticulumConfig {
|
||||
return m.config
|
||||
}
|
||||
|
||||
func (m *mockTransport) GetInterfaces() map[string]common.NetworkInterface {
|
||||
return m.interfaces
|
||||
}
|
||||
|
||||
func (m *mockTransport) RegisterDestination(hash []byte, dest interface{}) {
|
||||
}
|
||||
|
||||
type mockInterface struct {
|
||||
common.BaseInterface
|
||||
}
|
||||
|
||||
func (m *mockInterface) Send(data []byte, address string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestNewDestination(t *testing.T) {
|
||||
id, _ := identity.New()
|
||||
transport := &mockTransport{config: &common.ReticulumConfig{}}
|
||||
|
||||
dest, err := New(id, IN|OUT, SINGLE, "testapp", transport, "testaspect")
|
||||
if err != nil {
|
||||
t.Fatalf("New failed: %v", err)
|
||||
}
|
||||
if dest == nil {
|
||||
t.Fatal("New returned nil")
|
||||
}
|
||||
|
||||
if dest.ExpandName() != "testapp.testaspect" {
|
||||
t.Errorf("Expected name testapp.testaspect, got %s", dest.ExpandName())
|
||||
}
|
||||
|
||||
hash := dest.GetHash()
|
||||
if len(hash) != 16 {
|
||||
t.Errorf("Expected hash length 16, got %d", len(hash))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromHash(t *testing.T) {
|
||||
id, _ := identity.New()
|
||||
transport := &mockTransport{}
|
||||
hash := make([]byte, 16)
|
||||
|
||||
dest, err := FromHash(hash, id, SINGLE, transport)
|
||||
if err != nil {
|
||||
t.Fatalf("FromHash failed: %v", err)
|
||||
}
|
||||
if !bytes.Equal(dest.GetHash(), hash) {
|
||||
t.Error("Hashes don't match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestHandlers(t *testing.T) {
|
||||
id, _ := identity.New()
|
||||
dest, _ := New(id, IN, SINGLE, "test", &mockTransport{})
|
||||
|
||||
path := "test/path"
|
||||
response := []byte("hello")
|
||||
|
||||
err := dest.RegisterRequestHandler(path, func(p string, d []byte, rid []byte, lid []byte, ri *identity.Identity, ra int64) []byte {
|
||||
return response
|
||||
}, ALLOW_ALL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("RegisterRequestHandler failed: %v", err)
|
||||
}
|
||||
|
||||
result := dest.HandleRequest(path, nil, nil, nil, nil, 0)
|
||||
if !bytes.Equal(result, response) {
|
||||
t.Errorf("Expected response %q, got %q", response, result)
|
||||
}
|
||||
|
||||
if !dest.DeregisterRequestHandler(path) {
|
||||
t.Error("DeregisterRequestHandler failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDecrypt(t *testing.T) {
|
||||
id, _ := identity.New()
|
||||
dest, _ := New(id, IN|OUT, SINGLE, "test", &mockTransport{})
|
||||
|
||||
plaintext := []byte("hello world")
|
||||
ciphertext, err := dest.Encrypt(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Encrypt failed: %v", err)
|
||||
}
|
||||
|
||||
decrypted, err := dest.Decrypt(ciphertext)
|
||||
if err != nil {
|
||||
t.Fatalf("Decrypt failed: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(plaintext, decrypted) {
|
||||
t.Errorf("Decrypted data doesn't match: %q vs %q", decrypted, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRatchets(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
ratchetPath := filepath.Join(tmpDir, "ratchets")
|
||||
|
||||
id, _ := identity.New()
|
||||
dest, _ := New(id, IN|OUT, SINGLE, "test", &mockTransport{})
|
||||
|
||||
if !dest.EnableRatchets(ratchetPath) {
|
||||
t.Fatal("EnableRatchets failed")
|
||||
}
|
||||
|
||||
err := dest.RotateRatchets()
|
||||
if err != nil {
|
||||
t.Fatalf("RotateRatchets failed: %v", err)
|
||||
}
|
||||
|
||||
ratchets := dest.GetRatchets()
|
||||
if len(ratchets) != 1 {
|
||||
t.Errorf("Expected 1 ratchet, got %d", len(ratchets))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlainDestination(t *testing.T) {
|
||||
id, _ := identity.New()
|
||||
dest, _ := New(id, IN|OUT, PLAIN, "test", &mockTransport{})
|
||||
|
||||
plaintext := []byte("plain text")
|
||||
ciphertext, _ := dest.Encrypt(plaintext)
|
||||
if !bytes.Equal(plaintext, ciphertext) {
|
||||
t.Error("Plain destination should not encrypt")
|
||||
}
|
||||
|
||||
decrypted, _ := dest.Decrypt(ciphertext)
|
||||
if !bytes.Equal(plaintext, decrypted) {
|
||||
t.Error("Plain destination should not decrypt")
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package identity
|
||||
|
||||
import (
|
||||
@@ -8,17 +10,17 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/cryptography"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/cryptography"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
@@ -44,7 +46,7 @@ const (
|
||||
type Identity struct {
|
||||
privateKey []byte
|
||||
publicKey []byte
|
||||
signingKey ed25519.PrivateKey
|
||||
signingSeed []byte // 32-byte Ed25519 seed
|
||||
verificationKey ed25519.PublicKey
|
||||
hash []byte
|
||||
hexHash string
|
||||
@@ -56,9 +58,10 @@ type Identity struct {
|
||||
}
|
||||
|
||||
var (
|
||||
knownDestinations = make(map[string][]interface{})
|
||||
knownRatchets = make(map[string][]byte)
|
||||
ratchetPersistLock sync.Mutex
|
||||
knownDestinations = make(map[string][]interface{})
|
||||
knownDestinationsLock sync.RWMutex
|
||||
knownRatchets = make(map[string][]byte)
|
||||
ratchetPersistLock sync.Mutex
|
||||
)
|
||||
|
||||
func New() (*Identity, error) {
|
||||
@@ -76,13 +79,18 @@ func New() (*Identity, error) {
|
||||
i.privateKey = privKey
|
||||
i.publicKey = pubKey
|
||||
|
||||
// Generate Ed25519 signing keypair
|
||||
verificationKey, signingKey, err := cryptography.GenerateSigningKeyPair()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate Ed25519 keypair: %v", err)
|
||||
// Generate 32-byte Ed25519 seed
|
||||
var ed25519Seed [32]byte
|
||||
if _, err := io.ReadFull(rand.Reader, ed25519Seed[:]); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate Ed25519 seed: %v", err)
|
||||
}
|
||||
i.signingKey = signingKey
|
||||
i.verificationKey = verificationKey
|
||||
|
||||
// Derive Ed25519 keypair from seed
|
||||
privKeyEd := ed25519.NewKeyFromSeed(ed25519Seed[:])
|
||||
pubKeyEd := privKeyEd.Public().(ed25519.PublicKey)
|
||||
|
||||
i.signingSeed = ed25519Seed[:]
|
||||
i.verificationKey = pubKeyEd
|
||||
|
||||
return i, nil
|
||||
}
|
||||
@@ -96,11 +104,13 @@ func (i *Identity) GetPublicKey() []byte {
|
||||
}
|
||||
|
||||
func (i *Identity) GetPrivateKey() []byte {
|
||||
return append(i.privateKey, i.signingKey...)
|
||||
return append(i.privateKey, i.signingSeed...)
|
||||
}
|
||||
|
||||
func (i *Identity) Sign(data []byte) []byte {
|
||||
return cryptography.Sign(i.signingKey, data)
|
||||
// Derive Ed25519 private key from seed
|
||||
privKey := ed25519.NewKeyFromSeed(i.signingSeed)
|
||||
return cryptography.Sign(privKey, data)
|
||||
}
|
||||
|
||||
func (i *Identity) Verify(data []byte, signature []byte) bool {
|
||||
@@ -126,20 +136,25 @@ func (i *Identity) Encrypt(plaintext []byte, ratchet []byte) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Derive encryption key
|
||||
key, err := cryptography.DeriveKey(sharedSecret, i.GetSalt(), i.GetContext(), 32)
|
||||
// Derive key material (64 bytes: first 32 for HMAC, last 32 for encryption)
|
||||
salt := i.GetSalt()
|
||||
debug.Log(debug.DEBUG_ALL, "Encrypt: using salt", "salt", fmt.Sprintf("%x", salt), "identity_hash", fmt.Sprintf("%x", i.Hash()))
|
||||
key, err := cryptography.DeriveKey(sharedSecret, salt, i.GetContext(), 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hmacKey := key[:32]
|
||||
encryptionKey := key[32:64]
|
||||
|
||||
// Encrypt data
|
||||
ciphertext, err := cryptography.EncryptAESCBC(key[:16], plaintext)
|
||||
ciphertext, err := cryptography.EncryptAES256CBC(encryptionKey, plaintext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Calculate HMAC
|
||||
mac := cryptography.ComputeHMAC(key, append(ephemeralPubKey, ciphertext...))
|
||||
// Calculate HMAC over ciphertext only (iv + encrypted_data)
|
||||
mac := cryptography.ComputeHMAC(hmacKey, ciphertext)
|
||||
|
||||
// Combine components
|
||||
token := make([]byte, 0, len(ephemeralPubKey)+len(ciphertext)+len(mac))
|
||||
@@ -164,7 +179,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 {
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Failed to read random data for hash", "error", err)
|
||||
return nil // Or handle the error appropriately
|
||||
}
|
||||
return TruncatedHash(randomData)
|
||||
}
|
||||
|
||||
@@ -173,12 +192,14 @@ func Remember(packet []byte, destHash []byte, publicKey []byte, appData []byte)
|
||||
|
||||
// Store destination data as [packet, destHash, identity, appData]
|
||||
id := FromPublicKey(publicKey)
|
||||
knownDestinationsLock.Lock()
|
||||
knownDestinations[hashStr] = []interface{}{
|
||||
packet,
|
||||
destHash,
|
||||
id,
|
||||
appData,
|
||||
}
|
||||
knownDestinationsLock.Unlock()
|
||||
}
|
||||
|
||||
func ValidateAnnounce(packet []byte, destHash []byte, publicKey []byte, signature []byte, appData []byte) bool {
|
||||
@@ -210,13 +231,18 @@ func FromPublicKey(publicKey []byte) *Identity {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &Identity{
|
||||
id := &Identity{
|
||||
publicKey: publicKey[:KEYSIZE/16],
|
||||
verificationKey: publicKey[KEYSIZE/16:],
|
||||
ratchets: make(map[string][]byte),
|
||||
ratchetExpiry: make(map[string]int64),
|
||||
mutex: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
hash := cryptography.Hash(id.GetPublicKey())
|
||||
id.hash = hash[:TRUNCATED_HASHLENGTH/8]
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (i *Identity) Hex() string {
|
||||
@@ -228,9 +254,22 @@ func (i *Identity) String() string {
|
||||
}
|
||||
|
||||
func Recall(hash []byte) (*Identity, error) {
|
||||
// TODO: Implement persistence
|
||||
// For now just create new identity
|
||||
return New()
|
||||
hashStr := hex.EncodeToString(hash)
|
||||
|
||||
knownDestinationsLock.RLock()
|
||||
data, exists := knownDestinations[hashStr]
|
||||
knownDestinationsLock.RUnlock()
|
||||
|
||||
if exists {
|
||||
// data is [packet, destHash, identity, appData]
|
||||
if len(data) >= 3 {
|
||||
if id, ok := data[2].(*Identity); ok {
|
||||
return id, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("identity not found for hash %x", hash)
|
||||
}
|
||||
|
||||
func (i *Identity) GenerateHMACKey() []byte {
|
||||
@@ -256,52 +295,66 @@ 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.
|
||||
debug.Log(debug.DEBUG_TRACE, "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 {
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Failed to generate initial ratchet key", "error", 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 {
|
||||
for id, expiry := range i.ratchetExpiry {
|
||||
if expiry > latestTime {
|
||||
latestTime = expiry
|
||||
latestKey = i.ratchets[key]
|
||||
latestKey = i.ratchets[id]
|
||||
}
|
||||
}
|
||||
|
||||
if latestKey == nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Could not determine the latest ratchet key", "ratchet_count", len(i.ratchets))
|
||||
}
|
||||
|
||||
return latestKey
|
||||
}
|
||||
|
||||
func (i *Identity) Decrypt(ciphertextToken []byte, ratchets [][]byte, enforceRatchets bool, ratchetIDReceiver *common.RatchetIDReceiver) ([]byte, error) {
|
||||
if i.privateKey == nil {
|
||||
log.Printf("[DEBUG-1] Decryption failed: identity has no private key")
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Decryption failed: identity has no private key")
|
||||
return nil, errors.New("decryption failed because identity does not hold a private key")
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-7] Starting decryption for identity %s", i.GetHexHash())
|
||||
debug.Log(debug.DEBUG_ALL, "Starting decryption for identity", "hash", i.GetHexHash())
|
||||
if len(ratchets) > 0 {
|
||||
log.Printf("[DEBUG-7] Attempting decryption with %d ratchets", len(ratchets))
|
||||
debug.Log(debug.DEBUG_ALL, "Attempting decryption with ratchets", "count", len(ratchets))
|
||||
}
|
||||
|
||||
if len(ciphertextToken) <= KEYSIZE/8/2 {
|
||||
return nil, errors.New("decryption failed because the token size was invalid")
|
||||
}
|
||||
|
||||
// Extract peer public key and ciphertext
|
||||
peerPubBytes := ciphertextToken[:KEYSIZE/8/2]
|
||||
ciphertext := ciphertextToken[KEYSIZE/8/2:]
|
||||
// Extract components: ephemeralPubKey(32) + ciphertext + mac(32)
|
||||
if len(ciphertextToken) < 32+32+32 { // minimum sizes
|
||||
return nil, errors.New("token too short")
|
||||
}
|
||||
|
||||
peerPubBytes := ciphertextToken[:32]
|
||||
ciphertext := ciphertextToken[32 : len(ciphertextToken)-32]
|
||||
mac := ciphertextToken[len(ciphertextToken)-32:]
|
||||
|
||||
// Try decryption with ratchets first if provided
|
||||
if len(ratchets) > 0 {
|
||||
for _, ratchet := range ratchets {
|
||||
if decrypted, ratchetID, err := i.tryRatchetDecryption(peerPubBytes, ciphertext, ratchet); err == nil {
|
||||
if decrypted, ratchetID, err := i.tryRatchetDecryption(peerPubBytes, ciphertext, mac, ratchet); err == nil {
|
||||
if ratchetIDReceiver != nil {
|
||||
ratchetIDReceiver.LatestRatchetID = ratchetID
|
||||
}
|
||||
@@ -323,15 +376,25 @@ func (i *Identity) Decrypt(ciphertextToken []byte, ratchets [][]byte, enforceRat
|
||||
return nil, fmt.Errorf("failed to generate shared key: %v", err)
|
||||
}
|
||||
|
||||
// Derive key using HKDF
|
||||
hkdfReader := hkdf.New(sha256.New, sharedKey, i.GetSalt(), i.GetContext())
|
||||
derivedKey := make([]byte, 32)
|
||||
// Derive key material (64 bytes: first 32 for HMAC, last 32 for encryption)
|
||||
salt := i.GetSalt()
|
||||
debug.Log(debug.DEBUG_ALL, "Decrypt: using salt", "salt", fmt.Sprintf("%x", salt), "identity_hash", fmt.Sprintf("%x", i.Hash()))
|
||||
hkdfReader := hkdf.New(sha256.New, sharedKey, salt, i.GetContext())
|
||||
derivedKey := make([]byte, 64)
|
||||
if _, err := io.ReadFull(hkdfReader, derivedKey); err != nil {
|
||||
return nil, fmt.Errorf("failed to derive key: %v", err)
|
||||
}
|
||||
|
||||
hmacKey := derivedKey[:32]
|
||||
encryptionKey := derivedKey[32:64]
|
||||
|
||||
// Validate HMAC over ciphertext only (iv + encrypted_data)
|
||||
if !cryptography.ValidateHMAC(hmacKey, ciphertext, mac) {
|
||||
return nil, errors.New("invalid HMAC")
|
||||
}
|
||||
|
||||
// Create AES cipher
|
||||
block, err := aes.NewCipher(derivedKey)
|
||||
block, err := aes.NewCipher(encryptionKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create cipher: %v", err)
|
||||
}
|
||||
@@ -368,34 +431,42 @@ func (i *Identity) Decrypt(ciphertextToken []byte, ratchets [][]byte, enforceRat
|
||||
ratchetIDReceiver.LatestRatchetID = nil
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-7] Decryption completed successfully")
|
||||
debug.Log(debug.DEBUG_ALL, "Decryption completed successfully")
|
||||
return plaintext[:len(plaintext)-padding], nil
|
||||
}
|
||||
|
||||
// Helper function to attempt decryption using a ratchet
|
||||
func (i *Identity) tryRatchetDecryption(peerPubBytes, ciphertext, ratchet []byte) ([]byte, []byte, error) {
|
||||
func (i *Identity) tryRatchetDecryption(peerPubBytes, ciphertext, mac, ratchet []byte) (plaintext, ratchetID []byte, err error) {
|
||||
// Convert ratchet to private key
|
||||
ratchetPriv := ratchet
|
||||
|
||||
// Get ratchet ID
|
||||
ratchetPubBytes, err := curve25519.X25519(ratchetPriv, cryptography.GetBasepoint())
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-7] Failed to generate ratchet public key: %v", err)
|
||||
debug.Log(debug.DEBUG_ALL, "Failed to generate ratchet public key", "error", err)
|
||||
return nil, nil, err
|
||||
}
|
||||
ratchetID := i.GetRatchetID(ratchetPubBytes)
|
||||
ratchetID = i.GetRatchetID(ratchetPubBytes)
|
||||
|
||||
sharedSecret, err := cryptography.DeriveSharedSecret(ratchet, peerPubBytes)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
key, err := cryptography.DeriveKey(sharedSecret, i.GetSalt(), i.GetContext(), 32)
|
||||
key, err := cryptography.DeriveKey(sharedSecret, i.GetSalt(), i.GetContext(), 64)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
plaintext, err := cryptography.DecryptAESCBC(key, ciphertext)
|
||||
hmacKey := key[:32]
|
||||
encryptionKey := key[32:64]
|
||||
|
||||
// Validate HMAC over ciphertext only (iv + encrypted_data)
|
||||
if !cryptography.ValidateHMAC(hmacKey, ciphertext, mac) {
|
||||
return nil, nil, errors.New("invalid HMAC")
|
||||
}
|
||||
|
||||
plaintext, err = cryptography.DecryptAES256CBC(encryptionKey, ciphertext)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -404,12 +475,23 @@ func (i *Identity) tryRatchetDecryption(peerPubBytes, ciphertext, ratchet []byte
|
||||
}
|
||||
|
||||
func (i *Identity) EncryptWithHMAC(plaintext []byte, key []byte) ([]byte, error) {
|
||||
ciphertext, err := cryptography.EncryptAESCBC(key, plaintext)
|
||||
var hmacKey, encryptionKey []byte
|
||||
if len(key) == 64 {
|
||||
hmacKey = key[:32]
|
||||
encryptionKey = key[32:64]
|
||||
} else if len(key) == 32 {
|
||||
hmacKey = key[:16]
|
||||
encryptionKey = key[16:32]
|
||||
} else {
|
||||
return nil, errors.New("invalid key length for EncryptWithHMAC")
|
||||
}
|
||||
|
||||
ciphertext, err := cryptography.EncryptAES256CBC(encryptionKey, plaintext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mac := cryptography.ComputeHMAC(key, ciphertext)
|
||||
mac := cryptography.ComputeHMAC(hmacKey, ciphertext)
|
||||
return append(ciphertext, mac...), nil
|
||||
}
|
||||
|
||||
@@ -418,75 +500,346 @@ func (i *Identity) DecryptWithHMAC(data []byte, key []byte) ([]byte, error) {
|
||||
return nil, errors.New("data too short")
|
||||
}
|
||||
|
||||
var hmacKey, encryptionKey []byte
|
||||
if len(key) == 64 {
|
||||
hmacKey = key[:32]
|
||||
encryptionKey = key[32:64]
|
||||
} else if len(key) == 32 {
|
||||
hmacKey = key[:16]
|
||||
encryptionKey = key[16:32]
|
||||
} else {
|
||||
return nil, errors.New("invalid key length for DecryptWithHMAC")
|
||||
}
|
||||
|
||||
macStart := len(data) - cryptography.SHA256Size
|
||||
ciphertext := data[:macStart]
|
||||
messageMAC := data[macStart:]
|
||||
|
||||
if !cryptography.ValidateHMAC(key, ciphertext, messageMAC) {
|
||||
if !cryptography.ValidateHMAC(hmacKey, ciphertext, messageMAC) {
|
||||
return nil, errors.New("invalid HMAC")
|
||||
}
|
||||
|
||||
return cryptography.DecryptAESCBC(key, ciphertext)
|
||||
return cryptography.DecryptAES256CBC(encryptionKey, ciphertext)
|
||||
}
|
||||
|
||||
func (i *Identity) ToFile(path string) error {
|
||||
log.Printf("[DEBUG-7] Saving identity %s to file: %s", i.GetHexHash(), path)
|
||||
debug.Log(debug.DEBUG_ALL, "Saving identity to file", "hash", i.GetHexHash(), "path", path)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"private_key": i.privateKey,
|
||||
"public_key": i.publicKey,
|
||||
"signing_key": i.signingKey,
|
||||
"verification_key": i.verificationKey,
|
||||
"app_data": i.appData,
|
||||
if i.privateKey == nil || i.signingSeed == nil {
|
||||
return errors.New("cannot save identity without private keys")
|
||||
}
|
||||
|
||||
file, err := os.Create(path)
|
||||
// Store private keys as raw bytes
|
||||
// Format: [X25519 PrivKey (32 bytes)][Ed25519 PrivKey (32 bytes)]
|
||||
// Total: 64 bytes
|
||||
privateKeyBytes := make([]byte, 64)
|
||||
copy(privateKeyBytes[:32], i.privateKey)
|
||||
copy(privateKeyBytes[32:], i.signingSeed)
|
||||
|
||||
// Write raw bytes to file
|
||||
file, err := os.Create(path) // #nosec G304
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-1] Failed to create identity file: %v", err)
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Failed to create identity file", "error", err)
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if err := json.NewEncoder(file).Encode(data); err != nil {
|
||||
log.Printf("[DEBUG-1] Failed to encode identity data: %v", err)
|
||||
if _, err := file.Write(privateKeyBytes); err != nil {
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Failed to write identity data", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-7] Identity saved successfully")
|
||||
debug.Log(debug.DEBUG_ALL, "Identity saved successfully", "bytes", len(privateKeyBytes))
|
||||
return nil
|
||||
}
|
||||
|
||||
func FromFile(path string) (*Identity, error) {
|
||||
debug.Log(debug.DEBUG_ALL, "Loading identity from file", "path", path)
|
||||
|
||||
// Read the private key bytes from file
|
||||
// bearer:disable go_gosec_filesystem_filereadtaint
|
||||
data, err := os.ReadFile(path) // #nosec G304
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read identity file: %w", err)
|
||||
}
|
||||
|
||||
if len(data) != 64 {
|
||||
return nil, fmt.Errorf("invalid identity file: expected 64 bytes, got %d", len(data))
|
||||
}
|
||||
|
||||
// Parse the private keys
|
||||
// Format: [X25519 PrivKey (32 bytes)][Ed25519 PrivKey (32 bytes)]
|
||||
privateKey := data[:32]
|
||||
signingSeed := data[32:64]
|
||||
|
||||
// Create identity with initialized maps and mutex
|
||||
ident := &Identity{
|
||||
ratchets: make(map[string][]byte),
|
||||
ratchetExpiry: make(map[string]int64),
|
||||
mutex: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
if err := ident.loadPrivateKey(privateKey, signingSeed); err != nil {
|
||||
return nil, fmt.Errorf("failed to load private key: %w", err)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "Identity loaded from file", "hash", ident.GetHexHash())
|
||||
return ident, nil
|
||||
}
|
||||
|
||||
func LoadOrCreateTransportIdentity() (*Identity, error) {
|
||||
storagePath := os.Getenv("RETICULUM_STORAGE_PATH")
|
||||
if storagePath == "" {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get home directory: %w", err)
|
||||
}
|
||||
storagePath = fmt.Sprintf("%s/.reticulum/storage", homeDir)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(storagePath, 0700); err != nil {
|
||||
return nil, fmt.Errorf("failed to create storage directory: %w", err)
|
||||
}
|
||||
|
||||
transportIdentityPath := fmt.Sprintf("%s/transport_identity", storagePath)
|
||||
|
||||
if ident, err := FromFile(transportIdentityPath); err == nil {
|
||||
debug.Log(debug.DEBUG_INFO, "Loaded transport identity from storage")
|
||||
return ident, nil
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "No valid transport identity in storage, creating new one")
|
||||
ident, err := New()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create transport identity: %w", err)
|
||||
}
|
||||
|
||||
if err := ident.ToFile(transportIdentityPath); err != nil {
|
||||
return nil, fmt.Errorf("failed to save transport identity: %w", err)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "Created and saved transport identity")
|
||||
return ident, nil
|
||||
}
|
||||
|
||||
func (i *Identity) loadPrivateKey(privateKey, signingSeed []byte) error {
|
||||
if len(privateKey) != 32 || len(signingSeed) != 32 {
|
||||
return errors.New("invalid private key length")
|
||||
}
|
||||
|
||||
// Load X25519 private key
|
||||
i.privateKey = make([]byte, 32)
|
||||
copy(i.privateKey, privateKey)
|
||||
|
||||
// Load Ed25519 signing seed
|
||||
i.signingSeed = make([]byte, 32)
|
||||
copy(i.signingSeed, signingSeed)
|
||||
|
||||
// Derive public keys from private keys
|
||||
var err error
|
||||
i.publicKey, err = curve25519.X25519(i.privateKey, curve25519.Basepoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to derive X25519 public key: %w", err)
|
||||
}
|
||||
|
||||
signingKey := ed25519.NewKeyFromSeed(i.signingSeed)
|
||||
i.verificationKey = signingKey.Public().(ed25519.PublicKey)
|
||||
|
||||
publicKeyBytes := make([]byte, 0, len(i.publicKey)+len(i.verificationKey))
|
||||
publicKeyBytes = append(publicKeyBytes, i.publicKey...)
|
||||
publicKeyBytes = append(publicKeyBytes, i.verificationKey...)
|
||||
i.hash = TruncatedHash(publicKeyBytes)[:TRUNCATED_HASHLENGTH/8]
|
||||
i.hexHash = hex.EncodeToString(i.hash)
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Private key loaded successfully", "hash", i.GetHexHash())
|
||||
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
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_PACKETS, "Saving ratchets", "count", len(i.ratchets), "path", path)
|
||||
|
||||
// Convert ratchets to list format for msgpack
|
||||
ratchetList := make([][]byte, 0, len(i.ratchets))
|
||||
for _, ratchet := range i.ratchets {
|
||||
ratchetList = append(ratchetList, ratchet)
|
||||
}
|
||||
|
||||
// Pack ratchets using msgpack
|
||||
packedRatchets, err := msgpack.Marshal(ratchetList)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to pack ratchets: %w", err)
|
||||
}
|
||||
|
||||
// Sign the packed ratchets
|
||||
signature := i.Sign(packedRatchets)
|
||||
|
||||
// Create structure: {"signature": ..., "ratchets": ...}
|
||||
persistedData := map[string][]byte{
|
||||
"signature": signature,
|
||||
"ratchets": packedRatchets,
|
||||
}
|
||||
|
||||
// Pack the entire structure
|
||||
finalData, err := msgpack.Marshal(persistedData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to pack ratchet data: %w", err)
|
||||
}
|
||||
|
||||
// Write to temporary file first, then rename (atomic operation)
|
||||
tempPath := path + ".tmp"
|
||||
file, err := os.Create(tempPath) // #nosec G304
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp ratchet file: %w", err)
|
||||
}
|
||||
|
||||
if _, err := file.Write(finalData); err != nil {
|
||||
// #nosec G104 - Error already being handled, cleanup errors are non-critical
|
||||
file.Close()
|
||||
// #nosec G104 - Error already being handled, cleanup errors are non-critical
|
||||
os.Remove(tempPath)
|
||||
return fmt.Errorf("failed to write ratchet data: %w", err)
|
||||
}
|
||||
// #nosec G104 - File is being closed after successful write, error is non-critical
|
||||
file.Close()
|
||||
|
||||
// Atomic rename
|
||||
if err := os.Rename(tempPath, path); err != nil {
|
||||
// #nosec G104 - Error already being handled, cleanup errors are non-critical
|
||||
os.Remove(tempPath)
|
||||
return fmt.Errorf("failed to rename ratchet file: %w", err)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_PACKETS, "Ratchets saved successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func RecallIdentity(path string) (*Identity, error) {
|
||||
log.Printf("[DEBUG-7] Attempting to recall identity from: %s", path)
|
||||
debug.Log(debug.DEBUG_ALL, "Attempting to recall identity", "path", path)
|
||||
|
||||
file, err := os.Open(path)
|
||||
// bearer:disable go_gosec_filesystem_filereadtaint
|
||||
file, err := os.Open(path) // #nosec G304
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-1] Failed to open identity file: %v", err)
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Failed to open identity file", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var data map[string]interface{}
|
||||
if err := json.NewDecoder(file).Decode(&data); err != nil {
|
||||
log.Printf("[DEBUG-1] Failed to decode identity data: %v", err)
|
||||
// Read raw bytes
|
||||
// Format: [X25519 PrivKey (32 bytes)][Ed25519 PrivKey (32 bytes)]
|
||||
privateKeyBytes := make([]byte, 64)
|
||||
n, err := io.ReadFull(file, privateKeyBytes)
|
||||
if err != nil {
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Failed to read identity data", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
if n != 64 {
|
||||
return nil, fmt.Errorf("invalid identity file: expected 64 bytes, got %d", n)
|
||||
}
|
||||
|
||||
// Extract keys
|
||||
x25519PrivKey := privateKeyBytes[:32]
|
||||
ed25519Seed := privateKeyBytes[32:]
|
||||
|
||||
// Derive public keys
|
||||
x25519PubKey, err := curve25519.X25519(x25519PrivKey, curve25519.Basepoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to derive X25519 public key: %v", err)
|
||||
}
|
||||
|
||||
ed25519PrivKey := ed25519.NewKeyFromSeed(ed25519Seed)
|
||||
ed25519PubKey := ed25519PrivKey.Public().(ed25519.PublicKey)
|
||||
|
||||
id := &Identity{
|
||||
privateKey: data["private_key"].([]byte),
|
||||
publicKey: data["public_key"].([]byte),
|
||||
signingKey: data["signing_key"].(ed25519.PrivateKey),
|
||||
verificationKey: data["verification_key"].(ed25519.PublicKey),
|
||||
appData: data["app_data"].([]byte),
|
||||
privateKey: x25519PrivKey,
|
||||
publicKey: x25519PubKey,
|
||||
signingSeed: ed25519Seed,
|
||||
verificationKey: ed25519PubKey,
|
||||
ratchets: make(map[string][]byte),
|
||||
ratchetExpiry: make(map[string]int64),
|
||||
mutex: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-7] Successfully recalled identity with hash: %s", id.GetHexHash())
|
||||
// Generate hash
|
||||
combinedPub := make([]byte, KEYSIZE/8)
|
||||
copy(combinedPub[:KEYSIZE/16], id.publicKey)
|
||||
copy(combinedPub[KEYSIZE/16:], id.verificationKey)
|
||||
hash := sha256.Sum256(combinedPub)
|
||||
id.hash = hash[:TRUNCATED_HASHLENGTH/8]
|
||||
|
||||
debug.Log(debug.DEBUG_ALL, "Successfully recalled identity", "hash", id.GetHexHash())
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (i *Identity) loadRatchets(path string) error {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
|
||||
// bearer:disable go_gosec_filesystem_filereadtaint
|
||||
file, err := os.Open(path) // #nosec G304
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
debug.Log(debug.DEBUG_PACKETS, "No ratchet file found, skipping", "path", path)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to open ratchet file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Read all data
|
||||
fileData, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read ratchet file: %w", err)
|
||||
}
|
||||
|
||||
// Unpack outer structure: {"signature": ..., "ratchets": ...}
|
||||
var persistedData map[string][]byte
|
||||
if err := msgpack.Unmarshal(fileData, &persistedData); err != nil {
|
||||
return fmt.Errorf("failed to unpack ratchet data: %w", err)
|
||||
}
|
||||
|
||||
signature, hasSignature := persistedData["signature"]
|
||||
packedRatchets, hasRatchets := persistedData["ratchets"]
|
||||
|
||||
if !hasSignature || !hasRatchets {
|
||||
return fmt.Errorf("invalid ratchet file format: missing signature or ratchets")
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
if !i.Verify(packedRatchets, signature) {
|
||||
return fmt.Errorf("invalid ratchet file signature")
|
||||
}
|
||||
|
||||
// Unpack ratchet list
|
||||
var ratchetList [][]byte
|
||||
if err := msgpack.Unmarshal(packedRatchets, &ratchetList); err != nil {
|
||||
return fmt.Errorf("failed to unpack ratchet list: %w", err)
|
||||
}
|
||||
|
||||
// Store ratchets with generated IDs
|
||||
now := time.Now().Unix()
|
||||
for _, ratchet := range ratchetList {
|
||||
// Generate ratchet public key to create ID
|
||||
ratchetPub, err := curve25519.X25519(ratchet, curve25519.Basepoint)
|
||||
if err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to generate ratchet public key", "error", err)
|
||||
continue
|
||||
}
|
||||
ratchetID := i.GetRatchetID(ratchetPub)
|
||||
i.ratchets[string(ratchetID)] = ratchet
|
||||
i.ratchetExpiry[string(ratchetID)] = now + RATCHET_EXPIRY
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_PACKETS, "Loaded ratchets", "count", len(i.ratchets), "path", 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))
|
||||
@@ -509,7 +862,10 @@ func (i *Identity) GetRatchetID(ratchetPubBytes []byte) []byte {
|
||||
}
|
||||
|
||||
func GetKnownDestination(hash string) ([]interface{}, bool) {
|
||||
if data, exists := knownDestinations[hash]; exists {
|
||||
knownDestinationsLock.RLock()
|
||||
data, exists := knownDestinations[hash]
|
||||
knownDestinationsLock.RUnlock()
|
||||
if exists {
|
||||
return data, true
|
||||
}
|
||||
return nil, false
|
||||
@@ -539,12 +895,16 @@ func (i *Identity) SetRatchetKey(id string, key []byte) {
|
||||
|
||||
// NewIdentity creates a new Identity instance with fresh keys
|
||||
func NewIdentity() (*Identity, error) {
|
||||
// Generate Ed25519 signing keypair
|
||||
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate Ed25519 keypair: %v", err)
|
||||
// Generate 32-byte Ed25519 seed
|
||||
var ed25519Seed [32]byte
|
||||
if _, err := io.ReadFull(rand.Reader, ed25519Seed[:]); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate Ed25519 seed: %v", err)
|
||||
}
|
||||
|
||||
// Derive Ed25519 keypair from seed
|
||||
privKey := ed25519.NewKeyFromSeed(ed25519Seed[:])
|
||||
pubKey := privKey.Public().(ed25519.PublicKey)
|
||||
|
||||
// Generate X25519 encryption keypair
|
||||
var encPrivKey [32]byte
|
||||
if _, err := io.ReadFull(rand.Reader, encPrivKey[:]); err != nil {
|
||||
@@ -559,7 +919,7 @@ func NewIdentity() (*Identity, error) {
|
||||
i := &Identity{
|
||||
privateKey: encPrivKey[:],
|
||||
publicKey: encPubKey,
|
||||
signingKey: privKey,
|
||||
signingSeed: ed25519Seed[:],
|
||||
verificationKey: pubKey,
|
||||
ratchets: make(map[string][]byte),
|
||||
ratchetExpiry: make(map[string]int64),
|
||||
@@ -571,28 +931,50 @@ func NewIdentity() (*Identity, error) {
|
||||
copy(combinedPub[:KEYSIZE/16], i.publicKey)
|
||||
copy(combinedPub[KEYSIZE/16:], i.verificationKey)
|
||||
hash := sha256.Sum256(combinedPub)
|
||||
i.hash = hash[:]
|
||||
i.hash = hash[:TRUNCATED_HASHLENGTH/8]
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// FromBytes creates an Identity from a 64-byte private key representation
|
||||
func FromBytes(data []byte) (*Identity, error) {
|
||||
if len(data) != 64 {
|
||||
return nil, fmt.Errorf("invalid identity data: expected 64 bytes, got %d", len(data))
|
||||
}
|
||||
|
||||
privateKey := data[:32]
|
||||
signingSeed := data[32:64]
|
||||
|
||||
ident := &Identity{
|
||||
ratchets: make(map[string][]byte),
|
||||
ratchetExpiry: make(map[string]int64),
|
||||
mutex: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
if err := ident.loadPrivateKey(privateKey, signingSeed); err != nil {
|
||||
return nil, fmt.Errorf("failed to load private key: %w", err)
|
||||
}
|
||||
|
||||
return ident, nil
|
||||
}
|
||||
|
||||
func (i *Identity) RotateRatchet() ([]byte, error) {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
|
||||
log.Printf("[DEBUG-7] Rotating ratchet for identity %s", i.GetHexHash())
|
||||
debug.Log(debug.DEBUG_ALL, "Rotating ratchet for identity", "hash", i.GetHexHash())
|
||||
|
||||
// Generate new ratchet key
|
||||
newRatchet := make([]byte, RATCHETSIZE/8)
|
||||
if _, err := io.ReadFull(rand.Reader, newRatchet); err != nil {
|
||||
log.Printf("[DEBUG-1] Failed to generate new ratchet: %v", err)
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Failed to generate new ratchet", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get public key for ratchet ID
|
||||
ratchetPub, err := curve25519.X25519(newRatchet, curve25519.Basepoint)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-1] Failed to generate ratchet public key: %v", err)
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Failed to generate ratchet public key", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -603,7 +985,7 @@ func (i *Identity) RotateRatchet() ([]byte, error) {
|
||||
i.ratchets[string(ratchetID)] = newRatchet
|
||||
i.ratchetExpiry[string(ratchetID)] = expiry
|
||||
|
||||
log.Printf("[DEBUG-7] New ratchet generated with ID: %x, expiry: %d", ratchetID, expiry)
|
||||
debug.Log(debug.DEBUG_ALL, "New ratchet generated", "id", fmt.Sprintf("%x", ratchetID), "expiry", expiry)
|
||||
|
||||
// Cleanup old ratchets if we exceed max retained
|
||||
if len(i.ratchets) > MAX_RETAINED_RATCHETS {
|
||||
@@ -619,10 +1001,10 @@ func (i *Identity) RotateRatchet() ([]byte, error) {
|
||||
|
||||
delete(i.ratchets, oldestID)
|
||||
delete(i.ratchetExpiry, oldestID)
|
||||
log.Printf("[DEBUG-7] Cleaned up oldest ratchet with ID: %x", []byte(oldestID))
|
||||
debug.Log(debug.DEBUG_ALL, "Cleaned up oldest ratchet", "id", fmt.Sprintf("%x", []byte(oldestID)))
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-7] Current number of active ratchets: %d", len(i.ratchets))
|
||||
debug.Log(debug.DEBUG_ALL, "Current number of active ratchets", "count", len(i.ratchets))
|
||||
return newRatchet, nil
|
||||
}
|
||||
|
||||
@@ -630,7 +1012,7 @@ func (i *Identity) GetRatchets() [][]byte {
|
||||
i.mutex.RLock()
|
||||
defer i.mutex.RUnlock()
|
||||
|
||||
log.Printf("[DEBUG-7] Getting ratchets for identity %s", i.GetHexHash())
|
||||
debug.Log(debug.DEBUG_ALL, "Getting ratchets for identity", "hash", i.GetHexHash())
|
||||
|
||||
ratchets := make([][]byte, 0, len(i.ratchets))
|
||||
now := time.Now().Unix()
|
||||
@@ -648,7 +1030,7 @@ func (i *Identity) GetRatchets() [][]byte {
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-7] Retrieved %d active ratchets, cleaned up %d expired", len(ratchets), expired)
|
||||
debug.Log(debug.DEBUG_ALL, "Retrieved active ratchets", "active", len(ratchets), "expired", expired)
|
||||
return ratchets
|
||||
}
|
||||
|
||||
@@ -656,7 +1038,7 @@ func (i *Identity) CleanupExpiredRatchets() {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
|
||||
log.Printf("[DEBUG-7] Starting ratchet cleanup for identity %s", i.GetHexHash())
|
||||
debug.Log(debug.DEBUG_ALL, "Starting ratchet cleanup for identity", "hash", i.GetHexHash())
|
||||
|
||||
now := time.Now().Unix()
|
||||
cleaned := 0
|
||||
@@ -668,7 +1050,7 @@ func (i *Identity) CleanupExpiredRatchets() {
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-7] Cleaned up %d expired ratchets, %d remaining", cleaned, len(i.ratchets))
|
||||
debug.Log(debug.DEBUG_ALL, "Cleaned up expired ratchets", "cleaned", cleaned, "remaining", len(i.ratchets))
|
||||
}
|
||||
|
||||
// ValidateAnnounce validates an announce packet's signature
|
||||
|
||||
148
pkg/identity/identity_test.go
Normal file
148
pkg/identity/identity_test.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewIdentity(t *testing.T) {
|
||||
id, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("New() failed: %v", err)
|
||||
}
|
||||
if id == nil {
|
||||
t.Fatal("New() returned nil")
|
||||
}
|
||||
|
||||
pubKey := id.GetPublicKey()
|
||||
if len(pubKey) != 64 {
|
||||
t.Errorf("Expected public key length 64, got %d", len(pubKey))
|
||||
}
|
||||
|
||||
privKey := id.GetPrivateKey()
|
||||
if len(privKey) != 64 {
|
||||
t.Errorf("Expected private key length 64, got %d", len(privKey))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignVerify(t *testing.T) {
|
||||
id, _ := New()
|
||||
data := []byte("test data")
|
||||
sig := id.Sign(data)
|
||||
|
||||
if !id.Verify(data, sig) {
|
||||
t.Error("Verification failed for valid signature")
|
||||
}
|
||||
|
||||
if id.Verify([]byte("wrong data"), sig) {
|
||||
t.Error("Verification succeeded for wrong data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDecrypt(t *testing.T) {
|
||||
id, _ := New()
|
||||
plaintext := []byte("secret message")
|
||||
|
||||
ciphertext, err := id.Encrypt(plaintext, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Encrypt failed: %v", err)
|
||||
}
|
||||
|
||||
decrypted, err := id.Decrypt(ciphertext, nil, false, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Decrypt failed: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(plaintext, decrypted) {
|
||||
t.Errorf("Decrypted data doesn't match plaintext: %q vs %q", decrypted, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityHash(t *testing.T) {
|
||||
id, _ := New()
|
||||
h := id.Hash()
|
||||
if len(h) != TRUNCATED_HASHLENGTH/8 {
|
||||
t.Errorf("Expected hash length %d, got %d", TRUNCATED_HASHLENGTH/8, len(h))
|
||||
}
|
||||
|
||||
hexHash := id.Hex()
|
||||
if len(hexHash) != TRUNCATED_HASHLENGTH/4 {
|
||||
t.Errorf("Expected hex hash length %d, got %d", TRUNCATED_HASHLENGTH/4, len(hexHash))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileOperations(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
idPath := filepath.Join(tmpDir, "identity")
|
||||
|
||||
id, _ := New()
|
||||
err := id.ToFile(idPath)
|
||||
if err != nil {
|
||||
t.Fatalf("ToFile failed: %v", err)
|
||||
}
|
||||
|
||||
loadedID, err := FromFile(idPath)
|
||||
if err != nil {
|
||||
t.Fatalf("FromFile failed: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(id.GetPublicKey(), loadedID.GetPublicKey()) {
|
||||
t.Error("Loaded identity public key doesn't match original")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRatchets(t *testing.T) {
|
||||
id, _ := New()
|
||||
|
||||
ratchet, err := id.RotateRatchet()
|
||||
if err != nil {
|
||||
t.Fatalf("RotateRatchet failed: %v", err)
|
||||
}
|
||||
if len(ratchet) != RATCHETSIZE/8 {
|
||||
t.Errorf("Expected ratchet size %d, got %d", RATCHETSIZE/8, len(ratchet))
|
||||
}
|
||||
|
||||
ratchets := id.GetRatchets()
|
||||
if len(ratchets) != 1 {
|
||||
t.Errorf("Expected 1 ratchet, got %d", len(ratchets))
|
||||
}
|
||||
|
||||
id.CleanupExpiredRatchets()
|
||||
// Should still be there since it's not expired
|
||||
if len(id.GetRatchets()) != 1 {
|
||||
t.Error("Ratchet unexpectedly cleaned up")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecallIdentity(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
idPath := filepath.Join(tmpDir, "identity_recall")
|
||||
|
||||
id, _ := New()
|
||||
_ = id.ToFile(idPath)
|
||||
|
||||
recalledID, err := RecallIdentity(idPath)
|
||||
if err != nil {
|
||||
t.Fatalf("RecallIdentity failed: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(id.GetPublicKey(), recalledID.GetPublicKey()) {
|
||||
t.Error("Recalled identity public key doesn't match original")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncatedHash(t *testing.T) {
|
||||
data := []byte("some data")
|
||||
h := TruncatedHash(data)
|
||||
if len(h) != TRUNCATED_HASHLENGTH/8 {
|
||||
t.Errorf("Expected length %d, got %d", TRUNCATED_HASHLENGTH/8, len(h))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRandomHash(t *testing.T) {
|
||||
h := GetRandomHash()
|
||||
if len(h) != TRUNCATED_HASHLENGTH/8 {
|
||||
t.Errorf("Expected length %d, got %d", TRUNCATED_HASHLENGTH/8, len(h))
|
||||
}
|
||||
}
|
||||
@@ -1,96 +1,213 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
const (
|
||||
HW_MTU = 1196
|
||||
DEFAULT_DISCOVERY_PORT = 29716
|
||||
DEFAULT_DATA_PORT = 42671
|
||||
DEFAULT_GROUP_ID = "reticulum"
|
||||
BITRATE_GUESS = 10 * 1000 * 1000
|
||||
PEERING_TIMEOUT = 7500 * time.Millisecond
|
||||
SCOPE_LINK = "2"
|
||||
SCOPE_ADMIN = "4"
|
||||
SCOPE_SITE = "5"
|
||||
SCOPE_ORGANISATION = "8"
|
||||
SCOPE_GLOBAL = "e"
|
||||
PEERING_TIMEOUT = 22 * time.Second
|
||||
ANNOUNCE_INTERVAL = 1600 * time.Millisecond
|
||||
PEER_JOB_INTERVAL = 4 * time.Second
|
||||
MCAST_ECHO_TIMEOUT = 6500 * time.Millisecond
|
||||
|
||||
SCOPE_LINK = "2"
|
||||
SCOPE_ADMIN = "4"
|
||||
SCOPE_SITE = "5"
|
||||
SCOPE_ORGANISATION = "8"
|
||||
SCOPE_GLOBAL = "e"
|
||||
|
||||
MCAST_ADDR_TYPE_PERMANENT = "0"
|
||||
MCAST_ADDR_TYPE_TEMPORARY = "1"
|
||||
)
|
||||
|
||||
type AutoInterface struct {
|
||||
BaseInterface
|
||||
groupID []byte
|
||||
discoveryPort int
|
||||
dataPort int
|
||||
discoveryScope string
|
||||
peers map[string]*Peer
|
||||
linkLocalAddrs []string
|
||||
adoptedInterfaces map[string]string
|
||||
interfaceServers map[string]*net.UDPConn
|
||||
multicastEchoes map[string]time.Time
|
||||
mutex sync.RWMutex
|
||||
outboundConn *net.UDPConn
|
||||
groupID []byte
|
||||
groupHash []byte
|
||||
discoveryPort int
|
||||
dataPort int
|
||||
discoveryScope string
|
||||
multicastAddrType string
|
||||
mcastDiscoveryAddr string
|
||||
ifacNetname string
|
||||
peers map[string]*Peer
|
||||
linkLocalAddrs []string
|
||||
adoptedInterfaces map[string]*AdoptedInterface
|
||||
interfaceServers map[string]*net.UDPConn
|
||||
discoveryServers map[string]*net.UDPConn
|
||||
multicastEchoes map[string]time.Time
|
||||
timedOutInterfaces map[string]time.Time
|
||||
allowedInterfaces []string
|
||||
ignoredInterfaces []string
|
||||
outboundConn *net.UDPConn
|
||||
announceInterval time.Duration
|
||||
peerJobInterval time.Duration
|
||||
peeringTimeout time.Duration
|
||||
mcastEchoTimeout time.Duration
|
||||
done chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
type AdoptedInterface struct {
|
||||
name string
|
||||
linkLocalAddr string
|
||||
index int
|
||||
}
|
||||
|
||||
type Peer struct {
|
||||
ifaceName string
|
||||
lastHeard time.Time
|
||||
conn *net.UDPConn
|
||||
addr *net.UDPAddr
|
||||
}
|
||||
|
||||
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,
|
||||
groupID := DEFAULT_GROUP_ID
|
||||
if config.GroupID != "" {
|
||||
groupID = config.GroupID
|
||||
}
|
||||
|
||||
discoveryScope := SCOPE_LINK
|
||||
if config.DiscoveryScope != "" {
|
||||
discoveryScope = normalizeScope(config.DiscoveryScope)
|
||||
}
|
||||
|
||||
multicastAddrType := MCAST_ADDR_TYPE_TEMPORARY
|
||||
|
||||
discoveryPort := DEFAULT_DISCOVERY_PORT
|
||||
if config.DiscoveryPort != 0 {
|
||||
discoveryPort = config.DiscoveryPort
|
||||
}
|
||||
|
||||
dataPort := DEFAULT_DATA_PORT
|
||||
if config.DataPort != 0 {
|
||||
dataPort = config.DataPort
|
||||
}
|
||||
|
||||
groupHash := sha256.Sum256([]byte(groupID))
|
||||
|
||||
ifacNetname := hex.EncodeToString(groupHash[:])[:16]
|
||||
mcastAddr := fmt.Sprintf("ff%s%s::%s", discoveryScope, multicastAddrType, ifacNetname)
|
||||
|
||||
ai := &AutoInterface{
|
||||
BaseInterface: *base,
|
||||
discoveryPort: DEFAULT_DISCOVERY_PORT,
|
||||
dataPort: DEFAULT_DATA_PORT,
|
||||
discoveryScope: SCOPE_LINK,
|
||||
peers: make(map[string]*Peer),
|
||||
linkLocalAddrs: make([]string, 0),
|
||||
adoptedInterfaces: make(map[string]string),
|
||||
interfaceServers: make(map[string]*net.UDPConn),
|
||||
multicastEchoes: make(map[string]time.Time),
|
||||
}
|
||||
|
||||
if config.Port != 0 {
|
||||
ai.discoveryPort = config.Port
|
||||
}
|
||||
|
||||
if config.GroupID != "" {
|
||||
ai.groupID = []byte(config.GroupID)
|
||||
} else {
|
||||
ai.groupID = []byte("reticulum")
|
||||
BaseInterface: BaseInterface{
|
||||
Name: name,
|
||||
Mode: common.IF_MODE_FULL,
|
||||
Type: common.IF_TYPE_AUTO,
|
||||
Online: false,
|
||||
Enabled: config.Enabled,
|
||||
Detached: false,
|
||||
IN: true,
|
||||
OUT: false,
|
||||
MTU: HW_MTU,
|
||||
Bitrate: BITRATE_GUESS,
|
||||
},
|
||||
groupID: []byte(groupID),
|
||||
groupHash: groupHash[:],
|
||||
discoveryPort: discoveryPort,
|
||||
dataPort: dataPort,
|
||||
discoveryScope: discoveryScope,
|
||||
multicastAddrType: multicastAddrType,
|
||||
mcastDiscoveryAddr: mcastAddr,
|
||||
ifacNetname: ifacNetname,
|
||||
peers: make(map[string]*Peer),
|
||||
linkLocalAddrs: make([]string, 0),
|
||||
adoptedInterfaces: make(map[string]*AdoptedInterface),
|
||||
interfaceServers: make(map[string]*net.UDPConn),
|
||||
discoveryServers: make(map[string]*net.UDPConn),
|
||||
multicastEchoes: make(map[string]time.Time),
|
||||
timedOutInterfaces: make(map[string]time.Time),
|
||||
allowedInterfaces: make([]string, 0),
|
||||
ignoredInterfaces: make([]string, 0),
|
||||
announceInterval: ANNOUNCE_INTERVAL,
|
||||
peerJobInterval: PEER_JOB_INTERVAL,
|
||||
peeringTimeout: PEERING_TIMEOUT,
|
||||
mcastEchoTimeout: MCAST_ECHO_TIMEOUT,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "AutoInterface configured", "name", name, "group", groupID, "mcast_addr", mcastAddr)
|
||||
return ai, nil
|
||||
}
|
||||
|
||||
func normalizeScope(scope string) string {
|
||||
switch scope {
|
||||
case "link", "2":
|
||||
return SCOPE_LINK
|
||||
case "admin", "4":
|
||||
return SCOPE_ADMIN
|
||||
case "site", "5":
|
||||
return SCOPE_SITE
|
||||
case "organisation", "organization", "8":
|
||||
return SCOPE_ORGANISATION
|
||||
case "global", "e":
|
||||
return SCOPE_GLOBAL
|
||||
default:
|
||||
return SCOPE_LINK
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeMulticastType(mtype string) string {
|
||||
switch mtype {
|
||||
case "permanent", "0":
|
||||
return MCAST_ADDR_TYPE_PERMANENT
|
||||
case "temporary", "1":
|
||||
return MCAST_ADDR_TYPE_TEMPORARY
|
||||
default:
|
||||
return MCAST_ADDR_TYPE_TEMPORARY
|
||||
}
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) Start() error {
|
||||
ai.Mutex.Lock()
|
||||
// Only recreate done if it's nil or was closed
|
||||
select {
|
||||
case <-ai.done:
|
||||
ai.done = make(chan struct{})
|
||||
ai.stopOnce = sync.Once{}
|
||||
default:
|
||||
if ai.done == nil {
|
||||
ai.done = make(chan struct{})
|
||||
ai.stopOnce = sync.Once{}
|
||||
}
|
||||
}
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list interfaces: %v", err)
|
||||
}
|
||||
|
||||
for _, iface := range interfaces {
|
||||
if err := ai.configureInterface(&iface); err != nil {
|
||||
log.Printf("Failed to configure interface %s: %v", iface.Name, err)
|
||||
if ai.shouldIgnoreInterface(iface.Name) {
|
||||
debug.Log(debug.DEBUG_TRACE, "Ignoring interface", "name", iface.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(ai.allowedInterfaces) > 0 && !ai.isAllowedInterface(iface.Name) {
|
||||
debug.Log(debug.DEBUG_TRACE, "Interface not in allowed list", "name", iface.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
ifaceCopy := iface
|
||||
// bearer:disable go_gosec_memory_memory_aliasing
|
||||
if err := ai.configureInterface(&ifaceCopy); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to configure interface", "name", iface.Name, "error", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -99,39 +216,97 @@ func (ai *AutoInterface) Start() error {
|
||||
return fmt.Errorf("no suitable interfaces found")
|
||||
}
|
||||
|
||||
ai.Online = true
|
||||
ai.IN = true
|
||||
ai.OUT = true
|
||||
|
||||
go ai.peerJobs()
|
||||
go ai.announceLoop()
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "AutoInterface started", "adopted", len(ai.adoptedInterfaces))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) shouldIgnoreInterface(name string) bool {
|
||||
ignoreList := []string{"lo", "lo0", "tun0", "awdl0", "llw0", "en5", "dummy0"}
|
||||
|
||||
for _, ignored := range ai.ignoredInterfaces {
|
||||
if name == ignored {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, ignored := range ignoreList {
|
||||
if name == ignored {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) isAllowedInterface(name string) bool {
|
||||
for _, allowed := range ai.allowedInterfaces {
|
||||
if name == allowed {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) configureInterface(iface *net.Interface) error {
|
||||
if iface.Flags&net.FlagUp == 0 {
|
||||
return fmt.Errorf("interface is down")
|
||||
}
|
||||
|
||||
if iface.Flags&net.FlagLoopback != 0 {
|
||||
return fmt.Errorf("loopback interface")
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var linkLocalAddr string
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.IsLinkLocalUnicast() {
|
||||
ai.adoptedInterfaces[iface.Name] = ipnet.IP.String()
|
||||
ai.multicastEchoes[iface.Name] = time.Now()
|
||||
|
||||
if err := ai.startDiscoveryListener(iface); err != nil {
|
||||
return err
|
||||
if ipnet, ok := addr.(*net.IPNet); ok {
|
||||
if ipnet.IP.To4() == nil && ipnet.IP.IsLinkLocalUnicast() {
|
||||
linkLocalAddr = ipnet.IP.String()
|
||||
break
|
||||
}
|
||||
|
||||
if err := ai.startDataListener(iface); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if linkLocalAddr == "" {
|
||||
return fmt.Errorf("no link-local IPv6 address found")
|
||||
}
|
||||
|
||||
ai.Mutex.Lock()
|
||||
ai.adoptedInterfaces[iface.Name] = &AdoptedInterface{
|
||||
name: iface.Name,
|
||||
linkLocalAddr: linkLocalAddr,
|
||||
index: iface.Index,
|
||||
}
|
||||
ai.linkLocalAddrs = append(ai.linkLocalAddrs, linkLocalAddr)
|
||||
ai.multicastEchoes[iface.Name] = time.Now()
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
if err := ai.startDiscoveryListener(iface); err != nil {
|
||||
return fmt.Errorf("failed to start discovery listener: %v", err)
|
||||
}
|
||||
|
||||
if err := ai.startDataListener(iface); err != nil {
|
||||
return fmt.Errorf("failed to start data listener: %v", err)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "Configured interface", "name", iface.Name, "addr", linkLocalAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) startDiscoveryListener(iface *net.Interface) error {
|
||||
addr := &net.UDPAddr{
|
||||
IP: net.ParseIP(fmt.Sprintf("ff%s%s::1", ai.discoveryScope, SCOPE_LINK)),
|
||||
IP: net.ParseIP(ai.mcastDiscoveryAddr),
|
||||
Port: ai.discoveryPort,
|
||||
Zone: iface.Name,
|
||||
}
|
||||
@@ -141,47 +316,99 @@ func (ai *AutoInterface) startDiscoveryListener(iface *net.Interface) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := conn.SetReadBuffer(common.NUM_1024); err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to set discovery read buffer", "error", err)
|
||||
}
|
||||
|
||||
ai.Mutex.Lock()
|
||||
ai.discoveryServers[iface.Name] = conn
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
go ai.handleDiscovery(conn, iface.Name)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Discovery listener started", "interface", iface.Name, "addr", ai.mcastDiscoveryAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) startDataListener(iface *net.Interface) error {
|
||||
adoptedIface, exists := ai.adoptedInterfaces[iface.Name]
|
||||
if !exists {
|
||||
return fmt.Errorf("interface not adopted")
|
||||
}
|
||||
|
||||
addr := &net.UDPAddr{
|
||||
IP: net.IPv6zero,
|
||||
IP: net.ParseIP(adoptedIface.linkLocalAddr),
|
||||
Port: ai.dataPort,
|
||||
Zone: iface.Name,
|
||||
}
|
||||
|
||||
conn, err := net.ListenUDP("udp6", addr)
|
||||
if err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to listen on data port", "addr", addr, "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := conn.SetReadBuffer(ai.MTU); err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to set data read buffer", "error", err)
|
||||
}
|
||||
|
||||
ai.Mutex.Lock()
|
||||
ai.interfaceServers[iface.Name] = conn
|
||||
go ai.handleData(conn)
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
go ai.handleData(conn, iface.Name)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Data listener started", "interface", iface.Name, "addr", addr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) {
|
||||
buf := make([]byte, 1024)
|
||||
buf := make([]byte, common.NUM_1024)
|
||||
for {
|
||||
n, remoteAddr, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
log.Printf("Discovery read error: %v", err)
|
||||
continue
|
||||
ai.Mutex.RLock()
|
||||
done := ai.done
|
||||
ai.Mutex.RUnlock()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
ai.handlePeerAnnounce(remoteAddr, buf[:n], ifaceName)
|
||||
n, remoteAddr, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
if ai.IsOnline() {
|
||||
debug.Log(debug.DEBUG_ERROR, "Discovery read error", "interface", ifaceName, "error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if n >= len(ai.groupHash) {
|
||||
receivedHash := buf[:len(ai.groupHash)]
|
||||
if bytes.Equal(receivedHash, ai.groupHash) {
|
||||
ai.handlePeerAnnounce(remoteAddr, ifaceName)
|
||||
} else {
|
||||
debug.Log(debug.DEBUG_TRACE, "Received discovery with mismatched group hash", "interface", ifaceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) handleData(conn *net.UDPConn) {
|
||||
func (ai *AutoInterface) handleData(conn *net.UDPConn, ifaceName string) {
|
||||
buf := make([]byte, ai.GetMTU())
|
||||
for {
|
||||
ai.Mutex.RLock()
|
||||
done := ai.done
|
||||
ai.Mutex.RUnlock()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
n, _, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
if !ai.IsDetached() {
|
||||
log.Printf("Data read error: %v", err)
|
||||
if ai.IsOnline() {
|
||||
debug.Log(debug.DEBUG_ERROR, "Data read error", "interface", ifaceName, "error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -192,86 +419,187 @@ func (ai *AutoInterface) handleData(conn *net.UDPConn) {
|
||||
}
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) handlePeerAnnounce(addr *net.UDPAddr, data []byte, ifaceName string) {
|
||||
ai.mutex.Lock()
|
||||
defer ai.mutex.Unlock()
|
||||
func (ai *AutoInterface) handlePeerAnnounce(addr *net.UDPAddr, ifaceName string) {
|
||||
ai.Mutex.Lock()
|
||||
defer ai.Mutex.Unlock()
|
||||
|
||||
peerAddr := addr.IP.String()
|
||||
peerIP := addr.IP.String()
|
||||
|
||||
for _, localAddr := range ai.linkLocalAddrs {
|
||||
if peerAddr == localAddr {
|
||||
if peerIP == localAddr {
|
||||
ai.multicastEchoes[ifaceName] = time.Now()
|
||||
debug.Log(debug.DEBUG_TRACE, "Received own multicast echo", "interface", ifaceName)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, exists := ai.peers[peerAddr]; !exists {
|
||||
ai.peers[peerAddr] = &Peer{
|
||||
peerKey := peerIP + "%" + ifaceName
|
||||
|
||||
if peer, exists := ai.peers[peerKey]; exists {
|
||||
peer.lastHeard = time.Now()
|
||||
debug.Log(debug.DEBUG_TRACE, "Updated peer", "peer", peerIP, "interface", ifaceName)
|
||||
} else {
|
||||
ai.peers[peerKey] = &Peer{
|
||||
ifaceName: ifaceName,
|
||||
lastHeard: time.Now(),
|
||||
addr: addr,
|
||||
}
|
||||
log.Printf("Added peer %s on %s", peerAddr, ifaceName)
|
||||
} else {
|
||||
ai.peers[peerAddr].lastHeard = time.Now()
|
||||
debug.Log(debug.DEBUG_INFO, "Discovered new peer", "peer", peerIP, "interface", ifaceName)
|
||||
}
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) peerJobs() {
|
||||
ticker := time.NewTicker(PEERING_TIMEOUT)
|
||||
for range ticker.C {
|
||||
ai.mutex.Lock()
|
||||
now := time.Now()
|
||||
func (ai *AutoInterface) announceLoop() {
|
||||
ticker := time.NewTicker(ai.announceInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for addr, peer := range ai.peers {
|
||||
if now.Sub(peer.lastHeard) > PEERING_TIMEOUT {
|
||||
delete(ai.peers, addr)
|
||||
log.Printf("Removed timed out peer %s", addr)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if !ai.IsOnline() {
|
||||
return
|
||||
}
|
||||
ai.sendPeerAnnounce()
|
||||
case <-ai.done:
|
||||
return
|
||||
}
|
||||
|
||||
ai.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) Send(data []byte, address string) error {
|
||||
ai.mutex.RLock()
|
||||
defer ai.mutex.RUnlock()
|
||||
func (ai *AutoInterface) sendPeerAnnounce() {
|
||||
ai.Mutex.RLock()
|
||||
defer ai.Mutex.RUnlock()
|
||||
|
||||
for _, peer := range ai.peers {
|
||||
addr := &net.UDPAddr{
|
||||
IP: net.ParseIP(address),
|
||||
Port: ai.dataPort,
|
||||
Zone: peer.ifaceName,
|
||||
for ifaceName, adoptedIface := range ai.adoptedInterfaces {
|
||||
mcastAddr := &net.UDPAddr{
|
||||
IP: net.ParseIP(ai.mcastDiscoveryAddr),
|
||||
Port: ai.discoveryPort,
|
||||
Zone: ifaceName,
|
||||
}
|
||||
|
||||
if ai.outboundConn == nil {
|
||||
var err error
|
||||
ai.outboundConn, err = net.ListenUDP("udp6", &net.UDPAddr{Port: 0})
|
||||
if err != nil {
|
||||
return err
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to create outbound socket", "error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := ai.outboundConn.WriteToUDP(data, addr); err != nil {
|
||||
log.Printf("Failed to send to peer %s: %v", address, err)
|
||||
if _, err := ai.outboundConn.WriteToUDP(ai.groupHash, mcastAddr); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to send peer announce", "interface", ifaceName, "error", err)
|
||||
} else {
|
||||
debug.Log(debug.DEBUG_TRACE, "Sent peer announce", "interface", adoptedIface.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) peerJobs() {
|
||||
ticker := time.NewTicker(ai.peerJobInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if !ai.IsOnline() {
|
||||
return
|
||||
}
|
||||
|
||||
ai.Mutex.Lock()
|
||||
now := time.Now()
|
||||
|
||||
for peerKey, peer := range ai.peers {
|
||||
if now.Sub(peer.lastHeard) > ai.peeringTimeout {
|
||||
delete(ai.peers, peerKey)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Removed timed out peer", "peer", peerKey)
|
||||
}
|
||||
}
|
||||
|
||||
for ifaceName, echoTime := range ai.multicastEchoes {
|
||||
if now.Sub(echoTime) > ai.mcastEchoTimeout {
|
||||
if _, exists := ai.timedOutInterfaces[ifaceName]; !exists {
|
||||
debug.Log(debug.DEBUG_INFO, "Interface timed out", "interface", ifaceName)
|
||||
ai.timedOutInterfaces[ifaceName] = now
|
||||
}
|
||||
} else {
|
||||
delete(ai.timedOutInterfaces, ifaceName)
|
||||
}
|
||||
}
|
||||
|
||||
ai.Mutex.Unlock()
|
||||
case <-ai.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) Send(data []byte, address string) error {
|
||||
if !ai.IsOnline() {
|
||||
return fmt.Errorf("interface offline")
|
||||
}
|
||||
|
||||
ai.Mutex.RLock()
|
||||
defer ai.Mutex.RUnlock()
|
||||
|
||||
if len(ai.peers) == 0 {
|
||||
debug.Log(debug.DEBUG_TRACE, "No peers available for sending")
|
||||
return nil
|
||||
}
|
||||
|
||||
if ai.outboundConn == nil {
|
||||
var err error
|
||||
ai.outboundConn, err = net.ListenUDP("udp6", &net.UDPAddr{Port: 0})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create outbound socket: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
sentCount := 0
|
||||
for _, peer := range ai.peers {
|
||||
targetAddr := &net.UDPAddr{
|
||||
IP: peer.addr.IP,
|
||||
Port: ai.dataPort,
|
||||
Zone: peer.ifaceName,
|
||||
}
|
||||
|
||||
if _, err := ai.outboundConn.WriteToUDP(data, targetAddr); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to send to peer", "interface", peer.ifaceName, "error", err)
|
||||
continue
|
||||
}
|
||||
sentCount++
|
||||
}
|
||||
|
||||
if sentCount > 0 {
|
||||
debug.Log(debug.DEBUG_TRACE, "Sent data to peers", "count", sentCount, "bytes", len(data))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) Stop() error {
|
||||
ai.mutex.Lock()
|
||||
defer ai.mutex.Unlock()
|
||||
ai.Mutex.Lock()
|
||||
ai.Online = false
|
||||
ai.IN = false
|
||||
ai.OUT = false
|
||||
|
||||
for _, server := range ai.interfaceServers {
|
||||
server.Close()
|
||||
server.Close() // #nosec G104
|
||||
}
|
||||
|
||||
for _, server := range ai.discoveryServers {
|
||||
server.Close() // #nosec G104
|
||||
}
|
||||
|
||||
if ai.outboundConn != nil {
|
||||
ai.outboundConn.Close()
|
||||
ai.outboundConn.Close() // #nosec G104
|
||||
}
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
ai.stopOnce.Do(func() {
|
||||
if ai.done != nil {
|
||||
close(ai.done)
|
||||
}
|
||||
})
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "AutoInterface stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
295
pkg/interfaces/auto_test.go
Normal file
295
pkg/interfaces/auto_test.go
Normal file
@@ -0,0 +1,295 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/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,
|
||||
DiscoveryPort: 12345,
|
||||
DataPort: 54321,
|
||||
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 ai.dataPort != 54321 {
|
||||
t.Errorf("dataPort = %d; want 54321", ai.dataPort)
|
||||
}
|
||||
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]*AdoptedInterface)
|
||||
ai.interfaceServers = make(map[string]*net.UDPConn)
|
||||
ai.discoveryServers = make(map[string]*net.UDPConn)
|
||||
ai.multicastEchoes = make(map[string]time.Time)
|
||||
ai.timedOutInterfaces = 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"}
|
||||
|
||||
ai.Mutex.Lock()
|
||||
ai.linkLocalAddrs = append(ai.linkLocalAddrs, localAddrStr)
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
t.Run("AddPeer1", func(t *testing.T) {
|
||||
ai.Mutex.Lock()
|
||||
ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
// Give a small amount of time for the peer to be processed
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
ai.Mutex.RLock()
|
||||
count := len(ai.peers)
|
||||
peer, exists := ai.peers[peer1AddrStr]
|
||||
var ifaceName string
|
||||
if exists {
|
||||
ifaceName = peer.ifaceName
|
||||
}
|
||||
ai.Mutex.RUnlock()
|
||||
|
||||
if count != 1 {
|
||||
t.Fatalf("Expected 1 peer, got %d", count)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("Peer %s not found in map", peer1AddrStr)
|
||||
}
|
||||
if ifaceName != "eth0" {
|
||||
t.Errorf("Peer %s interface name = %s; want eth0", peer1AddrStr, ifaceName)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AddPeer2", func(t *testing.T) {
|
||||
ai.Mutex.Lock()
|
||||
ai.mockHandlePeerAnnounce(peer2Addr, "eth0")
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
// Give a small amount of time for the peer to be processed
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
ai.Mutex.RLock()
|
||||
count := len(ai.peers)
|
||||
_, exists := ai.peers[peer2AddrStr]
|
||||
ai.Mutex.RUnlock()
|
||||
|
||||
if count != 2 {
|
||||
t.Fatalf("Expected 2 peers, got %d", count)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("Peer %s not found in map", peer2AddrStr)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("IgnoreLocalAnnounce", func(t *testing.T) {
|
||||
ai.Mutex.Lock()
|
||||
ai.mockHandlePeerAnnounce(localAddr, "eth0")
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
// Give a small amount of time for the peer to be processed
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
ai.Mutex.RLock()
|
||||
count := len(ai.peers)
|
||||
ai.Mutex.RUnlock()
|
||||
|
||||
if count != 2 {
|
||||
t.Fatalf("Expected 2 peers after local announce, got %d", count)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UpdatePeerTimestamp", func(t *testing.T) {
|
||||
ai.Mutex.RLock()
|
||||
peer, exists := ai.peers[peer1AddrStr]
|
||||
var initialTime time.Time
|
||||
if exists {
|
||||
initialTime = peer.lastHeard
|
||||
}
|
||||
ai.Mutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
t.Fatalf("Peer %s not found before timestamp update", peer1AddrStr)
|
||||
}
|
||||
|
||||
ai.Mutex.Lock()
|
||||
ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
// Give a small amount of time for the peer to be processed
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
ai.Mutex.RLock()
|
||||
peer, exists = ai.peers[peer1AddrStr]
|
||||
var updatedTime time.Time
|
||||
if exists {
|
||||
updatedTime = peer.lastHeard
|
||||
}
|
||||
ai.Mutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
t.Fatalf("Peer %s not found after timestamp update", peer1AddrStr)
|
||||
}
|
||||
|
||||
if !updatedTime.After(initialTime) {
|
||||
t.Errorf("Peer timestamp was not updated after receiving another announce")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PeerTimeout", func(t *testing.T) {
|
||||
// Wait for peer timeout
|
||||
time.Sleep(testTimeout * 2)
|
||||
|
||||
ai.Mutex.RLock()
|
||||
count := len(ai.peers)
|
||||
ai.Mutex.RUnlock()
|
||||
|
||||
if count != 0 {
|
||||
t.Errorf("Expected all peers to timeout, got %d peers", count)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,17 +28,6 @@ const (
|
||||
TYPE_TCP = 0x02
|
||||
|
||||
PROPAGATION_RATE = 0.02 // 2% of interface bandwidth
|
||||
|
||||
DEBUG_LEVEL = 4 // Default debug level for interface logging
|
||||
|
||||
// Debug levels
|
||||
DEBUG_CRITICAL = 1
|
||||
DEBUG_ERROR = 2
|
||||
DEBUG_INFO = 3
|
||||
DEBUG_VERBOSE = 4
|
||||
DEBUG_TRACE = 5
|
||||
DEBUG_PACKETS = 6
|
||||
DEBUG_ALL = 7
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
@@ -78,8 +69,9 @@ type BaseInterface struct {
|
||||
TxBytes uint64
|
||||
RxBytes uint64
|
||||
lastTx time.Time
|
||||
lastRx time.Time
|
||||
|
||||
mutex sync.RWMutex
|
||||
Mutex sync.RWMutex
|
||||
packetCallback common.PacketCallback
|
||||
}
|
||||
|
||||
@@ -96,29 +88,30 @@ func NewBaseInterface(name string, ifType common.InterfaceType, enabled bool) Ba
|
||||
MTU: common.DEFAULT_MTU,
|
||||
Bitrate: BITRATE_MINIMUM,
|
||||
lastTx: time.Now(),
|
||||
lastRx: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
i.Mutex.Lock()
|
||||
defer i.Mutex.Unlock()
|
||||
i.packetCallback = callback
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetPacketCallback() common.PacketCallback {
|
||||
i.mutex.RLock()
|
||||
defer i.mutex.RUnlock()
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
return i.packetCallback
|
||||
}
|
||||
|
||||
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
||||
i.mutex.Lock()
|
||||
i.Mutex.Lock()
|
||||
i.RxBytes += uint64(len(data))
|
||||
i.mutex.Unlock()
|
||||
i.Mutex.Unlock()
|
||||
|
||||
i.mutex.RLock()
|
||||
i.Mutex.RLock()
|
||||
callback := i.packetCallback
|
||||
i.mutex.RUnlock()
|
||||
i.Mutex.RUnlock()
|
||||
|
||||
if callback != nil {
|
||||
callback(data, i)
|
||||
@@ -127,15 +120,15 @@ func (i *BaseInterface) ProcessIncoming(data []byte) {
|
||||
|
||||
func (i *BaseInterface) ProcessOutgoing(data []byte) error {
|
||||
if !i.Online || i.Detached {
|
||||
log.Printf("[DEBUG-1] Interface %s: Cannot process outgoing packet - interface offline or detached", i.Name)
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Interface cannot process outgoing packet - interface offline or detached", "name", i.Name)
|
||||
return fmt.Errorf("interface offline or detached")
|
||||
}
|
||||
|
||||
i.mutex.Lock()
|
||||
i.Mutex.Lock()
|
||||
i.TxBytes += uint64(len(data))
|
||||
i.mutex.Unlock()
|
||||
i.Mutex.Unlock()
|
||||
|
||||
log.Printf("[DEBUG-%d] Interface %s: Processed outgoing packet of %d bytes, total TX: %d", DEBUG_LEVEL, i.Name, len(data), i.TxBytes)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Interface processed outgoing packet", "name", i.Name, "bytes", len(data), "total_tx", i.TxBytes)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -145,7 +138,7 @@ func (i *BaseInterface) SendPathRequest(packet []byte) error {
|
||||
}
|
||||
|
||||
frame := make([]byte, 0, len(packet)+1)
|
||||
frame = append(frame, 0x01)
|
||||
frame = append(frame, common.HEX_0x01)
|
||||
frame = append(frame, packet...)
|
||||
|
||||
return i.ProcessOutgoing(frame)
|
||||
@@ -157,11 +150,11 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
|
||||
}
|
||||
|
||||
frame := make([]byte, 0, len(dest)+len(data)+9)
|
||||
frame = append(frame, 0x02)
|
||||
frame = append(frame, common.HEX_0x02)
|
||||
frame = append(frame, dest...)
|
||||
|
||||
ts := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix()))
|
||||
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) // #nosec G115
|
||||
frame = append(frame, ts...)
|
||||
frame = append(frame, data...)
|
||||
|
||||
@@ -169,35 +162,35 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Detach() {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
i.Mutex.Lock()
|
||||
defer i.Mutex.Unlock()
|
||||
i.Detached = true
|
||||
i.Online = false
|
||||
}
|
||||
|
||||
func (i *BaseInterface) IsEnabled() bool {
|
||||
i.mutex.RLock()
|
||||
defer i.mutex.RUnlock()
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
return i.Enabled && i.Online && !i.Detached
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Enable() {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
i.Mutex.Lock()
|
||||
defer i.Mutex.Unlock()
|
||||
|
||||
prevState := i.Enabled
|
||||
i.Enabled = true
|
||||
i.Online = true
|
||||
|
||||
log.Printf("[DEBUG-%d] Interface %s: State changed - Enabled: %v->%v, Online: %v->%v", DEBUG_INFO, i.Name, prevState, i.Enabled, !i.Online, i.Online)
|
||||
debug.Log(debug.DEBUG_INFO, "Interface state changed", "name", i.Name, "enabled_prev", prevState, "enabled", i.Enabled, "online_prev", !i.Online, "online", i.Online)
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Disable() {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
i.Mutex.Lock()
|
||||
defer i.Mutex.Unlock()
|
||||
i.Enabled = false
|
||||
i.Online = false
|
||||
log.Printf("[DEBUG-2] Interface %s: Disabled and offline", i.Name)
|
||||
debug.Log(debug.DEBUG_ERROR, "Interface disabled and offline", "name", i.Name)
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetName() string {
|
||||
@@ -217,14 +210,14 @@ func (i *BaseInterface) GetMTU() int {
|
||||
}
|
||||
|
||||
func (i *BaseInterface) IsOnline() bool {
|
||||
i.mutex.RLock()
|
||||
defer i.mutex.RUnlock()
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
return i.Online
|
||||
}
|
||||
|
||||
func (i *BaseInterface) IsDetached() bool {
|
||||
i.mutex.RLock()
|
||||
defer i.mutex.RUnlock()
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
return i.Detached
|
||||
}
|
||||
|
||||
@@ -237,11 +230,11 @@ func (i *BaseInterface) Stop() error {
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Send(data []byte, address string) error {
|
||||
log.Printf("[DEBUG-%d] Interface %s: Sending %d bytes to %s", DEBUG_LEVEL, i.Name, len(data), address)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Interface sending bytes", "name", i.Name, "bytes", len(data), "address", address)
|
||||
|
||||
err := i.ProcessOutgoing(data)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-1] Interface %s: Failed to send data: %v", i.Name, err)
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Interface failed to send data", "name", i.Name, "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -254,14 +247,14 @@ func (i *BaseInterface) GetConn() net.Conn {
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetBandwidthAvailable() bool {
|
||||
i.mutex.RLock()
|
||||
defer i.mutex.RUnlock()
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
|
||||
now := time.Now()
|
||||
timeSinceLastTx := now.Sub(i.lastTx)
|
||||
|
||||
if timeSinceLastTx > time.Second {
|
||||
log.Printf("[DEBUG-%d] Interface %s: Bandwidth available (idle for %.2fs)", DEBUG_VERBOSE, i.Name, timeSinceLastTx.Seconds())
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Interface bandwidth available", "name", i.Name, "idle_seconds", timeSinceLastTx.Seconds())
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -270,19 +263,19 @@ func (i *BaseInterface) GetBandwidthAvailable() bool {
|
||||
maxUsage := float64(i.Bitrate) * PROPAGATION_RATE
|
||||
|
||||
available := currentUsage < maxUsage
|
||||
log.Printf("[DEBUG-%d] Interface %s: Bandwidth stats - Current: %.2f bps, Max: %.2f bps, Usage: %.1f%%, Available: %v", DEBUG_VERBOSE, i.Name, currentUsage, maxUsage, (currentUsage/maxUsage)*100, available)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Interface bandwidth stats", "name", i.Name, "current_bps", currentUsage, "max_bps", maxUsage, "usage_percent", (currentUsage/maxUsage)*100, "available", available)
|
||||
|
||||
return available
|
||||
}
|
||||
|
||||
func (i *BaseInterface) updateBandwidthStats(bytes uint64) {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
i.Mutex.Lock()
|
||||
defer i.Mutex.Unlock()
|
||||
|
||||
i.TxBytes += bytes
|
||||
i.lastTx = time.Now()
|
||||
|
||||
log.Printf("[DEBUG-%d] Interface %s: Updated bandwidth stats - TX bytes: %d, Last TX: %v", DEBUG_LEVEL, i.Name, i.TxBytes, i.lastTx)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Interface updated bandwidth stats", "name", i.Name, "tx_bytes", i.TxBytes, "last_tx", i.lastTx)
|
||||
}
|
||||
|
||||
type InterceptedInterface struct {
|
||||
@@ -305,7 +298,7 @@ func (i *InterceptedInterface) Send(data []byte, addr string) error {
|
||||
// Call interceptor if provided
|
||||
if i.interceptor != nil && len(data) > 0 {
|
||||
if err := i.interceptor(data, i); err != nil {
|
||||
log.Printf("[DEBUG-2] Failed to intercept outgoing packet: %v", err)
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to intercept outgoing packet", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
229
pkg/interfaces/interface_test.go
Normal file
229
pkg/interfaces/interface_test.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,14 +23,26 @@ const (
|
||||
KISS_TFEND = 0xDC
|
||||
KISS_TFESC = 0xDD
|
||||
|
||||
TCP_USER_TIMEOUT = 24
|
||||
TCP_PROBE_AFTER = 5
|
||||
TCP_PROBE_INTERVAL = 2
|
||||
TCP_PROBES = 12
|
||||
RECONNECT_WAIT = 5
|
||||
INITIAL_TIMEOUT = 5
|
||||
INITIAL_BACKOFF = time.Second
|
||||
MAX_BACKOFF = time.Minute * 5
|
||||
DEFAULT_MTU = 1064
|
||||
BITRATE_GUESS_VAL = 10 * 1000 * 1000
|
||||
RECONNECT_WAIT = 5
|
||||
INITIAL_TIMEOUT = 5
|
||||
INITIAL_BACKOFF = time.Second
|
||||
MAX_BACKOFF = time.Minute * 5
|
||||
|
||||
TCP_USER_TIMEOUT_SEC = 24
|
||||
TCP_PROBE_AFTER_SEC = 5
|
||||
TCP_PROBE_INTERVAL_SEC = 2
|
||||
TCP_PROBES_COUNT = 12
|
||||
TCP_CONNECT_TIMEOUT = 10 * time.Second
|
||||
TCP_MILLISECONDS = 1000
|
||||
|
||||
I2P_USER_TIMEOUT_SEC = 45
|
||||
I2P_PROBE_AFTER_SEC = 10
|
||||
I2P_PROBE_INTERVAL_SEC = 9
|
||||
I2P_PROBES_COUNT = 5
|
||||
|
||||
SO_KEEPALIVE_ENABLE = 1
|
||||
)
|
||||
|
||||
type TCPClientInterface struct {
|
||||
@@ -45,12 +59,8 @@ type TCPClientInterface struct {
|
||||
maxReconnectTries int
|
||||
packetBuffer []byte
|
||||
packetType byte
|
||||
mutex sync.RWMutex
|
||||
enabled bool
|
||||
TxBytes uint64
|
||||
RxBytes uint64
|
||||
lastTx time.Time
|
||||
lastRx time.Time
|
||||
done chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func NewTCPClientInterface(name string, targetHost string, targetPort int, kissFraming bool, i2pTunneled bool, enabled bool) (*TCPClientInterface, error) {
|
||||
@@ -61,14 +71,14 @@ func NewTCPClientInterface(name string, targetHost string, targetPort int, kissF
|
||||
kissFraming: kissFraming,
|
||||
i2pTunneled: i2pTunneled,
|
||||
initiator: true,
|
||||
enabled: enabled,
|
||||
maxReconnectTries: TCP_PROBES,
|
||||
maxReconnectTries: RECONNECT_WAIT * TCP_PROBES_COUNT,
|
||||
packetBuffer: make([]byte, 0),
|
||||
neverConnected: true,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
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
|
||||
@@ -82,43 +92,81 @@ func NewTCPClientInterface(name string, targetHost string, targetPort int, kissF
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) Start() error {
|
||||
tc.mutex.Lock()
|
||||
defer tc.mutex.Unlock()
|
||||
|
||||
if !tc.Enabled {
|
||||
return fmt.Errorf("interface not enabled")
|
||||
tc.Mutex.Lock()
|
||||
if !tc.Enabled || tc.Detached {
|
||||
tc.Mutex.Unlock()
|
||||
return fmt.Errorf("interface not enabled or detached")
|
||||
}
|
||||
|
||||
if tc.conn != nil {
|
||||
tc.Online = true
|
||||
go tc.readLoop()
|
||||
tc.Mutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort)
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
// Only recreate done if it's nil or was closed
|
||||
select {
|
||||
case <-tc.done:
|
||||
tc.done = make(chan struct{})
|
||||
tc.stopOnce = sync.Once{}
|
||||
default:
|
||||
if tc.done == nil {
|
||||
tc.done = make(chan struct{})
|
||||
tc.stopOnce = sync.Once{}
|
||||
}
|
||||
}
|
||||
tc.Mutex.Unlock()
|
||||
|
||||
addr := net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort))
|
||||
conn, err := net.DialTimeout("tcp", addr, TCP_CONNECT_TIMEOUT)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tc.Mutex.Lock()
|
||||
tc.conn = conn
|
||||
tc.Mutex.Unlock()
|
||||
|
||||
// Set platform-specific timeouts
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
if err := tc.setTimeoutsLinux(); err != nil {
|
||||
log.Printf("[DEBUG-2] Failed to set Linux TCP timeouts: %v", err)
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to set Linux TCP timeouts", "error", err)
|
||||
}
|
||||
case "darwin":
|
||||
if err := tc.setTimeoutsOSX(); err != nil {
|
||||
log.Printf("[DEBUG-2] Failed to set OSX TCP timeouts: %v", err)
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to set OSX TCP timeouts", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
tc.Mutex.Lock()
|
||||
tc.Online = true
|
||||
tc.Mutex.Unlock()
|
||||
|
||||
go tc.readLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) Stop() error {
|
||||
tc.Mutex.Lock()
|
||||
tc.Enabled = false
|
||||
tc.Online = false
|
||||
if tc.conn != nil {
|
||||
_ = tc.conn.Close()
|
||||
tc.conn = nil
|
||||
}
|
||||
tc.Mutex.Unlock()
|
||||
|
||||
tc.stopOnce.Do(func() {
|
||||
if tc.done != nil {
|
||||
close(tc.done)
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) readLoop() {
|
||||
buffer := make([]byte, tc.MTU)
|
||||
inFrame := false
|
||||
@@ -126,10 +174,30 @@ func (tc *TCPClientInterface) readLoop() {
|
||||
dataBuffer := make([]byte, 0)
|
||||
|
||||
for {
|
||||
n, err := tc.conn.Read(buffer)
|
||||
tc.Mutex.RLock()
|
||||
conn := tc.conn
|
||||
done := tc.done
|
||||
tc.Mutex.RUnlock()
|
||||
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
tc.Mutex.Lock()
|
||||
tc.Online = false
|
||||
if tc.initiator && !tc.Detached {
|
||||
detached := tc.Detached
|
||||
initiator := tc.initiator
|
||||
tc.Mutex.Unlock()
|
||||
|
||||
if initiator && !detached {
|
||||
go tc.reconnect()
|
||||
} else {
|
||||
tc.teardown()
|
||||
@@ -137,59 +205,29 @@ func (tc *TCPClientInterface) readLoop() {
|
||||
return
|
||||
}
|
||||
|
||||
// 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]
|
||||
|
||||
if tc.kissFraming {
|
||||
// KISS framing logic
|
||||
if b == KISS_FEND {
|
||||
if inFrame && len(dataBuffer) > 0 {
|
||||
tc.handlePacket(dataBuffer)
|
||||
dataBuffer = dataBuffer[:0]
|
||||
}
|
||||
inFrame = !inFrame
|
||||
continue
|
||||
if b == HDLC_FLAG {
|
||||
if inFrame && len(dataBuffer) > 0 {
|
||||
tc.handlePacket(dataBuffer)
|
||||
dataBuffer = dataBuffer[:0]
|
||||
}
|
||||
inFrame = !inFrame
|
||||
continue
|
||||
}
|
||||
|
||||
if inFrame {
|
||||
if b == KISS_FESC {
|
||||
escape = true
|
||||
} else {
|
||||
if escape {
|
||||
if b == KISS_TFEND {
|
||||
b = KISS_FEND
|
||||
} else if b == KISS_TFESC {
|
||||
b = KISS_FESC
|
||||
}
|
||||
escape = false
|
||||
}
|
||||
dataBuffer = append(dataBuffer, b)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// HDLC framing logic
|
||||
if b == HDLC_FLAG {
|
||||
if inFrame && len(dataBuffer) > 0 {
|
||||
tc.handlePacket(dataBuffer)
|
||||
dataBuffer = dataBuffer[:0]
|
||||
}
|
||||
inFrame = !inFrame
|
||||
continue
|
||||
}
|
||||
|
||||
if inFrame {
|
||||
if b == HDLC_ESC {
|
||||
escape = true
|
||||
} else {
|
||||
if escape {
|
||||
b ^= HDLC_ESC_MASK
|
||||
escape = false
|
||||
}
|
||||
dataBuffer = append(dataBuffer, b)
|
||||
if inFrame {
|
||||
if b == HDLC_ESC {
|
||||
escape = true
|
||||
} else {
|
||||
if escape {
|
||||
b ^= HDLC_ESC_MASK
|
||||
escape = false
|
||||
}
|
||||
dataBuffer = append(dataBuffer, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,67 +236,74 @@ func (tc *TCPClientInterface) readLoop() {
|
||||
|
||||
func (tc *TCPClientInterface) handlePacket(data []byte) {
|
||||
if len(data) < 1 {
|
||||
log.Printf("[DEBUG-7] Received invalid packet: empty")
|
||||
debug.Log(debug.DEBUG_ALL, "Received invalid packet: empty")
|
||||
return
|
||||
}
|
||||
|
||||
tc.mutex.Lock()
|
||||
tc.packetType = data[0]
|
||||
tc.Mutex.Lock()
|
||||
tc.RxBytes += uint64(len(data))
|
||||
lastRx := time.Now()
|
||||
tc.lastRx = lastRx
|
||||
tc.mutex.Unlock()
|
||||
callback := tc.packetCallback
|
||||
tc.Mutex.Unlock()
|
||||
|
||||
log.Printf("[DEBUG-7] Received packet: type=0x%02x, size=%d bytes", tc.packetType, len(data))
|
||||
debug.Log(debug.DEBUG_ALL, "Received packet", "type", fmt.Sprintf("0x%02x", data[0]), "size", len(data))
|
||||
|
||||
payload := data[1:]
|
||||
|
||||
switch tc.packetType {
|
||||
case 0x01: // Announce packet
|
||||
log.Printf("[DEBUG-7] Processing announce packet: payload=%d bytes", len(payload))
|
||||
if len(payload) >= 53 {
|
||||
tc.BaseInterface.ProcessIncoming(payload)
|
||||
} else {
|
||||
log.Printf("[DEBUG-7] Announce packet too small: %d bytes", len(payload))
|
||||
}
|
||||
case 0x02: // Link packet
|
||||
log.Printf("[DEBUG-7] Processing link packet: payload=%d bytes", len(payload))
|
||||
if len(payload) < 40 {
|
||||
log.Printf("[DEBUG-7] Link packet too small: %d bytes", len(payload))
|
||||
return
|
||||
}
|
||||
tc.BaseInterface.ProcessIncoming(payload)
|
||||
case 0x03: // Announce packet
|
||||
tc.BaseInterface.ProcessIncoming(payload)
|
||||
case 0x04: // Transport packet
|
||||
tc.BaseInterface.ProcessIncoming(payload)
|
||||
default:
|
||||
// Unknown packet type
|
||||
return
|
||||
// For RNS packets, call the packet callback directly
|
||||
if callback != nil {
|
||||
debug.Log(debug.DEBUG_ALL, "Calling packet callback for RNS packet")
|
||||
callback(data, tc)
|
||||
} else {
|
||||
debug.Log(debug.DEBUG_ALL, "No packet callback set for TCP interface")
|
||||
}
|
||||
}
|
||||
|
||||
// Send implements the interface Send method for TCP interface
|
||||
func (tc *TCPClientInterface) Send(data []byte, address string) error {
|
||||
debug.Log(debug.DEBUG_ALL, "TCP interface sending bytes", "name", tc.Name, "bytes", len(data))
|
||||
|
||||
if !tc.IsEnabled() || !tc.IsOnline() {
|
||||
return fmt.Errorf("TCP interface %s is not online", tc.Name)
|
||||
}
|
||||
|
||||
// Send data directly - packet type is already in the first byte of data
|
||||
// TCP interface uses HDLC framing around the raw packet
|
||||
return tc.ProcessOutgoing(data)
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) ProcessOutgoing(data []byte) error {
|
||||
if !tc.Online {
|
||||
tc.Mutex.RLock()
|
||||
online := tc.Online
|
||||
tc.Mutex.RUnlock()
|
||||
|
||||
if !online {
|
||||
return fmt.Errorf("interface offline")
|
||||
}
|
||||
|
||||
tc.writing = true
|
||||
defer func() { tc.writing = false }()
|
||||
|
||||
// For TCP connections, use HDLC framing
|
||||
var frame []byte
|
||||
if tc.kissFraming {
|
||||
frame = append([]byte{KISS_FEND}, escapeKISS(data)...)
|
||||
frame = append(frame, KISS_FEND)
|
||||
} else {
|
||||
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
|
||||
frame = append(frame, HDLC_FLAG)
|
||||
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
|
||||
frame = append(frame, HDLC_FLAG)
|
||||
|
||||
tc.UpdateStats(uint64(len(frame)), false) // #nosec G115
|
||||
|
||||
debug.Log(debug.DEBUG_ALL, "TCP interface writing to network", "name", tc.Name, "bytes", len(frame))
|
||||
|
||||
tc.Mutex.RLock()
|
||||
conn := tc.conn
|
||||
tc.Mutex.RUnlock()
|
||||
|
||||
if conn == nil {
|
||||
return fmt.Errorf("connection closed")
|
||||
}
|
||||
|
||||
// Update TX stats before sending
|
||||
tc.UpdateStats(uint64(len(frame)), false)
|
||||
|
||||
_, err := tc.conn.Write(frame)
|
||||
_, err := conn.Write(frame)
|
||||
if err != nil {
|
||||
debug.Log(debug.DEBUG_CRITICAL, "TCP interface write failed", "name", tc.Name, "error", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -267,7 +312,7 @@ func (tc *TCPClientInterface) teardown() {
|
||||
tc.IN = false
|
||||
tc.OUT = false
|
||||
if tc.conn != nil {
|
||||
tc.conn.Close()
|
||||
_ = tc.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,9 +348,9 @@ func (tc *TCPClientInterface) SetPacketCallback(cb common.PacketCallback) {
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) IsEnabled() bool {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
return tc.enabled && tc.Online && !tc.Detached
|
||||
tc.Mutex.RLock()
|
||||
defer tc.Mutex.RUnlock()
|
||||
return tc.Enabled && tc.Online && !tc.Detached
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) GetName() string {
|
||||
@@ -313,31 +358,31 @@ func (tc *TCPClientInterface) GetName() string {
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) GetPacketCallback() common.PacketCallback {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
tc.Mutex.RLock()
|
||||
defer tc.Mutex.RUnlock()
|
||||
return tc.packetCallback
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) IsDetached() bool {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
tc.Mutex.RLock()
|
||||
defer tc.Mutex.RUnlock()
|
||||
return tc.Detached
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) IsOnline() bool {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
tc.Mutex.RLock()
|
||||
defer tc.Mutex.RUnlock()
|
||||
return tc.Online
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) reconnect() {
|
||||
tc.mutex.Lock()
|
||||
tc.Mutex.Lock()
|
||||
if tc.reconnecting {
|
||||
tc.mutex.Unlock()
|
||||
tc.Mutex.Unlock()
|
||||
return
|
||||
}
|
||||
tc.reconnecting = true
|
||||
tc.mutex.Unlock()
|
||||
tc.Mutex.Unlock()
|
||||
|
||||
backoff := time.Second
|
||||
maxBackoff := time.Minute * 5
|
||||
@@ -346,25 +391,23 @@ 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 {
|
||||
tc.mutex.Lock()
|
||||
tc.Mutex.Lock()
|
||||
tc.conn = conn
|
||||
tc.Online = true
|
||||
|
||||
tc.neverConnected = false
|
||||
tc.reconnecting = false
|
||||
tc.mutex.Unlock()
|
||||
tc.Mutex.Unlock()
|
||||
|
||||
go tc.readLoop()
|
||||
return
|
||||
}
|
||||
|
||||
// Log reconnection attempt
|
||||
fmt.Printf("Failed to reconnect to %s (attempt %d/%d): %v\n",
|
||||
addr, retries+1, tc.maxReconnectTries, err)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to reconnect", "target", net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort)), "attempt", retries+1, "maxTries", tc.maxReconnectTries, "error", err)
|
||||
|
||||
// Wait with exponential backoff
|
||||
time.Sleep(backoff)
|
||||
@@ -378,49 +421,49 @@ func (tc *TCPClientInterface) reconnect() {
|
||||
retries++
|
||||
}
|
||||
|
||||
tc.mutex.Lock()
|
||||
tc.Mutex.Lock()
|
||||
tc.reconnecting = false
|
||||
tc.mutex.Unlock()
|
||||
tc.Mutex.Unlock()
|
||||
|
||||
// If we've exhausted all retries, perform final teardown
|
||||
tc.teardown()
|
||||
fmt.Printf("Failed to reconnect to %s after %d attempts\n",
|
||||
fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort), tc.maxReconnectTries)
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to reconnect after all attempts", "target", net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort)), "maxTries", tc.maxReconnectTries)
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) Enable() {
|
||||
tc.mutex.Lock()
|
||||
defer tc.mutex.Unlock()
|
||||
tc.Mutex.Lock()
|
||||
defer tc.Mutex.Unlock()
|
||||
tc.Online = true
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) Disable() {
|
||||
tc.mutex.Lock()
|
||||
defer tc.mutex.Unlock()
|
||||
tc.Mutex.Lock()
|
||||
defer tc.Mutex.Unlock()
|
||||
tc.Online = false
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) IsConnected() bool {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
tc.Mutex.RLock()
|
||||
defer tc.Mutex.RUnlock()
|
||||
return tc.conn != nil && tc.Online && !tc.reconnecting
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) GetRTT() time.Duration {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
tc.Mutex.RLock()
|
||||
defer tc.Mutex.RUnlock()
|
||||
|
||||
if !tc.IsConnected() {
|
||||
return 0
|
||||
}
|
||||
|
||||
if tcpConn, ok := tc.conn.(*net.TCPConn); ok {
|
||||
var rtt time.Duration = 0
|
||||
var rtt time.Duration
|
||||
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 {
|
||||
debug.Log(debug.DEBUG_ERROR, "Error in SyscallConn Control", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return rtt
|
||||
@@ -430,84 +473,50 @@ func (tc *TCPClientInterface) GetRTT() time.Duration {
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) GetTxBytes() uint64 {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
tc.Mutex.RLock()
|
||||
defer tc.Mutex.RUnlock()
|
||||
return tc.TxBytes
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) GetRxBytes() uint64 {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
tc.Mutex.RLock()
|
||||
defer tc.Mutex.RUnlock()
|
||||
return tc.RxBytes
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) UpdateStats(bytes uint64, isRx bool) {
|
||||
tc.mutex.Lock()
|
||||
defer tc.mutex.Unlock()
|
||||
tc.Mutex.Lock()
|
||||
defer tc.Mutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
if isRx {
|
||||
tc.RxBytes += bytes
|
||||
tc.lastRx = now
|
||||
log.Printf("[DEBUG-5] Interface %s RX stats: bytes=%d total=%d last=%v",
|
||||
tc.Name, bytes, tc.RxBytes, tc.lastRx)
|
||||
debug.Log(debug.DEBUG_TRACE, "Interface RX stats", "name", tc.Name, "bytes", bytes, "total", tc.RxBytes, "last", tc.lastRx)
|
||||
} else {
|
||||
tc.TxBytes += bytes
|
||||
tc.lastTx = now
|
||||
log.Printf("[DEBUG-5] Interface %s TX stats: bytes=%d total=%d last=%v",
|
||||
tc.Name, bytes, tc.TxBytes, tc.lastTx)
|
||||
debug.Log(debug.DEBUG_TRACE, "Interface TX stats", "name", tc.Name, "bytes", bytes, "total", tc.TxBytes, "last", tc.lastTx)
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) GetStats() (tx uint64, rx uint64, lastTx time.Time, lastRx time.Time) {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
tc.Mutex.RLock()
|
||||
defer tc.Mutex.RUnlock()
|
||||
return tc.TxBytes, tc.RxBytes, tc.lastTx, tc.lastRx
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
if !tc.i2pTunneled {
|
||||
if err := tcpConn.SetKeepAlive(true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tcpConn.SetKeepAlivePeriod(time.Duration(TCP_PROBE_INTERVAL) * time.Second); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlive(true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type TCPServerInterface struct {
|
||||
BaseInterface
|
||||
connections map[string]net.Conn
|
||||
mutex sync.RWMutex
|
||||
bindAddr string
|
||||
bindPort int
|
||||
preferIPv6 bool
|
||||
kissFraming bool
|
||||
i2pTunneled bool
|
||||
packetCallback common.PacketCallback
|
||||
TxBytes uint64
|
||||
RxBytes uint64
|
||||
connections map[string]net.Conn
|
||||
listener net.Listener
|
||||
bindAddr string
|
||||
bindPort int
|
||||
preferIPv6 bool
|
||||
kissFraming bool
|
||||
i2pTunneled bool
|
||||
done chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFraming bool, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) {
|
||||
@@ -518,6 +527,7 @@ func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFrami
|
||||
Type: common.IF_TYPE_TCP,
|
||||
Online: false,
|
||||
MTU: common.DEFAULT_MTU,
|
||||
Enabled: true,
|
||||
Detached: false,
|
||||
},
|
||||
connections: make(map[string]net.Conn),
|
||||
@@ -526,6 +536,7 @@ func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFrami
|
||||
preferIPv6: preferIPv6,
|
||||
kissFraming: kissFraming,
|
||||
i2pTunneled: i2pTunneled,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
return ts, nil
|
||||
@@ -544,21 +555,21 @@ func (ts *TCPServerInterface) String() string {
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
ts.Mutex.Lock()
|
||||
defer ts.Mutex.Unlock()
|
||||
ts.packetCallback = callback
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) GetPacketCallback() common.PacketCallback {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
ts.Mutex.RLock()
|
||||
defer ts.Mutex.RUnlock()
|
||||
return ts.packetCallback
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) IsEnabled() bool {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
return ts.BaseInterface.Enabled && ts.BaseInterface.Online && !ts.BaseInterface.Detached
|
||||
ts.Mutex.RLock()
|
||||
defer ts.Mutex.RUnlock()
|
||||
return ts.Enabled && ts.Online && !ts.Detached
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) GetName() string {
|
||||
@@ -566,50 +577,81 @@ func (ts *TCPServerInterface) GetName() string {
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) IsDetached() bool {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
return ts.BaseInterface.Detached
|
||||
ts.Mutex.RLock()
|
||||
defer ts.Mutex.RUnlock()
|
||||
return ts.Detached
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) IsOnline() bool {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
ts.Mutex.RLock()
|
||||
defer ts.Mutex.RUnlock()
|
||||
return ts.Online
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) Enable() {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
ts.Mutex.Lock()
|
||||
defer ts.Mutex.Unlock()
|
||||
ts.Online = true
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) Disable() {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
ts.Mutex.Lock()
|
||||
defer ts.Mutex.Unlock()
|
||||
ts.Online = false
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) Start() error {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
ts.Mutex.Lock()
|
||||
if ts.listener != nil {
|
||||
ts.Mutex.Unlock()
|
||||
return fmt.Errorf("TCP server already started")
|
||||
}
|
||||
// Only recreate done if it's nil or was closed
|
||||
select {
|
||||
case <-ts.done:
|
||||
ts.done = make(chan struct{})
|
||||
ts.stopOnce = sync.Once{}
|
||||
default:
|
||||
if ts.done == nil {
|
||||
ts.done = make(chan struct{})
|
||||
ts.stopOnce = sync.Once{}
|
||||
}
|
||||
}
|
||||
ts.Mutex.Unlock()
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", ts.bindAddr, ts.bindPort)
|
||||
addr := net.JoinHostPort(ts.bindAddr, fmt.Sprintf("%d", ts.bindPort))
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start TCP server: %w", err)
|
||||
}
|
||||
|
||||
ts.Mutex.Lock()
|
||||
ts.listener = listener
|
||||
ts.Online = true
|
||||
ts.Mutex.Unlock()
|
||||
|
||||
// Accept connections in a goroutine
|
||||
go func() {
|
||||
for {
|
||||
ts.Mutex.RLock()
|
||||
done := ts.done
|
||||
ts.Mutex.RUnlock()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
if !ts.Online {
|
||||
ts.Mutex.RLock()
|
||||
online := ts.Online
|
||||
ts.Mutex.RUnlock()
|
||||
if !online {
|
||||
return // Normal shutdown
|
||||
}
|
||||
log.Printf("[DEBUG-2] Error accepting connection: %v", err)
|
||||
debug.Log(debug.DEBUG_ERROR, "Error accepting connection", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -622,60 +664,87 @@ func (ts *TCPServerInterface) Start() error {
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) Stop() error {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
|
||||
ts.Mutex.Lock()
|
||||
ts.Online = false
|
||||
if ts.listener != nil {
|
||||
_ = ts.listener.Close()
|
||||
ts.listener = nil
|
||||
}
|
||||
// Close all client connections
|
||||
for addr, conn := range ts.connections {
|
||||
_ = conn.Close()
|
||||
delete(ts.connections, addr)
|
||||
}
|
||||
ts.Mutex.Unlock()
|
||||
|
||||
ts.stopOnce.Do(func() {
|
||||
if ts.done != nil {
|
||||
close(ts.done)
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) GetTxBytes() uint64 {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
ts.Mutex.RLock()
|
||||
defer ts.Mutex.RUnlock()
|
||||
return ts.TxBytes
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) GetRxBytes() uint64 {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
ts.Mutex.RLock()
|
||||
defer ts.Mutex.RUnlock()
|
||||
return ts.RxBytes
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
|
||||
addr := conn.RemoteAddr().String()
|
||||
ts.mutex.Lock()
|
||||
ts.Mutex.Lock()
|
||||
ts.connections[addr] = conn
|
||||
ts.mutex.Unlock()
|
||||
ts.Mutex.Unlock()
|
||||
|
||||
defer func() {
|
||||
ts.mutex.Lock()
|
||||
ts.Mutex.Lock()
|
||||
delete(ts.connections, addr)
|
||||
ts.mutex.Unlock()
|
||||
conn.Close()
|
||||
ts.Mutex.Unlock()
|
||||
_ = conn.Close()
|
||||
}()
|
||||
|
||||
buffer := make([]byte, ts.MTU)
|
||||
for {
|
||||
ts.Mutex.RLock()
|
||||
done := ts.done
|
||||
ts.Mutex.RUnlock()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ts.mutex.Lock()
|
||||
ts.RxBytes += uint64(n)
|
||||
ts.mutex.Unlock()
|
||||
ts.Mutex.Lock()
|
||||
ts.RxBytes += uint64(n) // #nosec G115
|
||||
callback := ts.packetCallback
|
||||
ts.Mutex.Unlock()
|
||||
|
||||
if ts.packetCallback != nil {
|
||||
ts.packetCallback(buffer[:n], ts)
|
||||
if callback != nil {
|
||||
callback(buffer[:n], ts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
ts.Mutex.RLock()
|
||||
online := ts.Online
|
||||
ts.Mutex.RUnlock()
|
||||
|
||||
if !ts.Online {
|
||||
if !online {
|
||||
return fmt.Errorf("interface offline")
|
||||
}
|
||||
|
||||
@@ -688,12 +757,17 @@ func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
|
||||
frame = append(frame, HDLC_FLAG)
|
||||
}
|
||||
|
||||
ts.TxBytes += uint64(len(frame))
|
||||
|
||||
ts.Mutex.Lock()
|
||||
ts.TxBytes += uint64(len(frame)) // #nosec G115
|
||||
conns := make([]net.Conn, 0, len(ts.connections))
|
||||
for _, conn := range ts.connections {
|
||||
conns = append(conns, conn)
|
||||
}
|
||||
ts.Mutex.Unlock()
|
||||
|
||||
for _, conn := range conns {
|
||||
if _, err := conn.Write(frame); err != nil {
|
||||
log.Printf("[DEBUG-4] Error writing to connection %s: %v",
|
||||
conn.RemoteAddr(), err)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Error writing to connection", "address", conn.RemoteAddr(), "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
@@ -11,4 +13,4 @@ import (
|
||||
// Default implementation for non-Linux platforms
|
||||
func platformGetRTT(fd uintptr) time.Duration {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
61
pkg/interfaces/tcp_darwin.go
Normal file
61
pkg/interfaces/tcp_darwin.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||
return tc.setTimeoutsOSX()
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
rawConn, err := tcpConn.SyscallConn()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get raw connection: %v", err)
|
||||
}
|
||||
|
||||
var sockoptErr error
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
const TCP_KEEPALIVE = 0x10
|
||||
|
||||
var probeAfter int
|
||||
if tc.i2pTunneled {
|
||||
probeAfter = I2P_PROBE_AFTER_SEC
|
||||
} else {
|
||||
probeAfter = TCP_PROBE_AFTER_SEC
|
||||
}
|
||||
|
||||
if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, SO_KEEPALIVE_ENABLE); err != nil {
|
||||
sockoptErr = fmt.Errorf("failed to enable SO_KEEPALIVE: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_KEEPALIVE, probeAfter); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_KEEPALIVE", "error", err)
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("control failed: %v", err)
|
||||
}
|
||||
if sockoptErr != nil {
|
||||
return sockoptErr
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "TCP keepalive configured (OSX)", "i2p", tc.i2pTunneled)
|
||||
return nil
|
||||
}
|
||||
41
pkg/interfaces/tcp_freebsd.go
Normal file
41
pkg/interfaces/tcp_freebsd.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build freebsd
|
||||
// +build freebsd
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlive(true); err != nil {
|
||||
return fmt.Errorf("failed to enable keepalive: %v", err)
|
||||
}
|
||||
|
||||
keepalivePeriod := TCP_PROBE_INTERVAL_SEC * time.Second
|
||||
if tc.i2pTunneled {
|
||||
keepalivePeriod = I2P_PROBE_INTERVAL_SEC * time.Second
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlivePeriod(keepalivePeriod); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set keepalive period", "error", err)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "TCP keepalive configured (FreeBSD)", "i2p", tc.i2pTunneled)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||
return tc.setTimeoutsLinux()
|
||||
}
|
||||
@@ -1,32 +1,111 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
rawConn, err := tcpConn.SyscallConn()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get raw connection: %v", err)
|
||||
}
|
||||
|
||||
var sockoptErr error
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
var userTimeout, probeAfter, probeInterval, probeCount int
|
||||
|
||||
if tc.i2pTunneled {
|
||||
userTimeout = I2P_USER_TIMEOUT_SEC * TCP_MILLISECONDS
|
||||
probeAfter = I2P_PROBE_AFTER_SEC
|
||||
probeInterval = I2P_PROBE_INTERVAL_SEC
|
||||
probeCount = I2P_PROBES_COUNT
|
||||
} else {
|
||||
userTimeout = TCP_USER_TIMEOUT_SEC * TCP_MILLISECONDS
|
||||
probeAfter = TCP_PROBE_AFTER_SEC
|
||||
probeInterval = TCP_PROBE_INTERVAL_SEC
|
||||
probeCount = TCP_PROBES_COUNT
|
||||
}
|
||||
|
||||
const TCP_USER_TIMEOUT = 18
|
||||
const TCP_KEEPIDLE = 4
|
||||
const TCP_KEEPINTVL = 5
|
||||
const TCP_KEEPCNT = 6
|
||||
|
||||
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_USER_TIMEOUT, userTimeout); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_USER_TIMEOUT", "error", err)
|
||||
}
|
||||
|
||||
if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, SO_KEEPALIVE_ENABLE); err != nil {
|
||||
sockoptErr = fmt.Errorf("failed to enable SO_KEEPALIVE: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_KEEPIDLE, probeAfter); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_KEEPIDLE", "error", err)
|
||||
}
|
||||
|
||||
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_KEEPINTVL, probeInterval); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_KEEPINTVL", "error", err)
|
||||
}
|
||||
|
||||
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_KEEPCNT, probeCount); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_KEEPCNT", "error", err)
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("control failed: %v", err)
|
||||
}
|
||||
if sockoptErr != nil {
|
||||
return sockoptErr
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "TCP keepalive configured (Linux)", "i2p", tc.i2pTunneled)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||
return tc.setTimeoutsLinux()
|
||||
}
|
||||
|
||||
func platformGetRTT(fd uintptr) time.Duration {
|
||||
var info syscall.TCPInfo
|
||||
size := uint32(syscall.SizeofTCPInfo)
|
||||
// bearer:disable go_gosec_unsafe_unsafe
|
||||
infoLen := uint32(unsafe.Sizeof(info))
|
||||
|
||||
_, _, err := syscall.Syscall6(
|
||||
const TCP_INFO = 11
|
||||
// #nosec G103
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_GETSOCKOPT,
|
||||
fd,
|
||||
syscall.SOL_TCP,
|
||||
syscall.TCP_INFO,
|
||||
syscall.IPPROTO_TCP,
|
||||
TCP_INFO,
|
||||
// bearer:disable go_gosec_unsafe_unsafe
|
||||
uintptr(unsafe.Pointer(&info)),
|
||||
uintptr(unsafe.Pointer(&size)),
|
||||
// bearer:disable go_gosec_unsafe_unsafe
|
||||
uintptr(unsafe.Pointer(&infoLen)),
|
||||
0,
|
||||
)
|
||||
|
||||
if err != 0 {
|
||||
if errno != 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// RTT is in microseconds, convert to Duration
|
||||
return time.Duration(info.Rtt) * time.Microsecond
|
||||
}
|
||||
}
|
||||
|
||||
41
pkg/interfaces/tcp_netbsd.go
Normal file
41
pkg/interfaces/tcp_netbsd.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build netbsd
|
||||
// +build netbsd
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlive(true); err != nil {
|
||||
return fmt.Errorf("failed to enable keepalive: %v", err)
|
||||
}
|
||||
|
||||
keepalivePeriod := TCP_PROBE_INTERVAL_SEC * time.Second
|
||||
if tc.i2pTunneled {
|
||||
keepalivePeriod = I2P_PROBE_INTERVAL_SEC * time.Second
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlivePeriod(keepalivePeriod); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set keepalive period", "error", err)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "TCP keepalive configured (NetBSD)", "i2p", tc.i2pTunneled)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||
return tc.setTimeoutsLinux()
|
||||
}
|
||||
41
pkg/interfaces/tcp_openbsd.go
Normal file
41
pkg/interfaces/tcp_openbsd.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build openbsd
|
||||
// +build openbsd
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlive(true); err != nil {
|
||||
return fmt.Errorf("failed to enable keepalive: %v", err)
|
||||
}
|
||||
|
||||
keepalivePeriod := TCP_PROBE_INTERVAL_SEC * time.Second
|
||||
if tc.i2pTunneled {
|
||||
keepalivePeriod = I2P_PROBE_INTERVAL_SEC * time.Second
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlivePeriod(keepalivePeriod); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set keepalive period", "error", err)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "TCP keepalive configured (OpenBSD)", "i2p", tc.i2pTunneled)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||
return tc.setTimeoutsLinux()
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
14
pkg/interfaces/tcp_wasm.go
Normal file
14
pkg/interfaces/tcp_wasm.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build js && wasm
|
||||
// +build js,wasm
|
||||
|
||||
package interfaces
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||
return nil
|
||||
}
|
||||
42
pkg/interfaces/tcp_windows.go
Normal file
42
pkg/interfaces/tcp_windows.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||
return tc.setTimeoutsWindows()
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||
return tc.setTimeoutsWindows()
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsWindows() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlive(true); err != nil {
|
||||
return fmt.Errorf("failed to enable keepalive: %v", err)
|
||||
}
|
||||
|
||||
keepalivePeriod := TCP_PROBE_INTERVAL_SEC * time.Second
|
||||
if tc.i2pTunneled {
|
||||
keepalivePeriod = I2P_PROBE_INTERVAL_SEC * time.Second
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlivePeriod(keepalivePeriod); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set keepalive period", "error", err)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "TCP keepalive configured (Windows)", "i2p", tc.i2pTunneled)
|
||||
return nil
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
type UDPInterface struct {
|
||||
@@ -14,8 +16,9 @@ type UDPInterface struct {
|
||||
conn *net.UDPConn
|
||||
addr *net.UDPAddr
|
||||
targetAddr *net.UDPAddr
|
||||
mutex sync.RWMutex
|
||||
readBuffer []byte
|
||||
done chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) {
|
||||
@@ -36,9 +39,12 @@ func NewUDPInterface(name string, addr string, target string, enabled bool) (*UD
|
||||
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
|
||||
addr: udpAddr,
|
||||
targetAddr: targetAddr,
|
||||
readBuffer: make([]byte, common.DEFAULT_MTU),
|
||||
readBuffer: make([]byte, common.NUM_1064),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
ui.MTU = common.NUM_1064
|
||||
|
||||
return ui, nil
|
||||
}
|
||||
|
||||
@@ -55,27 +61,35 @@ func (ui *UDPInterface) GetMode() common.InterfaceMode {
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) IsOnline() bool {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
ui.Mutex.RLock()
|
||||
defer ui.Mutex.RUnlock()
|
||||
return ui.Online
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) IsDetached() bool {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
ui.Mutex.RLock()
|
||||
defer ui.Mutex.RUnlock()
|
||||
return ui.Detached
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Detach() {
|
||||
ui.mutex.Lock()
|
||||
defer ui.mutex.Unlock()
|
||||
ui.Mutex.Lock()
|
||||
defer ui.Mutex.Unlock()
|
||||
ui.Detached = true
|
||||
ui.Online = false
|
||||
if ui.conn != nil {
|
||||
ui.conn.Close()
|
||||
ui.conn.Close() // #nosec G104
|
||||
}
|
||||
ui.stopOnce.Do(func() {
|
||||
if ui.done != nil {
|
||||
close(ui.done)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Send(data []byte, addr string) error {
|
||||
debug.Log(debug.DEBUG_ALL, "UDP interface sending bytes", "name", ui.Name, "bytes", len(data))
|
||||
|
||||
if !ui.IsEnabled() {
|
||||
return fmt.Errorf("interface not enabled")
|
||||
}
|
||||
@@ -84,19 +98,28 @@ func (ui *UDPInterface) Send(data []byte, addr string) error {
|
||||
return fmt.Errorf("no target address configured")
|
||||
}
|
||||
|
||||
ui.Mutex.Lock()
|
||||
ui.TxBytes += uint64(len(data))
|
||||
ui.Mutex.Unlock()
|
||||
|
||||
_, err := ui.conn.WriteTo(data, ui.targetAddr)
|
||||
if err != nil {
|
||||
debug.Log(debug.DEBUG_CRITICAL, "UDP interface write failed", "name", ui.Name, "error", err)
|
||||
} else {
|
||||
debug.Log(debug.DEBUG_ALL, "UDP interface sent bytes successfully", "name", ui.Name, "bytes", len(data))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||
ui.mutex.Lock()
|
||||
defer ui.mutex.Unlock()
|
||||
ui.Mutex.Lock()
|
||||
defer ui.Mutex.Unlock()
|
||||
ui.packetCallback = callback
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetPacketCallback() common.PacketCallback {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
ui.Mutex.RLock()
|
||||
defer ui.Mutex.RUnlock()
|
||||
return ui.packetCallback
|
||||
}
|
||||
|
||||
@@ -120,9 +143,9 @@ func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
|
||||
return fmt.Errorf("UDP write failed: %v", err)
|
||||
}
|
||||
|
||||
ui.mutex.Lock()
|
||||
ui.Mutex.Lock()
|
||||
ui.TxBytes += uint64(len(data))
|
||||
ui.mutex.Unlock()
|
||||
ui.Mutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -132,14 +155,14 @@ func (ui *UDPInterface) GetConn() net.Conn {
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetTxBytes() uint64 {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
ui.Mutex.RLock()
|
||||
defer ui.Mutex.RUnlock()
|
||||
return ui.TxBytes
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetRxBytes() uint64 {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
ui.Mutex.RLock()
|
||||
defer ui.Mutex.RUnlock()
|
||||
return ui.RxBytes
|
||||
}
|
||||
|
||||
@@ -152,56 +175,119 @@ func (ui *UDPInterface) GetBitrate() int {
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Enable() {
|
||||
ui.mutex.Lock()
|
||||
defer ui.mutex.Unlock()
|
||||
ui.Mutex.Lock()
|
||||
defer ui.Mutex.Unlock()
|
||||
ui.Online = true
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Disable() {
|
||||
ui.mutex.Lock()
|
||||
defer ui.mutex.Unlock()
|
||||
ui.Mutex.Lock()
|
||||
defer ui.Mutex.Unlock()
|
||||
ui.Online = false
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Start() error {
|
||||
ui.Mutex.Lock()
|
||||
if ui.conn != nil {
|
||||
ui.Mutex.Unlock()
|
||||
return fmt.Errorf("UDP interface already started")
|
||||
}
|
||||
// Only recreate done if it's nil or was closed
|
||||
select {
|
||||
case <-ui.done:
|
||||
ui.done = make(chan struct{})
|
||||
ui.stopOnce = sync.Once{}
|
||||
default:
|
||||
if ui.done == nil {
|
||||
ui.done = make(chan struct{})
|
||||
ui.stopOnce = sync.Once{}
|
||||
}
|
||||
}
|
||||
ui.Mutex.Unlock()
|
||||
|
||||
conn, err := net.ListenUDP("udp", ui.addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ui.conn = conn
|
||||
|
||||
// Enable broadcast mode if we have a target address
|
||||
if ui.targetAddr != nil {
|
||||
// Get the raw connection file descriptor to set SO_BROADCAST
|
||||
if err := conn.SetReadBuffer(common.NUM_1064); err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to set read buffer size", "error", err)
|
||||
}
|
||||
if err := conn.SetWriteBuffer(common.NUM_1064); err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to set write buffer size", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
ui.Mutex.Lock()
|
||||
ui.Online = true
|
||||
ui.Mutex.Unlock()
|
||||
|
||||
// Start the read loop in a goroutine
|
||||
go ui.readLoop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Stop() error {
|
||||
ui.Detach()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) readLoop() {
|
||||
buffer := make([]byte, ui.MTU)
|
||||
buffer := make([]byte, common.NUM_1064)
|
||||
for {
|
||||
if ui.IsDetached() {
|
||||
ui.Mutex.RLock()
|
||||
online := ui.Online
|
||||
detached := ui.Detached
|
||||
conn := ui.conn
|
||||
done := ui.done
|
||||
ui.Mutex.RUnlock()
|
||||
|
||||
if !online || detached || conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
n, addr, err := ui.conn.ReadFromUDP(buffer)
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
n, remoteAddr, err := conn.ReadFromUDP(buffer)
|
||||
if err != nil {
|
||||
if !ui.IsDetached() {
|
||||
log.Printf("UDP read error: %v", err)
|
||||
ui.Mutex.RLock()
|
||||
stillOnline := ui.Online
|
||||
ui.Mutex.RUnlock()
|
||||
if stillOnline {
|
||||
debug.Log(debug.DEBUG_ERROR, "Error reading from UDP interface", "name", ui.Name, "error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ui.mutex.Lock()
|
||||
ui.Mutex.Lock()
|
||||
// #nosec G115 - Network read sizes are always positive and within safe range
|
||||
ui.RxBytes += uint64(n)
|
||||
ui.mutex.Unlock()
|
||||
|
||||
log.Printf("Received %d bytes from %s", n, addr.String())
|
||||
// Auto-discover target address from first packet if not set
|
||||
if ui.targetAddr == nil {
|
||||
debug.Log(debug.DEBUG_ALL, "UDP interface discovered peer", "name", ui.Name, "peer", remoteAddr.String())
|
||||
ui.targetAddr = remoteAddr
|
||||
}
|
||||
callback := ui.packetCallback
|
||||
ui.Mutex.Unlock()
|
||||
|
||||
if callback := ui.GetPacketCallback(); callback != nil {
|
||||
if callback != nil {
|
||||
callback(buffer[:n], ui)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) IsEnabled() bool {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
ui.Mutex.RLock()
|
||||
defer ui.Mutex.RUnlock()
|
||||
return ui.Enabled && ui.Online && !ui.Detached
|
||||
}
|
||||
|
||||
87
pkg/interfaces/udp_test.go
Normal file
87
pkg/interfaces/udp_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.quad4.io/Networks/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.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
|
||||
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.
|
||||
}
|
||||
714
pkg/interfaces/websocket_native.go
Normal file
714
pkg/interfaces/websocket_native.go
Normal file
@@ -0,0 +1,714 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
// WebSocketInterface is a native implementation of the WebSocket interface.
|
||||
// It is used to connect to the WebSocket server and send/receive data.
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
// bearer:disable go_gosec_blocklist_sha1
|
||||
"crypto/sha1" // #nosec G505
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
const (
|
||||
wsGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
|
||||
WS_BUFFER_SIZE = 4096
|
||||
WS_MTU = 1064
|
||||
WS_BITRATE = 10000000
|
||||
WS_HTTPS_PORT = 443
|
||||
WS_HTTP_PORT = 80
|
||||
WS_VERSION = "13"
|
||||
WS_CONNECT_TIMEOUT = 10 * time.Second
|
||||
WS_RECONNECT_DELAY = 2 * time.Second
|
||||
WS_KEY_SIZE = 16
|
||||
WS_MASK_KEY_SIZE = 4
|
||||
WS_HEADER_SIZE = 2
|
||||
WS_PAYLOAD_LEN_16BIT = 126
|
||||
WS_PAYLOAD_LEN_64BIT = 127
|
||||
WS_MAX_PAYLOAD_16BIT = 65536
|
||||
WS_FRAME_HEADER_FIN = 0x80
|
||||
WS_FRAME_HEADER_OPCODE = 0x0F
|
||||
WS_FRAME_HEADER_MASKED = 0x80
|
||||
WS_FRAME_HEADER_LEN = 0x7F
|
||||
WS_OPCODE_CONTINUATION = 0x00
|
||||
WS_OPCODE_TEXT = 0x01
|
||||
WS_OPCODE_BINARY = 0x02
|
||||
WS_OPCODE_CLOSE = 0x08
|
||||
WS_OPCODE_PING = 0x09
|
||||
WS_OPCODE_PONG = 0x0A
|
||||
)
|
||||
|
||||
type WebSocketInterface struct {
|
||||
BaseInterface
|
||||
wsURL string
|
||||
conn net.Conn
|
||||
reader *bufio.Reader
|
||||
connected bool
|
||||
messageQueue [][]byte
|
||||
readBuffer []byte
|
||||
writeBuffer []byte
|
||||
done chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func NewWebSocketInterface(name string, wsURL string, enabled bool) (*WebSocketInterface, error) {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "NewWebSocketInterface called", "name", name, "url", wsURL, "enabled", enabled)
|
||||
ws := &WebSocketInterface{
|
||||
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
|
||||
wsURL: wsURL,
|
||||
messageQueue: make([][]byte, 0),
|
||||
readBuffer: make([]byte, WS_BUFFER_SIZE),
|
||||
writeBuffer: make([]byte, WS_BUFFER_SIZE),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
ws.MTU = WS_MTU
|
||||
ws.Bitrate = WS_BITRATE
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "WebSocket interface initialized", "name", name, "mtu", ws.MTU, "bitrate", ws.Bitrate)
|
||||
return ws, nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetName() string {
|
||||
return wsi.Name
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetType() common.InterfaceType {
|
||||
return wsi.Type
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetMode() common.InterfaceMode {
|
||||
return wsi.Mode
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) IsOnline() bool {
|
||||
wsi.Mutex.RLock()
|
||||
defer wsi.Mutex.RUnlock()
|
||||
return wsi.Online && wsi.connected
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) IsDetached() bool {
|
||||
wsi.Mutex.RLock()
|
||||
defer wsi.Mutex.RUnlock()
|
||||
return wsi.Detached
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Detach() {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
wsi.Detached = true
|
||||
wsi.Online = false
|
||||
wsi.closeWebSocketLocked()
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Enable() {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
wsi.Enabled = true
|
||||
wsi.Online = true
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Disable() {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
wsi.Enabled = false
|
||||
wsi.closeWebSocketLocked()
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Start() error {
|
||||
wsi.Mutex.Lock()
|
||||
if !wsi.Enabled || wsi.Detached {
|
||||
wsi.Mutex.Unlock()
|
||||
debug.Log(debug.DEBUG_INFO, "WebSocket interface not enabled or detached", "name", wsi.Name)
|
||||
return fmt.Errorf("interface not enabled or detached")
|
||||
}
|
||||
if wsi.conn != nil {
|
||||
wsi.Mutex.Unlock()
|
||||
debug.Log(debug.DEBUG_INFO, "WebSocket already started", "name", wsi.Name)
|
||||
return fmt.Errorf("WebSocket already started")
|
||||
}
|
||||
// Only recreate done if it's nil or was closed
|
||||
select {
|
||||
case <-wsi.done:
|
||||
wsi.done = make(chan struct{})
|
||||
wsi.stopOnce = sync.Once{}
|
||||
default:
|
||||
if wsi.done == nil {
|
||||
wsi.done = make(chan struct{})
|
||||
wsi.stopOnce = sync.Once{}
|
||||
}
|
||||
}
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "Starting WebSocket connection", "name", wsi.Name, "url", wsi.wsURL)
|
||||
|
||||
u, err := url.Parse(wsi.wsURL)
|
||||
if err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Invalid WebSocket URL", "name", wsi.Name, "url", wsi.wsURL, "error", err)
|
||||
return fmt.Errorf("invalid WebSocket URL: %v", err)
|
||||
}
|
||||
|
||||
var conn net.Conn
|
||||
var host string
|
||||
|
||||
if u.Scheme == "wss" {
|
||||
host = u.Host
|
||||
if !strings.Contains(host, ":") {
|
||||
host += fmt.Sprintf(":%d", WS_HTTPS_PORT)
|
||||
}
|
||||
tcpConn, err := net.DialTimeout("tcp", host, WS_CONNECT_TIMEOUT)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect: %v", err)
|
||||
}
|
||||
tlsConn := tls.Client(tcpConn, &tls.Config{
|
||||
ServerName: u.Hostname(),
|
||||
InsecureSkipVerify: false,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
})
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
_ = tcpConn.Close()
|
||||
debug.Log(debug.DEBUG_ERROR, "TLS handshake failed", "name", wsi.Name, "host", host, "error", err)
|
||||
return fmt.Errorf("TLS handshake failed: %v", err)
|
||||
}
|
||||
conn = tlsConn
|
||||
} else if u.Scheme == "ws" {
|
||||
host = u.Host
|
||||
if !strings.Contains(host, ":") {
|
||||
host += fmt.Sprintf(":%d", WS_HTTP_PORT)
|
||||
}
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Connecting to WebSocket server", "name", wsi.Name, "host", host)
|
||||
tcpConn, err := net.DialTimeout("tcp", host, WS_CONNECT_TIMEOUT)
|
||||
if err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to connect to WebSocket server", "name", wsi.Name, "host", host, "error", err)
|
||||
return fmt.Errorf("failed to connect: %v", err)
|
||||
}
|
||||
conn = tcpConn
|
||||
} else {
|
||||
debug.Log(debug.DEBUG_ERROR, "Unsupported WebSocket scheme", "name", wsi.Name, "scheme", u.Scheme)
|
||||
return fmt.Errorf("unsupported scheme: %s (use ws:// or wss://)", u.Scheme)
|
||||
}
|
||||
|
||||
key, err := generateWebSocketKey()
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return fmt.Errorf("failed to generate key: %v", err)
|
||||
}
|
||||
|
||||
path := u.Path
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
if u.RawQuery != "" {
|
||||
path += "?" + u.RawQuery
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", path, nil)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Host = u.Host
|
||||
req.Header.Set("Upgrade", "websocket")
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
req.Header.Set("Sec-WebSocket-Key", key)
|
||||
req.Header.Set("Sec-WebSocket-Version", WS_VERSION)
|
||||
req.Header.Set("User-Agent", "Reticulum-Go/1.0")
|
||||
|
||||
if err := req.Write(conn); err != nil {
|
||||
_ = conn.Close()
|
||||
return fmt.Errorf("failed to send handshake: %v", err)
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(bufio.NewReader(conn), req)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return fmt.Errorf("failed to read handshake response: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||
_ = conn.Close()
|
||||
debug.Log(debug.DEBUG_ERROR, "WebSocket handshake failed", "name", wsi.Name, "status", resp.StatusCode)
|
||||
return fmt.Errorf("handshake failed: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" {
|
||||
_ = conn.Close()
|
||||
return fmt.Errorf("invalid upgrade header")
|
||||
}
|
||||
|
||||
accept := resp.Header.Get("Sec-WebSocket-Accept")
|
||||
expectedAccept := computeAcceptKey(key)
|
||||
if accept != expectedAccept {
|
||||
_ = conn.Close()
|
||||
return fmt.Errorf("invalid accept key")
|
||||
}
|
||||
|
||||
wsi.Mutex.Lock()
|
||||
wsi.conn = conn
|
||||
wsi.reader = bufio.NewReader(conn)
|
||||
wsi.connected = true
|
||||
wsi.Online = true
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "WebSocket connected", "name", wsi.Name, "url", wsi.wsURL)
|
||||
|
||||
queue := make([][]byte, len(wsi.messageQueue))
|
||||
copy(queue, wsi.messageQueue)
|
||||
wsi.messageQueue = wsi.messageQueue[:0]
|
||||
wsi.Mutex.Unlock() // Unlock after copying queue, before I/O
|
||||
|
||||
for _, msg := range queue {
|
||||
_ = wsi.sendWebSocketMessage(msg)
|
||||
}
|
||||
|
||||
go wsi.readLoop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Stop() error {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
|
||||
wsi.Enabled = false
|
||||
wsi.Online = false
|
||||
|
||||
wsi.stopOnce.Do(func() {
|
||||
if wsi.done != nil {
|
||||
close(wsi.done)
|
||||
}
|
||||
})
|
||||
|
||||
wsi.closeWebSocketLocked()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) closeWebSocket() {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
wsi.closeWebSocketLocked()
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) closeWebSocketLocked() {
|
||||
if wsi.conn != nil {
|
||||
wsi.sendCloseFrameLocked()
|
||||
_ = wsi.conn.Close()
|
||||
wsi.conn = nil
|
||||
wsi.reader = nil
|
||||
}
|
||||
wsi.connected = false
|
||||
wsi.Online = false
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) readLoop() {
|
||||
for {
|
||||
wsi.Mutex.RLock()
|
||||
conn := wsi.conn
|
||||
reader := wsi.reader
|
||||
done := wsi.done
|
||||
wsi.Mutex.RUnlock()
|
||||
|
||||
if conn == nil || reader == nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
data, err := wsi.readFrame()
|
||||
if err != nil {
|
||||
wsi.Mutex.Lock()
|
||||
wsi.connected = false
|
||||
wsi.Online = false
|
||||
if wsi.conn != nil {
|
||||
_ = wsi.conn.Close()
|
||||
wsi.conn = nil
|
||||
wsi.reader = nil
|
||||
}
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "WebSocket closed", "name", wsi.Name, "error", err)
|
||||
|
||||
time.Sleep(WS_RECONNECT_DELAY)
|
||||
|
||||
wsi.Mutex.RLock()
|
||||
stillEnabled := wsi.Enabled && !wsi.Detached
|
||||
wsi.Mutex.RUnlock()
|
||||
|
||||
if stillEnabled {
|
||||
go wsi.Start()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(data) > 0 {
|
||||
wsi.Mutex.Lock()
|
||||
wsi.RxBytes += uint64(len(data))
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
wsi.ProcessIncoming(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) readFrame() ([]byte, error) {
|
||||
wsi.Mutex.RLock()
|
||||
reader := wsi.reader
|
||||
wsi.Mutex.RUnlock()
|
||||
|
||||
if reader == nil {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
header := make([]byte, WS_HEADER_SIZE)
|
||||
if _, err := io.ReadFull(reader, header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fin := (header[0] & WS_FRAME_HEADER_FIN) != 0
|
||||
opcode := header[0] & WS_FRAME_HEADER_OPCODE
|
||||
masked := (header[1] & WS_FRAME_HEADER_MASKED) != 0
|
||||
payloadLen := int(header[1] & WS_FRAME_HEADER_LEN)
|
||||
|
||||
if opcode == WS_OPCODE_CLOSE {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
if opcode == WS_OPCODE_PING {
|
||||
return wsi.handlePingFrame(reader, payloadLen, masked)
|
||||
}
|
||||
|
||||
if opcode == WS_OPCODE_PONG {
|
||||
return wsi.handlePongFrame(reader, payloadLen, masked)
|
||||
}
|
||||
|
||||
if opcode != WS_OPCODE_BINARY {
|
||||
return nil, fmt.Errorf("unsupported opcode: %d", opcode)
|
||||
}
|
||||
|
||||
if payloadLen == WS_PAYLOAD_LEN_16BIT {
|
||||
lenBytes := make([]byte, 2)
|
||||
if _, err := io.ReadFull(reader, lenBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payloadLen = int(binary.BigEndian.Uint16(lenBytes))
|
||||
} else if payloadLen == WS_PAYLOAD_LEN_64BIT {
|
||||
lenBytes := make([]byte, 8)
|
||||
if _, err := io.ReadFull(reader, lenBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val := binary.BigEndian.Uint64(lenBytes)
|
||||
if val > uint64(math.MaxInt) {
|
||||
return nil, fmt.Errorf("payload length exceeds maximum integer value")
|
||||
}
|
||||
payloadLen = int(val) // #nosec G115
|
||||
}
|
||||
|
||||
maskKey := make([]byte, WS_MASK_KEY_SIZE)
|
||||
if masked {
|
||||
if _, err := io.ReadFull(reader, maskKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
payload := make([]byte, payloadLen)
|
||||
if _, err := io.ReadFull(reader, payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if masked {
|
||||
for i := 0; i < payloadLen; i++ {
|
||||
payload[i] ^= maskKey[i%WS_MASK_KEY_SIZE]
|
||||
}
|
||||
}
|
||||
|
||||
if !fin {
|
||||
nextFrame, err := wsi.readFrame()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(payload, nextFrame...), nil
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Send(data []byte, addr string) error {
|
||||
wsi.Mutex.RLock()
|
||||
enabled := wsi.Enabled
|
||||
detached := wsi.Detached
|
||||
connected := wsi.connected
|
||||
wsi.Mutex.RUnlock()
|
||||
|
||||
if !enabled || detached {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "WebSocket interface not enabled or detached, dropping packet", "name", wsi.Name, "bytes", len(data))
|
||||
return fmt.Errorf("interface not enabled")
|
||||
}
|
||||
|
||||
wsi.Mutex.Lock()
|
||||
wsi.TxBytes += uint64(len(data))
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
if !connected {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "WebSocket not connected, queuing packet", "name", wsi.Name, "bytes", len(data), "queue_size", len(wsi.messageQueue))
|
||||
wsi.Mutex.Lock()
|
||||
wsi.messageQueue = append(wsi.messageQueue, data)
|
||||
wsi.Mutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
packetType := "unknown"
|
||||
if len(data) > 0 {
|
||||
switch data[0] {
|
||||
case 0x01:
|
||||
packetType = "announce"
|
||||
case 0x02:
|
||||
packetType = "link"
|
||||
default:
|
||||
packetType = fmt.Sprintf("0x%02x", data[0])
|
||||
}
|
||||
}
|
||||
debug.Log(debug.DEBUG_INFO, "Sending packet over WebSocket", "name", wsi.Name, "bytes", len(data), "packet_type", packetType)
|
||||
return wsi.sendWebSocketMessage(data)
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) sendWebSocketMessage(data []byte) error {
|
||||
wsi.Mutex.RLock()
|
||||
conn := wsi.conn
|
||||
wsi.Mutex.RUnlock()
|
||||
|
||||
if conn == nil {
|
||||
return fmt.Errorf("WebSocket not initialized")
|
||||
}
|
||||
|
||||
frame := wsi.createFrame(data, WS_OPCODE_BINARY, true)
|
||||
wsi.Mutex.Lock()
|
||||
_, err := conn.Write(frame)
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send: %v", err)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "WebSocket sent packet successfully", "name", wsi.Name, "bytes", len(data), "frame_bytes", len(frame))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) sendCloseFrame() {
|
||||
wsi.Mutex.RLock()
|
||||
defer wsi.Mutex.RUnlock()
|
||||
wsi.sendCloseFrameLocked()
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) sendCloseFrameLocked() {
|
||||
conn := wsi.conn
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
frame := wsi.createFrame(nil, WS_OPCODE_CLOSE, true)
|
||||
_, _ = conn.Write(frame)
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) handlePingFrame(reader *bufio.Reader, payloadLen int, masked bool) ([]byte, error) {
|
||||
if payloadLen == WS_PAYLOAD_LEN_16BIT {
|
||||
lenBytes := make([]byte, 2)
|
||||
if _, err := io.ReadFull(reader, lenBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payloadLen = int(binary.BigEndian.Uint16(lenBytes))
|
||||
} else if payloadLen == WS_PAYLOAD_LEN_64BIT {
|
||||
lenBytes := make([]byte, 8)
|
||||
if _, err := io.ReadFull(reader, lenBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val := binary.BigEndian.Uint64(lenBytes)
|
||||
if val > uint64(math.MaxInt) {
|
||||
return nil, fmt.Errorf("payload length exceeds maximum integer value")
|
||||
}
|
||||
payloadLen = int(val) // #nosec G115
|
||||
}
|
||||
|
||||
maskKey := make([]byte, WS_MASK_KEY_SIZE)
|
||||
if masked {
|
||||
if _, err := io.ReadFull(reader, maskKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
payload := make([]byte, payloadLen)
|
||||
if payloadLen > 0 {
|
||||
if _, err := io.ReadFull(reader, payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if masked {
|
||||
for i := 0; i < payloadLen; i++ {
|
||||
payload[i] ^= maskKey[i%WS_MASK_KEY_SIZE]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wsi.sendPongFrame(payload)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) handlePongFrame(reader *bufio.Reader, payloadLen int, masked bool) ([]byte, error) {
|
||||
if payloadLen == WS_PAYLOAD_LEN_16BIT {
|
||||
lenBytes := make([]byte, 2)
|
||||
if _, err := io.ReadFull(reader, lenBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payloadLen = int(binary.BigEndian.Uint16(lenBytes))
|
||||
} else if payloadLen == WS_PAYLOAD_LEN_64BIT {
|
||||
lenBytes := make([]byte, 8)
|
||||
if _, err := io.ReadFull(reader, lenBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val := binary.BigEndian.Uint64(lenBytes)
|
||||
if val > uint64(math.MaxInt) {
|
||||
return nil, fmt.Errorf("payload length exceeds maximum integer value")
|
||||
}
|
||||
payloadLen = int(val) // #nosec G115
|
||||
}
|
||||
|
||||
maskKey := make([]byte, WS_MASK_KEY_SIZE)
|
||||
if masked {
|
||||
if _, err := io.ReadFull(reader, maskKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if payloadLen > 0 {
|
||||
payload := make([]byte, payloadLen)
|
||||
if _, err := io.ReadFull(reader, payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) sendPongFrame(data []byte) {
|
||||
wsi.Mutex.RLock()
|
||||
conn := wsi.conn
|
||||
wsi.Mutex.RUnlock()
|
||||
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
frame := wsi.createFrame(data, WS_OPCODE_PONG, true)
|
||||
wsi.Mutex.Lock()
|
||||
_, _ = conn.Write(frame)
|
||||
wsi.Mutex.Unlock()
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) createFrame(data []byte, opcode byte, fin bool) []byte {
|
||||
payloadLen := len(data)
|
||||
frame := make([]byte, WS_HEADER_SIZE)
|
||||
|
||||
if fin {
|
||||
frame[0] |= WS_FRAME_HEADER_FIN
|
||||
}
|
||||
frame[0] |= opcode
|
||||
|
||||
if payloadLen < WS_PAYLOAD_LEN_16BIT {
|
||||
frame[1] = byte(payloadLen)
|
||||
frame = append(frame, data...)
|
||||
} else if payloadLen < WS_MAX_PAYLOAD_16BIT {
|
||||
frame[1] = WS_PAYLOAD_LEN_16BIT // #nosec G602
|
||||
lenBytes := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(lenBytes, uint16(payloadLen)) // #nosec G115
|
||||
frame = append(frame, lenBytes...)
|
||||
frame = append(frame, data...)
|
||||
} else {
|
||||
frame[1] = WS_PAYLOAD_LEN_64BIT // #nosec G602
|
||||
lenBytes := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(lenBytes, uint64(payloadLen)) // #nosec G115
|
||||
frame = append(frame, lenBytes...)
|
||||
frame = append(frame, data...)
|
||||
}
|
||||
|
||||
return frame
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) ProcessOutgoing(data []byte) error {
|
||||
return wsi.Send(data, "")
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetConn() net.Conn {
|
||||
wsi.Mutex.RLock()
|
||||
defer wsi.Mutex.RUnlock()
|
||||
return wsi.conn
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetMTU() int {
|
||||
return wsi.MTU
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) IsEnabled() bool {
|
||||
wsi.Mutex.RLock()
|
||||
defer wsi.Mutex.RUnlock()
|
||||
return wsi.Enabled && wsi.Online && !wsi.Detached
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) SendPathRequest(packet []byte) error {
|
||||
return wsi.Send(packet, "")
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.Time) error {
|
||||
frame := make([]byte, 0, len(dest)+len(data)+9)
|
||||
frame = append(frame, WS_OPCODE_BINARY)
|
||||
frame = append(frame, dest...)
|
||||
ts := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) // #nosec G115
|
||||
frame = append(frame, ts...)
|
||||
frame = append(frame, data...)
|
||||
return wsi.Send(frame, "")
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetBandwidthAvailable() bool {
|
||||
return wsi.BaseInterface.GetBandwidthAvailable()
|
||||
}
|
||||
|
||||
func generateWebSocketKey() (string, error) {
|
||||
key := make([]byte, WS_KEY_SIZE)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(key), nil
|
||||
}
|
||||
|
||||
func computeAcceptKey(key string) string {
|
||||
// bearer:disable go_gosec_crypto_weak_crypto
|
||||
h := sha1.New() // #nosec G401
|
||||
h.Write([]byte(key))
|
||||
h.Write([]byte(wsGUID))
|
||||
// bearer:disable go_lang_weak_hash_sha1
|
||||
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
280
pkg/interfaces/websocket_native_test.go
Normal file
280
pkg/interfaces/websocket_native_test.go
Normal file
@@ -0,0 +1,280 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
)
|
||||
|
||||
func TestWebSocketGUID(t *testing.T) {
|
||||
if wsGUID != "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" {
|
||||
t.Errorf("wsGUID mismatch: expected RFC 6455 GUID, got %s", wsGUID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWebSocketKey(t *testing.T) {
|
||||
key1, err := generateWebSocketKey()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate key: %v", err)
|
||||
}
|
||||
|
||||
key2, err := generateWebSocketKey()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate key: %v", err)
|
||||
}
|
||||
|
||||
if key1 == key2 {
|
||||
t.Error("Generated keys should be unique")
|
||||
}
|
||||
|
||||
if len(key1) != 24 {
|
||||
t.Errorf("Expected base64-encoded key length 24, got %d", len(key1))
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeAcceptKey(t *testing.T) {
|
||||
testKey := "dGhlIHNhbXBsZSBub25jZQ=="
|
||||
expectedAccept := "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
|
||||
|
||||
accept := computeAcceptKey(testKey)
|
||||
if accept != expectedAccept {
|
||||
t.Errorf("Accept key mismatch: expected %s, got %s", expectedAccept, accept)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWebSocketInterface(t *testing.T) {
|
||||
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||
}
|
||||
|
||||
if ws.GetName() != "test" {
|
||||
t.Errorf("Expected name 'test', got %s", ws.GetName())
|
||||
}
|
||||
|
||||
if ws.GetType() != common.IF_TYPE_UDP {
|
||||
t.Errorf("Expected type IF_TYPE_UDP, got %v", ws.GetType())
|
||||
}
|
||||
|
||||
if ws.GetMTU() != 1064 {
|
||||
t.Errorf("Expected MTU 1064, got %d", ws.GetMTU())
|
||||
}
|
||||
|
||||
if ws.IsOnline() {
|
||||
t.Error("Interface should not be online before Start()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebSocketConnection(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping network test in short mode")
|
||||
}
|
||||
|
||||
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||
}
|
||||
|
||||
ws.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
|
||||
t.Logf("Received packet: %d bytes", len(data))
|
||||
})
|
||||
|
||||
err = ws.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start WebSocket: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
if !ws.IsOnline() {
|
||||
t.Error("WebSocket should be online after Start()")
|
||||
}
|
||||
|
||||
testData := []byte{0x01, 0x02, 0x03, 0x04}
|
||||
err = ws.Send(testData, "")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to send data: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
if err := ws.Stop(); err != nil {
|
||||
t.Errorf("Failed to stop WebSocket: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
if ws.IsOnline() {
|
||||
t.Error("WebSocket should be offline after Stop()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebSocketReconnection(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping network test in short mode")
|
||||
}
|
||||
|
||||
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||
}
|
||||
|
||||
err = ws.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start WebSocket: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
if !ws.IsOnline() {
|
||||
t.Error("WebSocket should be online")
|
||||
}
|
||||
|
||||
conn := ws.GetConn()
|
||||
if conn == nil {
|
||||
t.Error("GetConn() should return a connection")
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
if ws.IsOnline() {
|
||||
t.Log("WebSocket reconnected successfully")
|
||||
}
|
||||
|
||||
if err := ws.Stop(); err != nil {
|
||||
t.Errorf("Failed to stop WebSocket: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestWebSocketMessageQueue(t *testing.T) {
|
||||
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||
}
|
||||
|
||||
ws.Enable()
|
||||
|
||||
testData := []byte{0x01, 0x02, 0x03}
|
||||
err = ws.Send(testData, "")
|
||||
if err != nil {
|
||||
t.Errorf("Send should queue message when offline, got error: %v", err)
|
||||
}
|
||||
|
||||
if testing.Short() {
|
||||
return
|
||||
}
|
||||
|
||||
err = ws.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start WebSocket: %v", err)
|
||||
}
|
||||
|
||||
// Wait for interface to be online (up to 10 seconds)
|
||||
for i := 0; i < 100; i++ {
|
||||
if ws.IsOnline() {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
if !ws.IsOnline() {
|
||||
t.Error("WebSocket should be online")
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
if err := ws.Stop(); err != nil {
|
||||
t.Errorf("Failed to stop WebSocket: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestWebSocketFrameEncoding(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping frame encoding test in short mode")
|
||||
}
|
||||
|
||||
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||
}
|
||||
|
||||
err = ws.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start WebSocket: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
data []byte
|
||||
}{
|
||||
{"small frame", []byte{0x01, 0x02, 0x03}},
|
||||
{"medium frame", make([]byte, 200)},
|
||||
{"large frame", make([]byte, 1000)},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := ws.Send(tc.data, "")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to send %s: %v", tc.name, err)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
})
|
||||
}
|
||||
|
||||
if err := ws.Stop(); err != nil {
|
||||
t.Errorf("Failed to stop WebSocket: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestWebSocketEnableDisable(t *testing.T) {
|
||||
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||
}
|
||||
|
||||
if ws.IsEnabled() {
|
||||
t.Error("Interface should not be enabled initially")
|
||||
}
|
||||
|
||||
ws.Enable()
|
||||
if !ws.IsEnabled() {
|
||||
t.Error("Interface should be enabled after Enable()")
|
||||
}
|
||||
|
||||
ws.Disable()
|
||||
if ws.IsEnabled() {
|
||||
t.Error("Interface should not be enabled after Disable()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebSocketDetach(t *testing.T) {
|
||||
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||
}
|
||||
|
||||
if ws.IsDetached() {
|
||||
t.Error("Interface should not be detached initially")
|
||||
}
|
||||
|
||||
ws.Detach()
|
||||
if !ws.IsDetached() {
|
||||
t.Error("Interface should be detached after Detach()")
|
||||
}
|
||||
|
||||
if ws.IsOnline() {
|
||||
t.Error("Interface should be offline after Detach()")
|
||||
}
|
||||
}
|
||||
253
pkg/interfaces/websocket_wasm.go
Normal file
253
pkg/interfaces/websocket_wasm.go
Normal file
@@ -0,0 +1,253 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build js && wasm
|
||||
// +build js,wasm
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall/js"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
const (
|
||||
WS_MTU = 1064
|
||||
WS_BITRATE = 10000000
|
||||
WS_RECONNECT_DELAY = 2 * time.Second
|
||||
)
|
||||
|
||||
type WebSocketInterface struct {
|
||||
BaseInterface
|
||||
wsURL string
|
||||
ws js.Value
|
||||
connected bool
|
||||
messageQueue [][]byte
|
||||
}
|
||||
|
||||
func NewWebSocketInterface(name string, wsURL string, enabled bool) (*WebSocketInterface, error) {
|
||||
ws := &WebSocketInterface{
|
||||
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
|
||||
wsURL: wsURL,
|
||||
messageQueue: make([][]byte, 0),
|
||||
}
|
||||
|
||||
ws.MTU = WS_MTU
|
||||
ws.Bitrate = WS_BITRATE
|
||||
|
||||
return ws, nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetName() string {
|
||||
return wsi.Name
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetType() common.InterfaceType {
|
||||
return wsi.Type
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetMode() common.InterfaceMode {
|
||||
return wsi.Mode
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) IsOnline() bool {
|
||||
wsi.Mutex.RLock()
|
||||
defer wsi.Mutex.RUnlock()
|
||||
return wsi.Online && wsi.connected
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) IsDetached() bool {
|
||||
wsi.Mutex.RLock()
|
||||
defer wsi.Mutex.RUnlock()
|
||||
return wsi.Detached
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Detach() {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
wsi.Detached = true
|
||||
wsi.Online = false
|
||||
wsi.closeWebSocket()
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Enable() {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
wsi.Enabled = true
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Disable() {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
wsi.Enabled = false
|
||||
wsi.closeWebSocket()
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Start() error {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
|
||||
if wsi.ws.Truthy() {
|
||||
return fmt.Errorf("WebSocket already started")
|
||||
}
|
||||
|
||||
ws := js.Global().Get("WebSocket").New(wsi.wsURL)
|
||||
ws.Set("binaryType", "arraybuffer")
|
||||
|
||||
ws.Set("onopen", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
wsi.Mutex.Lock()
|
||||
wsi.connected = true
|
||||
wsi.Online = true
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "WebSocket connected", "name", wsi.Name, "url", wsi.wsURL)
|
||||
|
||||
wsi.Mutex.Lock()
|
||||
queue := make([][]byte, len(wsi.messageQueue))
|
||||
copy(queue, wsi.messageQueue)
|
||||
wsi.messageQueue = wsi.messageQueue[:0]
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
for _, msg := range queue {
|
||||
wsi.sendWebSocketMessage(msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
ws.Set("onmessage", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
if len(args) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
event := args[0]
|
||||
data := event.Get("data")
|
||||
|
||||
var packet []byte
|
||||
if data.Type() == js.TypeString {
|
||||
packet = []byte(data.String())
|
||||
} else if data.Type() == js.TypeObject {
|
||||
array := js.Global().Get("Uint8Array").New(data)
|
||||
length := array.Get("length").Int()
|
||||
packet = make([]byte, length)
|
||||
js.CopyBytesToGo(packet, array)
|
||||
} else {
|
||||
debug.Log(debug.DEBUG_ERROR, "Unknown WebSocket message type", "type", data.Type().String())
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(packet) < 1 {
|
||||
debug.Log(debug.DEBUG_ERROR, "WebSocket message empty")
|
||||
return nil
|
||||
}
|
||||
|
||||
wsi.Mutex.Lock()
|
||||
wsi.RxBytes += uint64(len(packet))
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
wsi.ProcessIncoming(packet)
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
ws.Set("onerror", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
debug.Log(debug.DEBUG_ERROR, "WebSocket error", "name", wsi.Name)
|
||||
return nil
|
||||
}))
|
||||
|
||||
ws.Set("onclose", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
wsi.Mutex.Lock()
|
||||
wsi.connected = false
|
||||
wsi.Online = false
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "WebSocket closed", "name", wsi.Name)
|
||||
|
||||
if wsi.Enabled && !wsi.Detached {
|
||||
time.Sleep(WS_RECONNECT_DELAY)
|
||||
go wsi.Start()
|
||||
}
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
wsi.ws = ws
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Stop() error {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
wsi.Enabled = false
|
||||
wsi.closeWebSocket()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) closeWebSocket() {
|
||||
if wsi.ws.Truthy() {
|
||||
wsi.ws.Call("close")
|
||||
wsi.ws = js.Value{}
|
||||
}
|
||||
wsi.connected = false
|
||||
wsi.Online = false
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Send(data []byte, addr string) error {
|
||||
if !wsi.IsEnabled() {
|
||||
return fmt.Errorf("interface not enabled")
|
||||
}
|
||||
|
||||
wsi.Mutex.Lock()
|
||||
wsi.TxBytes += uint64(len(data))
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
if !wsi.connected {
|
||||
wsi.Mutex.Lock()
|
||||
wsi.messageQueue = append(wsi.messageQueue, data)
|
||||
wsi.Mutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
return wsi.sendWebSocketMessage(data)
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) sendWebSocketMessage(data []byte) error {
|
||||
if !wsi.ws.Truthy() {
|
||||
return fmt.Errorf("WebSocket not initialized")
|
||||
}
|
||||
|
||||
if wsi.ws.Get("readyState").Int() != 1 {
|
||||
return fmt.Errorf("WebSocket not open")
|
||||
}
|
||||
|
||||
array := js.Global().Get("Uint8Array").New(len(data))
|
||||
js.CopyBytesToJS(array, data)
|
||||
|
||||
wsi.ws.Call("send", array)
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "WebSocket sent packet", "name", wsi.Name, "bytes", len(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) ProcessOutgoing(data []byte) error {
|
||||
return wsi.Send(data, "")
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetConn() net.Conn {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetMTU() int {
|
||||
return wsi.MTU
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) IsEnabled() bool {
|
||||
wsi.Mutex.RLock()
|
||||
defer wsi.Mutex.RUnlock()
|
||||
return wsi.Enabled && wsi.Online && !wsi.Detached
|
||||
}
|
||||
364
pkg/link/establishment_test.go
Normal file
364
pkg/link/establishment_test.go
Normal file
@@ -0,0 +1,364 @@
|
||||
package link
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/destination"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/transport"
|
||||
)
|
||||
|
||||
func TestEphemeralKeyGeneration(t *testing.T) {
|
||||
link := &Link{}
|
||||
|
||||
if err := link.generateEphemeralKeys(); err != nil {
|
||||
t.Fatalf("Failed to generate ephemeral keys: %v", err)
|
||||
}
|
||||
|
||||
if len(link.prv) != KEYSIZE {
|
||||
t.Errorf("Expected private key length %d, got %d", KEYSIZE, len(link.prv))
|
||||
}
|
||||
|
||||
if len(link.pub) != KEYSIZE {
|
||||
t.Errorf("Expected public key length %d, got %d", KEYSIZE, len(link.pub))
|
||||
}
|
||||
|
||||
if len(link.sigPriv) != 64 {
|
||||
t.Errorf("Expected signing private key length 64, got %d", len(link.sigPriv))
|
||||
}
|
||||
|
||||
if len(link.sigPub) != 32 {
|
||||
t.Errorf("Expected signing public key length 32, got %d", len(link.sigPub))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignallingBytes(t *testing.T) {
|
||||
mtu := 500
|
||||
mode := byte(MODE_AES256_CBC)
|
||||
|
||||
bytes := signallingBytes(mtu, mode)
|
||||
|
||||
if len(bytes) != LINK_MTU_SIZE {
|
||||
t.Errorf("Expected signalling bytes length %d, got %d", LINK_MTU_SIZE, len(bytes))
|
||||
}
|
||||
|
||||
extractedMTU := (int(bytes[0]&0x1F) << 16) | (int(bytes[1]) << 8) | int(bytes[2])
|
||||
if extractedMTU != mtu {
|
||||
t.Errorf("Expected MTU %d, got %d", mtu, extractedMTU)
|
||||
}
|
||||
|
||||
extractedMode := (bytes[0] & MODE_BYTEMASK) >> 5
|
||||
if extractedMode != mode {
|
||||
t.Errorf("Expected mode %d, got %d", mode, extractedMode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkIDGeneration(t *testing.T) {
|
||||
responderIdent, err := identity.NewIdentity()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create responder identity: %v", err)
|
||||
}
|
||||
|
||||
cfg := &common.ReticulumConfig{}
|
||||
transportInstance := transport.NewTransport(cfg)
|
||||
|
||||
dest, err := destination.New(responderIdent, destination.IN, destination.SINGLE, "test", transportInstance, "link")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create destination: %v", err)
|
||||
}
|
||||
|
||||
link := &Link{
|
||||
destination: dest,
|
||||
transport: transportInstance,
|
||||
initiator: true,
|
||||
}
|
||||
|
||||
if err := link.generateEphemeralKeys(); err != nil {
|
||||
t.Fatalf("Failed to generate keys: %v", err)
|
||||
}
|
||||
|
||||
link.mode = MODE_DEFAULT
|
||||
link.mtu = 500
|
||||
|
||||
signalling := signallingBytes(link.mtu, link.mode)
|
||||
requestData := make([]byte, 0, ECPUBSIZE+LINK_MTU_SIZE)
|
||||
requestData = append(requestData, link.pub...)
|
||||
requestData = append(requestData, link.sigPub...)
|
||||
requestData = append(requestData, signalling...)
|
||||
|
||||
pkt := &packet.Packet{
|
||||
HeaderType: packet.HeaderType1,
|
||||
PacketType: packet.PacketTypeLinkReq,
|
||||
TransportType: 0,
|
||||
Context: packet.ContextNone,
|
||||
ContextFlag: packet.FlagUnset,
|
||||
Hops: 0,
|
||||
DestinationType: dest.GetType(),
|
||||
DestinationHash: dest.GetHash(),
|
||||
Data: requestData,
|
||||
}
|
||||
|
||||
if err := pkt.Pack(); err != nil {
|
||||
t.Fatalf("Failed to pack packet: %v", err)
|
||||
}
|
||||
|
||||
linkID := linkIDFromPacket(pkt)
|
||||
|
||||
if len(linkID) != 16 {
|
||||
t.Errorf("Expected link ID length 16, got %d", len(linkID))
|
||||
}
|
||||
|
||||
t.Logf("Generated link ID: %x", linkID)
|
||||
}
|
||||
|
||||
func TestHandshake(t *testing.T) {
|
||||
link1 := &Link{}
|
||||
link2 := &Link{}
|
||||
|
||||
if err := link1.generateEphemeralKeys(); err != nil {
|
||||
t.Fatalf("Failed to generate keys for link1: %v", err)
|
||||
}
|
||||
|
||||
if err := link2.generateEphemeralKeys(); err != nil {
|
||||
t.Fatalf("Failed to generate keys for link2: %v", err)
|
||||
}
|
||||
|
||||
link1.peerPub = link2.pub
|
||||
link2.peerPub = link1.pub
|
||||
|
||||
link1.linkID = []byte("test-link-id-abc")
|
||||
link2.linkID = []byte("test-link-id-abc")
|
||||
|
||||
link1.mode = MODE_AES256_CBC
|
||||
link2.mode = MODE_AES256_CBC
|
||||
|
||||
if err := link1.performHandshake(); err != nil {
|
||||
t.Fatalf("Link1 handshake failed: %v", err)
|
||||
}
|
||||
|
||||
if err := link2.performHandshake(); err != nil {
|
||||
t.Fatalf("Link2 handshake failed: %v", err)
|
||||
}
|
||||
|
||||
if string(link1.sharedKey) != string(link2.sharedKey) {
|
||||
t.Error("Shared keys do not match")
|
||||
}
|
||||
|
||||
if string(link1.derivedKey) != string(link2.derivedKey) {
|
||||
t.Error("Derived keys do not match")
|
||||
}
|
||||
|
||||
if link1.status != STATUS_HANDSHAKE {
|
||||
t.Errorf("Expected link1 status HANDSHAKE, got %d", link1.status)
|
||||
}
|
||||
|
||||
if link2.status != STATUS_HANDSHAKE {
|
||||
t.Errorf("Expected link2 status HANDSHAKE, got %d", link2.status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkEstablishment(t *testing.T) {
|
||||
responderIdent, err := identity.NewIdentity()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create responder identity: %v", err)
|
||||
}
|
||||
|
||||
cfg := &common.ReticulumConfig{}
|
||||
transportInstance := transport.NewTransport(cfg)
|
||||
|
||||
dest, err := destination.New(responderIdent, destination.IN, destination.SINGLE, "test", transportInstance, "link")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create destination: %v", err)
|
||||
}
|
||||
|
||||
initiatorLink := &Link{
|
||||
destination: dest,
|
||||
transport: transportInstance,
|
||||
initiator: true,
|
||||
}
|
||||
|
||||
responderLink := &Link{
|
||||
transport: transportInstance,
|
||||
initiator: false,
|
||||
}
|
||||
|
||||
if err := initiatorLink.generateEphemeralKeys(); err != nil {
|
||||
t.Fatalf("Failed to generate initiator keys: %v", err)
|
||||
}
|
||||
|
||||
initiatorLink.mode = MODE_DEFAULT
|
||||
initiatorLink.mtu = 500
|
||||
|
||||
signalling := signallingBytes(initiatorLink.mtu, initiatorLink.mode)
|
||||
requestData := make([]byte, 0, ECPUBSIZE+LINK_MTU_SIZE)
|
||||
requestData = append(requestData, initiatorLink.pub...)
|
||||
requestData = append(requestData, initiatorLink.sigPub...)
|
||||
requestData = append(requestData, signalling...)
|
||||
|
||||
linkRequestPkt := &packet.Packet{
|
||||
HeaderType: packet.HeaderType1,
|
||||
PacketType: packet.PacketTypeLinkReq,
|
||||
TransportType: 0,
|
||||
Context: packet.ContextNone,
|
||||
ContextFlag: packet.FlagUnset,
|
||||
Hops: 0,
|
||||
DestinationType: dest.GetType(),
|
||||
DestinationHash: dest.GetHash(),
|
||||
Data: requestData,
|
||||
}
|
||||
|
||||
if err := linkRequestPkt.Pack(); err != nil {
|
||||
t.Fatalf("Failed to pack link request: %v", err)
|
||||
}
|
||||
|
||||
initiatorLink.linkID = linkIDFromPacket(linkRequestPkt)
|
||||
initiatorLink.requestTime = time.Now()
|
||||
initiatorLink.status = STATUS_PENDING
|
||||
|
||||
t.Logf("Initiator link request created, link_id=%x", initiatorLink.linkID)
|
||||
|
||||
responderLink.peerPub = linkRequestPkt.Data[0:KEYSIZE]
|
||||
responderLink.peerSigPub = linkRequestPkt.Data[KEYSIZE:ECPUBSIZE]
|
||||
responderLink.linkID = linkIDFromPacket(linkRequestPkt)
|
||||
responderLink.initiator = false
|
||||
|
||||
t.Logf("Responder link ID=%x (len=%d)", responderLink.linkID, len(responderLink.linkID))
|
||||
|
||||
if len(responderLink.linkID) == 0 {
|
||||
t.Fatal("Responder link ID is empty!")
|
||||
}
|
||||
|
||||
if len(linkRequestPkt.Data) >= ECPUBSIZE+LINK_MTU_SIZE {
|
||||
mtuBytes := linkRequestPkt.Data[ECPUBSIZE : ECPUBSIZE+LINK_MTU_SIZE]
|
||||
responderLink.mtu = (int(mtuBytes[0]&0x1F) << 16) | (int(mtuBytes[1]) << 8) | int(mtuBytes[2])
|
||||
responderLink.mode = (mtuBytes[0] & MODE_BYTEMASK) >> 5
|
||||
}
|
||||
|
||||
if err := responderLink.generateEphemeralKeys(); err != nil {
|
||||
t.Fatalf("Failed to generate responder keys: %v", err)
|
||||
}
|
||||
|
||||
if err := responderLink.performHandshake(); err != nil {
|
||||
t.Fatalf("Responder handshake failed: %v", err)
|
||||
}
|
||||
|
||||
responderLink.status = STATUS_ACTIVE
|
||||
responderLink.establishedAt = time.Now()
|
||||
|
||||
if string(responderLink.linkID) != string(initiatorLink.linkID) {
|
||||
t.Error("Link IDs do not match between initiator and responder")
|
||||
}
|
||||
|
||||
t.Logf("Responder handshake successful, shared_key_len=%d", len(responderLink.sharedKey))
|
||||
}
|
||||
|
||||
func TestLinkProofValidation(t *testing.T) {
|
||||
responderIdent, err := identity.NewIdentity()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create responder identity: %v", err)
|
||||
}
|
||||
|
||||
cfg := &common.ReticulumConfig{}
|
||||
transportInstance := transport.NewTransport(cfg)
|
||||
|
||||
dest, err := destination.New(responderIdent, destination.IN, destination.SINGLE, "test", transportInstance, "link")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create destination: %v", err)
|
||||
}
|
||||
|
||||
initiatorLink := &Link{
|
||||
destination: dest,
|
||||
transport: transportInstance,
|
||||
initiator: true,
|
||||
}
|
||||
|
||||
responderLink := &Link{
|
||||
transport: transportInstance,
|
||||
initiator: false,
|
||||
}
|
||||
|
||||
if err := initiatorLink.generateEphemeralKeys(); err != nil {
|
||||
t.Fatalf("Failed to generate initiator keys: %v", err)
|
||||
}
|
||||
|
||||
initiatorLink.mode = MODE_DEFAULT
|
||||
initiatorLink.mtu = 500
|
||||
|
||||
signalling := signallingBytes(initiatorLink.mtu, initiatorLink.mode)
|
||||
requestData := make([]byte, 0, ECPUBSIZE+LINK_MTU_SIZE)
|
||||
requestData = append(requestData, initiatorLink.pub...)
|
||||
requestData = append(requestData, initiatorLink.sigPub...)
|
||||
requestData = append(requestData, signalling...)
|
||||
|
||||
linkRequestPkt := &packet.Packet{
|
||||
HeaderType: packet.HeaderType1,
|
||||
PacketType: packet.PacketTypeLinkReq,
|
||||
TransportType: 0,
|
||||
Context: packet.ContextNone,
|
||||
ContextFlag: packet.FlagUnset,
|
||||
Hops: 0,
|
||||
DestinationType: dest.GetType(),
|
||||
DestinationHash: dest.GetHash(),
|
||||
Data: requestData,
|
||||
}
|
||||
|
||||
if err := linkRequestPkt.Pack(); err != nil {
|
||||
t.Fatalf("Failed to pack link request: %v", err)
|
||||
}
|
||||
|
||||
initiatorLink.linkID = linkIDFromPacket(linkRequestPkt)
|
||||
initiatorLink.requestTime = time.Now()
|
||||
initiatorLink.status = STATUS_PENDING
|
||||
|
||||
responderLink.peerPub = linkRequestPkt.Data[0:KEYSIZE]
|
||||
responderLink.peerSigPub = linkRequestPkt.Data[KEYSIZE:ECPUBSIZE]
|
||||
responderLink.linkID = linkIDFromPacket(linkRequestPkt)
|
||||
responderLink.initiator = false
|
||||
|
||||
if len(linkRequestPkt.Data) >= ECPUBSIZE+LINK_MTU_SIZE {
|
||||
mtuBytes := linkRequestPkt.Data[ECPUBSIZE : ECPUBSIZE+LINK_MTU_SIZE]
|
||||
responderLink.mtu = (int(mtuBytes[0]&0x1F) << 16) | (int(mtuBytes[1]) << 8) | int(mtuBytes[2])
|
||||
responderLink.mode = (mtuBytes[0] & MODE_BYTEMASK) >> 5
|
||||
} else {
|
||||
responderLink.mtu = 500
|
||||
responderLink.mode = MODE_DEFAULT
|
||||
}
|
||||
|
||||
if err := responderLink.generateEphemeralKeys(); err != nil {
|
||||
t.Fatalf("Failed to generate responder keys: %v", err)
|
||||
}
|
||||
|
||||
if err := responderLink.performHandshake(); err != nil {
|
||||
t.Fatalf("Responder handshake failed: %v", err)
|
||||
}
|
||||
|
||||
proofPkt, err := responderLink.GenerateLinkProof(responderIdent)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate link proof: %v", err)
|
||||
}
|
||||
|
||||
if err := initiatorLink.ValidateLinkProof(proofPkt, nil); err != nil {
|
||||
t.Fatalf("Initiator failed to validate link proof: %v", err)
|
||||
}
|
||||
|
||||
if initiatorLink.status != STATUS_ACTIVE {
|
||||
t.Errorf("Expected initiator status ACTIVE, got %d", initiatorLink.status)
|
||||
}
|
||||
|
||||
if string(initiatorLink.sharedKey) != string(responderLink.sharedKey) {
|
||||
t.Error("Shared keys do not match after full handshake")
|
||||
}
|
||||
|
||||
if string(initiatorLink.derivedKey) != string(responderLink.derivedKey) {
|
||||
t.Error("Derived keys do not match after full handshake")
|
||||
}
|
||||
|
||||
t.Logf("Full link establishment successful")
|
||||
t.Logf("Link ID: %x", initiatorLink.linkID)
|
||||
t.Logf("Shared key length: %d", len(initiatorLink.sharedKey))
|
||||
t.Logf("Derived key length: %d", len(initiatorLink.derivedKey))
|
||||
t.Logf("RTT: %.3f seconds", initiatorLink.rtt)
|
||||
}
|
||||
1697
pkg/link/link.go
1697
pkg/link/link.go
File diff suppressed because it is too large
Load Diff
218
pkg/link/link_test.go
Normal file
218
pkg/link/link_test.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package link
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/destination"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
|
||||
)
|
||||
|
||||
type mockTransport struct {
|
||||
sentPackets []*packet.Packet
|
||||
}
|
||||
|
||||
func (m *mockTransport) SendPacket(pkt *packet.Packet) error {
|
||||
m.sentPackets = append(m.sentPackets, pkt)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockTransport) RegisterLink(linkID []byte, link interface{}) {
|
||||
}
|
||||
|
||||
func (m *mockTransport) GetConfig() *common.ReticulumConfig {
|
||||
return &common.ReticulumConfig{}
|
||||
}
|
||||
|
||||
func (m *mockTransport) GetInterfaces() map[string]common.NetworkInterface {
|
||||
return make(map[string]common.NetworkInterface)
|
||||
}
|
||||
|
||||
func (m *mockTransport) RegisterDestination(hash []byte, dest interface{}) {
|
||||
}
|
||||
|
||||
type mockInterface struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (m *mockInterface) GetName() string {
|
||||
return m.name
|
||||
}
|
||||
|
||||
func (m *mockInterface) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockInterface) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockInterface) Send(data []byte, address string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockInterface) ProcessIncoming(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockInterface) SetPacketCallback(cb func([]byte, common.NetworkInterface)) {
|
||||
}
|
||||
|
||||
func (m *mockInterface) GetType() string {
|
||||
return "mock"
|
||||
}
|
||||
|
||||
func (m *mockInterface) GetMTU() int {
|
||||
return 500
|
||||
}
|
||||
|
||||
func (m *mockInterface) Detach() {
|
||||
}
|
||||
|
||||
func (m *mockInterface) Enable() {
|
||||
}
|
||||
|
||||
func (m *mockInterface) Disable() {
|
||||
}
|
||||
|
||||
func (m *mockInterface) IsEnabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *mockInterface) IsOnline() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *mockInterface) IsDetached() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *mockInterface) GetPacketCallback() func([]byte, common.NetworkInterface) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockInterface) GetConn() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockInterface) ProcessOutgoing(data []byte) ([]byte, error) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (m *mockInterface) SendPathRequest(destHash []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockInterface) SendLinkPacket(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockInterface) GetBandwidthAvailable() float64 {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
func TestLinkRequestResponse(t *testing.T) {
|
||||
serverIdent, err := identity.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create server identity: %v", err)
|
||||
}
|
||||
|
||||
clientIdent, err := identity.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client identity: %v", err)
|
||||
}
|
||||
|
||||
mockTrans := &mockTransport{
|
||||
sentPackets: make([]*packet.Packet, 0),
|
||||
}
|
||||
|
||||
serverDest, err := destination.New(serverIdent, destination.IN, destination.SINGLE, "testapp", mockTrans, "server")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create server destination: %v", err)
|
||||
}
|
||||
|
||||
expectedResponse := []byte("response data")
|
||||
testPath := "/test/path"
|
||||
|
||||
err = serverDest.RegisterRequestHandler(testPath, func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity *identity.Identity, requestedAt int64) []byte {
|
||||
if path != testPath {
|
||||
t.Errorf("Expected path %s, got %s", testPath, path)
|
||||
}
|
||||
return expectedResponse
|
||||
}, destination.ALLOW_ALL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register request handler: %v", err)
|
||||
}
|
||||
|
||||
// Test the handler is registered correctly
|
||||
pathHash := identity.TruncatedHash([]byte(testPath))
|
||||
handler := serverDest.GetRequestHandler(pathHash)
|
||||
if handler == nil {
|
||||
t.Fatal("Handler not found after registration")
|
||||
}
|
||||
|
||||
// Call the handler
|
||||
testLinkID := make([]byte, 16)
|
||||
result := handler(pathHash, []byte("test data"), []byte("request-id"), testLinkID, clientIdent, time.Now())
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Handler returned nil")
|
||||
}
|
||||
|
||||
responseBytes, ok := result.([]byte)
|
||||
if !ok {
|
||||
t.Fatalf("Handler returned unexpected type: %T", result)
|
||||
}
|
||||
|
||||
if !bytes.Equal(responseBytes, expectedResponse) {
|
||||
t.Errorf("Expected response %q, got %q", expectedResponse, responseBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkRequestHandlerNotFound(t *testing.T) {
|
||||
serverIdent, _ := identity.New()
|
||||
mockTrans := &mockTransport{sentPackets: make([]*packet.Packet, 0)}
|
||||
|
||||
serverDest, _ := destination.New(serverIdent, destination.IN, destination.SINGLE, "testapp", mockTrans, "server")
|
||||
|
||||
nonExistentPath := "/does/not/exist"
|
||||
pathHash := identity.TruncatedHash([]byte(nonExistentPath))
|
||||
|
||||
handler := serverDest.GetRequestHandler(pathHash)
|
||||
if handler != nil {
|
||||
t.Error("Expected no handler for non-existent path, but found one")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkResponseHandling(t *testing.T) {
|
||||
// This test verifies the basic structure for response handling
|
||||
// Full integration testing would require a proper transport setup
|
||||
|
||||
requestID := []byte("test-request-id-")
|
||||
responseData := []byte("response payload")
|
||||
|
||||
receipt := &RequestReceipt{
|
||||
requestID: requestID,
|
||||
status: STATUS_PENDING,
|
||||
}
|
||||
|
||||
// Verify initial state
|
||||
if receipt.status != STATUS_PENDING {
|
||||
t.Errorf("Expected initial status PENDING, got %d", receipt.status)
|
||||
}
|
||||
|
||||
// Simulate setting response
|
||||
receipt.response = responseData
|
||||
receipt.status = STATUS_ACTIVE
|
||||
|
||||
if !bytes.Equal(receipt.response, responseData) {
|
||||
t.Errorf("Expected response %q, got %q", responseData, receipt.response)
|
||||
}
|
||||
|
||||
if receipt.status != STATUS_ACTIVE {
|
||||
t.Errorf("Expected status ACTIVE after response, got %d", receipt.status)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package packet
|
||||
|
||||
const (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package packet
|
||||
|
||||
import (
|
||||
@@ -6,10 +8,10 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -67,6 +69,7 @@ type Packet struct {
|
||||
|
||||
DestinationType byte
|
||||
DestinationHash []byte
|
||||
Destination interface{}
|
||||
TransportID []byte
|
||||
Data []byte
|
||||
|
||||
@@ -85,6 +88,21 @@ type Packet struct {
|
||||
Q *float64
|
||||
|
||||
Addresses []byte
|
||||
Link interface{}
|
||||
|
||||
receipt *PacketReceipt
|
||||
}
|
||||
|
||||
type PacketConfig struct {
|
||||
DestType byte
|
||||
Data []byte
|
||||
PacketType byte
|
||||
Context byte
|
||||
TransportType byte
|
||||
HeaderType byte
|
||||
TransportID []byte
|
||||
CreateReceipt bool
|
||||
ContextFlag byte
|
||||
}
|
||||
|
||||
func NewPacket(destType byte, data []byte, packetType byte, context byte,
|
||||
@@ -113,29 +131,34 @@ func (p *Packet) Pack() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-6] Packing packet: type=%d, header=%d", p.PacketType, p.HeaderType)
|
||||
debug.Log(debug.DEBUG_PACKETS, "Packing packet", "type", p.PacketType, "header", 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)
|
||||
debug.Log(debug.DEBUG_TRACE, "Created packet header", "flags", fmt.Sprintf("%08b", flags), "hops", p.Hops)
|
||||
|
||||
header = append(header, p.DestinationHash...)
|
||||
|
||||
if p.HeaderType == HeaderType2 {
|
||||
if p.TransportID == nil {
|
||||
return errors.New("transport ID required for header type 2")
|
||||
}
|
||||
header = append(header, p.TransportID...)
|
||||
log.Printf("[DEBUG-7] Added transport ID to header: %x", p.TransportID)
|
||||
debug.Log(debug.DEBUG_ALL, "Added transport ID to header", "transport_id", fmt.Sprintf("%x", p.TransportID))
|
||||
}
|
||||
|
||||
header = append(header, p.DestinationHash...)
|
||||
header = append(header, p.Context)
|
||||
log.Printf("[DEBUG-6] Final header length: %d bytes", len(header))
|
||||
debug.Log(debug.DEBUG_PACKETS, "Final header length", "bytes", len(header))
|
||||
|
||||
p.Raw = append(header, p.Data...)
|
||||
log.Printf("[DEBUG-5] Final packet size: %d bytes", len(p.Raw))
|
||||
debug.Log(debug.DEBUG_TRACE, "Final packet size", "bytes", len(p.Raw))
|
||||
|
||||
if len(p.Raw) > MTU {
|
||||
return errors.New("packet size exceeds MTU")
|
||||
@@ -143,7 +166,7 @@ func (p *Packet) Pack() error {
|
||||
|
||||
p.Packed = true
|
||||
p.updateHash()
|
||||
log.Printf("[DEBUG-7] Packet hash: %x", p.PacketHash)
|
||||
debug.Log(debug.DEBUG_ALL, "Packet hash", "hash", fmt.Sprintf("%x", p.PacketHash))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -164,14 +187,16 @@ func (p *Packet) Unpack() error {
|
||||
dstLen := 16 // Truncated hash length
|
||||
|
||||
if p.HeaderType == HeaderType2 {
|
||||
// Header Type 2: Header(2) + DestHash(16) + TransportID(16) + Context(1) + Data
|
||||
if len(p.Raw) < 2*dstLen+3 {
|
||||
return errors.New("packet too short for header type 2")
|
||||
}
|
||||
p.TransportID = p.Raw[2 : dstLen+2]
|
||||
p.DestinationHash = p.Raw[dstLen+2 : 2*dstLen+2]
|
||||
p.DestinationHash = p.Raw[2 : dstLen+2] // Destination hash first
|
||||
p.TransportID = p.Raw[dstLen+2 : 2*dstLen+2] // Transport ID second
|
||||
p.Context = p.Raw[2*dstLen+2]
|
||||
p.Data = p.Raw[2*dstLen+3:]
|
||||
} else {
|
||||
// Header Type 1: Header(2) + DestHash(16) + Context(1) + Data
|
||||
if len(p.Raw) < dstLen+3 {
|
||||
return errors.New("packet too short for header type 1")
|
||||
}
|
||||
@@ -193,11 +218,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:]...)
|
||||
// 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:]...)
|
||||
// Start hash from DestHash (index 2)
|
||||
if len(p.Raw) > 2 {
|
||||
hashable = append(hashable, p.Raw[2:]...)
|
||||
}
|
||||
}
|
||||
return hashable
|
||||
}
|
||||
@@ -206,6 +239,18 @@ func (p *Packet) updateHash() {
|
||||
p.PacketHash = p.GetHash()
|
||||
}
|
||||
|
||||
func (p *Packet) Hash() []byte {
|
||||
return p.GetHash()
|
||||
}
|
||||
|
||||
func (p *Packet) TruncatedHash() []byte {
|
||||
hash := p.GetHash()
|
||||
if len(hash) >= 16 {
|
||||
return hash[:16]
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
func (p *Packet) Serialize() ([]byte, error) {
|
||||
if !p.Packed {
|
||||
if err := p.Pack(); err != nil {
|
||||
@@ -219,13 +264,13 @@ 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, fmt.Sprintf("%x", appData))
|
||||
debug.Log(debug.DEBUG_ALL, "Creating new announce packet", "dest_hash", fmt.Sprintf("%x", destHash), "app_data", fmt.Sprintf("%x", appData))
|
||||
|
||||
// Get public key separated into encryption and signing keys
|
||||
pubKey := identity.GetPublicKey()
|
||||
encKey := pubKey[:32]
|
||||
signKey := pubKey[32:]
|
||||
log.Printf("[DEBUG-6] Using public keys: encKey=%x, signKey=%x", encKey, signKey)
|
||||
debug.Log(debug.DEBUG_PACKETS, "Using public keys", "enc_key", fmt.Sprintf("%x", encKey), "sign_key", fmt.Sprintf("%x", signKey))
|
||||
|
||||
// Parse app name from first msgpack element if possible
|
||||
// For nodes, we'll use "reticulum.node" as the name hash
|
||||
@@ -250,15 +295,19 @@ func NewAnnouncePacket(destHash []byte, identity *identity.Identity, appData []b
|
||||
// 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)
|
||||
debug.Log(debug.DEBUG_PACKETS, "Using name hash", "name", appName, "hash", fmt.Sprintf("%x", nameHash10))
|
||||
|
||||
// Create random hash (10 bytes) - 5 bytes random + 5 bytes time
|
||||
randomHash := make([]byte, 10)
|
||||
rand.Read(randomHash[:5])
|
||||
_, err := rand.Read(randomHash[:5]) // #nosec G104
|
||||
if err != nil {
|
||||
debug.Log(debug.DEBUG_PACKETS, "Failed to read random bytes for hash", "error", err)
|
||||
return nil, err // 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
|
||||
copy(randomHash[5:], timeBytes[:5])
|
||||
log.Printf("[DEBUG-6] Generated random hash: %x", randomHash)
|
||||
debug.Log(debug.DEBUG_PACKETS, "Generated random hash", "hash", fmt.Sprintf("%x", randomHash))
|
||||
|
||||
// Prepare ratchet ID if available (not yet implemented)
|
||||
var ratchetID []byte
|
||||
@@ -272,11 +321,11 @@ func NewAnnouncePacket(destHash []byte, identity *identity.Identity, appData []b
|
||||
signedData = append(signedData, nameHash10...)
|
||||
signedData = append(signedData, randomHash...)
|
||||
signedData = append(signedData, appData...)
|
||||
log.Printf("[DEBUG-5] Created signed data (%d bytes)", len(signedData))
|
||||
debug.Log(debug.DEBUG_TRACE, "Created signed data", "bytes", len(signedData))
|
||||
|
||||
// Sign the data
|
||||
signature := identity.Sign(signedData)
|
||||
log.Printf("[DEBUG-6] Generated signature: %x", signature)
|
||||
debug.Log(debug.DEBUG_PACKETS, "Generated signature", "signature", fmt.Sprintf("%x", signature))
|
||||
|
||||
// Combine all fields according to spec
|
||||
// Data structure: Public Key (32) + Signing Key (32) + Name Hash (10) + Random Hash (10) + Ratchet (optional) + Signature (64) + App Data
|
||||
@@ -291,7 +340,7 @@ func NewAnnouncePacket(destHash []byte, identity *identity.Identity, appData []b
|
||||
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))
|
||||
debug.Log(debug.DEBUG_TRACE, "Combined packet data", "bytes", len(data))
|
||||
|
||||
// Create the packet with header type 2 (two address fields)
|
||||
p := &Packet{
|
||||
@@ -302,6 +351,6 @@ func NewAnnouncePacket(destHash []byte, identity *identity.Identity, appData []b
|
||||
Data: data,
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG-4] Created announce packet: type=%d, header=%d", p.PacketType, p.HeaderType)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Created announce packet", "type", p.PacketType, "header", p.HeaderType)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
331
pkg/packet/packet_test.go
Normal file
331
pkg/packet/packet_test.go
Normal file
@@ -0,0 +1,331 @@
|
||||
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
|
||||
}
|
||||
|
||||
// BenchmarkPacketOperations benchmarks packet creation, packing, and hashing
|
||||
func BenchmarkPacketOperations(b *testing.B) {
|
||||
// Prepare test data (keep under MTU limit)
|
||||
data := randomBytes(256)
|
||||
transportID := randomBytes(16)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Create packet
|
||||
packet := NewPacket(0x00, data, PacketTypeData, ContextNone, 0x00, HeaderType1, transportID, false, 0x00)
|
||||
|
||||
// Pack the packet
|
||||
if err := packet.Pack(); err != nil {
|
||||
b.Fatalf("Packet.Pack() failed: %v", err)
|
||||
}
|
||||
|
||||
// Get hash (triggers crypto operations)
|
||||
_ = packet.GetHash()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkPacketSerializeDeserialize benchmarks the full pack/unpack cycle
|
||||
func BenchmarkPacketSerializeDeserialize(b *testing.B) {
|
||||
// Prepare test data (keep under MTU limit)
|
||||
data := randomBytes(256)
|
||||
transportID := randomBytes(16)
|
||||
|
||||
// Create and pack original packet
|
||||
originalPacket := NewPacket(0x00, data, PacketTypeData, ContextNone, 0x00, HeaderType1, transportID, false, 0x00)
|
||||
if err := originalPacket.Pack(); err != nil {
|
||||
b.Fatalf("Original packet.Pack() failed: %v", err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Create new packet from raw data
|
||||
packet := &Packet{Raw: make([]byte, len(originalPacket.Raw))}
|
||||
copy(packet.Raw, originalPacket.Raw)
|
||||
|
||||
// Unpack the packet
|
||||
if err := packet.Unpack(); err != nil {
|
||||
b.Fatalf("Packet.Unpack() failed: %v", err)
|
||||
}
|
||||
|
||||
// Re-pack
|
||||
if err := packet.Pack(); err != nil {
|
||||
b.Fatalf("Packet.Pack() failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
344
pkg/packet/receipt.go
Normal file
344
pkg/packet/receipt.go
Normal file
@@ -0,0 +1,344 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package packet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
||||
)
|
||||
|
||||
const (
|
||||
RECEIPT_FAILED = 0x00
|
||||
RECEIPT_SENT = 0x01
|
||||
RECEIPT_DELIVERED = 0x02
|
||||
RECEIPT_CULLED = 0xFF
|
||||
|
||||
EXPL_LENGTH = (identity.HASHLENGTH + identity.SIGLENGTH) / 8
|
||||
IMPL_LENGTH = identity.SIGLENGTH / 8
|
||||
)
|
||||
|
||||
type PacketReceipt struct {
|
||||
mutex sync.RWMutex
|
||||
|
||||
hash []byte
|
||||
truncatedHash []byte
|
||||
sent bool
|
||||
sentAt time.Time
|
||||
proved bool
|
||||
status byte
|
||||
destination interface{}
|
||||
timeout time.Duration
|
||||
concludedAt time.Time
|
||||
proofPacket *Packet
|
||||
|
||||
deliveryCallback func(*PacketReceipt)
|
||||
timeoutCallback func(*PacketReceipt)
|
||||
|
||||
link interface{}
|
||||
destinationHash []byte
|
||||
destinationIdent *identity.Identity
|
||||
timeoutCheckDone chan bool
|
||||
}
|
||||
|
||||
func NewPacketReceipt(pkt *Packet) *PacketReceipt {
|
||||
hash := pkt.Hash()
|
||||
receipt := &PacketReceipt{
|
||||
hash: hash,
|
||||
truncatedHash: pkt.TruncatedHash(),
|
||||
sent: true,
|
||||
sentAt: time.Now(),
|
||||
proved: false,
|
||||
status: RECEIPT_SENT,
|
||||
destination: pkt.Destination,
|
||||
timeout: calculateTimeout(pkt),
|
||||
timeoutCheckDone: make(chan bool, 1),
|
||||
}
|
||||
|
||||
go receipt.timeoutWatchdog()
|
||||
|
||||
debug.Log(debug.DEBUG_PACKETS, "Created packet receipt", "hash", fmt.Sprintf("%x", receipt.truncatedHash))
|
||||
return receipt
|
||||
}
|
||||
|
||||
func calculateTimeout(pkt *Packet) time.Duration {
|
||||
baseTimeout := 15 * time.Second
|
||||
|
||||
if pkt.Hops > 0 {
|
||||
baseTimeout += time.Duration(pkt.Hops) * (3 * time.Second)
|
||||
}
|
||||
|
||||
return baseTimeout
|
||||
}
|
||||
|
||||
func (pr *PacketReceipt) GetStatus() byte {
|
||||
pr.mutex.RLock()
|
||||
defer pr.mutex.RUnlock()
|
||||
return pr.status
|
||||
}
|
||||
|
||||
func (pr *PacketReceipt) GetHash() []byte {
|
||||
pr.mutex.RLock()
|
||||
defer pr.mutex.RUnlock()
|
||||
return pr.hash
|
||||
}
|
||||
|
||||
func (pr *PacketReceipt) IsDelivered() bool {
|
||||
pr.mutex.RLock()
|
||||
defer pr.mutex.RUnlock()
|
||||
return pr.status == RECEIPT_DELIVERED
|
||||
}
|
||||
|
||||
func (pr *PacketReceipt) IsFailed() bool {
|
||||
pr.mutex.RLock()
|
||||
defer pr.mutex.RUnlock()
|
||||
return pr.status == RECEIPT_FAILED
|
||||
}
|
||||
|
||||
func (pr *PacketReceipt) ValidateProofPacket(proofPacket *Packet) bool {
|
||||
if proofPacket.Link != nil {
|
||||
return pr.ValidateLinkProof(proofPacket.Data, proofPacket.Link, proofPacket)
|
||||
}
|
||||
return pr.ValidateProof(proofPacket.Data, proofPacket)
|
||||
}
|
||||
|
||||
func (pr *PacketReceipt) ValidateLinkProof(proof []byte, link interface{}, proofPacket *Packet) bool {
|
||||
if len(proof) == EXPL_LENGTH {
|
||||
proofHash := proof[:identity.HASHLENGTH/8]
|
||||
signature := proof[identity.HASHLENGTH/8 : identity.HASHLENGTH/8+identity.SIGLENGTH/8]
|
||||
|
||||
pr.mutex.RLock()
|
||||
hashMatch := string(proofHash) == string(pr.hash)
|
||||
pr.mutex.RUnlock()
|
||||
|
||||
if !hashMatch {
|
||||
return false
|
||||
}
|
||||
|
||||
proofValid := pr.validateLinkSignature(signature, link)
|
||||
if proofValid {
|
||||
pr.mutex.Lock()
|
||||
pr.status = RECEIPT_DELIVERED
|
||||
pr.proved = true
|
||||
pr.concludedAt = time.Now()
|
||||
pr.proofPacket = proofPacket
|
||||
callback := pr.deliveryCallback
|
||||
pr.mutex.Unlock()
|
||||
|
||||
if callback != nil {
|
||||
go callback(pr)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_PACKETS, "Link proof validated", "hash", fmt.Sprintf("%x", pr.truncatedHash))
|
||||
return true
|
||||
}
|
||||
} else if len(proof) == IMPL_LENGTH {
|
||||
debug.Log(debug.DEBUG_TRACE, "Implicit link proof not yet implemented")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (pr *PacketReceipt) ValidateProof(proof []byte, proofPacket *Packet) bool {
|
||||
if len(proof) == EXPL_LENGTH {
|
||||
proofHash := proof[:identity.HASHLENGTH/8]
|
||||
signature := proof[identity.HASHLENGTH/8 : identity.HASHLENGTH/8+identity.SIGLENGTH/8]
|
||||
|
||||
pr.mutex.RLock()
|
||||
hashMatch := string(proofHash) == string(pr.hash)
|
||||
ident := pr.destinationIdent
|
||||
pr.mutex.RUnlock()
|
||||
|
||||
debug.Log(debug.DEBUG_PACKETS, "Explicit proof validation", "len", len(proof), "hashMatch", hashMatch, "hasIdent", ident != nil)
|
||||
|
||||
if !hashMatch {
|
||||
debug.Log(debug.DEBUG_PACKETS, "Proof hash mismatch")
|
||||
return false
|
||||
}
|
||||
|
||||
if ident == nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Cannot validate proof without destination identity")
|
||||
return false
|
||||
}
|
||||
|
||||
proofValid := ident.Verify(pr.hash, signature)
|
||||
debug.Log(debug.DEBUG_PACKETS, "Signature verification result", "valid", proofValid)
|
||||
if proofValid {
|
||||
pr.mutex.Lock()
|
||||
pr.status = RECEIPT_DELIVERED
|
||||
pr.proved = true
|
||||
pr.concludedAt = time.Now()
|
||||
pr.proofPacket = proofPacket
|
||||
callback := pr.deliveryCallback
|
||||
pr.mutex.Unlock()
|
||||
|
||||
if callback != nil {
|
||||
go callback(pr)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_PACKETS, "Proof validated", "hash", fmt.Sprintf("%x", pr.truncatedHash))
|
||||
return true
|
||||
}
|
||||
} else if len(proof) == IMPL_LENGTH {
|
||||
signature := proof[:identity.SIGLENGTH/8]
|
||||
|
||||
pr.mutex.RLock()
|
||||
ident := pr.destinationIdent
|
||||
pr.mutex.RUnlock()
|
||||
|
||||
if ident == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
proofValid := ident.Verify(pr.hash, signature)
|
||||
if proofValid {
|
||||
pr.mutex.Lock()
|
||||
pr.status = RECEIPT_DELIVERED
|
||||
pr.proved = true
|
||||
pr.concludedAt = time.Now()
|
||||
pr.proofPacket = proofPacket
|
||||
callback := pr.deliveryCallback
|
||||
pr.mutex.Unlock()
|
||||
|
||||
if callback != nil {
|
||||
go callback(pr)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_PACKETS, "Implicit proof validated", "hash", fmt.Sprintf("%x", pr.truncatedHash))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (pr *PacketReceipt) validateLinkSignature(signature []byte, link interface{}) bool {
|
||||
type linkValidator interface {
|
||||
Validate(signature, message []byte) bool
|
||||
}
|
||||
|
||||
if validator, ok := link.(linkValidator); ok {
|
||||
return validator.Validate(signature, pr.hash)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_TRACE, "Link does not implement Validate method")
|
||||
return false
|
||||
}
|
||||
|
||||
func (pr *PacketReceipt) GetRTT() time.Duration {
|
||||
pr.mutex.RLock()
|
||||
defer pr.mutex.RUnlock()
|
||||
|
||||
if pr.concludedAt.IsZero() {
|
||||
return 0
|
||||
}
|
||||
|
||||
return pr.concludedAt.Sub(pr.sentAt)
|
||||
}
|
||||
|
||||
func (pr *PacketReceipt) IsTimedOut() bool {
|
||||
pr.mutex.RLock()
|
||||
defer pr.mutex.RUnlock()
|
||||
|
||||
return time.Since(pr.sentAt) > pr.timeout
|
||||
}
|
||||
|
||||
func (pr *PacketReceipt) checkTimeout() {
|
||||
pr.mutex.Lock()
|
||||
|
||||
if pr.status != RECEIPT_SENT {
|
||||
pr.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if time.Since(pr.sentAt) <= pr.timeout {
|
||||
pr.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if pr.timeout < 0 {
|
||||
pr.status = RECEIPT_CULLED
|
||||
} else {
|
||||
pr.status = RECEIPT_FAILED
|
||||
}
|
||||
|
||||
pr.concludedAt = time.Now()
|
||||
callback := pr.timeoutCallback
|
||||
pr.mutex.Unlock()
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Packet receipt timed out", "hash", fmt.Sprintf("%x", pr.truncatedHash))
|
||||
|
||||
if callback != nil {
|
||||
go callback(pr)
|
||||
}
|
||||
}
|
||||
|
||||
func (pr *PacketReceipt) timeoutWatchdog() {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
pr.checkTimeout()
|
||||
|
||||
pr.mutex.RLock()
|
||||
status := pr.status
|
||||
pr.mutex.RUnlock()
|
||||
|
||||
if status != RECEIPT_SENT {
|
||||
return
|
||||
}
|
||||
case <-pr.timeoutCheckDone:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pr *PacketReceipt) SetTimeout(timeout time.Duration) {
|
||||
pr.mutex.Lock()
|
||||
defer pr.mutex.Unlock()
|
||||
pr.timeout = timeout
|
||||
}
|
||||
|
||||
func (pr *PacketReceipt) SetDeliveryCallback(callback func(*PacketReceipt)) {
|
||||
pr.mutex.Lock()
|
||||
defer pr.mutex.Unlock()
|
||||
pr.deliveryCallback = callback
|
||||
}
|
||||
|
||||
func (pr *PacketReceipt) SetTimeoutCallback(callback func(*PacketReceipt)) {
|
||||
pr.mutex.Lock()
|
||||
defer pr.mutex.Unlock()
|
||||
pr.timeoutCallback = callback
|
||||
}
|
||||
|
||||
func (pr *PacketReceipt) SetDestinationIdentity(ident *identity.Identity) {
|
||||
pr.mutex.Lock()
|
||||
defer pr.mutex.Unlock()
|
||||
pr.destinationIdent = ident
|
||||
}
|
||||
|
||||
func (pr *PacketReceipt) SetLink(link interface{}) {
|
||||
pr.mutex.Lock()
|
||||
defer pr.mutex.Unlock()
|
||||
pr.link = link
|
||||
}
|
||||
|
||||
func (pr *PacketReceipt) Cancel() {
|
||||
pr.mutex.Lock()
|
||||
defer pr.mutex.Unlock()
|
||||
|
||||
if pr.status == RECEIPT_SENT {
|
||||
pr.status = RECEIPT_CULLED
|
||||
pr.concludedAt = time.Now()
|
||||
}
|
||||
|
||||
select {
|
||||
case pr.timeoutCheckDone <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
209
pkg/packet/receipt_test.go
Normal file
209
pkg/packet/receipt_test.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package packet
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
||||
)
|
||||
|
||||
func TestPacketReceiptCreation(t *testing.T) {
|
||||
testIdent, err := identity.NewIdentity()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create identity: %v", err)
|
||||
}
|
||||
|
||||
destHash := testIdent.Hash()
|
||||
data := []byte("test packet data")
|
||||
|
||||
pkt := &Packet{
|
||||
HeaderType: HeaderType1,
|
||||
PacketType: PacketTypeData,
|
||||
TransportType: 0,
|
||||
Context: ContextNone,
|
||||
ContextFlag: FlagUnset,
|
||||
Hops: 0,
|
||||
DestinationType: 0x00,
|
||||
DestinationHash: destHash,
|
||||
Data: data,
|
||||
CreateReceipt: true,
|
||||
}
|
||||
|
||||
if err := pkt.Pack(); err != nil {
|
||||
t.Fatalf("Failed to pack packet: %v", err)
|
||||
}
|
||||
|
||||
receipt := NewPacketReceipt(pkt)
|
||||
if receipt == nil {
|
||||
t.Fatal("Receipt creation failed")
|
||||
}
|
||||
|
||||
if receipt.GetStatus() != RECEIPT_SENT {
|
||||
t.Errorf("Expected status SENT, got %d", receipt.GetStatus())
|
||||
}
|
||||
|
||||
hash := receipt.GetHash()
|
||||
if len(hash) == 0 {
|
||||
t.Error("Receipt hash is empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketReceiptTimeout(t *testing.T) {
|
||||
testIdent, err := identity.NewIdentity()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create identity: %v", err)
|
||||
}
|
||||
|
||||
destHash := testIdent.Hash()
|
||||
data := []byte("test data")
|
||||
|
||||
pkt := &Packet{
|
||||
HeaderType: HeaderType1,
|
||||
PacketType: PacketTypeData,
|
||||
TransportType: 0,
|
||||
Context: ContextNone,
|
||||
ContextFlag: FlagUnset,
|
||||
Hops: 0,
|
||||
DestinationType: 0x00,
|
||||
DestinationHash: destHash,
|
||||
Data: data,
|
||||
CreateReceipt: true,
|
||||
}
|
||||
|
||||
if err := pkt.Pack(); err != nil {
|
||||
t.Fatalf("Failed to pack packet: %v", err)
|
||||
}
|
||||
|
||||
receipt := NewPacketReceipt(pkt)
|
||||
receipt.SetTimeout(100 * time.Millisecond)
|
||||
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
|
||||
if !receipt.IsTimedOut() {
|
||||
t.Error("Receipt should be timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketReceiptProofValidation(t *testing.T) {
|
||||
testIdent, err := identity.NewIdentity()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create identity: %v", err)
|
||||
}
|
||||
|
||||
destHash := testIdent.Hash()
|
||||
data := []byte("test data")
|
||||
|
||||
pkt := &Packet{
|
||||
HeaderType: HeaderType1,
|
||||
PacketType: PacketTypeData,
|
||||
TransportType: 0,
|
||||
Context: ContextNone,
|
||||
ContextFlag: FlagUnset,
|
||||
Hops: 0,
|
||||
DestinationType: 0x00,
|
||||
DestinationHash: destHash,
|
||||
Data: data,
|
||||
CreateReceipt: true,
|
||||
}
|
||||
|
||||
if err := pkt.Pack(); err != nil {
|
||||
t.Fatalf("Failed to pack packet: %v", err)
|
||||
}
|
||||
|
||||
receipt := NewPacketReceipt(pkt)
|
||||
receipt.SetDestinationIdentity(testIdent)
|
||||
|
||||
packetHash := pkt.GetHash()
|
||||
t.Logf("Packet hash: %x", packetHash)
|
||||
|
||||
signature := testIdent.Sign(packetHash)
|
||||
|
||||
t.Logf("PacketHash length: %d", len(packetHash))
|
||||
t.Logf("Signature length: %d", len(signature))
|
||||
t.Logf("EXPL_LENGTH constant: %d", EXPL_LENGTH)
|
||||
|
||||
if testIdent.Verify(packetHash, signature) {
|
||||
t.Log("Direct verification succeeded")
|
||||
} else {
|
||||
t.Error("Direct verification failed")
|
||||
}
|
||||
|
||||
proof := make([]byte, 0, EXPL_LENGTH)
|
||||
proof = append(proof, packetHash...)
|
||||
proof = append(proof, signature...)
|
||||
|
||||
t.Logf("Proof length: %d", len(proof))
|
||||
|
||||
proofPacket := &Packet{
|
||||
PacketType: PacketTypeProof,
|
||||
Data: proof,
|
||||
}
|
||||
|
||||
if !receipt.ValidateProof(proof, proofPacket) {
|
||||
t.Errorf("Valid proof was rejected. Proof len=%d, expected=%d", len(proof), EXPL_LENGTH)
|
||||
}
|
||||
|
||||
if receipt.GetStatus() != RECEIPT_DELIVERED {
|
||||
t.Errorf("Expected status DELIVERED, got %d", receipt.GetStatus())
|
||||
}
|
||||
|
||||
if !receipt.IsDelivered() {
|
||||
t.Error("Receipt should be marked as delivered")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketReceiptCallbacks(t *testing.T) {
|
||||
testIdent, err := identity.NewIdentity()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create identity: %v", err)
|
||||
}
|
||||
|
||||
destHash := testIdent.Hash()
|
||||
data := []byte("test data")
|
||||
|
||||
pkt := &Packet{
|
||||
HeaderType: HeaderType1,
|
||||
PacketType: PacketTypeData,
|
||||
TransportType: 0,
|
||||
Context: ContextNone,
|
||||
ContextFlag: FlagUnset,
|
||||
Hops: 0,
|
||||
DestinationType: 0x00,
|
||||
DestinationHash: destHash,
|
||||
Data: data,
|
||||
CreateReceipt: true,
|
||||
}
|
||||
|
||||
if err := pkt.Pack(); err != nil {
|
||||
t.Fatalf("Failed to pack packet: %v", err)
|
||||
}
|
||||
|
||||
receipt := NewPacketReceipt(pkt)
|
||||
receipt.SetDestinationIdentity(testIdent)
|
||||
|
||||
deliveryCalled := make(chan bool, 1)
|
||||
receipt.SetDeliveryCallback(func(r *PacketReceipt) {
|
||||
deliveryCalled <- true
|
||||
})
|
||||
|
||||
packetHash := pkt.GetHash()
|
||||
signature := testIdent.Sign(packetHash)
|
||||
|
||||
proof := make([]byte, 0, EXPL_LENGTH)
|
||||
proof = append(proof, packetHash...)
|
||||
proof = append(proof, signature...)
|
||||
|
||||
proofPacket := &Packet{
|
||||
PacketType: PacketTypeProof,
|
||||
Data: proof,
|
||||
}
|
||||
|
||||
receipt.ValidateProof(proof, proofPacket)
|
||||
|
||||
select {
|
||||
case <-deliveryCalled:
|
||||
// Success
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("Delivery callback was not called")
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user