Compare commits
410 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 | ||
|
|
ae9a35e3bb | ||
|
|
32d32380d8 | ||
|
|
5e40f0bfe8 | ||
| 315b35fc81 | |||
| 54dec6aa89 | |||
| 92c8faec11 | |||
| 2aff4989e5 | |||
| f1d2a31be6 | |||
| f604d1a3c8 | |||
| 26a54436f7 | |||
| 2fd85a1034 | |||
| c8e81cd9f0 | |||
| 2f61ce9bf3 | |||
| b647e7c6c2 | |||
| 6b3990d399 | |||
| 041b439a66 | |||
| 534982b99d | |||
| 7379d07aba | |||
| 03345bc256 | |||
| e486923e8f | |||
| d7f41b785f | |||
| 15303a21dc | |||
| 4d4863aeeb | |||
| 76a4103a56 | |||
| 96348ce349 | |||
|
|
322711ba20 | ||
|
|
772248b31f | ||
|
|
fa1c80169e | ||
|
|
cb1e4a1115 | ||
|
|
836e97b17d | ||
|
|
87d3b4a58b | ||
|
|
77729e07e1 | ||
|
|
79e1caa815 | ||
|
|
a5b905bbaf | ||
|
|
c870406244 | ||
|
|
ea8daf6bb2 | ||
|
|
d79406e354 | ||
|
|
f9b8d29780 | ||
|
|
0cebfb2193 | ||
|
|
9e229287e8 | ||
|
|
9508e6e195 | ||
|
|
5acbef454f | ||
|
|
0862830431 | ||
|
|
6cdc02346f | ||
|
|
3ffd5b72a1 | ||
|
|
73af84e24f | ||
|
|
ae40d2879c | ||
|
|
a2499e4a15 | ||
|
|
30ea1dd0c7 | ||
|
|
785bc7d782 | ||
|
|
144f5bea6a | ||
|
|
a3c701e205 | ||
|
|
a8a7607eb6 | ||
|
|
a2947a3adb | ||
|
|
2cb37102fb | ||
|
|
54c401e2a5 | ||
|
|
8df4039b18 | ||
|
|
12156adae9 | ||
|
|
a34e3d274e | ||
|
|
f3d22dfcd4 | ||
|
|
99d8e44182 | ||
|
|
083991c997 | ||
|
|
9ca24d96ab | ||
|
|
b478ca346e | ||
|
|
20b532e005 | ||
|
|
80eac50632 | ||
|
|
f15d8f6a84 | ||
|
|
c523d6f542 | ||
|
|
8a175e3051 | ||
|
|
28d46921d3 | ||
|
|
613ceddb0b | ||
|
|
599dd91979 | ||
|
|
e724886578 | ||
|
|
3034c0b0b4 | ||
|
|
3ed2c67742 | ||
|
|
f2c146b7c5 | ||
|
|
59cef5e56a | ||
|
|
ef613cc873 | ||
|
|
7a7ce84778 | ||
|
|
7ef7e60a87 | ||
|
|
73349d4a28 | ||
|
|
31128a6758 | ||
|
|
566ce5da96 | ||
|
|
139926be05 | ||
|
|
decbd8f29a | ||
|
|
0f5f5cbb13 | ||
|
|
a2476c9551 | ||
|
|
bfc75a2290 | ||
|
|
2e01fa565d |
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 }}*
|
||||||
31
.gitignore
vendored
31
.gitignore
vendored
@@ -1,8 +1,31 @@
|
|||||||
reticulum-client
|
# Build artifacts
|
||||||
reticulum-server
|
|
||||||
|
|
||||||
bin/
|
bin/
|
||||||
|
|
||||||
|
# Test coverage reports
|
||||||
|
coverage.out
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
*.log
|
||||||
logs/
|
logs/
|
||||||
|
|
||||||
|
# Local environment variables
|
||||||
.env
|
.env
|
||||||
.json
|
|
||||||
|
# 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
|
||||||
7
CONTRIBUTING.md
Normal file
7
CONTRIBUTING.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
Send issues, suggestions, `.patch` files, or any feedback to one of the preferred methods:
|
||||||
|
|
||||||
|
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
|
||||||
12
LICENSE
Normal file
12
LICENSE
Normal file
@@ -0,0 +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.
|
||||||
|
|
||||||
|
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.
|
||||||
213
README.md
213
README.md
@@ -1,4 +1,215 @@
|
|||||||
# Reticulum-Go
|
# Reticulum-Go
|
||||||
|
|
||||||
Reticulum Network Stack in Go.
|
[](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)
|
||||||
|
|
||||||
|
A high-performance Go implementation of the [Reticulum Network Stack](https://github.com/markqvist/Reticulum)
|
||||||
|
|
||||||
|
## Project Goals:
|
||||||
|
|
||||||
|
- **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
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Go 1.24 or later
|
||||||
|
- [Task](https://taskfile.dev/) for build automation
|
||||||
|
|
||||||
|
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
|
||||||
|
nix develop
|
||||||
|
```
|
||||||
|
|
||||||
|
This will enter a development environment with Go and Task pre-configured.
|
||||||
|
|
||||||
|
## 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.
|
||||||
24
SECURITY.md
Normal file
24
SECURITY.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supply Chain Security
|
||||||
|
|
||||||
|
- 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 `v0.46.0` for core cryptographic primitives
|
||||||
|
- hkdf
|
||||||
|
- curve25519
|
||||||
|
|
||||||
|
- go/crypto
|
||||||
|
- ed25519
|
||||||
|
- sha256
|
||||||
|
- rand
|
||||||
|
- aes
|
||||||
|
- cipher
|
||||||
|
- hmac
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Refer to [https://quad4.io/security](https://quad4.io/security) for how to report vulnerabilities.
|
||||||
11
TODO.md
Normal file
11
TODO.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Working on creating a project and issues to better track things. Check out https://git.quad4.io/Networks/Reticulum-Go/projects/2
|
||||||
|
|
||||||
|
## Todo
|
||||||
|
|
||||||
|
- 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
|
||||||
102
To-Do
102
To-Do
@@ -1,102 +0,0 @@
|
|||||||
To-Do List
|
|
||||||
|
|
||||||
Core Components
|
|
||||||
[âś“] Basic Configuration System
|
|
||||||
[âś“] Basic config structure
|
|
||||||
[âś“] Default settings
|
|
||||||
[âś“] Config file loading/saving
|
|
||||||
[âś“] Path management
|
|
||||||
|
|
||||||
[âś“] Constants Definition
|
|
||||||
[âś“] Packet constants
|
|
||||||
[âś“] MTU constants
|
|
||||||
[âś“] Header types
|
|
||||||
[âś“] Additional protocol constants
|
|
||||||
|
|
||||||
[âś“] Identity Management
|
|
||||||
[âś“] Identity creation
|
|
||||||
[âś“] Key pair generation
|
|
||||||
[âś“] Identity storage/recall
|
|
||||||
|
|
||||||
[âś“] Packet Handling
|
|
||||||
[âś“] Packet creation
|
|
||||||
[âś“] Packet validation
|
|
||||||
[âś“] Basic proof system
|
|
||||||
|
|
||||||
[âś“] Crypto Implementation
|
|
||||||
[âś“] Basic encryption
|
|
||||||
[âś“] Key exchange
|
|
||||||
[âś“] Hash functions
|
|
||||||
[âś“] Ratchet implementation
|
|
||||||
|
|
||||||
[âś“] Transport Layer
|
|
||||||
[âś“] Path management
|
|
||||||
[âś“] Basic packet routing
|
|
||||||
[âś“] Announce handling
|
|
||||||
[âś“] Link management
|
|
||||||
[âś“] Resource cleanup
|
|
||||||
[âś“] Network layer integration
|
|
||||||
|
|
||||||
[âś“] Destination System
|
|
||||||
[âś“] Destination creation
|
|
||||||
[âś“] Destination types (IN/OUT)
|
|
||||||
[âś“] Destination aspects
|
|
||||||
[âś“] Announce implementation
|
|
||||||
[âś“] Ratchet support
|
|
||||||
[âś“] Request handlers
|
|
||||||
|
|
||||||
[âś“] Link System
|
|
||||||
[âś“] Link establishment
|
|
||||||
[âś“] Link teardown
|
|
||||||
[âś“] Basic packet transfer
|
|
||||||
[âś“] Encryption/Decryption
|
|
||||||
[âś“] Identity verification
|
|
||||||
[âś“] Request/Response handling
|
|
||||||
|
|
||||||
[âś“] Resource System
|
|
||||||
[âś“] Resource creation
|
|
||||||
[âś“] Resource transfer
|
|
||||||
[âś“] Compression
|
|
||||||
[âś“] Progress tracking
|
|
||||||
[âś“] Segmentation
|
|
||||||
[âś“] Cleanup routines
|
|
||||||
|
|
||||||
Basic Features
|
|
||||||
[âś“] Network Interface
|
|
||||||
[âś“] Basic UDP transport
|
|
||||||
[âś“] TCP transport
|
|
||||||
[ ] Interface discovery
|
|
||||||
[ ] Connection management
|
|
||||||
[âś“] Packet framing
|
|
||||||
[âś“] Transport integration
|
|
||||||
|
|
||||||
[âś“] Announce System
|
|
||||||
[âś“] Announce creation
|
|
||||||
[âś“] Announce propagation
|
|
||||||
[âś“] Path requests
|
|
||||||
|
|
||||||
[âś“] Resource Management
|
|
||||||
[âś“] Resource tracking
|
|
||||||
[âś“] Memory management
|
|
||||||
[âś“] Cleanup routines
|
|
||||||
|
|
||||||
[âś“] Client Implementation
|
|
||||||
[âś“] Basic client structure
|
|
||||||
[âś“] Configuration handling
|
|
||||||
[âś“] Interactive mode
|
|
||||||
[âś“] Link establishment
|
|
||||||
[âś“] Message sending/receiving
|
|
||||||
|
|
||||||
Next Immediate Tasks:
|
|
||||||
1. [âś“] Fix import cycles by creating common package
|
|
||||||
2. [ ] Implement Interface discovery
|
|
||||||
3. [ ] Implement Connection management
|
|
||||||
4. [ ] Test network layer integration end-to-end
|
|
||||||
5. [ ] Add error handling for network failures
|
|
||||||
6. [ ] Implement interface auto-configuration
|
|
||||||
7. [ ] Complete NetworkInterface implementation
|
|
||||||
8. [ ] Add comprehensive interface tests
|
|
||||||
9. [ ] Implement connection retry logic
|
|
||||||
10. [ ] Add metrics collection for interfaces
|
|
||||||
11. [ ] Add client reconnection handling
|
|
||||||
12. [ ] Implement client-side path caching
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/internal/config"
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/destination"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
configPath = flag.String("config", "", "Path to config file")
|
|
||||||
targetHash = flag.String("target", "", "Target destination hash")
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
var cfg *common.ReticulumConfig
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if *configPath == "" {
|
|
||||||
cfg, err = config.InitConfig()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to initialize config: %v", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cfg, err = config.LoadConfig(*configPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to load config: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable transport by default for client
|
|
||||||
cfg.EnableTransport = true
|
|
||||||
|
|
||||||
// Initialize transport
|
|
||||||
transport, err := transport.NewTransport(cfg)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to initialize transport: %v", err)
|
|
||||||
}
|
|
||||||
defer transport.Close()
|
|
||||||
|
|
||||||
// If target specified, establish connection
|
|
||||||
if *targetHash != "" {
|
|
||||||
destHash, err := identity.HashFromHex(*targetHash)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Invalid destination hash: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request path if needed
|
|
||||||
if !transport.HasPath(destHash) {
|
|
||||||
fmt.Println("Requesting path to destination...")
|
|
||||||
if err := transport.RequestPath(destHash, "", nil, true); err != nil {
|
|
||||||
log.Fatalf("Failed to request path: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get destination identity
|
|
||||||
destIdentity, err := identity.Recall(destHash)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to recall identity: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create destination
|
|
||||||
dest, err := destination.New(
|
|
||||||
destIdentity,
|
|
||||||
destination.OUT,
|
|
||||||
destination.SINGLE,
|
|
||||||
"client",
|
|
||||||
"direct",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to create destination: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable and configure ratchets
|
|
||||||
dest.SetRetainedRatchets(destination.RATCHET_COUNT)
|
|
||||||
dest.SetRatchetInterval(destination.RATCHET_INTERVAL)
|
|
||||||
dest.EnforceRatchets()
|
|
||||||
|
|
||||||
// Create link
|
|
||||||
link := transport.NewLink(dest.Hash(), func() {
|
|
||||||
fmt.Println("Link established")
|
|
||||||
}, func() {
|
|
||||||
fmt.Println("Link closed")
|
|
||||||
})
|
|
||||||
|
|
||||||
defer link.Teardown()
|
|
||||||
|
|
||||||
// Set packet callback
|
|
||||||
link.SetPacketCallback(func(data []byte) {
|
|
||||||
fmt.Printf("Received: %s\n", string(data))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Start interactive loop
|
|
||||||
go interactiveLoop(link)
|
|
||||||
} else {
|
|
||||||
fmt.Println("No target specified. Use -target <hash> to connect to a destination")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for interrupt
|
|
||||||
sigChan := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
<-sigChan
|
|
||||||
}
|
|
||||||
|
|
||||||
func interactiveLoop(link *transport.Link) {
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
|
||||||
for {
|
|
||||||
fmt.Print("> ")
|
|
||||||
input, err := reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error reading input: %v\n", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
input = strings.TrimSpace(input)
|
|
||||||
if input == "quit" || input == "exit" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := link.Send([]byte(input)); err != nil {
|
|
||||||
fmt.Printf("Failed to send: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
642
cmd/reticulum-go/main.go
Normal file
642
cmd/reticulum-go/main.go
Normal file
@@ -0,0 +1,642 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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 (
|
||||||
|
interceptPackets = flag.Bool("intercept-packets", false, "Enable packet interception")
|
||||||
|
interceptOutput = flag.String("intercept-output", "packets.log", "Output file for intercepted packets")
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
APP_NAME = "Reticulum-Go Test Node"
|
||||||
|
APP_ASPECT = "node" // Always use "node" for node announces
|
||||||
|
)
|
||||||
|
|
||||||
|
type Reticulum struct {
|
||||||
|
config *common.ReticulumConfig
|
||||||
|
transport *transport.Transport
|
||||||
|
interfaces []interfaces.Interface
|
||||||
|
channels map[string]*channel.Channel
|
||||||
|
buffers map[string]*buffer.Buffer
|
||||||
|
pathRequests map[string]*common.PathRequest
|
||||||
|
announceHistory map[string]announceRecord
|
||||||
|
announceHistoryMu sync.RWMutex
|
||||||
|
identity *identity.Identity
|
||||||
|
destination *destination.Destination
|
||||||
|
storage *storage.Manager
|
||||||
|
|
||||||
|
// Node-specific information
|
||||||
|
maxTransferSize int16 // Max transfer size in KB
|
||||||
|
nodeEnabled bool // Whether this node is enabled
|
||||||
|
nodeTimestamp int64 // Last node announcement timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
type announceRecord struct {
|
||||||
|
timestamp int64
|
||||||
|
appData []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
cfg = config.DefaultConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default app name and aspect if not provided
|
||||||
|
if cfg.AppName == "" {
|
||||||
|
cfg.AppName = APP_NAME
|
||||||
|
}
|
||||||
|
if cfg.AppAspect == "" {
|
||||||
|
cfg.AppAspect = APP_ASPECT // Always use "node" for node announcements
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := initializeDirectories(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize directories: %v", err)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
debug.Log(debug.DEBUG_INFO, "Transport initialized")
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create destination
|
||||||
|
debug.Log(debug.DEBUG_INFO, "Creating destination...")
|
||||||
|
dest, err := destination.New(
|
||||||
|
ident,
|
||||||
|
destination.IN,
|
||||||
|
destination.SINGLE,
|
||||||
|
"nomadnetwork",
|
||||||
|
t,
|
||||||
|
"node",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create destination: %v", err)
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
|
||||||
|
r := &Reticulum{
|
||||||
|
config: cfg,
|
||||||
|
transport: t,
|
||||||
|
interfaces: make([]interfaces.Interface, 0),
|
||||||
|
channels: make(map[string]*channel.Channel),
|
||||||
|
buffers: make(map[string]*buffer.Buffer),
|
||||||
|
pathRequests: make(map[string]*common.PathRequest),
|
||||||
|
announceHistory: make(map[string]announceRecord),
|
||||||
|
identity: ident,
|
||||||
|
destination: dest,
|
||||||
|
storage: storageMgr,
|
||||||
|
|
||||||
|
// Node-specific information
|
||||||
|
maxTransferSize: common.NUM_500, // Default 500KB
|
||||||
|
nodeEnabled: true, // Enabled by default
|
||||||
|
nodeTimestamp: nodeTimestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable destination features
|
||||||
|
dest.AcceptsLinks(true)
|
||||||
|
// Enable ratchets and point to a file for persistence.
|
||||||
|
// The actual path should probably be configurable.
|
||||||
|
ratchetPath := ".git.quad4.io/Networks/Reticulum-Go/storage/ratchets/" + r.identity.GetHexHash()
|
||||||
|
dest.EnableRatchets(ratchetPath)
|
||||||
|
dest.SetProofStrategy(destination.PROVE_APP)
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "Configured destination features")
|
||||||
|
|
||||||
|
// Initialize interfaces from config
|
||||||
|
for name, ifaceConfig := range cfg.Interfaces {
|
||||||
|
if !ifaceConfig.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var iface interfaces.Interface
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch ifaceConfig.Type {
|
||||||
|
case common.STR_TCP_CLIENT:
|
||||||
|
iface, err = interfaces.NewTCPClientInterface(
|
||||||
|
name,
|
||||||
|
ifaceConfig.TargetHost,
|
||||||
|
ifaceConfig.TargetPort,
|
||||||
|
ifaceConfig.KISSFraming,
|
||||||
|
ifaceConfig.I2PTunneled,
|
||||||
|
ifaceConfig.Enabled,
|
||||||
|
)
|
||||||
|
case "UDPInterface":
|
||||||
|
iface, err = interfaces.NewUDPInterface(
|
||||||
|
name,
|
||||||
|
ifaceConfig.Address,
|
||||||
|
ifaceConfig.TargetHost,
|
||||||
|
ifaceConfig.Enabled,
|
||||||
|
)
|
||||||
|
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:
|
||||||
|
debug.Log(debug.DEBUG_CRITICAL, "Unknown interface type", common.STR_TYPE, ifaceConfig.Type)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if cfg.PanicOnInterfaceErr {
|
||||||
|
return nil, fmt.Errorf("failed to create 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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Configuring interface", common.STR_NAME, name, common.STR_TYPE, ifaceConfig.Type)
|
||||||
|
r.interfaces = append(r.interfaces, iface)
|
||||||
|
debug.Log(debug.DEBUG_INFO, "Interface started successfully", common.STR_NAME, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reticulum) handleInterface(iface common.NetworkInterface) {
|
||||||
|
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
|
||||||
|
|
||||||
|
rw := buffer.CreateBidirectionalBuffer(
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
ch,
|
||||||
|
func(size int) {
|
||||||
|
data := make([]byte, size)
|
||||||
|
debug.Log(debug.DEBUG_PACKETS, "Interface reading bytes from buffer", common.STR_NAME, iface.GetName(), "size", size)
|
||||||
|
iface.ProcessIncoming(data)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
r.buffers[iface.GetName()] = &buffer.Buffer{
|
||||||
|
ReadWriter: rw,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reticulum) monitorInterfaces() {
|
||||||
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
for _, iface := range r.interfaces {
|
||||||
|
if tcpClient, ok := iface.(*interfaces.TCPClientInterface); ok {
|
||||||
|
stats := fmt.Sprintf("Interface %s status - Connected: %v, TX: %d bytes (%.2f Kbps), RX: %d bytes (%.2f Kbps)",
|
||||||
|
iface.GetName(),
|
||||||
|
tcpClient.IsConnected(),
|
||||||
|
tcpClient.GetTxBytes(),
|
||||||
|
float64(tcpClient.GetTxBytes()*8)/(5*1024),
|
||||||
|
tcpClient.GetRxBytes(),
|
||||||
|
float64(tcpClient.GetRxBytes()*8)/(5*1024),
|
||||||
|
)
|
||||||
|
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
stats = fmt.Sprintf("%s, RTT: %v", stats, tcpClient.GetRTT())
|
||||||
|
}
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "Interface status", "stats", stats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
debug.Init()
|
||||||
|
debug.Log(debug.DEBUG_CRITICAL, "Initializing Reticulum", "debug_level", debug.GetDebugLevel())
|
||||||
|
|
||||||
|
cfg, err := config.InitConfig()
|
||||||
|
if err != nil {
|
||||||
|
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 {
|
||||||
|
debug.GetLogger().Error("Failed to create Reticulum instance", common.STR_ERROR, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start monitoring interfaces
|
||||||
|
go r.monitorInterfaces()
|
||||||
|
|
||||||
|
// Register announce handler
|
||||||
|
handler := NewAnnounceHandler(r, []string{"*"})
|
||||||
|
r.transport.RegisterAnnounceHandler(handler)
|
||||||
|
|
||||||
|
// Start Reticulum
|
||||||
|
if err := r.Start(); err != nil {
|
||||||
|
debug.GetLogger().Error("Failed to start Reticulum", common.STR_ERROR, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-sigChan
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_CRITICAL, "Shutting down...")
|
||||||
|
if err := r.Stop(); err != nil {
|
||||||
|
debug.Log(debug.DEBUG_CRITICAL, "Error during shutdown", common.STR_ERROR, err)
|
||||||
|
}
|
||||||
|
debug.Log(debug.DEBUG_CRITICAL, "Goodbye!")
|
||||||
|
}
|
||||||
|
|
||||||
|
type transportWrapper struct {
|
||||||
|
*transport.Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *transportWrapper) GetRTT() float64 {
|
||||||
|
return 0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *transportWrapper) RTT() float64 {
|
||||||
|
return tw.GetRTT()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *transportWrapper) GetStatus() byte {
|
||||||
|
return transport.STATUS_ACTIVE
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *transportWrapper) Send(data []byte) interface{} {
|
||||||
|
p := &packet.Packet{
|
||||||
|
PacketType: packet.PacketTypeData,
|
||||||
|
Hops: 0,
|
||||||
|
Data: data,
|
||||||
|
HeaderType: packet.HeaderType1,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := tw.Transport.SendPacket(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *transportWrapper) Resend(p interface{}) error {
|
||||||
|
if pkt, ok := p.(*packet.Packet); ok {
|
||||||
|
return tw.Transport.SendPacket(pkt)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("invalid packet type")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *transportWrapper) SetPacketTimeout(packet interface{}, callback func(interface{}), timeout time.Duration) {
|
||||||
|
time.AfterFunc(timeout, func() {
|
||||||
|
callback(packet)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *transportWrapper) SetPacketDelivered(packet interface{}, callback func(interface{})) {
|
||||||
|
callback(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (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{
|
||||||
|
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, common.NUM_0700); err != nil { // #nosec G301
|
||||||
|
return fmt.Errorf("failed to create directory %s: %v", dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reticulum) Start() error {
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Starting Reticulum...")
|
||||||
|
|
||||||
|
if err := r.transport.Start(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start transport: %v", err)
|
||||||
|
}
|
||||||
|
debug.Log(debug.DEBUG_INFO, "Transport started successfully")
|
||||||
|
|
||||||
|
// Start interfaces
|
||||||
|
for _, iface := range r.interfaces {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
debug.Log(debug.DEBUG_INFO, "Interface started successfully", "name", iface.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for interfaces to initialize
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start periodic announce goroutine
|
||||||
|
go func() {
|
||||||
|
// Wait a bit before the first announce
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
for {
|
||||||
|
debug.Log(debug.DEBUG_INFO, "Announcing destination...")
|
||||||
|
err := r.destination.Announce(false, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
debug.Log(debug.DEBUG_CRITICAL, "Could not send announce", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(60 * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go r.monitorInterfaces()
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Reticulum started successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reticulum) Stop() error {
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Stopping Reticulum...")
|
||||||
|
|
||||||
|
for _, buf := range r.buffers {
|
||||||
|
if err := buf.Close(); err != nil {
|
||||||
|
debug.Log(debug.DEBUG_CRITICAL, "Error closing buffer", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ch := range r.channels {
|
||||||
|
if err := ch.Close(); err != nil {
|
||||||
|
debug.Log(debug.DEBUG_CRITICAL, "Error closing channel", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, iface := range r.interfaces {
|
||||||
|
if err := iface.Stop(); err != nil {
|
||||||
|
debug.Log(debug.DEBUG_CRITICAL, "Error stopping interface", "name", iface.GetName(), "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.transport.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close transport: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Reticulum stopped successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnnounceHandler struct {
|
||||||
|
aspectFilter []string
|
||||||
|
reticulum *Reticulum
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnnounceHandler(r *Reticulum, aspectFilter []string) *AnnounceHandler {
|
||||||
|
return &AnnounceHandler{
|
||||||
|
aspectFilter: aspectFilter,
|
||||||
|
reticulum: r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnnounceHandler) AspectFilter() []string {
|
||||||
|
return h.aspectFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appData []byte, 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 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])
|
||||||
|
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 {
|
||||||
|
// 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 {
|
||||||
|
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()
|
||||||
|
debug.Log(debug.DEBUG_ALL, "Active ratchets", "count", len(ratchets))
|
||||||
|
|
||||||
|
if len(ratchets) > 0 {
|
||||||
|
ratchetKey := identity.GetCurrentRatchetKey()
|
||||||
|
if ratchetKey != nil {
|
||||||
|
ratchetID := identity.GetRatchetID(ratchetKey)
|
||||||
|
debug.Log(debug.DEBUG_ALL, "Current ratchet ID", "id", fmt.Sprintf("%x", ratchetID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a better record with more info
|
||||||
|
recordType := "peer"
|
||||||
|
if isNode {
|
||||||
|
recordType = "node"
|
||||||
|
debug.Log(debug.DEBUG_INFO, "Storing node in announce history", "enabled", nodeEnabled, "timestamp", nodeTimestamp, "maxsize", fmt.Sprintf("%dKB", nodeMaxSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
h.reticulum.announceHistoryMu.Lock()
|
||||||
|
h.reticulum.announceHistory[identity.GetHexHash()] = announceRecord{
|
||||||
|
timestamp: time.Now().Unix(),
|
||||||
|
appData: appData,
|
||||||
|
}
|
||||||
|
h.reticulum.announceHistoryMu.Unlock()
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "Stored announce in history", "type", recordType, "identity", identity.GetHexHash())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnnounceHandler) ReceivePathResponses() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reticulum) GetDestination() *destination.Destination {
|
||||||
|
return r.destination
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reticulum) createNodeAppData() []byte {
|
||||||
|
// Create a msgpack array with 3 elements
|
||||||
|
// [Bool, Int32, Int16] for [enable, timestamp, max_transfer_size]
|
||||||
|
appData := []byte{common.HEX_0x93} // Array with 3 elements
|
||||||
|
|
||||||
|
// Element 0: Boolean for enable/disable peer
|
||||||
|
if r.nodeEnabled {
|
||||||
|
appData = append(appData, common.HEX_0xC3) // true
|
||||||
|
} else {
|
||||||
|
appData = append(appData, common.HEX_0xC2) // false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Element 1: Int32 timestamp (current time)
|
||||||
|
r.nodeTimestamp = time.Now().Unix()
|
||||||
|
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, common.HEX_0xD1) // int16 format
|
||||||
|
sizeBytes := make([]byte, common.TWO)
|
||||||
|
binary.BigEndian.PutUint16(sizeBytes, uint16(r.maxTransferSize)) // #nosec G115
|
||||||
|
appData = append(appData, sizeBytes...)
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/internal/config"
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/interfaces"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Reticulum struct {
|
|
||||||
config *config.ReticulumConfig
|
|
||||||
transport *transport.Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewReticulum(cfg *config.ReticulumConfig) (*Reticulum, error) {
|
|
||||||
if cfg == nil {
|
|
||||||
cfg = config.DefaultConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize transport
|
|
||||||
t, err := transport.NewTransport(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Reticulum{
|
|
||||||
config: cfg,
|
|
||||||
transport: t,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reticulum) Start() error {
|
|
||||||
// Initialize interfaces based on config
|
|
||||||
for _, ifaceConfig := range r.config.Interfaces {
|
|
||||||
var iface interfaces.Interface
|
|
||||||
|
|
||||||
switch ifaceConfig.Type {
|
|
||||||
case "tcp":
|
|
||||||
client, err := interfaces.NewTCPClient(
|
|
||||||
ifaceConfig.Name,
|
|
||||||
ifaceConfig.Address,
|
|
||||||
ifaceConfig.Port,
|
|
||||||
ifaceConfig.KISSFraming,
|
|
||||||
ifaceConfig.I2PTunneled,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to create TCP interface %s: %v", ifaceConfig.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
iface = client
|
|
||||||
|
|
||||||
case "tcpserver":
|
|
||||||
server, err := interfaces.NewTCPServer(
|
|
||||||
ifaceConfig.Name,
|
|
||||||
ifaceConfig.Address,
|
|
||||||
ifaceConfig.Port,
|
|
||||||
ifaceConfig.PreferIPv6,
|
|
||||||
ifaceConfig.I2PTunneled,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to create TCP server interface %s: %v", ifaceConfig.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
iface = server
|
|
||||||
|
|
||||||
default:
|
|
||||||
log.Printf("Unknown interface type: %s", ifaceConfig.Type)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set packet callback to transport
|
|
||||||
iface.SetPacketCallback(r.transport.HandlePacket)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Reticulum initialized with config at: %s", r.config.ConfigPath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reticulum) Stop() error {
|
|
||||||
if err := r.transport.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Initialize configuration
|
|
||||||
cfg, err := config.InitConfig()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to initialize config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new reticulum instance
|
|
||||||
r, err := NewReticulum(cfg)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to create Reticulum instance: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start reticulum
|
|
||||||
if err := r.Start(); err != nil {
|
|
||||||
log.Fatalf("Failed to start Reticulum: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for interrupt signal
|
|
||||||
sigChan := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
<-sigChan
|
|
||||||
|
|
||||||
// Clean shutdown
|
|
||||||
if err := r.Stop(); err != nil {
|
|
||||||
log.Printf("Error during shutdown: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
enable_transport = true
|
|
||||||
share_instance = true
|
|
||||||
shared_instance_port = 37428
|
|
||||||
instance_control_port = 37429
|
|
||||||
panic_on_interface_error = false
|
|
||||||
loglevel = 4
|
|
||||||
|
|
||||||
[interfaces]
|
|
||||||
[interfaces."Local TCP"]
|
|
||||||
type = "TCPClientInterface"
|
|
||||||
enabled = true
|
|
||||||
target_host = "127.0.0.1"
|
|
||||||
target_port = 4242
|
|
||||||
|
|
||||||
[interfaces."Local UDP"]
|
|
||||||
type = "UDPInterface"
|
|
||||||
enabled = true
|
|
||||||
interface = "lo"
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
enable_transport = true
|
|
||||||
share_instance = true
|
|
||||||
shared_instance_port = 37430
|
|
||||||
instance_control_port = 37431
|
|
||||||
panic_on_interface_error = false
|
|
||||||
loglevel = 4
|
|
||||||
|
|
||||||
[interfaces]
|
|
||||||
[interfaces."Local TCP"]
|
|
||||||
type = "TCPClientInterface"
|
|
||||||
enabled = true
|
|
||||||
target_host = "127.0.0.1"
|
|
||||||
target_port = 4243
|
|
||||||
|
|
||||||
[interfaces."Local UDP"]
|
|
||||||
type = "UDPInterface"
|
|
||||||
enabled = true
|
|
||||||
interface = "lo"
|
|
||||||
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";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
11
go.mod
11
go.mod
@@ -1,9 +1,10 @@
|
|||||||
module github.com/Sudo-Ivan/reticulum-go
|
module git.quad4.io/Networks/Reticulum-Go
|
||||||
|
|
||||||
go 1.23.4
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/pelletier/go-toml v1.9.5
|
github.com/vmihailenco/msgpack/v5 v5.4.1
|
||||||
golang.org/x/crypto v0.31.0
|
golang.org/x/crypto v0.46.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
|
|||||||
22
go.sum
22
go.sum
@@ -1,8 +1,14 @@
|
|||||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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,11 +1,16 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/pelletier/go-toml"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -16,13 +21,13 @@ const (
|
|||||||
|
|
||||||
func DefaultConfig() *common.ReticulumConfig {
|
func DefaultConfig() *common.ReticulumConfig {
|
||||||
return &common.ReticulumConfig{
|
return &common.ReticulumConfig{
|
||||||
EnableTransport: false,
|
EnableTransport: true,
|
||||||
ShareInstance: true,
|
ShareInstance: true,
|
||||||
SharedInstancePort: DefaultSharedInstancePort,
|
SharedInstancePort: DefaultSharedInstancePort,
|
||||||
InstanceControlPort: DefaultInstanceControlPort,
|
InstanceControlPort: DefaultInstanceControlPort,
|
||||||
PanicOnInterfaceErr: false,
|
PanicOnInterfaceErr: false,
|
||||||
LogLevel: DefaultLogLevel,
|
LogLevel: DefaultLogLevel,
|
||||||
Interfaces: make(map[string]common.InterfaceConfig),
|
Interfaces: make(map[string]*common.InterfaceConfig),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +36,7 @@ func GetConfigPath() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return filepath.Join(homeDir, ".reticulum", "config"), nil
|
return filepath.Join(homeDir, ".reticulum-go", "config"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnsureConfigDir() error {
|
func EnsureConfigDir() error {
|
||||||
@@ -40,65 +45,211 @@ func EnsureConfigDir() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Join(homeDir, ".reticulum")
|
configDir := filepath.Join(homeDir, ".reticulum-go")
|
||||||
return os.MkdirAll(configDir, 0755)
|
return os.MkdirAll(configDir, 0700) // #nosec G301
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseValue parses string values into appropriate types
|
||||||
|
func parseValue(value string) interface{} {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
|
||||||
|
// Try bool
|
||||||
|
if value == "true" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if value == "false" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try int
|
||||||
|
if i, err := strconv.Atoi(value); err == nil {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return as string
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfig loads the configuration from the specified path
|
// LoadConfig loads the configuration from the specified path
|
||||||
func LoadConfig(path string) (*common.ReticulumConfig, error) {
|
func LoadConfig(path string) (*common.ReticulumConfig, error) {
|
||||||
data, err := os.ReadFile(path)
|
// bearer:disable go_gosec_filesystem_filereadtaint
|
||||||
|
file, err := os.Open(path) // #nosec G304
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
cfg := DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
if err := toml.Unmarshal(data, cfg); err != nil {
|
cfg.ConfigPath = path
|
||||||
return nil, err
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
var currentInterface *common.InterfaceConfig
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
|
// Skip comments and empty lines
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle interface sections
|
||||||
|
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||||
|
name := strings.Trim(line, "[]")
|
||||||
|
currentInterface = &common.InterfaceConfig{Name: name}
|
||||||
|
cfg.Interfaces[name] = currentInterface
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
|
||||||
|
if currentInterface != nil {
|
||||||
|
// Parse interface config
|
||||||
|
switch key {
|
||||||
|
case "type":
|
||||||
|
currentInterface.Type = value
|
||||||
|
case "enabled":
|
||||||
|
currentInterface.Enabled = value == "true"
|
||||||
|
case "address":
|
||||||
|
currentInterface.Address = value
|
||||||
|
case "port":
|
||||||
|
currentInterface.Port, _ = strconv.Atoi(value)
|
||||||
|
case "target_host":
|
||||||
|
currentInterface.TargetHost = value
|
||||||
|
case "target_port":
|
||||||
|
currentInterface.TargetPort, _ = strconv.Atoi(value)
|
||||||
|
case "discovery_port":
|
||||||
|
currentInterface.DiscoveryPort, _ = strconv.Atoi(value)
|
||||||
|
case "data_port":
|
||||||
|
currentInterface.DataPort, _ = strconv.Atoi(value)
|
||||||
|
case "discovery_scope":
|
||||||
|
currentInterface.DiscoveryScope = value
|
||||||
|
case "group_id":
|
||||||
|
currentInterface.GroupID = value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Parse global config
|
||||||
|
switch key {
|
||||||
|
case "enable_transport":
|
||||||
|
cfg.EnableTransport = value == "true"
|
||||||
|
case "share_instance":
|
||||||
|
cfg.ShareInstance = value == "true"
|
||||||
|
case "shared_instance_port":
|
||||||
|
cfg.SharedInstancePort, _ = strconv.Atoi(value)
|
||||||
|
case "instance_control_port":
|
||||||
|
cfg.InstanceControlPort, _ = strconv.Atoi(value)
|
||||||
|
case "panic_on_interface_error":
|
||||||
|
cfg.PanicOnInterfaceErr = value == "true"
|
||||||
|
case "loglevel":
|
||||||
|
cfg.LogLevel, _ = strconv.Atoi(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.ConfigPath = path
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveConfig saves the configuration to the specified path
|
// SaveConfig saves the configuration to the specified path
|
||||||
func SaveConfig(cfg *common.ReticulumConfig) error {
|
func SaveConfig(cfg *common.ReticulumConfig) error {
|
||||||
data, err := toml.Marshal(cfg)
|
if cfg.ConfigPath == "" {
|
||||||
if err != nil {
|
return fmt.Errorf("config path not set")
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(cfg.ConfigPath, data, 0644)
|
var builder strings.Builder
|
||||||
|
|
||||||
|
// Write global config
|
||||||
|
builder.WriteString("# Reticulum Configuration\n")
|
||||||
|
builder.WriteString(fmt.Sprintf("enable_transport = %v\n", cfg.EnableTransport))
|
||||||
|
builder.WriteString(fmt.Sprintf("share_instance = %v\n", cfg.ShareInstance))
|
||||||
|
builder.WriteString(fmt.Sprintf("shared_instance_port = %d\n", cfg.SharedInstancePort))
|
||||||
|
builder.WriteString(fmt.Sprintf("instance_control_port = %d\n", cfg.InstanceControlPort))
|
||||||
|
builder.WriteString(fmt.Sprintf("panic_on_interface_error = %v\n", cfg.PanicOnInterfaceErr))
|
||||||
|
builder.WriteString(fmt.Sprintf("loglevel = %d\n\n", cfg.LogLevel))
|
||||||
|
|
||||||
|
// Write interface configs
|
||||||
|
for name, iface := range cfg.Interfaces {
|
||||||
|
builder.WriteString(fmt.Sprintf("[%s]\n", name))
|
||||||
|
builder.WriteString(fmt.Sprintf("type = %s\n", iface.Type))
|
||||||
|
builder.WriteString(fmt.Sprintf("enabled = %v\n", iface.Enabled))
|
||||||
|
|
||||||
|
if iface.Address != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("address = %s\n", iface.Address))
|
||||||
|
}
|
||||||
|
if iface.Port != 0 {
|
||||||
|
builder.WriteString(fmt.Sprintf("port = %d\n", iface.Port))
|
||||||
|
}
|
||||||
|
if iface.TargetHost != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("target_host = %s\n", iface.TargetHost))
|
||||||
|
}
|
||||||
|
if iface.TargetPort != 0 {
|
||||||
|
builder.WriteString(fmt.Sprintf("target_port = %d\n", iface.TargetPort))
|
||||||
|
}
|
||||||
|
if iface.DiscoveryPort != 0 {
|
||||||
|
builder.WriteString(fmt.Sprintf("discovery_port = %d\n", iface.DiscoveryPort))
|
||||||
|
}
|
||||||
|
if iface.DataPort != 0 {
|
||||||
|
builder.WriteString(fmt.Sprintf("data_port = %d\n", iface.DataPort))
|
||||||
|
}
|
||||||
|
if iface.DiscoveryScope != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("discovery_scope = %s\n", iface.DiscoveryScope))
|
||||||
|
}
|
||||||
|
if iface.GroupID != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("group_id = %s\n", iface.GroupID))
|
||||||
|
}
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(cfg.ConfigPath, []byte(builder.String()), 0600) // #nosec G306
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateDefaultConfig creates a default configuration file
|
// CreateDefaultConfig creates a default configuration file
|
||||||
func CreateDefaultConfig(path string) error {
|
func CreateDefaultConfig(path string) error {
|
||||||
cfg := DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
|
cfg.ConfigPath = path
|
||||||
|
|
||||||
// Add default interface
|
cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{
|
||||||
cfg.Interfaces["Default Interface"] = common.InterfaceConfig{
|
|
||||||
Type: "AutoInterface",
|
Type: "AutoInterface",
|
||||||
Enabled: false,
|
Enabled: true,
|
||||||
|
GroupID: "reticulum",
|
||||||
|
DiscoveryScope: "link",
|
||||||
|
DiscoveryPort: 29716,
|
||||||
|
DataPort: 42671,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add default quad4net interface
|
cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{
|
||||||
cfg.Interfaces["quad4net tcp"] = common.InterfaceConfig{
|
Type: "TCPClientInterface",
|
||||||
|
Enabled: true,
|
||||||
|
TargetHost: "127.0.0.1",
|
||||||
|
TargetPort: 4242,
|
||||||
|
Name: "Go-RNS-Testnet",
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Interfaces["Quad4 TCP"] = &common.InterfaceConfig{
|
||||||
Type: "TCPClientInterface",
|
Type: "TCPClientInterface",
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
TargetHost: "rns.quad4.io",
|
TargetHost: "rns.quad4.io",
|
||||||
TargetPort: 4242,
|
TargetPort: 4242,
|
||||||
|
Name: "Quad4 TCP",
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := toml.Marshal(cfg)
|
cfg.Interfaces["Local UDP"] = &common.InterfaceConfig{
|
||||||
if err != nil {
|
Type: "UDPInterface",
|
||||||
|
Enabled: false,
|
||||||
|
Address: "0.0.0.0",
|
||||||
|
Port: 37696,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { // #nosec G301
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create config directory if it doesn't exist
|
return SaveConfig(cfg)
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.WriteFile(path, data, 0644)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitConfig initializes the configuration system
|
// InitConfig initializes the configuration system
|
||||||
|
|||||||
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,109 +1,164 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package announce
|
package announce
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
||||||
|
"golang.org/x/crypto/curve25519"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
PACKET_TYPE_DATA = 0x00
|
||||||
|
PACKET_TYPE_ANNOUNCE = 0x01
|
||||||
|
PACKET_TYPE_LINK = 0x02
|
||||||
|
PACKET_TYPE_PROOF = 0x03
|
||||||
|
|
||||||
|
// Announce Types
|
||||||
ANNOUNCE_NONE = 0x00
|
ANNOUNCE_NONE = 0x00
|
||||||
ANNOUNCE_PATH = 0x01
|
ANNOUNCE_PATH = 0x01
|
||||||
ANNOUNCE_IDENTITY = 0x02
|
ANNOUNCE_IDENTITY = 0x02
|
||||||
|
|
||||||
|
// Header Types
|
||||||
|
HEADER_TYPE_1 = 0x00 // One address field
|
||||||
|
HEADER_TYPE_2 = 0x01 // Two address fields
|
||||||
|
|
||||||
|
// Propagation Types
|
||||||
|
PROP_TYPE_BROADCAST = 0x00
|
||||||
|
PROP_TYPE_TRANSPORT = 0x01
|
||||||
|
|
||||||
|
DEST_TYPE_SINGLE = 0x00
|
||||||
|
DEST_TYPE_GROUP = 0x01
|
||||||
|
DEST_TYPE_PLAIN = 0x02
|
||||||
|
DEST_TYPE_LINK = 0x03
|
||||||
|
|
||||||
|
// IFAC Flag
|
||||||
|
IFAC_NONE = 0x00
|
||||||
|
IFAC_AUTH = 0x80
|
||||||
|
|
||||||
MAX_HOPS = 128
|
MAX_HOPS = 128
|
||||||
PROPAGATION_RATE = 0.02 // 2% of interface bandwidth
|
PROPAGATION_RATE = 0.02 // 2% of interface bandwidth
|
||||||
RETRY_INTERVAL = 300 // 5 minutes
|
RETRY_INTERVAL = 300 // 5 minutes
|
||||||
MAX_RETRIES = 3
|
MAX_RETRIES = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
type AnnounceHandler interface {
|
|
||||||
AspectFilter() []string
|
|
||||||
ReceivedAnnounce(destinationHash []byte, announcedIdentity *identity.Identity, appData []byte) error
|
|
||||||
ReceivePathResponses() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Announce struct {
|
type Announce struct {
|
||||||
mutex sync.RWMutex
|
mutex *sync.RWMutex
|
||||||
destinationHash []byte
|
destinationHash []byte
|
||||||
|
destinationName string
|
||||||
identity *identity.Identity
|
identity *identity.Identity
|
||||||
appData []byte
|
appData []byte
|
||||||
|
config *common.ReticulumConfig
|
||||||
hops uint8
|
hops uint8
|
||||||
timestamp int64
|
timestamp int64
|
||||||
signature []byte
|
signature []byte
|
||||||
pathResponse bool
|
pathResponse bool
|
||||||
retries int
|
retries int
|
||||||
handlers []AnnounceHandler
|
handlers []Handler
|
||||||
|
ratchetID []byte
|
||||||
|
packet []byte
|
||||||
|
hash []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(dest *identity.Identity, appData []byte, pathResponse bool) (*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")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(destinationHash) == 0 {
|
||||||
|
return nil, errors.New("destination hash required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if destinationName == "" {
|
||||||
|
return nil, errors.New("destination name required")
|
||||||
|
}
|
||||||
|
|
||||||
a := &Announce{
|
a := &Announce{
|
||||||
|
mutex: &sync.RWMutex{},
|
||||||
identity: dest,
|
identity: dest,
|
||||||
|
destinationHash: destinationHash,
|
||||||
|
destinationName: destinationName,
|
||||||
appData: appData,
|
appData: appData,
|
||||||
|
config: config,
|
||||||
hops: 0,
|
hops: 0,
|
||||||
timestamp: time.Now().Unix(),
|
timestamp: time.Now().Unix(),
|
||||||
pathResponse: pathResponse,
|
pathResponse: pathResponse,
|
||||||
retries: 0,
|
retries: 0,
|
||||||
handlers: make([]AnnounceHandler, 0),
|
handlers: make([]Handler, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate destination hash
|
// Get current ratchet ID if enabled
|
||||||
hash := sha256.New()
|
currentRatchet := dest.GetCurrentRatchetKey()
|
||||||
hash.Write(dest.GetPublicKey())
|
if currentRatchet != nil {
|
||||||
a.destinationHash = hash.Sum(nil)[:16] // Truncated hash
|
ratchetPub, err := curve25519.X25519(currentRatchet, curve25519.Basepoint)
|
||||||
|
if err == nil {
|
||||||
|
a.ratchetID = dest.GetRatchetID(ratchetPub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sign the announce
|
// Sign announce data
|
||||||
signData := append(a.destinationHash, a.appData...)
|
signData := append(a.destinationHash, a.appData...)
|
||||||
|
if a.ratchetID != nil {
|
||||||
|
signData = append(signData, a.ratchetID...)
|
||||||
|
}
|
||||||
a.signature = dest.Sign(signData)
|
a.signature = dest.Sign(signData)
|
||||||
|
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Announce) Propagate(interfaces []transport.Interface) error {
|
func (a *Announce) Propagate(interfaces []common.NetworkInterface) error {
|
||||||
a.mutex.Lock()
|
a.mutex.RLock()
|
||||||
defer a.mutex.Unlock()
|
defer a.mutex.RUnlock()
|
||||||
|
|
||||||
if a.hops >= MAX_HOPS {
|
debug.Log(debug.DEBUG_TRACE, "Propagating announce across interfaces", "count", len(interfaces))
|
||||||
return errors.New("maximum hop count reached")
|
|
||||||
|
var packet []byte
|
||||||
|
if a.packet != nil {
|
||||||
|
debug.Log(debug.DEBUG_TRACE, "Using cached packet", "bytes", len(a.packet))
|
||||||
|
packet = a.packet
|
||||||
|
} else {
|
||||||
|
debug.Log(debug.DEBUG_TRACE, "Creating new packet")
|
||||||
|
packet = a.CreatePacket()
|
||||||
|
a.packet = packet
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment hop count
|
|
||||||
a.hops++
|
|
||||||
|
|
||||||
// Create announce packet
|
|
||||||
packet := make([]byte, 0)
|
|
||||||
packet = append(packet, a.destinationHash...)
|
|
||||||
packet = append(packet, a.identity.GetPublicKey()...)
|
|
||||||
packet = append(packet, byte(a.hops))
|
|
||||||
|
|
||||||
if a.appData != nil {
|
|
||||||
packet = append(packet, a.appData...)
|
|
||||||
}
|
|
||||||
|
|
||||||
packet = append(packet, a.signature...)
|
|
||||||
|
|
||||||
// Propagate to all interfaces
|
|
||||||
for _, iface := range interfaces {
|
for _, iface := range interfaces {
|
||||||
if err := iface.SendAnnounce(packet, a.pathResponse); err != nil {
|
if !iface.IsEnabled() {
|
||||||
return err
|
debug.Log(debug.DEBUG_TRACE, "Skipping disabled interface", "name", iface.GetName())
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
if !iface.GetBandwidthAvailable() {
|
||||||
|
debug.Log(debug.DEBUG_TRACE, "Skipping interface with insufficient bandwidth", "name", iface.GetName())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_TRACE, "Sending announce on interface", "name", iface.GetName())
|
||||||
|
if err := iface.Send(packet, ""); err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
debug.Log(debug.DEBUG_TRACE, "Successfully sent announce on interface", "name", iface.GetName())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Announce) RegisterHandler(handler AnnounceHandler) {
|
func (a *Announce) RegisterHandler(handler Handler) {
|
||||||
a.mutex.Lock()
|
a.mutex.Lock()
|
||||||
defer a.mutex.Unlock()
|
defer a.mutex.Unlock()
|
||||||
a.handlers = append(a.handlers, handler)
|
a.handlers = append(a.handlers, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Announce) DeregisterHandler(handler AnnounceHandler) {
|
func (a *Announce) DeregisterHandler(handler Handler) {
|
||||||
a.mutex.Lock()
|
a.mutex.Lock()
|
||||||
defer a.mutex.Unlock()
|
defer a.mutex.Unlock()
|
||||||
for i, h := range a.handlers {
|
for i, h := range a.handlers {
|
||||||
@@ -118,28 +173,113 @@ func (a *Announce) HandleAnnounce(data []byte) error {
|
|||||||
a.mutex.Lock()
|
a.mutex.Lock()
|
||||||
defer a.mutex.Unlock()
|
defer a.mutex.Unlock()
|
||||||
|
|
||||||
// Validate announce data
|
debug.Log(debug.DEBUG_TRACE, "Handling announce packet", "bytes", len(data))
|
||||||
if len(data) < 16+32+1 { // Min size: hash + pubkey + hops
|
|
||||||
return errors.New("invalid announce data")
|
// Minimum packet size validation
|
||||||
|
// header(2) + desthash(16) + context(1) + enckey(32) + signkey(32) + namehash(10) +
|
||||||
|
// randomhash(10) + signature(64) + min app data(3)
|
||||||
|
if len(data) < 170 {
|
||||||
|
debug.Log(debug.DEBUG_TRACE, "Invalid announce data length", "bytes", len(data), "minimum", 170)
|
||||||
|
return errors.New("invalid announce data length")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract fields
|
// Extract header and check packet type
|
||||||
destHash := data[:16]
|
header := data[:2]
|
||||||
pubKey := data[16:48]
|
if header[0]&0x03 != PACKET_TYPE_ANNOUNCE {
|
||||||
hops := data[48]
|
return errors.New("not an announce packet")
|
||||||
appData := data[49 : len(data)-64]
|
}
|
||||||
signature := data[len(data)-64:]
|
|
||||||
|
// Get hop count
|
||||||
|
hopCount := header[1]
|
||||||
|
if hopCount > MAX_HOPS {
|
||||||
|
debug.Log(debug.DEBUG_TRACE, "Announce exceeded max hops", "hops", hopCount)
|
||||||
|
return errors.New("announce exceeded maximum hop count")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the packet based on header type
|
||||||
|
headerType := (header[0] & 0b01000000) >> 6
|
||||||
|
var contextByte byte
|
||||||
|
var packetData []byte
|
||||||
|
|
||||||
|
if headerType == HEADER_TYPE_2 {
|
||||||
|
// Header type 2 format: header(2) + desthash(16) + transportid(16) + context(1) + data
|
||||||
|
if len(data) < 35 {
|
||||||
|
return errors.New("header type 2 packet too short")
|
||||||
|
}
|
||||||
|
destHash := data[2:18]
|
||||||
|
transportID := data[18:34]
|
||||||
|
contextByte = data[34]
|
||||||
|
packetData = data[35:]
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return errors.New("header type 1 packet too short")
|
||||||
|
}
|
||||||
|
destHash := data[2:18]
|
||||||
|
contextByte = data[18]
|
||||||
|
packetData = data[19:]
|
||||||
|
|
||||||
|
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 (32) + Signature (64) + App Data
|
||||||
|
|
||||||
|
if len(packetData) < 180 { // 32 + 32 + 10 + 10 + 32 + 64
|
||||||
|
return errors.New("announce data too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the components
|
||||||
|
encKey := packetData[:32]
|
||||||
|
signKey := packetData[32:64]
|
||||||
|
nameHash := packetData[64:74]
|
||||||
|
randomHash := packetData[74:84]
|
||||||
|
ratchetData := packetData[84:116]
|
||||||
|
signature := packetData[116:180]
|
||||||
|
appData := packetData[180:]
|
||||||
|
|
||||||
|
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
|
||||||
|
if headerType == HEADER_TYPE_2 {
|
||||||
|
destHash = data[2:18]
|
||||||
|
} else {
|
||||||
|
destHash = data[2:18]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine public keys
|
||||||
|
pubKey := append(encKey, signKey...)
|
||||||
|
|
||||||
|
// Create announced identity from public keys
|
||||||
|
announcedIdentity := identity.FromPublicKey(pubKey)
|
||||||
|
if announcedIdentity == nil {
|
||||||
|
return errors.New("invalid identity public key")
|
||||||
|
}
|
||||||
|
|
||||||
// Verify signature
|
// Verify signature
|
||||||
signData := append(destHash, appData...)
|
signedData := make([]byte, 0)
|
||||||
if !a.identity.Verify(signData, signature) {
|
signedData = append(signedData, destHash...)
|
||||||
|
signedData = append(signedData, encKey...)
|
||||||
|
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) {
|
||||||
return errors.New("invalid announce signature")
|
return errors.New("invalid announce signature")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process announce with registered handlers
|
// Process with handlers
|
||||||
for _, handler := range a.handlers {
|
for _, handler := range a.handlers {
|
||||||
if handler.ReceivePathResponses() || !a.pathResponse {
|
if handler.ReceivePathResponses() || !a.pathResponse {
|
||||||
if err := handler.ReceivedAnnounce(destHash, a.identity, appData); err != nil {
|
if err := handler.ReceivedAnnounce(destHash, announcedIdentity, appData, hopCount); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,7 +288,7 @@ func (a *Announce) HandleAnnounce(data []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Announce) RequestPath(destHash []byte, onInterface transport.Interface) error {
|
func (a *Announce) RequestPath(destHash []byte, onInterface common.NetworkInterface) error {
|
||||||
a.mutex.Lock()
|
a.mutex.Lock()
|
||||||
defer a.mutex.Unlock()
|
defer a.mutex.Unlock()
|
||||||
|
|
||||||
@@ -158,9 +298,225 @@ func (a *Announce) RequestPath(destHash []byte, onInterface transport.Interface)
|
|||||||
packet = append(packet, byte(0)) // Initial hop count
|
packet = append(packet, byte(0)) // Initial hop count
|
||||||
|
|
||||||
// Send path request
|
// Send path request
|
||||||
if err := onInterface.SendPathRequest(packet); err != nil {
|
return onInterface.Send(packet, "")
|
||||||
return err
|
}
|
||||||
|
|
||||||
|
// CreateHeader creates a Reticulum packet header according to spec
|
||||||
|
func CreateHeader(ifacFlag byte, headerType byte, contextFlag byte, propType byte, destType byte, packetType byte, hops byte) []byte {
|
||||||
|
header := make([]byte, 2)
|
||||||
|
|
||||||
|
// First byte: [IFAC Flag], [Header Type], [Context Flag], [Propagation Type], [Destination Type] and [Packet Type]
|
||||||
|
header[0] = ifacFlag | (headerType << 6) | (contextFlag << 5) |
|
||||||
|
(propType << 4) | (destType << 2) | packetType
|
||||||
|
|
||||||
|
// Second byte: Number of hops
|
||||||
|
header[1] = hops
|
||||||
|
|
||||||
|
return header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Announce) CreatePacket() []byte {
|
||||||
|
// This function creates the complete announce packet according to the Reticulum specification.
|
||||||
|
// Announce Packet Structure:
|
||||||
|
// [Header (2 bytes)][Dest Hash (16 bytes)][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]
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// 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_1,
|
||||||
|
contextFlag,
|
||||||
|
PROP_TYPE_BROADCAST,
|
||||||
|
DEST_TYPE_SINGLE,
|
||||||
|
PACKET_TYPE_ANNOUNCE,
|
||||||
|
a.hops,
|
||||||
|
)
|
||||||
|
|
||||||
|
// 4. Context Byte
|
||||||
|
contextByte := byte(0)
|
||||||
|
if a.pathResponse {
|
||||||
|
contextByte = 0x0B // PATH_RESPONSE context
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, destHash...)
|
||||||
|
validationData = append(validationData, pubKey...)
|
||||||
|
validationData = append(validationData, nameHash10...)
|
||||||
|
validationData = append(validationData, randomHash...)
|
||||||
|
if len(ratchetData) > 0 {
|
||||||
|
validationData = append(validationData, ratchetData...)
|
||||||
|
}
|
||||||
|
validationData = append(validationData, a.appData...)
|
||||||
|
signature := a.identity.Sign(validationData)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnnouncePacket struct {
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnnouncePacket(pubKey []byte, appData []byte, announceID []byte) *AnnouncePacket {
|
||||||
|
packet := &AnnouncePacket{}
|
||||||
|
|
||||||
|
// Build packet data
|
||||||
|
packet.Data = make([]byte, 0, len(pubKey)+len(appData)+len(announceID)+4)
|
||||||
|
|
||||||
|
// Add header
|
||||||
|
packet.Data = append(packet.Data, PACKET_TYPE_ANNOUNCE)
|
||||||
|
packet.Data = append(packet.Data, ANNOUNCE_IDENTITY)
|
||||||
|
|
||||||
|
// Add public key
|
||||||
|
packet.Data = append(packet.Data, pubKey...)
|
||||||
|
|
||||||
|
// Add app data length and content
|
||||||
|
appDataLen := make([]byte, 2)
|
||||||
|
binary.BigEndian.PutUint16(appDataLen, uint16(len(appData))) // #nosec G115
|
||||||
|
packet.Data = append(packet.Data, appDataLen...)
|
||||||
|
packet.Data = append(packet.Data, appData...)
|
||||||
|
|
||||||
|
// Add announce ID
|
||||||
|
packet.Data = append(packet.Data, announceID...)
|
||||||
|
|
||||||
|
return packet
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAnnounce creates a new announce packet for a destination
|
||||||
|
func NewAnnounce(identity *identity.Identity, 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 {
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Nil identity provided")
|
||||||
|
return nil, errors.New("identity cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("config cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
appData: appData,
|
||||||
|
ratchetID: ratchetID,
|
||||||
|
pathResponse: pathResponse,
|
||||||
|
destinationHash: destHash,
|
||||||
|
hops: 0,
|
||||||
|
mutex: &sync.RWMutex{},
|
||||||
|
handlers: make([]Handler, 0),
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_TRACE, "Created announce object", "destHash", fmt.Sprintf("%x", a.destinationHash), "hops", a.hops)
|
||||||
|
|
||||||
|
// Create initial packet
|
||||||
|
packet := a.CreatePacket()
|
||||||
|
a.packet = packet
|
||||||
|
|
||||||
|
// Generate hash
|
||||||
|
hash := a.Hash()
|
||||||
|
debug.Log(debug.DEBUG_TRACE, "Generated announce hash", "hash", fmt.Sprintf("%x", hash))
|
||||||
|
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Announce) Hash() []byte {
|
||||||
|
if a.hash == nil {
|
||||||
|
// Generate hash from announce data
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write(a.destinationHash)
|
||||||
|
h.Write(a.identity.GetPublicKey())
|
||||||
|
h.Write([]byte{a.hops})
|
||||||
|
h.Write(a.appData)
|
||||||
|
if a.ratchetID != nil {
|
||||||
|
h.Write(a.ratchetID)
|
||||||
|
}
|
||||||
|
a.hash = h.Sum(nil)
|
||||||
|
}
|
||||||
|
return a.hash
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Announce) GetPacket() []byte {
|
||||||
|
a.mutex.Lock()
|
||||||
|
defer a.mutex.Unlock()
|
||||||
|
|
||||||
|
if a.packet == nil {
|
||||||
|
// Use CreatePacket to generate the packet
|
||||||
|
a.packet = a.CreatePacket()
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.packet
|
||||||
}
|
}
|
||||||
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
9
pkg/announce/handler.go
Normal file
9
pkg/announce/handler.go
Normal file
@@ -0,0 +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, hops uint8) error
|
||||||
|
ReceivePathResponses() bool
|
||||||
|
}
|
||||||
266
pkg/buffer/buffer.go
Normal file
266
pkg/buffer/buffer.go
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
package buffer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"compress/bzip2"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/channel"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
StreamIDMax = 0x3fff // 16383
|
||||||
|
MaxChunkLen = 16 * 1024
|
||||||
|
MaxDataLen = 457 // MDU - 2 - 6 (2 for stream header, 6 for channel envelope)
|
||||||
|
CompressTries = 4
|
||||||
|
|
||||||
|
// Stream header flags
|
||||||
|
StreamHeaderEOF = 0x8000
|
||||||
|
StreamHeaderCompressed = 0x4000
|
||||||
|
|
||||||
|
// Message type
|
||||||
|
StreamDataMessageType = 0x01
|
||||||
|
|
||||||
|
// Header size
|
||||||
|
StreamHeaderSize = 2
|
||||||
|
|
||||||
|
// Compression threshold
|
||||||
|
CompressThreshold = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
type StreamDataMessage struct {
|
||||||
|
StreamID uint16
|
||||||
|
Data []byte
|
||||||
|
EOF bool
|
||||||
|
Compressed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *StreamDataMessage) Pack() ([]byte, error) {
|
||||||
|
headerVal := uint16(m.StreamID & StreamIDMax)
|
||||||
|
if m.EOF {
|
||||||
|
headerVal |= StreamHeaderEOF
|
||||||
|
}
|
||||||
|
if m.Compressed {
|
||||||
|
headerVal |= StreamHeaderCompressed
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
if err := binary.Write(buf, binary.BigEndian, headerVal); err != nil { // #nosec G104
|
||||||
|
return nil, err // Or handle the error appropriately
|
||||||
|
}
|
||||||
|
buf.Write(m.Data)
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *StreamDataMessage) GetType() uint16 {
|
||||||
|
return StreamDataMessageType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *StreamDataMessage) Unpack(data []byte) error {
|
||||||
|
if len(data) < StreamHeaderSize {
|
||||||
|
return io.ErrShortBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
header := binary.BigEndian.Uint16(data[:StreamHeaderSize])
|
||||||
|
m.StreamID = header & StreamIDMax
|
||||||
|
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 map[int]func(int)
|
||||||
|
nextCallbackID int
|
||||||
|
messageHandlerID int
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRawChannelReader(streamID int, ch *channel.Channel) *RawChannelReader {
|
||||||
|
reader := &RawChannelReader{
|
||||||
|
streamID: streamID,
|
||||||
|
channel: ch,
|
||||||
|
buffer: bytes.NewBuffer(nil),
|
||||||
|
callbacks: make(map[int]func(int)),
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.messageHandlerID = ch.AddMessageHandler(reader.HandleMessage)
|
||||||
|
return reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RawChannelReader) AddReadyCallback(cb func(int)) int {
|
||||||
|
r.mutex.Lock()
|
||||||
|
defer r.mutex.Unlock()
|
||||||
|
id := r.nextCallbackID
|
||||||
|
r.nextCallbackID++
|
||||||
|
r.callbacks[id] = cb
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RawChannelReader) RemoveReadyCallback(id int) {
|
||||||
|
r.mutex.Lock()
|
||||||
|
defer r.mutex.Unlock()
|
||||||
|
delete(r.callbacks, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RawChannelReader) Read(p []byte) (n int, err error) {
|
||||||
|
r.mutex.Lock()
|
||||||
|
defer r.mutex.Unlock()
|
||||||
|
|
||||||
|
if r.buffer.Len() == 0 && r.eof {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = r.buffer.Read(p)
|
||||||
|
if err == io.EOF && !r.eof {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RawChannelReader) HandleMessage(msg channel.MessageBase) bool { // #nosec G115
|
||||||
|
if streamMsg, ok := msg.(*StreamDataMessage); ok && streamMsg.StreamID == uint16(r.streamID) {
|
||||||
|
r.mutex.Lock()
|
||||||
|
defer r.mutex.Unlock()
|
||||||
|
|
||||||
|
if streamMsg.Compressed {
|
||||||
|
decompressed := decompressData(streamMsg.Data)
|
||||||
|
r.buffer.Write(decompressed)
|
||||||
|
} else {
|
||||||
|
r.buffer.Write(streamMsg.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if streamMsg.EOF {
|
||||||
|
r.eof = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify callbacks
|
||||||
|
for _, cb := range r.callbacks {
|
||||||
|
cb(r.buffer.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type RawChannelWriter struct {
|
||||||
|
streamID int
|
||||||
|
channel *channel.Channel
|
||||||
|
eof bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRawChannelWriter(streamID int, ch *channel.Channel) *RawChannelWriter {
|
||||||
|
return &RawChannelWriter{
|
||||||
|
streamID: streamID,
|
||||||
|
channel: ch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *RawChannelWriter) Write(p []byte) (n int, err error) {
|
||||||
|
if len(p) > MaxChunkLen {
|
||||||
|
p = p[:MaxChunkLen]
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := &StreamDataMessage{
|
||||||
|
StreamID: uint16(w.streamID), // #nosec G115
|
||||||
|
Data: p,
|
||||||
|
EOF: w.eof,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p) > CompressThreshold {
|
||||||
|
for try := 1; try < CompressTries; try++ {
|
||||||
|
chunkLen := len(p) / try
|
||||||
|
compressed := compressData(p[:chunkLen])
|
||||||
|
if len(compressed) < MaxDataLen && len(compressed) < chunkLen {
|
||||||
|
msg.Data = compressed
|
||||||
|
msg.Compressed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.channel.Send(msg); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *RawChannelWriter) Close() error {
|
||||||
|
w.eof = true
|
||||||
|
_, err := w.Write(nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Buffer struct {
|
||||||
|
ReadWriter *bufio.ReadWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) Write(p []byte) (n int, err error) {
|
||||||
|
return b.ReadWriter.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) Read(p []byte) (n int, err error) {
|
||||||
|
return b.ReadWriter.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) Close() error {
|
||||||
|
return b.ReadWriter.Writer.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateReader(streamID int, ch *channel.Channel, readyCallback func(int)) *bufio.Reader {
|
||||||
|
raw := NewRawChannelReader(streamID, ch)
|
||||||
|
if readyCallback != nil {
|
||||||
|
raw.AddReadyCallback(readyCallback)
|
||||||
|
}
|
||||||
|
return bufio.NewReader(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateWriter(streamID int, ch *channel.Channel) *bufio.Writer {
|
||||||
|
raw := NewRawChannelWriter(streamID, ch)
|
||||||
|
return bufio.NewWriter(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateBidirectionalBuffer(receiveStreamID, sendStreamID int, ch *channel.Channel, readyCallback func(int)) *bufio.ReadWriter {
|
||||||
|
reader := CreateReader(receiveStreamID, ch, readyCallback)
|
||||||
|
writer := CreateWriter(sendStreamID, ch)
|
||||||
|
return bufio.NewReadWriter(reader, writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func compressData(data []byte) []byte {
|
||||||
|
var compressed bytes.Buffer
|
||||||
|
w := bytes.NewBuffer(data)
|
||||||
|
r := bzip2.NewReader(w)
|
||||||
|
// 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
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
304
pkg/channel/channel.go
Normal file
304
pkg/channel/channel.go
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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 (
|
||||||
|
// Window sizes and thresholds
|
||||||
|
WindowInitial = 2
|
||||||
|
WindowMin = 2
|
||||||
|
WindowMinSlow = 2
|
||||||
|
WindowMinMedium = 5
|
||||||
|
WindowMinFast = 16
|
||||||
|
WindowMaxSlow = 5
|
||||||
|
WindowMaxMedium = 12
|
||||||
|
WindowMaxFast = 48
|
||||||
|
WindowMax = WindowMaxFast
|
||||||
|
WindowFlexibility = 4
|
||||||
|
|
||||||
|
// RTT thresholds
|
||||||
|
RTTFast = 0.18
|
||||||
|
RTTMedium = 0.75
|
||||||
|
RTTSlow = 1.45
|
||||||
|
|
||||||
|
// Sequence numbers
|
||||||
|
SeqMax uint16 = 0xFFFF
|
||||||
|
SeqModulus uint16 = SeqMax
|
||||||
|
|
||||||
|
FastRateThreshold = 10
|
||||||
|
|
||||||
|
// 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
|
||||||
|
type MessageState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
MsgStateNew MessageState = iota
|
||||||
|
MsgStateSent
|
||||||
|
MsgStateDelivered
|
||||||
|
MsgStateFailed
|
||||||
|
)
|
||||||
|
|
||||||
|
// MessageBase defines the interface for messages that can be sent over a channel
|
||||||
|
type MessageBase interface {
|
||||||
|
Pack() ([]byte, error)
|
||||||
|
Unpack([]byte) error
|
||||||
|
GetType() uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel manages reliable message delivery over a transport link
|
||||||
|
type Channel struct {
|
||||||
|
link transport.LinkInterface
|
||||||
|
mutex sync.RWMutex
|
||||||
|
txRing []*Envelope
|
||||||
|
rxRing []*Envelope
|
||||||
|
window int
|
||||||
|
windowMax int
|
||||||
|
windowMin int
|
||||||
|
windowFlex int
|
||||||
|
nextSequence uint16
|
||||||
|
nextRxSequence uint16
|
||||||
|
maxTries int
|
||||||
|
fastRateRounds int
|
||||||
|
medRateRounds int
|
||||||
|
messageHandlers []messageHandlerEntry
|
||||||
|
nextHandlerID int
|
||||||
|
}
|
||||||
|
|
||||||
|
type messageHandlerEntry struct {
|
||||||
|
id int
|
||||||
|
handler func(MessageBase) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Envelope wraps a message with metadata for transmission
|
||||||
|
type Envelope struct {
|
||||||
|
Sequence uint16
|
||||||
|
Message MessageBase
|
||||||
|
Raw []byte
|
||||||
|
Packet interface{}
|
||||||
|
Tries int
|
||||||
|
Timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChannel creates a new Channel instance
|
||||||
|
func NewChannel(link transport.LinkInterface) *Channel {
|
||||||
|
return &Channel{
|
||||||
|
link: link,
|
||||||
|
messageHandlers: make([]messageHandlerEntry, 0),
|
||||||
|
mutex: sync.RWMutex{},
|
||||||
|
windowMax: WindowMaxSlow,
|
||||||
|
windowMin: WindowMinSlow,
|
||||||
|
window: WindowInitial,
|
||||||
|
maxTries: DefaultMaxTries,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send transmits a message over the channel
|
||||||
|
func (c *Channel) Send(msg MessageBase) error {
|
||||||
|
if c.link.GetStatus() != transport.STATUS_ACTIVE {
|
||||||
|
return errors.New("link not ready")
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &Envelope{
|
||||||
|
Sequence: c.nextSequence,
|
||||||
|
Message: msg,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
c.nextSequence = (c.nextSequence + common.ONE) % SeqModulus
|
||||||
|
c.txRing = append(c.txRing, env)
|
||||||
|
c.mutex.Unlock()
|
||||||
|
|
||||||
|
data, err := msg.Pack()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
env.Raw = data
|
||||||
|
packet := c.link.Send(data)
|
||||||
|
env.Packet = packet
|
||||||
|
env.Tries++
|
||||||
|
|
||||||
|
timeout := c.getPacketTimeout(env.Tries)
|
||||||
|
c.link.SetPacketTimeout(packet, c.handleTimeout, timeout)
|
||||||
|
c.link.SetPacketDelivered(packet, c.handleDelivered)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleTimeout handles packet timeout events
|
||||||
|
func (c *Channel) handleTimeout(packet interface{}) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
for _, env := range c.txRing {
|
||||||
|
if env.Packet == packet {
|
||||||
|
if env.Tries >= c.maxTries {
|
||||||
|
// Remove from ring and notify failure
|
||||||
|
return
|
||||||
|
}
|
||||||
|
env.Tries++
|
||||||
|
if err := c.link.Resend(packet); err != nil { // #nosec G104
|
||||||
|
// Handle resend error, e.g., log it or mark envelope as failed
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDelivered handles packet delivery confirmations
|
||||||
|
func (c *Channel) handleDelivered(packet interface{}) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
for i, env := range c.txRing {
|
||||||
|
if env.Packet == packet {
|
||||||
|
c.txRing = append(c.txRing[:i], c.txRing[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Channel) getPacketTimeout(tries int) time.Duration {
|
||||||
|
rtt := c.link.GetRTT()
|
||||||
|
if rtt < RTTMinThreshold {
|
||||||
|
rtt = RTTMinThreshold
|
||||||
|
}
|
||||||
|
|
||||||
|
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) int {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
id := c.nextHandlerID
|
||||||
|
c.nextHandlerID++
|
||||||
|
c.messageHandlers = append(c.messageHandlers, messageHandlerEntry{id: id, handler: handler})
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Channel) RemoveMessageHandler(id int) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
for i, entry := range c.messageHandlers {
|
||||||
|
if entry.id == id {
|
||||||
|
c.messageHandlers = append(c.messageHandlers[:i], c.messageHandlers[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Channel) updateRateThresholds() {
|
||||||
|
rtt := c.link.RTT()
|
||||||
|
|
||||||
|
if rtt > RTTFast {
|
||||||
|
c.fastRateRounds = common.ZERO
|
||||||
|
|
||||||
|
if rtt > RTTMedium {
|
||||||
|
c.medRateRounds = common.ZERO
|
||||||
|
} else {
|
||||||
|
c.medRateRounds++
|
||||||
|
if c.windowMax < WindowMaxMedium && c.medRateRounds == FastRateThreshold {
|
||||||
|
c.windowMax = WindowMaxMedium
|
||||||
|
c.windowMin = WindowMinMedium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.fastRateRounds++
|
||||||
|
if c.windowMax < WindowMaxFast && c.fastRateRounds == FastRateThreshold {
|
||||||
|
c.windowMax = WindowMaxFast
|
||||||
|
c.windowMin = WindowMinFast
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Channel) 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()
|
||||||
|
// Cleanup resources
|
||||||
|
return nil
|
||||||
|
}
|
||||||
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,5 +1,17 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package common
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DEFAULT_SHARED_INSTANCE_PORT = 37428
|
||||||
|
DEFAULT_INSTANCE_CONTROL_PORT = 37429
|
||||||
|
DEFAULT_LOG_LEVEL = 20
|
||||||
|
)
|
||||||
|
|
||||||
// ConfigProvider interface for accessing configuration
|
// ConfigProvider interface for accessing configuration
|
||||||
type ConfigProvider interface {
|
type ConfigProvider interface {
|
||||||
GetConfigPath() string
|
GetConfigPath() string
|
||||||
@@ -9,21 +21,75 @@ type ConfigProvider interface {
|
|||||||
|
|
||||||
// InterfaceConfig represents interface configuration
|
// InterfaceConfig represents interface configuration
|
||||||
type InterfaceConfig struct {
|
type InterfaceConfig struct {
|
||||||
Type string `toml:"type"`
|
Name string
|
||||||
Enabled bool `toml:"enabled"`
|
Type string
|
||||||
TargetHost string `toml:"target_host,omitempty"`
|
Enabled bool
|
||||||
TargetPort int `toml:"target_port,omitempty"`
|
Address string
|
||||||
Interface string `toml:"interface,omitempty"`
|
Port int
|
||||||
|
TargetHost string
|
||||||
|
TargetPort int
|
||||||
|
TargetAddress string
|
||||||
|
Interface string
|
||||||
|
KISSFraming bool
|
||||||
|
I2PTunneled bool
|
||||||
|
PreferIPv6 bool
|
||||||
|
MaxReconnTries int
|
||||||
|
Bitrate int64
|
||||||
|
MTU int
|
||||||
|
GroupID string
|
||||||
|
DiscoveryScope string
|
||||||
|
DiscoveryPort int
|
||||||
|
DataPort int
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReticulumConfig represents the main configuration structure
|
// ReticulumConfig represents the main configuration structure
|
||||||
type ReticulumConfig struct {
|
type ReticulumConfig struct {
|
||||||
EnableTransport bool `toml:"enable_transport"`
|
ConfigPath string
|
||||||
ShareInstance bool `toml:"share_instance"`
|
EnableTransport bool
|
||||||
SharedInstancePort int `toml:"shared_instance_port"`
|
ShareInstance bool
|
||||||
InstanceControlPort int `toml:"instance_control_port"`
|
SharedInstancePort int
|
||||||
PanicOnInterfaceErr bool `toml:"panic_on_interface_error"`
|
InstanceControlPort int
|
||||||
LogLevel int `toml:"loglevel"`
|
PanicOnInterfaceErr bool
|
||||||
ConfigPath string `toml:"-"`
|
LogLevel int
|
||||||
Interfaces map[string]InterfaceConfig
|
Interfaces map[string]*InterfaceConfig
|
||||||
|
AppName string
|
||||||
|
AppAspect string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReticulumConfig creates a new ReticulumConfig with default values
|
||||||
|
func NewReticulumConfig() *ReticulumConfig {
|
||||||
|
return &ReticulumConfig{
|
||||||
|
EnableTransport: true,
|
||||||
|
ShareInstance: false,
|
||||||
|
SharedInstancePort: DEFAULT_SHARED_INSTANCE_PORT,
|
||||||
|
InstanceControlPort: DEFAULT_INSTANCE_CONTROL_PORT,
|
||||||
|
PanicOnInterfaceErr: false,
|
||||||
|
LogLevel: DEFAULT_LOG_LEVEL,
|
||||||
|
Interfaces: make(map[string]*InterfaceConfig),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the configuration is valid
|
||||||
|
func (c *ReticulumConfig) Validate() error {
|
||||||
|
if c.SharedInstancePort < 1 || c.SharedInstancePort > 65535 {
|
||||||
|
return fmt.Errorf("invalid shared instance port: %d", c.SharedInstancePort)
|
||||||
|
}
|
||||||
|
if c.InstanceControlPort < 1 || c.InstanceControlPort > 65535 {
|
||||||
|
return fmt.Errorf("invalid instance control port: %d", c.InstanceControlPort)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultConfig() *ReticulumConfig {
|
||||||
|
return &ReticulumConfig{
|
||||||
|
EnableTransport: true,
|
||||||
|
ShareInstance: false,
|
||||||
|
SharedInstancePort: DEFAULT_SHARED_INSTANCE_PORT,
|
||||||
|
InstanceControlPort: DEFAULT_INSTANCE_CONTROL_PORT,
|
||||||
|
PanicOnInterfaceErr: false,
|
||||||
|
LogLevel: DEFAULT_LOG_LEVEL,
|
||||||
|
Interfaces: make(map[string]*InterfaceConfig),
|
||||||
|
AppName: "Go Client",
|
||||||
|
AppAspect: "node",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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,15 +1,25 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package common
|
package common
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Interface Types
|
// Interface Types
|
||||||
IF_TYPE_UDP InterfaceType = iota
|
IF_TYPE_NONE InterfaceType = iota
|
||||||
|
IF_TYPE_UDP
|
||||||
IF_TYPE_TCP
|
IF_TYPE_TCP
|
||||||
IF_TYPE_UNIX
|
IF_TYPE_UNIX
|
||||||
|
IF_TYPE_I2P
|
||||||
|
IF_TYPE_BLUETOOTH
|
||||||
|
IF_TYPE_SERIAL
|
||||||
|
IF_TYPE_AUTO
|
||||||
|
|
||||||
// Interface Modes
|
// Interface Modes
|
||||||
IF_MODE_FULL InterfaceMode = iota
|
IF_MODE_FULL InterfaceMode = iota
|
||||||
IF_MODE_POINT
|
IF_MODE_POINT
|
||||||
IF_MODE_GATEWAY
|
IF_MODE_GATEWAY
|
||||||
|
IF_MODE_ACCESS_POINT
|
||||||
|
IF_MODE_ROAMING
|
||||||
|
IF_MODE_BOUNDARY
|
||||||
|
|
||||||
// Transport Modes
|
// Transport Modes
|
||||||
TRANSPORT_MODE_DIRECT TransportMode = iota
|
TRANSPORT_MODE_DIRECT TransportMode = iota
|
||||||
@@ -22,7 +32,115 @@ const (
|
|||||||
PATH_STATUS_RELAY
|
PATH_STATUS_RELAY
|
||||||
PATH_STATUS_FAILED
|
PATH_STATUS_FAILED
|
||||||
|
|
||||||
|
// Resource Status
|
||||||
|
RESOURCE_STATUS_PENDING = 0x00
|
||||||
|
RESOURCE_STATUS_ACTIVE = 0x01
|
||||||
|
RESOURCE_STATUS_COMPLETE = 0x02
|
||||||
|
RESOURCE_STATUS_FAILED = 0x03
|
||||||
|
RESOURCE_STATUS_CANCELLED = 0x04
|
||||||
|
|
||||||
|
// Link Status
|
||||||
|
LINK_STATUS_PENDING = 0x00
|
||||||
|
LINK_STATUS_ACTIVE = 0x01
|
||||||
|
LINK_STATUS_CLOSED = 0x02
|
||||||
|
LINK_STATUS_FAILED = 0x03
|
||||||
|
|
||||||
|
// Direction Constants
|
||||||
|
IN = 0x01
|
||||||
|
OUT = 0x02
|
||||||
|
|
||||||
// Common Constants
|
// Common Constants
|
||||||
DEFAULT_MTU = 1500
|
DEFAULT_MTU = 1500
|
||||||
MAX_PACKET_SIZE = 65535
|
MAX_PACKET_SIZE = 65535
|
||||||
|
BITRATE_MINIMUM = 5
|
||||||
|
|
||||||
|
// Timeouts and Intervals
|
||||||
|
ESTABLISH_TIMEOUT = 6
|
||||||
|
KEEPALIVE_INTERVAL = 360
|
||||||
|
STALE_TIME = 720
|
||||||
|
PATH_REQUEST_TTL = 300
|
||||||
|
ANNOUNCE_TIMEOUT = 15
|
||||||
|
|
||||||
|
// 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,45 +1,53 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NetworkInterface combines both low-level and high-level interface requirements
|
// NetworkInterface defines the interface for all network communication methods
|
||||||
type NetworkInterface interface {
|
type NetworkInterface interface {
|
||||||
// Low-level network operations
|
// Core interface operations
|
||||||
Start() error
|
Start() error
|
||||||
Stop() error
|
Stop() error
|
||||||
|
Enable()
|
||||||
|
Disable()
|
||||||
|
Detach()
|
||||||
|
|
||||||
|
// Network operations
|
||||||
Send(data []byte, address string) error
|
Send(data []byte, address string) error
|
||||||
Receive() ([]byte, string, error)
|
GetConn() net.Conn
|
||||||
|
GetMTU() int
|
||||||
|
GetName() string
|
||||||
|
|
||||||
|
// Interface properties
|
||||||
GetType() InterfaceType
|
GetType() InterfaceType
|
||||||
GetMode() InterfaceMode
|
GetMode() InterfaceMode
|
||||||
GetMTU() int
|
IsEnabled() bool
|
||||||
|
IsOnline() bool
|
||||||
|
IsDetached() bool
|
||||||
|
GetBandwidthAvailable() bool
|
||||||
|
|
||||||
// High-level packet operations
|
// Packet handling
|
||||||
ProcessIncoming([]byte)
|
ProcessIncoming([]byte)
|
||||||
ProcessOutgoing([]byte) error
|
ProcessOutgoing([]byte) error
|
||||||
SendPathRequest([]byte) error
|
SendPathRequest([]byte) error
|
||||||
SendLinkPacket([]byte, []byte, time.Time) error
|
SendLinkPacket([]byte, []byte, time.Time) error
|
||||||
Detach()
|
|
||||||
SetPacketCallback(PacketCallback)
|
SetPacketCallback(PacketCallback)
|
||||||
|
GetPacketCallback() PacketCallback
|
||||||
// Additional required fields
|
|
||||||
GetName() string
|
|
||||||
GetConn() net.Conn
|
|
||||||
IsEnabled() bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PacketCallback func([]byte, interface{})
|
// BaseInterface provides common implementation for network interfaces
|
||||||
|
|
||||||
// BaseInterface provides common implementation
|
|
||||||
type BaseInterface struct {
|
type BaseInterface struct {
|
||||||
Name string
|
Name string
|
||||||
Mode InterfaceMode
|
Mode InterfaceMode
|
||||||
Type InterfaceType
|
Type InterfaceType
|
||||||
|
|
||||||
Online bool
|
Online bool
|
||||||
|
Enabled bool
|
||||||
Detached bool
|
Detached bool
|
||||||
|
|
||||||
IN bool
|
IN bool
|
||||||
@@ -50,8 +58,154 @@ type BaseInterface struct {
|
|||||||
|
|
||||||
TxBytes uint64
|
TxBytes uint64
|
||||||
RxBytes uint64
|
RxBytes uint64
|
||||||
|
lastTx time.Time
|
||||||
|
|
||||||
mutex sync.RWMutex
|
Mutex sync.RWMutex
|
||||||
owner interface{}
|
Owner interface{}
|
||||||
packetCallback PacketCallback
|
PacketCallback PacketCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBaseInterface creates a new BaseInterface instance
|
||||||
|
func NewBaseInterface(name string, ifaceType InterfaceType, enabled bool) BaseInterface {
|
||||||
|
return BaseInterface{
|
||||||
|
Name: name,
|
||||||
|
Type: ifaceType,
|
||||||
|
Mode: IF_MODE_FULL,
|
||||||
|
Enabled: enabled,
|
||||||
|
MTU: DEFAULT_MTU,
|
||||||
|
Bitrate: BITRATE_MINIMUM,
|
||||||
|
lastTx: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default implementations for BaseInterface
|
||||||
|
func (i *BaseInterface) GetType() InterfaceType {
|
||||||
|
return i.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetMode() InterfaceMode {
|
||||||
|
return i.Mode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetMTU() int {
|
||||||
|
return i.MTU
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetName() string {
|
||||||
|
return i.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) IsEnabled() bool {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.Enabled && i.Online && !i.Detached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) IsOnline() bool {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.Online
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) IsDetached() bool {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.Detached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) SetPacketCallback(callback PacketCallback) {
|
||||||
|
i.Mutex.Lock()
|
||||||
|
defer i.Mutex.Unlock()
|
||||||
|
i.PacketCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetPacketCallback() PacketCallback {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.PacketCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Detach() {
|
||||||
|
i.Mutex.Lock()
|
||||||
|
defer i.Mutex.Unlock()
|
||||||
|
i.Detached = true
|
||||||
|
i.Online = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Enable() {
|
||||||
|
i.Mutex.Lock()
|
||||||
|
defer i.Mutex.Unlock()
|
||||||
|
i.Enabled = true
|
||||||
|
i.Online = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Disable() {
|
||||||
|
i.Mutex.Lock()
|
||||||
|
defer i.Mutex.Unlock()
|
||||||
|
i.Enabled = false
|
||||||
|
i.Online = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default implementations that should be overridden by specific interfaces
|
||||||
|
func (i *BaseInterface) Start() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Stop() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetConn() net.Conn {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Send(data []byte, address string) error {
|
||||||
|
return i.ProcessOutgoing(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
||||||
|
if i.PacketCallback != nil {
|
||||||
|
i.PacketCallback(data, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) ProcessOutgoing(data []byte) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) SendPathRequest(data []byte) error {
|
||||||
|
return i.Send(data, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.Time) error {
|
||||||
|
// Create link packet
|
||||||
|
packet := make([]byte, 0, len(dest)+len(data)+9) // 1 byte type + dest + 8 byte timestamp
|
||||||
|
packet = append(packet, 0x02) // Link packet type
|
||||||
|
packet = append(packet, dest...)
|
||||||
|
|
||||||
|
ts := make([]byte, 8)
|
||||||
|
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) // #nosec G115
|
||||||
|
packet = append(packet, ts...)
|
||||||
|
|
||||||
|
packet = append(packet, data...)
|
||||||
|
|
||||||
|
return i.Send(packet, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetBandwidthAvailable() bool {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
|
||||||
|
// If no transmission in last second, bandwidth is available
|
||||||
|
if time.Since(i.lastTx) > time.Second {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate current bandwidth usage
|
||||||
|
bytesPerSec := float64(i.TxBytes) / time.Since(i.lastTx).Seconds()
|
||||||
|
currentUsage := bytesPerSec * 8 // Convert to bits/sec
|
||||||
|
|
||||||
|
// Check if usage is below threshold (2% of total bitrate)
|
||||||
|
maxUsage := float64(i.Bitrate) * 0.02 // 2% propagation rate
|
||||||
|
return currentUsage < maxUsage
|
||||||
}
|
}
|
||||||
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,36 +1,84 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Interface related types
|
// Destination type constants
|
||||||
type InterfaceMode byte
|
const (
|
||||||
type InterfaceType byte
|
DESTINATION_SINGLE = 0x00
|
||||||
|
DESTINATION_GROUP = 0x01
|
||||||
|
DESTINATION_PLAIN = 0x02
|
||||||
|
)
|
||||||
|
|
||||||
// Transport related types
|
// Transport related types
|
||||||
type TransportMode byte
|
type TransportMode byte
|
||||||
type PathStatus byte
|
type PathStatus byte
|
||||||
|
|
||||||
// Common structs
|
// Path represents routing information for a destination
|
||||||
type Path struct {
|
type Path struct {
|
||||||
Interface NetworkInterface
|
Interface NetworkInterface
|
||||||
Address string
|
|
||||||
Status PathStatus
|
|
||||||
LastSeen time.Time
|
LastSeen time.Time
|
||||||
NextHop []byte
|
NextHop []byte
|
||||||
Hops uint8
|
Hops uint8
|
||||||
LastUpdated time.Time
|
LastUpdated time.Time
|
||||||
|
HopCount uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common callbacks
|
// Common callbacks
|
||||||
type ProofRequestedCallback func(interface{}) bool
|
type ProofRequestedCallback func([]byte, []byte)
|
||||||
type LinkEstablishedCallback func(interface{})
|
type LinkEstablishedCallback func(interface{})
|
||||||
|
type PacketCallback func([]byte, NetworkInterface)
|
||||||
|
|
||||||
// Request handler
|
// RequestHandler manages path requests and responses
|
||||||
type RequestHandler struct {
|
type RequestHandler struct {
|
||||||
Path string
|
Path string
|
||||||
ResponseGenerator func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity interface{}, requestedAt int64) []byte
|
ResponseGenerator func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity interface{}, requestedAt int64) []byte
|
||||||
AllowMode byte
|
AllowMode byte
|
||||||
AllowedList [][]byte
|
AllowedList [][]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interface types
|
||||||
|
type InterfaceMode byte
|
||||||
|
type InterfaceType byte
|
||||||
|
|
||||||
|
// RatchetIDReceiver holds ratchet ID information
|
||||||
|
type RatchetIDReceiver struct {
|
||||||
|
LatestRatchetID []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkStats holds interface statistics
|
||||||
|
type NetworkStats struct {
|
||||||
|
BytesSent uint64
|
||||||
|
BytesReceived uint64
|
||||||
|
PacketsSent uint64
|
||||||
|
PacketsReceived uint64
|
||||||
|
LastUpdated time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinkStatus represents the current state of a link
|
||||||
|
type LinkStatus struct {
|
||||||
|
Established bool
|
||||||
|
LastSeen time.Time
|
||||||
|
RTT time.Duration
|
||||||
|
Quality float64
|
||||||
|
Hops uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathRequest represents a path discovery request
|
||||||
|
type PathRequest struct {
|
||||||
|
DestinationHash []byte
|
||||||
|
Tag []byte
|
||||||
|
TTL int
|
||||||
|
Recursive bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathResponse represents a path discovery response
|
||||||
|
type PathResponse struct {
|
||||||
|
DestinationHash []byte
|
||||||
|
NextHop []byte
|
||||||
|
Hops uint8
|
||||||
|
Tag []byte
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,49 +1,272 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"bufio"
|
||||||
"gopkg.in/yaml.v3"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Identity struct {
|
Identity struct {
|
||||||
Name string `yaml:"name"`
|
Name string
|
||||||
StoragePath string `yaml:"storage_path"`
|
StoragePath string
|
||||||
} `yaml:"identity"`
|
}
|
||||||
|
|
||||||
Interfaces []struct {
|
Interfaces []struct {
|
||||||
Name string `yaml:"name"`
|
Name string
|
||||||
Type string `yaml:"type"`
|
Type string
|
||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool
|
||||||
ListenPort int `yaml:"listen_port"`
|
ListenPort int
|
||||||
ListenIP string `yaml:"listen_ip"`
|
ListenIP string
|
||||||
KissFraming bool `yaml:"kiss_framing"`
|
KissFraming bool
|
||||||
I2PTunneled bool `yaml:"i2p_tunneled"`
|
I2PTunneled bool
|
||||||
} `yaml:"interfaces"`
|
}
|
||||||
|
|
||||||
Transport struct {
|
Transport struct {
|
||||||
AnnounceInterval int `yaml:"announce_interval"`
|
AnnounceInterval int
|
||||||
PathRequestTimeout int `yaml:"path_request_timeout"`
|
PathRequestTimeout int
|
||||||
MaxHops int `yaml:"max_hops"`
|
MaxHops int
|
||||||
BitrateLimit int64 `yaml:"bitrate_limit"`
|
BitrateLimit int64
|
||||||
} `yaml:"transport"`
|
}
|
||||||
|
|
||||||
Logging struct {
|
Logging struct {
|
||||||
Level string `yaml:"level"`
|
Level string
|
||||||
File string `yaml:"file"`
|
File string
|
||||||
} `yaml:"logging"`
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(path string) (*Config, error) {
|
func LoadConfig(path string) (*Config, error) {
|
||||||
data, err := ioutil.ReadFile(path)
|
// bearer:disable go_gosec_filesystem_filereadtaint
|
||||||
|
file, err := os.Open(path) // #nosec G304
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
var cfg Config
|
cfg := &Config{}
|
||||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
scanner := bufio.NewScanner(file)
|
||||||
|
var currentSection string
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
|
// Skip comments and empty lines
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle sections
|
||||||
|
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||||
|
currentSection = strings.Trim(line, "[]")
|
||||||
|
|
||||||
|
// If this is an interface section, append new interface
|
||||||
|
if strings.HasPrefix(currentSection, "interface ") {
|
||||||
|
cfg.Interfaces = append(cfg.Interfaces, struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Enabled bool
|
||||||
|
ListenPort int
|
||||||
|
ListenIP string
|
||||||
|
KissFraming bool
|
||||||
|
I2PTunneled bool
|
||||||
|
}{})
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse key-value pairs
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
|
||||||
|
switch currentSection {
|
||||||
|
case "identity":
|
||||||
|
switch key {
|
||||||
|
case "name":
|
||||||
|
cfg.Identity.Name = value
|
||||||
|
case "storage_path":
|
||||||
|
cfg.Identity.StoragePath = value
|
||||||
|
}
|
||||||
|
|
||||||
|
case "transport":
|
||||||
|
switch key {
|
||||||
|
case "announce_interval":
|
||||||
|
cfg.Transport.AnnounceInterval, _ = strconv.Atoi(value)
|
||||||
|
case "path_request_timeout":
|
||||||
|
cfg.Transport.PathRequestTimeout, _ = strconv.Atoi(value)
|
||||||
|
case "max_hops":
|
||||||
|
cfg.Transport.MaxHops, _ = strconv.Atoi(value)
|
||||||
|
case "bitrate_limit":
|
||||||
|
cfg.Transport.BitrateLimit, _ = strconv.ParseInt(value, 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "logging":
|
||||||
|
switch key {
|
||||||
|
case "level":
|
||||||
|
cfg.Logging.Level = value
|
||||||
|
case "file":
|
||||||
|
cfg.Logging.File = value
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Handle interface sections
|
||||||
|
if strings.HasPrefix(currentSection, "interface ") && len(cfg.Interfaces) > 0 {
|
||||||
|
iface := &cfg.Interfaces[len(cfg.Interfaces)-1]
|
||||||
|
switch key {
|
||||||
|
case "name":
|
||||||
|
iface.Name = value
|
||||||
|
case "type":
|
||||||
|
iface.Type = value
|
||||||
|
case "enabled":
|
||||||
|
iface.Enabled = value == "true"
|
||||||
|
case "listen_port":
|
||||||
|
iface.ListenPort, _ = strconv.Atoi(value)
|
||||||
|
case "listen_ip":
|
||||||
|
iface.ListenIP = value
|
||||||
|
case "kiss_framing":
|
||||||
|
iface.KissFraming = value == "true"
|
||||||
|
case "i2p_tunneled":
|
||||||
|
iface.I2PTunneled = value == "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cfg, nil
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveConfig(cfg *Config, path string) error {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
// Write Identity section
|
||||||
|
builder.WriteString("[identity]\n")
|
||||||
|
builder.WriteString(fmt.Sprintf("name = %s\n", cfg.Identity.Name))
|
||||||
|
builder.WriteString(fmt.Sprintf("storage_path = %s\n\n", cfg.Identity.StoragePath))
|
||||||
|
|
||||||
|
// Write Transport section
|
||||||
|
builder.WriteString("[transport]\n")
|
||||||
|
builder.WriteString(fmt.Sprintf("announce_interval = %d\n", cfg.Transport.AnnounceInterval))
|
||||||
|
builder.WriteString(fmt.Sprintf("path_request_timeout = %d\n", cfg.Transport.PathRequestTimeout))
|
||||||
|
builder.WriteString(fmt.Sprintf("max_hops = %d\n", cfg.Transport.MaxHops))
|
||||||
|
builder.WriteString(fmt.Sprintf("bitrate_limit = %d\n\n", cfg.Transport.BitrateLimit))
|
||||||
|
|
||||||
|
// Write Logging section
|
||||||
|
builder.WriteString("[logging]\n")
|
||||||
|
builder.WriteString(fmt.Sprintf("level = %s\n", cfg.Logging.Level))
|
||||||
|
builder.WriteString(fmt.Sprintf("file = %s\n\n", cfg.Logging.File))
|
||||||
|
|
||||||
|
// Write Interface sections
|
||||||
|
for _, iface := range cfg.Interfaces {
|
||||||
|
builder.WriteString(fmt.Sprintf("[interface %s]\n", iface.Name))
|
||||||
|
builder.WriteString(fmt.Sprintf("type = %s\n", iface.Type))
|
||||||
|
builder.WriteString(fmt.Sprintf("enabled = %v\n", iface.Enabled))
|
||||||
|
builder.WriteString(fmt.Sprintf("listen_port = %d\n", iface.ListenPort))
|
||||||
|
builder.WriteString(fmt.Sprintf("listen_ip = %s\n", iface.ListenIP))
|
||||||
|
builder.WriteString(fmt.Sprintf("kiss_framing = %v\n", iface.KissFraming))
|
||||||
|
builder.WriteString(fmt.Sprintf("i2p_tunneled = %v\n\n", iface.I2PTunneled))
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(path, []byte(builder.String()), 0600) // #nosec G306
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetConfigDir() string {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to current directory if home directory cannot be determined
|
||||||
|
return ".reticulum-go"
|
||||||
|
}
|
||||||
|
return filepath.Join(homeDir, ".reticulum-go")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDefaultConfigPath() string {
|
||||||
|
return filepath.Join(GetConfigDir(), "config")
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnsureConfigDir() error {
|
||||||
|
configDir := GetConfigDir()
|
||||||
|
return os.MkdirAll(configDir, 0700) // #nosec G301
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitConfig() (*Config, error) {
|
||||||
|
// Ensure config directory exists
|
||||||
|
if err := EnsureConfigDir(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create config directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := GetDefaultConfigPath()
|
||||||
|
|
||||||
|
// Check if config file exists
|
||||||
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
// Create default config
|
||||||
|
cfg := &Config{}
|
||||||
|
|
||||||
|
// Set default values
|
||||||
|
cfg.Identity.Name = "reticulum-node"
|
||||||
|
cfg.Identity.StoragePath = filepath.Join(GetConfigDir(), "storage")
|
||||||
|
|
||||||
|
cfg.Transport.AnnounceInterval = 300
|
||||||
|
cfg.Transport.PathRequestTimeout = 15
|
||||||
|
cfg.Transport.MaxHops = 8
|
||||||
|
cfg.Transport.BitrateLimit = 1000000
|
||||||
|
|
||||||
|
cfg.Logging.Level = "info"
|
||||||
|
cfg.Logging.File = filepath.Join(GetConfigDir(), "reticulum.log")
|
||||||
|
|
||||||
|
cfg.Interfaces = append(cfg.Interfaces, struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Enabled bool
|
||||||
|
ListenPort int
|
||||||
|
ListenIP string
|
||||||
|
KissFraming bool
|
||||||
|
I2PTunneled bool
|
||||||
|
}{
|
||||||
|
Name: "Local UDP",
|
||||||
|
Type: "UDPInterface",
|
||||||
|
Enabled: true,
|
||||||
|
ListenPort: 37697,
|
||||||
|
ListenIP: "0.0.0.0",
|
||||||
|
})
|
||||||
|
|
||||||
|
cfg.Interfaces = append(cfg.Interfaces, struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Enabled bool
|
||||||
|
ListenPort int
|
||||||
|
ListenIP string
|
||||||
|
KissFraming bool
|
||||||
|
I2PTunneled bool
|
||||||
|
}{
|
||||||
|
Name: "Auto Discovery",
|
||||||
|
Type: "AutoInterface",
|
||||||
|
Enabled: true,
|
||||||
|
ListenPort: 29717,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Save default config
|
||||||
|
if err := SaveConfig(cfg, configPath); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save default config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config
|
||||||
|
cfg, err := LoadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
}
|
}
|
||||||
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
112
pkg/cryptography/aes.go
Normal file
112
pkg/cryptography/aes.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
package cryptography
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AES256KeySize is the size of an AES-256 key in bytes.
|
||||||
|
AES256KeySize = 32 // 256 bits
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateAES256Key generates a random AES-256 key.
|
||||||
|
func GenerateAES256Key() ([]byte, error) {
|
||||||
|
key := make([]byte, AES256KeySize)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptAES256CBC encrypts data using AES-256 in CBC mode.
|
||||||
|
// The IV is prepended to the ciphertext.
|
||||||
|
func EncryptAES256CBC(key, plaintext []byte) ([]byte, error) {
|
||||||
|
if len(key) != AES256KeySize {
|
||||||
|
return nil, errors.New("invalid key size: must be 32 bytes for AES-256")
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a random IV.
|
||||||
|
iv := make([]byte, aes.BlockSize)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add PKCS7 padding.
|
||||||
|
padding := aes.BlockSize - len(plaintext)%aes.BlockSize
|
||||||
|
padtext := make([]byte, len(plaintext)+padding)
|
||||||
|
copy(padtext, plaintext)
|
||||||
|
for i := len(plaintext); i < len(padtext); i++ {
|
||||||
|
padtext[i] = byte(padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt the data.
|
||||||
|
mode := cipher.NewCBCEncrypter(block, iv) // #nosec G407
|
||||||
|
ciphertext := make([]byte, len(padtext))
|
||||||
|
mode.CryptBlocks(ciphertext, padtext)
|
||||||
|
|
||||||
|
// Prepend the IV to the ciphertext.
|
||||||
|
return append(iv, ciphertext...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptAES256CBC decrypts data using AES-256 in CBC mode.
|
||||||
|
// It assumes the IV is prepended to the ciphertext.
|
||||||
|
func DecryptAES256CBC(key, ciphertext []byte) ([]byte, error) {
|
||||||
|
if len(key) != AES256KeySize {
|
||||||
|
return nil, errors.New("invalid key size: must be 32 bytes for AES-256")
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ciphertext) < aes.BlockSize {
|
||||||
|
return nil, errors.New("ciphertext is too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the IV from the beginning of the ciphertext.
|
||||||
|
iv := ciphertext[:aes.BlockSize]
|
||||||
|
ciphertext = ciphertext[aes.BlockSize:]
|
||||||
|
|
||||||
|
if len(ciphertext)%aes.BlockSize != 0 {
|
||||||
|
return nil, errors.New("ciphertext is not a multiple of the block size")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt the data.
|
||||||
|
mode := cipher.NewCBCDecrypter(block, iv)
|
||||||
|
plaintext := make([]byte, len(ciphertext))
|
||||||
|
mode.CryptBlocks(plaintext, ciphertext)
|
||||||
|
|
||||||
|
// Remove PKCS7 padding.
|
||||||
|
if len(plaintext) == 0 {
|
||||||
|
return nil, errors.New("invalid padding: plaintext is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
padding := int(plaintext[len(plaintext)-1])
|
||||||
|
if padding > aes.BlockSize || padding == 0 {
|
||||||
|
return nil, errors.New("invalid padding size")
|
||||||
|
}
|
||||||
|
if len(plaintext) < padding {
|
||||||
|
return nil, errors.New("invalid padding: padding size is larger than plaintext")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the padding bytes.
|
||||||
|
for i := len(plaintext) - padding; i < len(plaintext); i++ {
|
||||||
|
if plaintext[i] != byte(padding) {
|
||||||
|
return nil, errors.New("invalid padding bytes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext[:len(plaintext)-padding], nil
|
||||||
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
24
pkg/cryptography/constants.go
Normal file
24
pkg/cryptography/constants.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
package cryptography
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/curve25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SHA256Size = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetBasepoint returns the standard Curve25519 basepoint
|
||||||
|
func GetBasepoint() []byte {
|
||||||
|
return curve25519.Basepoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hash(data []byte) []byte {
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write(data)
|
||||||
|
return h.Sum(nil)
|
||||||
|
}
|
||||||
27
pkg/cryptography/curve25519.go
Normal file
27
pkg/cryptography/curve25519.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
package cryptography
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/curve25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateKeyPair() (privateKey, publicKey []byte, err error) {
|
||||||
|
privateKey = make([]byte, curve25519.ScalarSize)
|
||||||
|
if _, err := rand.Read(privateKey); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKey, err = curve25519.X25519(privateKey, curve25519.Basepoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return privateKey, publicKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeriveSharedSecret(privateKey, peerPublicKey []byte) ([]byte, error) {
|
||||||
|
return curve25519.X25519(privateKey, peerPublicKey)
|
||||||
|
}
|
||||||
63
pkg/cryptography/curve25519_test.go
Normal file
63
pkg/cryptography/curve25519_test.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package cryptography
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/curve25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateKeyPair(t *testing.T) {
|
||||||
|
priv1, pub1, err := GenerateKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateKeyPair failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(priv1) != curve25519.ScalarSize {
|
||||||
|
t.Errorf("Private key length is %d, want %d", len(priv1), curve25519.ScalarSize)
|
||||||
|
}
|
||||||
|
if len(pub1) != curve25519.PointSize {
|
||||||
|
t.Errorf("Public key length is %d, want %d", len(pub1), curve25519.PointSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate another pair, should be different
|
||||||
|
priv2, pub2, err := GenerateKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Second GenerateKeyPair failed: %v", err)
|
||||||
|
}
|
||||||
|
if bytes.Equal(priv1, priv2) {
|
||||||
|
t.Error("Generated private keys are identical")
|
||||||
|
}
|
||||||
|
if bytes.Equal(pub1, pub2) {
|
||||||
|
t.Error("Generated public keys are identical")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeriveSharedSecret(t *testing.T) {
|
||||||
|
privA, pubA, err := GenerateKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateKeyPair A failed: %v", err)
|
||||||
|
}
|
||||||
|
privB, pubB, err := GenerateKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateKeyPair B failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretA, err := DeriveSharedSecret(privA, pubB)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeriveSharedSecret (A perspective) failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretB, err := DeriveSharedSecret(privB, pubA)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeriveSharedSecret (B perspective) failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(secretA, secretB) {
|
||||||
|
t.Errorf("Derived shared secrets do not match:\nSecret A: %x\nSecret B: %x", secretA, secretB)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(secretA) != curve25519.PointSize { // Shared secret length
|
||||||
|
t.Errorf("Shared secret length is %d, want %d", len(secretA), curve25519.PointSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
20
pkg/cryptography/ed25519.go
Normal file
20
pkg/cryptography/ed25519.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
package cryptography
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateSigningKeyPair() (ed25519.PublicKey, ed25519.PrivateKey, error) {
|
||||||
|
return ed25519.GenerateKey(rand.Reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sign(privateKey ed25519.PrivateKey, message []byte) []byte {
|
||||||
|
return ed25519.Sign(privateKey, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Verify(publicKey ed25519.PublicKey, message, signature []byte) bool {
|
||||||
|
return ed25519.Verify(publicKey, message, signature)
|
||||||
|
}
|
||||||
79
pkg/cryptography/ed25519_test.go
Normal file
79
pkg/cryptography/ed25519_test.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package cryptography
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateSigningKeyPair(t *testing.T) {
|
||||||
|
pub1, priv1, err := GenerateSigningKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateSigningKeyPair failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pub1) != ed25519.PublicKeySize {
|
||||||
|
t.Errorf("Public key length is %d, want %d", len(pub1), ed25519.PublicKeySize)
|
||||||
|
}
|
||||||
|
if len(priv1) != ed25519.PrivateKeySize {
|
||||||
|
t.Errorf("Private key length is %d, want %d", len(priv1), ed25519.PrivateKeySize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate another pair, should be different
|
||||||
|
pub2, priv2, err := GenerateSigningKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Second GenerateSigningKeyPair failed: %v", err)
|
||||||
|
}
|
||||||
|
if pub1.Equal(pub2) {
|
||||||
|
t.Error("Generated public keys are identical")
|
||||||
|
}
|
||||||
|
if priv1.Equal(priv2) {
|
||||||
|
t.Error("Generated private keys are identical")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignAndVerify(t *testing.T) {
|
||||||
|
pub, priv, err := GenerateSigningKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateSigningKeyPair failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
message := []byte("This message needs to be signed.")
|
||||||
|
|
||||||
|
signature := Sign(priv, message)
|
||||||
|
if len(signature) != ed25519.SignatureSize {
|
||||||
|
t.Errorf("Signature length is %d, want %d", len(signature), ed25519.SignatureSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify correct signature
|
||||||
|
if !Verify(pub, message, signature) {
|
||||||
|
t.Errorf("Verify failed for a valid signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify with tampered message
|
||||||
|
tamperedMessage := append(message, '!')
|
||||||
|
if Verify(pub, tamperedMessage, signature) {
|
||||||
|
t.Errorf("Verify succeeded for a tampered message")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify with tampered signature
|
||||||
|
tamperedSignature := append(signature[:len(signature)-1], ^signature[len(signature)-1])
|
||||||
|
if Verify(pub, message, tamperedSignature) {
|
||||||
|
t.Errorf("Verify succeeded for a tampered signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify with wrong public key
|
||||||
|
wrongPub, _, _ := GenerateSigningKeyPair()
|
||||||
|
if Verify(wrongPub, message, signature) {
|
||||||
|
t.Errorf("Verify succeeded with the wrong public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify empty message
|
||||||
|
emptyMessage := []byte("")
|
||||||
|
emptySig := Sign(priv, emptyMessage)
|
||||||
|
if !Verify(pub, emptyMessage, emptySig) {
|
||||||
|
t.Errorf("Verify failed for an empty message")
|
||||||
|
}
|
||||||
|
if Verify(pub, message, emptySig) {
|
||||||
|
t.Errorf("Verify succeeded comparing non-empty message with empty signature")
|
||||||
|
}
|
||||||
|
}
|
||||||
50
pkg/cryptography/hkdf.go
Normal file
50
pkg/cryptography/hkdf.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
package cryptography
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeriveKey(secret, salt, info []byte, length int) ([]byte, error) {
|
||||||
|
hashLen := 32
|
||||||
|
|
||||||
|
if length < 1 {
|
||||||
|
return nil, errors.New("invalid output key length")
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
28
pkg/cryptography/hmac.go
Normal file
28
pkg/cryptography/hmac.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
package cryptography
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateHMACKey(size int) ([]byte, error) {
|
||||||
|
key := make([]byte, size)
|
||||||
|
if _, err := rand.Read(key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ComputeHMAC(key, message []byte) []byte {
|
||||||
|
h := hmac.New(sha256.New, key)
|
||||||
|
h.Write(message)
|
||||||
|
return h.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateHMAC(key, message, messageHMAC []byte) bool {
|
||||||
|
expectedHMAC := ComputeHMAC(key, message)
|
||||||
|
return hmac.Equal(messageHMAC, expectedHMAC)
|
||||||
|
}
|
||||||
80
pkg/cryptography/hmac_test.go
Normal file
80
pkg/cryptography/hmac_test.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package cryptography
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateHMACKey(t *testing.T) {
|
||||||
|
testSizes := []int{16, 32, 64}
|
||||||
|
for _, size := range testSizes {
|
||||||
|
t.Run("Size"+string(rune(size)), func(t *testing.T) { // Simple name conversion
|
||||||
|
key, err := GenerateHMACKey(size)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateHMACKey(%d) failed: %v", size, err)
|
||||||
|
}
|
||||||
|
if len(key) != size {
|
||||||
|
t.Errorf("GenerateHMACKey(%d) returned key of length %d; want %d", size, len(key), size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if key is not all zeros (basic check for randomness)
|
||||||
|
isZero := true
|
||||||
|
for _, b := range key {
|
||||||
|
if b != 0 {
|
||||||
|
isZero = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isZero {
|
||||||
|
t.Errorf("GenerateHMACKey(%d) returned an all-zero key", size)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeAndValidateHMAC(t *testing.T) {
|
||||||
|
key, err := GenerateHMACKey(32) // Use SHA256 key size
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate HMAC key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
message := []byte("This is a test message.")
|
||||||
|
|
||||||
|
// Compute HMAC
|
||||||
|
computedHMAC := ComputeHMAC(key, message)
|
||||||
|
if len(computedHMAC) != 32 { // SHA256 output size
|
||||||
|
t.Errorf("ComputeHMAC returned HMAC of length %d; want 32", len(computedHMAC))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate correct HMAC
|
||||||
|
if !ValidateHMAC(key, message, computedHMAC) {
|
||||||
|
t.Errorf("ValidateHMAC failed for correctly computed HMAC")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate incorrect HMAC (tampered message)
|
||||||
|
tamperedMessage := append(message, byte('!'))
|
||||||
|
if ValidateHMAC(key, tamperedMessage, computedHMAC) {
|
||||||
|
t.Errorf("ValidateHMAC succeeded for tampered message")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate incorrect HMAC (tampered key)
|
||||||
|
wrongKey, _ := GenerateHMACKey(32)
|
||||||
|
if ValidateHMAC(wrongKey, message, computedHMAC) {
|
||||||
|
t.Errorf("ValidateHMAC succeeded for incorrect key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate incorrect HMAC (tampered HMAC)
|
||||||
|
tamperedHMAC := append(computedHMAC[:len(computedHMAC)-1], ^computedHMAC[len(computedHMAC)-1])
|
||||||
|
if ValidateHMAC(key, message, tamperedHMAC) {
|
||||||
|
t.Errorf("ValidateHMAC succeeded for tampered HMAC")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate empty message
|
||||||
|
emptyMessage := []byte("")
|
||||||
|
emptyHMAC := ComputeHMAC(key, emptyMessage)
|
||||||
|
if !ValidateHMAC(key, emptyMessage, emptyHMAC) {
|
||||||
|
t.Errorf("ValidateHMAC failed for empty message")
|
||||||
|
}
|
||||||
|
if ValidateHMAC(key, message, emptyHMAC) {
|
||||||
|
t.Errorf("ValidateHMAC succeeded comparing non-empty message with empty HMAC")
|
||||||
|
}
|
||||||
|
}
|
||||||
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,21 +1,35 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package destination
|
package destination
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/announce"
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
|
"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 (
|
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
|
IN = 0x01
|
||||||
OUT = 0x02
|
OUT = 0x02
|
||||||
|
|
||||||
|
// Destination types
|
||||||
SINGLE = 0x00
|
SINGLE = 0x00
|
||||||
GROUP = 0x01
|
GROUP = 0x01
|
||||||
PLAIN = 0x02
|
PLAIN = 0x02
|
||||||
@@ -41,6 +55,21 @@ type RequestHandler struct {
|
|||||||
ResponseGenerator func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity *identity.Identity, requestedAt int64) []byte
|
ResponseGenerator func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity *identity.Identity, requestedAt int64) []byte
|
||||||
AllowMode byte
|
AllowMode byte
|
||||||
AllowedList [][]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 {
|
type Destination struct {
|
||||||
@@ -49,7 +78,8 @@ type Destination struct {
|
|||||||
destType byte
|
destType byte
|
||||||
appName string
|
appName string
|
||||||
aspects []string
|
aspects []string
|
||||||
hash []byte
|
hashValue []byte
|
||||||
|
transport Transport
|
||||||
|
|
||||||
acceptsLinks bool
|
acceptsLinks bool
|
||||||
proofStrategy byte
|
proofStrategy byte
|
||||||
@@ -63,20 +93,22 @@ type Destination struct {
|
|||||||
ratchetCount int
|
ratchetCount int
|
||||||
ratchetInterval int
|
ratchetInterval int
|
||||||
enforceRatchets bool
|
enforceRatchets bool
|
||||||
|
latestRatchetTime time.Time
|
||||||
|
latestRatchetID []byte
|
||||||
|
ratchets [][]byte
|
||||||
|
ratchetFileLock sync.Mutex
|
||||||
|
|
||||||
defaultAppData []byte
|
defaultAppData []byte
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
|
|
||||||
requestHandlers map[string]*RequestHandler
|
requestHandlers map[string]*RequestHandler
|
||||||
callbacks struct {
|
|
||||||
packetReceived common.PacketCallback
|
|
||||||
proofRequested common.ProofRequestedCallback
|
|
||||||
linkEstablished common.LinkEstablishedCallback
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(id *identity.Identity, direction byte, destType byte, appName string, aspects ...string) (*Destination, error) {
|
func New(id *identity.Identity, direction byte, destType byte, appName string, transport Transport, aspects ...string) (*Destination, error) {
|
||||||
|
debug.Log(debug.DEBUG_INFO, "Creating new destination", "app", appName, "type", destType, "direction", direction)
|
||||||
|
|
||||||
if id == nil {
|
if id == nil {
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil")
|
||||||
return nil, errors.New("identity cannot be nil")
|
return nil, errors.New("identity cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +118,7 @@ func New(id *identity.Identity, direction byte, destType byte, appName string, a
|
|||||||
destType: destType,
|
destType: destType,
|
||||||
appName: appName,
|
appName: appName,
|
||||||
aspects: aspects,
|
aspects: aspects,
|
||||||
|
transport: transport,
|
||||||
acceptsLinks: false,
|
acceptsLinks: false,
|
||||||
proofStrategy: PROVE_NONE,
|
proofStrategy: PROVE_NONE,
|
||||||
ratchetCount: RATCHET_COUNT,
|
ratchetCount: RATCHET_COUNT,
|
||||||
@@ -94,19 +127,69 @@ func New(id *identity.Identity, direction byte, destType byte, appName string, a
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate destination hash
|
// Generate destination hash
|
||||||
d.hash = d.Hash()
|
d.hashValue = d.calculateHash()
|
||||||
|
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
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Destination) Hash() []byte {
|
// FromHash creates a destination from a known hash (e.g., from an announce).
|
||||||
nameHash := sha256.Sum256([]byte(d.ExpandName()))
|
// This is used by clients to create destination objects for servers they've discovered.
|
||||||
identityHash := sha256.Sum256(d.identity.GetPublicKey())
|
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))
|
||||||
|
|
||||||
combined := append(nameHash[:], identityHash[:]...)
|
if id == nil {
|
||||||
finalHash := sha256.Sum256(combined)
|
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil")
|
||||||
|
return nil, errors.New("identity cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
return finalHash[:16] // Truncated to 128 bits
|
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 {
|
||||||
|
debug.Log(debug.DEBUG_TRACE, "Calculating hash for destination", "name", d.ExpandName())
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
// Concatenate name_hash (10 bytes) + identity_hash (16 bytes) = 26 bytes
|
||||||
|
combined := append(nameHash10, identityHash...)
|
||||||
|
|
||||||
|
// 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 {
|
func (d *Destination) ExpandName() string {
|
||||||
@@ -117,80 +200,68 @@ func (d *Destination) ExpandName() string {
|
|||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Destination) Announce(appData []byte) error {
|
func (d *Destination) Announce(pathResponse bool, tag []byte, attachedInterface common.NetworkInterface) error {
|
||||||
d.mutex.Lock()
|
d.mutex.Lock()
|
||||||
defer d.mutex.Unlock()
|
defer d.mutex.Unlock()
|
||||||
|
|
||||||
// If no specific appData provided, use default
|
debug.Log(debug.DEBUG_VERBOSE, "Announcing destination", "name", d.ExpandName(), "path_response", pathResponse)
|
||||||
if appData == nil {
|
|
||||||
appData = d.defaultAppData
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create announce packet
|
appData := d.defaultAppData
|
||||||
packet := make([]byte, 0)
|
|
||||||
|
|
||||||
// Add destination hash
|
// Create announce packet using announce package
|
||||||
packet = append(packet, d.hash...)
|
announceObj, err := announce.New(d.identity, d.hashValue, d.ExpandName(), appData, pathResponse, d.transport.GetConfig())
|
||||||
|
|
||||||
// Add identity public key
|
|
||||||
packet = append(packet, d.identity.GetPublicKey()...)
|
|
||||||
|
|
||||||
// Add flags byte
|
|
||||||
flags := byte(0)
|
|
||||||
if d.acceptsLinks {
|
|
||||||
flags |= 0x01
|
|
||||||
}
|
|
||||||
if d.ratchetsEnabled {
|
|
||||||
flags |= 0x02
|
|
||||||
}
|
|
||||||
packet = append(packet, flags)
|
|
||||||
|
|
||||||
// Add proof strategy
|
|
||||||
packet = append(packet, d.proofStrategy)
|
|
||||||
|
|
||||||
// Add app data length and data if present
|
|
||||||
if appData != nil {
|
|
||||||
appDataLen := uint16(len(appData))
|
|
||||||
lenBytes := make([]byte, 2)
|
|
||||||
binary.BigEndian.PutUint16(lenBytes, appDataLen)
|
|
||||||
packet = append(packet, lenBytes...)
|
|
||||||
packet = append(packet, appData...)
|
|
||||||
} else {
|
|
||||||
// No app data
|
|
||||||
packet = append(packet, 0x00, 0x00)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add ratchet data if enabled
|
|
||||||
if d.ratchetsEnabled {
|
|
||||||
// Add ratchet interval
|
|
||||||
intervalBytes := make([]byte, 4)
|
|
||||||
binary.BigEndian.PutUint32(intervalBytes, uint32(d.ratchetInterval))
|
|
||||||
packet = append(packet, intervalBytes...)
|
|
||||||
|
|
||||||
// Add current ratchet key
|
|
||||||
ratchetKey := d.identity.GetCurrentRatchetKey()
|
|
||||||
if ratchetKey == nil {
|
|
||||||
return errors.New("failed to get current ratchet key")
|
|
||||||
}
|
|
||||||
packet = append(packet, ratchetKey...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign the announce packet
|
|
||||||
signature, err := d.Sign(packet)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to sign announce packet: %w", err)
|
return fmt.Errorf("failed to create announce: %w", err)
|
||||||
}
|
}
|
||||||
packet = append(packet, signature...)
|
|
||||||
|
|
||||||
// Send announce packet through transport layer
|
packet := announceObj.GetPacket()
|
||||||
// This will need to be implemented in the transport package
|
if packet == nil {
|
||||||
return transport.SendAnnounce(packet)
|
return errors.New("failed to create announce packet")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pathResponse && tag != nil {
|
||||||
|
debug.Log(debug.DEBUG_INFO, "Sending path response announce", "tag", fmt.Sprintf("%x", tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.transport == nil {
|
||||||
|
return errors.New("transport not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Destination) AcceptsLinks(accepts bool) {
|
func (d *Destination) AcceptsLinks(accepts bool) {
|
||||||
d.mutex.Lock()
|
d.mutex.Lock()
|
||||||
defer d.mutex.Unlock()
|
defer d.mutex.Unlock()
|
||||||
d.acceptsLinks = accepts
|
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) {
|
func (d *Destination) SetLinkEstablishedCallback(callback common.LinkEstablishedCallback) {
|
||||||
@@ -199,12 +270,72 @@ func (d *Destination) SetLinkEstablishedCallback(callback common.LinkEstablished
|
|||||||
d.linkCallback = callback
|
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) {
|
func (d *Destination) SetPacketCallback(callback common.PacketCallback) {
|
||||||
d.mutex.Lock()
|
d.mutex.Lock()
|
||||||
defer d.mutex.Unlock()
|
defer d.mutex.Unlock()
|
||||||
d.packetCallback = callback
|
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) {
|
func (d *Destination) SetProofRequestedCallback(callback common.ProofRequestedCallback) {
|
||||||
d.mutex.Lock()
|
d.mutex.Lock()
|
||||||
defer d.mutex.Unlock()
|
defer d.mutex.Unlock()
|
||||||
@@ -221,8 +352,27 @@ func (d *Destination) EnableRatchets(path string) bool {
|
|||||||
d.mutex.Lock()
|
d.mutex.Lock()
|
||||||
defer d.mutex.Unlock()
|
defer d.mutex.Unlock()
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "No ratchet file path specified")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
d.ratchetsEnabled = true
|
d.ratchetsEnabled = true
|
||||||
d.ratchetPath = path
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +425,7 @@ func (d *Destination) RegisterRequestHandler(path string, responseGen func(strin
|
|||||||
return errors.New("invalid allow mode")
|
return errors.New("invalid allow mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
if allow == ALLOW_LIST && (allowedList == nil || len(allowedList) == 0) {
|
if allow == ALLOW_LIST && len(allowedList) == 0 {
|
||||||
return errors.New("allowed list required for ALLOW_LIST mode")
|
return errors.New("allowed list required for ALLOW_LIST mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,21 +453,88 @@ func (d *Destination) DeregisterRequestHandler(path string) bool {
|
|||||||
return false
|
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) {
|
func (d *Destination) Encrypt(plaintext []byte) ([]byte, error) {
|
||||||
if d.destType == PLAIN {
|
if d.destType == PLAIN {
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "Using plaintext transmission for PLAIN destination")
|
||||||
return plaintext, nil
|
return plaintext, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.identity == nil {
|
if d.identity == nil {
|
||||||
|
debug.Log(debug.DEBUG_INFO, "Cannot encrypt: no identity available")
|
||||||
return nil, errors.New("no identity available for encryption")
|
return nil, errors.New("no identity available for encryption")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "Encrypting bytes for destination", "bytes", len(plaintext), "destType", d.destType)
|
||||||
|
|
||||||
switch d.destType {
|
switch d.destType {
|
||||||
case SINGLE:
|
case SINGLE:
|
||||||
return d.identity.Encrypt(plaintext, nil)
|
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:
|
case GROUP:
|
||||||
return d.identity.EncryptSymmetric(plaintext)
|
key := d.identity.GetCurrentRatchetKey()
|
||||||
|
if key == nil {
|
||||||
|
debug.Log(debug.DEBUG_INFO, "Cannot encrypt: no ratchet key available")
|
||||||
|
return nil, errors.New("no ratchet key available")
|
||||||
|
}
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "Encrypting for group with ratchet key", "key", fmt.Sprintf("%x", key[:8]))
|
||||||
|
return d.identity.EncryptWithHMAC(plaintext, key)
|
||||||
default:
|
default:
|
||||||
|
debug.Log(debug.DEBUG_INFO, "Unsupported destination type for encryption", "destType", d.destType)
|
||||||
return nil, errors.New("unsupported destination type for encryption")
|
return nil, errors.New("unsupported destination type for encryption")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -331,14 +548,15 @@ func (d *Destination) Decrypt(ciphertext []byte) ([]byte, error) {
|
|||||||
return nil, errors.New("no identity available for decryption")
|
return nil, errors.New("no identity available for decryption")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch d.destType {
|
// Create empty ratchet receiver to get latest ratchet ID if available
|
||||||
case SINGLE:
|
ratchetReceiver := &common.RatchetIDReceiver{}
|
||||||
return d.identity.Decrypt(ciphertext, nil)
|
|
||||||
case GROUP:
|
// Call Decrypt with full parameter list:
|
||||||
return d.identity.DecryptSymmetric(ciphertext)
|
// - ciphertext: the encrypted data
|
||||||
default:
|
// - ratchets: nil since we're not providing specific ratchets
|
||||||
return nil, errors.New("unsupported destination type for decryption")
|
// - enforceRatchets: false to allow fallback to normal decryption
|
||||||
}
|
// - ratchetIDReceiver: to receive the latest ratchet ID used
|
||||||
|
return d.identity.Decrypt(ciphertext, nil, false, ratchetReceiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Destination) Sign(data []byte) ([]byte, error) {
|
func (d *Destination) Sign(data []byte) ([]byte, error) {
|
||||||
@@ -348,3 +566,215 @@ func (d *Destination) Sign(data []byte) ([]byte, error) {
|
|||||||
signature := d.identity.Sign(data)
|
signature := d.identity.Sign(data)
|
||||||
return signature, nil
|
return signature, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Destination) GetPublicKey() []byte {
|
||||||
|
if d.identity == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return d.identity.GetPublicKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Destination) GetIdentity() *identity.Identity {
|
||||||
|
return d.identity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Destination) GetType() byte {
|
||||||
|
return d.destType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Destination) GetHash() []byte {
|
||||||
|
d.mutex.RLock()
|
||||||
|
defer d.mutex.RUnlock()
|
||||||
|
if d.hashValue == nil {
|
||||||
|
d.mutex.RUnlock()
|
||||||
|
d.mutex.Lock()
|
||||||
|
defer d.mutex.Unlock()
|
||||||
|
if d.hashValue == nil {
|
||||||
|
d.hashValue = d.calculateHash()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d.hashValue
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
605
pkg/interfaces/auto.go
Normal file
605
pkg/interfaces/auto.go
Normal file
@@ -0,0 +1,605 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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 = 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
|
||||||
|
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
|
||||||
|
addr *net.UDPAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterface, error) {
|
||||||
|
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: 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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ai.adoptedInterfaces) == 0 {
|
||||||
|
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 {
|
||||||
|
if ipnet.IP.To4() == nil && ipnet.IP.IsLinkLocalUnicast() {
|
||||||
|
linkLocalAddr = ipnet.IP.String()
|
||||||
|
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(ai.mcastDiscoveryAddr),
|
||||||
|
Port: ai.discoveryPort,
|
||||||
|
Zone: iface.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.ListenMulticastUDP("udp6", iface, addr)
|
||||||
|
if err != nil {
|
||||||
|
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.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
|
||||||
|
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, common.NUM_1024)
|
||||||
|
for {
|
||||||
|
ai.Mutex.RLock()
|
||||||
|
done := ai.done
|
||||||
|
ai.Mutex.RUnlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 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.IsOnline() {
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Data read error", "interface", ifaceName, "error", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if callback := ai.GetPacketCallback(); callback != nil {
|
||||||
|
callback(buf[:n], ai)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ai *AutoInterface) handlePeerAnnounce(addr *net.UDPAddr, ifaceName string) {
|
||||||
|
ai.Mutex.Lock()
|
||||||
|
defer ai.Mutex.Unlock()
|
||||||
|
|
||||||
|
peerIP := addr.IP.String()
|
||||||
|
|
||||||
|
for _, localAddr := range ai.linkLocalAddrs {
|
||||||
|
if peerIP == localAddr {
|
||||||
|
ai.multicastEchoes[ifaceName] = time.Now()
|
||||||
|
debug.Log(debug.DEBUG_TRACE, "Received own multicast echo", "interface", ifaceName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
debug.Log(debug.DEBUG_INFO, "Discovered new peer", "peer", peerIP, "interface", ifaceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ai *AutoInterface) announceLoop() {
|
||||||
|
ticker := time.NewTicker(ai.announceInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if !ai.IsOnline() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ai.sendPeerAnnounce()
|
||||||
|
case <-ai.done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ai *AutoInterface) sendPeerAnnounce() {
|
||||||
|
ai.Mutex.RLock()
|
||||||
|
defer ai.Mutex.RUnlock()
|
||||||
|
|
||||||
|
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 {
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Failed to create outbound socket", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
ai.Online = false
|
||||||
|
ai.IN = false
|
||||||
|
ai.OUT = false
|
||||||
|
|
||||||
|
for _, server := range ai.interfaceServers {
|
||||||
|
server.Close() // #nosec G104
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, server := range ai.discoveryServers {
|
||||||
|
server.Close() // #nosec G104
|
||||||
|
}
|
||||||
|
|
||||||
|
if ai.outboundConn != nil {
|
||||||
|
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,51 +1,135 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package interfaces
|
package interfaces
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"encoding/binary"
|
|
||||||
|
|
||||||
"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 (
|
const (
|
||||||
BITRATE_MINIMUM = 5 // Minimum required bitrate in bits/sec
|
BITRATE_MINIMUM = 1200 // Minimum bitrate in bits/second
|
||||||
|
MODE_FULL = 0x01
|
||||||
|
|
||||||
|
// Interface modes
|
||||||
|
MODE_GATEWAY = 0x02
|
||||||
|
MODE_ACCESS_POINT = 0x03
|
||||||
|
MODE_ROAMING = 0x04
|
||||||
|
MODE_BOUNDARY = 0x05
|
||||||
|
|
||||||
|
// Interface types
|
||||||
|
TYPE_UDP = 0x01
|
||||||
|
TYPE_TCP = 0x02
|
||||||
|
|
||||||
|
PROPAGATION_RATE = 0.02 // 2% of interface bandwidth
|
||||||
)
|
)
|
||||||
|
|
||||||
// BaseInterface embeds common.BaseInterface and implements common.Interface
|
type Interface interface {
|
||||||
|
GetName() string
|
||||||
|
GetType() common.InterfaceType
|
||||||
|
GetMode() common.InterfaceMode
|
||||||
|
IsOnline() bool
|
||||||
|
IsDetached() bool
|
||||||
|
IsEnabled() bool
|
||||||
|
Detach()
|
||||||
|
Enable()
|
||||||
|
Disable()
|
||||||
|
Send(data []byte, addr string) error
|
||||||
|
SetPacketCallback(common.PacketCallback)
|
||||||
|
GetPacketCallback() common.PacketCallback
|
||||||
|
ProcessIncoming([]byte)
|
||||||
|
ProcessOutgoing([]byte) error
|
||||||
|
SendPathRequest([]byte) error
|
||||||
|
SendLinkPacket([]byte, []byte, time.Time) error
|
||||||
|
Start() error
|
||||||
|
Stop() error
|
||||||
|
GetMTU() int
|
||||||
|
GetConn() net.Conn
|
||||||
|
GetBandwidthAvailable() bool
|
||||||
|
common.NetworkInterface
|
||||||
|
}
|
||||||
|
|
||||||
type BaseInterface struct {
|
type BaseInterface struct {
|
||||||
common.BaseInterface
|
Name string
|
||||||
|
Mode common.InterfaceMode
|
||||||
|
Type common.InterfaceType
|
||||||
|
Online bool
|
||||||
|
Enabled bool
|
||||||
|
Detached bool
|
||||||
|
IN bool
|
||||||
|
OUT bool
|
||||||
|
MTU int
|
||||||
|
Bitrate int64
|
||||||
|
TxBytes uint64
|
||||||
|
RxBytes uint64
|
||||||
|
lastTx time.Time
|
||||||
|
lastRx time.Time
|
||||||
|
|
||||||
|
Mutex sync.RWMutex
|
||||||
|
packetCallback common.PacketCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBaseInterface(name string, ifType common.InterfaceType, enabled bool) BaseInterface {
|
||||||
|
return BaseInterface{
|
||||||
|
Name: name,
|
||||||
|
Mode: common.IF_MODE_FULL,
|
||||||
|
Type: ifType,
|
||||||
|
Online: false,
|
||||||
|
Enabled: enabled,
|
||||||
|
Detached: false,
|
||||||
|
IN: false,
|
||||||
|
OUT: false,
|
||||||
|
MTU: common.DEFAULT_MTU,
|
||||||
|
Bitrate: BITRATE_MINIMUM,
|
||||||
|
lastTx: time.Now(),
|
||||||
|
lastRx: time.Now(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {
|
func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||||
i.mutex.Lock()
|
i.Mutex.Lock()
|
||||||
defer i.mutex.Unlock()
|
defer i.Mutex.Unlock()
|
||||||
i.packetCallback = callback
|
i.packetCallback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetPacketCallback() common.PacketCallback {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.packetCallback
|
||||||
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
||||||
i.mutex.RLock()
|
i.Mutex.Lock()
|
||||||
|
i.RxBytes += uint64(len(data))
|
||||||
|
i.Mutex.Unlock()
|
||||||
|
|
||||||
|
i.Mutex.RLock()
|
||||||
callback := i.packetCallback
|
callback := i.packetCallback
|
||||||
i.mutex.RUnlock()
|
i.Mutex.RUnlock()
|
||||||
|
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
callback(data, i)
|
callback(data, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
i.RxBytes += uint64(len(data))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) ProcessOutgoing(data []byte) error {
|
func (i *BaseInterface) ProcessOutgoing(data []byte) error {
|
||||||
i.TxBytes += uint64(len(data))
|
if !i.Online || i.Detached {
|
||||||
return nil
|
debug.Log(debug.DEBUG_CRITICAL, "Interface cannot process outgoing packet - interface offline or detached", "name", i.Name)
|
||||||
}
|
return fmt.Errorf("interface offline or detached")
|
||||||
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) Detach() {
|
i.Mutex.Lock()
|
||||||
i.mutex.Lock()
|
i.TxBytes += uint64(len(data))
|
||||||
defer i.mutex.Unlock()
|
i.Mutex.Unlock()
|
||||||
i.Detached = true
|
|
||||||
i.Online = false
|
debug.Log(debug.DEBUG_VERBOSE, "Interface processed outgoing packet", "name", i.Name, "bytes", len(data), "total_tx", i.TxBytes)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) SendPathRequest(packet []byte) error {
|
func (i *BaseInterface) SendPathRequest(packet []byte) error {
|
||||||
@@ -53,8 +137,8 @@ func (i *BaseInterface) SendPathRequest(packet []byte) error {
|
|||||||
return fmt.Errorf("interface offline or detached")
|
return fmt.Errorf("interface offline or detached")
|
||||||
}
|
}
|
||||||
|
|
||||||
frame := make([]byte, 0, len(packet)+2)
|
frame := make([]byte, 0, len(packet)+1)
|
||||||
frame = append(frame, 0x01)
|
frame = append(frame, common.HEX_0x01)
|
||||||
frame = append(frame, packet...)
|
frame = append(frame, packet...)
|
||||||
|
|
||||||
return i.ProcessOutgoing(frame)
|
return i.ProcessOutgoing(frame)
|
||||||
@@ -66,14 +150,158 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
|
|||||||
}
|
}
|
||||||
|
|
||||||
frame := make([]byte, 0, len(dest)+len(data)+9)
|
frame := make([]byte, 0, len(dest)+len(data)+9)
|
||||||
frame = append(frame, 0x02)
|
frame = append(frame, common.HEX_0x02)
|
||||||
frame = append(frame, dest...)
|
frame = append(frame, dest...)
|
||||||
|
|
||||||
ts := make([]byte, 8)
|
ts := make([]byte, 8)
|
||||||
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix()))
|
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) // #nosec G115
|
||||||
frame = append(frame, ts...)
|
frame = append(frame, ts...)
|
||||||
|
|
||||||
frame = append(frame, data...)
|
frame = append(frame, data...)
|
||||||
|
|
||||||
return i.ProcessOutgoing(frame)
|
return i.ProcessOutgoing(frame)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Detach() {
|
||||||
|
i.Mutex.Lock()
|
||||||
|
defer i.Mutex.Unlock()
|
||||||
|
i.Detached = true
|
||||||
|
i.Online = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) IsEnabled() bool {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.Enabled && i.Online && !i.Detached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Enable() {
|
||||||
|
i.Mutex.Lock()
|
||||||
|
defer i.Mutex.Unlock()
|
||||||
|
|
||||||
|
prevState := i.Enabled
|
||||||
|
i.Enabled = true
|
||||||
|
i.Online = true
|
||||||
|
|
||||||
|
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.Enabled = false
|
||||||
|
i.Online = false
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Interface disabled and offline", "name", i.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetName() string {
|
||||||
|
return i.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetType() common.InterfaceType {
|
||||||
|
return i.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetMode() common.InterfaceMode {
|
||||||
|
return i.Mode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetMTU() int {
|
||||||
|
return i.MTU
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) IsOnline() bool {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.Online
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) IsDetached() bool {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
return i.Detached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Start() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Stop() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) Send(data []byte, address string) error {
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "Interface sending bytes", "name", i.Name, "bytes", len(data), "address", address)
|
||||||
|
|
||||||
|
err := i.ProcessOutgoing(data)
|
||||||
|
if err != nil {
|
||||||
|
debug.Log(debug.DEBUG_CRITICAL, "Interface failed to send data", "name", i.Name, "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
i.updateBandwidthStats(uint64(len(data)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetConn() net.Conn {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *BaseInterface) GetBandwidthAvailable() bool {
|
||||||
|
i.Mutex.RLock()
|
||||||
|
defer i.Mutex.RUnlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
timeSinceLastTx := now.Sub(i.lastTx)
|
||||||
|
|
||||||
|
if timeSinceLastTx > time.Second {
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "Interface bandwidth available", "name", i.Name, "idle_seconds", timeSinceLastTx.Seconds())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesPerSec := float64(i.TxBytes) / timeSinceLastTx.Seconds()
|
||||||
|
currentUsage := bytesPerSec * 8
|
||||||
|
maxUsage := float64(i.Bitrate) * PROPAGATION_RATE
|
||||||
|
|
||||||
|
available := currentUsage < maxUsage
|
||||||
|
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.TxBytes += bytes
|
||||||
|
i.lastTx = time.Now()
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "Interface updated bandwidth stats", "name", i.Name, "tx_bytes", i.TxBytes, "last_tx", i.lastTx)
|
||||||
|
}
|
||||||
|
|
||||||
|
type InterceptedInterface struct {
|
||||||
|
Interface
|
||||||
|
interceptor func([]byte, common.NetworkInterface) error
|
||||||
|
originalSend func([]byte, string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create constructor for intercepted interface
|
||||||
|
func NewInterceptedInterface(base Interface, interceptor func([]byte, common.NetworkInterface) error) *InterceptedInterface {
|
||||||
|
return &InterceptedInterface{
|
||||||
|
Interface: base,
|
||||||
|
interceptor: interceptor,
|
||||||
|
originalSend: base.Send,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Send method for intercepted interface
|
||||||
|
func (i *InterceptedInterface) Send(data []byte, addr string) error {
|
||||||
|
// Call interceptor if provided
|
||||||
|
if i.interceptor != nil && len(data) > 0 {
|
||||||
|
if err := i.interceptor(data, i); err != nil {
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Failed to intercept outgoing packet", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call original send
|
||||||
|
return i.originalSend(data, addr)
|
||||||
|
}
|
||||||
|
|||||||
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,10 +1,16 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package interfaces
|
package interfaces
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -17,16 +23,30 @@ const (
|
|||||||
KISS_TFEND = 0xDC
|
KISS_TFEND = 0xDC
|
||||||
KISS_TFESC = 0xDD
|
KISS_TFESC = 0xDD
|
||||||
|
|
||||||
TCP_USER_TIMEOUT = 24
|
DEFAULT_MTU = 1064
|
||||||
TCP_PROBE_AFTER = 5
|
BITRATE_GUESS_VAL = 10 * 1000 * 1000
|
||||||
TCP_PROBE_INTERVAL = 2
|
|
||||||
TCP_PROBES = 12
|
|
||||||
RECONNECT_WAIT = 5
|
RECONNECT_WAIT = 5
|
||||||
INITIAL_TIMEOUT = 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 {
|
type TCPClientInterface struct {
|
||||||
Interface
|
BaseInterface
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
targetAddr string
|
targetAddr string
|
||||||
targetPort int
|
targetPort int
|
||||||
@@ -39,81 +59,112 @@ type TCPClientInterface struct {
|
|||||||
maxReconnectTries int
|
maxReconnectTries int
|
||||||
packetBuffer []byte
|
packetBuffer []byte
|
||||||
packetType byte
|
packetType byte
|
||||||
|
done chan struct{}
|
||||||
|
stopOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTCPClient(name string, targetAddr string, targetPort int, kissFraming bool, i2pTunneled bool) (*TCPClientInterface, error) {
|
func NewTCPClientInterface(name string, targetHost string, targetPort int, kissFraming bool, i2pTunneled bool, enabled bool) (*TCPClientInterface, error) {
|
||||||
tc := &TCPClientInterface{
|
tc := &TCPClientInterface{
|
||||||
Interface: Interface{
|
BaseInterface: NewBaseInterface(name, common.IF_TYPE_TCP, enabled),
|
||||||
Name: name,
|
targetAddr: targetHost,
|
||||||
Mode: MODE_FULL,
|
|
||||||
MTU: 1064,
|
|
||||||
Bitrate: 10000000, // 10Mbps estimate
|
|
||||||
},
|
|
||||||
targetAddr: targetAddr,
|
|
||||||
targetPort: targetPort,
|
targetPort: targetPort,
|
||||||
kissFraming: kissFraming,
|
kissFraming: kissFraming,
|
||||||
i2pTunneled: i2pTunneled,
|
i2pTunneled: i2pTunneled,
|
||||||
initiator: true,
|
initiator: true,
|
||||||
|
maxReconnectTries: RECONNECT_WAIT * TCP_PROBES_COUNT,
|
||||||
|
packetBuffer: make([]byte, 0),
|
||||||
|
neverConnected: true,
|
||||||
|
done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tc.connect(true); err != nil {
|
if enabled {
|
||||||
go tc.reconnect()
|
addr := net.JoinHostPort(targetHost, fmt.Sprintf("%d", targetPort))
|
||||||
} else {
|
conn, err := net.Dial("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tc.conn = conn
|
||||||
|
tc.Online = true
|
||||||
go tc.readLoop()
|
go tc.readLoop()
|
||||||
}
|
}
|
||||||
|
|
||||||
return tc, nil
|
return tc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) connect(initial bool) error {
|
func (tc *TCPClientInterface) Start() error {
|
||||||
addr := fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort)
|
tc.Mutex.Lock()
|
||||||
conn, err := net.DialTimeout("tcp", addr, time.Second*INITIAL_TIMEOUT)
|
if !tc.Enabled || tc.Detached {
|
||||||
if err != nil {
|
tc.Mutex.Unlock()
|
||||||
if initial {
|
return fmt.Errorf("interface not enabled or detached")
|
||||||
return fmt.Errorf("initial connection failed: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tc.conn != nil {
|
||||||
|
tc.Online = true
|
||||||
|
go tc.readLoop()
|
||||||
|
tc.Mutex.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tc.Mutex.Lock()
|
||||||
tc.conn = conn
|
tc.conn = conn
|
||||||
tc.Online = true
|
tc.Mutex.Unlock()
|
||||||
tc.writing = false
|
|
||||||
tc.neverConnected = false
|
|
||||||
|
|
||||||
// Set TCP options
|
// Set platform-specific timeouts
|
||||||
if tcpConn, ok := conn.(*net.TCPConn); ok {
|
switch runtime.GOOS {
|
||||||
tcpConn.SetNoDelay(true)
|
case "linux":
|
||||||
tcpConn.SetKeepAlive(true)
|
if err := tc.setTimeoutsLinux(); err != nil {
|
||||||
tcpConn.SetKeepAlivePeriod(time.Second * TCP_PROBE_INTERVAL)
|
debug.Log(debug.DEBUG_ERROR, "Failed to set Linux TCP timeouts", "error", err)
|
||||||
|
}
|
||||||
|
case "darwin":
|
||||||
|
if err := tc.setTimeoutsOSX(); err != nil {
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) reconnect() {
|
func (tc *TCPClientInterface) Stop() error {
|
||||||
if tc.initiator && !tc.reconnecting {
|
tc.Mutex.Lock()
|
||||||
tc.reconnecting = true
|
tc.Enabled = false
|
||||||
attempts := 0
|
tc.Online = false
|
||||||
|
if tc.conn != nil {
|
||||||
for !tc.Online {
|
_ = tc.conn.Close()
|
||||||
time.Sleep(time.Second * RECONNECT_WAIT)
|
tc.conn = nil
|
||||||
attempts++
|
|
||||||
|
|
||||||
if tc.maxReconnectTries > 0 && attempts > tc.maxReconnectTries {
|
|
||||||
tc.teardown()
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
tc.Mutex.Unlock()
|
||||||
|
|
||||||
if err := tc.connect(false); err != nil {
|
tc.stopOnce.Do(func() {
|
||||||
continue
|
if tc.done != nil {
|
||||||
|
close(tc.done)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
go tc.readLoop()
|
return nil
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
tc.reconnecting = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) readLoop() {
|
func (tc *TCPClientInterface) readLoop() {
|
||||||
@@ -123,10 +174,30 @@ func (tc *TCPClientInterface) readLoop() {
|
|||||||
dataBuffer := make([]byte, 0)
|
dataBuffer := make([]byte, 0)
|
||||||
|
|
||||||
for {
|
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 {
|
if err != nil {
|
||||||
|
tc.Mutex.Lock()
|
||||||
tc.Online = false
|
tc.Online = false
|
||||||
if tc.initiator && !tc.Detached {
|
detached := tc.Detached
|
||||||
|
initiator := tc.initiator
|
||||||
|
tc.Mutex.Unlock()
|
||||||
|
|
||||||
|
if initiator && !detached {
|
||||||
go tc.reconnect()
|
go tc.reconnect()
|
||||||
} else {
|
} else {
|
||||||
tc.teardown()
|
tc.teardown()
|
||||||
@@ -134,42 +205,21 @@ func (tc *TCPClientInterface) readLoop() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tc.UpdateStats(uint64(n), true) // #nosec G115
|
||||||
|
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
b := buffer[i]
|
b := buffer[i]
|
||||||
|
|
||||||
if tc.kissFraming {
|
if b == HDLC_FLAG {
|
||||||
// KISS framing logic
|
if inFrame && len(dataBuffer) > 0 {
|
||||||
if inFrame && b == KISS_FEND {
|
|
||||||
inFrame = false
|
|
||||||
tc.handlePacket(dataBuffer)
|
tc.handlePacket(dataBuffer)
|
||||||
dataBuffer = dataBuffer[:0]
|
dataBuffer = dataBuffer[:0]
|
||||||
} else if b == KISS_FEND {
|
|
||||||
inFrame = true
|
|
||||||
} else if inFrame {
|
|
||||||
if b == KISS_FESC {
|
|
||||||
escape = true
|
|
||||||
} else {
|
|
||||||
if escape {
|
|
||||||
if b == KISS_TFEND {
|
|
||||||
b = KISS_FEND
|
|
||||||
}
|
}
|
||||||
if b == KISS_TFESC {
|
inFrame = !inFrame
|
||||||
b = KISS_FESC
|
continue
|
||||||
}
|
}
|
||||||
escape = false
|
|
||||||
}
|
if inFrame {
|
||||||
dataBuffer = append(dataBuffer, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// HDLC framing logic
|
|
||||||
if inFrame && b == HDLC_FLAG {
|
|
||||||
inFrame = false
|
|
||||||
tc.handlePacket(dataBuffer)
|
|
||||||
dataBuffer = dataBuffer[:0]
|
|
||||||
} else if b == HDLC_FLAG {
|
|
||||||
inFrame = true
|
|
||||||
} else if inFrame {
|
|
||||||
if b == HDLC_ESC {
|
if b == HDLC_ESC {
|
||||||
escape = true
|
escape = true
|
||||||
} else {
|
} else {
|
||||||
@@ -182,55 +232,79 @@ func (tc *TCPClientInterface) readLoop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) handlePacket(data []byte) {
|
func (tc *TCPClientInterface) handlePacket(data []byte) {
|
||||||
if len(data) < 1 {
|
if len(data) < 1 {
|
||||||
|
debug.Log(debug.DEBUG_ALL, "Received invalid packet: empty")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
packetType := data[0]
|
tc.Mutex.Lock()
|
||||||
payload := data[1:]
|
tc.RxBytes += uint64(len(data))
|
||||||
|
lastRx := time.Now()
|
||||||
|
tc.lastRx = lastRx
|
||||||
|
callback := tc.packetCallback
|
||||||
|
tc.Mutex.Unlock()
|
||||||
|
|
||||||
switch packetType {
|
debug.Log(debug.DEBUG_ALL, "Received packet", "type", fmt.Sprintf("0x%02x", data[0]), "size", len(data))
|
||||||
case 0x01: // Path request
|
|
||||||
tc.Interface.ProcessIncoming(payload)
|
// For RNS packets, call the packet callback directly
|
||||||
case 0x02: // Link packet
|
if callback != nil {
|
||||||
if len(payload) < 40 { // minimum size for link packet
|
debug.Log(debug.DEBUG_ALL, "Calling packet callback for RNS packet")
|
||||||
return
|
callback(data, tc)
|
||||||
}
|
} else {
|
||||||
tc.Interface.ProcessIncoming(payload)
|
debug.Log(debug.DEBUG_ALL, "No packet callback set for TCP interface")
|
||||||
default:
|
|
||||||
// Unknown packet type
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
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")
|
return fmt.Errorf("interface offline")
|
||||||
}
|
}
|
||||||
|
|
||||||
tc.writing = true
|
tc.writing = true
|
||||||
defer func() { tc.writing = false }()
|
defer func() { tc.writing = false }()
|
||||||
|
|
||||||
|
// For TCP connections, use HDLC framing
|
||||||
var frame []byte
|
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([]byte{HDLC_FLAG}, escapeHDLC(data)...)
|
||||||
frame = append(frame, HDLC_FLAG)
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := tc.conn.Write(frame); err != nil {
|
_, err := conn.Write(frame)
|
||||||
tc.teardown()
|
if err != nil {
|
||||||
return fmt.Errorf("write failed: %v", err)
|
debug.Log(debug.DEBUG_CRITICAL, "TCP interface write failed", "name", tc.Name, "error", err)
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
tc.Interface.ProcessOutgoing(data)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) teardown() {
|
func (tc *TCPClientInterface) teardown() {
|
||||||
@@ -238,7 +312,7 @@ func (tc *TCPClientInterface) teardown() {
|
|||||||
tc.IN = false
|
tc.IN = false
|
||||||
tc.OUT = false
|
tc.OUT = false
|
||||||
if tc.conn != nil {
|
if tc.conn != nil {
|
||||||
tc.conn.Close()
|
_ = tc.conn.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,130 +343,203 @@ func escapeKISS(data []byte) []byte {
|
|||||||
return escaped
|
return escaped
|
||||||
}
|
}
|
||||||
|
|
||||||
type TCPServerInterface struct {
|
func (tc *TCPClientInterface) SetPacketCallback(cb common.PacketCallback) {
|
||||||
Interface
|
tc.packetCallback = cb
|
||||||
server net.Listener
|
|
||||||
bindAddr string
|
|
||||||
bindPort int
|
|
||||||
i2pTunneled bool
|
|
||||||
preferIPv6 bool
|
|
||||||
spawned []*TCPClientInterface
|
|
||||||
spawnedMutex sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTCPServer(name string, bindAddr string, bindPort int, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) {
|
func (tc *TCPClientInterface) IsEnabled() bool {
|
||||||
ts := &TCPServerInterface{
|
tc.Mutex.RLock()
|
||||||
Interface: Interface{
|
defer tc.Mutex.RUnlock()
|
||||||
Name: name,
|
return tc.Enabled && tc.Online && !tc.Detached
|
||||||
Mode: MODE_FULL,
|
|
||||||
MTU: 1064,
|
|
||||||
Bitrate: 10000000, // 10Mbps estimate
|
|
||||||
},
|
|
||||||
bindAddr: bindAddr,
|
|
||||||
bindPort: bindPort,
|
|
||||||
i2pTunneled: i2pTunneled,
|
|
||||||
preferIPv6: preferIPv6,
|
|
||||||
spawned: make([]*TCPClientInterface, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve bind address
|
|
||||||
var addr string
|
|
||||||
if ts.bindAddr == "" {
|
|
||||||
if ts.preferIPv6 {
|
|
||||||
addr = fmt.Sprintf("[::0]:%d", ts.bindPort)
|
|
||||||
} else {
|
|
||||||
addr = fmt.Sprintf("0.0.0.0:%d", ts.bindPort)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
addr = fmt.Sprintf("%s:%d", ts.bindAddr, ts.bindPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create listener
|
|
||||||
var err error
|
|
||||||
ts.server, err = net.Listen("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create TCP listener: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ts.Online = true
|
|
||||||
ts.IN = true
|
|
||||||
|
|
||||||
// Start accept loop
|
|
||||||
go ts.acceptLoop()
|
|
||||||
|
|
||||||
return ts, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) acceptLoop() {
|
func (tc *TCPClientInterface) GetName() string {
|
||||||
for {
|
return tc.Name
|
||||||
conn, err := ts.server.Accept()
|
}
|
||||||
if err != nil {
|
|
||||||
if !ts.Detached {
|
func (tc *TCPClientInterface) GetPacketCallback() common.PacketCallback {
|
||||||
// Log error and continue accepting
|
tc.Mutex.RLock()
|
||||||
continue
|
defer tc.Mutex.RUnlock()
|
||||||
|
return tc.packetCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) IsDetached() bool {
|
||||||
|
tc.Mutex.RLock()
|
||||||
|
defer tc.Mutex.RUnlock()
|
||||||
|
return tc.Detached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) IsOnline() bool {
|
||||||
|
tc.Mutex.RLock()
|
||||||
|
defer tc.Mutex.RUnlock()
|
||||||
|
return tc.Online
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) reconnect() {
|
||||||
|
tc.Mutex.Lock()
|
||||||
|
if tc.reconnecting {
|
||||||
|
tc.Mutex.Unlock()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
tc.reconnecting = true
|
||||||
|
tc.Mutex.Unlock()
|
||||||
|
|
||||||
|
backoff := time.Second
|
||||||
|
maxBackoff := time.Minute * 5
|
||||||
|
retries := 0
|
||||||
|
|
||||||
|
for retries < tc.maxReconnectTries {
|
||||||
|
tc.teardown()
|
||||||
|
|
||||||
|
addr := net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort))
|
||||||
|
|
||||||
|
conn, err := net.Dial("tcp", addr)
|
||||||
|
if err == nil {
|
||||||
|
tc.Mutex.Lock()
|
||||||
|
tc.conn = conn
|
||||||
|
tc.Online = true
|
||||||
|
|
||||||
|
tc.neverConnected = false
|
||||||
|
tc.reconnecting = false
|
||||||
|
tc.Mutex.Unlock()
|
||||||
|
|
||||||
|
go tc.readLoop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new client interface for this connection
|
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)
|
||||||
client := &TCPClientInterface{
|
|
||||||
Interface: Interface{
|
// Wait with exponential backoff
|
||||||
Name: fmt.Sprintf("Client-%s-%s", ts.Name, conn.RemoteAddr()),
|
time.Sleep(backoff)
|
||||||
Mode: ts.Mode,
|
|
||||||
MTU: ts.MTU,
|
// Increase backoff time exponentially
|
||||||
|
backoff *= 2
|
||||||
|
if backoff > maxBackoff {
|
||||||
|
backoff = maxBackoff
|
||||||
|
}
|
||||||
|
|
||||||
|
retries++
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.Mutex.Lock()
|
||||||
|
tc.reconnecting = false
|
||||||
|
tc.Mutex.Unlock()
|
||||||
|
|
||||||
|
tc.teardown()
|
||||||
|
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.Online = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) Disable() {
|
||||||
|
tc.Mutex.Lock()
|
||||||
|
defer tc.Mutex.Unlock()
|
||||||
|
tc.Online = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) IsConnected() bool {
|
||||||
|
tc.Mutex.RLock()
|
||||||
|
defer tc.Mutex.RUnlock()
|
||||||
|
return tc.conn != nil && tc.Online && !tc.reconnecting
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) GetRTT() time.Duration {
|
||||||
|
tc.Mutex.RLock()
|
||||||
|
defer tc.Mutex.RUnlock()
|
||||||
|
|
||||||
|
if !tc.IsConnected() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if tcpConn, ok := tc.conn.(*net.TCPConn); ok {
|
||||||
|
var rtt time.Duration
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
if info, err := tcpConn.SyscallConn(); err == nil {
|
||||||
|
if err := info.Control(func(fd uintptr) { // #nosec G104
|
||||||
|
rtt = platformGetRTT(fd)
|
||||||
|
}); err != nil {
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Error in SyscallConn Control", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rtt
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) GetTxBytes() uint64 {
|
||||||
|
tc.Mutex.RLock()
|
||||||
|
defer tc.Mutex.RUnlock()
|
||||||
|
return tc.TxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) GetRxBytes() uint64 {
|
||||||
|
tc.Mutex.RLock()
|
||||||
|
defer tc.Mutex.RUnlock()
|
||||||
|
return tc.RxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) UpdateStats(bytes uint64, isRx bool) {
|
||||||
|
tc.Mutex.Lock()
|
||||||
|
defer tc.Mutex.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if isRx {
|
||||||
|
tc.RxBytes += bytes
|
||||||
|
tc.lastRx = now
|
||||||
|
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
|
||||||
|
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()
|
||||||
|
return tc.TxBytes, tc.RxBytes, tc.lastTx, tc.lastRx
|
||||||
|
}
|
||||||
|
|
||||||
|
type TCPServerInterface struct {
|
||||||
|
BaseInterface
|
||||||
|
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) {
|
||||||
|
ts := &TCPServerInterface{
|
||||||
|
BaseInterface: BaseInterface{
|
||||||
|
Name: name,
|
||||||
|
Mode: common.IF_MODE_FULL,
|
||||||
|
Type: common.IF_TYPE_TCP,
|
||||||
|
Online: false,
|
||||||
|
MTU: common.DEFAULT_MTU,
|
||||||
|
Enabled: true,
|
||||||
|
Detached: false,
|
||||||
},
|
},
|
||||||
conn: conn,
|
connections: make(map[string]net.Conn),
|
||||||
i2pTunneled: ts.i2pTunneled,
|
bindAddr: bindAddr,
|
||||||
|
bindPort: bindPort,
|
||||||
|
preferIPv6: preferIPv6,
|
||||||
|
kissFraming: kissFraming,
|
||||||
|
i2pTunneled: i2pTunneled,
|
||||||
|
done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure TCP options
|
return ts, nil
|
||||||
if tcpConn, ok := conn.(*net.TCPConn); ok {
|
|
||||||
tcpConn.SetNoDelay(true)
|
|
||||||
tcpConn.SetKeepAlive(true)
|
|
||||||
tcpConn.SetKeepAlivePeriod(time.Duration(TCP_PROBE_INTERVAL) * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
client.Online = true
|
|
||||||
client.IN = ts.IN
|
|
||||||
client.OUT = ts.OUT
|
|
||||||
|
|
||||||
// Add to spawned interfaces
|
|
||||||
ts.spawnedMutex.Lock()
|
|
||||||
ts.spawned = append(ts.spawned, client)
|
|
||||||
ts.spawnedMutex.Unlock()
|
|
||||||
|
|
||||||
// Start client read loop
|
|
||||||
go client.readLoop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *TCPServerInterface) Detach() {
|
|
||||||
ts.Interface.Detach()
|
|
||||||
|
|
||||||
if ts.server != nil {
|
|
||||||
ts.server.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
ts.spawnedMutex.Lock()
|
|
||||||
for _, client := range ts.spawned {
|
|
||||||
client.Detach()
|
|
||||||
}
|
|
||||||
ts.spawned = nil
|
|
||||||
ts.spawnedMutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
|
|
||||||
ts.spawnedMutex.RLock()
|
|
||||||
defer ts.spawnedMutex.RUnlock()
|
|
||||||
|
|
||||||
var lastErr error
|
|
||||||
for _, client := range ts.spawned {
|
|
||||||
if err := client.ProcessOutgoing(data); err != nil {
|
|
||||||
lastErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lastErr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) String() string {
|
func (ts *TCPServerInterface) String() string {
|
||||||
@@ -406,3 +553,223 @@ func (ts *TCPServerInterface) String() string {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("TCPServerInterface[%s/%s:%d]", ts.Name, addr, ts.bindPort)
|
return fmt.Sprintf("TCPServerInterface[%s/%s:%d]", ts.Name, addr, ts.bindPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||||
|
ts.Mutex.Lock()
|
||||||
|
defer ts.Mutex.Unlock()
|
||||||
|
ts.packetCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) GetPacketCallback() common.PacketCallback {
|
||||||
|
ts.Mutex.RLock()
|
||||||
|
defer ts.Mutex.RUnlock()
|
||||||
|
return ts.packetCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) IsEnabled() bool {
|
||||||
|
ts.Mutex.RLock()
|
||||||
|
defer ts.Mutex.RUnlock()
|
||||||
|
return ts.Enabled && ts.Online && !ts.Detached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) GetName() string {
|
||||||
|
return ts.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) IsDetached() bool {
|
||||||
|
ts.Mutex.RLock()
|
||||||
|
defer ts.Mutex.RUnlock()
|
||||||
|
return ts.Detached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) IsOnline() bool {
|
||||||
|
ts.Mutex.RLock()
|
||||||
|
defer ts.Mutex.RUnlock()
|
||||||
|
return ts.Online
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) Enable() {
|
||||||
|
ts.Mutex.Lock()
|
||||||
|
defer ts.Mutex.Unlock()
|
||||||
|
ts.Online = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) Disable() {
|
||||||
|
ts.Mutex.Lock()
|
||||||
|
defer ts.Mutex.Unlock()
|
||||||
|
ts.Online = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) Start() error {
|
||||||
|
ts.Mutex.Lock()
|
||||||
|
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 := 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 {
|
||||||
|
ts.Mutex.RLock()
|
||||||
|
online := ts.Online
|
||||||
|
ts.Mutex.RUnlock()
|
||||||
|
if !online {
|
||||||
|
return // Normal shutdown
|
||||||
|
}
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Error accepting connection", "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle each connection in a separate goroutine
|
||||||
|
go ts.handleConnection(conn)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) Stop() error {
|
||||||
|
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()
|
||||||
|
return ts.TxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) GetRxBytes() uint64 {
|
||||||
|
ts.Mutex.RLock()
|
||||||
|
defer ts.Mutex.RUnlock()
|
||||||
|
return ts.RxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
|
||||||
|
addr := conn.RemoteAddr().String()
|
||||||
|
ts.Mutex.Lock()
|
||||||
|
ts.connections[addr] = conn
|
||||||
|
ts.Mutex.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
ts.Mutex.Lock()
|
||||||
|
delete(ts.connections, addr)
|
||||||
|
ts.Mutex.Unlock()
|
||||||
|
_ = conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
buffer := make([]byte, ts.MTU)
|
||||||
|
for {
|
||||||
|
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) // #nosec G115
|
||||||
|
callback := ts.packetCallback
|
||||||
|
ts.Mutex.Unlock()
|
||||||
|
|
||||||
|
if callback != nil {
|
||||||
|
callback(buffer[:n], ts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
|
||||||
|
ts.Mutex.RLock()
|
||||||
|
online := ts.Online
|
||||||
|
ts.Mutex.RUnlock()
|
||||||
|
|
||||||
|
if !online {
|
||||||
|
return fmt.Errorf("interface offline")
|
||||||
|
}
|
||||||
|
|
||||||
|
var frame []byte
|
||||||
|
if ts.kissFraming {
|
||||||
|
frame = append([]byte{KISS_FEND}, escapeKISS(data)...)
|
||||||
|
frame = append(frame, KISS_FEND)
|
||||||
|
} else {
|
||||||
|
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
|
||||||
|
frame = append(frame, HDLC_FLAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.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 {
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "Error writing to connection", "address", conn.RemoteAddr(), "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
16
pkg/interfaces/tcp_common.go
Normal file
16
pkg/interfaces/tcp_common.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
//go:build !linux
|
||||||
|
// +build !linux
|
||||||
|
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// platformGetRTT is defined in OS-specific files
|
||||||
|
// Default implementation for non-Linux platforms
|
||||||
|
func platformGetRTT(fd uintptr) time.Duration {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
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()
|
||||||
|
}
|
||||||
111
pkg/interfaces/tcp_linux.go
Normal file
111
pkg/interfaces/tcp_linux.go
Normal file
@@ -0,0 +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
|
||||||
|
// bearer:disable go_gosec_unsafe_unsafe
|
||||||
|
infoLen := uint32(unsafe.Sizeof(info))
|
||||||
|
|
||||||
|
const TCP_INFO = 11
|
||||||
|
// #nosec G103
|
||||||
|
_, _, errno := syscall.Syscall6(
|
||||||
|
syscall.SYS_GETSOCKOPT,
|
||||||
|
fd,
|
||||||
|
syscall.IPPROTO_TCP,
|
||||||
|
TCP_INFO,
|
||||||
|
// bearer:disable go_gosec_unsafe_unsafe
|
||||||
|
uintptr(unsafe.Pointer(&info)),
|
||||||
|
// bearer:disable go_gosec_unsafe_unsafe
|
||||||
|
uintptr(unsafe.Pointer(&infoLen)),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
if errno != 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
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,88 +1,141 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package interfaces
|
package interfaces
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UDPInterface struct {
|
type UDPInterface struct {
|
||||||
Interface
|
BaseInterface
|
||||||
conn *net.UDPConn
|
conn *net.UDPConn
|
||||||
listenAddr *net.UDPAddr
|
addr *net.UDPAddr
|
||||||
targetAddr *net.UDPAddr
|
targetAddr *net.UDPAddr
|
||||||
readBuffer []byte
|
readBuffer []byte
|
||||||
|
done chan struct{}
|
||||||
|
stopOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUDPInterface(name string, listenAddr string, targetAddr string) (*UDPInterface, error) {
|
func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) {
|
||||||
|
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetAddr *net.UDPAddr
|
||||||
|
if target != "" {
|
||||||
|
targetAddr, err = net.ResolveUDPAddr("udp", target)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ui := &UDPInterface{
|
ui := &UDPInterface{
|
||||||
Interface: Interface{
|
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
|
||||||
Name: name,
|
addr: udpAddr,
|
||||||
Mode: MODE_FULL,
|
targetAddr: targetAddr,
|
||||||
MTU: 1500,
|
readBuffer: make([]byte, common.NUM_1064),
|
||||||
Bitrate: 100000000, // 100Mbps estimate for UDP
|
done: make(chan struct{}),
|
||||||
},
|
|
||||||
readBuffer: make([]byte, 65535),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse listen address
|
ui.MTU = common.NUM_1064
|
||||||
laddr, err := net.ResolveUDPAddr("udp", listenAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid listen address: %v", err)
|
|
||||||
}
|
|
||||||
ui.listenAddr = laddr
|
|
||||||
|
|
||||||
// Parse target address if provided
|
|
||||||
if targetAddr != "" {
|
|
||||||
taddr, err := net.ResolveUDPAddr("udp", targetAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid target address: %v", err)
|
|
||||||
}
|
|
||||||
ui.targetAddr = taddr
|
|
||||||
ui.OUT = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create UDP connection
|
|
||||||
conn, err := net.ListenUDP("udp", ui.listenAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to listen on UDP: %v", err)
|
|
||||||
}
|
|
||||||
ui.conn = conn
|
|
||||||
ui.IN = true
|
|
||||||
ui.Online = true
|
|
||||||
|
|
||||||
// Start read loop
|
|
||||||
go ui.readLoop()
|
|
||||||
|
|
||||||
return ui, nil
|
return ui, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) readLoop() {
|
func (ui *UDPInterface) GetName() string {
|
||||||
for {
|
return ui.Name
|
||||||
if !ui.Online {
|
}
|
||||||
return
|
|
||||||
|
func (ui *UDPInterface) GetType() common.InterfaceType {
|
||||||
|
return ui.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) GetMode() common.InterfaceMode {
|
||||||
|
return ui.Mode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) IsOnline() bool {
|
||||||
|
ui.Mutex.RLock()
|
||||||
|
defer ui.Mutex.RUnlock()
|
||||||
|
return ui.Online
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) IsDetached() bool {
|
||||||
|
ui.Mutex.RLock()
|
||||||
|
defer ui.Mutex.RUnlock()
|
||||||
|
return ui.Detached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) Detach() {
|
||||||
|
ui.Mutex.Lock()
|
||||||
|
defer ui.Mutex.Unlock()
|
||||||
|
ui.Detached = true
|
||||||
|
ui.Online = false
|
||||||
|
if ui.conn != nil {
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
n, addr, err := ui.conn.ReadFromUDP(ui.readBuffer)
|
if ui.targetAddr == nil {
|
||||||
|
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 {
|
if err != nil {
|
||||||
if !ui.Detached {
|
debug.Log(debug.DEBUG_CRITICAL, "UDP interface write failed", "name", ui.Name, "error", err)
|
||||||
// Log error
|
} else {
|
||||||
}
|
debug.Log(debug.DEBUG_ALL, "UDP interface sent bytes successfully", "name", ui.Name, "bytes", len(data))
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Copy received data
|
func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||||
data := make([]byte, n)
|
ui.Mutex.Lock()
|
||||||
copy(data, ui.readBuffer[:n])
|
defer ui.Mutex.Unlock()
|
||||||
|
ui.packetCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
// Process packet
|
func (ui *UDPInterface) GetPacketCallback() common.PacketCallback {
|
||||||
ui.ProcessIncoming(data)
|
ui.Mutex.RLock()
|
||||||
|
defer ui.Mutex.RUnlock()
|
||||||
|
return ui.packetCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) ProcessIncoming(data []byte) {
|
||||||
|
if callback := ui.GetPacketCallback(); callback != nil {
|
||||||
|
callback(data, ui)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
|
func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
|
||||||
if !ui.Online || ui.targetAddr == nil {
|
if !ui.IsOnline() {
|
||||||
return fmt.Errorf("interface offline or no target address configured")
|
return fmt.Errorf("interface offline")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.targetAddr == nil {
|
||||||
|
return fmt.Errorf("no target address configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := ui.conn.WriteToUDP(data, ui.targetAddr)
|
_, err := ui.conn.WriteToUDP(data, ui.targetAddr)
|
||||||
@@ -90,13 +143,151 @@ func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
|
|||||||
return fmt.Errorf("UDP write failed: %v", err)
|
return fmt.Errorf("UDP write failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.Interface.ProcessOutgoing(data)
|
ui.Mutex.Lock()
|
||||||
|
ui.TxBytes += uint64(len(data))
|
||||||
|
ui.Mutex.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) Detach() {
|
func (ui *UDPInterface) GetConn() net.Conn {
|
||||||
ui.Interface.Detach()
|
return ui.conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) GetTxBytes() uint64 {
|
||||||
|
ui.Mutex.RLock()
|
||||||
|
defer ui.Mutex.RUnlock()
|
||||||
|
return ui.TxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) GetRxBytes() uint64 {
|
||||||
|
ui.Mutex.RLock()
|
||||||
|
defer ui.Mutex.RUnlock()
|
||||||
|
return ui.RxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) GetMTU() int {
|
||||||
|
return ui.MTU
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) GetBitrate() int {
|
||||||
|
return int(ui.Bitrate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) Enable() {
|
||||||
|
ui.Mutex.Lock()
|
||||||
|
defer ui.Mutex.Unlock()
|
||||||
|
ui.Online = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) Disable() {
|
||||||
|
ui.Mutex.Lock()
|
||||||
|
defer ui.Mutex.Unlock()
|
||||||
|
ui.Online = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) Start() error {
|
||||||
|
ui.Mutex.Lock()
|
||||||
if ui.conn != nil {
|
if ui.conn != nil {
|
||||||
ui.conn.Close()
|
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, common.NUM_1064)
|
||||||
|
for {
|
||||||
|
ui.Mutex.RLock()
|
||||||
|
online := ui.Online
|
||||||
|
detached := ui.Detached
|
||||||
|
conn := ui.conn
|
||||||
|
done := ui.done
|
||||||
|
ui.Mutex.RUnlock()
|
||||||
|
|
||||||
|
if !online || detached || conn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
n, remoteAddr, err := conn.ReadFromUDP(buffer)
|
||||||
|
if err != nil {
|
||||||
|
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()
|
||||||
|
// #nosec G115 - Network read sizes are always positive and within safe range
|
||||||
|
ui.RxBytes += uint64(n)
|
||||||
|
|
||||||
|
// 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 != nil {
|
||||||
|
callback(buffer[:n], ui)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) IsEnabled() bool {
|
||||||
|
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)
|
||||||
|
}
|
||||||
2044
pkg/link/link.go
2044
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
|
package packet
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -5,10 +7,6 @@ const (
|
|||||||
EncryptedMDU = 383 // Maximum size of payload data in encrypted packet
|
EncryptedMDU = 383 // Maximum size of payload data in encrypted packet
|
||||||
PlainMDU = 464 // Maximum size of payload data in unencrypted packet
|
PlainMDU = 464 // Maximum size of payload data in unencrypted packet
|
||||||
|
|
||||||
// Header Types
|
|
||||||
HeaderType1 = 0 // Two byte header, one 16 byte address field
|
|
||||||
HeaderType2 = 1 // Two byte header, two 16 byte address fields
|
|
||||||
|
|
||||||
// Propagation Types
|
// Propagation Types
|
||||||
PropagationBroadcast = 0
|
PropagationBroadcast = 0
|
||||||
PropagationTransport = 1
|
PropagationTransport = 1
|
||||||
@@ -19,9 +17,7 @@ const (
|
|||||||
DestinationPlain = 2
|
DestinationPlain = 2
|
||||||
DestinationLink = 3
|
DestinationLink = 3
|
||||||
|
|
||||||
// Packet Types
|
// Minimum packet sizes
|
||||||
PacketData = 0
|
MinAnnounceSize = 170 // header(2) + desthash(16) + context(1) + enckey(32) + signkey(32) +
|
||||||
PacketAnnounce = 1
|
// namehash(10) + randomhash(10) + signature(64) + min appdata(3)
|
||||||
PacketLinkRequest = 2
|
|
||||||
PacketProof = 3
|
|
||||||
)
|
)
|
||||||
@@ -1,171 +1,356 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
package packet
|
package packet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
HeaderSize = 2
|
// Packet Types
|
||||||
AddressSize = 16
|
PacketTypeData = 0x00
|
||||||
ContextSize = 1
|
PacketTypeAnnounce = 0x01
|
||||||
MaxDataSize = 465 // Maximum size of payload data
|
PacketTypeLinkReq = 0x02
|
||||||
)
|
PacketTypeProof = 0x03
|
||||||
|
|
||||||
// Header flags and types
|
// Header Types
|
||||||
const (
|
HeaderType1 = 0x00
|
||||||
// First byte flags
|
HeaderType2 = 0x01
|
||||||
IFACFlag = 0x80 // Interface authentication code flag
|
|
||||||
HeaderTypeFlag = 0x40 // Header type flag
|
|
||||||
ContextFlag = 0x20 // Context flag
|
|
||||||
PropagationFlags = 0x18 // Propagation type flags (bits 3-4)
|
|
||||||
DestinationFlags = 0x06 // Destination type flags (bits 1-2)
|
|
||||||
PacketTypeFlags = 0x01 // Packet type flags (bit 0)
|
|
||||||
|
|
||||||
// Second byte
|
// Context Types
|
||||||
HopsField = 0xFF // Number of hops (entire byte)
|
ContextNone = 0x00
|
||||||
|
ContextResource = 0x01
|
||||||
|
ContextResourceAdv = 0x02
|
||||||
|
ContextResourceReq = 0x03
|
||||||
|
ContextResourceHMU = 0x04
|
||||||
|
ContextResourcePRF = 0x05
|
||||||
|
ContextResourceICL = 0x06
|
||||||
|
ContextResourceRCL = 0x07
|
||||||
|
ContextCacheReq = 0x08
|
||||||
|
ContextRequest = 0x09
|
||||||
|
ContextResponse = 0x0A
|
||||||
|
ContextPathResponse = 0x0B
|
||||||
|
ContextCommand = 0x0C
|
||||||
|
ContextCmdStatus = 0x0D
|
||||||
|
ContextChannel = 0x0E
|
||||||
|
ContextKeepalive = 0xFA
|
||||||
|
ContextLinkIdentify = 0xFB
|
||||||
|
ContextLinkClose = 0xFC
|
||||||
|
ContextLinkProof = 0xFD
|
||||||
|
ContextLRRTT = 0xFE
|
||||||
|
ContextLRProof = 0xFF
|
||||||
|
|
||||||
|
// Flag Values
|
||||||
|
FlagSet = 0x01
|
||||||
|
FlagUnset = 0x00
|
||||||
|
|
||||||
|
// Header sizes
|
||||||
|
HeaderMaxSize = 64
|
||||||
|
MTU = 500
|
||||||
|
|
||||||
|
AddressSize = 32 // Size of address/hash fields in bytes
|
||||||
)
|
)
|
||||||
|
|
||||||
type Packet struct {
|
type Packet struct {
|
||||||
Header [2]byte
|
HeaderType byte
|
||||||
Addresses []byte // Either 16 or 32 bytes depending on header type
|
PacketType byte
|
||||||
|
TransportType byte
|
||||||
Context byte
|
Context byte
|
||||||
|
ContextFlag byte
|
||||||
|
Hops byte
|
||||||
|
|
||||||
|
DestinationType byte
|
||||||
|
DestinationHash []byte
|
||||||
|
Destination interface{}
|
||||||
|
TransportID []byte
|
||||||
Data []byte
|
Data []byte
|
||||||
AccessCode []byte // Optional: Only present if IFAC flag is set
|
|
||||||
|
Raw []byte
|
||||||
|
Packed bool
|
||||||
|
Sent bool
|
||||||
|
CreateReceipt bool
|
||||||
|
FromPacked bool
|
||||||
|
|
||||||
|
SentAt time.Time
|
||||||
|
PacketHash []byte
|
||||||
|
RatchetID []byte
|
||||||
|
|
||||||
|
RSSI *float64
|
||||||
|
SNR *float64
|
||||||
|
Q *float64
|
||||||
|
|
||||||
|
Addresses []byte
|
||||||
|
Link interface{}
|
||||||
|
|
||||||
|
receipt *PacketReceipt
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPacket(headerType, propagationType, destinationType, packetType byte, hops byte) *Packet {
|
type PacketConfig struct {
|
||||||
p := &Packet{
|
DestType byte
|
||||||
Header: [2]byte{0, hops},
|
Data []byte
|
||||||
Addresses: make([]byte, 0),
|
PacketType byte
|
||||||
Data: make([]byte, 0),
|
Context byte
|
||||||
|
TransportType byte
|
||||||
|
HeaderType byte
|
||||||
|
TransportID []byte
|
||||||
|
CreateReceipt bool
|
||||||
|
ContextFlag byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPacket(destType byte, data []byte, packetType byte, context byte,
|
||||||
|
transportType byte, headerType byte, transportID []byte, createReceipt bool,
|
||||||
|
contextFlag byte) *Packet {
|
||||||
|
|
||||||
|
return &Packet{
|
||||||
|
HeaderType: headerType,
|
||||||
|
PacketType: packetType,
|
||||||
|
TransportType: transportType,
|
||||||
|
Context: context,
|
||||||
|
ContextFlag: contextFlag,
|
||||||
|
Hops: 0,
|
||||||
|
DestinationType: destType,
|
||||||
|
Data: data,
|
||||||
|
TransportID: transportID,
|
||||||
|
CreateReceipt: createReceipt,
|
||||||
|
Packed: false,
|
||||||
|
Sent: false,
|
||||||
|
FromPacked: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Packet) Pack() error {
|
||||||
|
if p.Packed {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set header type
|
debug.Log(debug.DEBUG_PACKETS, "Packing packet", "type", p.PacketType, "header", p.HeaderType)
|
||||||
if headerType == HeaderType2 {
|
|
||||||
p.Header[0] |= HeaderTypeFlag
|
// Create header byte (Corrected order)
|
||||||
p.Addresses = make([]byte, 2*AddressSize) // Two address fields
|
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}
|
||||||
|
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...)
|
||||||
|
debug.Log(debug.DEBUG_ALL, "Added transport ID to header", "transport_id", fmt.Sprintf("%x", p.TransportID))
|
||||||
|
}
|
||||||
|
|
||||||
|
header = append(header, p.Context)
|
||||||
|
debug.Log(debug.DEBUG_PACKETS, "Final header length", "bytes", len(header))
|
||||||
|
|
||||||
|
p.Raw = append(header, p.Data...)
|
||||||
|
debug.Log(debug.DEBUG_TRACE, "Final packet size", "bytes", len(p.Raw))
|
||||||
|
|
||||||
|
if len(p.Raw) > MTU {
|
||||||
|
return errors.New("packet size exceeds MTU")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Packed = true
|
||||||
|
p.updateHash()
|
||||||
|
debug.Log(debug.DEBUG_ALL, "Packet hash", "hash", fmt.Sprintf("%x", p.PacketHash))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Packet) Unpack() error {
|
||||||
|
if len(p.Raw) < 3 {
|
||||||
|
return errors.New("packet too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := p.Raw[0]
|
||||||
|
p.Hops = p.Raw[1]
|
||||||
|
|
||||||
|
p.HeaderType = (flags & 0b01000000) >> 6
|
||||||
|
p.ContextFlag = (flags & 0b00100000) >> 5
|
||||||
|
p.TransportType = (flags & 0b00010000) >> 4
|
||||||
|
p.DestinationType = (flags & 0b00001100) >> 2
|
||||||
|
p.PacketType = flags & 0b00000011
|
||||||
|
|
||||||
|
dstLen := 16 // Truncated hash length
|
||||||
|
|
||||||
|
if p.HeaderType == HeaderType2 {
|
||||||
|
// 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.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 {
|
} else {
|
||||||
p.Addresses = make([]byte, AddressSize) // One address field
|
// 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")
|
||||||
|
}
|
||||||
|
p.TransportID = nil
|
||||||
|
p.DestinationHash = p.Raw[2 : dstLen+2]
|
||||||
|
p.Context = p.Raw[dstLen+2]
|
||||||
|
p.Data = p.Raw[dstLen+3:]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set propagation type
|
p.Packed = false
|
||||||
p.Header[0] |= (propagationType << 3) & PropagationFlags
|
p.updateHash()
|
||||||
|
|
||||||
// Set destination type
|
|
||||||
p.Header[0] |= (destinationType << 1) & DestinationFlags
|
|
||||||
|
|
||||||
// Set packet type
|
|
||||||
p.Header[0] |= packetType & PacketTypeFlags
|
|
||||||
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Packet) SetAccessCode(code []byte) {
|
|
||||||
p.AccessCode = code
|
|
||||||
p.Header[0] |= IFACFlag
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Packet) SetContext(context byte) {
|
|
||||||
p.Context = context
|
|
||||||
p.Header[0] |= ContextFlag
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Packet) SetData(data []byte) error {
|
|
||||||
if len(data) > MaxDataSize {
|
|
||||||
return errors.New("data exceeds maximum allowed size")
|
|
||||||
}
|
|
||||||
p.Data = data
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Packet) SetAddress(index int, address []byte) error {
|
func (p *Packet) GetHash() []byte {
|
||||||
if len(address) != AddressSize {
|
hashable := p.getHashablePart()
|
||||||
return errors.New("invalid address size")
|
hash := sha256.Sum256(hashable)
|
||||||
}
|
return hash[:]
|
||||||
|
}
|
||||||
|
|
||||||
offset := index * AddressSize
|
func (p *Packet) getHashablePart() []byte {
|
||||||
if offset+AddressSize > len(p.Addresses) {
|
hashable := []byte{p.Raw[0] & 0b00001111} // Lower 4 bits of flags
|
||||||
return errors.New("address index out of range")
|
if p.HeaderType == HeaderType2 {
|
||||||
|
// 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 {
|
||||||
|
// Start hash from DestHash (index 2)
|
||||||
|
if len(p.Raw) > 2 {
|
||||||
|
hashable = append(hashable, p.Raw[2:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hashable
|
||||||
|
}
|
||||||
|
|
||||||
copy(p.Addresses[offset:], address)
|
func (p *Packet) updateHash() {
|
||||||
return nil
|
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) {
|
func (p *Packet) Serialize() ([]byte, error) {
|
||||||
totalSize := HeaderSize + len(p.Addresses) + ContextSize + len(p.Data)
|
if !p.Packed {
|
||||||
if p.AccessCode != nil {
|
if err := p.Pack(); err != nil {
|
||||||
totalSize += len(p.AccessCode)
|
return nil, fmt.Errorf("failed to pack packet: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer := make([]byte, totalSize)
|
p.Addresses = p.DestinationHash
|
||||||
offset := 0
|
|
||||||
|
|
||||||
// Write header
|
return p.Raw, nil
|
||||||
copy(buffer[offset:], p.Header[:])
|
|
||||||
offset += HeaderSize
|
|
||||||
|
|
||||||
// Write access code if present
|
|
||||||
if p.AccessCode != nil {
|
|
||||||
copy(buffer[offset:], p.AccessCode)
|
|
||||||
offset += len(p.AccessCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write addresses
|
|
||||||
copy(buffer[offset:], p.Addresses)
|
|
||||||
offset += len(p.Addresses)
|
|
||||||
|
|
||||||
// Write context
|
|
||||||
buffer[offset] = p.Context
|
|
||||||
offset += ContextSize
|
|
||||||
|
|
||||||
// Write data
|
|
||||||
copy(buffer[offset:], p.Data)
|
|
||||||
|
|
||||||
return buffer, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParsePacket(data []byte) (*Packet, error) {
|
func NewAnnouncePacket(destHash []byte, identity *identity.Identity, appData []byte, transportID []byte) (*Packet, error) {
|
||||||
if len(data) < HeaderSize {
|
debug.Log(debug.DEBUG_ALL, "Creating new announce packet", "dest_hash", fmt.Sprintf("%x", destHash), "app_data", fmt.Sprintf("%x", appData))
|
||||||
return nil, errors.New("packet data too short")
|
|
||||||
|
// Get public key separated into encryption and signing keys
|
||||||
|
pubKey := identity.GetPublicKey()
|
||||||
|
encKey := pubKey[:32]
|
||||||
|
signKey := pubKey[32:]
|
||||||
|
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
|
||||||
|
var appName string
|
||||||
|
if len(appData) > 2 && appData[0] == 0x93 {
|
||||||
|
// This is a node announce, use standard node name
|
||||||
|
appName = "reticulum.node"
|
||||||
|
} else if len(appData) > 3 && appData[0] == 0x92 && appData[1] == 0xc4 {
|
||||||
|
// Try to extract name from peer announce appData
|
||||||
|
nameLen := int(appData[2])
|
||||||
|
if 3+nameLen <= len(appData) {
|
||||||
|
appName = string(appData[3 : 3+nameLen])
|
||||||
|
} else {
|
||||||
|
// Default fallback
|
||||||
|
appName = "reticulum-go.node"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default fallback
|
||||||
|
appName = "reticulum-go.node"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create name hash (10 bytes)
|
||||||
|
nameHash := sha256.Sum256([]byte(appName))
|
||||||
|
nameHash10 := nameHash[:10]
|
||||||
|
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)
|
||||||
|
_, 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())) // #nosec G115
|
||||||
|
copy(randomHash[5:], timeBytes[:5])
|
||||||
|
debug.Log(debug.DEBUG_PACKETS, "Generated random hash", "hash", fmt.Sprintf("%x", randomHash))
|
||||||
|
|
||||||
|
// Prepare ratchet ID if available (not yet implemented)
|
||||||
|
var ratchetID []byte
|
||||||
|
|
||||||
|
// Prepare data for signature
|
||||||
|
// Signature consists of destination hash, public keys, name hash, random hash, and app data
|
||||||
|
signedData := make([]byte, 0, len(destHash)+len(encKey)+len(signKey)+len(nameHash10)+len(randomHash)+len(appData))
|
||||||
|
signedData = append(signedData, destHash...)
|
||||||
|
signedData = append(signedData, encKey...)
|
||||||
|
signedData = append(signedData, signKey...)
|
||||||
|
signedData = append(signedData, nameHash10...)
|
||||||
|
signedData = append(signedData, randomHash...)
|
||||||
|
signedData = append(signedData, appData...)
|
||||||
|
debug.Log(debug.DEBUG_TRACE, "Created signed data", "bytes", len(signedData))
|
||||||
|
|
||||||
|
// Sign the data
|
||||||
|
signature := identity.Sign(signedData)
|
||||||
|
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
|
||||||
|
data := make([]byte, 0, 32+32+10+10+64+len(appData))
|
||||||
|
data = append(data, encKey...) // Encryption key (32 bytes)
|
||||||
|
data = append(data, signKey...) // Signing key (32 bytes)
|
||||||
|
data = append(data, nameHash10...) // Name hash (10 bytes)
|
||||||
|
data = append(data, randomHash...) // Random hash (10 bytes)
|
||||||
|
if ratchetID != nil {
|
||||||
|
data = append(data, ratchetID...) // Ratchet ID (32 bytes if present)
|
||||||
|
}
|
||||||
|
data = append(data, signature...) // Signature (64 bytes)
|
||||||
|
data = append(data, appData...) // Application data (variable)
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_TRACE, "Combined packet data", "bytes", len(data))
|
||||||
|
|
||||||
|
// Create the packet with header type 2 (two address fields)
|
||||||
p := &Packet{
|
p := &Packet{
|
||||||
Header: [2]byte{data[0], data[1]},
|
HeaderType: HeaderType2,
|
||||||
|
PacketType: PacketTypeAnnounce,
|
||||||
|
TransportID: transportID,
|
||||||
|
DestinationHash: destHash,
|
||||||
|
Data: data,
|
||||||
}
|
}
|
||||||
|
|
||||||
offset := HeaderSize
|
debug.Log(debug.DEBUG_VERBOSE, "Created announce packet", "type", p.PacketType, "header", p.HeaderType)
|
||||||
|
|
||||||
// Handle access code if present
|
|
||||||
if p.Header[0]&IFACFlag != 0 {
|
|
||||||
// Access code handling would go here
|
|
||||||
// For now, we'll assume no access code
|
|
||||||
return nil, errors.New("access code handling not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine address size based on header type
|
|
||||||
addrLen := AddressSize
|
|
||||||
if p.Header[0]&HeaderTypeFlag != 0 {
|
|
||||||
addrLen = 2 * AddressSize
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data[offset:]) < addrLen+ContextSize {
|
|
||||||
return nil, errors.New("packet data too short for addresses and context")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy addresses
|
|
||||||
p.Addresses = make([]byte, addrLen)
|
|
||||||
copy(p.Addresses, data[offset:offset+addrLen])
|
|
||||||
offset += addrLen
|
|
||||||
|
|
||||||
// Copy context
|
|
||||||
p.Context = data[offset]
|
|
||||||
offset++
|
|
||||||
|
|
||||||
// Copy remaining data
|
|
||||||
p.Data = make([]byte, len(data)-offset)
|
|
||||||
copy(p.Data, data[offset:])
|
|
||||||
|
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user