mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-18 09:27:17 +01:00
Compare commits
1588 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d61bd5ce51 | ||
|
|
bad3e9a989 | ||
|
|
9adfd73121 | ||
|
|
4a652aaf55 | ||
|
|
16c986978d | ||
|
|
107b7c5f64 | ||
|
|
edcde00dcc | ||
|
|
7d466625d6 | ||
|
|
8399a9ece7 | ||
|
|
966f0ab9c3 | ||
|
|
aaa3c9a8d8 | ||
|
|
bc44de3196 | ||
|
|
12b784d126 | ||
|
|
71f6636cc3 | ||
|
|
cc1fe30045 | ||
|
|
4ec352f1f6 | ||
|
|
df530245bd | ||
|
|
1a022bb3f4 | ||
|
|
2e57ca7743 | ||
|
|
69d04f1b76 | ||
|
|
74f97a6621 | ||
|
|
dc1b70d2d7 | ||
|
|
6fac5d2d3e | ||
|
|
4275cdae38 | ||
|
|
45c821fa98 | ||
|
|
d4b7ae808f | ||
|
|
7687dca456 | ||
|
|
45d6e3bab7 | ||
|
|
41eb8c2ffa | ||
|
|
2e3ebefc4e | ||
|
|
5aa7dc09e5 | ||
|
|
c7d4703622 | ||
|
|
7e99f3465f | ||
|
|
e9d7edef12 | ||
|
|
13441286d1 | ||
|
|
86f35878fb | ||
|
|
7556a06716 | ||
|
|
7385761bdf | ||
|
|
581503e160 | ||
|
|
243e7e9e95 | ||
|
|
8b5cb947c8 | ||
|
|
9ea9e62ee8 | ||
|
|
1ebba20216 | ||
|
|
7bfb57ea30 | ||
|
|
25ceb512b4 | ||
|
|
9205af3a4f | ||
|
|
08f4d9e95f | ||
|
|
a44b9e352c | ||
|
|
424398442b | ||
|
|
724617a2b3 | ||
|
|
61c8ac04e8 | ||
|
|
cc27942c4d | ||
|
|
1c2515cb29 | ||
|
|
45720db754 | ||
|
|
1b9cfa6540 | ||
|
|
f1d906ac11 | ||
|
|
2835fd5fb0 | ||
|
|
818d75c8b7 | ||
|
|
11d0c61b9c | ||
|
|
f1bc5de3ea | ||
|
|
c00854a124 | ||
|
|
117dbb62f4 | ||
|
|
2c28bc116c | ||
|
|
1d90bec9ed | ||
|
|
b2df749cd1 | ||
|
|
1916f73e78 | ||
|
|
99ab9beb4a | ||
|
|
5de064aa47 | ||
|
|
880e11c414 | ||
|
|
0dfce823bf | ||
|
|
c2583fc756 | ||
|
|
cf6246d58a | ||
|
|
fb040afe90 | ||
|
|
dc8abe943d | ||
|
|
587b83cf14 | ||
|
|
425ff0b25c | ||
|
|
1f6614e337 | ||
|
|
9ba102a33d | ||
|
|
a4658caf02 | ||
|
|
ef9ee0e169 | ||
|
|
7eadec9752 | ||
|
|
dd35a4159f | ||
|
|
f28667e23e | ||
|
|
31c616246b | ||
|
|
8009da9e4d | ||
|
|
390859bd1f | ||
|
|
590743f1ef | ||
|
|
1f4c30a48e | ||
|
|
bae7387a5d | ||
|
|
67fc48383d | ||
|
|
1406881071 | ||
|
|
7976befda4 | ||
|
|
8139311074 | ||
|
|
2690bf548d | ||
|
|
d3358ebd89 | ||
|
|
243662c13b | ||
|
|
fd74bfedf0 | ||
|
|
a47170da39 | ||
|
|
89a4ca767d | ||
|
|
3dbbde164b | ||
|
|
588e9f5b18 | ||
|
|
e75eede332 | ||
|
|
a3bf88cc9c | ||
|
|
e4658a8f09 | ||
|
|
e25ccdbd24 | ||
|
|
5087800fd7 | ||
|
|
9b1af57859 | ||
|
|
bb7471cc9c | ||
|
|
d7f33b7390 | ||
|
|
1978329314 | ||
|
|
dba8441e8a | ||
|
|
44fc678496 | ||
|
|
0b410311da | ||
|
|
dc39f0cb6e | ||
|
|
e232b9d122 | ||
|
|
a403b2b629 | ||
|
|
41f8d3cfc0 | ||
|
|
5ab0392cd3 | ||
|
|
54b9e7f236 | ||
|
|
45b89cd452 | ||
|
|
09702266a9 | ||
|
|
14f3ed95ea | ||
|
|
72fea96c7b | ||
|
|
eb3aa21e37 | ||
|
|
a6e86ea420 | ||
|
|
dd96e09a7a | ||
|
|
4d08efbd4f | ||
|
|
aef646be6f | ||
|
|
f67480d085 | ||
|
|
736985b79d | ||
|
|
1fb1ee0279 | ||
|
|
4b2a6023bb | ||
|
|
5852053ef9 | ||
|
|
c687795cd8 | ||
|
|
93af695e95 | ||
|
|
58325e60b4 | ||
|
|
135a4ff6c7 | ||
|
|
b134b92704 | ||
|
|
376ac61279 | ||
|
|
dca701e044 | ||
|
|
4bb3af3671 | ||
|
|
95efc127cf | ||
|
|
6e55c4624b | ||
|
|
e9374364dd | ||
|
|
216679eb8d | ||
|
|
505a3d3972 | ||
|
|
27512b4d04 | ||
|
|
5f418b62c7 | ||
|
|
bd92c46375 | ||
|
|
21a23dd147 | ||
|
|
88d7255c7a | ||
|
|
ea67095967 | ||
|
|
86a46d191d | ||
|
|
b7250b29e0 | ||
|
|
e44ecc0ccc | ||
|
|
6f9f995100 | ||
|
|
496aec6bb6 | ||
|
|
4afed02fc2 | ||
|
|
f7eb4b132a | ||
|
|
ff934a4bb2 | ||
|
|
db0cbc6577 | ||
|
|
de3f92246f | ||
|
|
c143593284 | ||
|
|
31bf889d4a | ||
|
|
baa7e72ad6 | ||
|
|
f43e07fe60 | ||
|
|
d319ee99ad | ||
|
|
ab58559afc | ||
|
|
a6bdbb5603 | ||
|
|
a0c589c546 | ||
|
|
76b8252755 | ||
|
|
d547872a41 | ||
|
|
8d4618cedf | ||
|
|
2ba758939b | ||
|
|
fdd37b777a | ||
|
|
bc19a54976 | ||
|
|
12d999809f | ||
|
|
6771293336 | ||
|
|
d240c9dfee | ||
|
|
c7eda38933 | ||
|
|
09caa888ad | ||
|
|
e41a487371 | ||
|
|
7c08a8da2e | ||
|
|
82df824490 | ||
|
|
2f341001c1 | ||
|
|
25ee8041da | ||
|
|
8687a57b6c | ||
|
|
3f4ed31e46 | ||
|
|
9930f3fa2e | ||
|
|
2157545e17 | ||
|
|
f721395ff0 | ||
|
|
0dc7c59af1 | ||
|
|
e3fe126a5c | ||
|
|
aa2575696d | ||
|
|
c1f9c2c957 | ||
|
|
c098fef615 | ||
|
|
9cdc985fb0 | ||
|
|
2034738422 | ||
|
|
55a42b81de | ||
|
|
48627753d6 | ||
|
|
09b514393d | ||
|
|
3b2ae5dbd6 | ||
|
|
fac3d67a51 | ||
|
|
cb642d7b32 | ||
|
|
9285977495 | ||
|
|
e00cd8a35b | ||
|
|
8ac459c038 | ||
|
|
1bcaf0dab5 | ||
|
|
a291a49a0e | ||
|
|
28fdf3d2f4 | ||
|
|
84b17baf46 | ||
|
|
06ddb178f8 | ||
|
|
61fa7d2665 | ||
|
|
615521ee1c | ||
|
|
bbe308e821 | ||
|
|
c156173757 | ||
|
|
b1aae1cacf | ||
|
|
f46552b477 | ||
|
|
efe1350ffd | ||
|
|
219eedf3c5 | ||
|
|
f6dcc8f118 | ||
|
|
4d6541c851 | ||
|
|
c9db350cbc | ||
|
|
56374d595a | ||
|
|
d81521f293 | ||
|
|
e9ac3cd1a9 | ||
|
|
d33ff2192a | ||
|
|
910ef639a4 | ||
|
|
3cbd70f73a | ||
|
|
83d70d3bb2 | ||
|
|
bbb1b8497f | ||
|
|
d57d76dc65 | ||
|
|
ef893974ea | ||
|
|
b90f2409ab | ||
|
|
36e9b0d416 | ||
|
|
306cb7a20e | ||
|
|
e3915210aa | ||
|
|
e8fb202ea9 | ||
|
|
082b2f5da2 | ||
|
|
e670acb4b8 | ||
|
|
77e486f4fe | ||
|
|
3ccaba3163 | ||
|
|
705923960c | ||
|
|
ca737c8979 | ||
|
|
b6b5d4dbd7 | ||
|
|
b2919fbaf6 | ||
|
|
722c40d103 | ||
|
|
860d9c71b6 | ||
|
|
e354d901c4 | ||
|
|
921a8fb935 | ||
|
|
975354cdc1 | ||
|
|
7d38bfd2d2 | ||
|
|
5506cafa26 | ||
|
|
9fd5bff81a | ||
|
|
38041ca5b8 | ||
|
|
61be88c1d3 | ||
|
|
cb4dcb962e | ||
|
|
1797a222cd | ||
|
|
098fb7e62d | ||
|
|
d4dfec8293 | ||
|
|
f29b69ff3b | ||
|
|
5e00e1c437 | ||
|
|
39c8cc2820 | ||
|
|
56232dbd0e | ||
|
|
baf774f927 | ||
|
|
a3c82209c6 | ||
|
|
386d946bd2 | ||
|
|
ee9bf31d30 | ||
|
|
2c87eebee3 | ||
|
|
5be784d567 | ||
|
|
a999c51bf8 | ||
|
|
7ca722b256 | ||
|
|
51295be463 | ||
|
|
51fc5f017a | ||
|
|
e4996733fc | ||
|
|
f76d86dfa2 | ||
|
|
8778f4ea73 | ||
|
|
6f75bb7593 | ||
|
|
964ba1eac1 | ||
|
|
6e7b571946 | ||
|
|
fc7a81faf5 | ||
|
|
488ad160e7 | ||
|
|
1ec2872f3d | ||
|
|
9c3346dd9d | ||
|
|
203faa8e7e | ||
|
|
fbc853fa6a | ||
|
|
3fefbdfded | ||
|
|
48be6def12 | ||
|
|
94d6b7a168 | ||
|
|
1ca4b4939e | ||
|
|
f8716d990e | ||
|
|
5a91db8d10 | ||
|
|
3e73be60a1 | ||
|
|
af9363209b | ||
|
|
ccc35b2a00 | ||
|
|
44536139c1 | ||
|
|
2b4c39a79e | ||
|
|
ddf78aacba | ||
|
|
f5a006ce81 | ||
|
|
290af4e311 | ||
|
|
feafdf05f2 | ||
|
|
b09bfd6c1e | ||
|
|
e13b18621d | ||
|
|
53f3397b7a | ||
|
|
19968834d2 | ||
|
|
d41c6f8d77 | ||
|
|
dcc5ab8952 | ||
|
|
cc8858332d | ||
|
|
82f02ea2bf | ||
|
|
046ff8a020 | ||
|
|
dc9ae32e8f | ||
|
|
5640d5d454 | ||
|
|
c66de99fcb | ||
|
|
eef994082c | ||
|
|
8c670ab92e | ||
|
|
d11ddb7c91 | ||
|
|
78aea4b4d2 | ||
|
|
80dd142861 | ||
|
|
92aa61e732 | ||
|
|
848f26aa86 | ||
|
|
81e500fcfc | ||
|
|
f417e0fa25 | ||
|
|
cb5a8e7b9d | ||
|
|
16cad11e89 | ||
|
|
2bfbdbf519 | ||
|
|
d5e9a7b3b6 | ||
|
|
7ea415078f | ||
|
|
e67704695b | ||
|
|
804c7eec60 | ||
|
|
ea8be56bf8 | ||
|
|
20c77edce5 | ||
|
|
4f2f0f58e2 | ||
|
|
ac8ad149b8 | ||
|
|
14ec80c883 | ||
|
|
5de5f854ce | ||
|
|
3d8994b42e | ||
|
|
66043e4a26 | ||
|
|
d1e403e16f | ||
|
|
e72e20af69 | ||
|
|
ad6201c27a | ||
|
|
c4c9e9300c | ||
|
|
b23c3f1c3b | ||
|
|
38c0419483 | ||
|
|
357ce38b18 | ||
|
|
ef34c3ffdd | ||
|
|
2e411373a2 | ||
|
|
3dedd66ad1 | ||
|
|
98f047d88a | ||
|
|
973a58e982 | ||
|
|
4b55d1c607 | ||
|
|
63eff4707c | ||
|
|
55a74c36b0 | ||
|
|
fbabb7b7fb | ||
|
|
7a1841e9a5 | ||
|
|
d82bfd0ebd | ||
|
|
1f41c035ea | ||
|
|
c2c9f42fb3 | ||
|
|
60cfff3435 | ||
|
|
c93a460043 | ||
|
|
9bf7a0beef | ||
|
|
c89c737ecd | ||
|
|
382fc61a9c | ||
|
|
b2de33e835 | ||
|
|
86644054e6 | ||
|
|
c2dcabe144 | ||
|
|
c59ddc1df6 | ||
|
|
f4db874fd6 | ||
|
|
f334f5c13c | ||
|
|
5acc4c3894 | ||
|
|
a8aa82f687 | ||
|
|
0f3a1ac6e6 | ||
|
|
9fceda6729 | ||
|
|
becb49e864 | ||
|
|
3aed41e078 | ||
|
|
8047067b2b | ||
|
|
c3fa7c66a7 | ||
|
|
cab68807ee | ||
|
|
d08be872a0 | ||
|
|
bb5f0cdf09 | ||
|
|
a150f1a628 | ||
|
|
584db2efce | ||
|
|
c27bc0e129 | ||
|
|
b46b464e65 | ||
|
|
52ec309f6b | ||
|
|
6051f75145 | ||
|
|
f4f104d206 | ||
|
|
448a2fbd6f | ||
|
|
74224c8e87 | ||
|
|
ae57edfcb0 | ||
|
|
fc23e262d7 | ||
|
|
11a3935e0c | ||
|
|
42e7adbf86 | ||
|
|
1e0c7a15d8 | ||
|
|
ba8edb160f | ||
|
|
4852efcf9c | ||
|
|
ef40793301 | ||
|
|
80862bcd2e | ||
|
|
45b16abd68 | ||
|
|
f411e17d80 | ||
|
|
024100aa8c | ||
|
|
9d508c5950 | ||
|
|
2ff5e5c0b6 | ||
|
|
2a05c6a630 | ||
|
|
6776f20332 | ||
|
|
5043ef778f | ||
|
|
22bcf1201b | ||
|
|
acecd827d6 | ||
|
|
b2713a4b83 | ||
|
|
e2aeef3a86 | ||
|
|
9545482a44 | ||
|
|
d406b940d9 | ||
|
|
dc1175ad69 | ||
|
|
1409a4e8b9 | ||
|
|
8ec9752656 | ||
|
|
a932688ca3 | ||
|
|
55c1c918ba | ||
|
|
14e243d245 | ||
|
|
f7149453d6 | ||
|
|
00d137d05c | ||
|
|
f9affba9fc | ||
|
|
6b3bf84148 | ||
|
|
62a667758d | ||
|
|
ddd27156fc | ||
|
|
af8e2d56b2 | ||
|
|
74a215b894 | ||
|
|
ccdc0046fd | ||
|
|
2f7fdc4c51 | ||
|
|
de1f4da126 | ||
|
|
a48ccb4423 | ||
|
|
193fd9a249 | ||
|
|
0bc4c4af77 | ||
|
|
5fa1417add | ||
|
|
b763c92645 | ||
|
|
09b14a47e9 | ||
|
|
83a69322fa | ||
|
|
3aba5a1911 | ||
|
|
ca805edfe0 | ||
|
|
7205bf47de | ||
|
|
b12999210f | ||
|
|
8b8969f033 | ||
|
|
025ebab1ce | ||
|
|
ea7bd0d19a | ||
|
|
f889f5c08d | ||
|
|
932c20f32d | ||
|
|
2a08c55e39 | ||
|
|
93e1d17090 | ||
|
|
d72d403e2c | ||
|
|
b5d70a0592 | ||
|
|
da71dcf058 | ||
|
|
6b17272347 | ||
|
|
98afb02e7f | ||
|
|
103fd3b904 | ||
|
|
59917f52d7 | ||
|
|
24fb2e07e6 | ||
|
|
8f1c02ca72 | ||
|
|
e359bc8fd9 | ||
|
|
7b028adaa9 | ||
|
|
f3913e1f6f | ||
|
|
b72f3bde53 | ||
|
|
6077a1d70b | ||
|
|
59cae0967a | ||
|
|
1e1999b0af | ||
|
|
b64725f2f8 | ||
|
|
124069aaa4 | ||
|
|
d56663d3f9 | ||
|
|
d1476edf91 | ||
|
|
4ed6c7c74d | ||
|
|
f31b1b5ed3 | ||
|
|
e0d25e475c | ||
|
|
ef65481394 | ||
|
|
1e9303b1ef | ||
|
|
2c290a3916 | ||
|
|
58a2dc73dd | ||
|
|
1c080e067d | ||
|
|
2717dc963a | ||
|
|
4509622dde | ||
|
|
60c13a797b | ||
|
|
5e1da915dc | ||
|
|
3288624cf2 | ||
|
|
190d5e1ece | ||
|
|
0d2229cca0 | ||
|
|
493c0afdfa | ||
|
|
99c1922342 | ||
|
|
a483e15a20 | ||
|
|
fbe82c3082 | ||
|
|
24bcc2d2d2 | ||
|
|
d8c8cff8b7 | ||
|
|
ef54d336a2 | ||
|
|
0a5df1bd7f | ||
|
|
205928a741 | ||
|
|
11d18091fd | ||
|
|
3be72e5c68 | ||
|
|
a9847b6f81 | ||
|
|
04d823d616 | ||
|
|
1be2ea44a2 | ||
|
|
978407ae7e | ||
|
|
81f8bad77d | ||
|
|
f7de703c15 | ||
|
|
acf7490991 | ||
|
|
7770ce7025 | ||
|
|
c9c5677b35 | ||
|
|
226ee2e5e5 | ||
|
|
aec937a114 | ||
|
|
bab9471bde | ||
|
|
4ebd1dbf32 | ||
|
|
82a4a61df0 | ||
|
|
9e56ea5db1 | ||
|
|
719682c99f | ||
|
|
f81a2b6607 | ||
|
|
f47ba0a9b5 | ||
|
|
52e949de85 | ||
|
|
abeb26b556 | ||
|
|
23d392d88b | ||
|
|
d588664bfa | ||
|
|
41ce784a7f | ||
|
|
577169d03c | ||
|
|
b43274e9e6 | ||
|
|
d83c367e7f | ||
|
|
d9fbd53870 | ||
|
|
7f54f50af8 | ||
|
|
8339c42470 | ||
|
|
ed39942d65 | ||
|
|
998488f285 | ||
|
|
aac5016b78 | ||
|
|
d2b4d3e6e3 | ||
|
|
a2d4c468cd | ||
|
|
c550255458 | ||
|
|
6a3e28dfd7 | ||
|
|
4513c221d5 | ||
|
|
245dba034e | ||
|
|
f39896fe30 | ||
|
|
b051987a1c | ||
|
|
c128557c81 | ||
|
|
6405325e56 | ||
|
|
c3d2a90501 | ||
|
|
31d49453a7 | ||
|
|
04657420b8 | ||
|
|
2f0b8b6c09 | ||
|
|
5e15fd4bbe | ||
|
|
a5022e31a2 | ||
|
|
a057f0e956 | ||
|
|
dfe0014609 | ||
|
|
dfc2d5e35c | ||
|
|
d3bfb2488b | ||
|
|
baf5b5eff1 | ||
|
|
1c7e3e42f8 | ||
|
|
beb1913285 | ||
|
|
e14d6baedb | ||
|
|
cfb37d5bd0 | ||
|
|
f53d384533 | ||
|
|
8360aa59d1 | ||
|
|
6ec1016f29 | ||
|
|
35b0dcb418 | ||
|
|
353f818b41 | ||
|
|
b58cabf998 | ||
|
|
231c0c7665 | ||
|
|
9931c10fa6 | ||
|
|
d56a6bc19d | ||
|
|
e0a110cad3 | ||
|
|
d1eb3470b5 | ||
|
|
e52c86e0b7 | ||
|
|
c19d82c876 | ||
|
|
d2f317b44d | ||
|
|
ba9cb083cf | ||
|
|
06669534cd | ||
|
|
07d6f36159 | ||
|
|
55018c8ab6 | ||
|
|
0862920324 | ||
|
|
b32750d545 | ||
|
|
a836920eca | ||
|
|
6b89cd9106 | ||
|
|
11af9d107a | ||
|
|
7a9b8b3fb9 | ||
|
|
90efa36193 | ||
|
|
1e78a0a0a0 | ||
|
|
52324fbef2 | ||
|
|
8b40baa49f | ||
|
|
35a3e3fef6 | ||
|
|
fce9ce21c9 | ||
|
|
475e697490 | ||
|
|
68ac4f952d | ||
|
|
a2e6688056 | ||
|
|
e02cacdf2a | ||
|
|
46c7ee4d84 | ||
|
|
f39513483b | ||
|
|
731121595c | ||
|
|
8025af6067 | ||
|
|
47910774dd | ||
|
|
b6bfd19cc2 | ||
|
|
e3b53a548d | ||
|
|
a954ac8946 | ||
|
|
814ff33352 | ||
|
|
b1d5c4b091 | ||
|
|
72dc783e23 | ||
|
|
1c95bbba6e | ||
|
|
0c552c9cea | ||
|
|
5631b1540a | ||
|
|
24f949f053 | ||
|
|
9d712b91ff | ||
|
|
4189ffa1db | ||
|
|
e906b358fa | ||
|
|
f179de9231 | ||
|
|
1d546624de | ||
|
|
ecc9d306d1 | ||
|
|
5ce1c7865e | ||
|
|
7d17a01de1 | ||
|
|
cabb840a91 | ||
|
|
4825f768f3 | ||
|
|
5fdb023188 | ||
|
|
4abf61a421 | ||
|
|
96b7c3fcec | ||
|
|
f8c57d930f | ||
|
|
880d66c75e | ||
|
|
4649c8d479 | ||
|
|
20021b3cae | ||
|
|
cfa9201f82 | ||
|
|
b5328fe5e7 | ||
|
|
25fbcc4ab9 | ||
|
|
421aaecba4 | ||
|
|
01773976d1 | ||
|
|
2263d6063e | ||
|
|
cfe0f6bb70 | ||
|
|
a90d2b90d1 | ||
|
|
af9629424e | ||
|
|
ee6cf29bc1 | ||
|
|
c4a780e061 | ||
|
|
09c244ef3c | ||
|
|
bd0fe36c53 | ||
|
|
d240da4393 | ||
|
|
9470a14fe8 | ||
|
|
d3568d9c35 | ||
|
|
44ef351840 | ||
|
|
a39d527fc1 | ||
|
|
22ab043e06 | ||
|
|
b670cdbd49 | ||
|
|
45e34d691a | ||
|
|
e82480a639 | ||
|
|
e39407886d | ||
|
|
3135e377a9 | ||
|
|
bdb3343a7c | ||
|
|
b411c6d504 | ||
|
|
966a59b5c9 | ||
|
|
58db228e25 | ||
|
|
e737737415 | ||
|
|
9087c4f195 | ||
|
|
4705989f4b | ||
|
|
cb506120dd | ||
|
|
88aaf956e5 | ||
|
|
ecfd018b0b | ||
|
|
54bf84dcba | ||
|
|
57200bc1e9 | ||
|
|
6f9bb410f5 | ||
|
|
e62e667b49 | ||
|
|
abe81541db | ||
|
|
9e5d33714c | ||
|
|
93a81fd558 | ||
|
|
72923b8cfa | ||
|
|
24ba4c2a46 | ||
|
|
ed07bf42ce | ||
|
|
371e756307 | ||
|
|
32d8292b17 | ||
|
|
717fd0e58c | ||
|
|
2628d9e8a8 | ||
|
|
c90795e614 | ||
|
|
4a6bed7728 | ||
|
|
216c03c5ff | ||
|
|
2e9f113224 | ||
|
|
9d58977fa6 | ||
|
|
8469b6406c | ||
|
|
b163771956 | ||
|
|
c1221e61d4 | ||
|
|
4a8bd48ad5 | ||
|
|
ade93d49a3 | ||
|
|
82ee75daab | ||
|
|
f0ab14cb1e | ||
|
|
5b7c392297 | ||
|
|
1f1ae38e4d | ||
|
|
22d44a6bb0 | ||
|
|
6a5cd1266b | ||
|
|
1cf18657b6 | ||
|
|
63c4bdc73d | ||
|
|
20a1649275 | ||
|
|
0f3b8e68ce | ||
|
|
5a3e3f19c7 | ||
|
|
df193a42fc | ||
|
|
f1e204f7fd | ||
|
|
ff08c40403 | ||
|
|
d8266f779f | ||
|
|
9711867fbe | ||
|
|
fc8592ab45 | ||
|
|
3dbab118af | ||
|
|
1f50ee7f2f | ||
|
|
cee6eaecff | ||
|
|
67a6b89ea5 | ||
|
|
78be9b1c71 | ||
|
|
26856b612a | ||
|
|
36ceba3ae7 | ||
|
|
f45f3fba79 | ||
|
|
4bbff323e3 | ||
|
|
2e68baa93e | ||
|
|
a162371ec5 | ||
|
|
8f9c76daa5 | ||
|
|
8b3e058885 | ||
|
|
023cbc81bc | ||
|
|
b490e8c475 | ||
|
|
8e27886235 | ||
|
|
7435b8e485 | ||
|
|
21724c037f | ||
|
|
44b4cff35e | ||
|
|
1e24765b17 | ||
|
|
a1f2a84a16 | ||
|
|
453262832a | ||
|
|
99e975145c | ||
|
|
e300170c51 | ||
|
|
1382137f20 | ||
|
|
54d7508f5d | ||
|
|
71ca8c738e | ||
|
|
f1eefde964 | ||
|
|
84e7a6591e | ||
|
|
30c76cfc5f | ||
|
|
a8ba42e360 | ||
|
|
cd291556fc | ||
|
|
0d41809630 | ||
|
|
53acf75c04 | ||
|
|
cf30fe6cfc | ||
|
|
55bbcae911 | ||
|
|
b30c0d7dc0 | ||
|
|
198ae2cd02 | ||
|
|
26938eb6ed | ||
|
|
48823a860f | ||
|
|
985ff0a74d | ||
|
|
43b493c60e | ||
|
|
e0e0fab127 | ||
|
|
fc0dbd940c | ||
|
|
0208e6286f | ||
|
|
2c0b68c8c2 | ||
|
|
c05059765d | ||
|
|
a06787593c | ||
|
|
8fe94d6d14 | ||
|
|
4ddfb48b9d | ||
|
|
31dc112591 | ||
|
|
6797897814 | ||
|
|
99eccd0b95 | ||
|
|
0387739b94 | ||
|
|
ead27c72f1 | ||
|
|
455a85e6a0 | ||
|
|
8424fd9f1a | ||
|
|
75ee0e63bd | ||
|
|
1ce607029a | ||
|
|
1e80ad2a44 | ||
|
|
4daefa19d1 | ||
|
|
491231e439 | ||
|
|
c90ec8caa1 | ||
|
|
9eb674029e | ||
|
|
e41c6530ab | ||
|
|
afd35c183d | ||
|
|
f190483b4e | ||
|
|
7b0ed09772 | ||
|
|
4415bffc35 | ||
|
|
ddab2766b4 | ||
|
|
ef95682116 | ||
|
|
dd65a8d04b | ||
|
|
aa23b5b595 | ||
|
|
c55c6c84bc | ||
|
|
a45e5e17db | ||
|
|
b8c0961de3 | ||
|
|
62d3d200e6 | ||
|
|
bf32cafd90 | ||
|
|
1c182b5a7d | ||
|
|
ad60f377ba | ||
|
|
75db09b1f3 | ||
|
|
6dd849f480 | ||
|
|
e2ae29795d | ||
|
|
92fa0f8168 | ||
|
|
b090598b68 | ||
|
|
2cec88d3ce | ||
|
|
4df31263b5 | ||
|
|
9eae809690 | ||
|
|
f1ba554a24 | ||
|
|
f9a8aede20 | ||
|
|
e275ee634c | ||
|
|
797d88772f | ||
|
|
8ef8015a7f | ||
|
|
5fce4b445b | ||
|
|
7552a706a7 | ||
|
|
e1bc6d1f44 | ||
|
|
56850a9580 | ||
|
|
5f780f4902 | ||
|
|
ccb4639f43 | ||
|
|
ac1470d81d | ||
|
|
efaabfa63a | ||
|
|
9043cf25c5 | ||
|
|
98e90d7a0b | ||
|
|
82c829de18 | ||
|
|
2fe4fef779 | ||
|
|
91302ceed7 | ||
|
|
7fa7b55b18 | ||
|
|
69ee8495d8 | ||
|
|
28d9a72908 | ||
|
|
770c698332 | ||
|
|
cd4c843025 | ||
|
|
f0cf89060b | ||
|
|
f79a15bac6 | ||
|
|
2b4a70a550 | ||
|
|
f06741428c | ||
|
|
16e6e72454 | ||
|
|
100d2c392f | ||
|
|
829eb08e37 | ||
|
|
53d54a09b0 | ||
|
|
62c551c7fe | ||
|
|
80e59bb481 | ||
|
|
7a5afc3612 | ||
|
|
2c0349c11c | ||
|
|
8e3c2cc8d4 | ||
|
|
d35afdb3c9 | ||
|
|
ae093ebf40 | ||
|
|
aa8af4185b | ||
|
|
0029cf69d6 | ||
|
|
33e400a17e | ||
|
|
1d22bcfed9 | ||
|
|
978d82060e | ||
|
|
7aa1215491 | ||
|
|
0b69589586 | ||
|
|
bca3cd84d1 | ||
|
|
ce4bf2f646 | ||
|
|
c49016f22c | ||
|
|
8da63daf02 | ||
|
|
c5fd21552e | ||
|
|
27409abc24 | ||
|
|
21c9e46274 | ||
|
|
22a12d3116 | ||
|
|
89d93dd878 | ||
|
|
66853dfc52 | ||
|
|
c72f66d64b | ||
|
|
59bc342a40 | ||
|
|
e11579df10 | ||
|
|
6a8f6fb4b5 | ||
|
|
8f20bd3840 | ||
|
|
f1abb745fe | ||
|
|
cb2990f6e8 | ||
|
|
fb2f850311 | ||
|
|
2b9c0f09ee | ||
|
|
efe3eb4ce7 | ||
|
|
a1c1a79976 | ||
|
|
90ba355d16 | ||
|
|
01179adfa8 | ||
|
|
e4be403bef | ||
|
|
e1cdf4da0f | ||
|
|
5148cb3b8b | ||
|
|
56c6a9f8fe | ||
|
|
be257b0532 | ||
|
|
0534bc38b2 | ||
|
|
604e2481a6 | ||
|
|
4f557043a5 | ||
|
|
03d609e4e1 | ||
|
|
db6fc65876 | ||
|
|
c6a05f7b35 | ||
|
|
9e4aa32120 | ||
|
|
759995972d | ||
|
|
03401488f6 | ||
|
|
1e790be70c | ||
|
|
4410637f8b | ||
|
|
3947152336 | ||
|
|
af8d2c74f6 | ||
|
|
e107f8d476 | ||
|
|
b427ff1f88 | ||
|
|
e513db62b0 | ||
|
|
2f33ee02d9 | ||
|
|
59490dcac0 | ||
|
|
5afa93a8f1 | ||
|
|
c8e9ed8440 | ||
|
|
8363dfe257 | ||
|
|
080bbc18eb | ||
|
|
1a0edc8bfe | ||
|
|
e8d1d524b9 | ||
|
|
edada22ac0 | ||
|
|
76fb0cfdbb | ||
|
|
5df2553774 | ||
|
|
31812430f1 | ||
|
|
d668b03175 | ||
|
|
663a107c06 | ||
|
|
806184e98b | ||
|
|
08ee82d7b0 | ||
|
|
bcc19167d4 | ||
|
|
858f65ee5a | ||
|
|
43566bbcfd | ||
|
|
ec8cca1245 | ||
|
|
4a65de99a8 | ||
|
|
7461344004 | ||
|
|
b815c6fd69 | ||
|
|
28c9a2e9d0 | ||
|
|
9e0bdd964c | ||
|
|
077641beaa | ||
|
|
ef483403da | ||
|
|
0a8aa2b215 | ||
|
|
5a984f5c0c | ||
|
|
d60688c66f | ||
|
|
23482da259 | ||
|
|
62776229cb | ||
|
|
36fab0cd50 | ||
|
|
8f03662982 | ||
|
|
aad44031c4 | ||
|
|
51813e6030 | ||
|
|
f661907268 | ||
|
|
be85633c32 | ||
|
|
392946fe33 | ||
|
|
671024965f | ||
|
|
8d9aef3cd5 | ||
|
|
b5b4f0453a | ||
|
|
8ca6ac2752 | ||
|
|
27f7e08e18 | ||
|
|
f3e08dc9ea | ||
|
|
e3797ea96b | ||
|
|
146e7781be | ||
|
|
d2e2086540 | ||
|
|
d105f866ff | ||
|
|
1c001ed9df | ||
|
|
366c89164f | ||
|
|
36f13c61bb | ||
|
|
c8935102c3 | ||
|
|
a9e4f82e30 | ||
|
|
f966ca8b83 | ||
|
|
2da7ea56d5 | ||
|
|
232f720e77 | ||
|
|
2f476603d3 | ||
|
|
366fede517 | ||
|
|
7ef8354eb0 | ||
|
|
fbb07011f1 | ||
|
|
a7da8ffb90 | ||
|
|
95fe294f7d | ||
|
|
cdb3ffe439 | ||
|
|
7707fc6f36 | ||
|
|
765328affb | ||
|
|
3c515b0258 | ||
|
|
c6f65ba69f | ||
|
|
8c9a2b022b | ||
|
|
2e8248cd5b | ||
|
|
2b91d99ec6 | ||
|
|
f7688a942a | ||
|
|
574056a7e3 | ||
|
|
84e8dc0e06 | ||
|
|
fb8ce6c878 | ||
|
|
d961c11eb7 | ||
|
|
90f8e82f14 | ||
|
|
14bb66d12f | ||
|
|
7093985b57 | ||
|
|
a557684542 | ||
|
|
b0876331e6 | ||
|
|
cba7338d8d | ||
|
|
f72d9aee80 | ||
|
|
480fb4818c | ||
|
|
78a3c8a8e4 | ||
|
|
9cb7cc84ee | ||
|
|
2f24a1db41 | ||
|
|
4a2cc70b52 | ||
|
|
3021672de5 | ||
|
|
5d2df3550b | ||
|
|
c0c6e21a16 | ||
|
|
8c03c5e82e | ||
|
|
dfd2f3962c | ||
|
|
d315710310 | ||
|
|
3424cc4e51 | ||
|
|
361931ed96 | ||
|
|
e4f6994dfc | ||
|
|
827a27911c | ||
|
|
1e39d0b186 | ||
|
|
fd223c7542 | ||
|
|
40aa937f54 | ||
|
|
47ab6b8a92 | ||
|
|
7420abf175 | ||
|
|
e9a8194cf8 | ||
|
|
9006049d33 | ||
|
|
39381a17de | ||
|
|
9460549eff | ||
|
|
5ea82645ef | ||
|
|
597abc5b06 | ||
|
|
350265e31f | ||
|
|
5680a306ff | ||
|
|
16cb09bda5 | ||
|
|
9a3c40f6a6 | ||
|
|
821e4a225a | ||
|
|
939c99b0cf | ||
|
|
79b9c7011d | ||
|
|
e7ff7402b4 | ||
|
|
91f6369ba9 | ||
|
|
17ef5cb9a5 | ||
|
|
e8109f1b78 | ||
|
|
f3840d56af | ||
|
|
4a5e0b8d81 | ||
|
|
4ef29f027e | ||
|
|
d4d2efe925 | ||
|
|
1078731f2d | ||
|
|
1739afae24 | ||
|
|
9f0c29c009 | ||
|
|
6220d02f32 | ||
|
|
c166b12515 | ||
|
|
189c870630 | ||
|
|
cdead9ba8a | ||
|
|
21616f4d42 | ||
|
|
0a348278ca | ||
|
|
98d0c9a4f6 | ||
|
|
34a3739545 | ||
|
|
7bb34b8788 | ||
|
|
f6dc432419 | ||
|
|
9b2ee628aa | ||
|
|
357ad26a0e | ||
|
|
a3e705373c | ||
|
|
71ad13256e | ||
|
|
68929631f2 | ||
|
|
9c04065c33 | ||
|
|
09db57db8f | ||
|
|
f9b7e64d53 | ||
|
|
50262f2acc | ||
|
|
a4d99b54af | ||
|
|
485aa0f52b | ||
|
|
f8b732c9b8 | ||
|
|
ac72f77a74 | ||
|
|
626d48d151 | ||
|
|
07511281b8 | ||
|
|
7c11c9c91a | ||
|
|
2cabe4c416 | ||
|
|
dc88a037eb | ||
|
|
2fe8531e51 | ||
|
|
fddd2651fc | ||
|
|
deb0781871 | ||
|
|
8114b04ab6 | ||
|
|
767560804d | ||
|
|
8074b93992 | ||
|
|
588dd41244 | ||
|
|
61b0147a7c | ||
|
|
0d388a396c | ||
|
|
135c79d2ad | ||
|
|
9925b042d8 | ||
|
|
1d16d514c7 | ||
|
|
bda547198e | ||
|
|
5f1b78ec84 | ||
|
|
b7e9a85be0 | ||
|
|
080c1cee4f | ||
|
|
baebede816 | ||
|
|
f455251645 | ||
|
|
8d06f7cf02 | ||
|
|
4af2eaa6a3 | ||
|
|
f5b8879b87 | ||
|
|
7501fee448 | ||
|
|
b7b5090673 | ||
|
|
4f94a0f08a | ||
|
|
2281c8ac39 | ||
|
|
2cc152d0ab | ||
|
|
7b86bb262c | ||
|
|
ed2a4251f1 | ||
|
|
847811a52c | ||
|
|
d25d5b734c | ||
|
|
bc4792b7fd | ||
|
|
7850cbc4bf | ||
|
|
97fa648b2f | ||
|
|
c5cf867cd9 | ||
|
|
03ea9bb760 | ||
|
|
a1a5bf921e | ||
|
|
3e1a7a0dc5 | ||
|
|
2c21387ad9 | ||
|
|
5e8e4fa4a1 | ||
|
|
a41107d021 | ||
|
|
281523ee06 | ||
|
|
2504510c61 | ||
|
|
7153fc8bb5 | ||
|
|
3af094d788 | ||
|
|
785ea71a20 | ||
|
|
05d2f77c0c | ||
|
|
e22366e524 | ||
|
|
2b51c47846 | ||
|
|
dd6af9b8e0 | ||
|
|
c66b17583f | ||
|
|
3ce3520c45 | ||
|
|
8d1e7f4331 | ||
|
|
f0b04afa11 | ||
|
|
f1bfd13da3 | ||
|
|
161cd84150 | ||
|
|
da39593c15 | ||
|
|
571f36e405 | ||
|
|
a4b1200475 | ||
|
|
43807dcba9 | ||
|
|
99a72451d9 | ||
|
|
b8900999a4 | ||
|
|
e6f77376b9 | ||
|
|
b2a6a20f10 | ||
|
|
265b52dccb | ||
|
|
0c112e1db1 | ||
|
|
8eef7db1c6 | ||
|
|
05cbf99237 | ||
|
|
651a7cf83e | ||
|
|
ee27237083 | ||
|
|
72306e91a2 | ||
|
|
75d272be14 | ||
|
|
a8a209f0b0 | ||
|
|
1b7b6196c5 | ||
|
|
ed7937a026 | ||
|
|
f2de4692ea | ||
|
|
16b046bd44 | ||
|
|
7129e2cc9d | ||
|
|
01432fa778 | ||
|
|
9731d28ec3 | ||
|
|
99fbb31554 | ||
|
|
18d258aaa2 | ||
|
|
1af6dd9cf8 | ||
|
|
0da183f084 | ||
|
|
205726b045 | ||
|
|
9cd5237bb8 | ||
|
|
964e94b3ba | ||
|
|
9f54f40f5a | ||
|
|
7047d37f70 | ||
|
|
5b1d45a8fe | ||
|
|
a319957f3e | ||
|
|
816166a30a | ||
|
|
5dd2ea776a | ||
|
|
3b94c7bb43 | ||
|
|
f0198616ad | ||
|
|
267fd403da | ||
|
|
0a8bb7eae5 | ||
|
|
409048c206 | ||
|
|
f84bd6a1e8 | ||
|
|
d5c0e62be1 | ||
|
|
40c4344f73 | ||
|
|
3bd8aca2d2 | ||
|
|
a21bdedbc1 | ||
|
|
797ebd7771 | ||
|
|
04e9ecbc76 | ||
|
|
41d37579dc | ||
|
|
10d23828a7 | ||
|
|
19e3392825 | ||
|
|
6bf4846ae8 | ||
|
|
afcd37dac6 | ||
|
|
c2ff497cc9 | ||
|
|
decd2c2ded | ||
|
|
02d1c9ce98 | ||
|
|
5c9083a5df | ||
|
|
3c7fafa91f | ||
|
|
fd50f8fcab | ||
|
|
1a93df5886 | ||
|
|
bdc086c285 | ||
|
|
82042e0b99 | ||
|
|
c807b30c8f | ||
|
|
72dc76ec74 | ||
|
|
71619042fd | ||
|
|
429a77de8e | ||
|
|
b1f72620dc | ||
|
|
2a54aed135 | ||
|
|
040c1f6f78 | ||
|
|
07bce90521 | ||
|
|
508b093278 | ||
|
|
9bed5bf872 | ||
|
|
6d0a2cd301 | ||
|
|
e1ee08361d | ||
|
|
3332ce34c5 | ||
|
|
2c57e439d5 | ||
|
|
73e2660e59 | ||
|
|
9120bbea34 | ||
|
|
58ea9750d7 | ||
|
|
a59ad97e5e | ||
|
|
0a7b28caf5 | ||
|
|
eaf191e350 | ||
|
|
ecb89f80a0 | ||
|
|
9626b65593 | ||
|
|
c9b5516330 | ||
|
|
4363ca88aa | ||
|
|
3353060ad4 | ||
|
|
ddc3b8575e | ||
|
|
136a2ec89f | ||
|
|
021c68f2a7 | ||
|
|
989a09274f | ||
|
|
39c5886d7a | ||
|
|
1a5f3735cf | ||
|
|
4d47eb0e91 | ||
|
|
af7c59b5c2 | ||
|
|
693bf68864 | ||
|
|
c9ddf3d165 | ||
|
|
1549b56866 | ||
|
|
2cd1f22e68 | ||
|
|
688f38943d | ||
|
|
043bbd7a11 | ||
|
|
f997423fd7 | ||
|
|
1871ef3d38 | ||
|
|
7c56c88dd4 | ||
|
|
4d7422dd90 | ||
|
|
eccabc0588 | ||
|
|
0c7b188587 | ||
|
|
4c97b79adf | ||
|
|
8ae9573b07 | ||
|
|
43fce6e739 | ||
|
|
78900772bb | ||
|
|
c16a0444ca | ||
|
|
0d518166ee | ||
|
|
6ae391a3c9 | ||
|
|
357897a0cd | ||
|
|
10a0a8fe09 | ||
|
|
98443be80c | ||
|
|
bf7f6e99c5 | ||
|
|
b6e468e54e | ||
|
|
dfc634a362 | ||
|
|
d9b6b82f07 | ||
|
|
4ad6257dab | ||
|
|
e3e3f1dfdc | ||
|
|
60f83bb7bf | ||
|
|
bbc10cb105 | ||
|
|
83ea19dd92 | ||
|
|
7ec42dce4d | ||
|
|
a9da7ce6fc | ||
|
|
1586610a44 | ||
|
|
254224c0e8 | ||
|
|
9b66772a12 | ||
|
|
322878b0b7 | ||
|
|
9e181d25ce | ||
|
|
4c311fd78e | ||
|
|
9936b3af5b | ||
|
|
648fd23a57 | ||
|
|
7dd00d2424 | ||
|
|
9e83fe7329 | ||
|
|
166c9c75e9 | ||
|
|
b9882f8985 | ||
|
|
37a166731d | ||
|
|
66db583432 | ||
|
|
f7eb80a6ea | ||
|
|
79f40f3d22 | ||
|
|
ed3b26653c | ||
|
|
2bb13129de | ||
|
|
fc29e8f9fa | ||
|
|
495c2c7390 | ||
|
|
b984386bab | ||
|
|
3781bb93e1 | ||
|
|
3a4dc3f876 | ||
|
|
2c43f1412e | ||
|
|
5d3a93f103 | ||
|
|
5faba1b5a9 | ||
|
|
4e7bd3579b | ||
|
|
49da8a31d2 | ||
|
|
dd2b8f600d | ||
|
|
8b1a3a31ff | ||
|
|
d429374924 | ||
|
|
dd0bbdc7b4 | ||
|
|
64e85c3076 | ||
|
|
68771ce399 | ||
|
|
bcc7faa8e5 | ||
|
|
fb0dc7dea0 | ||
|
|
0fad7b3411 | ||
|
|
1adba05065 | ||
|
|
fe7740f1b0 | ||
|
|
b253dce7e1 | ||
|
|
589b3a7a13 | ||
|
|
26d259b952 | ||
|
|
04e118c081 | ||
|
|
2af2346e35 | ||
|
|
7cd44b5ad3 | ||
|
|
81d96394b9 | ||
|
|
76fe5345d8 | ||
|
|
ef277ef57f | ||
|
|
9a12dab600 | ||
|
|
51f6391ded | ||
|
|
e10e6cfe4d | ||
|
|
d887a37f60 | ||
|
|
1abd1e257f | ||
|
|
137b0820b0 | ||
|
|
3f85d7f813 | ||
|
|
6b6dae129f | ||
|
|
2c3672a7ea | ||
|
|
645a58464c | ||
|
|
fcbb51dce7 | ||
|
|
c7c6a097f0 | ||
|
|
0ce7f29976 | ||
|
|
f2df756c17 | ||
|
|
28b5d44e11 | ||
|
|
e7bb6bc798 | ||
|
|
c572382f6a | ||
|
|
e28c4a1b4d | ||
|
|
f5708fd539 | ||
|
|
5769abb626 | ||
|
|
4ebe0abba0 | ||
|
|
8109c9ac4f | ||
|
|
2ce1ceb460 | ||
|
|
9d701ad671 | ||
|
|
4aee44fe11 | ||
|
|
adb41a80c5 | ||
|
|
642e6ebdc8 | ||
|
|
74828943a6 | ||
|
|
f906e04581 | ||
|
|
b3c47e759f | ||
|
|
8bbb5d2e09 | ||
|
|
7fe03be73f | ||
|
|
abb0124011 | ||
|
|
a98b2bb71a | ||
|
|
bc1702e6cf | ||
|
|
577a5366e8 | ||
|
|
7fedd5729e | ||
|
|
35c0463829 | ||
|
|
1b40f81fcc | ||
|
|
afefd925ea | ||
|
|
0850562bf9 | ||
|
|
bc2335a54e | ||
|
|
5a9fc3ad18 | ||
|
|
29f85db022 | ||
|
|
6034908a95 | ||
|
|
ef3dbc217b | ||
|
|
01357617ae | ||
|
|
4775f4ea31 | ||
|
|
ae7b27e1c9 | ||
|
|
70c8c4b4aa | ||
|
|
b7802f4e3e | ||
|
|
6f35a6f5e9 | ||
|
|
5e2ce9e1e6 | ||
|
|
e04080bf1c | ||
|
|
55134c8426 | ||
|
|
0e886f5ddf | ||
|
|
1e97d1230a | ||
|
|
d44ce0ee6f | ||
|
|
c30d3f585f | ||
|
|
112859caa5 | ||
|
|
6b669fc540 | ||
|
|
cb9b7d55fd | ||
|
|
c506db1ef4 | ||
|
|
65afc73f25 | ||
|
|
7e60d1803c | ||
|
|
3ecc0f95bf | ||
|
|
c1db404c0d | ||
|
|
b38bff41d8 | ||
|
|
6e30d39b78 | ||
|
|
753e193d62 | ||
|
|
4819972399 | ||
|
|
ba8705fb84 | ||
|
|
9f71fc2dd5 | ||
|
|
a587ada170 | ||
|
|
320e29ba84 | ||
|
|
cd74b76483 | ||
|
|
2fe0b888bd | ||
|
|
af14966b09 | ||
|
|
5fa0d47c0d | ||
|
|
659ad29875 | ||
|
|
a0a81240ce | ||
|
|
89f08f0da7 | ||
|
|
85c1a48d3a | ||
|
|
846c1a104e | ||
|
|
4dda54c9e6 | ||
|
|
1ab34ed46f | ||
|
|
e7aaa95ec5 | ||
|
|
1042d12df6 | ||
|
|
751594860a | ||
|
|
84675b5c0f | ||
|
|
e7be27413c | ||
|
|
654194b274 | ||
|
|
36069cbe6d | ||
|
|
34d5edd6b9 | ||
|
|
57a7c04a4c | ||
|
|
87279688e6 | ||
|
|
783b352e3b | ||
|
|
f683ab64ab | ||
|
|
942651dc16 | ||
|
|
2e86f8e6d8 | ||
|
|
c66694aa32 | ||
|
|
f2a9ddd1a6 | ||
|
|
6aefe4d5d9 | ||
|
|
00f60a6e78 | ||
|
|
34858a1ba0 | ||
|
|
4ae3d5344c | ||
|
|
276684f076 | ||
|
|
2baeb6a572 | ||
|
|
adb067a57f | ||
|
|
0995c8b839 | ||
|
|
0aa00ab226 | ||
|
|
c5d96f96e1 | ||
|
|
4d94d12e9c | ||
|
|
d82594bf09 | ||
|
|
2f275ca81e | ||
|
|
59f4eaf3ea | ||
|
|
8a9cb2527e | ||
|
|
e53d6d216d | ||
|
|
ec78a92234 | ||
|
|
f948d05b90 | ||
|
|
48430fd9c3 | ||
|
|
843d7b2231 | ||
|
|
51b8806184 | ||
|
|
68b2d79700 | ||
|
|
17e8532e6f | ||
|
|
be81415a75 | ||
|
|
b6c806a789 | ||
|
|
32871a8a3c | ||
|
|
c6630a9f20 | ||
|
|
2cbee10527 | ||
|
|
aff8a3b401 | ||
|
|
a9f6c4eb20 | ||
|
|
28d4373f67 | ||
|
|
452bb0b0d7 | ||
|
|
eabdd3de00 | ||
|
|
fcfb7a0105 | ||
|
|
5d5c623f09 | ||
|
|
cebc0c5405 | ||
|
|
52d5e2f36d | ||
|
|
ef1863f810 | ||
|
|
cd749ac6a4 | ||
|
|
3f9d73d784 | ||
|
|
58cfba7695 | ||
|
|
d1cb7a5ce4 | ||
|
|
863bb3f474 | ||
|
|
a4f44348ef | ||
|
|
51f9afb471 | ||
|
|
f8bdc7044c | ||
|
|
796a4a693a | ||
|
|
1c1ba1b55e | ||
|
|
3af3a88f66 | ||
|
|
25eeabb9f9 | ||
|
|
fb9de4c4ad | ||
|
|
497879fb4b | ||
|
|
6e9b5cc113 | ||
|
|
edc1ad952d | ||
|
|
4188bbc5bd | ||
|
|
10591452e4 | ||
|
|
c269bd04d3 | ||
|
|
acdb324f7d | ||
|
|
d3842ec3c3 | ||
|
|
e1cac9f92f | ||
|
|
4533cc592f | ||
|
|
23614fe0d0 | ||
|
|
d723403b6b | ||
|
|
f3b21e6bd9 | ||
|
|
6a2638c70c | ||
|
|
b162dcbfbe | ||
|
|
25a2de2a90 | ||
|
|
67b2286df0 | ||
|
|
64728d10ad | ||
|
|
ae69019265 | ||
|
|
c07f2ed722 | ||
|
|
2951304647 | ||
|
|
d936e24692 | ||
|
|
ba26e6a5d6 | ||
|
|
6194bac4c4 | ||
|
|
a1d1325ad6 | ||
|
|
cceebff93a | ||
|
|
f97e3f65fe | ||
|
|
5214ae1760 | ||
|
|
6be3aef367 | ||
|
|
6712e9b109 | ||
|
|
50a0686648 | ||
|
|
d47afa3081 | ||
|
|
1ddfe2fb92 | ||
|
|
3ae3d18566 | ||
|
|
5fdb171d65 | ||
|
|
99e43fe340 | ||
|
|
cf1ecbc826 | ||
|
|
5ff27b9e3d | ||
|
|
291304af75 | ||
|
|
b63ebfcb3b | ||
|
|
c6a9a816f6 | ||
|
|
f5cf716a91 | ||
|
|
6dbee61742 | ||
|
|
d89d97b61f | ||
|
|
01b7ec2a99 | ||
|
|
ddbee9ec19 | ||
|
|
0bbadc6d6d | ||
|
|
64584c73b2 | ||
|
|
8df28628ec | ||
|
|
3bf520541b | ||
|
|
a531896bd6 | ||
|
|
e005b42d18 | ||
|
|
1f6573b6da | ||
|
|
73af381c4c | ||
|
|
625bf4dfdc | ||
|
|
46b4090629 | ||
|
|
91e012987e | ||
|
|
a86d316d07 | ||
|
|
76454df5e6 | ||
|
|
67b6e40f85 | ||
|
|
9889b5a8d3 | ||
|
|
00fc75b61b | ||
|
|
4ee93a1351 | ||
|
|
669d13b89a | ||
|
|
5fa86b5eb7 | ||
|
|
369cdf8c4f | ||
|
|
0397f69853 | ||
|
|
81177926ff | ||
|
|
e5bbb18414 | ||
|
|
cfa74d69ae | ||
|
|
bee26f43d4 | ||
|
|
a3ab32e9ab | ||
|
|
c847fe4747 | ||
|
|
a278711421 | ||
|
|
01ffe0d97c | ||
|
|
bd732dfa0a | ||
|
|
8b8e1773e8 | ||
|
|
b296fb2965 | ||
|
|
53557e38b6 | ||
|
|
c0c61709ca | ||
|
|
56b778f19c | ||
|
|
f4d532598c | ||
|
|
53fa28ae77 | ||
|
|
f38b3abdbc | ||
|
|
99207ae606 | ||
|
|
d3b8cb8cba | ||
|
|
51c6eb4597 | ||
|
|
d47b672aa5 | ||
|
|
64e30f59e8 | ||
|
|
cef7b3d396 | ||
|
|
7184c9cfe9 | ||
|
|
da04a0dff4 | ||
|
|
d91b66ae87 | ||
|
|
5c40f4aa84 | ||
|
|
1797896fa6 | ||
|
|
d1c9e18c97 | ||
|
|
ef83ed0596 | ||
|
|
d89155a6ee | ||
|
|
921ce23dde | ||
|
|
929b7f7059 | ||
|
|
de7805f281 | ||
|
|
03cad9f315 | ||
|
|
aa6fafd52f | ||
|
|
01ff63a007 | ||
|
|
99746bad8e | ||
|
|
21b67e97af | ||
|
|
668639e484 | ||
|
|
e9b2079599 | ||
|
|
5fb7d21c80 | ||
|
|
f5e00a6ef4 | ||
|
|
b06cbc0fee | ||
|
|
abbcbad5e9 | ||
|
|
fab39a461f | ||
|
|
9c3edff92b | ||
|
|
e8f4cd18a4 | ||
|
|
e566fd9b57 | ||
|
|
6211ddcdf0 | ||
|
|
245f073350 | ||
|
|
dd629f516b | ||
|
|
31080edd59 | ||
|
|
b679655cd5 | ||
|
|
ca3b062f89 | ||
|
|
de6c1be51b | ||
|
|
4f09dbf044 | ||
|
|
e6b4630ce9 | ||
|
|
90bababd38 | ||
|
|
90130411f9 | ||
|
|
ae61a2335d | ||
|
|
8329a8ea9c | ||
|
|
ef52ccb929 | ||
|
|
ed9d8aab6f | ||
|
|
aa16287447 | ||
|
|
a7a922308e | ||
|
|
ba13b81b0e | ||
|
|
d172552fb0 | ||
|
|
2a8ab27fc1 | ||
|
|
e8c3e4c75f | ||
|
|
ed887a5cfc | ||
|
|
1bac96dc2a | ||
|
|
c3b779a810 | ||
|
|
44cfd65f6c | ||
|
|
f5a36f94bb | ||
|
|
e951194bee | ||
|
|
478311fe9e | ||
|
|
48dd1397e8 | ||
|
|
ebedbc931f | ||
|
|
9065d990e5 | ||
|
|
b38d7595a7 | ||
|
|
860e914b90 | ||
|
|
ac3af49aa7 | ||
|
|
415f169f48 | ||
|
|
e2b08d8667 | ||
|
|
91e7f4894a | ||
|
|
a78dba5191 | ||
|
|
c7208c90c6 | ||
|
|
da6a2756fa | ||
|
|
9a6a66f5a8 | ||
|
|
90487bfde6 | ||
|
|
4120fd8d1c | ||
|
|
6f3a5ebe6e | ||
|
|
a935f200a3 | ||
|
|
f474ae4f75 | ||
|
|
345a4417a6 | ||
|
|
8cca83723c | ||
|
|
aa2fcd47c2 | ||
|
|
0580a7d3cd | ||
|
|
a43c242c66 | ||
|
|
45d4b92fc6 | ||
|
|
72df9ff3e4 | ||
|
|
48bf31fd0e | ||
|
|
4ee5383f7d | ||
|
|
33fb60a32d | ||
|
|
d10d0e49fa | ||
|
|
dc3575c8fd | ||
|
|
17115cfb0b | ||
|
|
498082f7e5 | ||
|
|
99216ffe59 | ||
|
|
f426dbc9cf | ||
|
|
1c611cc9b9 | ||
|
|
dc43e26770 | ||
|
|
79ae26f1b5 |
75
.env.example
Normal file
75
.env.example
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# docker image tag (latest, nightly)
|
||||||
|
TAG=latest
|
||||||
|
|
||||||
|
# set timezone to get correct log timestamp
|
||||||
|
TZ=ETC/UTC
|
||||||
|
|
||||||
|
# container uid and gid (must match the owner of mounted directories)
|
||||||
|
GODOXY_UID=1000
|
||||||
|
GODOXY_GID=1000
|
||||||
|
|
||||||
|
# Set GODOXY_API_JWT_SECURE=false to allow http
|
||||||
|
GODOXY_API_JWT_SECURE=true
|
||||||
|
# API JWT Configuration (common)
|
||||||
|
# generate secret with `openssl rand -base64 32`
|
||||||
|
GODOXY_API_JWT_SECRET=
|
||||||
|
# the JWT token time-to-live
|
||||||
|
# leave empty to use default (24 hours)
|
||||||
|
# format: https://pkg.go.dev/time#Duration
|
||||||
|
GODOXY_API_JWT_TOKEN_TTL=
|
||||||
|
|
||||||
|
# API/WebUI user password login credentials (optional)
|
||||||
|
# These fields are not required for OIDC authentication
|
||||||
|
GODOXY_API_USER=admin
|
||||||
|
GODOXY_API_PASSWORD=password
|
||||||
|
|
||||||
|
# OIDC Configuration (optional)
|
||||||
|
# Uncomment and configure these values to enable OIDC authentication.
|
||||||
|
#
|
||||||
|
# GODOXY_OIDC_ISSUER_URL=https://accounts.google.com
|
||||||
|
# GODOXY_OIDC_CLIENT_ID=your-client-id
|
||||||
|
# GODOXY_OIDC_CLIENT_SECRET=your-client-secret
|
||||||
|
# GODOXY_OIDC_SCOPES=openid, profile, email, groups # you may also include `offline_access` if your Idp supports it (e.g. Authentik, Pocket ID)
|
||||||
|
#
|
||||||
|
# User definitions: Uncomment and configure these values to restrict access to specific users or groups.
|
||||||
|
# These two fields act as a logical AND operator. For example, given the following membership:
|
||||||
|
# user1, group1
|
||||||
|
# user2, group1
|
||||||
|
# user3, group2
|
||||||
|
# user1, group2
|
||||||
|
# You can allow access to user3 AND all users of group1 by providing:
|
||||||
|
# # GODOXY_OIDC_ALLOWED_USERS=user3
|
||||||
|
# # GODOXY_OIDC_ALLOWED_GROUPS=group1
|
||||||
|
#
|
||||||
|
# Comma-separated list of allowed users.
|
||||||
|
# GODOXY_OIDC_ALLOWED_USERS=user1,user2
|
||||||
|
# Optional: Comma-separated list of allowed groups.
|
||||||
|
# GODOXY_OIDC_ALLOWED_GROUPS=group1,group2
|
||||||
|
|
||||||
|
# Proxy listening address
|
||||||
|
GODOXY_HTTP_ADDR=:80
|
||||||
|
GODOXY_HTTPS_ADDR=:443
|
||||||
|
|
||||||
|
# Enable HTTP3
|
||||||
|
GODOXY_HTTP3_ENABLED=true
|
||||||
|
|
||||||
|
# API listening address
|
||||||
|
GODOXY_API_ADDR=127.0.0.1:8888
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
GODOXY_METRICS_DISABLE_CPU=false
|
||||||
|
GODOXY_METRICS_DISABLE_MEMORY=false
|
||||||
|
GODOXY_METRICS_DISABLE_DISK=false
|
||||||
|
GODOXY_METRICS_DISABLE_NETWORK=false
|
||||||
|
GODOXY_METRICS_DISABLE_SENSORS=false
|
||||||
|
|
||||||
|
# Frontend aliases (subdomains / FQDNs, e.g. godoxy, godoxy.domain.com)
|
||||||
|
GODOXY_FRONTEND_ALIASES=godoxy
|
||||||
|
|
||||||
|
# Docker socket
|
||||||
|
# /var/run/podman/podman.sock for podman
|
||||||
|
DOCKER_SOCKET=/var/run/docker.sock
|
||||||
|
SOCKET_PROXY_LISTEN_ADDR=127.0.0.1:2375
|
||||||
|
|
||||||
|
# Debug mode
|
||||||
|
GODOXY_DEBUG=false
|
||||||
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: yusing # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
polar: # Replace with a single Polar username
|
||||||
|
buy_me_a_coffee: yusingwysq # Replace with a single Buy Me a Coffee username
|
||||||
|
thanks_dev: # Replace with a single thanks.dev username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
50
.github/workflows/agent-binary.yml
vendored
Normal file
50
.github/workflows/agent-binary.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: GoDoxy agent binary
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
paths:
|
||||||
|
- "agent/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- runner: ubuntu-latest
|
||||||
|
platform: linux/amd64
|
||||||
|
binary_name: godoxy-agent-linux-amd64
|
||||||
|
- runner: ubuntu-24.04-arm
|
||||||
|
platform: linux/arm64
|
||||||
|
binary_name: godoxy-agent-linux-arm64
|
||||||
|
name: Build ${{ matrix.platform }}
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
- name: Verify dependencies
|
||||||
|
run: go mod verify
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
make agent=1 NAME=${{ matrix.binary_name }} build
|
||||||
|
- name: Check binary
|
||||||
|
run: |
|
||||||
|
file bin/${{ matrix.binary_name }}
|
||||||
|
- name: Upload
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.binary_name }}
|
||||||
|
path: bin/${{ matrix.binary_name }}
|
||||||
|
- name: Upload to release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
with:
|
||||||
|
files: bin/${{ matrix.binary_name }}
|
||||||
24
.github/workflows/docker-image-nightly.yml
vendored
Normal file
24
.github/workflows/docker-image-nightly.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: Docker Image CI (nightly)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "*" # matches every branch that doesn't contain a '/'
|
||||||
|
- "*/*" # matches every branch containing a single '/'
|
||||||
|
- "**" # matches every branch
|
||||||
|
- "!dependabot/*"
|
||||||
|
- "!main" # excludes main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-nightly:
|
||||||
|
uses: ./.github/workflows/docker-image.yml
|
||||||
|
with:
|
||||||
|
image_name: ${{ github.repository_owner }}/godoxy
|
||||||
|
tag: nightly
|
||||||
|
target: main
|
||||||
|
build-nightly-agent:
|
||||||
|
uses: ./.github/workflows/docker-image.yml
|
||||||
|
with:
|
||||||
|
image_name: ${{ github.repository_owner }}/godoxy-agent
|
||||||
|
tag: nightly
|
||||||
|
target: agent
|
||||||
20
.github/workflows/docker-image-prod.yml
vendored
Normal file
20
.github/workflows/docker-image-prod.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: Docker Image CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-prod:
|
||||||
|
uses: ./.github/workflows/docker-image.yml
|
||||||
|
with:
|
||||||
|
image_name: ${{ github.repository_owner }}/godoxy
|
||||||
|
tag: latest
|
||||||
|
target: main
|
||||||
|
build-prod-agent:
|
||||||
|
uses: ./.github/workflows/docker-image.yml
|
||||||
|
with:
|
||||||
|
image_name: ${{ github.repository_owner }}/godoxy-agent
|
||||||
|
tag: latest
|
||||||
|
target: agent
|
||||||
22
.github/workflows/docker-image-socket-proxy.yml
vendored
Normal file
22
.github/workflows/docker-image-socket-proxy.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Docker Image CI (socket-proxy)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "socket-proxy/**"
|
||||||
|
- "socket-proxy.Dockerfile"
|
||||||
|
- ".github/workflows/docker-image-socket-proxy.yml"
|
||||||
|
tags-ignore:
|
||||||
|
- "**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
uses: ./.github/workflows/docker-image.yml
|
||||||
|
with:
|
||||||
|
image_name: ${{ github.repository_owner }}/socket-proxy
|
||||||
|
tag: latest
|
||||||
|
target: socket-proxy
|
||||||
|
dockerfile: socket-proxy.Dockerfile
|
||||||
169
.github/workflows/docker-image.yml
vendored
169
.github/workflows/docker-image.yml
vendored
@@ -1,21 +1,158 @@
|
|||||||
name: Docker Image CI
|
name: Docker Image CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_call:
|
||||||
tags:
|
inputs:
|
||||||
- "*"
|
tag:
|
||||||
jobs:
|
required: true
|
||||||
build_and_push:
|
type: string
|
||||||
runs-on: ubuntu-latest
|
image_name:
|
||||||
steps:
|
required: true
|
||||||
- name: Set up Docker Build and Push
|
type: string
|
||||||
id: docker_build_push
|
target:
|
||||||
uses: GlueOps/github-actions-build-push-containers@v0.3.7
|
required: true
|
||||||
with:
|
type: string
|
||||||
tags: ${{ github.ref_name }}
|
dockerfile:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: Dockerfile
|
||||||
|
|
||||||
- name: Tag as latest
|
env:
|
||||||
if: startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, '-')
|
REGISTRY: ghcr.io
|
||||||
|
MAKE_ARGS: ${{ inputs.target }}=1
|
||||||
|
DIGEST_PATH: /tmp/digests/${{ inputs.target }}
|
||||||
|
DIGEST_NAME_SUFFIX: ${{ inputs.target }}
|
||||||
|
DOCKERFILE: ${{ inputs.dockerfile }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- runner: ubuntu-latest
|
||||||
|
platform: linux/amd64
|
||||||
|
- runner: ubuntu-24.04-arm
|
||||||
|
platform: linux/arm64
|
||||||
|
|
||||||
|
name: Build ${{ matrix.platform }}
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
id-token: write
|
||||||
|
attestations: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
docker tag ghcr.io/${{ github.repository }}:${{ github.ref_name }} ghcr.io/${{ github.repository }}:latest
|
platform=${{ matrix.platform }}
|
||||||
docker push ghcr.io/${{ github.repository }}:latest
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ inputs.image_name }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ inputs.tag }},event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
|
||||||
|
- name: Login to registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push by digest
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
file: ${{ env.DOCKERFILE }}
|
||||||
|
outputs: type=image,name=${{ env.REGISTRY }}/${{ inputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
||||||
|
cache-from: |
|
||||||
|
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }}
|
||||||
|
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }}
|
||||||
|
cache-to: |
|
||||||
|
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max
|
||||||
|
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }},mode=max
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ github.ref_name }}
|
||||||
|
MAKE_ARGS=${{ env.MAKE_ARGS }}
|
||||||
|
|
||||||
|
- name: Generate artifact attestation
|
||||||
|
uses: actions/attest-build-provenance@v1
|
||||||
|
with:
|
||||||
|
subject-name: ${{ env.REGISTRY }}/${{ inputs.image_name }}
|
||||||
|
subject-digest: ${{ steps.build.outputs.digest }}
|
||||||
|
push-to-registry: true
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p ${{ env.DIGEST_PATH }}
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "${{ env.DIGEST_PATH }}/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digests-${{ env.PLATFORM_PAIR }}-${{ env.DIGEST_NAME_SUFFIX }}
|
||||||
|
path: ${{ env.DIGEST_PATH }}/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
merge:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: ${{ env.DIGEST_PATH }}
|
||||||
|
pattern: digests-*-${{ env.DIGEST_NAME_SUFFIX }}
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ inputs.image_name }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ inputs.tag }},event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
|
||||||
|
- name: Login to registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Create manifest list and push
|
||||||
|
id: push
|
||||||
|
working-directory: ${{ env.DIGEST_PATH }}
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
|
$(printf '${{ env.REGISTRY }}/${{ inputs.image_name }}@sha256:%s ' *)
|
||||||
|
|
||||||
|
- name: Inspect image
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ inputs.image_name }}:${{ steps.meta.outputs.version }}
|
||||||
|
|||||||
39
.github/workflows/merge-main-into-compat.yml
vendored
Normal file
39
.github/workflows/merge-main-into-compat.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Cherry-pick into Compat
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/merge-main-into-compat.yml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cherry-pick:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Configure git user
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
- name: Cherry-pick commits from last tag
|
||||||
|
run: |
|
||||||
|
git fetch origin compat
|
||||||
|
git checkout compat
|
||||||
|
CURRENT_TAG=${{ github.ref_name }}
|
||||||
|
PREV_TAG=$(git describe --tags --abbrev=0 $CURRENT_TAG^ 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ -z "$PREV_TAG" ]; then
|
||||||
|
echo "No previous tag found. Cherry-picking all commits up to $CURRENT_TAG"
|
||||||
|
git rev-list --reverse --no-merges $CURRENT_TAG | xargs -r git cherry-pick
|
||||||
|
else
|
||||||
|
echo "Cherry-picking commits from $PREV_TAG to $CURRENT_TAG"
|
||||||
|
git rev-list --reverse --no-merges $PREV_TAG..$CURRENT_TAG | xargs -r git cherry-pick
|
||||||
|
fi
|
||||||
|
- name: Push compat
|
||||||
|
run: |
|
||||||
|
git push origin compat
|
||||||
31
.gitignore
vendored
31
.gitignore
vendored
@@ -1,10 +1,17 @@
|
|||||||
compose.yml
|
compose.yml
|
||||||
|
*.compose.yml
|
||||||
|
|
||||||
|
config
|
||||||
|
certs
|
||||||
config*/
|
config*/
|
||||||
|
!schemas/**
|
||||||
certs*/
|
certs*/
|
||||||
bin/
|
bin/
|
||||||
|
error_pages/
|
||||||
templates/codemirror/
|
!examples/error_pages/
|
||||||
|
profiles/
|
||||||
|
data/
|
||||||
|
debug/
|
||||||
|
|
||||||
logs/
|
logs/
|
||||||
log/
|
log/
|
||||||
@@ -13,8 +20,24 @@ log/
|
|||||||
|
|
||||||
go.work.sum
|
go.work.sum
|
||||||
|
|
||||||
!src/**/
|
!cmd/**/
|
||||||
|
!internal/**/
|
||||||
|
|
||||||
todo.md
|
todo.md
|
||||||
|
|
||||||
.*.swp
|
.*.swp
|
||||||
|
.aider*
|
||||||
|
mtrace.json
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
.cursorrules
|
||||||
|
.cursor/
|
||||||
|
.windsurfrules
|
||||||
|
test.Dockerfile
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
|
||||||
|
!agent.compose.yml
|
||||||
|
!agent/pkg/**
|
||||||
|
dev-data/
|
||||||
@@ -11,5 +11,5 @@ build-image:
|
|||||||
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin
|
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin
|
||||||
script:
|
script:
|
||||||
- echo building $CI_REGISTRY_IMAGE
|
- echo building $CI_REGISTRY_IMAGE
|
||||||
- docker build --pull -t $CI_REGISTRY_IMAGE .
|
- docker build --no-cache --build-arg VERSION=$CI_COMMIT_REF_NAME -t $CI_REGISTRY_IMAGE .
|
||||||
- docker push $CI_REGISTRY_IMAGE
|
- docker push $CI_REGISTRY_IMAGE
|
||||||
|
|||||||
12
.gitmodules
vendored
12
.gitmodules
vendored
@@ -1,3 +1,9 @@
|
|||||||
[submodule "frontend"]
|
[submodule "internal/gopsutil"]
|
||||||
path = frontend
|
path = internal/gopsutil
|
||||||
url = https://github.com/yusing/go-proxy-frontend
|
url = https://github.com/godoxy-app/gopsutil.git
|
||||||
|
[submodule "internal/go-oidc"]
|
||||||
|
path = internal/go-oidc
|
||||||
|
url = https://github.com/godoxy-app/go-oidc.git
|
||||||
|
[submodule "goutils"]
|
||||||
|
path = goutils
|
||||||
|
url = https://github.com/yusing/goutils.git
|
||||||
|
|||||||
151
.golangci.yml
Normal file
151
.golangci.yml
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
version: "2"
|
||||||
|
linters:
|
||||||
|
default: all
|
||||||
|
disable:
|
||||||
|
# - bodyclose
|
||||||
|
- containedctx
|
||||||
|
# - contextcheck
|
||||||
|
- cyclop
|
||||||
|
- depguard
|
||||||
|
# - dupl
|
||||||
|
- err113
|
||||||
|
- exhaustive
|
||||||
|
- exhaustruct
|
||||||
|
- funcorder
|
||||||
|
- forcetypeassert
|
||||||
|
- gochecknoglobals
|
||||||
|
- gochecknoinits
|
||||||
|
- gocognit
|
||||||
|
- goconst
|
||||||
|
- gocyclo
|
||||||
|
- godot
|
||||||
|
- gomoddirectives
|
||||||
|
- gosmopolitan
|
||||||
|
- ireturn
|
||||||
|
- lll
|
||||||
|
- maintidx
|
||||||
|
- makezero
|
||||||
|
- mnd
|
||||||
|
- nakedret
|
||||||
|
- nestif
|
||||||
|
- nlreturn
|
||||||
|
- nonamedreturns
|
||||||
|
- noinlineerr
|
||||||
|
- paralleltest
|
||||||
|
- revive
|
||||||
|
- rowserrcheck
|
||||||
|
- sqlclosecheck
|
||||||
|
- tagalign
|
||||||
|
- tagliatelle
|
||||||
|
- testpackage
|
||||||
|
- tparallel
|
||||||
|
- varnamelen
|
||||||
|
- wrapcheck
|
||||||
|
- wsl
|
||||||
|
- wsl_v5
|
||||||
|
settings:
|
||||||
|
errcheck:
|
||||||
|
exclude-functions:
|
||||||
|
- fmt.Fprintln
|
||||||
|
forbidigo:
|
||||||
|
forbid:
|
||||||
|
- pattern: ^print(ln)?$
|
||||||
|
funlen:
|
||||||
|
lines: -1
|
||||||
|
statements: 120
|
||||||
|
gocyclo:
|
||||||
|
min-complexity: 14
|
||||||
|
godox:
|
||||||
|
keywords:
|
||||||
|
- FIXME
|
||||||
|
gomoddirectives:
|
||||||
|
replace-allow-list:
|
||||||
|
- github.com/abbot/go-http-auth
|
||||||
|
- github.com/gorilla/mux
|
||||||
|
- github.com/mailgun/minheap
|
||||||
|
- github.com/mailgun/multibuf
|
||||||
|
- github.com/jaguilar/vt100
|
||||||
|
- github.com/cucumber/godog
|
||||||
|
- github.com/http-wasm/http-wasm-host-go
|
||||||
|
govet:
|
||||||
|
disable:
|
||||||
|
- shadow
|
||||||
|
enable-all: true
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
revive:
|
||||||
|
rules:
|
||||||
|
- name: struct-tag
|
||||||
|
- name: blank-imports
|
||||||
|
- name: context-as-argument
|
||||||
|
- name: context-keys-type
|
||||||
|
- name: error-return
|
||||||
|
- name: error-strings
|
||||||
|
- name: error-naming
|
||||||
|
- name: exported
|
||||||
|
disabled: true
|
||||||
|
- name: if-return
|
||||||
|
- name: increment-decrement
|
||||||
|
- name: var-naming
|
||||||
|
- name: var-declaration
|
||||||
|
- name: package-comments
|
||||||
|
disabled: true
|
||||||
|
- name: range
|
||||||
|
- name: receiver-naming
|
||||||
|
- name: time-naming
|
||||||
|
- name: unexported-return
|
||||||
|
- name: indent-error-flow
|
||||||
|
- name: errorf
|
||||||
|
- name: empty-block
|
||||||
|
- name: superfluous-else
|
||||||
|
- name: unused-parameter
|
||||||
|
disabled: true
|
||||||
|
- name: unreachable-code
|
||||||
|
- name: redefines-builtin-id
|
||||||
|
staticcheck:
|
||||||
|
checks:
|
||||||
|
- all
|
||||||
|
- -SA1019
|
||||||
|
dot-import-whitelist:
|
||||||
|
- github.com/yusing/godoxy/internal/utils/testing
|
||||||
|
tagalign:
|
||||||
|
align: false
|
||||||
|
sort: true
|
||||||
|
order:
|
||||||
|
- description
|
||||||
|
- json
|
||||||
|
- toml
|
||||||
|
- yaml
|
||||||
|
- yml
|
||||||
|
- label
|
||||||
|
- label-slice-as-struct
|
||||||
|
- file
|
||||||
|
- kv
|
||||||
|
- export
|
||||||
|
testifylint:
|
||||||
|
disable:
|
||||||
|
- suite-dont-use-pkg
|
||||||
|
- require-error
|
||||||
|
- go-require
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofmt
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
9
.trunk/.gitignore
vendored
Normal file
9
.trunk/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
*out
|
||||||
|
*logs
|
||||||
|
*actions
|
||||||
|
*notifications
|
||||||
|
*tools
|
||||||
|
plugins
|
||||||
|
user_trunk.yaml
|
||||||
|
user.yaml
|
||||||
|
tmp
|
||||||
42
.trunk/trunk.yaml
Normal file
42
.trunk/trunk.yaml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# This file controls the behavior of Trunk: https://docs.trunk.io/cli
|
||||||
|
# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
|
||||||
|
version: 0.1
|
||||||
|
cli:
|
||||||
|
version: 1.25.0
|
||||||
|
# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
|
||||||
|
plugins:
|
||||||
|
sources:
|
||||||
|
- id: trunk
|
||||||
|
ref: v1.7.2
|
||||||
|
uri: https://github.com/trunk-io/plugins
|
||||||
|
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
|
||||||
|
runtimes:
|
||||||
|
enabled:
|
||||||
|
- node@22.16.0
|
||||||
|
- python@3.10.8
|
||||||
|
- go@1.24.3
|
||||||
|
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
|
||||||
|
lint:
|
||||||
|
disabled:
|
||||||
|
- markdownlint
|
||||||
|
- yamllint
|
||||||
|
enabled:
|
||||||
|
- checkov@3.2.471
|
||||||
|
- golangci-lint2@2.5.0
|
||||||
|
- hadolint@2.14.0
|
||||||
|
- actionlint@1.7.7
|
||||||
|
- git-diff-check
|
||||||
|
- gofmt@1.20.4
|
||||||
|
- osv-scanner@2.2.2
|
||||||
|
- oxipng@9.1.5
|
||||||
|
- prettier@3.6.2
|
||||||
|
- shellcheck@0.11.0
|
||||||
|
- shfmt@3.6.0
|
||||||
|
- trufflehog@3.90.8
|
||||||
|
actions:
|
||||||
|
disabled:
|
||||||
|
- trunk-announce
|
||||||
|
- trunk-check-pre-push
|
||||||
|
- trunk-fmt-pre-commit
|
||||||
|
enabled:
|
||||||
|
- trunk-upgrade-available
|
||||||
10
.vscode/settings.example.json
vendored
10
.vscode/settings.example.json
vendored
@@ -1,13 +1,11 @@
|
|||||||
{
|
{
|
||||||
"yaml.schemas": {
|
"yaml.schemas": {
|
||||||
"https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
|
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/types/godoxy/config.schema.json": [
|
||||||
"config.example.yml",
|
"config.example.yml",
|
||||||
"config.yml"
|
"config.yml"
|
||||||
],
|
],
|
||||||
"https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
|
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/types/godoxy/routes.schema.json": [
|
||||||
"providers.example.yml",
|
"providers.example.yml"
|
||||||
"*.providers.yml",
|
|
||||||
"providers.yml"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
74
Dockerfile
74
Dockerfile
@@ -1,30 +1,70 @@
|
|||||||
FROM golang:1.23.1-alpine AS builder
|
# Stage 1: deps
|
||||||
RUN apk add --no-cache tzdata
|
FROM golang:1.25.6-alpine AS deps
|
||||||
COPY src /src
|
HEALTHCHECK NONE
|
||||||
ENV GOCACHE=/root/.cache/go-build
|
|
||||||
WORKDIR /src
|
|
||||||
RUN --mount=type=cache,target="/go/pkg/mod" \
|
|
||||||
--mount=type=cache,target="/root/.cache/go-build" \
|
|
||||||
go mod download && \
|
|
||||||
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o go-proxy github.com/yusing/go-proxy
|
|
||||||
|
|
||||||
|
# package version does not matter
|
||||||
|
# trunk-ignore(hadolint/DL3018)
|
||||||
|
RUN apk add --no-cache tzdata make libcap-setcap
|
||||||
|
|
||||||
|
ENV GOPATH=/root/go
|
||||||
|
ENV GOCACHE=/root/.cache/go-build
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY goutils/go.mod goutils/go.sum ./goutils/
|
||||||
|
COPY internal/go-oidc/go.mod internal/go-oidc/go.sum ./internal/go-oidc/
|
||||||
|
COPY internal/gopsutil/go.mod internal/gopsutil/go.sum ./internal/gopsutil/
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
|
||||||
|
# remove godoxy stuff from go.mod first
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
--mount=type=cache,target=/root/go/pkg/mod \
|
||||||
|
sed -i '/^module github\.com\/yusing\/godoxy/!{/github\.com\/yusing\/godoxy/d}' go.mod && \
|
||||||
|
sed -i '/^module github\.com\/yusing\/goutils/!{/github\.com\/yusing\/goutils/d}' go.mod && \
|
||||||
|
go mod download -x
|
||||||
|
|
||||||
|
# Stage 2: builder
|
||||||
|
FROM deps AS builder
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
COPY Makefile ./
|
||||||
|
COPY cmd ./cmd
|
||||||
|
COPY internal ./internal
|
||||||
|
COPY pkg ./pkg
|
||||||
|
COPY agent ./agent
|
||||||
|
COPY socket-proxy ./socket-proxy
|
||||||
|
COPY goutils ./goutils
|
||||||
|
|
||||||
|
ARG VERSION
|
||||||
|
ENV VERSION=${VERSION}
|
||||||
|
|
||||||
|
ARG MAKE_ARGS
|
||||||
|
ENV MAKE_ARGS=${MAKE_ARGS}
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
--mount=type=cache,target=/root/go/pkg/mod \
|
||||||
|
make ${MAKE_ARGS} docker=1 build
|
||||||
|
|
||||||
|
# Stage 3: Final image
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
|
||||||
LABEL maintainer="yusing@6uo.me"
|
LABEL maintainer="yusing@6uo.me"
|
||||||
|
LABEL proxy.exclude=1
|
||||||
|
LABEL proxy.#1.healthcheck.disable=true
|
||||||
|
|
||||||
# copy timezone data
|
# copy timezone data
|
||||||
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
||||||
|
|
||||||
# copy binary
|
# copy binary
|
||||||
COPY --from=builder /src/go-proxy /app/
|
COPY --from=builder /app/run /app/run
|
||||||
COPY schema/ /app/schema
|
|
||||||
|
# copy certs
|
||||||
|
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
|
||||||
|
|
||||||
ENV DOCKER_HOST=unix:///var/run/docker.sock
|
ENV DOCKER_HOST=unix:///var/run/docker.sock
|
||||||
ENV GOPROXY_DEBUG=0
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
EXPOSE 8888
|
|
||||||
EXPOSE 443
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
CMD ["/app/go-proxy"]
|
|
||||||
|
CMD ["/app/run"]
|
||||||
11
Jenkinsfile
vendored
Normal file
11
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
node {
|
||||||
|
stage('SCM') {
|
||||||
|
checkout scm
|
||||||
|
}
|
||||||
|
stage('SonarQube Analysis') {
|
||||||
|
def scannerHome = tool 'SonarScanner';
|
||||||
|
withSonarQubeEnv() {
|
||||||
|
sh "${scannerHome}/bin/sonar-scanner"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
LICENSE
26
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2024 [fullname]
|
Copyright (c) 2024 - present Yusing
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -19,3 +19,27 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
internal/net/gphttp/reverseproxy/reverse_proxy_mod.go is copied from et/http/httputil/reverseproxy.go with modifications to adapt to this project.
|
||||||
|
|
||||||
|
Copyright 2011 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
internal/utils/io.go has a modified version of io.Copy with context and HTTP flusher handling.
|
||||||
|
|
||||||
|
Copyright 2009 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
internal/utils/strutils/split_join.go is copied from strings.Split and strings.Join with modifications to adapt to this project.
|
||||||
|
|
||||||
|
Copyright 2009 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.
|
||||||
|
|||||||
193
Makefile
193
Makefile
@@ -1,47 +1,180 @@
|
|||||||
.PHONY: all build up quick-restart restart logs get udp-server
|
shell := /bin/sh
|
||||||
|
export VERSION ?= $(shell git describe --tags --abbrev=0)
|
||||||
|
export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
|
||||||
|
export GOOS = linux
|
||||||
|
|
||||||
all: build quick-restart logs
|
REPO_URL ?= https://github.com/yusing/godoxy
|
||||||
|
|
||||||
setup:
|
WEBUI_DIR ?= ../godoxy-webui
|
||||||
mkdir -p config certs
|
DOCS_DIR ?= ${WEBUI_DIR}/wiki
|
||||||
[ -f config/config.yml ] || cp config.example.yml config/config.yml
|
|
||||||
[ -f config/providers.yml ] || touch config/providers.yml
|
|
||||||
|
|
||||||
build:
|
GO_TAGS = sonic
|
||||||
mkdir -p bin
|
LDFLAGS = -X github.com/yusing/goutils/version.version=${VERSION} -checklinkname=0
|
||||||
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy github.com/yusing/go-proxy
|
|
||||||
|
ifeq ($(agent), 1)
|
||||||
|
NAME = godoxy-agent
|
||||||
|
PWD = ${shell pwd}/agent
|
||||||
|
else ifeq ($(socket-proxy), 1)
|
||||||
|
NAME = godoxy-socket-proxy
|
||||||
|
PWD = ${shell pwd}/socket-proxy
|
||||||
|
else
|
||||||
|
NAME = godoxy
|
||||||
|
PWD = ${shell pwd}
|
||||||
|
endif
|
||||||
|
|
||||||
|
ifeq ($(trace), 1)
|
||||||
|
debug = 1
|
||||||
|
GODOXY_TRACE ?= 1
|
||||||
|
GODEBUG = gctrace=1 inittrace=1 schedtrace=3000
|
||||||
|
endif
|
||||||
|
|
||||||
|
ifeq ($(race), 1)
|
||||||
|
CGO_ENABLED = 1
|
||||||
|
GODOXY_DEBUG = 1
|
||||||
|
GO_TAGS += debug
|
||||||
|
BUILD_FLAGS += -race
|
||||||
|
else ifeq ($(debug), 1)
|
||||||
|
CGO_ENABLED = 1
|
||||||
|
GODOXY_DEBUG = 1
|
||||||
|
GO_TAGS += debug
|
||||||
|
# FIXME: BUILD_FLAGS += -asan -gcflags=all='-N -l'
|
||||||
|
else ifeq ($(pprof), 1)
|
||||||
|
CGO_ENABLED = 0
|
||||||
|
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1
|
||||||
|
GO_TAGS += pprof
|
||||||
|
VERSION := ${VERSION}-pprof
|
||||||
|
else
|
||||||
|
CGO_ENABLED = 0
|
||||||
|
LDFLAGS += -s -w
|
||||||
|
GO_TAGS += production
|
||||||
|
BUILD_FLAGS += -pgo=auto
|
||||||
|
endif
|
||||||
|
|
||||||
|
BUILD_FLAGS += -tags '$(GO_TAGS)' -ldflags='$(LDFLAGS)'
|
||||||
|
BIN_PATH := $(shell pwd)/bin/${NAME}
|
||||||
|
|
||||||
|
export NAME
|
||||||
|
export CGO_ENABLED
|
||||||
|
export GODOXY_DEBUG
|
||||||
|
export GODOXY_TRACE
|
||||||
|
export GODEBUG
|
||||||
|
export GORACE
|
||||||
|
export BUILD_FLAGS
|
||||||
|
|
||||||
|
ifeq ($(shell id -u), 0)
|
||||||
|
SETCAP_CMD = setcap
|
||||||
|
else
|
||||||
|
SETCAP_CMD = sudo setcap
|
||||||
|
endif
|
||||||
|
|
||||||
|
|
||||||
|
# CAP_NET_BIND_SERVICE: permission for binding to :80 and :443
|
||||||
|
POST_BUILD = $(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep ${BIN_PATH};
|
||||||
|
ifeq ($(docker), 1)
|
||||||
|
POST_BUILD += mkdir -p /app && mv ${BIN_PATH} /app/run;
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: debug
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test ./src/...
|
CGO_ENABLED=1 go test -v -race ${BUILD_FLAGS} ./internal/...
|
||||||
|
|
||||||
up:
|
docker-build-test:
|
||||||
docker compose up -d
|
docker build -t godoxy .
|
||||||
|
docker build --build-arg=MAKE_ARGS=agent=1 -t godoxy-agent .
|
||||||
|
docker build --build-arg=MAKE_ARGS=socket-proxy=1 -t godoxy-socket-proxy .
|
||||||
|
|
||||||
restart:
|
go_ver := $(shell go version | cut -d' ' -f3 | cut -d'o' -f2)
|
||||||
docker compose restart -t 0
|
files := $(shell find . -name go.mod -type f -or -name Dockerfile -type f)
|
||||||
|
gomod_paths := $(shell find . -name go.mod -type f | xargs dirname)
|
||||||
|
|
||||||
logs:
|
update-go:
|
||||||
docker compose logs -f
|
for file in ${files}; do \
|
||||||
|
echo "updating $$file"; \
|
||||||
|
sed -i 's|go \([0-9]\+\.[0-9]\+\.[0-9]\+\)|go ${go_ver}|g' $$file; \
|
||||||
|
sed -i 's|FROM golang:.*-alpine|FROM golang:${go_ver}-alpine|g' $$file; \
|
||||||
|
done
|
||||||
|
for path in ${gomod_paths}; do \
|
||||||
|
echo "go mod tidy $$path"; \
|
||||||
|
cd ${PWD}/$$path && go mod tidy; \
|
||||||
|
done
|
||||||
|
|
||||||
get:
|
update-deps:
|
||||||
cd src && go get -u && go mod tidy && cd ..
|
for path in ${gomod_paths}; do \
|
||||||
|
echo "go get -u $$path"; \
|
||||||
|
cd ${PWD}/$$path && go get -u ./... && go mod tidy; \
|
||||||
|
done
|
||||||
|
|
||||||
debug:
|
mod-tidy:
|
||||||
make build && sudo GOPROXY_DEBUG=1 bin/go-proxy
|
for path in ${gomod_paths}; do \
|
||||||
|
echo "go mod tidy $$path"; \
|
||||||
|
cd ${PWD}/$$path && go mod tidy; \
|
||||||
|
done
|
||||||
|
|
||||||
archive:
|
build:
|
||||||
git archive HEAD -o ../go-proxy-$$(date +"%Y%m%d%H%M").zip
|
mkdir -p $(shell dirname ${BIN_PATH})
|
||||||
|
go build -C ${PWD} ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
|
||||||
|
${POST_BUILD}
|
||||||
|
|
||||||
repush:
|
run:
|
||||||
git reset --soft HEAD^
|
cd ${PWD} && [ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ./cmd
|
||||||
git add -A
|
|
||||||
git commit -m "repush"
|
dev:
|
||||||
git push gitlab dev --force
|
docker compose -f dev.compose.yml $(args)
|
||||||
|
|
||||||
|
dev-build: build
|
||||||
|
docker compose -f dev.compose.yml up -t 0 -d app --force-recreate
|
||||||
|
|
||||||
|
benchmark:
|
||||||
|
@if [ -z "$(TARGET)" ]; then \
|
||||||
|
docker compose -f dev.compose.yml up -d --force-recreate godoxy traefik caddy nginx; \
|
||||||
|
else \
|
||||||
|
docker compose -f dev.compose.yml up -d --force-recreate $(TARGET); \
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
@./scripts/benchmark.sh
|
||||||
|
|
||||||
|
dev-run: build
|
||||||
|
cd dev-data && ${BIN_PATH}
|
||||||
|
|
||||||
|
mtrace:
|
||||||
|
${BIN_PATH} debug-ls-mtrace > mtrace.json
|
||||||
|
|
||||||
rapid-crash:
|
rapid-crash:
|
||||||
sudo docker run --restart=always --name test_crash debian:bookworm-slim /bin/cat &&\
|
docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
|
||||||
sleep 3 &&\
|
sleep 3 &&\
|
||||||
sudo docker rm -f test_crash
|
docker rm -f test_crash
|
||||||
|
|
||||||
debug-list-containers:
|
debug-list-containers:
|
||||||
bash -c 'echo -e "GET /containers/json HTTP/1.0\r\n" | sudo netcat -U /var/run/docker.sock | tail -n +9 | jq'
|
bash -c 'echo -e "GET /containers/json HTTP/1.0\r\n" | sudo netcat -U /var/run/docker.sock | tail -n +9 | jq'
|
||||||
|
|
||||||
|
ci-test:
|
||||||
|
mkdir -p /tmp/artifacts
|
||||||
|
act -n --artifact-server-path /tmp/artifacts -s GITHUB_TOKEN="$$(gh auth token)"
|
||||||
|
|
||||||
|
cloc:
|
||||||
|
scc -w -i go --not-match '_test.go$$'
|
||||||
|
|
||||||
|
push-github:
|
||||||
|
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
||||||
|
|
||||||
|
gen-swagger:
|
||||||
|
# go install github.com/swaggo/swag/cmd/swag@latest
|
||||||
|
swag init --parseDependency --parseInternal --parseFuncBody -g handler.go -d internal/api -o internal/api/v1/docs
|
||||||
|
python3 scripts/fix-swagger-json.py
|
||||||
|
# we don't need this
|
||||||
|
rm internal/api/v1/docs/docs.go
|
||||||
|
|
||||||
|
gen-swagger-markdown: gen-swagger
|
||||||
|
# brew tap go-swagger/go-swagger && brew install go-swagger
|
||||||
|
swagger generate markdown -f internal/api/v1/docs/swagger.yaml --skip-validation --output ${DOCS_DIR}/src/API.md
|
||||||
|
|
||||||
|
gen-api-types: gen-swagger
|
||||||
|
# --disable-throw-on-error
|
||||||
|
bunx --bun swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
|
||||||
|
--responses -o ${WEBUI_DIR}/lib -n api.ts -p internal/api/v1/docs/swagger.json
|
||||||
|
bunx --bun prettier --config ${WEBUI_DIR}/.prettierrc --write ${WEBUI_DIR}/lib/api.ts
|
||||||
|
|
||||||
|
.PHONY: update-wiki
|
||||||
|
update-wiki:
|
||||||
|
DOCS_DIR=${DOCS_DIR} REPO_URL=${REPO_URL} bun --bun scripts/update-wiki/main.ts
|
||||||
|
|||||||
263
README.md
263
README.md
@@ -1,141 +1,206 @@
|
|||||||
# go-proxy
|
<div align="center">
|
||||||
|
|
||||||
|
<img src="assets/godoxy.png" width="200">
|
||||||
|
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|

|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
[](https://sonarcloud.io/summary/new_code?id=go-proxy)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
|
||||||
|
|
||||||
[繁體中文文檔請看此](README_CHT.md)
|

|
||||||
|
[](https://discord.gg/umReR62nRd)
|
||||||
|
|
||||||
A lightweight, easy-to-use, and [performant](docs/benchmark_result.md) reverse proxy with a web UI.
|
A lightweight, simple, and performant reverse proxy with WebUI.
|
||||||
|
|
||||||
|
<h5>
|
||||||
|
<a href="https://docs.godoxy.dev">Website</a> | <a href="https://docs.godoxy.dev/Home.html">Wiki</a> | <a href="https://discord.gg/umReR62nRd">Discord</a>
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<h5>EN | <a href="README_CHT.md">中文</a></h5>
|
||||||
|
|
||||||
|
<img src="screenshots/webui.jpg" style="max-width: 650">
|
||||||
|
|
||||||
|
Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e48936a1-godoxy-assistant)! (Thanks to [@ismesid](https://github.com/arevindh))
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
## Table of content
|
## Table of content
|
||||||
|
|
||||||
<!-- TOC -->
|
<!-- TOC -->
|
||||||
|
|
||||||
- [go-proxy](#go-proxy)
|
- [Table of content](#table-of-content)
|
||||||
- [Table of content](#table-of-content)
|
- [Running demo](#running-demo)
|
||||||
- [Key Points](#key-points)
|
- [Key Features](#key-features)
|
||||||
- [Getting Started](#getting-started)
|
- [Prerequisites](#prerequisites)
|
||||||
- [Setup](#setup)
|
- [Setup](#setup)
|
||||||
- [Commands line arguments](#commands-line-arguments)
|
- [How does GoDoxy work](#how-does-godoxy-work)
|
||||||
- [Environment variables](#environment-variables)
|
- [Update / Uninstall system agent](#update--uninstall-system-agent)
|
||||||
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
|
- [Screenshots](#screenshots)
|
||||||
- [Config File](#config-file)
|
- [idlesleeper](#idlesleeper)
|
||||||
- [Provider File](#provider-file)
|
- [Metrics and Logs](#metrics-and-logs)
|
||||||
- [Showcase](#showcase)
|
- [Manual Setup](#manual-setup)
|
||||||
- [idlesleeper](#idlesleeper)
|
- [Folder structrue](#folder-structrue)
|
||||||
- [Build it yourself](#build-it-yourself)
|
- [Build it yourself](#build-it-yourself)
|
||||||
|
- [Star History](#star-history)
|
||||||
|
|
||||||
## Key Points
|
## Running demo
|
||||||
|
|
||||||
- Easy to use
|
<https://demo.godoxy.dev>
|
||||||
- Effortless configuration
|
|
||||||
- Error messages is clear and detailed, easy troubleshooting
|
|
||||||
- Auto certificate obtaining and renewal (See [Supported DNS Challenge Providers](docs/dns_providers.md))
|
|
||||||
- Auto configuration for docker containers
|
|
||||||
- Auto hot-reload on container state / config file changes
|
|
||||||
- Stop containers on idle, wake it up on traffic _(optional, see [showcase](#idlesleeper))_
|
|
||||||
- HTTP(s) reserve proxy
|
|
||||||
- TCP and UDP port forwarding
|
|
||||||
- Web UI for configuration and monitoring (See [screenshots](https://github.com/yusing/go-proxy-frontend?tab=readme-ov-file#screenshots))
|
|
||||||
- Written in **[Go](https://go.dev)**
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
## Key Features
|
||||||
|
|
||||||
## Getting Started
|
- **Simple**
|
||||||
|
- Effortless configuration with [simple labels](https://docs.godoxy.dev/Docker-labels-and-Route-Files) or WebUI
|
||||||
|
- [Simple multi-node setup](https://docs.godoxy.dev/Configurations#multi-docker-nodes-setup)
|
||||||
|
- Detailed error messages for easy troubleshooting.
|
||||||
|
- **ACL**: connection / request level access control
|
||||||
|
- IP/CIDR
|
||||||
|
- Country **(Maxmind account required)**
|
||||||
|
- Timezone **(Maxmind account required)**
|
||||||
|
- **Access logging**
|
||||||
|
- Periodic notification of access summaries for number of allowed and blocked connections
|
||||||
|
- **Advanced Automation**
|
||||||
|
- Automatic SSL certificate management with Let's Encrypt ([using DNS-01 Challenge](https://docs.godoxy.dev/DNS-01-Providers))
|
||||||
|
- Auto-configuration for Docker containers
|
||||||
|
- Hot-reloading of configurations and container state changes
|
||||||
|
- **Container Runtime Support**
|
||||||
|
- Docker
|
||||||
|
- Podman
|
||||||
|
- **Idle-sleep**: stop and wake containers based on traffic _(see [screenshots](#idlesleeper))_
|
||||||
|
- Docker containers
|
||||||
|
- Proxmox LXCs
|
||||||
|
- **Traffic Management**
|
||||||
|
- HTTP reserve proxy
|
||||||
|
- TCP/UDP port forwarding
|
||||||
|
- **OpenID Connect support**: SSO and secure your apps easily
|
||||||
|
- **ForwardAuth support**: integrate with any auth provider (e.g. TinyAuth)
|
||||||
|
- **Customization**
|
||||||
|
- [HTTP middlewares](https://docs.godoxy.dev/Middlewares)
|
||||||
|
- [Custom error pages support](https://docs.godoxy.dev/Custom-Error-Pages)
|
||||||
|
- **Web UI**
|
||||||
|
- App Dashboard
|
||||||
|
- Config Editor
|
||||||
|
- Uptime and System Metrics
|
||||||
|
- Docker Logs Viewer
|
||||||
|
- **Cross-Platform support**
|
||||||
|
- Supports **linux/amd64** and **linux/arm64**
|
||||||
|
- **Efficient and Performant**
|
||||||
|
- Written in **[Go](https://go.dev)**
|
||||||
|
|
||||||
### Setup
|
## Prerequisites
|
||||||
|
|
||||||
1. Setup DNS Records, e.g.
|
Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
|
||||||
|
|
||||||
- A Record: `*.y.z` -> `10.0.10.1`
|
- A Record: `*.domain.com` -> `10.0.10.1`
|
||||||
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
|
- AAAA Record (if you use IPv6): `*.domain.com` -> `::ffff:a00:a01`
|
||||||
|
|
||||||
2. Setup `go-proxy` [See here](docs/docker.md)
|
## Setup
|
||||||
|
|
||||||
3. Setup `docker-socket-proxy` (see [example](docs/docker_socket_proxy.md) other machine that is running docker (if any)
|
> [!NOTE]
|
||||||
|
> GoDoxy is designed to be running in `host` network mode, do not change it.
|
||||||
|
>
|
||||||
|
> To change listening ports, modify `.env`.
|
||||||
|
|
||||||
4. Configure `go-proxy`
|
1. Prepare a new directory for docker compose and config files.
|
||||||
- with text editor (e.g. Visual Studio Code)
|
|
||||||
- or with web config editor via `http://gp.y.z`
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
2. Run setup script inside the directory, or [set up manually](#manual-setup)
|
||||||
|
|
||||||
### Commands line arguments
|
```shell
|
||||||
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
||||||
|
```
|
||||||
|
|
||||||
| Argument | Description | Example |
|
3. Start the docker compose service from generated `compose.yml`:
|
||||||
| ----------- | -------------------------------- | -------------------------- |
|
|
||||||
| empty | start proxy server | |
|
|
||||||
| `validate` | validate config and exit | |
|
|
||||||
| `reload` | trigger a force reload of config | |
|
|
||||||
| `ls-config` | list config and exit | `go-proxy ls-config \| jq` |
|
|
||||||
| `ls-route` | list proxy entries and exit | `go-proxy ls-route \| jq` |
|
|
||||||
|
|
||||||
**run with `docker exec <container_name> /app/go-proxy <command>`**
|
```shell
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
### Environment variables
|
4. You may now do some extra configuration on WebUI `https://godoxy.yourdomain.com`
|
||||||
|
|
||||||
| Environment Variable | Description | Default | Values |
|
## How does GoDoxy work
|
||||||
| ------------------------------ | ------------------------------------------- | ---------------- | ------------- |
|
|
||||||
| `GOPROXY_NO_SCHEMA_VALIDATION` | disable schema validation | `false` | boolean |
|
|
||||||
| `GOPROXY_DEBUG` | enable debug behaviors | `false` | boolean |
|
|
||||||
| `GOPROXY_HTTP_ADDR` | http server listening address | `:80` | `[host]:port` |
|
|
||||||
| `GOPROXY_HTTPS_ADDR` | https server listening address (if enabled) | `:443` | `[host]:port` |
|
|
||||||
| `GOPROXY_API_ADDR` | api server listening address | `127.0.0.1:8888` | `[host]:port` |
|
|
||||||
|
|
||||||
### Use JSON Schema in VSCode
|
1. List all the containers
|
||||||
|
2. Read container name, labels and port configurations for each of them
|
||||||
|
3. Create a route if applicable (a route is like a "Virtual Host" in NPM)
|
||||||
|
4. Watch for container / config changes and update automatically
|
||||||
|
|
||||||
Copy [`.vscode/settings.example.json`](.vscode/settings.example.json) to `.vscode/settings.json` and modify it to fit your needs
|
> [!NOTE]
|
||||||
|
> GoDoxy uses the label `proxy.aliases` as the subdomain(s), if unset it defaults to the `container_name` field in docker compose.
|
||||||
|
>
|
||||||
|
> For example, with the label `proxy.aliases: qbt` you can access your app via `qbt.domain.com`.
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
## Update / Uninstall system agent
|
||||||
|
|
||||||
### Config File
|
Update:
|
||||||
|
|
||||||
See [config.example.yml](config.example.yml) for more
|
```bash
|
||||||
|
bash -c "$(curl -fsSL https://github.com/yusing/godoxy/raw/refs/heads/main/scripts/install-agent.sh)" -- update
|
||||||
```yaml
|
|
||||||
# autocert configuration
|
|
||||||
autocert:
|
|
||||||
email: # ACME Email
|
|
||||||
domains: # a list of domains for cert registration
|
|
||||||
provider: # DNS Challenge provider
|
|
||||||
options: # provider specific options
|
|
||||||
- ...
|
|
||||||
# reverse proxy providers configuration
|
|
||||||
providers:
|
|
||||||
include:
|
|
||||||
- providers.yml
|
|
||||||
- other_file_1.yml
|
|
||||||
- ...
|
|
||||||
docker:
|
|
||||||
local: $DOCKER_HOST
|
|
||||||
remote-1: tcp://10.0.2.1:2375
|
|
||||||
remote-2: ssh://root:1234@10.0.2.2
|
|
||||||
```
|
```
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
Uninstall:
|
||||||
|
|
||||||
### Provider File
|
```bash
|
||||||
|
bash -c "$(curl -fsSL https://github.com/yusing/godoxy/raw/refs/heads/main/scripts/install-agent.sh)" -- uninstall
|
||||||
|
```
|
||||||
|
|
||||||
See [Fields](docs/docker.md#fields)
|
## Screenshots
|
||||||
|
|
||||||
See [providers.example.yml](providers.example.yml) for examples
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
|
||||||
|
|
||||||
## Showcase
|
|
||||||
|
|
||||||
### idlesleeper
|
### idlesleeper
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
### Metrics and Logs
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><img src="screenshots/routes.jpg" alt="Routes" width="350"/></td>
|
||||||
|
<td align="center"><img src="screenshots/servers.jpg" alt="Servers" width="350"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><b>Routes</b></td>
|
||||||
|
<td align="center"><b>Servers</b></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Manual Setup
|
||||||
|
|
||||||
|
1. Make `config` directory then grab `config.example.yml` into `config/config.yml`
|
||||||
|
|
||||||
|
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/godoxy/main/config.example.yml -O config/config.yml`
|
||||||
|
|
||||||
|
2. Grab `.env.example` into `.env`
|
||||||
|
|
||||||
|
`wget https://raw.githubusercontent.com/yusing/godoxy/main/.env.example -O .env`
|
||||||
|
|
||||||
|
3. Grab `compose.example.yml` into `compose.yml`
|
||||||
|
|
||||||
|
`wget https://raw.githubusercontent.com/yusing/godoxy/main/compose.example.yml -O compose.yml`
|
||||||
|
|
||||||
|
### Folder structrue
|
||||||
|
|
||||||
|
```shell
|
||||||
|
├── certs
|
||||||
|
│ ├── cert.crt
|
||||||
|
│ └── priv.key
|
||||||
|
├── compose.yml
|
||||||
|
├── config
|
||||||
|
│ ├── config.yml
|
||||||
|
│ ├── middlewares
|
||||||
|
│ │ ├── middleware1.yml
|
||||||
|
│ │ ├── middleware2.yml
|
||||||
|
│ ├── provider1.yml
|
||||||
|
│ └── provider2.yml
|
||||||
|
├── data
|
||||||
|
│ ├── metrics # metrics data
|
||||||
|
│ │ ├── uptime.json
|
||||||
|
│ │ └── system_info.json
|
||||||
|
└── .env
|
||||||
|
```
|
||||||
|
|
||||||
## Build it yourself
|
## Build it yourself
|
||||||
|
|
||||||
1. Clone the repository `git clone https://github.com/yusing/go-proxy --depth=1`
|
1. Clone the repository `git clone https://github.com/yusing/godoxy --depth=1`
|
||||||
|
|
||||||
2. Install / Upgrade [go (>=1.22)](https://go.dev/doc/install) and `make` if not already
|
2. Install / Upgrade [go (>=1.22)](https://go.dev/doc/install) and `make` if not already
|
||||||
|
|
||||||
@@ -145,4 +210,8 @@ See [providers.example.yml](providers.example.yml) for examples
|
|||||||
|
|
||||||
5. build binary with `make build`
|
5. build binary with `make build`
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#yusing/godoxy&Date)
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
[🔼Back to top](#table-of-content)
|
||||||
|
|||||||
248
README_CHT.md
248
README_CHT.md
@@ -1,144 +1,198 @@
|
|||||||
# go-proxy
|
<div align="center">
|
||||||
|
|
||||||
|
<img src="assets/godoxy.png" width="200">
|
||||||
|
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||||
|

|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
|
||||||
|
|
||||||
一個輕量化、易用且[高效](docs/benchmark_result.md)的反向代理和端口轉發工具
|

|
||||||
|
[](https://discord.gg/umReR62nRd)
|
||||||
|
|
||||||
|
輕量、易用、 高效能,且帶有主頁和配置面板的反向代理
|
||||||
|
|
||||||
|
<h5>
|
||||||
|
<a href="https://docs.godoxy.dev">網站</a> | <a href="https://docs.godoxy.dev/Home.html">文檔</a> | <a href="https://discord.gg/umReR62nRd">Discord</a>
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<h5><a href="README.md">EN</a> | 中文</h5>
|
||||||
|
|
||||||
|
<img src="https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f" style="max-width: 650">
|
||||||
|
|
||||||
|
有疑問? 問 [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e48936a1-godoxy-assistant)!(鳴謝 [@ismesid](https://github.com/arevindh))
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
## 目錄
|
## 目錄
|
||||||
|
|
||||||
<!-- TOC -->
|
<!-- TOC -->
|
||||||
|
|
||||||
- [go-proxy](#go-proxy)
|
- [目錄](#目錄)
|
||||||
- [目錄](#目錄)
|
- [運行示例](#運行示例)
|
||||||
- [重點](#重點)
|
- [主要特點](#主要特點)
|
||||||
- [入門指南](#入門指南)
|
- [前置需求](#前置需求)
|
||||||
- [安裝](#安裝)
|
- [安裝](#安裝)
|
||||||
- [命令行參數](#命令行參數)
|
- [手動安裝](#手動安裝)
|
||||||
- [環境變量](#環境變量)
|
- [資料夾結構](#資料夾結構)
|
||||||
- [VSCode 中使用 JSON Schema](#vscode-中使用-json-schema)
|
- [更新 / 卸載系統代理 (System Agent)](#更新--卸載系統代理-system-agent)
|
||||||
- [配置文件](#配置文件)
|
- [截圖](#截圖)
|
||||||
- [透過文件配置](#透過文件配置)
|
- [閒置休眠](#閒置休眠)
|
||||||
- [展示](#展示)
|
- [監控](#監控)
|
||||||
- [idlesleeper](#idlesleeper)
|
- [自行編譯](#自行編譯)
|
||||||
- [源碼編譯](#源碼編譯)
|
- [Star History](#star-history)
|
||||||
|
|
||||||
## 重點
|
## 運行示例
|
||||||
|
|
||||||
- 易用
|
<https://demo.godoxy.dev>
|
||||||
- 不需花費太多時間就能輕鬆配置
|
|
||||||
- 除錯簡單
|
|
||||||
- 自動處理 HTTPS 證書(參見[可用的 DNS 供應商](docs/dns_providers.md))
|
|
||||||
- 透過 Docker 容器自動配置
|
|
||||||
- 容器狀態變更時自動熱重載
|
|
||||||
- 容器閒置時自動暫停/停止,入站時自動喚醒
|
|
||||||
- HTTP(s)反向代理
|
|
||||||
- TCP/UDP 端口轉發
|
|
||||||
- 用於配置和監控的前端 Web 面板([截圖](https://github.com/yusing/go-proxy-frontend?tab=readme-ov-file#screenshots))
|
|
||||||
- 使用 **[Go](https://go.dev)** 編寫
|
|
||||||
|
|
||||||
[🔼 返回頂部](#目錄)
|
## 主要特點
|
||||||
|
|
||||||
## 入門指南
|
- **簡單易用**
|
||||||
|
- 透過 Docker[標籤](https://docs.godoxy.dev/Docker-labels-and-Route-Files)或 WebUI 輕鬆設定
|
||||||
|
- [簡單的多節點設置](https://docs.godoxy.dev/Configurations#multi-docker-nodes-setup)
|
||||||
|
- 詳細的錯誤訊息,便於故障排除
|
||||||
|
- **存取控制 (ACL)**:連線/請求層級存取控制
|
||||||
|
- IP/CIDR
|
||||||
|
- 國家 **(需要 Maxmind 帳戶)**
|
||||||
|
- 時區 **(需要 Maxmind 帳戶)**
|
||||||
|
- **存取日誌記錄**
|
||||||
|
- 定時發送摘要 (允許和拒絕的連線次數)
|
||||||
|
- **自動化**
|
||||||
|
- 使用 Let's Encrypt 自動管理 SSL 憑證 ([使用 DNS-01 驗證](https://docs.godoxy.dev/DNS-01-Providers))
|
||||||
|
- Docker 容器自動配置
|
||||||
|
- 設定檔與容器狀態變更時自動熱重載
|
||||||
|
- **容器運行時支援**
|
||||||
|
- Docker
|
||||||
|
- Podman
|
||||||
|
- **閒置休眠**:根據流量停止和喚醒容器 _(參見[截圖](#閒置休眠))_
|
||||||
|
- Docker 容器
|
||||||
|
- Proxmox LXC 容器
|
||||||
|
- **流量管理**
|
||||||
|
- HTTP 反向代理
|
||||||
|
- TCP/UDP 連接埠轉送
|
||||||
|
- **OpenID Connect 支援**:輕鬆實現單點登入 (SSO) 並保護您的應用程式
|
||||||
|
- **ForwardAuth 支援**:整合任何 auth provider (例如 TinyAuth)
|
||||||
|
- **客製化**
|
||||||
|
- [HTTP 中介軟體](https://docs.godoxy.dev/Middlewares)
|
||||||
|
- [支援自訂錯誤頁面](https://docs.godoxy.dev/Custom-Error-Pages)
|
||||||
|
- **網頁使用者介面 (Web UI)**
|
||||||
|
- 應用程式一覽
|
||||||
|
- 設定編輯器
|
||||||
|
- 執行時間與系統指標
|
||||||
|
- Docker 日誌檢視器
|
||||||
|
- **跨平台支援**
|
||||||
|
- 支援 **linux/amd64** 與 **linux/arm64**
|
||||||
|
- **高效能**
|
||||||
|
- 以 **[Go](https://go.dev)** 語言編寫
|
||||||
|
|
||||||
### 安裝
|
## 前置需求
|
||||||
|
|
||||||
1. 設置 DNS 記錄,例如:
|
設置 DNS 記錄指向運行 `GoDoxy` 的機器,例如:
|
||||||
|
|
||||||
- A 記錄: `*.y.z` -> `10.0.10.1`
|
- A 記錄:`*.y.z` -> `10.0.10.1`
|
||||||
- AAAA 記錄: `*.y.z` -> `::ffff:a00:a01`
|
- AAAA 記錄:`*.y.z` -> `::ffff:a00:a01`
|
||||||
|
|
||||||
2. 安裝 `go-proxy` [參見這裡](docs/docker.md)
|
## 安裝
|
||||||
|
|
||||||
3. 配置 `go-proxy`
|
> [!NOTE]
|
||||||
- 使用文本編輯器 (推薦 Visual Studio Code [參見 VSCode 使用 schema](#vscode-中使用-json-schema))
|
> GoDoxy 僅在 `host` 網路模式下運作,請勿更改。
|
||||||
- 或通過 `http://gp.y.z` 使用網頁配置編輯器
|
>
|
||||||
|
> 如需更改監聽埠,請修改 `.env`。
|
||||||
|
|
||||||
[🔼 返回頂部](#目錄)
|
1. 準備一個新目錄用於 docker compose 和配置文件。
|
||||||
|
|
||||||
### 命令行參數
|
2. 在目錄內運行安裝腳本,或[手動安裝](#手動安裝)
|
||||||
|
|
||||||
| 參數 | 描述 | 示例 |
|
```shell
|
||||||
| ----------- | -------------- | -------------------------- |
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
||||||
| 空 | 啟動代理服務器 | |
|
```
|
||||||
| `validate` | 驗證配置並退出 | |
|
|
||||||
| `reload` | 強制刷新配置 | |
|
|
||||||
| `ls-config` | 列出配置並退出 | `go-proxy ls-config \| jq` |
|
|
||||||
| `ls-route` | 列出路由並退出 | `go-proxy ls-route \| jq` |
|
|
||||||
|
|
||||||
**使用 `docker exec <容器名稱> /app/go-proxy <參數>` 運行**
|
3. 現在可以在 WebUI `https://godoxy.yourdomain.com` 進行額外配置
|
||||||
|
|
||||||
### 環境變量
|
### 手動安裝
|
||||||
|
|
||||||
| 環境變量 | 描述 | 默認 | 格式 |
|
1. 建立 `config` 目錄,然後將 `config.example.yml` 下載到 `config/config.yml`
|
||||||
| ------------------------------ | ---------------- | ---------------- | ------------- |
|
|
||||||
| `GOPROXY_NO_SCHEMA_VALIDATION` | 禁用 schema 驗證 | `false` | boolean |
|
|
||||||
| `GOPROXY_DEBUG` | 啟用調試輸出 | `false` | boolean |
|
|
||||||
| `GOPROXY_HTTP_ADDR` | http 收聽地址 | `:80` | `[host]:port` |
|
|
||||||
| `GOPROXY_HTTPS_ADDR` | https 收聽地址 | `:443` | `[host]:port` |
|
|
||||||
| `GOPROXY_API_ADDR` | api 收聽地址 | `127.0.0.1:8888` | `[host]:port` |
|
|
||||||
|
|
||||||
### VSCode 中使用 JSON Schema
|
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/godoxy/main/config.example.yml -O config/config.yml`
|
||||||
|
|
||||||
複製 [`.vscode/settings.example.json`](.vscode/settings.example.json) 到 `.vscode/settings.json` 並根據需求修改
|
2. 將 `.env.example` 下載到 `.env`
|
||||||
|
|
||||||
[🔼 返回頂部](#目錄)
|
`wget https://raw.githubusercontent.com/yusing/godoxy/main/.env.example -O .env`
|
||||||
|
|
||||||
### 配置文件
|
3. 將 `compose.example.yml` 下載到 `compose.yml`
|
||||||
|
|
||||||
參見 [config.example.yml](config.example.yml) 了解更多
|
`wget https://raw.githubusercontent.com/yusing/godoxy/main/compose.example.yml -O compose.yml`
|
||||||
|
|
||||||
```yaml
|
### 資料夾結構
|
||||||
# autocert 配置
|
|
||||||
autocert:
|
```shell
|
||||||
email: # ACME 電子郵件
|
├── certs
|
||||||
domains: # 域名列表
|
│ ├── cert.crt
|
||||||
provider: # DNS 供應商
|
│ └── priv.key
|
||||||
options: # 供應商個別配置
|
├── compose.yml
|
||||||
- ...
|
├── config
|
||||||
# 配置文件 / docker
|
│ ├── config.yml
|
||||||
providers:
|
│ ├── middlewares
|
||||||
include:
|
│ │ ├── middleware1.yml
|
||||||
- providers.yml
|
│ │ ├── middleware2.yml
|
||||||
- other_file_1.yml
|
│ ├── provider1.yml
|
||||||
- ...
|
│ └── provider2.yml
|
||||||
docker:
|
├── data
|
||||||
local: $DOCKER_HOST
|
│ ├── metrics # metrics data
|
||||||
remote-1: tcp://10.0.2.1:2375
|
│ │ ├── uptime.json
|
||||||
remote-2: ssh://root:1234@10.0.2.2
|
│ │ └── system_info.json
|
||||||
|
└── .env
|
||||||
```
|
```
|
||||||
|
|
||||||
[🔼 返回頂部](#目錄)
|
## 更新 / 卸載系統代理 (System Agent)
|
||||||
|
|
||||||
### 透過文件配置
|
更新:
|
||||||
|
|
||||||
參見 [Fields](docs/docker.md#fields)
|
```bash
|
||||||
|
sudo /bin/bash -c "$(curl -fsSL https://github.com/yusing/godoxy/raw/refs/heads/main/scripts/install-agent.sh)" -- update
|
||||||
|
```
|
||||||
|
|
||||||
參見範例 [providers.example.yml](providers.example.yml)
|
卸載:
|
||||||
|
|
||||||
[🔼 返回頂部](#目錄)
|
```bash
|
||||||
|
sudo /bin/bash -c "$(curl -fsSL https://github.com/yusing/godoxy/raw/refs/heads/main/scripts/install-agent.sh)" -- uninstall
|
||||||
|
```
|
||||||
|
|
||||||
## 展示
|
## 截圖
|
||||||
|
|
||||||
### idlesleeper
|
### 閒置休眠
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
[🔼 返回頂部](#目錄)
|
### 監控
|
||||||
|
|
||||||
## 源碼編譯
|
<div align="center">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><img src="screenshots/routes.jpg" alt="Routes" width="350"/></td>
|
||||||
|
<td align="center"><img src="screenshots/servers.jpg" alt="Servers" width="350"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><b>路由</b></td>
|
||||||
|
<td align="center"><b>伺服器</b></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
1. 獲取源碼 `git clone https://github.com/yusing/go-proxy --depth=1`
|
## 自行編譯
|
||||||
|
|
||||||
2. 安裝/升級 [go 版本 (>=1.22)](https://go.dev/doc/install) 和 `make`(如果尚未安裝)
|
1. 克隆儲存庫 `git clone https://github.com/yusing/godoxy --depth=1`
|
||||||
|
|
||||||
3. 如果之前編譯過(go 版本 < 1.22),請使用 `go clean -cache` 清除緩存
|
2. 如果尚未安裝,請安裝/升級 [go (>=1.22)](https://go.dev/doc/install) 和 `make`
|
||||||
|
|
||||||
4. 使用 `make get` 獲取依賴項
|
3. 如果之前編譯過(go < 1.22),請使用 `go clean -cache` 清除快取
|
||||||
|
|
||||||
5. 使用 `make build` 編譯
|
4. 使用 `make get` 獲取依賴
|
||||||
|
|
||||||
[🔼 返回頂部](#目錄)
|
5. 使用 `make build` 編譯二進制檔案
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#yusing/godoxy&Date)
|
||||||
|
|
||||||
|
[🔼 回到頂部](#目錄)
|
||||||
|
|||||||
52
agent/cmd/README.md
Normal file
52
agent/cmd/README.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# agent/cmd
|
||||||
|
|
||||||
|
The main entry point for the GoDoxy Agent, a secure monitoring and proxy agent that runs alongside Docker containers.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This package contains the `main.go` entry point for the GoDoxy Agent. The agent is a TLS-enabled server that provides:
|
||||||
|
|
||||||
|
- Secure Docker socket proxying with client certificate authentication
|
||||||
|
- HTTP proxy capabilities for container traffic
|
||||||
|
- System metrics collection and monitoring
|
||||||
|
- Health check endpoints
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[main] --> B[Logger Init]
|
||||||
|
A --> C[Load CA Certificate]
|
||||||
|
A --> D[Load Server Certificate]
|
||||||
|
A --> E[Log Version Info]
|
||||||
|
A --> F[Start Agent Server]
|
||||||
|
A --> G[Start Socket Proxy]
|
||||||
|
A --> H[Start System Info Poller]
|
||||||
|
A --> I[Wait Exit]
|
||||||
|
|
||||||
|
F --> F1[TLS with mTLS]
|
||||||
|
F --> F2[Agent Handler]
|
||||||
|
G --> G1[Docker Socket Proxy]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Main Function Flow
|
||||||
|
|
||||||
|
1. **Logger Setup**: Configures zerolog with console output
|
||||||
|
1. **Certificate Loading**: Loads CA and server certificates for TLS/mTLS
|
||||||
|
1. **Version Logging**: Logs agent version and configuration
|
||||||
|
1. **Agent Server**: Starts the main HTTPS server with agent handlers
|
||||||
|
1. **Socket Proxy**: Starts Docker socket proxy if configured
|
||||||
|
1. **System Monitoring**: Starts system info polling
|
||||||
|
1. **Graceful Shutdown**: Waits for exit signal (3 second timeout)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
See `agent/pkg/env/README.md` for configuration options.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `agent/pkg/agent` - Core agent types and constants
|
||||||
|
- `agent/pkg/env` - Environment configuration
|
||||||
|
- `agent/pkg/server` - Server implementation
|
||||||
|
- `socketproxy/pkg` - Docker socket proxy
|
||||||
|
- `internal/metrics/systeminfo` - System metrics
|
||||||
167
agent/cmd/main.go
Normal file
167
agent/cmd/main.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
stdlog "log"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/env"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/handler"
|
||||||
|
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||||
|
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
strutils "github.com/yusing/goutils/strings"
|
||||||
|
"github.com/yusing/goutils/task"
|
||||||
|
"github.com/yusing/goutils/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: support IPv6
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
writer := zerolog.ConsoleWriter{
|
||||||
|
Out: os.Stderr,
|
||||||
|
TimeFormat: "01-02 15:04",
|
||||||
|
}
|
||||||
|
zerolog.TimeFieldFormat = writer.TimeFormat
|
||||||
|
log.Logger = zerolog.New(writer).Level(zerolog.InfoLevel).With().Timestamp().Logger()
|
||||||
|
ca := &agent.PEMPair{}
|
||||||
|
err := ca.Load(env.AgentCACert)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("init CA error")
|
||||||
|
}
|
||||||
|
caCert, err := ca.ToTLSCert()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("init CA error")
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := &agent.PEMPair{}
|
||||||
|
srv.Load(env.AgentSSLCert)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("init SSL error")
|
||||||
|
}
|
||||||
|
srvCert, err := srv.ToTLSCert()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("init SSL error")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Msgf("GoDoxy Agent version %s", version.Get())
|
||||||
|
log.Info().Msgf("Agent name: %s", env.AgentName)
|
||||||
|
log.Info().Msgf("Agent port: %d", env.AgentPort)
|
||||||
|
log.Info().Msgf("Agent runtime: %s", env.Runtime)
|
||||||
|
|
||||||
|
log.Info().Msg(`
|
||||||
|
Tips:
|
||||||
|
1. To change the agent name, you can set the AGENT_NAME environment variable.
|
||||||
|
2. To change the agent port, you can set the AGENT_PORT environment variable.
|
||||||
|
`)
|
||||||
|
|
||||||
|
t := task.RootTask("agent", false)
|
||||||
|
|
||||||
|
// One TCP listener on AGENT_PORT, then multiplex by TLS ALPN:
|
||||||
|
// - Stream ALPN: route to TCP stream tunnel handler (via http.Server.TLSNextProto)
|
||||||
|
// - Otherwise: route to HTTPS API handler
|
||||||
|
tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{Port: env.AgentPort})
|
||||||
|
if err != nil {
|
||||||
|
gperr.LogFatal("failed to listen on port", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
caCertPool.AddCert(caCert.Leaf)
|
||||||
|
|
||||||
|
muxTLSConfig := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{*srvCert},
|
||||||
|
ClientCAs: caCertPool,
|
||||||
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
// Keep HTTP limited to HTTP/1.1 (matching current agent server behavior)
|
||||||
|
// and add the stream tunnel ALPN for multiplexing.
|
||||||
|
NextProtos: []string{"http/1.1", stream.StreamALPN},
|
||||||
|
}
|
||||||
|
if env.AgentSkipClientCertCheck {
|
||||||
|
muxTLSConfig.ClientAuth = tls.NoClientCert
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLS listener feeds the HTTP server. ALPN stream connections are intercepted
|
||||||
|
// using http.Server.TLSNextProto.
|
||||||
|
tlsLn := tls.NewListener(tcpListener, muxTLSConfig)
|
||||||
|
|
||||||
|
streamSrv := stream.NewTCPServerHandler(t.Context())
|
||||||
|
|
||||||
|
httpSrv := &http.Server{
|
||||||
|
Handler: handler.NewAgentHandler(),
|
||||||
|
BaseContext: func(net.Listener) context.Context {
|
||||||
|
return t.Context()
|
||||||
|
},
|
||||||
|
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){
|
||||||
|
// When a client negotiates StreamALPN, net/http will call this hook instead
|
||||||
|
// of treating the connection as HTTP.
|
||||||
|
stream.StreamALPN: func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
|
||||||
|
// ServeConn blocks until the tunnel finishes.
|
||||||
|
streamSrv.ServeConn(conn)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
{
|
||||||
|
subtask := t.Subtask("agent-http", true)
|
||||||
|
t.OnCancel("stop_http", func() {
|
||||||
|
_ = streamSrv.Close()
|
||||||
|
_ = httpSrv.Close()
|
||||||
|
_ = tlsLn.Close()
|
||||||
|
})
|
||||||
|
go func() {
|
||||||
|
err := httpSrv.Serve(tlsLn)
|
||||||
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Error().Err(err).Msg("agent HTTP server stopped with error")
|
||||||
|
}
|
||||||
|
subtask.Finish(err)
|
||||||
|
}()
|
||||||
|
log.Info().Int("port", env.AgentPort).Msg("HTTPS API server started (ALPN mux enabled)")
|
||||||
|
}
|
||||||
|
log.Info().Int("port", env.AgentPort).Msg("TCP stream handler started (via TLSNextProto)")
|
||||||
|
|
||||||
|
{
|
||||||
|
udpServer := stream.NewUDPServer(t.Context(), "udp", &net.UDPAddr{Port: env.AgentPort}, caCert.Leaf, srvCert)
|
||||||
|
subtask := t.Subtask("agent-stream-udp", true)
|
||||||
|
t.OnCancel("stop_stream_udp", func() {
|
||||||
|
_ = udpServer.Close()
|
||||||
|
})
|
||||||
|
go func() {
|
||||||
|
err := udpServer.Start()
|
||||||
|
subtask.Finish(err)
|
||||||
|
}()
|
||||||
|
log.Info().Int("port", env.AgentPort).Msg("UDP stream server started")
|
||||||
|
}
|
||||||
|
|
||||||
|
if socketproxy.ListenAddr != "" {
|
||||||
|
runtime := strutils.Title(string(env.Runtime))
|
||||||
|
|
||||||
|
log.Info().Msgf("%s socket listening on: %s", runtime, socketproxy.ListenAddr)
|
||||||
|
l, err := net.Listen("tcp", socketproxy.ListenAddr)
|
||||||
|
if err != nil {
|
||||||
|
gperr.LogFatal("failed to listen on port", err)
|
||||||
|
}
|
||||||
|
errLog := log.Logger.With().Str("level", "error").Str("component", "socketproxy").Logger()
|
||||||
|
srv := http.Server{
|
||||||
|
Handler: socketproxy.NewHandler(),
|
||||||
|
BaseContext: func(net.Listener) context.Context {
|
||||||
|
return t.Context()
|
||||||
|
},
|
||||||
|
ErrorLog: stdlog.New(&errLog, "", 0),
|
||||||
|
}
|
||||||
|
srv.Serve(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
systeminfo.Poller.Start()
|
||||||
|
|
||||||
|
task.WaitExit(3)
|
||||||
|
}
|
||||||
104
agent/go.mod
Normal file
104
agent/go.mod
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
module github.com/yusing/godoxy/agent
|
||||||
|
|
||||||
|
go 1.25.6
|
||||||
|
|
||||||
|
replace (
|
||||||
|
github.com/shirou/gopsutil/v4 => ../internal/gopsutil
|
||||||
|
github.com/yusing/godoxy => ../
|
||||||
|
github.com/yusing/godoxy/socketproxy => ../socket-proxy
|
||||||
|
github.com/yusing/goutils => ../goutils
|
||||||
|
github.com/yusing/goutils/http/reverseproxy => ../goutils/http/reverseproxy
|
||||||
|
github.com/yusing/goutils/http/websocket => ../goutils/http/websocket
|
||||||
|
github.com/yusing/goutils/server => ../goutils/server
|
||||||
|
)
|
||||||
|
|
||||||
|
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.14.2
|
||||||
|
github.com/gin-gonic/gin v1.11.0
|
||||||
|
github.com/gorilla/websocket v1.5.3
|
||||||
|
github.com/pion/dtls/v3 v3.0.10
|
||||||
|
github.com/pion/transport/v3 v3.1.1
|
||||||
|
github.com/rs/zerolog v1.34.0
|
||||||
|
github.com/stretchr/testify v1.11.1
|
||||||
|
github.com/yusing/godoxy v0.24.1
|
||||||
|
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
|
||||||
|
github.com/yusing/goutils v0.7.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/docker/cli v29.1.4+incompatible // indirect
|
||||||
|
github.com/docker/go-connections v0.6.0 // indirect
|
||||||
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
|
github.com/ebitengine/purego v0.9.1 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.3 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
|
github.com/moby/moby/api v1.52.0 // indirect
|
||||||
|
github.com/moby/moby/client v0.2.1 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/pion/logging v0.2.4 // indirect
|
||||||
|
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
|
github.com/puzpuzpuz/xsync/v4 v4.3.0 // indirect
|
||||||
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.12 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||||
|
github.com/yusing/ds v0.4.1 // indirect
|
||||||
|
github.com/yusing/gointernals v0.1.16 // indirect
|
||||||
|
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260116021320-b12ef77f3743 // indirect
|
||||||
|
github.com/yusing/goutils/http/websocket v0.0.0-20260116021320-b12ef77f3743 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||||
|
golang.org/x/arch v0.23.0 // indirect
|
||||||
|
golang.org/x/crypto v0.47.0 // indirect
|
||||||
|
golang.org/x/net v0.49.0 // indirect
|
||||||
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
|
golang.org/x/text v0.33.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
272
agent/go.sum
Normal file
272
agent/go.sum
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||||
|
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
||||||
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||||
|
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
||||||
|
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
|
||||||
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
|
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
||||||
|
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
||||||
|
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
||||||
|
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
|
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||||
|
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/diskfs/go-diskfs v1.7.0 h1:vonWmt5CMowXwUc79jWyGrf2DIMeoOjkLlMnQYGVOs8=
|
||||||
|
github.com/diskfs/go-diskfs v1.7.0/go.mod h1:LhQyXqOugWFRahYUSw47NyZJPezFzB9UELwhpszLP/k=
|
||||||
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||||
|
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||||
|
github.com/docker/cli v29.1.4+incompatible h1:AI8fwZhqsAsrqZnVv9h6lbexeW/LzNTasf6A4vcNN8M=
|
||||||
|
github.com/docker/cli v29.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
|
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||||
|
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
|
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
|
||||||
|
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
|
||||||
|
github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA=
|
||||||
|
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||||
|
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||||
|
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
||||||
|
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
||||||
|
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||||
|
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||||
|
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
|
github.com/luthermonson/go-proxmox v0.3.2 h1:/zUg6FCl9cAABx0xU3OIgtDtClY0gVXxOCsrceDNylc=
|
||||||
|
github.com/luthermonson/go-proxmox v0.3.2/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||||
|
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||||
|
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
|
||||||
|
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
|
||||||
|
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||||
|
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
|
||||||
|
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
|
||||||
|
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
|
||||||
|
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||||
|
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||||
|
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||||
|
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||||
|
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||||
|
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||||
|
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||||
|
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/puzpuzpuz/xsync/v4 v4.3.0 h1:w/bWkEJdYuRNYhHn5eXnIT8LzDM1O629X1I9MJSkD7Q=
|
||||||
|
github.com/puzpuzpuz/xsync/v4 v4.3.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||||
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
|
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||||
|
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||||
|
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
|
||||||
|
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
||||||
|
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
|
||||||
|
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
||||||
|
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||||
|
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||||
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||||
|
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||||
|
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||||
|
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||||
|
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
||||||
|
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
||||||
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
|
github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
|
||||||
|
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||||
|
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
||||||
|
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||||
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
|
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||||
|
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||||
|
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||||
|
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
|
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
|
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
|
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
|
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||||
|
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||||
108
agent/pkg/agent/README.md
Normal file
108
agent/pkg/agent/README.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# Agent Package
|
||||||
|
|
||||||
|
The `agent` package provides the client-side implementation for interacting with GoDoxy agents. It handles agent configuration, secure communication via TLS, and provides utilities for agent deployment and management.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph GoDoxy Server
|
||||||
|
AP[Agent Pool] --> AC[AgentConfig]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Agent Communication
|
||||||
|
AC -->|HTTPS| AI[Agent Info API]
|
||||||
|
AC -->|TLS| ST[Stream Tunneling]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Deployment
|
||||||
|
G[Generator] --> DC[Docker Compose]
|
||||||
|
G --> IS[Install Script]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Security
|
||||||
|
NA[NewAgent] --> Certs[Certificates]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
| ---------------------------------------- | --------------------------------------------------------- |
|
||||||
|
| [`config.go`](config.go) | Core configuration, initialization, and API client logic. |
|
||||||
|
| [`new_agent.go`](new_agent.go) | Agent creation and certificate generation logic. |
|
||||||
|
| [`docker_compose.go`](docker_compose.go) | Generator for agent Docker Compose configurations. |
|
||||||
|
| [`bare_metal.go`](bare_metal.go) | Generator for bare metal installation scripts. |
|
||||||
|
| [`env.go`](env.go) | Environment configuration types and constants. |
|
||||||
|
| `common/` | Shared constants and utilities for agents. |
|
||||||
|
|
||||||
|
## Core Types
|
||||||
|
|
||||||
|
### [`AgentConfig`](config.go:29)
|
||||||
|
|
||||||
|
The primary struct used by the GoDoxy server to manage a connection to an agent. It stores the agent's address, metadata, and TLS configuration.
|
||||||
|
|
||||||
|
### [`AgentInfo`](config.go:45)
|
||||||
|
|
||||||
|
Contains basic metadata about the agent, including its version, name, and container runtime (Docker or Podman).
|
||||||
|
|
||||||
|
### [`PEMPair`](new_agent.go:53)
|
||||||
|
|
||||||
|
A utility struct for handling PEM-encoded certificate and key pairs, supporting encryption, decryption, and conversion to `tls.Certificate`.
|
||||||
|
|
||||||
|
## Agent Creation and Certificate Management
|
||||||
|
|
||||||
|
### Certificate Generation
|
||||||
|
|
||||||
|
The [`NewAgent`](new_agent.go:147) function creates a complete certificate infrastructure for an agent:
|
||||||
|
|
||||||
|
- **CA Certificate**: Self-signed root certificate with 1000-year validity.
|
||||||
|
- **Server Certificate**: For the agent's HTTPS server, signed by the CA.
|
||||||
|
- **Client Certificate**: For the GoDoxy server to authenticate with the agent.
|
||||||
|
|
||||||
|
All certificates use ECDSA with P-256 curve and SHA-256 signatures.
|
||||||
|
|
||||||
|
### Certificate Security
|
||||||
|
|
||||||
|
- Certificates are encrypted using AES-GCM with a provided encryption key.
|
||||||
|
- The [`PEMPair`](new_agent.go:53) struct provides methods for encryption, decryption, and conversion to `tls.Certificate`.
|
||||||
|
- Base64 encoding is used for certificate storage and transmission.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### 1. Secure Communication
|
||||||
|
|
||||||
|
All communication between the GoDoxy server and agents is secured using mutual TLS (mTLS). The [`AgentConfig`](config.go:29) handles the loading of CA and client certificates to establish secure connections.
|
||||||
|
|
||||||
|
### 2. Agent Discovery and Initialization
|
||||||
|
|
||||||
|
The [`Init`](config.go:231) and [`InitWithCerts`](config.go:110) methods allow the server to:
|
||||||
|
|
||||||
|
- Fetch agent metadata (version, name, runtime).
|
||||||
|
- Verify compatibility between server and agent versions.
|
||||||
|
- Test support for TCP and UDP stream tunneling.
|
||||||
|
|
||||||
|
### 3. Deployment Generators
|
||||||
|
|
||||||
|
The package provides interfaces and implementations for generating deployment artifacts:
|
||||||
|
|
||||||
|
- **Docker Compose**: Generates a `docker-compose.yml` for running the agent as a container via [`AgentComposeConfig.Generate()`](docker_compose.go:21).
|
||||||
|
- **Bare Metal**: Generates a shell script to install and run the agent as a systemd service via [`AgentEnvConfig.Generate()`](bare_metal.go:27).
|
||||||
|
|
||||||
|
### 4. Fake Docker Host
|
||||||
|
|
||||||
|
The package supports a "fake" Docker host scheme (`agent://<addr>`) to identify containers managed by an agent, allowing the GoDoxy server to route requests appropriately. See [`IsDockerHostAgent`](config.go:90) and [`GetAgentAddrFromDockerHost`](config.go:94).
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg := &agent.AgentConfig{}
|
||||||
|
cfg.Parse("192.168.1.100:8081")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := cfg.Init(ctx); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Connected to agent: %s (Version: %s)\n", cfg.Name, cfg.Version)
|
||||||
|
```
|
||||||
33
agent/pkg/agent/bare_metal.go
Normal file
33
agent/pkg/agent/bare_metal.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
installScript = `AGENT_NAME="{{.Name}}" \
|
||||||
|
AGENT_PORT="{{.Port}}" \
|
||||||
|
AGENT_CA_CERT="{{.CACert}}" \
|
||||||
|
AGENT_SSL_CERT="{{.SSLCert}}" \
|
||||||
|
{{ if eq .ContainerRuntime "nerdctl" -}}
|
||||||
|
DOCKER_SOCKET="/var/run/containerd/containerd.sock" \
|
||||||
|
RUNTIME="nerdctl" \
|
||||||
|
{{ else if eq .ContainerRuntime "podman" -}}
|
||||||
|
DOCKER_SOCKET="/var/run/podman/podman.sock" \
|
||||||
|
RUNTIME="podman" \
|
||||||
|
{{ else -}}
|
||||||
|
DOCKER_SOCKET="/var/run/docker.sock" \
|
||||||
|
RUNTIME="docker" \
|
||||||
|
{{ end -}}
|
||||||
|
bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/install-agent.sh)"`
|
||||||
|
installScriptTemplate = template.Must(template.New("install.sh").Parse(installScript))
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *AgentEnvConfig) Generate() (string, error) {
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, 4096))
|
||||||
|
if err := installScriptTemplate.Execute(buf, c); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
3
agent/pkg/agent/common/common.go
Normal file
3
agent/pkg/agent/common/common.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
const CertsDNSName = "godoxy.agent"
|
||||||
364
agent/pkg/agent/config.go
Normal file
364
agent/pkg/agent/config.go
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||||
|
agentstream "github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/certs"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
httputils "github.com/yusing/goutils/http"
|
||||||
|
"github.com/yusing/goutils/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AgentConfig struct {
|
||||||
|
AgentInfo
|
||||||
|
|
||||||
|
Addr string `json:"addr"`
|
||||||
|
IsTCPStreamSupported bool `json:"supports_tcp_stream"`
|
||||||
|
IsUDPStreamSupported bool `json:"supports_udp_stream"`
|
||||||
|
|
||||||
|
// for stream
|
||||||
|
caCert *x509.Certificate
|
||||||
|
clientCert *tls.Certificate
|
||||||
|
|
||||||
|
tlsConfig tls.Config
|
||||||
|
|
||||||
|
l zerolog.Logger
|
||||||
|
} // @name Agent
|
||||||
|
|
||||||
|
type AgentInfo struct {
|
||||||
|
Version version.Version `json:"version" swaggertype:"string"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Runtime ContainerRuntime `json:"runtime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated. Replaced by EndpointInfo
|
||||||
|
const (
|
||||||
|
EndpointVersion = "/version"
|
||||||
|
EndpointName = "/name"
|
||||||
|
EndpointRuntime = "/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
EndpointInfo = "/info"
|
||||||
|
EndpointProxyHTTP = "/proxy/http"
|
||||||
|
EndpointHealth = "/health"
|
||||||
|
EndpointLogs = "/logs"
|
||||||
|
EndpointSystemInfo = "/system_info"
|
||||||
|
|
||||||
|
AgentHost = common.CertsDNSName
|
||||||
|
|
||||||
|
APIEndpointBase = "/godoxy/agent"
|
||||||
|
APIBaseURL = "https://" + AgentHost + APIEndpointBase
|
||||||
|
|
||||||
|
DockerHost = "https://" + AgentHost
|
||||||
|
|
||||||
|
FakeDockerHostPrefix = "agent://"
|
||||||
|
FakeDockerHostPrefixLen = len(FakeDockerHostPrefix)
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustParseURL(urlStr string) *url.URL {
|
||||||
|
u, err := url.Parse(urlStr)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
AgentURL = mustParseURL(APIBaseURL)
|
||||||
|
HTTPProxyURL = mustParseURL(APIBaseURL + EndpointProxyHTTP)
|
||||||
|
HTTPProxyURLPrefixLen = len(APIEndpointBase + EndpointProxyHTTP)
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsDockerHostAgent(dockerHost string) bool {
|
||||||
|
return strings.HasPrefix(dockerHost, FakeDockerHostPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAgentAddrFromDockerHost(dockerHost string) string {
|
||||||
|
return dockerHost[FakeDockerHostPrefixLen:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *AgentConfig) FakeDockerHost() string {
|
||||||
|
return FakeDockerHostPrefix + cfg.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *AgentConfig) Parse(addr string) error {
|
||||||
|
cfg.Addr = addr
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverVersion = version.Get()
|
||||||
|
|
||||||
|
// InitWithCerts initializes the agent config with the given CA, certificate, and key.
|
||||||
|
func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte) error {
|
||||||
|
clientCert, err := tls.X509KeyPair(crt, key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cfg.clientCert = &clientCert
|
||||||
|
|
||||||
|
// create tls config
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
ok := caCertPool.AppendCertsFromPEM(ca)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("invalid ca certificate")
|
||||||
|
}
|
||||||
|
// Keep the CA leaf for stream client dialing.
|
||||||
|
if block, _ := pem.Decode(ca); block == nil || block.Type != "CERTIFICATE" {
|
||||||
|
return errors.New("invalid ca certificate")
|
||||||
|
} else if cert, err := x509.ParseCertificate(block.Bytes); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
cfg.caCert = cert
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.tlsConfig = tls.Config{
|
||||||
|
Certificates: []tls.Certificate{clientCert},
|
||||||
|
RootCAs: caCertPool,
|
||||||
|
ServerName: common.CertsDNSName,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
status, err := cfg.fetchJSON(ctx, EndpointInfo, &cfg.AgentInfo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var streamUnsupportedErrs gperr.Builder
|
||||||
|
|
||||||
|
if status == http.StatusOK {
|
||||||
|
// test stream server connection
|
||||||
|
const fakeAddress = "localhost:8080" // it won't be used, just for testing
|
||||||
|
// test TCP stream support
|
||||||
|
err := agentstream.TCPHealthCheck(cfg.Addr, cfg.caCert, cfg.clientCert)
|
||||||
|
if err != nil {
|
||||||
|
streamUnsupportedErrs.Addf("failed to connect to stream server via TCP: %w", err)
|
||||||
|
} else {
|
||||||
|
cfg.IsTCPStreamSupported = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// test UDP stream support
|
||||||
|
err = agentstream.UDPHealthCheck(cfg.Addr, cfg.caCert, cfg.clientCert)
|
||||||
|
if err != nil {
|
||||||
|
streamUnsupportedErrs.Addf("failed to connect to stream server via UDP: %w", err)
|
||||||
|
} else {
|
||||||
|
cfg.IsUDPStreamSupported = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// old agent does not support EndpointInfo
|
||||||
|
// fallback with old logic
|
||||||
|
cfg.IsTCPStreamSupported = false
|
||||||
|
cfg.IsUDPStreamSupported = false
|
||||||
|
streamUnsupportedErrs.Adds("agent version is too old, does not support stream tunneling")
|
||||||
|
|
||||||
|
// get agent name
|
||||||
|
name, _, err := cfg.fetchString(ctx, EndpointName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Name = name
|
||||||
|
|
||||||
|
// check agent version
|
||||||
|
agentVersion, _, err := cfg.fetchString(ctx, EndpointVersion)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Version = version.Parse(agentVersion)
|
||||||
|
|
||||||
|
// check agent runtime
|
||||||
|
runtime, status, err := cfg.fetchString(ctx, EndpointRuntime)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case http.StatusOK:
|
||||||
|
switch runtime {
|
||||||
|
case "docker":
|
||||||
|
cfg.Runtime = ContainerRuntimeDocker
|
||||||
|
// case "nerdctl":
|
||||||
|
// cfg.Runtime = ContainerRuntimeNerdctl
|
||||||
|
case "podman":
|
||||||
|
cfg.Runtime = ContainerRuntimePodman
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid agent runtime: %s", runtime)
|
||||||
|
}
|
||||||
|
case http.StatusNotFound:
|
||||||
|
// backward compatibility, old agent does not have runtime endpoint
|
||||||
|
cfg.Runtime = ContainerRuntimeDocker
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("failed to get agent runtime: HTTP %d %s", status, runtime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.l = log.With().Str("agent", cfg.Name).Logger()
|
||||||
|
|
||||||
|
if err := streamUnsupportedErrs.Error(); err != nil {
|
||||||
|
gperr.LogWarn("agent has limited/no stream tunneling support, TCP and UDP routes via agent will not work", err, &cfg.l)
|
||||||
|
}
|
||||||
|
|
||||||
|
if serverVersion.IsNewerThanMajor(cfg.Version) {
|
||||||
|
log.Warn().Msgf("agent %s major version mismatch: server: %s, agent: %s", cfg.Name, serverVersion, cfg.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Msgf("agent %q initialized", cfg.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the agent config with the given context.
|
||||||
|
func (cfg *AgentConfig) Init(ctx context.Context) error {
|
||||||
|
filepath, ok := certs.AgentCertsFilepath(cfg.Addr)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid agent host: %s", cfg.Addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
certData, err := os.ReadFile(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read agent certs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca, crt, key, err := certs.ExtractCert(certData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to extract agent certs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg.InitWithCerts(ctx, ca, crt, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTCPClient creates a new TCP client for the agent.
|
||||||
|
//
|
||||||
|
// It returns an error if
|
||||||
|
// - the agent is not initialized
|
||||||
|
// - the agent does not support TCP stream tunneling
|
||||||
|
// - the agent stream server address is not initialized
|
||||||
|
func (cfg *AgentConfig) NewTCPClient(targetAddress string) (net.Conn, error) {
|
||||||
|
if cfg.caCert == nil || cfg.clientCert == nil {
|
||||||
|
return nil, errors.New("agent is not initialized")
|
||||||
|
}
|
||||||
|
if !cfg.IsTCPStreamSupported {
|
||||||
|
return nil, errors.New("agent does not support TCP stream tunneling")
|
||||||
|
}
|
||||||
|
return agentstream.NewTCPClient(cfg.Addr, targetAddress, cfg.caCert, cfg.clientCert)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUDPClient creates a new UDP client for the agent.
|
||||||
|
//
|
||||||
|
// It returns an error if
|
||||||
|
// - the agent is not initialized
|
||||||
|
// - the agent does not support UDP stream tunneling
|
||||||
|
// - the agent stream server address is not initialized
|
||||||
|
func (cfg *AgentConfig) NewUDPClient(targetAddress string) (net.Conn, error) {
|
||||||
|
if cfg.caCert == nil || cfg.clientCert == nil {
|
||||||
|
return nil, errors.New("agent is not initialized")
|
||||||
|
}
|
||||||
|
if !cfg.IsUDPStreamSupported {
|
||||||
|
return nil, errors.New("agent does not support UDP stream tunneling")
|
||||||
|
}
|
||||||
|
return agentstream.NewUDPClient(cfg.Addr, targetAddress, cfg.caCert, cfg.clientCert)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *AgentConfig) Transport() *http.Transport {
|
||||||
|
return &http.Transport{
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
if addr != AgentHost+":443" {
|
||||||
|
return nil, &net.AddrError{Err: "invalid address", Addr: addr}
|
||||||
|
}
|
||||||
|
if network != "tcp" {
|
||||||
|
return nil, &net.OpError{Op: "dial", Net: network, Source: nil, Addr: nil}
|
||||||
|
}
|
||||||
|
return cfg.DialContext(ctx)
|
||||||
|
},
|
||||||
|
TLSClientConfig: &cfg.tlsConfig,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *AgentConfig) TLSConfig() *tls.Config {
|
||||||
|
return &cfg.tlsConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var dialer = &net.Dialer{Timeout: 5 * time.Second}
|
||||||
|
|
||||||
|
func (cfg *AgentConfig) DialContext(ctx context.Context) (net.Conn, error) {
|
||||||
|
return dialer.DialContext(ctx, "tcp", cfg.Addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *AgentConfig) String() string {
|
||||||
|
return cfg.Name + "@" + cfg.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *AgentConfig) do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, APIBaseURL+endpoint, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
client := http.Client{
|
||||||
|
Transport: cfg.Transport(),
|
||||||
|
}
|
||||||
|
return client.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *AgentConfig) fetchString(ctx context.Context, endpoint string) (string, int, error) {
|
||||||
|
resp, err := cfg.do(ctx, "GET", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
data, release, err := httputils.ReadAllBody(resp)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
ret := string(data)
|
||||||
|
release(data)
|
||||||
|
return ret, resp.StatusCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchJSON fetches a JSON response from the agent and unmarshals it into the provided struct
|
||||||
|
//
|
||||||
|
// It will return the status code of the response, and error if any.
|
||||||
|
// If the status code is not http.StatusOK, out will be unchanged but error will still be nil.
|
||||||
|
func (cfg *AgentConfig) fetchJSON(ctx context.Context, endpoint string, out any) (int, error) {
|
||||||
|
resp, err := cfg.do(ctx, "GET", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
data, release, err := httputils.ReadAllBody(resp)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer release(data)
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return resp.StatusCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(data, out)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return resp.StatusCode, nil
|
||||||
|
}
|
||||||
28
agent/pkg/agent/docker_compose.go
Normal file
28
agent/pkg/agent/docker_compose.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed templates/agent.compose.yml.tmpl
|
||||||
|
agentComposeYAML string
|
||||||
|
agentComposeYAMLTemplate = template.Must(template.New("agent.compose.yml.tmpl").Parse(agentComposeYAML))
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DockerImageProduction = "ghcr.io/yusing/godoxy-agent:latest"
|
||||||
|
DockerImageNightly = "ghcr.io/yusing/godoxy-agent:nightly"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *AgentComposeConfig) Generate() (string, error) {
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||||
|
err := agentComposeYAMLTemplate.Execute(buf, c)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
25
agent/pkg/agent/env.go
Normal file
25
agent/pkg/agent/env.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
type (
|
||||||
|
ContainerRuntime string
|
||||||
|
AgentEnvConfig struct {
|
||||||
|
Name string
|
||||||
|
Port int
|
||||||
|
CACert string
|
||||||
|
SSLCert string
|
||||||
|
ContainerRuntime ContainerRuntime
|
||||||
|
}
|
||||||
|
AgentComposeConfig struct {
|
||||||
|
Image string
|
||||||
|
*AgentEnvConfig
|
||||||
|
}
|
||||||
|
Generator interface {
|
||||||
|
Generate() (string, error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ContainerRuntimeDocker ContainerRuntime = "docker"
|
||||||
|
ContainerRuntimePodman ContainerRuntime = "podman"
|
||||||
|
// ContainerRuntimeNerdctl ContainerRuntime = "nerdctl"
|
||||||
|
)
|
||||||
245
agent/pkg/agent/new_agent.go
Normal file
245
agent/pkg/agent/new_agent.go
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func toPEMPair(certDER []byte, key *ecdsa.PrivateKey) *PEMPair {
|
||||||
|
marshaledKey, err := marshalECPrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
// This is a critical internal error during PEM encoding of a newly generated key.
|
||||||
|
// Panicking is acceptable here as it indicates a fundamental issue.
|
||||||
|
panic(fmt.Sprintf("failed to marshal EC private key for PEM encoding: %v", err))
|
||||||
|
}
|
||||||
|
return &PEMPair{
|
||||||
|
Cert: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}),
|
||||||
|
Key: pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: marshaledKey}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalECPrivateKey(key *ecdsa.PrivateKey) ([]byte, error) {
|
||||||
|
derBytes, err := x509.MarshalECPrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal EC private key: %w", err)
|
||||||
|
}
|
||||||
|
return derBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func b64Encode(data []byte) string {
|
||||||
|
return base64.StdEncoding.EncodeToString(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func b64Decode(data string) ([]byte, error) {
|
||||||
|
return base64.StdEncoding.DecodeString(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PEMPair struct {
|
||||||
|
Cert, Key []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PEMPair) String() string {
|
||||||
|
return b64Encode(p.Cert) + ";" + b64Encode(p.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PEMPair) Load(data string) (err error) {
|
||||||
|
parts := strings.Split(data, ";")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return errors.New("invalid PEM pair")
|
||||||
|
}
|
||||||
|
p.Cert, err = b64Decode(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Key, err = b64Decode(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PEMPair) Encrypt(encKey []byte) (PEMPair, error) {
|
||||||
|
cert, err := encrypt(p.Cert, encKey)
|
||||||
|
if err != nil {
|
||||||
|
return PEMPair{}, err
|
||||||
|
}
|
||||||
|
key, err := encrypt(p.Key, encKey)
|
||||||
|
if err != nil {
|
||||||
|
return PEMPair{}, err
|
||||||
|
}
|
||||||
|
return PEMPair{Cert: cert, Key: key}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PEMPair) Decrypt(encKey []byte) (PEMPair, error) {
|
||||||
|
cert, err := decrypt(p.Cert, encKey)
|
||||||
|
if err != nil {
|
||||||
|
return PEMPair{}, err
|
||||||
|
}
|
||||||
|
key, err := decrypt(p.Key, encKey)
|
||||||
|
if err != nil {
|
||||||
|
return PEMPair{}, err
|
||||||
|
}
|
||||||
|
return PEMPair{Cert: cert, Key: key}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encrypt(data []byte, key []byte) ([]byte, error) {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return gcm.Seal(nonce, nonce, data, nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decrypt(data []byte, key []byte) ([]byte, error) {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := data[:gcm.NonceSize()]
|
||||||
|
ciphertext := data[gcm.NonceSize():]
|
||||||
|
return gcm.Open(nil, nonce, ciphertext, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PEMPair) ToTLSCert() (*tls.Certificate, error) {
|
||||||
|
cert, err := tls.X509KeyPair(p.Cert, p.Key)
|
||||||
|
return &cert, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSerialNumber() (*big.Int, error) {
|
||||||
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) // 128-bit random number
|
||||||
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate serial number: %w", err)
|
||||||
|
}
|
||||||
|
return serialNumber, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAgent() (ca, srv, client *PEMPair, err error) {
|
||||||
|
caSerialNumber, err := newSerialNumber()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
// Create the CA's certificate
|
||||||
|
caTemplate := &x509.Certificate{
|
||||||
|
SerialNumber: caSerialNumber,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
Organization: []string{"GoDoxy"},
|
||||||
|
CommonName: common.CertsDNSName,
|
||||||
|
},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().AddDate(1000, 0, 0), // 1000 years
|
||||||
|
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
IsCA: true,
|
||||||
|
MaxPathLen: 0,
|
||||||
|
MaxPathLenZero: true,
|
||||||
|
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
||||||
|
}
|
||||||
|
|
||||||
|
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
caDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ca = toPEMPair(caDER, caKey)
|
||||||
|
|
||||||
|
// Generate a new private key for the server certificate
|
||||||
|
serverKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
serverSerialNumber, err := newSerialNumber()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
srvTemplate := &x509.Certificate{
|
||||||
|
SerialNumber: serverSerialNumber,
|
||||||
|
Issuer: caTemplate.Subject,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
Organization: caTemplate.Subject.Organization,
|
||||||
|
OrganizationalUnit: []string{"Server"},
|
||||||
|
CommonName: common.CertsDNSName,
|
||||||
|
},
|
||||||
|
DNSNames: []string{common.CertsDNSName},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().AddDate(1000, 0, 0), // Add validity period
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
||||||
|
}
|
||||||
|
|
||||||
|
srvCertDER, err := x509.CreateCertificate(rand.Reader, srvTemplate, caTemplate, &serverKey.PublicKey, caKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
srv = toPEMPair(srvCertDER, serverKey)
|
||||||
|
|
||||||
|
clientKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientSerialNumber, err := newSerialNumber()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
clientTemplate := &x509.Certificate{
|
||||||
|
SerialNumber: clientSerialNumber,
|
||||||
|
Issuer: caTemplate.Subject,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
Organization: caTemplate.Subject.Organization,
|
||||||
|
OrganizationalUnit: []string{"Client"},
|
||||||
|
CommonName: common.CertsDNSName,
|
||||||
|
},
|
||||||
|
DNSNames: []string{common.CertsDNSName},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().AddDate(1000, 0, 0),
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||||
|
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
||||||
|
}
|
||||||
|
clientCertDER, err := x509.CreateCertificate(rand.Reader, clientTemplate, caTemplate, &clientKey.PublicKey, caKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client = toPEMPair(clientCertDER, clientKey)
|
||||||
|
return ca, srv, client, err
|
||||||
|
}
|
||||||
113
agent/pkg/agent/new_agent_test.go
Normal file
113
agent/pkg/agent/new_agent_test.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewAgent(t *testing.T) {
|
||||||
|
ca, srv, client, err := NewAgent()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, ca)
|
||||||
|
require.NotNil(t, srv)
|
||||||
|
require.NotNil(t, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPEMPair(t *testing.T) {
|
||||||
|
ca, srv, client, err := NewAgent()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for i, p := range []*PEMPair{ca, srv, client} {
|
||||||
|
t.Run(fmt.Sprintf("load-%d", i), func(t *testing.T) {
|
||||||
|
var pp PEMPair
|
||||||
|
err := pp.Load(p.String())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, p.Cert, pp.Cert)
|
||||||
|
require.Equal(t, p.Key, pp.Key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPEMPairToTLSCert(t *testing.T) {
|
||||||
|
ca, srv, client, err := NewAgent()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for i, p := range []*PEMPair{ca, srv, client} {
|
||||||
|
t.Run(fmt.Sprintf("toTLSCert-%d", i), func(t *testing.T) {
|
||||||
|
cert, err := p.ToTLSCert()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, cert)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerClient(t *testing.T) {
|
||||||
|
ca, srv, client, err := NewAgent()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
srvTLS, err := srv.ToTLSCert()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, srvTLS)
|
||||||
|
|
||||||
|
clientTLS, err := client.ToTLSCert()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, clientTLS)
|
||||||
|
|
||||||
|
caPool := x509.NewCertPool()
|
||||||
|
require.True(t, caPool.AppendCertsFromPEM(ca.Cert))
|
||||||
|
|
||||||
|
srvTLSConfig := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{*srvTLS},
|
||||||
|
ClientCAs: caPool,
|
||||||
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||||
|
}
|
||||||
|
|
||||||
|
clientTLSConfig := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{*clientTLS},
|
||||||
|
RootCAs: caPool,
|
||||||
|
ServerName: common.CertsDNSName,
|
||||||
|
}
|
||||||
|
|
||||||
|
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
server.TLS = srvTLSConfig
|
||||||
|
server.StartTLS()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: &http.Transport{TLSClientConfig: clientTLSConfig},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := httpClient.Get(server.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, resp.StatusCode, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPEMPairEncryptDecrypt(t *testing.T) {
|
||||||
|
encKey := make([]byte, 32)
|
||||||
|
_, err := rand.Read(encKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ca, _, _, err := NewAgent()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
encCA, err := ca.Encrypt(encKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, encCA)
|
||||||
|
|
||||||
|
decCA, err := encCA.Decrypt(encKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, decCA)
|
||||||
|
|
||||||
|
require.Equal(t, string(ca.Cert), string(decCA.Cert))
|
||||||
|
require.Equal(t, string(ca.Key), string(decCA.Key))
|
||||||
|
}
|
||||||
197
agent/pkg/agent/stream/README.md
Normal file
197
agent/pkg/agent/stream/README.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# Stream proxy protocol
|
||||||
|
|
||||||
|
This package implements a small header-based handshake that allows an authenticated client to request forwarding to a `(host, port)` destination. It supports both TCP-over-TLS and UDP-over-DTLS transports.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph Client
|
||||||
|
TC[TCPClient] -->|TLS| TSS[TCPServer]
|
||||||
|
UC[UDPClient] -->|DTLS| USS[UDPServer]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Stream Protocol
|
||||||
|
H[StreamRequestHeader]
|
||||||
|
end
|
||||||
|
|
||||||
|
TSS -->|Redirect| DST1[Destination TCP]
|
||||||
|
USS -->|Forward UDP| DST2[Destination UDP]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Header
|
||||||
|
|
||||||
|
The on-wire header is a fixed-size binary blob:
|
||||||
|
|
||||||
|
- `Version` (8 bytes)
|
||||||
|
- `HostLength` (1 byte)
|
||||||
|
- `Host` (255 bytes, NUL padded)
|
||||||
|
- `PortLength` (1 byte)
|
||||||
|
- `Port` (5 bytes, NUL padded)
|
||||||
|
- `Flag` (1 byte, protocol flags)
|
||||||
|
- `Checksum` (4 bytes, big-endian CRC32)
|
||||||
|
|
||||||
|
Total: `headerSize = 8 + 1 + 255 + 1 + 5 + 1 + 4 = 275` bytes.
|
||||||
|
|
||||||
|
Checksum is `crc32.ChecksumIEEE(header[0:headerSize-4])`.
|
||||||
|
|
||||||
|
### Flags
|
||||||
|
|
||||||
|
The `Flag` field is a bitmask of protocol flags defined by `FlagType`:
|
||||||
|
|
||||||
|
| Flag | Value | Purpose |
|
||||||
|
| ---------------------- | ----- | ---------------------------------------------------------------------- |
|
||||||
|
| `FlagCloseImmediately` | `1` | Health check probe - server closes immediately after validating header |
|
||||||
|
|
||||||
|
See [`FlagType`](header.go:26) and [`FlagCloseImmediately`](header.go:28).
|
||||||
|
|
||||||
|
See [`StreamRequestHeader`](header.go:30).
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
| ----------------------------------- | ------------------------------------------------------------ |
|
||||||
|
| [`header.go`](header.go) | Stream request header structure and validation. |
|
||||||
|
| [`tcp_client.go`](tcp_client.go:12) | TCP client implementation with TLS transport. |
|
||||||
|
| [`tcp_server.go`](tcp_server.go:13) | TCP server implementation for handling stream requests. |
|
||||||
|
| [`udp_client.go`](udp_client.go:13) | UDP client implementation with DTLS transport. |
|
||||||
|
| [`udp_server.go`](udp_server.go:17) | UDP server implementation for handling DTLS stream requests. |
|
||||||
|
| [`common.go`](common.go:11) | Connection manager and shared constants. |
|
||||||
|
|
||||||
|
## Constants
|
||||||
|
|
||||||
|
| Constant | Value | Purpose |
|
||||||
|
| ---------------------- | ------------------------- | ------------------------------------------------------- |
|
||||||
|
| `StreamALPN` | `"godoxy-agent-stream/1"` | TLS ALPN protocol for stream multiplexing. |
|
||||||
|
| `headerSize` | `275` bytes | Total size of the stream request header. |
|
||||||
|
| `dialTimeout` | `10s` | Timeout for establishing destination connections. |
|
||||||
|
| `readDeadline` | `10s` | Read timeout for UDP destination sockets. |
|
||||||
|
| `FlagCloseImmediately` | `1` | Flag for health check probe - server closes immediately |
|
||||||
|
|
||||||
|
See [`common.go`](common.go:11).
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
### Types
|
||||||
|
|
||||||
|
#### `StreamRequestHeader`
|
||||||
|
|
||||||
|
Represents the on-wire protocol header used to negotiate a stream tunnel.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type StreamRequestHeader struct {
|
||||||
|
Version [8]byte // Fixed to "0.1.0" with NUL padding
|
||||||
|
HostLength byte // Actual host name length (0-255)
|
||||||
|
Host [255]byte // NUL-padded host name
|
||||||
|
PortLength byte // Actual port string length (0-5)
|
||||||
|
Port [5]byte // NUL-padded port string
|
||||||
|
Flag FlagType // Protocol flags (e.g., FlagCloseImmediately)
|
||||||
|
Checksum [4]byte // CRC32 checksum of header without checksum
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
|
||||||
|
- `NewStreamRequestHeader(host, port string) (*StreamRequestHeader, error)` - Creates a header for the given host and port. Returns error if host exceeds 255 bytes or port exceeds 5 bytes.
|
||||||
|
- `NewStreamHealthCheckHeader() *StreamRequestHeader` - Creates a header with `FlagCloseImmediately` set for health check probes.
|
||||||
|
- `Validate() bool` - Validates the version and checksum.
|
||||||
|
- `GetHostPort() (string, string)` - Extracts the host and port from the header.
|
||||||
|
- `ShouldCloseImmediately() bool` - Returns true if `FlagCloseImmediately` is set.
|
||||||
|
|
||||||
|
### TCP Functions
|
||||||
|
|
||||||
|
- [`NewTCPClient()`](tcp_client.go:26) - Creates a TLS client connection and sends the stream header.
|
||||||
|
- [`NewTCPServerHandler()`](tcp_server.go:24) - Creates a handler for ALPN-multiplexed connections (no listener).
|
||||||
|
- [`NewTCPServerFromListener()`](tcp_server.go:36) - Wraps an existing TLS listener.
|
||||||
|
- [`NewTCPServer()`](tcp_server.go:45) - Creates a fully-configured TCP server with TLS listener.
|
||||||
|
|
||||||
|
### UDP Functions
|
||||||
|
|
||||||
|
- [`NewUDPClient()`](udp_client.go:27) - Creates a DTLS client connection and sends the stream header.
|
||||||
|
- [`NewUDPServer()`](udp_server.go:26) - Creates a DTLS server listening on the given UDP address.
|
||||||
|
|
||||||
|
## Health Check Probes
|
||||||
|
|
||||||
|
The protocol supports health check probes using the `FlagCloseImmediately` flag. When a client sends a header with this flag set, the server validates the header and immediately closes the connection without establishing a destination tunnel.
|
||||||
|
|
||||||
|
This is useful for:
|
||||||
|
|
||||||
|
- Connectivity testing between agent and server
|
||||||
|
- Verifying TLS/DTLS handshake and mTLS authentication
|
||||||
|
- Monitoring stream protocol availability
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
header := stream.NewStreamHealthCheckHeader()
|
||||||
|
// Send header over TLS/DTLS connection
|
||||||
|
// Server will validate and close immediately
|
||||||
|
```
|
||||||
|
|
||||||
|
Both TCP and UDP servers silently handle health check probes without logging errors.
|
||||||
|
|
||||||
|
See [`NewStreamHealthCheckHeader()`](header.go:66) and [`FlagCloseImmediately`](header.go:28).
|
||||||
|
|
||||||
|
## TCP behavior
|
||||||
|
|
||||||
|
1. Client establishes a TLS connection to the stream server.
|
||||||
|
2. Client sends exactly one header as a handshake.
|
||||||
|
3. After the handshake, both sides proxy raw TCP bytes between client and destination.
|
||||||
|
|
||||||
|
Server reads the header using `io.ReadFull` to avoid dropping bytes.
|
||||||
|
|
||||||
|
See [`NewTCPClient()`](tcp_client.go:26) and [`(*TCPServer).redirect()`](tcp_server.go:116).
|
||||||
|
|
||||||
|
## UDP-over-DTLS behavior
|
||||||
|
|
||||||
|
1. Client establishes a DTLS connection to the stream server.
|
||||||
|
2. Client sends exactly one header as a handshake.
|
||||||
|
3. After the handshake, both sides proxy raw UDP datagrams:
|
||||||
|
- client -> destination: DTLS payload is written to destination `UDPConn`
|
||||||
|
- destination -> client: destination payload is written back to the DTLS connection
|
||||||
|
|
||||||
|
Responses do **not** include a header.
|
||||||
|
|
||||||
|
The UDP server uses a bidirectional forwarding model:
|
||||||
|
|
||||||
|
- One goroutine forwards from client to destination
|
||||||
|
- Another goroutine forwards from destination to client
|
||||||
|
|
||||||
|
The destination reader uses `readDeadline` to periodically wake up and check for context cancellation. Timeouts do not terminate the session.
|
||||||
|
|
||||||
|
See [`NewUDPClient()`](udp_client.go:27) and [`(*UDPServer).handleDTLSConnection()`](udp_server.go:89).
|
||||||
|
|
||||||
|
## Connection Management
|
||||||
|
|
||||||
|
Both `TCPServer` and `UDPServer` create a dedicated destination connection per incoming stream session and close it when the session ends (no destination connection reuse).
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
| Error | Description |
|
||||||
|
| --------------------- | ----------------------------------------------- |
|
||||||
|
| `ErrInvalidHeader` | Header validation failed (version or checksum). |
|
||||||
|
| `ErrCloseImmediately` | Health check probe - server closed immediately. |
|
||||||
|
|
||||||
|
Errors from connection creation are propagated to the caller.
|
||||||
|
|
||||||
|
See [`header.go`](header.go:23).
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
This package is used by the agent to provide stream tunneling capabilities. See the parent [`agent`](../README.md) package for integration details with the GoDoxy server.
|
||||||
|
|
||||||
|
### Certificate Requirements
|
||||||
|
|
||||||
|
Both TCP and UDP servers require:
|
||||||
|
|
||||||
|
- CA certificate for client verification
|
||||||
|
- Server certificate for TLS/DTLS termination
|
||||||
|
|
||||||
|
Both clients require:
|
||||||
|
|
||||||
|
- CA certificate for server verification
|
||||||
|
- Client certificate for mTLS authentication
|
||||||
|
|
||||||
|
### ALPN Protocol
|
||||||
|
|
||||||
|
The `StreamALPN` constant (`"godoxy-agent-stream/1"`) is used to multiplex stream tunnel traffic and HTTPS API traffic on the same port. Connections negotiating this ALPN are routed to the stream handler.
|
||||||
24
agent/pkg/agent/stream/common.go
Normal file
24
agent/pkg/agent/stream/common.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package stream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pion/dtls/v3"
|
||||||
|
"github.com/yusing/goutils/synk"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dialTimeout = 10 * time.Second
|
||||||
|
readDeadline = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// StreamALPN is the TLS ALPN protocol id used to multiplex the TCP stream tunnel
|
||||||
|
// and the HTTPS API on the same TCP port.
|
||||||
|
//
|
||||||
|
// When a client negotiates this ALPN, the agent will route the connection to the
|
||||||
|
// stream tunnel handler instead of the HTTP handler.
|
||||||
|
const StreamALPN = "godoxy-agent-stream/1"
|
||||||
|
|
||||||
|
var dTLSCipherSuites = []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}
|
||||||
|
|
||||||
|
var sizedPool = synk.GetSizedBytesPool()
|
||||||
117
agent/pkg/agent/stream/header.go
Normal file
117
agent/pkg/agent/stream/header.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package stream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"hash/crc32"
|
||||||
|
"reflect"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
versionSize = 8
|
||||||
|
hostSize = 255
|
||||||
|
portSize = 5
|
||||||
|
flagSize = 1
|
||||||
|
checksumSize = 4 // crc32 checksum
|
||||||
|
|
||||||
|
headerSize = versionSize + 1 + hostSize + 1 + portSize + flagSize + checksumSize
|
||||||
|
)
|
||||||
|
|
||||||
|
var version = [versionSize]byte{'0', '.', '1', '.', '0', 0, 0, 0}
|
||||||
|
|
||||||
|
var ErrInvalidHeader = errors.New("invalid header")
|
||||||
|
var ErrCloseImmediately = errors.New("close immediately")
|
||||||
|
|
||||||
|
type FlagType uint8
|
||||||
|
|
||||||
|
const FlagCloseImmediately FlagType = 1 << iota
|
||||||
|
|
||||||
|
type StreamRequestHeader struct {
|
||||||
|
Version [versionSize]byte
|
||||||
|
|
||||||
|
HostLength byte
|
||||||
|
Host [hostSize]byte
|
||||||
|
|
||||||
|
PortLength byte
|
||||||
|
Port [portSize]byte
|
||||||
|
|
||||||
|
Flag FlagType
|
||||||
|
Checksum [checksumSize]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if headerSize != reflect.TypeFor[StreamRequestHeader]().Size() {
|
||||||
|
panic("headerSize does not match the size of StreamRequestHeader")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStreamRequestHeader(host, port string) (*StreamRequestHeader, error) {
|
||||||
|
if len(host) > hostSize {
|
||||||
|
return nil, fmt.Errorf("host is too long: max %d characters, got %d", hostSize, len(host))
|
||||||
|
}
|
||||||
|
if len(port) > portSize {
|
||||||
|
return nil, fmt.Errorf("port is too long: max %d characters, got %d", portSize, len(port))
|
||||||
|
}
|
||||||
|
header := &StreamRequestHeader{}
|
||||||
|
copy(header.Version[:], version[:])
|
||||||
|
header.HostLength = byte(len(host))
|
||||||
|
copy(header.Host[:], host)
|
||||||
|
header.PortLength = byte(len(port))
|
||||||
|
copy(header.Port[:], port)
|
||||||
|
header.updateChecksum()
|
||||||
|
return header, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStreamHealthCheckHeader() *StreamRequestHeader {
|
||||||
|
header := &StreamRequestHeader{}
|
||||||
|
copy(header.Version[:], version[:])
|
||||||
|
header.Flag |= FlagCloseImmediately
|
||||||
|
header.updateChecksum()
|
||||||
|
return header
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToHeader converts header byte array to a copy of itself as a StreamRequestHeader.
|
||||||
|
func ToHeader(buf *[headerSize]byte) StreamRequestHeader {
|
||||||
|
return *(*StreamRequestHeader)(unsafe.Pointer(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *StreamRequestHeader) GetHostPort() (string, string) {
|
||||||
|
return string(h.Host[:h.HostLength]), string(h.Port[:h.PortLength])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *StreamRequestHeader) Validate() bool {
|
||||||
|
if h.Version != version {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if h.HostLength > hostSize {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if h.PortLength > portSize {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return h.validateChecksum()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *StreamRequestHeader) ShouldCloseImmediately() bool {
|
||||||
|
return h.Flag&FlagCloseImmediately != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *StreamRequestHeader) updateChecksum() {
|
||||||
|
checksum := crc32.ChecksumIEEE(h.BytesWithoutChecksum())
|
||||||
|
binary.BigEndian.PutUint32(h.Checksum[:], checksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *StreamRequestHeader) validateChecksum() bool {
|
||||||
|
checksum := crc32.ChecksumIEEE(h.BytesWithoutChecksum())
|
||||||
|
return checksum == binary.BigEndian.Uint32(h.Checksum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *StreamRequestHeader) BytesWithoutChecksum() []byte {
|
||||||
|
return (*[headerSize - checksumSize]byte)(unsafe.Pointer(h))[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *StreamRequestHeader) Bytes() []byte {
|
||||||
|
return (*[headerSize]byte)(unsafe.Pointer(h))[:]
|
||||||
|
}
|
||||||
26
agent/pkg/agent/stream/payload_test.go
Normal file
26
agent/pkg/agent/stream/payload_test.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package stream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStreamRequestHeader_RoundTripAndChecksum(t *testing.T) {
|
||||||
|
h, err := NewStreamRequestHeader("example.com", "443")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewStreamRequestHeader: %v", err)
|
||||||
|
}
|
||||||
|
if !h.Validate() {
|
||||||
|
t.Fatalf("expected header to validate")
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf [headerSize]byte
|
||||||
|
copy(buf[:], h.Bytes())
|
||||||
|
h2 := ToHeader(&buf)
|
||||||
|
if !h2.Validate() {
|
||||||
|
t.Fatalf("expected round-tripped header to validate")
|
||||||
|
}
|
||||||
|
host, port := h2.GetHostPort()
|
||||||
|
if host != "example.com" || port != "443" {
|
||||||
|
t.Fatalf("unexpected host/port: %q:%q", host, port)
|
||||||
|
}
|
||||||
|
}
|
||||||
122
agent/pkg/agent/stream/tcp_client.go
Normal file
122
agent/pkg/agent/stream/tcp_client.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package stream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TCPClient struct {
|
||||||
|
conn net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTCPClient creates a new TCP client for the agent.
|
||||||
|
//
|
||||||
|
// It will establish a TLS connection and send a stream request header to the server.
|
||||||
|
//
|
||||||
|
// It returns an error if
|
||||||
|
// - the target address is invalid
|
||||||
|
// - the stream request header is invalid
|
||||||
|
// - the TLS configuration is invalid
|
||||||
|
// - the TLS connection fails
|
||||||
|
// - the stream request header is not sent
|
||||||
|
func NewTCPClient(serverAddr, targetAddress string, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||||
|
host, port, err := net.SplitHostPort(targetAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err := NewStreamRequestHeader(host, port)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return newTCPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TCPHealthCheck(serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
|
||||||
|
header := NewStreamHealthCheckHeader()
|
||||||
|
|
||||||
|
conn, err := newTCPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTCPClientWIthHeader(serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||||
|
// Setup TLS configuration
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
caCertPool.AddCert(caCert)
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{*clientCert},
|
||||||
|
RootCAs: caCertPool,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
NextProtos: []string{StreamALPN},
|
||||||
|
ServerName: common.CertsDNSName,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establish TLS connection
|
||||||
|
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: dialTimeout}, "tcp", serverAddr, tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Send the stream header once as a handshake.
|
||||||
|
if _, err := conn.Write(header.Bytes()); err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TCPClient{
|
||||||
|
conn: conn,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TCPClient) Read(p []byte) (n int, err error) {
|
||||||
|
return c.conn.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TCPClient) Write(p []byte) (n int, err error) {
|
||||||
|
return c.conn.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TCPClient) LocalAddr() net.Addr {
|
||||||
|
return c.conn.LocalAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TCPClient) RemoteAddr() net.Addr {
|
||||||
|
return c.conn.RemoteAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TCPClient) SetDeadline(t time.Time) error {
|
||||||
|
return c.conn.SetDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TCPClient) SetReadDeadline(t time.Time) error {
|
||||||
|
return c.conn.SetReadDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TCPClient) SetWriteDeadline(t time.Time) error {
|
||||||
|
return c.conn.SetWriteDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TCPClient) Close() error {
|
||||||
|
return c.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectionState exposes the underlying TLS connection state when the client is
|
||||||
|
// backed by *tls.Conn.
|
||||||
|
//
|
||||||
|
// This is primarily used by tests and diagnostics.
|
||||||
|
func (c *TCPClient) ConnectionState() tls.ConnectionState {
|
||||||
|
if tc, ok := c.conn.(*tls.Conn); ok {
|
||||||
|
return tc.ConnectionState()
|
||||||
|
}
|
||||||
|
return tls.ConnectionState{}
|
||||||
|
}
|
||||||
179
agent/pkg/agent/stream/tcp_server.go
Normal file
179
agent/pkg/agent/stream/tcp_server.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package stream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
ioutils "github.com/yusing/goutils/io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TCPServer struct {
|
||||||
|
ctx context.Context
|
||||||
|
listener net.Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTCPServerHandler creates a TCP stream server that can serve already-accepted
|
||||||
|
// connections (e.g. handed off by an ALPN multiplexer).
|
||||||
|
//
|
||||||
|
// This variant does not require a listener. Use TCPServer.ServeConn to handle
|
||||||
|
// each incoming stream connection.
|
||||||
|
func NewTCPServerHandler(ctx context.Context) *TCPServer {
|
||||||
|
s := &TCPServer{ctx: ctx}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTCPServerFromListener creates a TCP stream server from an already-prepared
|
||||||
|
// listener.
|
||||||
|
//
|
||||||
|
// The listener is expected to yield connections that are already secured (e.g.
|
||||||
|
// a TLS/mTLS listener, or pre-handshaked *tls.Conn). This is used when the agent
|
||||||
|
// multiplexes HTTPS and stream-tunnel traffic on the same port.
|
||||||
|
func NewTCPServerFromListener(ctx context.Context, listener net.Listener) *TCPServer {
|
||||||
|
s := &TCPServer{
|
||||||
|
ctx: ctx,
|
||||||
|
listener: listener,
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTCPServer(ctx context.Context, listener *net.TCPListener, caCert *x509.Certificate, serverCert *tls.Certificate) *TCPServer {
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
caCertPool.AddCert(caCert)
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{*serverCert},
|
||||||
|
ClientCAs: caCertPool,
|
||||||
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
NextProtos: []string{StreamALPN},
|
||||||
|
}
|
||||||
|
|
||||||
|
tcpListener := tls.NewListener(listener, tlsConfig)
|
||||||
|
return NewTCPServerFromListener(ctx, tcpListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TCPServer) Start() error {
|
||||||
|
if s.listener == nil {
|
||||||
|
return net.ErrClosed
|
||||||
|
}
|
||||||
|
context.AfterFunc(s.ctx, func() {
|
||||||
|
_ = s.listener.Close()
|
||||||
|
})
|
||||||
|
for {
|
||||||
|
conn, err := s.listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, net.ErrClosed) && s.ctx.Err() != nil {
|
||||||
|
return s.ctx.Err()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go s.handle(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeConn serves a single stream connection.
|
||||||
|
//
|
||||||
|
// The provided connection is expected to be already secured (TLS/mTLS) and to
|
||||||
|
// speak the stream protocol (i.e. the client will send the stream header first).
|
||||||
|
//
|
||||||
|
// This method blocks until the stream finishes.
|
||||||
|
func (s *TCPServer) ServeConn(conn net.Conn) {
|
||||||
|
s.handle(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TCPServer) Addr() net.Addr {
|
||||||
|
if s.listener == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.listener.Addr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TCPServer) Close() error {
|
||||||
|
if s.listener == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.listener.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TCPServer) logger(clientConn net.Conn) *zerolog.Logger {
|
||||||
|
ev := log.With().Str("protocol", "tcp").
|
||||||
|
Str("remote", clientConn.RemoteAddr().String())
|
||||||
|
if s.listener != nil {
|
||||||
|
ev = ev.Str("addr", s.listener.Addr().String())
|
||||||
|
}
|
||||||
|
l := ev.Logger()
|
||||||
|
return &l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TCPServer) loggerWithDst(dstConn net.Conn, clientConn net.Conn) *zerolog.Logger {
|
||||||
|
ev := log.With().Str("protocol", "tcp").
|
||||||
|
Str("remote", clientConn.RemoteAddr().String()).
|
||||||
|
Str("dst", dstConn.RemoteAddr().String())
|
||||||
|
if s.listener != nil {
|
||||||
|
ev = ev.Str("addr", s.listener.Addr().String())
|
||||||
|
}
|
||||||
|
l := ev.Logger()
|
||||||
|
return &l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TCPServer) handle(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
dst, err := s.redirect(conn)
|
||||||
|
if err != nil {
|
||||||
|
// Health check probe: close connection
|
||||||
|
if errors.Is(err, ErrCloseImmediately) {
|
||||||
|
s.logger(conn).Info().Msg("Health check received")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.logger(conn).Err(err).Msg("failed to redirect connection")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer dst.Close()
|
||||||
|
pipe := ioutils.NewBidirectionalPipe(s.ctx, conn, dst)
|
||||||
|
err = pipe.Start()
|
||||||
|
if err != nil {
|
||||||
|
s.loggerWithDst(dst, conn).Err(err).Msg("failed to start bidirectional pipe")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TCPServer) redirect(conn net.Conn) (net.Conn, error) {
|
||||||
|
// Read the stream header once as a handshake.
|
||||||
|
var headerBuf [headerSize]byte
|
||||||
|
_ = conn.SetReadDeadline(time.Now().Add(dialTimeout))
|
||||||
|
if _, err := io.ReadFull(conn, headerBuf[:]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = conn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
|
header := ToHeader(&headerBuf)
|
||||||
|
if !header.Validate() {
|
||||||
|
return nil, ErrInvalidHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health check: close immediately if FlagCloseImmediately is set
|
||||||
|
if header.ShouldCloseImmediately() {
|
||||||
|
return nil, ErrCloseImmediately
|
||||||
|
}
|
||||||
|
|
||||||
|
// get destination connection
|
||||||
|
host, port := header.GetHostPort()
|
||||||
|
return s.createDestConnection(host, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TCPServer) createDestConnection(host, port string) (net.Conn, error) {
|
||||||
|
addr := net.JoinHostPort(host, port)
|
||||||
|
conn, err := net.DialTimeout("tcp", addr, dialTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
26
agent/pkg/agent/stream/tests/healthcheck_test.go
Normal file
26
agent/pkg/agent/stream/tests/healthcheck_test.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package stream_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTCPHealthCheck(t *testing.T) {
|
||||||
|
certs := genTestCerts(t)
|
||||||
|
|
||||||
|
srv := startTCPServer(t, certs)
|
||||||
|
|
||||||
|
err := stream.TCPHealthCheck(srv.Addr.String(), certs.CaCert, certs.ClientCert)
|
||||||
|
require.NoError(t, err, "health check")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDPHealthCheck(t *testing.T) {
|
||||||
|
certs := genTestCerts(t)
|
||||||
|
|
||||||
|
srv := startUDPServer(t, certs)
|
||||||
|
|
||||||
|
err := stream.UDPHealthCheck(srv.Addr.String(), certs.CaCert, certs.ClientCert)
|
||||||
|
require.NoError(t, err, "health check")
|
||||||
|
}
|
||||||
94
agent/pkg/agent/stream/tests/mux_test.go
Normal file
94
agent/pkg/agent/stream/tests/mux_test.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package stream_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTLSALPNMux_HTTPAndStreamShareOnePort(t *testing.T) {
|
||||||
|
certs := genTestCerts(t)
|
||||||
|
|
||||||
|
baseLn, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0})
|
||||||
|
require.NoError(t, err, "listen tcp")
|
||||||
|
defer baseLn.Close()
|
||||||
|
baseAddr := baseLn.Addr().String()
|
||||||
|
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
caCertPool.AddCert(certs.CaCert)
|
||||||
|
|
||||||
|
serverTLS := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{*certs.SrvCert},
|
||||||
|
ClientCAs: caCertPool,
|
||||||
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
NextProtos: []string{"http/1.1", stream.StreamALPN},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
streamSrv := stream.NewTCPServerHandler(ctx)
|
||||||
|
defer func() { _ = streamSrv.Close() }()
|
||||||
|
|
||||||
|
tlsLn := tls.NewListener(baseLn, serverTLS)
|
||||||
|
defer func() { _ = tlsLn.Close() }()
|
||||||
|
|
||||||
|
// HTTP server
|
||||||
|
httpSrv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
_, _ = w.Write([]byte("ok"))
|
||||||
|
}),
|
||||||
|
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){
|
||||||
|
stream.StreamALPN: func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
|
||||||
|
streamSrv.ServeConn(conn)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
go func() { _ = httpSrv.Serve(tlsLn) }()
|
||||||
|
defer func() { _ = httpSrv.Close() }()
|
||||||
|
|
||||||
|
// Stream destination
|
||||||
|
dstAddr, closeDst := startTCPEcho(t)
|
||||||
|
defer closeDst()
|
||||||
|
|
||||||
|
// HTTP client over the same port
|
||||||
|
clientTLS := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{*certs.ClientCert},
|
||||||
|
RootCAs: caCertPool,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
NextProtos: []string{"http/1.1"},
|
||||||
|
ServerName: common.CertsDNSName,
|
||||||
|
}
|
||||||
|
hc, err := tls.Dial("tcp", baseAddr, clientTLS)
|
||||||
|
require.NoError(t, err, "dial https")
|
||||||
|
defer hc.Close()
|
||||||
|
_ = hc.SetDeadline(time.Now().Add(2 * time.Second))
|
||||||
|
_, err = hc.Write([]byte("GET / HTTP/1.1\r\nHost: godoxy-agent\r\n\r\n"))
|
||||||
|
require.NoError(t, err, "write http request")
|
||||||
|
r := bufio.NewReader(hc)
|
||||||
|
statusLine, err := r.ReadString('\n')
|
||||||
|
require.NoError(t, err, "read status line")
|
||||||
|
require.Contains(t, statusLine, "200", "expected 200")
|
||||||
|
|
||||||
|
// Stream client over the same port
|
||||||
|
client := NewTCPClient(t, baseAddr, dstAddr, certs)
|
||||||
|
defer client.Close()
|
||||||
|
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
|
||||||
|
msg := []byte("ping over mux")
|
||||||
|
_, err = client.Write(msg)
|
||||||
|
require.NoError(t, err, "write stream payload")
|
||||||
|
buf := make([]byte, len(msg))
|
||||||
|
_, err = io.ReadFull(client, buf)
|
||||||
|
require.NoError(t, err, "read stream payload")
|
||||||
|
require.Equal(t, msg, buf)
|
||||||
|
}
|
||||||
201
agent/pkg/agent/stream/tests/server_flow_test.go
Normal file
201
agent/pkg/agent/stream/tests/server_flow_test.go
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package stream_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pion/dtls/v3"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTCPServer_FullFlow(t *testing.T) {
|
||||||
|
certs := genTestCerts(t)
|
||||||
|
|
||||||
|
dstAddr, closeDst := startTCPEcho(t)
|
||||||
|
defer closeDst()
|
||||||
|
|
||||||
|
srv := startTCPServer(t, certs)
|
||||||
|
|
||||||
|
client := NewTCPClient(t, srv.Addr.String(), dstAddr, certs)
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Ensure ALPN is negotiated as expected (required for multiplexing).
|
||||||
|
withState, ok := client.(interface{ ConnectionState() tls.ConnectionState })
|
||||||
|
require.True(t, ok, "tcp client should expose TLS connection state")
|
||||||
|
require.Equal(t, stream.StreamALPN, withState.ConnectionState().NegotiatedProtocol)
|
||||||
|
|
||||||
|
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
|
||||||
|
msg := []byte("ping over tcp")
|
||||||
|
_, err := client.Write(msg)
|
||||||
|
require.NoError(t, err, "write to client")
|
||||||
|
|
||||||
|
buf := make([]byte, len(msg))
|
||||||
|
_, err = io.ReadFull(client, buf)
|
||||||
|
require.NoError(t, err, "read from client")
|
||||||
|
require.Equal(t, string(msg), string(buf), "unexpected echo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTCPServer_ConcurrentConnections(t *testing.T) {
|
||||||
|
certs := genTestCerts(t)
|
||||||
|
|
||||||
|
dstAddr, closeDst := startTCPEcho(t)
|
||||||
|
defer closeDst()
|
||||||
|
|
||||||
|
srv := startTCPServer(t, certs)
|
||||||
|
|
||||||
|
const nClients = 25
|
||||||
|
|
||||||
|
errs := make(chan error, nClients)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(nClients)
|
||||||
|
|
||||||
|
for i := range nClients {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
client := NewTCPClient(t, srv.Addr.String(), dstAddr, certs)
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
|
||||||
|
msg := fmt.Appendf(nil, "ping over tcp %d", i)
|
||||||
|
if _, err := client.Write(msg); err != nil {
|
||||||
|
errs <- fmt.Errorf("write to client: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, len(msg))
|
||||||
|
if _, err := io.ReadFull(client, buf); err != nil {
|
||||||
|
errs <- fmt.Errorf("read from client: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if string(msg) != string(buf) {
|
||||||
|
errs <- fmt.Errorf("unexpected echo: got=%q want=%q", string(buf), string(msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(errs)
|
||||||
|
for err := range errs {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDPServer_RejectInvalidClient(t *testing.T) {
|
||||||
|
certs := genTestCerts(t)
|
||||||
|
|
||||||
|
// Generate a self-signed client cert that is NOT signed by the CA
|
||||||
|
_, _, invalidClientPEM, err := agent.NewAgent()
|
||||||
|
require.NoError(t, err, "generate invalid client certs")
|
||||||
|
invalidClientCert, err := invalidClientPEM.ToTLSCert()
|
||||||
|
require.NoError(t, err, "parse invalid client cert")
|
||||||
|
|
||||||
|
dstAddr, closeDst := startUDPEcho(t)
|
||||||
|
defer closeDst()
|
||||||
|
|
||||||
|
srv := startUDPServer(t, certs)
|
||||||
|
|
||||||
|
|
||||||
|
// Try to connect with a client cert from a different CA
|
||||||
|
_, err = stream.NewUDPClient(srv.Addr.String(), dstAddr, certs.CaCert, invalidClientCert)
|
||||||
|
require.Error(t, err, "expected error when connecting with client cert from different CA")
|
||||||
|
|
||||||
|
var handshakeErr *dtls.HandshakeError
|
||||||
|
require.ErrorAs(t, err, &handshakeErr, "expected handshake error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDPServer_RejectClientWithoutCert(t *testing.T) {
|
||||||
|
certs := genTestCerts(t)
|
||||||
|
|
||||||
|
dstAddr, closeDst := startUDPEcho(t)
|
||||||
|
defer closeDst()
|
||||||
|
|
||||||
|
srv := startUDPServer(t, certs)
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
// Try to connect without any client certificate
|
||||||
|
// Create a TLS cert without a private key to simulate no client cert
|
||||||
|
emptyCert := &tls.Certificate{}
|
||||||
|
_, err := stream.NewUDPClient(srv.Addr.String(), dstAddr, certs.CaCert, emptyCert)
|
||||||
|
require.Error(t, err, "expected error when connecting without client cert")
|
||||||
|
|
||||||
|
require.ErrorContains(t, err, "no certificate provided", "expected no cert error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDPServer_FullFlow(t *testing.T) {
|
||||||
|
certs := genTestCerts(t)
|
||||||
|
|
||||||
|
dstAddr, closeDst := startUDPEcho(t)
|
||||||
|
defer closeDst()
|
||||||
|
|
||||||
|
srv := startUDPServer(t, certs)
|
||||||
|
|
||||||
|
client := NewUDPClient(t, srv.Addr.String(), dstAddr, certs)
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
|
||||||
|
msg := []byte("ping over udp")
|
||||||
|
_, err := client.Write(msg)
|
||||||
|
require.NoError(t, err, "write to client")
|
||||||
|
|
||||||
|
buf := make([]byte, 2048)
|
||||||
|
n, err := client.Read(buf)
|
||||||
|
require.NoError(t, err, "read from client")
|
||||||
|
require.Equal(t, string(msg), string(buf[:n]), "unexpected echo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDPServer_ConcurrentConnections(t *testing.T) {
|
||||||
|
certs := genTestCerts(t)
|
||||||
|
|
||||||
|
dstAddr, closeDst := startUDPEcho(t)
|
||||||
|
defer closeDst()
|
||||||
|
|
||||||
|
srv := startUDPServer(t, certs)
|
||||||
|
|
||||||
|
const nClients = 25
|
||||||
|
|
||||||
|
errs := make(chan error, nClients)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(nClients)
|
||||||
|
|
||||||
|
for i := range nClients {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
client := NewUDPClient(t, srv.Addr.String(), dstAddr, certs)
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
_ = client.SetDeadline(time.Now().Add(5 * time.Second))
|
||||||
|
msg := fmt.Appendf(nil, "ping over udp %d", i)
|
||||||
|
if _, err := client.Write(msg); err != nil {
|
||||||
|
errs <- fmt.Errorf("write to client: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 2048)
|
||||||
|
n, err := client.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
errs <- fmt.Errorf("read from client: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if string(msg) != string(buf[:n]) {
|
||||||
|
errs <- fmt.Errorf("unexpected echo: got=%q want=%q", string(buf[:n]), string(msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(errs)
|
||||||
|
for err := range errs {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
177
agent/pkg/agent/stream/tests/testutils_test.go
Normal file
177
agent/pkg/agent/stream/tests/testutils_test.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package stream_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pion/transport/v3/udp"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CertBundle holds all certificates needed for testing.
|
||||||
|
type CertBundle struct {
|
||||||
|
CaCert *x509.Certificate
|
||||||
|
SrvCert *tls.Certificate
|
||||||
|
ClientCert *tls.Certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
// genTestCerts generates certificates for testing and returns them as a CertBundle.
|
||||||
|
func genTestCerts(t *testing.T) CertBundle {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
caPEM, srvPEM, clientPEM, err := agent.NewAgent()
|
||||||
|
require.NoError(t, err, "generate agent certs")
|
||||||
|
|
||||||
|
caCert, err := caPEM.ToTLSCert()
|
||||||
|
require.NoError(t, err, "parse CA cert")
|
||||||
|
srvCert, err := srvPEM.ToTLSCert()
|
||||||
|
require.NoError(t, err, "parse server cert")
|
||||||
|
clientCert, err := clientPEM.ToTLSCert()
|
||||||
|
require.NoError(t, err, "parse client cert")
|
||||||
|
|
||||||
|
return CertBundle{
|
||||||
|
CaCert: caCert.Leaf,
|
||||||
|
SrvCert: srvCert,
|
||||||
|
ClientCert: clientCert,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startTCPEcho starts a TCP echo server and returns its address and close function.
|
||||||
|
func startTCPEcho(t *testing.T) (addr string, closeFn func()) {
|
||||||
|
t.Helper()
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err, "listen tcp")
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
for {
|
||||||
|
c, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
_, _ = io.Copy(conn, conn)
|
||||||
|
}(c)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ln.Addr().String(), func() {
|
||||||
|
_ = ln.Close()
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startUDPEcho starts a UDP echo server and returns its address and close function.
|
||||||
|
func startUDPEcho(t *testing.T) (addr string, closeFn func()) {
|
||||||
|
t.Helper()
|
||||||
|
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err, "listen udp")
|
||||||
|
uc := pc.(*net.UDPConn)
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
buf := make([]byte, 65535)
|
||||||
|
for {
|
||||||
|
n, raddr, err := uc.ReadFromUDP(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = uc.WriteToUDP(buf[:n], raddr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return uc.LocalAddr().String(), func() {
|
||||||
|
_ = uc.Close()
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServer wraps a server with its startup goroutine for cleanup.
|
||||||
|
type TestServer struct {
|
||||||
|
Server interface{ Close() error }
|
||||||
|
Addr net.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// startTCPServer starts a TCP server and returns a TestServer for cleanup.
|
||||||
|
func startTCPServer(t *testing.T, certs CertBundle) TestServer {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
tcpLn, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0})
|
||||||
|
require.NoError(t, err, "listen tcp")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
|
|
||||||
|
srv := stream.NewTCPServer(ctx, tcpLn, certs.CaCert, certs.SrvCert)
|
||||||
|
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
go func() { errCh <- srv.Start() }()
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
cancel()
|
||||||
|
_ = srv.Close()
|
||||||
|
err := <-errCh
|
||||||
|
if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, net.ErrClosed) {
|
||||||
|
t.Logf("tcp server exit: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return TestServer{
|
||||||
|
Server: srv,
|
||||||
|
Addr: srv.Addr(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startUDPServer starts a UDP server and returns a TestServer for cleanup.
|
||||||
|
func startUDPServer(t *testing.T, certs CertBundle) TestServer {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
|
|
||||||
|
srv := stream.NewUDPServer(ctx, "udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}, certs.CaCert, certs.SrvCert)
|
||||||
|
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
go func() { errCh <- srv.Start() }()
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
cancel()
|
||||||
|
_ = srv.Close()
|
||||||
|
err := <-errCh
|
||||||
|
if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, net.ErrClosed) && !errors.Is(err, udp.ErrClosedListener) {
|
||||||
|
t.Logf("udp server exit: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return TestServer{
|
||||||
|
Server: srv,
|
||||||
|
Addr: srv.Addr(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTCPClient creates a TCP client connected to the server with test certificates.
|
||||||
|
func NewTCPClient(t *testing.T, serverAddr, targetAddress string, certs CertBundle) net.Conn {
|
||||||
|
t.Helper()
|
||||||
|
client, err := stream.NewTCPClient(serverAddr, targetAddress, certs.CaCert, certs.ClientCert)
|
||||||
|
require.NoError(t, err, "create tcp client")
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUDPClient creates a UDP client connected to the server with test certificates.
|
||||||
|
func NewUDPClient(t *testing.T, serverAddr, targetAddress string, certs CertBundle) net.Conn {
|
||||||
|
t.Helper()
|
||||||
|
client, err := stream.NewUDPClient(serverAddr, targetAddress, certs.CaCert, certs.ClientCert)
|
||||||
|
require.NoError(t, err, "create udp client")
|
||||||
|
return client
|
||||||
|
}
|
||||||
118
agent/pkg/agent/stream/udp_client.go
Normal file
118
agent/pkg/agent/stream/udp_client.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package stream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pion/dtls/v3"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UDPClient struct {
|
||||||
|
conn net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUDPClient creates a new UDP client for the agent.
|
||||||
|
//
|
||||||
|
// It will establish a DTLS connection and send a stream request header to the server.
|
||||||
|
//
|
||||||
|
// It returns an error if
|
||||||
|
// - the target address is invalid
|
||||||
|
// - the stream request header is invalid
|
||||||
|
// - the DTLS configuration is invalid
|
||||||
|
// - the DTLS connection fails
|
||||||
|
// - the stream request header is not sent
|
||||||
|
func NewUDPClient(serverAddr, targetAddress string, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||||
|
host, port, err := net.SplitHostPort(targetAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err := NewStreamRequestHeader(host, port)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return newUDPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUDPClientWIthHeader(serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||||
|
// Setup DTLS configuration
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
caCertPool.AddCert(caCert)
|
||||||
|
|
||||||
|
dtlsConfig := &dtls.Config{
|
||||||
|
Certificates: []tls.Certificate{*clientCert},
|
||||||
|
RootCAs: caCertPool,
|
||||||
|
InsecureSkipVerify: false,
|
||||||
|
ExtendedMasterSecret: dtls.RequireExtendedMasterSecret,
|
||||||
|
ServerName: common.CertsDNSName,
|
||||||
|
CipherSuites: dTLSCipherSuites,
|
||||||
|
}
|
||||||
|
|
||||||
|
raddr, err := net.ResolveUDPAddr("udp", serverAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establish DTLS connection
|
||||||
|
conn, err := dtls.Dial("udp", raddr, dtlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Send the stream header once as a handshake.
|
||||||
|
if _, err := conn.Write(header.Bytes()); err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UDPClient{
|
||||||
|
conn: conn,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UDPHealthCheck(serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
|
||||||
|
header := NewStreamHealthCheckHeader()
|
||||||
|
|
||||||
|
conn, err := newUDPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UDPClient) Read(p []byte) (n int, err error) {
|
||||||
|
return c.conn.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UDPClient) Write(p []byte) (n int, err error) {
|
||||||
|
return c.conn.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UDPClient) LocalAddr() net.Addr {
|
||||||
|
return c.conn.LocalAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UDPClient) RemoteAddr() net.Addr {
|
||||||
|
return c.conn.RemoteAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UDPClient) SetDeadline(t time.Time) error {
|
||||||
|
return c.conn.SetDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UDPClient) SetReadDeadline(t time.Time) error {
|
||||||
|
return c.conn.SetReadDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UDPClient) SetWriteDeadline(t time.Time) error {
|
||||||
|
return c.conn.SetWriteDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UDPClient) Close() error {
|
||||||
|
return c.conn.Close()
|
||||||
|
}
|
||||||
208
agent/pkg/agent/stream/udp_server.go
Normal file
208
agent/pkg/agent/stream/udp_server.go
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
package stream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pion/dtls/v3"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UDPServer struct {
|
||||||
|
ctx context.Context
|
||||||
|
network string
|
||||||
|
laddr *net.UDPAddr
|
||||||
|
listener net.Listener
|
||||||
|
|
||||||
|
dtlsConfig *dtls.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUDPServer(ctx context.Context, network string, laddr *net.UDPAddr, caCert *x509.Certificate, serverCert *tls.Certificate) *UDPServer {
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
caCertPool.AddCert(caCert)
|
||||||
|
|
||||||
|
dtlsConfig := &dtls.Config{
|
||||||
|
Certificates: []tls.Certificate{*serverCert},
|
||||||
|
ClientCAs: caCertPool,
|
||||||
|
ClientAuth: dtls.RequireAndVerifyClientCert,
|
||||||
|
ExtendedMasterSecret: dtls.RequireExtendedMasterSecret,
|
||||||
|
CipherSuites: dTLSCipherSuites,
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &UDPServer{
|
||||||
|
ctx: ctx,
|
||||||
|
network: network,
|
||||||
|
laddr: laddr,
|
||||||
|
dtlsConfig: dtlsConfig,
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPServer) Start() error {
|
||||||
|
listener, err := dtls.Listen(s.network, s.laddr, s.dtlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.listener = listener
|
||||||
|
|
||||||
|
context.AfterFunc(s.ctx, func() {
|
||||||
|
_ = s.listener.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := s.listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
// Expected error when context cancelled
|
||||||
|
if errors.Is(err, net.ErrClosed) && s.ctx.Err() != nil {
|
||||||
|
return s.ctx.Err()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go s.handleDTLSConnection(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPServer) Addr() net.Addr {
|
||||||
|
if s.listener != nil {
|
||||||
|
return s.listener.Addr()
|
||||||
|
}
|
||||||
|
return s.laddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPServer) Close() error {
|
||||||
|
if s.listener != nil {
|
||||||
|
return s.listener.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPServer) logger(clientConn net.Conn) *zerolog.Logger {
|
||||||
|
l := log.With().Str("protocol", "udp").
|
||||||
|
Str("addr", s.Addr().String()).
|
||||||
|
Str("remote", clientConn.RemoteAddr().String()).Logger()
|
||||||
|
return &l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPServer) loggerWithDst(clientConn net.Conn, dstConn *net.UDPConn) *zerolog.Logger {
|
||||||
|
l := log.With().Str("protocol", "udp").
|
||||||
|
Str("addr", s.Addr().String()).
|
||||||
|
Str("remote", clientConn.RemoteAddr().String()).
|
||||||
|
Str("dst", dstConn.RemoteAddr().String()).Logger()
|
||||||
|
return &l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPServer) handleDTLSConnection(clientConn net.Conn) {
|
||||||
|
defer clientConn.Close()
|
||||||
|
|
||||||
|
// Read the stream header once as a handshake.
|
||||||
|
var headerBuf [headerSize]byte
|
||||||
|
_ = clientConn.SetReadDeadline(time.Now().Add(dialTimeout))
|
||||||
|
if _, err := io.ReadFull(clientConn, headerBuf[:]); err != nil {
|
||||||
|
s.logger(clientConn).Err(err).Msg("failed to read stream header")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = clientConn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
|
header := ToHeader(&headerBuf)
|
||||||
|
if !header.Validate() {
|
||||||
|
s.logger(clientConn).Error().Bytes("header", headerBuf[:]).Msg("invalid stream header received")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health check probe: close connection
|
||||||
|
if header.ShouldCloseImmediately() {
|
||||||
|
s.logger(clientConn).Info().Msg("Health check received")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
host, port := header.GetHostPort()
|
||||||
|
dstConn, err := s.createDestConnection(host, port)
|
||||||
|
if err != nil {
|
||||||
|
s.logger(clientConn).Err(err).Msg("failed to get or create destination connection")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer dstConn.Close()
|
||||||
|
|
||||||
|
go s.forwardFromDestination(dstConn, clientConn)
|
||||||
|
|
||||||
|
buf := sizedPool.GetSized(65535)
|
||||||
|
defer sizedPool.Put(buf)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
n, err := clientConn.Read(buf)
|
||||||
|
// Per net.Conn contract, Read may return (n > 0, err == io.EOF).
|
||||||
|
// Always forward any bytes we got before acting on the error.
|
||||||
|
if n > 0 {
|
||||||
|
if _, werr := dstConn.Write(buf[:n]); werr != nil {
|
||||||
|
s.logger(clientConn).Err(werr).Msgf("failed to write %d bytes to destination", n)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// Expected shutdown paths.
|
||||||
|
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.logger(clientConn).Err(err).Msg("failed to read from client")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPServer) createDestConnection(host, port string) (*net.UDPConn, error) {
|
||||||
|
addr := net.JoinHostPort(host, port)
|
||||||
|
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dstConn, err := net.DialUDP("udp", nil, udpAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dstConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPServer) forwardFromDestination(dstConn *net.UDPConn, clientConn net.Conn) {
|
||||||
|
buffer := sizedPool.GetSized(65535)
|
||||||
|
defer sizedPool.Put(buffer)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
_ = dstConn.SetReadDeadline(time.Now().Add(readDeadline))
|
||||||
|
n, err := dstConn.Read(buffer)
|
||||||
|
if err != nil {
|
||||||
|
// The destination socket can be closed when the client disconnects (e.g. during
|
||||||
|
// the stream support probe in AgentConfig.StartWithCerts). Treat that as a
|
||||||
|
// normal exit and avoid noisy logs.
|
||||||
|
if errors.Is(err, net.ErrClosed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.loggerWithDst(clientConn, dstConn).Err(err).Msg("failed to read from destination")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := clientConn.Write(buffer[:n]); err != nil {
|
||||||
|
s.loggerWithDst(clientConn, dstConn).Err(err).Msgf("failed to write %d bytes to client", n)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
agent/pkg/agent/templates/agent.compose.yml.tmpl
Normal file
67
agent/pkg/agent/templates/agent.compose.yml.tmpl
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
services:
|
||||||
|
agent:
|
||||||
|
image: "{{.Image}}"
|
||||||
|
container_name: godoxy-agent
|
||||||
|
restart: always
|
||||||
|
{{ if eq .ContainerRuntime "podman" -}}
|
||||||
|
ports:
|
||||||
|
- "{{.Port}}:{{.Port}}/tcp"
|
||||||
|
- "{{.Port}}:{{.Port}}/udp"
|
||||||
|
{{ else -}}
|
||||||
|
network_mode: host # do not change this
|
||||||
|
{{ end -}}
|
||||||
|
environment:
|
||||||
|
{{ if eq .ContainerRuntime "nerdctl" -}}
|
||||||
|
DOCKER_SOCKET: "/var/run/containerd/containerd.sock"
|
||||||
|
RUNTIME: "nerdctl"
|
||||||
|
{{ else if eq .ContainerRuntime "podman" -}}
|
||||||
|
DOCKER_SOCKET: "/var/run/podman/podman.sock"
|
||||||
|
RUNTIME: "podman"
|
||||||
|
{{ else -}}
|
||||||
|
DOCKER_SOCKET: "/var/run/docker.sock"
|
||||||
|
RUNTIME: "docker"
|
||||||
|
{{ end -}}
|
||||||
|
AGENT_NAME: "{{.Name}}"
|
||||||
|
AGENT_PORT: "{{.Port}}"
|
||||||
|
AGENT_CA_CERT: "{{.CACert}}"
|
||||||
|
AGENT_SSL_CERT: "{{.SSLCert}}"
|
||||||
|
# use agent as a docker socket proxy: [host]:port
|
||||||
|
# set LISTEN_ADDR to enable (e.g. 127.0.0.1:2375)
|
||||||
|
LISTEN_ADDR:
|
||||||
|
POST: false
|
||||||
|
ALLOW_RESTARTS: false
|
||||||
|
ALLOW_START: false
|
||||||
|
ALLOW_STOP: false
|
||||||
|
AUTH: false
|
||||||
|
BUILD: false
|
||||||
|
COMMIT: false
|
||||||
|
CONFIGS: false
|
||||||
|
CONTAINERS: false
|
||||||
|
DISTRIBUTION: false
|
||||||
|
EVENTS: true
|
||||||
|
EXEC: false
|
||||||
|
GRPC: false
|
||||||
|
IMAGES: false
|
||||||
|
INFO: false
|
||||||
|
NETWORKS: false
|
||||||
|
NODES: false
|
||||||
|
PING: true
|
||||||
|
PLUGINS: false
|
||||||
|
SECRETS: false
|
||||||
|
SERVICES: false
|
||||||
|
SESSION: false
|
||||||
|
SWARM: false
|
||||||
|
SYSTEM: false
|
||||||
|
TASKS: false
|
||||||
|
VERSION: true
|
||||||
|
VOLUMES: false
|
||||||
|
volumes:
|
||||||
|
{{ if eq .ContainerRuntime "podman" -}}
|
||||||
|
- /var/run/podman/podman.sock:/var/run/podman/podman.sock
|
||||||
|
{{ else if eq .ContainerRuntime "nerdctl" -}}
|
||||||
|
- /var/run/containerd/containerd.sock:/var/run/containerd/containerd.sock
|
||||||
|
- /var/lib/nerdctl:/var/lib/nerdctl:ro # required to read metadata like network info
|
||||||
|
{{ else -}}
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
{{ end -}}
|
||||||
|
- ./data:/app/data
|
||||||
122
agent/pkg/agentproxy/README.md
Normal file
122
agent/pkg/agentproxy/README.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# agent/pkg/agentproxy
|
||||||
|
|
||||||
|
Package for configuring HTTP proxy connections through the GoDoxy Agent using HTTP headers.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This package provides types and functions for parsing and setting agent proxy configuration via HTTP headers. It supports both a modern base64-encoded JSON format and a legacy header-based format for backward compatibility.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[HTTP Request] --> B[ConfigFromHeaders]
|
||||||
|
B --> C{Modern Format?}
|
||||||
|
C -->|Yes| D[Parse X-Proxy-Config Base64 JSON]
|
||||||
|
C -->|No| E[Parse Legacy Headers]
|
||||||
|
D --> F[Config]
|
||||||
|
E --> F
|
||||||
|
|
||||||
|
F --> G[SetAgentProxyConfigHeaders]
|
||||||
|
G --> H[Modern Headers]
|
||||||
|
G --> I[Legacy Headers]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Public Types
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Config struct {
|
||||||
|
Scheme string // Proxy scheme (http or https)
|
||||||
|
Host string // Proxy host (hostname or hostname:port)
|
||||||
|
HTTPConfig // Extended HTTP configuration
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `HTTPConfig` embedded type (from `internal/route/types`) includes:
|
||||||
|
|
||||||
|
- `NoTLSVerify` - Skip TLS certificate verification
|
||||||
|
- `ResponseHeaderTimeout` - Timeout for response headers
|
||||||
|
- `DisableCompression` - Disable gzip compression
|
||||||
|
|
||||||
|
## Public Functions
|
||||||
|
|
||||||
|
### ConfigFromHeaders
|
||||||
|
|
||||||
|
```go
|
||||||
|
func ConfigFromHeaders(h http.Header) (Config, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
Parses proxy configuration from HTTP request headers. Tries modern format first, falls back to legacy format if not present.
|
||||||
|
|
||||||
|
### proxyConfigFromHeaders
|
||||||
|
|
||||||
|
```go
|
||||||
|
func proxyConfigFromHeaders(h http.Header) (Config, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
Parses the modern base64-encoded JSON format from `X-Proxy-Config` header.
|
||||||
|
|
||||||
|
### proxyConfigFromHeadersLegacy
|
||||||
|
|
||||||
|
```go
|
||||||
|
func proxyConfigFromHeadersLegacy(h http.Header) Config
|
||||||
|
```
|
||||||
|
|
||||||
|
Parses the legacy header format:
|
||||||
|
|
||||||
|
- `X-Proxy-Host` - Proxy host
|
||||||
|
- `X-Proxy-Https` - Whether to use HTTPS
|
||||||
|
- `X-Proxy-Skip-Tls-Verify` - Skip TLS verification
|
||||||
|
- `X-Proxy-Response-Header-Timeout` - Response timeout in seconds
|
||||||
|
|
||||||
|
### SetAgentProxyConfigHeaders
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (cfg *Config) SetAgentProxyConfigHeaders(h http.Header)
|
||||||
|
```
|
||||||
|
|
||||||
|
Sets headers for modern format with base64-encoded JSON config.
|
||||||
|
|
||||||
|
### SetAgentProxyConfigHeadersLegacy
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (cfg *Config) SetAgentProxyConfigHeadersLegacy(h http.Header)
|
||||||
|
```
|
||||||
|
|
||||||
|
Sets headers for legacy format with individual header fields.
|
||||||
|
|
||||||
|
## Header Constants
|
||||||
|
|
||||||
|
Modern headers:
|
||||||
|
|
||||||
|
- `HeaderXProxyScheme` - Proxy scheme
|
||||||
|
- `HeaderXProxyHost` - Proxy host
|
||||||
|
- `HeaderXProxyConfig` - Base64-encoded JSON config
|
||||||
|
|
||||||
|
Legacy headers (deprecated):
|
||||||
|
|
||||||
|
- `HeaderXProxyHTTPS`
|
||||||
|
- `HeaderXProxySkipTLSVerify`
|
||||||
|
- `HeaderXProxyResponseHeaderTimeout`
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Reading configuration from incoming request headers
|
||||||
|
func handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cfg, err := agentproxy.ConfigFromHeaders(r.Header)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid proxy config", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use cfg.Scheme and cfg.Host to proxy the request
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
This package is used by `agent/pkg/handler/proxy_http.go` to configure reverse proxy connections based on request headers.
|
||||||
73
agent/pkg/agentproxy/config.go
Normal file
73
agent/pkg/agentproxy/config.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package agentproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
route "github.com/yusing/godoxy/internal/route/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Scheme string `json:"scheme,omitempty"`
|
||||||
|
Host string `json:"host,omitempty"` // host or host:port
|
||||||
|
|
||||||
|
route.HTTPConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigFromHeaders(h http.Header) (Config, error) {
|
||||||
|
cfg, err := proxyConfigFromHeaders(h)
|
||||||
|
if cfg.Host == "" || err != nil {
|
||||||
|
cfg = proxyConfigFromHeadersLegacy(h)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func proxyConfigFromHeadersLegacy(h http.Header) (cfg Config) {
|
||||||
|
cfg.Host = h.Get(HeaderXProxyHost)
|
||||||
|
isHTTPS, _ := strconv.ParseBool(h.Get(HeaderXProxyHTTPS))
|
||||||
|
cfg.NoTLSVerify, _ = strconv.ParseBool(h.Get(HeaderXProxySkipTLSVerify))
|
||||||
|
responseHeaderTimeout, err := strconv.Atoi(h.Get(HeaderXProxyResponseHeaderTimeout))
|
||||||
|
if err != nil {
|
||||||
|
responseHeaderTimeout = 0
|
||||||
|
}
|
||||||
|
cfg.ResponseHeaderTimeout = time.Duration(responseHeaderTimeout) * time.Second
|
||||||
|
|
||||||
|
cfg.Scheme = "http"
|
||||||
|
if isHTTPS {
|
||||||
|
cfg.Scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func proxyConfigFromHeaders(h http.Header) (cfg Config, err error) {
|
||||||
|
cfg.Scheme = h.Get(HeaderXProxyScheme)
|
||||||
|
cfg.Host = h.Get(HeaderXProxyHost)
|
||||||
|
|
||||||
|
cfgBase64 := h.Get(HeaderXProxyConfig)
|
||||||
|
cfgJSON, err := base64.StdEncoding.DecodeString(cfgBase64)
|
||||||
|
if err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sonic.Unmarshal(cfgJSON, &cfg)
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) SetAgentProxyConfigHeadersLegacy(h http.Header) {
|
||||||
|
h.Set(HeaderXProxyHost, cfg.Host)
|
||||||
|
h.Set(HeaderXProxyHTTPS, strconv.FormatBool(cfg.Scheme == "https"))
|
||||||
|
h.Set(HeaderXProxySkipTLSVerify, strconv.FormatBool(cfg.NoTLSVerify))
|
||||||
|
h.Set(HeaderXProxyResponseHeaderTimeout, strconv.Itoa(int(cfg.ResponseHeaderTimeout.Round(time.Second).Seconds())))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) SetAgentProxyConfigHeaders(h http.Header) {
|
||||||
|
h.Set(HeaderXProxyHost, cfg.Host)
|
||||||
|
h.Set(HeaderXProxyScheme, string(cfg.Scheme))
|
||||||
|
cfgJSON, _ := sonic.Marshal(cfg.HTTPConfig)
|
||||||
|
cfgBase64 := base64.StdEncoding.EncodeToString(cfgJSON)
|
||||||
|
h.Set(HeaderXProxyConfig, cfgBase64)
|
||||||
|
}
|
||||||
14
agent/pkg/agentproxy/headers.go
Normal file
14
agent/pkg/agentproxy/headers.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package agentproxy
|
||||||
|
|
||||||
|
const (
|
||||||
|
HeaderXProxyScheme = "X-Proxy-Scheme"
|
||||||
|
HeaderXProxyHost = "X-Proxy-Host"
|
||||||
|
HeaderXProxyConfig = "X-Proxy-Config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// deprecated
|
||||||
|
const (
|
||||||
|
HeaderXProxyHTTPS = "X-Proxy-Https"
|
||||||
|
HeaderXProxySkipTLSVerify = "X-Proxy-Skip-Tls-Verify"
|
||||||
|
HeaderXProxyResponseHeaderTimeout = "X-Proxy-Response-Header-Timeout"
|
||||||
|
)
|
||||||
102
agent/pkg/certs/README.md
Normal file
102
agent/pkg/certs/README.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# agent/pkg/certs
|
||||||
|
|
||||||
|
Certificate management package for creating and extracting certificate archives.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This package provides utilities for packaging SSL certificates into ZIP archives and extracting them. It is used by the GoDoxy Agent to distribute certificates to clients in a convenient format.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[Raw Certs] --> B[ZipCert]
|
||||||
|
B --> C[ZIP Archive]
|
||||||
|
C --> D[ca.pem]
|
||||||
|
C --> E[cert.pem]
|
||||||
|
C --> F[key.pem]
|
||||||
|
|
||||||
|
G[ZIP Archive] --> H[ExtractCert]
|
||||||
|
H --> I[ca, crt, key]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Public Functions
|
||||||
|
|
||||||
|
### ZipCert
|
||||||
|
|
||||||
|
```go
|
||||||
|
func ZipCert(ca, crt, key []byte) ([]byte, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates a ZIP archive containing three PEM files:
|
||||||
|
|
||||||
|
- `ca.pem` - CA certificate
|
||||||
|
- `cert.pem` - Server/client certificate
|
||||||
|
- `key.pem` - Private key
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `ca` - CA certificate in PEM format
|
||||||
|
- `crt` - Certificate in PEM format
|
||||||
|
- `key` - Private key in PEM format
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
|
||||||
|
- ZIP archive bytes
|
||||||
|
- Error if packing fails
|
||||||
|
|
||||||
|
### ExtractCert
|
||||||
|
|
||||||
|
```go
|
||||||
|
func ExtractCert(data []byte) (ca, crt, key []byte, err error)
|
||||||
|
```
|
||||||
|
|
||||||
|
Extracts certificates from a ZIP archive created by `ZipCert`.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `data` - ZIP archive bytes
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
|
||||||
|
- `ca` - CA certificate bytes
|
||||||
|
- `crt` - Certificate bytes
|
||||||
|
- `key` - Private key bytes
|
||||||
|
- Error if extraction fails
|
||||||
|
|
||||||
|
### AgentCertsFilepath
|
||||||
|
|
||||||
|
```go
|
||||||
|
func AgentCertsFilepath(host string) (filepathOut string, ok bool)
|
||||||
|
```
|
||||||
|
|
||||||
|
Generates the file path for storing agent certificates.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `host` - Agent hostname
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
|
||||||
|
- Full file path within `certs/` directory
|
||||||
|
- `false` if host is invalid (contains path separators or special characters)
|
||||||
|
|
||||||
|
### isValidAgentHost
|
||||||
|
|
||||||
|
```go
|
||||||
|
func isValidAgentHost(host string) bool
|
||||||
|
```
|
||||||
|
|
||||||
|
Validates that a host string is safe for use in file paths.
|
||||||
|
|
||||||
|
## Constants
|
||||||
|
|
||||||
|
```go
|
||||||
|
const AgentCertsBasePath = "certs"
|
||||||
|
```
|
||||||
|
|
||||||
|
Base directory for storing certificate archives.
|
||||||
|
|
||||||
|
## File Format
|
||||||
|
|
||||||
|
The ZIP archive uses `zip.Store` compression (no compression) for fast creation and extraction. Each file is stored with its standard name (`ca.pem`, `cert.pem`, `key.pem`).
|
||||||
85
agent/pkg/certs/zip.go
Normal file
85
agent/pkg/certs/zip.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package certs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
strutils "github.com/yusing/goutils/strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const AgentCertsBasePath = "certs"
|
||||||
|
|
||||||
|
func writeFile(zipWriter *zip.Writer, name string, data []byte) error {
|
||||||
|
w, err := zipWriter.CreateHeader(&zip.FileHeader{
|
||||||
|
Name: name,
|
||||||
|
Method: zip.Store,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = w.Write(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFile(f *zip.File) ([]byte, error) {
|
||||||
|
r, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
return io.ReadAll(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ZipCert(ca, crt, key []byte) ([]byte, error) {
|
||||||
|
data := bytes.NewBuffer(make([]byte, 0, 6144))
|
||||||
|
zipWriter := zip.NewWriter(data)
|
||||||
|
defer zipWriter.Close()
|
||||||
|
|
||||||
|
if err := writeFile(zipWriter, "ca.pem", ca); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := writeFile(zipWriter, "cert.pem", crt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := writeFile(zipWriter, "key.pem", key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := zipWriter.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidAgentHost(host string) bool {
|
||||||
|
return strutils.IsValidFilename(host + ".zip")
|
||||||
|
}
|
||||||
|
|
||||||
|
func AgentCertsFilepath(host string) (filepathOut string, ok bool) {
|
||||||
|
if !isValidAgentHost(host) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return filepath.Join(AgentCertsBasePath, host+".zip"), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractCert(data []byte) (ca, crt, key []byte, err error) {
|
||||||
|
zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
for _, file := range zipReader.File {
|
||||||
|
switch file.Name {
|
||||||
|
case "ca.pem":
|
||||||
|
ca, err = readFile(file)
|
||||||
|
case "cert.pem":
|
||||||
|
crt, err = readFile(file)
|
||||||
|
case "key.pem":
|
||||||
|
key, err = readFile(file)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ca, crt, key, nil
|
||||||
|
}
|
||||||
20
agent/pkg/certs/zip_test.go
Normal file
20
agent/pkg/certs/zip_test.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package certs_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/certs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestZipCert(t *testing.T) {
|
||||||
|
ca, crt, key := []byte("test1"), []byte("test2"), []byte("test3")
|
||||||
|
zipData, err := certs.ZipCert(ca, crt, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ca2, crt2, key2, err := certs.ExtractCert(zipData)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ca, ca2)
|
||||||
|
require.Equal(t, crt, crt2)
|
||||||
|
require.Equal(t, key, key2)
|
||||||
|
}
|
||||||
52
agent/pkg/env/README.md
vendored
Normal file
52
agent/pkg/env/README.md
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# agent/pkg/env
|
||||||
|
|
||||||
|
Environment configuration package for the GoDoxy Agent.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This package manages environment variable parsing and provides a centralized location for all agent configuration options. It is automatically initialized on import.
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
| Variable | Type | Default | Description |
|
||||||
|
| -------------------------- | ---------------- | ---------------------- | --------------------------------------- |
|
||||||
|
| `DockerSocket` | string | `/var/run/docker.sock` | Path to Docker socket |
|
||||||
|
| `AgentName` | string | System hostname | Agent identifier |
|
||||||
|
| `AgentPort` | int | `8890` | Agent server port |
|
||||||
|
| `AgentSkipClientCertCheck` | bool | `false` | Skip mTLS certificate verification |
|
||||||
|
| `AgentCACert` | string | (empty) | Base64 Encoded CA certificate + key |
|
||||||
|
| `AgentSSLCert` | string | (empty) | Base64 Encoded server certificate + key |
|
||||||
|
| `Runtime` | ContainerRuntime | `docker` | Container runtime (docker or podman) |
|
||||||
|
|
||||||
|
## ContainerRuntime Type
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ContainerRuntime string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ContainerRuntimeDocker ContainerRuntime = "docker"
|
||||||
|
ContainerRuntimePodman ContainerRuntime = "podman"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Public Functions
|
||||||
|
|
||||||
|
### DefaultAgentName
|
||||||
|
|
||||||
|
```go
|
||||||
|
func DefaultAgentName() string
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns the system hostname as the default agent name. Falls back to `"agent"` if hostname cannot be determined.
|
||||||
|
|
||||||
|
### Load
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Load()
|
||||||
|
```
|
||||||
|
|
||||||
|
Reloads all environment variables from the environment. Called automatically on package init, but can be called again to refresh configuration.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
The `Load()` function validates that `Runtime` is either `docker` or `podman`. An invalid runtime causes a fatal error.
|
||||||
49
agent/pkg/env/env.go
vendored
Normal file
49
agent/pkg/env/env.go
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
|
"github.com/yusing/goutils/env"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DefaultAgentName() string {
|
||||||
|
name, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
return "agent"
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
AgentName string
|
||||||
|
AgentPort int
|
||||||
|
AgentSkipClientCertCheck bool
|
||||||
|
AgentCACert string
|
||||||
|
AgentSSLCert string
|
||||||
|
DockerSocket string
|
||||||
|
Runtime agent.ContainerRuntime
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() {
|
||||||
|
DockerSocket = env.GetEnvString("DOCKER_SOCKET", "/var/run/docker.sock")
|
||||||
|
AgentName = env.GetEnvString("AGENT_NAME", DefaultAgentName())
|
||||||
|
AgentPort = env.GetEnvInt("AGENT_PORT", 8890)
|
||||||
|
AgentSkipClientCertCheck = env.GetEnvBool("AGENT_SKIP_CLIENT_CERT_CHECK", false)
|
||||||
|
|
||||||
|
AgentCACert = env.GetEnvString("AGENT_CA_CERT", "")
|
||||||
|
AgentSSLCert = env.GetEnvString("AGENT_SSL_CERT", "")
|
||||||
|
Runtime = agent.ContainerRuntime(env.GetEnvString("RUNTIME", "docker"))
|
||||||
|
|
||||||
|
switch Runtime {
|
||||||
|
case agent.ContainerRuntimeDocker, agent.ContainerRuntimePodman: //, agent.ContainerRuntimeNerdctl:
|
||||||
|
default:
|
||||||
|
log.Fatal().Str("runtime", string(Runtime)).Msg("invalid runtime")
|
||||||
|
}
|
||||||
|
}
|
||||||
127
agent/pkg/handler/README.md
Normal file
127
agent/pkg/handler/README.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# agent/pkg/handler
|
||||||
|
|
||||||
|
HTTP request handler package for the GoDoxy Agent.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This package provides the HTTP handler for the GoDoxy Agent server, including endpoints for:
|
||||||
|
|
||||||
|
- Version information
|
||||||
|
- Agent name and runtime
|
||||||
|
- Health checks
|
||||||
|
- System metrics (via SSE)
|
||||||
|
- HTTP proxy routing
|
||||||
|
- Docker socket proxying
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[HTTP Request] --> B[NewAgentHandler]
|
||||||
|
B --> C{ServeMux Router}
|
||||||
|
|
||||||
|
C --> D[GET /version]
|
||||||
|
C --> E[GET /name]
|
||||||
|
C --> F[GET /runtime]
|
||||||
|
C --> G[GET /health]
|
||||||
|
C --> H[GET /system-info]
|
||||||
|
C --> I[GET /proxy/http/#123;path...#125;]
|
||||||
|
C --> J[ /#42; Docker Socket]
|
||||||
|
|
||||||
|
H --> K[Gin Router]
|
||||||
|
K --> L[WebSocket Upgrade]
|
||||||
|
L --> M[SystemInfo Poller]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Public Types
|
||||||
|
|
||||||
|
### ServeMux
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ServeMux struct{ *http.ServeMux }
|
||||||
|
```
|
||||||
|
|
||||||
|
Wrapper around `http.ServeMux` with agent-specific endpoint helpers.
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
|
||||||
|
- `HandleEndpoint(method, endpoint string, handler http.HandlerFunc)` - Registers handler with API base path
|
||||||
|
- `HandleFunc(endpoint string, handler http.HandlerFunc)` - Registers GET handler with API base path
|
||||||
|
|
||||||
|
## Public Functions
|
||||||
|
|
||||||
|
### NewAgentHandler
|
||||||
|
|
||||||
|
```go
|
||||||
|
func NewAgentHandler() http.Handler
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates and configures the HTTP handler for the agent server. Sets up:
|
||||||
|
|
||||||
|
- Gin-based metrics handler with WebSocket support for SSE
|
||||||
|
- All standard agent endpoints
|
||||||
|
- HTTP proxy endpoint
|
||||||
|
- Docker socket proxy fallback
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
| Endpoint | Method | Description |
|
||||||
|
| ----------------------- | -------- | ------------------------------------ |
|
||||||
|
| `/version` | GET | Returns agent version |
|
||||||
|
| `/name` | GET | Returns agent name |
|
||||||
|
| `/runtime` | GET | Returns container runtime |
|
||||||
|
| `/health` | GET | Health check with scheme query param |
|
||||||
|
| `/system-info` | GET | System metrics via SSE or WebSocket |
|
||||||
|
| `/proxy/http/{path...}` | GET/POST | HTTP proxy with config from headers |
|
||||||
|
| `/*` | \* | Docker socket proxy |
|
||||||
|
|
||||||
|
## Sub-packages
|
||||||
|
|
||||||
|
### proxy_http.go
|
||||||
|
|
||||||
|
Handles HTTP proxy requests by reading configuration from request headers and proxying to the configured upstream.
|
||||||
|
|
||||||
|
**Key Function:**
|
||||||
|
|
||||||
|
- `ProxyHTTP(w, r)` - Proxies HTTP requests based on `X-Proxy-*` headers
|
||||||
|
|
||||||
|
### check_health.go
|
||||||
|
|
||||||
|
Handles health check requests for various schemes.
|
||||||
|
|
||||||
|
**Key Function:**
|
||||||
|
|
||||||
|
- `CheckHealth(w, r)` - Performs health checks with configurable scheme
|
||||||
|
|
||||||
|
**Supported Schemes:**
|
||||||
|
|
||||||
|
- `http`, `https` - HTTP health check
|
||||||
|
- `h2c` - HTTP/2 cleartext health check
|
||||||
|
- `tcp`, `udp`, `tcp4`, `udp4`, `tcp6`, `udp6` - TCP/UDP health check
|
||||||
|
- `fileserver` - File existence check
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/handler"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("/", handler.NewAgentHandler())
|
||||||
|
|
||||||
|
http.ListenAndServe(":8890", mux)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## WebSocket Support
|
||||||
|
|
||||||
|
The handler includes a permissive WebSocket upgrader for internal use (no origin check). This enables real-time system metrics streaming via Server-Sent Events (SSE).
|
||||||
|
|
||||||
|
## Docker Socket Integration
|
||||||
|
|
||||||
|
All unmatched requests fall through to the Docker socket handler, allowing the agent to proxy Docker API calls when configured.
|
||||||
90
agent/pkg/handler/check_health.go
Normal file
90
agent/pkg/handler/check_health.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
healthcheck "github.com/yusing/godoxy/internal/health/check"
|
||||||
|
"github.com/yusing/godoxy/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
||||||
|
query := r.URL.Query()
|
||||||
|
scheme := query.Get("scheme")
|
||||||
|
if scheme == "" {
|
||||||
|
http.Error(w, "missing scheme", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timeout := parseMsOrDefault(query.Get("timeout"))
|
||||||
|
|
||||||
|
var (
|
||||||
|
result types.HealthCheckResult
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
switch scheme {
|
||||||
|
case "fileserver":
|
||||||
|
path := query.Get("path")
|
||||||
|
if path == "" {
|
||||||
|
http.Error(w, "missing path", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err = healthcheck.FileServer(path)
|
||||||
|
case "http", "https", "h2c": // path is optional
|
||||||
|
host := query.Get("host")
|
||||||
|
path := query.Get("path")
|
||||||
|
if host == "" {
|
||||||
|
http.Error(w, "missing host", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
url := url.URL{Scheme: scheme, Host: host}
|
||||||
|
if scheme == "h2c" {
|
||||||
|
result, err = healthcheck.H2C(r.Context(), &url, http.MethodHead, path, timeout)
|
||||||
|
} else {
|
||||||
|
result, err = healthcheck.HTTP(&url, http.MethodHead, path, timeout)
|
||||||
|
}
|
||||||
|
case "tcp", "udp", "tcp4", "udp4", "tcp6", "udp6":
|
||||||
|
host := query.Get("host")
|
||||||
|
if host == "" {
|
||||||
|
http.Error(w, "missing host", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hasPort := strings.Contains(host, ":")
|
||||||
|
port := query.Get("port")
|
||||||
|
if port != "" && hasPort {
|
||||||
|
http.Error(w, "port and host with port cannot both be provided", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if port != "" {
|
||||||
|
host = net.JoinHostPort(host, port)
|
||||||
|
}
|
||||||
|
url := url.URL{Scheme: scheme, Host: host}
|
||||||
|
result, err = healthcheck.Stream(r.Context(), &url, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
sonic.ConfigDefault.NewEncoder(w).Encode(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMsOrDefault(msStr string) time.Duration {
|
||||||
|
if msStr == "" {
|
||||||
|
return types.HealthCheckTimeoutDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutMs, _ := strconv.ParseInt(msStr, 10, 64)
|
||||||
|
if timeoutMs == 0 {
|
||||||
|
return types.HealthCheckTimeoutDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Duration(timeoutMs) * time.Millisecond
|
||||||
|
}
|
||||||
226
agent/pkg/handler/check_health_test.go
Normal file
226
agent/pkg/handler/check_health_test.go
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
package handler_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/handler"
|
||||||
|
"github.com/yusing/godoxy/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckHealthHTTP(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupServer func() *httptest.Server
|
||||||
|
queryParams map[string]string
|
||||||
|
expectedStatus int
|
||||||
|
expectedHealthy bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid",
|
||||||
|
setupServer: func() *httptest.Server {
|
||||||
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
queryParams: map[string]string{
|
||||||
|
"scheme": "http",
|
||||||
|
"host": "localhost",
|
||||||
|
"path": "/",
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedHealthy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InvalidQuery",
|
||||||
|
setupServer: nil,
|
||||||
|
queryParams: map[string]string{
|
||||||
|
"scheme": "http",
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ConnectionError",
|
||||||
|
setupServer: nil,
|
||||||
|
queryParams: map[string]string{
|
||||||
|
"scheme": "http",
|
||||||
|
"host": "localhost:12345",
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedHealthy: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var server *httptest.Server
|
||||||
|
if tt.setupServer != nil {
|
||||||
|
server = tt.setupServer()
|
||||||
|
defer server.Close()
|
||||||
|
u, _ := url.Parse(server.URL)
|
||||||
|
tt.queryParams["scheme"] = u.Scheme
|
||||||
|
tt.queryParams["host"] = u.Host
|
||||||
|
tt.queryParams["path"] = u.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
query := url.Values{}
|
||||||
|
for key, value := range tt.queryParams {
|
||||||
|
query.Set(key, value)
|
||||||
|
}
|
||||||
|
request := httptest.NewRequest(http.MethodGet, agent.APIEndpointBase+agent.EndpointHealth+"?"+query.Encode(), nil)
|
||||||
|
handler.CheckHealth(recorder, request)
|
||||||
|
|
||||||
|
require.Equal(t, recorder.Code, tt.expectedStatus)
|
||||||
|
|
||||||
|
if tt.expectedStatus == http.StatusOK {
|
||||||
|
var result types.HealthCheckResult
|
||||||
|
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
|
||||||
|
require.Equal(t, result.Healthy, tt.expectedHealthy)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckHealthFileServer(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
expectedStatus int
|
||||||
|
expectedHealthy bool
|
||||||
|
expectedDetail string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ValidPath",
|
||||||
|
path: t.TempDir(),
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedHealthy: true,
|
||||||
|
expectedDetail: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InvalidPath",
|
||||||
|
path: "/invalid",
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedHealthy: false,
|
||||||
|
expectedDetail: "stat /invalid: no such file or directory",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
query := url.Values{}
|
||||||
|
query.Set("scheme", "fileserver")
|
||||||
|
query.Set("path", tt.path)
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
request := httptest.NewRequest(http.MethodGet, agent.APIEndpointBase+agent.EndpointHealth+"?"+query.Encode(), nil)
|
||||||
|
handler.CheckHealth(recorder, request)
|
||||||
|
|
||||||
|
require.Equal(t, recorder.Code, tt.expectedStatus)
|
||||||
|
|
||||||
|
var result types.HealthCheckResult
|
||||||
|
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
|
||||||
|
require.Equal(t, result.Healthy, tt.expectedHealthy)
|
||||||
|
require.Equal(t, result.Detail, tt.expectedDetail)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckHealthTCPUDP(t *testing.T) {
|
||||||
|
tcp, err := net.Listen("tcp", "localhost:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
go func() {
|
||||||
|
conn, err := tcp.Accept()
|
||||||
|
require.NoError(t, err)
|
||||||
|
conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
udp, err := net.ListenPacket("udp", "localhost:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, addr, err := udp.ReadFrom(buf)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, string(buf[:n]), "ping")
|
||||||
|
_, _ = udp.WriteTo([]byte("pong"), addr)
|
||||||
|
udp.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
scheme string
|
||||||
|
host string
|
||||||
|
port int
|
||||||
|
expectedStatus int
|
||||||
|
expectedHealthy bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ValidTCP",
|
||||||
|
scheme: "tcp",
|
||||||
|
host: "localhost",
|
||||||
|
port: tcp.Addr().(*net.TCPAddr).Port,
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedHealthy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InvalidHost",
|
||||||
|
scheme: "tcp",
|
||||||
|
host: "",
|
||||||
|
port: 8080,
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
expectedHealthy: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ValidUDP",
|
||||||
|
scheme: "udp",
|
||||||
|
host: "localhost",
|
||||||
|
port: udp.LocalAddr().(*net.UDPAddr).Port,
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedHealthy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InvalidHost",
|
||||||
|
scheme: "udp",
|
||||||
|
host: "",
|
||||||
|
port: 8080,
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
expectedHealthy: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Port in both host and port",
|
||||||
|
scheme: "tcp",
|
||||||
|
host: "localhost:1234",
|
||||||
|
port: 1234,
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
expectedHealthy: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
query := url.Values{}
|
||||||
|
query.Set("scheme", tt.scheme)
|
||||||
|
query.Set("host", tt.host)
|
||||||
|
query.Set("port", strconv.Itoa(tt.port))
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
request := httptest.NewRequest(http.MethodGet, agent.APIEndpointBase+agent.EndpointHealth+"?"+query.Encode(), nil)
|
||||||
|
handler.CheckHealth(recorder, request)
|
||||||
|
|
||||||
|
require.Equal(t, recorder.Code, tt.expectedStatus)
|
||||||
|
|
||||||
|
if tt.expectedStatus == http.StatusOK {
|
||||||
|
var result types.HealthCheckResult
|
||||||
|
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
|
||||||
|
require.Equal(t, result.Healthy, tt.expectedHealthy)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
60
agent/pkg/handler/handler.go
Normal file
60
agent/pkg/handler/handler.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/env"
|
||||||
|
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||||
|
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
|
||||||
|
"github.com/yusing/goutils/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServeMux struct{ *http.ServeMux }
|
||||||
|
|
||||||
|
func (mux ServeMux) HandleEndpoint(method, endpoint string, handler http.HandlerFunc) {
|
||||||
|
mux.ServeMux.HandleFunc(method+" "+agent.APIEndpointBase+endpoint, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mux ServeMux) HandleFunc(endpoint string, handler http.HandlerFunc) {
|
||||||
|
mux.ServeMux.HandleFunc(agent.APIEndpointBase+endpoint, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
var upgrader = &websocket.Upgrader{
|
||||||
|
// no origin check needed for internal websocket
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAgentHandler() http.Handler {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
mux := ServeMux{http.NewServeMux()}
|
||||||
|
|
||||||
|
metricsHandler := gin.Default()
|
||||||
|
{
|
||||||
|
metrics := metricsHandler.Group(agent.APIEndpointBase)
|
||||||
|
metrics.GET(agent.EndpointSystemInfo, func(c *gin.Context) {
|
||||||
|
c.Set("upgrader", upgrader)
|
||||||
|
systeminfo.Poller.ServeHTTP(c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.HandleFunc(agent.EndpointProxyHTTP+"/{path...}", ProxyHTTP)
|
||||||
|
mux.HandleFunc(agent.EndpointInfo, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentInfo := agent.AgentInfo{
|
||||||
|
Version: version.Get(),
|
||||||
|
Name: env.AgentName,
|
||||||
|
Runtime: env.Runtime,
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
sonic.ConfigDefault.NewEncoder(w).Encode(agentInfo)
|
||||||
|
})
|
||||||
|
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
|
||||||
|
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, metricsHandler.ServeHTTP)
|
||||||
|
mux.ServeMux.HandleFunc("/", socketproxy.DockerSocketHandler(env.DockerSocket))
|
||||||
|
return mux
|
||||||
|
}
|
||||||
72
agent/pkg/handler/proxy_http.go
Normal file
72
agent/pkg/handler/proxy_http.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agentproxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewTransport() *http.Transport {
|
||||||
|
return &http.Transport{
|
||||||
|
MaxIdleConnsPerHost: 100,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
ResponseHeaderTimeout: 60 * time.Second,
|
||||||
|
WriteBufferSize: 16 * 1024, // 16KB
|
||||||
|
ReadBufferSize: 16 * 1024, // 16KB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cfg, err := agentproxy.ConfigFromHeaders(r.Header)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to parse agent proxy config: %s", err.Error()), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transport := NewTransport()
|
||||||
|
if cfg.ResponseHeaderTimeout > 0 {
|
||||||
|
transport.ResponseHeaderTimeout = cfg.ResponseHeaderTimeout
|
||||||
|
}
|
||||||
|
if cfg.DisableCompression {
|
||||||
|
transport.DisableCompression = true
|
||||||
|
}
|
||||||
|
|
||||||
|
transport.TLSClientConfig, err = cfg.BuildTLSConfig(r.URL)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to build TLS client config: %s", err.Error()), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip the {API_BASE}/proxy/http prefix while preserving URL escaping.
|
||||||
|
//
|
||||||
|
// NOTE: `r.URL.Path` is decoded. If we rewrite it without keeping `RawPath`
|
||||||
|
// in sync, Go may re-escape the path (e.g. turning "%5B" into "%255B"),
|
||||||
|
// which breaks urls with percent-encoded characters, like Next.js static chunk URLs.
|
||||||
|
prefix := agent.APIEndpointBase + agent.EndpointProxyHTTP
|
||||||
|
r.URL.Path = strings.TrimPrefix(r.URL.Path, prefix)
|
||||||
|
if r.URL.RawPath != "" {
|
||||||
|
if after, ok := strings.CutPrefix(r.URL.RawPath, prefix); ok {
|
||||||
|
r.URL.RawPath = after
|
||||||
|
} else {
|
||||||
|
// RawPath is no longer a valid encoding for Path; force Go to re-derive it.
|
||||||
|
r.URL.RawPath = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.RequestURI = ""
|
||||||
|
|
||||||
|
rp := &httputil.ReverseProxy{
|
||||||
|
Director: func(r *http.Request) {
|
||||||
|
r.URL.Scheme = cfg.Scheme
|
||||||
|
r.URL.Host = cfg.Host
|
||||||
|
},
|
||||||
|
Transport: transport,
|
||||||
|
}
|
||||||
|
rp.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
BIN
assets/godoxy.png
Normal file
BIN
assets/godoxy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
73
cmd/README.md
Normal file
73
cmd/README.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# cmd
|
||||||
|
|
||||||
|
Main entry point package for GoDoxy, a lightweight reverse proxy with WebUI for Docker containers.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This package contains the `main.go` entry point that initializes and starts the GoDoxy server. It coordinates the initialization of all core components including configuration loading, API server, authentication, and monitoring services.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[main] --> B[Init Profiling]
|
||||||
|
A --> C[Init Logger]
|
||||||
|
A --> D[Parallel Init]
|
||||||
|
D --> D1[DNS Providers]
|
||||||
|
D --> D2[Icon Cache]
|
||||||
|
D --> D3[System Info Poller]
|
||||||
|
D --> D4[Middleware Compose Files]
|
||||||
|
A --> E[JWT Secret Setup]
|
||||||
|
A --> F[Create Directories]
|
||||||
|
A --> G[Load Config]
|
||||||
|
A --> H[Start Proxy Servers]
|
||||||
|
A --> I[Init Auth]
|
||||||
|
A --> J[Start API Server]
|
||||||
|
A --> K[Debug Server]
|
||||||
|
A --> L[Uptime Poller]
|
||||||
|
A --> M[Watch Changes]
|
||||||
|
A --> N[Wait Exit]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Main Function Flow
|
||||||
|
|
||||||
|
The `main()` function performs the following initialization steps:
|
||||||
|
|
||||||
|
1. **Profiling Setup**: Initializes pprof endpoints for performance monitoring
|
||||||
|
1. **Logger Initialization**: Configures zerolog with memory logging
|
||||||
|
1. **Parallel Initialization**: Starts DNS providers, icon cache, system info poller, and middleware
|
||||||
|
1. **JWT Secret**: Ensures API JWT secret is set (generates random if not provided)
|
||||||
|
1. **Directory Preparation**: Creates required directories for logs, certificates, etc.
|
||||||
|
1. **Configuration Loading**: Loads YAML configuration and reports any errors
|
||||||
|
1. **Proxy Servers**: Starts HTTP/HTTPS proxy servers based on configuration
|
||||||
|
1. **Authentication**: Initializes authentication system with access control
|
||||||
|
1. **API Server**: Starts the REST API server with all configured routes
|
||||||
|
1. **Debug Server**: Starts the debug page server (development mode)
|
||||||
|
1. **Monitoring**: Starts uptime and system info polling
|
||||||
|
1. **Change Watcher**: Starts watching for Docker container and configuration changes
|
||||||
|
1. **Graceful Shutdown**: Waits for exit signal with configured timeout
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The main configuration is loaded from `config/config.yml`. Required directories include:
|
||||||
|
|
||||||
|
- `logs/` - Log files
|
||||||
|
- `config/` - Configuration directory
|
||||||
|
- `certs/` - SSL certificates
|
||||||
|
- `proxy/` - Proxy-related files
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
- `API_JWT_SECRET` - Secret key for JWT authentication (optional, auto-generated if not set)
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `internal/api` - REST API handlers
|
||||||
|
- `internal/auth` - Authentication and ACL
|
||||||
|
- `internal/config` - Configuration management
|
||||||
|
- `internal/dnsproviders` - DNS provider integration
|
||||||
|
- `internal/homepage` - WebUI dashboard
|
||||||
|
- `internal/logging` - Logging infrastructure
|
||||||
|
- `internal/metrics` - System metrics collection
|
||||||
|
- `internal/route` - HTTP routing and middleware
|
||||||
|
- `github.com/yusing/goutils/task` - Task lifecycle management
|
||||||
18
cmd/bench_server/Dockerfile
Normal file
18
cmd/bench_server/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM golang:1.25.6-alpine AS builder
|
||||||
|
|
||||||
|
HEALTHCHECK NONE
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
COPY main.go ./
|
||||||
|
|
||||||
|
RUN go build -o bench_server main.go
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
COPY --from=builder /src/bench_server /app/run
|
||||||
|
|
||||||
|
USER 1001:1001
|
||||||
|
|
||||||
|
CMD ["/app/run"]
|
||||||
3
cmd/bench_server/go.mod
Normal file
3
cmd/bench_server/go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module github.com/yusing/godoxy/cmd/bench_server
|
||||||
|
|
||||||
|
go 1.25.6
|
||||||
0
cmd/bench_server/go.sum
Normal file
0
cmd/bench_server/go.sum
Normal file
34
cmd/bench_server/main.go
Normal file
34
cmd/bench_server/main.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"math/rand/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var printables = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
var random = make([]byte, 4096)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for i := range random {
|
||||||
|
random[i] = printables[rand.IntN(len(printables))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(random)
|
||||||
|
})
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: ":80",
|
||||||
|
Handler: handler,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Bench server listening on :80")
|
||||||
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatalf("ListenAndServe: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
257
cmd/debug_page.go
Normal file
257
cmd/debug_page.go
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
//go:build !production
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yusing/godoxy/internal/api"
|
||||||
|
apiV1 "github.com/yusing/godoxy/internal/api/v1"
|
||||||
|
agentApi "github.com/yusing/godoxy/internal/api/v1/agent"
|
||||||
|
authApi "github.com/yusing/godoxy/internal/api/v1/auth"
|
||||||
|
certApi "github.com/yusing/godoxy/internal/api/v1/cert"
|
||||||
|
dockerApi "github.com/yusing/godoxy/internal/api/v1/docker"
|
||||||
|
fileApi "github.com/yusing/godoxy/internal/api/v1/file"
|
||||||
|
homepageApi "github.com/yusing/godoxy/internal/api/v1/homepage"
|
||||||
|
metricsApi "github.com/yusing/godoxy/internal/api/v1/metrics"
|
||||||
|
routeApi "github.com/yusing/godoxy/internal/api/v1/route"
|
||||||
|
"github.com/yusing/godoxy/internal/auth"
|
||||||
|
"github.com/yusing/godoxy/internal/idlewatcher"
|
||||||
|
idlewatcherTypes "github.com/yusing/godoxy/internal/idlewatcher/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type debugMux struct {
|
||||||
|
endpoints []debugEndpoint
|
||||||
|
mux http.ServeMux
|
||||||
|
}
|
||||||
|
|
||||||
|
type debugEndpoint struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDebugMux() *debugMux {
|
||||||
|
return &debugMux{
|
||||||
|
endpoints: make([]debugEndpoint, 0),
|
||||||
|
mux: *http.NewServeMux(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mux *debugMux) registerEndpoint(name, method, path string) {
|
||||||
|
mux.endpoints = append(mux.endpoints, debugEndpoint{name: name, method: method, path: path})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mux *debugMux) HandleFunc(name, method, path string, handler http.HandlerFunc) {
|
||||||
|
mux.registerEndpoint(name, method, path)
|
||||||
|
mux.mux.HandleFunc(method+" "+path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mux *debugMux) Finalize() {
|
||||||
|
mux.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
fmt.Fprintln(w, `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #f8f9fa;
|
||||||
|
background-color: #121212;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f8f9fa;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
color: #e9ecef;
|
||||||
|
}
|
||||||
|
.link {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.method {
|
||||||
|
color: #6c757d;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
.path {
|
||||||
|
color: #6c757d;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Method</th>
|
||||||
|
<th>Path</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>`)
|
||||||
|
for _, endpoint := range mux.endpoints {
|
||||||
|
fmt.Fprintf(w, "<tr><td><a class='link' href=%q>%s</a></td><td class='method'>%s</td><td class='path'>%s</td></tr>", endpoint.path, endpoint.name, endpoint.method, endpoint.path)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(w, `
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func listenDebugServer() {
|
||||||
|
mux := newDebugMux()
|
||||||
|
mux.mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "image/svg+xml")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text x="50" y="50" text-anchor="middle" dominant-baseline="middle">🐙</text></svg>`))
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("Auth block page", "GET", "/auth/block", AuthBlockPageHandler)
|
||||||
|
mux.HandleFunc("Idlewatcher loading page", "GET", idlewatcherTypes.PathPrefix, idlewatcher.DebugHandler)
|
||||||
|
apiHandler := newApiHandler(mux)
|
||||||
|
mux.mux.HandleFunc("/api/v1/", apiHandler.ServeHTTP)
|
||||||
|
|
||||||
|
mux.Finalize()
|
||||||
|
|
||||||
|
go http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Pragma", "no-cache")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
w.Header().Set("Expires", "0")
|
||||||
|
mux.mux.ServeHTTP(w, r)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func newApiHandler(debugMux *debugMux) *gin.Engine {
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(api.ErrorHandler())
|
||||||
|
r.Use(api.ErrorLoggingMiddleware())
|
||||||
|
r.Use(api.NoCache())
|
||||||
|
|
||||||
|
registerGinRoute := func(router gin.IRouter, method, name string, path string, handler gin.HandlerFunc) {
|
||||||
|
if group, ok := router.(*gin.RouterGroup); ok {
|
||||||
|
debugMux.registerEndpoint(name, method, group.BasePath()+path)
|
||||||
|
} else {
|
||||||
|
debugMux.registerEndpoint(name, method, path)
|
||||||
|
}
|
||||||
|
router.Handle(method, path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerGinRoute(r, "GET", "App version", "/api/v1/version", apiV1.Version)
|
||||||
|
|
||||||
|
v1 := r.Group("/api/v1")
|
||||||
|
if auth.IsEnabled() {
|
||||||
|
v1Auth := v1.Group("/auth")
|
||||||
|
{
|
||||||
|
registerGinRoute(v1Auth, "HEAD", "Auth check", "/check", authApi.Check)
|
||||||
|
registerGinRoute(v1Auth, "POST", "Auth login", "/login", authApi.Login)
|
||||||
|
registerGinRoute(v1Auth, "GET", "Auth callback", "/callback", authApi.Callback)
|
||||||
|
registerGinRoute(v1Auth, "POST", "Auth callback", "/callback", authApi.Callback)
|
||||||
|
registerGinRoute(v1Auth, "POST", "Auth logout", "/logout", authApi.Logout)
|
||||||
|
registerGinRoute(v1Auth, "GET", "Auth logout", "/logout", authApi.Logout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// enable cache for favicon
|
||||||
|
registerGinRoute(v1, "GET", "Route favicon", "/favicon", apiV1.FavIcon)
|
||||||
|
registerGinRoute(v1, "GET", "Route health", "/health", apiV1.Health)
|
||||||
|
registerGinRoute(v1, "GET", "List icons", "/icons", apiV1.Icons)
|
||||||
|
registerGinRoute(v1, "POST", "Config reload", "/reload", apiV1.Reload)
|
||||||
|
registerGinRoute(v1, "GET", "Route stats", "/stats", apiV1.Stats)
|
||||||
|
|
||||||
|
route := v1.Group("/route")
|
||||||
|
{
|
||||||
|
registerGinRoute(route, "GET", "List routes", "/list", routeApi.Routes)
|
||||||
|
registerGinRoute(route, "GET", "Get route", "/:which", routeApi.Route)
|
||||||
|
registerGinRoute(route, "GET", "List providers", "/providers", routeApi.Providers)
|
||||||
|
registerGinRoute(route, "GET", "List routes by provider", "/by_provider", routeApi.ByProvider)
|
||||||
|
registerGinRoute(route, "POST", "Playground", "/playground", routeApi.Playground)
|
||||||
|
}
|
||||||
|
|
||||||
|
file := v1.Group("/file")
|
||||||
|
{
|
||||||
|
registerGinRoute(file, "GET", "List files", "/list", fileApi.List)
|
||||||
|
registerGinRoute(file, "GET", "Get file", "/content", fileApi.Get)
|
||||||
|
registerGinRoute(file, "PUT", "Set file", "/content", fileApi.Set)
|
||||||
|
registerGinRoute(file, "POST", "Set file", "/content", fileApi.Set)
|
||||||
|
registerGinRoute(file, "POST", "Validate file", "/validate", fileApi.Validate)
|
||||||
|
}
|
||||||
|
|
||||||
|
homepage := v1.Group("/homepage")
|
||||||
|
{
|
||||||
|
registerGinRoute(homepage, "GET", "List categories", "/categories", homepageApi.Categories)
|
||||||
|
registerGinRoute(homepage, "GET", "List items", "/items", homepageApi.Items)
|
||||||
|
registerGinRoute(homepage, "POST", "Set item", "/set/item", homepageApi.SetItem)
|
||||||
|
registerGinRoute(homepage, "POST", "Set items batch", "/set/items_batch", homepageApi.SetItemsBatch)
|
||||||
|
registerGinRoute(homepage, "POST", "Set item visible", "/set/item_visible", homepageApi.SetItemVisible)
|
||||||
|
registerGinRoute(homepage, "POST", "Set item favorite", "/set/item_favorite", homepageApi.SetItemFavorite)
|
||||||
|
registerGinRoute(homepage, "POST", "Set item sort order", "/set/item_sort_order", homepageApi.SetItemSortOrder)
|
||||||
|
registerGinRoute(homepage, "POST", "Set item all sort order", "/set/item_all_sort_order", homepageApi.SetItemAllSortOrder)
|
||||||
|
registerGinRoute(homepage, "POST", "Set item fav sort order", "/set/item_fav_sort_order", homepageApi.SetItemFavSortOrder)
|
||||||
|
registerGinRoute(homepage, "POST", "Set category order", "/set/category_order", homepageApi.SetCategoryOrder)
|
||||||
|
registerGinRoute(homepage, "POST", "Item click", "/item_click", homepageApi.ItemClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
cert := v1.Group("/cert")
|
||||||
|
{
|
||||||
|
registerGinRoute(cert, "GET", "Get cert info", "/info", certApi.Info)
|
||||||
|
registerGinRoute(cert, "GET", "Renew cert", "/renew", certApi.Renew)
|
||||||
|
}
|
||||||
|
|
||||||
|
agent := v1.Group("/agent")
|
||||||
|
{
|
||||||
|
registerGinRoute(agent, "GET", "List agents", "/list", agentApi.List)
|
||||||
|
registerGinRoute(agent, "POST", "Create agent", "/create", agentApi.Create)
|
||||||
|
registerGinRoute(agent, "POST", "Verify agent", "/verify", agentApi.Verify)
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics := v1.Group("/metrics")
|
||||||
|
{
|
||||||
|
registerGinRoute(metrics, "GET", "Get system info", "/system_info", metricsApi.SystemInfo)
|
||||||
|
registerGinRoute(metrics, "GET", "Get all system info", "/all_system_info", metricsApi.AllSystemInfo)
|
||||||
|
registerGinRoute(metrics, "GET", "Get uptime", "/uptime", metricsApi.Uptime)
|
||||||
|
}
|
||||||
|
|
||||||
|
docker := v1.Group("/docker")
|
||||||
|
{
|
||||||
|
registerGinRoute(docker, "GET", "Get container", "/container/:id", dockerApi.GetContainer)
|
||||||
|
registerGinRoute(docker, "GET", "List containers", "/containers", dockerApi.Containers)
|
||||||
|
registerGinRoute(docker, "GET", "Get docker info", "/info", dockerApi.Info)
|
||||||
|
registerGinRoute(docker, "GET", "Get docker logs", "/logs/:id", dockerApi.Logs)
|
||||||
|
registerGinRoute(docker, "POST", "Start docker container", "/start", dockerApi.Start)
|
||||||
|
registerGinRoute(docker, "POST", "Stop docker container", "/stop", dockerApi.Stop)
|
||||||
|
registerGinRoute(docker, "POST", "Restart docker container", "/restart", dockerApi.Restart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthBlockPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
auth.WriteBlockPage(w, http.StatusForbidden, "Forbidden", "Login", "/login")
|
||||||
|
}
|
||||||
7
cmd/debug_page_prod.go
Normal file
7
cmd/debug_page_prod.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build production
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
func listenDebugServer() {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
18
cmd/h2c_test_server/Dockerfile
Normal file
18
cmd/h2c_test_server/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM golang:1.25.6-alpine AS builder
|
||||||
|
|
||||||
|
HEALTHCHECK NONE
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
COPY main.go ./
|
||||||
|
|
||||||
|
RUN go build -o h2c_test_server main.go
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
COPY --from=builder /src/h2c_test_server /app/run
|
||||||
|
|
||||||
|
USER 1001:1001
|
||||||
|
|
||||||
|
CMD ["/app/run"]
|
||||||
7
cmd/h2c_test_server/go.mod
Normal file
7
cmd/h2c_test_server/go.mod
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module github.com/yusing/godoxy/cmd/h2c_test_server
|
||||||
|
|
||||||
|
go 1.25.6
|
||||||
|
|
||||||
|
require golang.org/x/net v0.49.0
|
||||||
|
|
||||||
|
require golang.org/x/text v0.33.0 // indirect
|
||||||
4
cmd/h2c_test_server/go.sum
Normal file
4
cmd/h2c_test_server/go.sum
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
26
cmd/h2c_test_server/main.go
Normal file
26
cmd/h2c_test_server/main.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
"golang.org/x/net/http2/h2c"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
})
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: ":80",
|
||||||
|
Handler: h2c.NewHandler(handler, &http2.Server{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("H2C server listening on :80")
|
||||||
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatalf("ListenAndServe: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
89
cmd/main.go
Executable file
89
cmd/main.go
Executable file
@@ -0,0 +1,89 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/yusing/godoxy/internal/api"
|
||||||
|
"github.com/yusing/godoxy/internal/auth"
|
||||||
|
"github.com/yusing/godoxy/internal/common"
|
||||||
|
"github.com/yusing/godoxy/internal/config"
|
||||||
|
"github.com/yusing/godoxy/internal/dnsproviders"
|
||||||
|
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||||
|
"github.com/yusing/godoxy/internal/logging"
|
||||||
|
"github.com/yusing/godoxy/internal/logging/memlogger"
|
||||||
|
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||||
|
"github.com/yusing/godoxy/internal/metrics/uptime"
|
||||||
|
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||||
|
"github.com/yusing/godoxy/internal/route/rules"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
"github.com/yusing/goutils/server"
|
||||||
|
"github.com/yusing/goutils/task"
|
||||||
|
"github.com/yusing/goutils/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parallel(fns ...func()) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, fn := range fns {
|
||||||
|
wg.Go(fn)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
initProfiling()
|
||||||
|
|
||||||
|
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
|
||||||
|
log.Info().Msgf("GoDoxy version %s", version.Get())
|
||||||
|
log.Trace().Msg("trace enabled")
|
||||||
|
parallel(
|
||||||
|
dnsproviders.InitProviders,
|
||||||
|
iconlist.InitCache,
|
||||||
|
systeminfo.Poller.Start,
|
||||||
|
middleware.LoadComposeFiles,
|
||||||
|
)
|
||||||
|
|
||||||
|
if common.APIJWTSecret == nil {
|
||||||
|
log.Warn().Msg("API_JWT_SECRET is not set, using random key")
|
||||||
|
common.APIJWTSecret = common.RandomJWTKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range common.RequiredDirectories {
|
||||||
|
prepareDirectory(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
gperr.LogWarn("errors in config", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.StartProxyServers()
|
||||||
|
|
||||||
|
if err := auth.Initialize(); err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("failed to initialize authentication")
|
||||||
|
}
|
||||||
|
rules.InitAuthHandler(auth.AuthOrProceed)
|
||||||
|
|
||||||
|
// API Handler needs to start after auth is initialized.
|
||||||
|
server.StartServer(task.RootTask("api_server", false), server.Options{
|
||||||
|
Name: "api",
|
||||||
|
HTTPAddr: common.APIHTTPAddr,
|
||||||
|
Handler: api.NewHandler(),
|
||||||
|
})
|
||||||
|
|
||||||
|
listenDebugServer()
|
||||||
|
|
||||||
|
uptime.Poller.Start()
|
||||||
|
config.WatchChanges()
|
||||||
|
|
||||||
|
task.WaitExit(config.Value().TimeoutShutdown)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareDirectory(dir string) {
|
||||||
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||||
|
if err = os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
log.Fatal().Msgf("failed to create directory %s: %v", dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
cmd/pprof_production.go
Normal file
7
cmd/pprof_production.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build !pprof
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
func initProfiling() {
|
||||||
|
// no profiling in production
|
||||||
|
}
|
||||||
48
cmd/pprof_prof.go
Normal file
48
cmd/pprof_prof.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
//go:build pprof
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
_ "net/http/pprof"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
strutils "github.com/yusing/goutils/strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initProfiling() {
|
||||||
|
go func() {
|
||||||
|
log.Info().Msgf("pprof server started at http://localhost:7777/debug/pprof/")
|
||||||
|
log.Error().Err(http.ListenAndServe(":7777", nil)).Msg("pprof server failed")
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(time.Second * 10)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
var m runtime.MemStats
|
||||||
|
var gcStats debug.GCStats
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
debug.ReadGCStats(&gcStats)
|
||||||
|
|
||||||
|
log.Info().Msgf("-----------------------------------------------------")
|
||||||
|
log.Info().Msgf("Timestamp: %s", time.Now().Format(time.RFC3339))
|
||||||
|
log.Info().Msgf(" Go Heap - In Use (Alloc/HeapAlloc): %s", strutils.FormatByteSize(m.Alloc))
|
||||||
|
log.Info().Msgf(" Go Heap - Reserved from OS (HeapSys): %s", strutils.FormatByteSize(m.HeapSys))
|
||||||
|
log.Info().Msgf(" Go Stacks - In Use (StackInuse): %s", strutils.FormatByteSize(m.StackInuse))
|
||||||
|
log.Info().Msgf(" Go Runtime - Other Sys (MSpanInuse, MCacheInuse, BuckHashSys, GCSys, OtherSys): %s", strutils.FormatByteSize(m.MSpanInuse+m.MCacheInuse+m.BuckHashSys+m.GCSys+m.OtherSys))
|
||||||
|
log.Info().Msgf(" Go Runtime - Total from OS (Sys): %s", strutils.FormatByteSize(m.Sys))
|
||||||
|
log.Info().Msgf(" Go Runtime - Freed from OS (HeapReleased): %s", strutils.FormatByteSize(m.HeapReleased))
|
||||||
|
log.Info().Msgf(" Number of Goroutines: %d", runtime.NumGoroutine())
|
||||||
|
log.Info().Msgf(" Number of completed GC cycles: %d", m.NumGC)
|
||||||
|
log.Info().Msgf(" Number of GCs: %d", gcStats.NumGC)
|
||||||
|
log.Info().Msgf(" Total GC time: %s", gcStats.PauseTotal)
|
||||||
|
log.Info().Msgf(" Last GC time: %s", gcStats.LastGC.Format(time.DateTime))
|
||||||
|
log.Info().Msg("-----------------------------------------------------")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -1,33 +1,84 @@
|
|||||||
|
---
|
||||||
services:
|
services:
|
||||||
frontend:
|
socket-proxy:
|
||||||
image: ghcr.io/yusing/go-proxy-frontend:latest
|
container_name: socket-proxy
|
||||||
container_name: go-proxy-frontend
|
image: ghcr.io/yusing/socket-proxy:latest
|
||||||
|
environment:
|
||||||
|
- ALLOW_START=1
|
||||||
|
- ALLOW_STOP=1
|
||||||
|
- ALLOW_RESTARTS=1
|
||||||
|
- CONTAINERS=1
|
||||||
|
- EVENTS=1
|
||||||
|
- INFO=1
|
||||||
|
- PING=1
|
||||||
|
- POST=1
|
||||||
|
- VERSION=1
|
||||||
|
volumes:
|
||||||
|
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host
|
tmpfs:
|
||||||
labels:
|
- /run
|
||||||
- proxy.aliases=gp
|
ports:
|
||||||
- proxy.gp.port=3000
|
- ${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}:2375
|
||||||
|
frontend:
|
||||||
|
image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}
|
||||||
|
# lite variant
|
||||||
|
# image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}-lite
|
||||||
|
container_name: godoxy-frontend
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file: .env
|
||||||
|
# comment out `user` for lite variant
|
||||||
|
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /app/.next/cache # next image caching
|
||||||
|
|
||||||
|
# for lite variant, do not change uid/gid
|
||||||
|
# - /var/cache/nginx:uid=101,gid=101
|
||||||
|
# - /run:uid=101,gid=101
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- all
|
||||||
depends_on:
|
depends_on:
|
||||||
- app
|
- app
|
||||||
|
labels:
|
||||||
|
proxy.aliases: ${GODOXY_FRONTEND_ALIASES:-godoxy}
|
||||||
|
# proxy.#1.middlewares.cidr_whitelist: |
|
||||||
|
# status: 403
|
||||||
|
# message: IP not allowed
|
||||||
|
# allow:
|
||||||
|
# - 127.0.0.1
|
||||||
|
# - 10.0.0.0/8
|
||||||
|
# - 192.168.0.0/16
|
||||||
|
# - 172.16.0.0/12
|
||||||
app:
|
app:
|
||||||
image: ghcr.io/yusing/go-proxy:latest
|
image: ghcr.io/yusing/godoxy:${TAG:-latest}
|
||||||
container_name: go-proxy
|
container_name: godoxy-proxy
|
||||||
restart: always
|
restart: always
|
||||||
network_mode: host
|
network_mode: host # do not change this
|
||||||
|
env_file: .env
|
||||||
|
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
|
||||||
|
depends_on:
|
||||||
|
socket-proxy:
|
||||||
|
condition: service_started
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- all
|
||||||
|
cap_add:
|
||||||
|
- NET_BIND_SERVICE
|
||||||
environment:
|
environment:
|
||||||
# (Optional) change this to your timezone to get correct log timestamp
|
- DOCKER_HOST=tcp://${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}
|
||||||
TZ: ETC/UTC
|
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
|
- ./logs:/app/logs
|
||||||
|
- ./error_pages:/app/error_pages:ro
|
||||||
|
- ./data:/app/data
|
||||||
|
|
||||||
# (Optional) choose one of below to enable https
|
# This path stores certs obtained from autocert and agent TLS client certs
|
||||||
# 1. use existing certificate
|
- ./certs:/app/certs
|
||||||
# if your cert is not named `cert.crt` change `cert_path` in `config/config.yml`
|
|
||||||
# if your cert key is not named `priv.key` change `key_path` in `config/config.yml`
|
|
||||||
|
|
||||||
# - /path/to/certs:/app/certs
|
# mount existing certificate
|
||||||
|
# - /path/to/certs/cert.crt:/app/certs/cert.crt
|
||||||
# 2. use autocert, certs will be stored in ./certs (or other path you specify)
|
# - /path/to/certs/priv.key:/app/certs/priv.key
|
||||||
|
|
||||||
# - ./certs:/app/certs
|
|
||||||
|
|||||||
@@ -1,37 +1,164 @@
|
|||||||
# Autocert (choose one below and uncomment to enable)
|
# Autocert (choose one below and uncomment to enable)
|
||||||
|
#
|
||||||
# 1. use existing cert
|
# 1. use existing cert
|
||||||
|
|
||||||
# autocert:
|
# autocert:
|
||||||
# provider: local
|
# provider: local
|
||||||
# cert_path: certs/cert.crt # optional, uncomment only if you need to change it
|
# cert_path: /path/to/cert.crt # default: /app/certs/cert.crt
|
||||||
# key_path: certs/priv.key # optional, uncomment only if you need to change it
|
# key_path: /path/to/priv.key # default: /app/certs/priv.key
|
||||||
|
|
||||||
# 2. cloudflare
|
# 2. cloudflare
|
||||||
# autocert:
|
# autocert:
|
||||||
# provider: cloudflare
|
# provider: cloudflare
|
||||||
# email: # ACME Email
|
# email: abc@gmail.com # ACME Email
|
||||||
# domains: # a list of domains for cert registration
|
# domains: # a list of domains for cert registration
|
||||||
# - x.y.z
|
# - "*.domain.com"
|
||||||
|
# - "domain.com"
|
||||||
# options:
|
# options:
|
||||||
# - auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
# auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
||||||
|
|
||||||
# 3. other providers, check readme for more
|
# 3. other providers, see https://docs.godoxy.dev/DNS-01-Providers
|
||||||
|
|
||||||
|
# Access Control
|
||||||
|
# When enabled, it will be applied globally at connection level,
|
||||||
|
# all incoming connections (web, tcp and udp) will be checked against the ACL rules.
|
||||||
|
|
||||||
|
# acl:
|
||||||
|
# default: allow # or deny (default: allow)
|
||||||
|
# allow_local: true # or false (default: true)
|
||||||
|
# allow:
|
||||||
|
# - ip:1.2.3.4
|
||||||
|
# - cidr:1.2.3.4/32
|
||||||
|
# - country:US
|
||||||
|
# - timezone:Asia/Shanghai
|
||||||
|
# deny:
|
||||||
|
# - ip:1.2.3.4
|
||||||
|
# - cidr:1.2.3.4/32
|
||||||
|
# - country:US
|
||||||
|
# - timezone:Asia/Shanghai
|
||||||
|
# log: # warning: logging ACL can be slow based on the number of incoming connections and configured rules
|
||||||
|
# path: /app/logs/acl.log # (default: none)
|
||||||
|
# stdout: false # (default: false)
|
||||||
|
# keep: 30 days # (default: 30 days)
|
||||||
|
# log_allowed: false # (default: false)
|
||||||
|
# notify:
|
||||||
|
# interval: 1m # (default: 1m)
|
||||||
|
# to: [gotify, discord] # names under providers.notification
|
||||||
|
# include_allowed: false # (default: false)
|
||||||
|
|
||||||
|
entrypoint:
|
||||||
|
# Proxy Protocol: https://www.haproxy.com/blog/use-the-proxy-protocol-to-preserve-a-clients-ip-address
|
||||||
|
# When set to true, web entrypoint and all tcp routes will be wrapped with Proxy Protocol listener in order to preserve the client's IP address.
|
||||||
|
# Note that HTTP/3 with proxy protocol is not supported yet.
|
||||||
|
support_proxy_protocol: false
|
||||||
|
|
||||||
|
# Below define an example of middleware config
|
||||||
|
# 1. set security headers
|
||||||
|
# 2. block non local IP connections
|
||||||
|
# 3. redirect HTTP to HTTPS
|
||||||
|
#
|
||||||
|
middlewares:
|
||||||
|
- use: CloudflareRealIP
|
||||||
|
- use: ModifyResponse
|
||||||
|
set_headers:
|
||||||
|
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
|
||||||
|
Access-Control-Allow-Headers: "*"
|
||||||
|
Access-Control-Allow-Origin: "*"
|
||||||
|
Access-Control-Max-Age: 180
|
||||||
|
Vary: "*"
|
||||||
|
X-XSS-Protection: 1; mode=block
|
||||||
|
Content-Security-Policy: "object-src 'self'; frame-ancestors 'self';"
|
||||||
|
X-Content-Type-Options: nosniff
|
||||||
|
X-Frame-Options: SAMEORIGIN
|
||||||
|
Referrer-Policy: same-origin
|
||||||
|
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
|
||||||
|
# - use: RedirectHTTP
|
||||||
|
|
||||||
|
# below enables access log
|
||||||
|
access_log:
|
||||||
|
format: combined
|
||||||
|
path: /app/logs/entrypoint.log
|
||||||
|
stdout: false # (default: false)
|
||||||
|
keep: 30 days # (default: 30 days)
|
||||||
|
|
||||||
|
# customize behavior for non-existent routes, e.g. pass over to another proxy
|
||||||
|
#
|
||||||
|
# rules:
|
||||||
|
# not_found:
|
||||||
|
# - name: default
|
||||||
|
# do: proxy http://other-proxy:8080
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
healthcheck:
|
||||||
|
interval: 5s
|
||||||
|
timeout: 15s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
include:
|
# include files are standalone yaml files under `config/` directory
|
||||||
- providers.yml # config/providers.yml
|
#
|
||||||
# add some more below if you want
|
# include:
|
||||||
# - file1.yml # config/file_1.yml
|
# - file1.yml
|
||||||
# - file2.yml
|
# - file2.yml
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
# for value format, see https://docs.docker.com/reference/cli/dockerd/
|
# $DOCKER_HOST implies environment variable `DOCKER_HOST` or unix:///var/run/docker.sock by default
|
||||||
# $DOCKER_HOST implies unix:///var/run/docker.sock by default
|
|
||||||
local: $DOCKER_HOST
|
local: $DOCKER_HOST
|
||||||
|
|
||||||
|
# explicit only mode
|
||||||
|
# only containers with explicit aliases will be proxied
|
||||||
|
# add "!" after provider name to enable explicit only mode
|
||||||
|
#
|
||||||
|
# local!: $DOCKER_HOST
|
||||||
|
#
|
||||||
# add more docker providers if needed
|
# add more docker providers if needed
|
||||||
|
# for value format, see https://docs.docker.com/reference/cli/dockerd/
|
||||||
|
#
|
||||||
# remote-1: tcp://10.0.2.1:2375
|
# remote-1: tcp://10.0.2.1:2375
|
||||||
# remote-2: ssh://root:1234@10.0.2.2
|
# remote-2: ssh://root:1234@10.0.2.2
|
||||||
|
|
||||||
# Fixed options (optional, non hot-reloadable)
|
# notification providers
|
||||||
|
#
|
||||||
|
# notification:
|
||||||
|
# - name: ntfy
|
||||||
|
# provider: ntfy
|
||||||
|
# url: https://ntfy.domain.tld
|
||||||
|
# topic: godoxy
|
||||||
|
# - name: gotify
|
||||||
|
# provider: gotify
|
||||||
|
# url: https://gotify.domain.tld
|
||||||
|
# token: abcd
|
||||||
|
# - name: discord
|
||||||
|
# provider: webhook
|
||||||
|
# url: https://discord.com/api/webhooks/...
|
||||||
|
# template: discord # this means use payload template from internal/notif/templates/discord.json
|
||||||
|
# - name: pushover
|
||||||
|
# provider: webhook
|
||||||
|
# url: https://api.pushover.net/1/messages.json
|
||||||
|
# mime_type: application/x-www-form-urlencoded
|
||||||
|
# payload: '{"token": "your-app-token", "user": "your-user-key", "title": $title, "message": $message}'
|
||||||
|
|
||||||
# timeout_shutdown: 5
|
# Proxmox providers (for idlesleep support for proxmox LXCs)
|
||||||
# redirect_to_https: false
|
#
|
||||||
|
# proxmox:
|
||||||
|
# - url: https://pve.domain.com:8006/api2/json
|
||||||
|
# token_id: root@pam!abcdef
|
||||||
|
# secret: aaaa-bbbb-cccc-dddd
|
||||||
|
# no_tls_verify: true
|
||||||
|
|
||||||
|
# Match domains
|
||||||
|
# See https://docs.godoxy.dev/Certificates-and-domain-matching
|
||||||
|
#
|
||||||
|
# match_domains:
|
||||||
|
# - my.site
|
||||||
|
# - node1.my.app
|
||||||
|
|
||||||
|
# homepage config
|
||||||
|
homepage:
|
||||||
|
# use default app categories detected from alias or docker image name
|
||||||
|
use_default_categories: true
|
||||||
|
|
||||||
|
# Below are fixed options (non hot-reloadable)
|
||||||
|
|
||||||
|
# timeout for shutdown (in seconds)
|
||||||
|
timeout_shutdown: 5
|
||||||
|
|||||||
7
dev.Dockerfile
Normal file
7
dev.Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
CMD ["/app/run"]
|
||||||
257
dev.compose.yml
Normal file
257
dev.compose.yml
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
x-benchmark: &benchmark
|
||||||
|
restart: no
|
||||||
|
labels:
|
||||||
|
proxy.exclude: true
|
||||||
|
proxy.#1.healthcheck.disable: true
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: godoxy-dev
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: dev.Dockerfile
|
||||||
|
container_name: godoxy-proxy-dev
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file: dev.env
|
||||||
|
environment:
|
||||||
|
DOCKER_HOST: unix:///var/run/docker.sock
|
||||||
|
TZ: Asia/Hong_Kong
|
||||||
|
API_ADDR: 127.0.0.1:8999
|
||||||
|
API_USER: dev
|
||||||
|
API_PASSWORD: 1234
|
||||||
|
API_SKIP_ORIGIN_CHECK: true
|
||||||
|
API_JWT_TTL: 24h
|
||||||
|
DEBUG: true
|
||||||
|
API_JWT_SECRET: 1234567891234567
|
||||||
|
labels:
|
||||||
|
proxy.exclude: true
|
||||||
|
proxy.#1.healthcheck.disable: true
|
||||||
|
ipc: host
|
||||||
|
network_mode: host
|
||||||
|
volumes:
|
||||||
|
- ./bin/godoxy:/app/run:ro
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./dev-data/config:/app/config
|
||||||
|
- ./dev-data/certs:/app/certs
|
||||||
|
- ./dev-data/error_pages:/app/error_pages:ro
|
||||||
|
- ./dev-data/data:/app/data
|
||||||
|
- ./dev-data/logs:/app/logs
|
||||||
|
- ~/certs/myCA.pem:/etc/ssl/certs/ca.crt:ro
|
||||||
|
parca:
|
||||||
|
image: ghcr.io/parca-dev/parca:v0.24.2
|
||||||
|
container_name: godoxy-parca
|
||||||
|
restart: unless-stopped
|
||||||
|
command: [/parca, --config-path, /parca.yaml]
|
||||||
|
network_mode: host
|
||||||
|
# ports:
|
||||||
|
# - 7070:7070
|
||||||
|
configs:
|
||||||
|
- source: parca
|
||||||
|
target: /parca.yaml
|
||||||
|
labels:
|
||||||
|
proxy.#1.port: "7070"
|
||||||
|
tinyauth:
|
||||||
|
image: ghcr.io/steveiliop56/tinyauth:v3
|
||||||
|
container_name: tinyauth
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- SECRET=12345678912345671234567891234567
|
||||||
|
- APP_URL=https://tinyauth.my.app
|
||||||
|
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
|
||||||
|
labels:
|
||||||
|
proxy.tinyauth.port: "3000"
|
||||||
|
jotty: # issue #182
|
||||||
|
image: ghcr.io/fccview/jotty:latest
|
||||||
|
container_name: jotty
|
||||||
|
user: "1000:1000"
|
||||||
|
tmpfs:
|
||||||
|
- /app/data:rw,uid=1000,gid=1000
|
||||||
|
- /app/config:rw,uid=1000,gid=1000
|
||||||
|
- /app/.next/cache:rw,uid=1000,gid=1000
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
labels:
|
||||||
|
proxy.aliases: "jotty.my.app"
|
||||||
|
postgres-test:
|
||||||
|
image: postgres:18-alpine
|
||||||
|
container_name: postgres-test
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=postgres
|
||||||
|
- POSTGRES_PASSWORD=postgres
|
||||||
|
- POSTGRES_DB=postgres
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
h2c_test_server:
|
||||||
|
build:
|
||||||
|
context: cmd/h2c_test_server
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: h2c_test
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
proxy.#1.scheme: h2c
|
||||||
|
proxy.#1.port: 80
|
||||||
|
bench: # returns 4096 bytes of random data
|
||||||
|
<<: *benchmark
|
||||||
|
build:
|
||||||
|
context: cmd/bench_server
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: bench
|
||||||
|
godoxy:
|
||||||
|
<<: *benchmark
|
||||||
|
build: .
|
||||||
|
container_name: godoxy-benchmark
|
||||||
|
ports:
|
||||||
|
- 8080:80
|
||||||
|
configs:
|
||||||
|
- source: godoxy_config
|
||||||
|
target: /app/config/config.yml
|
||||||
|
- source: godoxy_provider
|
||||||
|
target: /app/config/providers.yml
|
||||||
|
traefik:
|
||||||
|
<<: *benchmark
|
||||||
|
image: traefik:latest
|
||||||
|
container_name: traefik
|
||||||
|
command:
|
||||||
|
- --api.insecure=true
|
||||||
|
- --entrypoints.web.address=:8081
|
||||||
|
- --providers.file.directory=/etc/traefik/dynamic
|
||||||
|
- --providers.file.watch=true
|
||||||
|
- --log.level=ERROR
|
||||||
|
ports:
|
||||||
|
- 8081:8081
|
||||||
|
configs:
|
||||||
|
- source: traefik_config
|
||||||
|
target: /etc/traefik/dynamic/routes.yml
|
||||||
|
caddy:
|
||||||
|
<<: *benchmark
|
||||||
|
image: caddy:latest
|
||||||
|
container_name: caddy
|
||||||
|
ports:
|
||||||
|
- 8082:80
|
||||||
|
configs:
|
||||||
|
- source: caddy_config
|
||||||
|
target: /etc/caddy/Caddyfile
|
||||||
|
tmpfs:
|
||||||
|
- /data
|
||||||
|
- /config
|
||||||
|
nginx:
|
||||||
|
<<: *benchmark
|
||||||
|
image: nginx:latest
|
||||||
|
container_name: nginx
|
||||||
|
command: nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
|
||||||
|
ports:
|
||||||
|
- 8083:80
|
||||||
|
configs:
|
||||||
|
- source: nginx_config
|
||||||
|
target: /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
configs:
|
||||||
|
godoxy_config:
|
||||||
|
content: |
|
||||||
|
providers:
|
||||||
|
include:
|
||||||
|
- providers.yml
|
||||||
|
godoxy_provider:
|
||||||
|
content: |
|
||||||
|
bench.domain.com:
|
||||||
|
host: bench
|
||||||
|
traefik_config:
|
||||||
|
content: |
|
||||||
|
http:
|
||||||
|
routers:
|
||||||
|
bench:
|
||||||
|
rule: "Host(`bench.domain.com`)"
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
service: bench
|
||||||
|
services:
|
||||||
|
bench:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://bench:80"
|
||||||
|
caddy_config:
|
||||||
|
content: |
|
||||||
|
{
|
||||||
|
admin off
|
||||||
|
auto_https off
|
||||||
|
default_bind 0.0.0.0
|
||||||
|
|
||||||
|
servers {
|
||||||
|
protocols h1 h2c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http://bench.domain.com {
|
||||||
|
reverse_proxy bench:80
|
||||||
|
}
|
||||||
|
nginx_config:
|
||||||
|
content: |
|
||||||
|
worker_processes auto;
|
||||||
|
worker_rlimit_nofile 65535;
|
||||||
|
error_log /dev/null;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 10240;
|
||||||
|
multi_accept on;
|
||||||
|
use epoll;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
access_log off;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
keepalive_requests 10000;
|
||||||
|
|
||||||
|
upstream backend {
|
||||||
|
server bench:80;
|
||||||
|
keepalive 128;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
server_name _;
|
||||||
|
http2 on;
|
||||||
|
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name bench.domain.com;
|
||||||
|
http2 on;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
proxy_set_header Host $$host;
|
||||||
|
proxy_set_header X-Real-IP $$remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $$proxy_add_x_forwarded_for;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parca:
|
||||||
|
content: |
|
||||||
|
object_storage:
|
||||||
|
bucket:
|
||||||
|
type: "FILESYSTEM"
|
||||||
|
config:
|
||||||
|
directory: "./data"
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: "parca"
|
||||||
|
scrape_interval: "1s"
|
||||||
|
static_configs:
|
||||||
|
- targets: [ 'localhost:7777' ]
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# Adding provider support
|
|
||||||
|
|
||||||
## **CloudDNS** as an example
|
|
||||||
|
|
||||||
1. Fork this repo, modify [autocert.go](../src/go-proxy/autocert.go#L305)
|
|
||||||
|
|
||||||
```go
|
|
||||||
var providersGenMap = map[string]ProviderGenerator{
|
|
||||||
"cloudflare": providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
|
|
||||||
// add here, e.g.
|
|
||||||
"clouddns": providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Go to [https://go-acme.github.io/lego/dns/clouddns](https://go-acme.github.io/lego/dns/clouddns/) and check for required config
|
|
||||||
|
|
||||||
3. Build `go-proxy` with `make build`
|
|
||||||
|
|
||||||
4. Set required config in `config.yml` `autocert` -> `options` section
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# From https://go-acme.github.io/lego/dns/clouddns/
|
|
||||||
CLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \
|
|
||||||
CLOUDDNS_EMAIL=you@example.com \
|
|
||||||
CLOUDDNS_PASSWORD=b9841238feb177a84330f \
|
|
||||||
lego --email you@example.com --dns clouddns --domains my.example.org run
|
|
||||||
```
|
|
||||||
|
|
||||||
Should turn into:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
autocert:
|
|
||||||
...
|
|
||||||
options:
|
|
||||||
client_id: bLsdFAks23429841238feb177a572aX
|
|
||||||
email: you@example.com
|
|
||||||
password: b9841238feb177a84330f
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Run with `GOPROXY_NO_SCHEMA_VALIDATION=1` and test if it works
|
|
||||||
6. Commit and create pull request
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
# Benchmarks
|
|
||||||
|
|
||||||
Benchmarked with `wrk` and `traefik/whoami`'s `/bench` endpoint
|
|
||||||
|
|
||||||
## Remote benchmark
|
|
||||||
|
|
||||||
- Direct connection
|
|
||||||
|
|
||||||
```shell
|
|
||||||
root@yusing-pc:~# wrk -t 10 -c 200 -d 10s -H "Host: bench.6uo.me" --latency http://10.0.100.3:8003/bench
|
|
||||||
Running 10s test @ http://10.0.100.3:8003/bench
|
|
||||||
10 threads and 200 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 94.75ms 199.92ms 1.68s 91.27%
|
|
||||||
Req/Sec 4.24k 1.79k 18.79k 72.13%
|
|
||||||
Latency Distribution
|
|
||||||
50% 1.14ms
|
|
||||||
75% 120.23ms
|
|
||||||
90% 245.63ms
|
|
||||||
99% 1.03s
|
|
||||||
423444 requests in 10.10s, 50.88MB read
|
|
||||||
Socket errors: connect 0, read 0, write 0, timeout 29
|
|
||||||
Requests/sec: 41926.32
|
|
||||||
Transfer/sec: 5.04MB
|
|
||||||
```
|
|
||||||
|
|
||||||
- With reverse proxy
|
|
||||||
|
|
||||||
```shell
|
|
||||||
root@yusing-pc:~# wrk -t 10 -c 200 -d 10s -H "Host: bench.6uo.me" --latency http://10.0.1.7/bench
|
|
||||||
Running 10s test @ http://10.0.1.7/bench
|
|
||||||
10 threads and 200 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 79.35ms 169.79ms 1.69s 92.55%
|
|
||||||
Req/Sec 4.27k 1.90k 19.61k 75.81%
|
|
||||||
Latency Distribution
|
|
||||||
50% 1.12ms
|
|
||||||
75% 105.66ms
|
|
||||||
90% 200.22ms
|
|
||||||
99% 814.59ms
|
|
||||||
409836 requests in 10.10s, 49.25MB read
|
|
||||||
Socket errors: connect 0, read 0, write 0, timeout 18
|
|
||||||
Requests/sec: 40581.61
|
|
||||||
Transfer/sec: 4.88MB
|
|
||||||
```
|
|
||||||
|
|
||||||
## Local benchmark (client running wrk and `go-proxy` server are under same proxmox host but different LXCs)
|
|
||||||
|
|
||||||
- Direct connection
|
|
||||||
|
|
||||||
```shell
|
|
||||||
root@http-benchmark-client:~# wrk -t 10 -c 200 -d 10s --latency http://10.0.100.1/bench
|
|
||||||
Running 10s test @ http://10.0.100.1/bench
|
|
||||||
10 threads and 200 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 434.08us 539.35us 8.76ms 85.28%
|
|
||||||
Req/Sec 67.71k 6.31k 87.21k 71.20%
|
|
||||||
Latency Distribution
|
|
||||||
50% 153.00us
|
|
||||||
75% 646.00us
|
|
||||||
90% 1.18ms
|
|
||||||
99% 2.38ms
|
|
||||||
6739591 requests in 10.01s, 809.85MB read
|
|
||||||
Requests/sec: 673608.15
|
|
||||||
Transfer/sec: 80.94MB
|
|
||||||
```
|
|
||||||
|
|
||||||
- With `go-proxy` reverse proxy
|
|
||||||
|
|
||||||
```shell
|
|
||||||
root@http-benchmark-client:~# wrk -t 10 -c 200 -d 10s -H "Host: bench.6uo.me" --latency http://10.0.1.7/bench
|
|
||||||
Running 10s test @ http://10.0.1.7/bench
|
|
||||||
10 threads and 200 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 1.23ms 0.96ms 11.43ms 72.09%
|
|
||||||
Req/Sec 17.48k 1.76k 21.48k 70.20%
|
|
||||||
Latency Distribution
|
|
||||||
50% 0.98ms
|
|
||||||
75% 1.76ms
|
|
||||||
90% 2.54ms
|
|
||||||
99% 4.24ms
|
|
||||||
1739079 requests in 10.01s, 208.97MB read
|
|
||||||
Requests/sec: 173779.44
|
|
||||||
Transfer/sec: 20.88MB
|
|
||||||
```
|
|
||||||
|
|
||||||
- With `traefik-v3`
|
|
||||||
|
|
||||||
```shell
|
|
||||||
root@traefik-benchmark:~# wrk -t10 -c200 -d10s -H "Host: benchmark.whoami" --latency http://127.0.0.1:8000/bench
|
|
||||||
Running 10s test @ http://127.0.0.1:8000/bench
|
|
||||||
10 threads and 200 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 2.81ms 10.36ms 180.26ms 98.57%
|
|
||||||
Req/Sec 11.35k 1.74k 13.76k 85.54%
|
|
||||||
Latency Distribution
|
|
||||||
50% 1.59ms
|
|
||||||
75% 2.27ms
|
|
||||||
90% 3.17ms
|
|
||||||
99% 37.91ms
|
|
||||||
1125723 requests in 10.01s, 109.50MB read
|
|
||||||
Requests/sec: 112499.59
|
|
||||||
Transfer/sec: 10.94MB
|
|
||||||
```
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# Supported DNS Providers
|
|
||||||
|
|
||||||
<!-- TOC -->
|
|
||||||
|
|
||||||
- [Supported DNS Providers](#supported-dns-providers)
|
|
||||||
- [Cloudflare](#cloudflare)
|
|
||||||
- [CloudDNS](#clouddns)
|
|
||||||
- [DuckDNS](#duckdns)
|
|
||||||
- [OVHCloud](#ovhcloud)
|
|
||||||
- [Implement other DNS providers](#implement-other-dns-providers)
|
|
||||||
|
|
||||||
## Cloudflare
|
|
||||||
|
|
||||||
`auth_token` your zone API token
|
|
||||||
|
|
||||||
Follow [this guide](https://cloudkul.com/blog/automcatic-renew-and-generate-ssl-on-your-website-using-lego-client/) to create a new token with `Zone.DNS` read and edit permissions
|
|
||||||
|
|
||||||
## CloudDNS
|
|
||||||
|
|
||||||
- `client_id`
|
|
||||||
|
|
||||||
- `email`
|
|
||||||
|
|
||||||
- `password`
|
|
||||||
|
|
||||||
## DuckDNS
|
|
||||||
|
|
||||||
- `token`: DuckDNS Token
|
|
||||||
|
|
||||||
Tested by [earvingad](https://github.com/earvingad)
|
|
||||||
|
|
||||||
## OVHCloud
|
|
||||||
|
|
||||||
_Note, `application_key` and `oauth2_config` **CANNOT** be used together_
|
|
||||||
|
|
||||||
- `api_endpoint`: Endpoint URL, or one of
|
|
||||||
- `ovh-eu`,
|
|
||||||
- `ovh-ca`,
|
|
||||||
- `ovh-us`,
|
|
||||||
- `kimsufi-eu`,
|
|
||||||
- `kimsufi-ca`,
|
|
||||||
- `soyoustart-eu`,
|
|
||||||
- `soyoustart-ca`
|
|
||||||
- `application_secret`
|
|
||||||
- `application_key`
|
|
||||||
- `consumer_key`
|
|
||||||
- `oauth2_config`: Client ID and Client Secret
|
|
||||||
- `client_id`
|
|
||||||
- `client_secret`
|
|
||||||
|
|
||||||
## Implement other DNS providers
|
|
||||||
|
|
||||||
See [add_dns_provider.md](docs/add_dns_provider.md)
|
|
||||||
315
docs/docker.md
315
docs/docker.md
@@ -1,315 +0,0 @@
|
|||||||
# Docker compose guide
|
|
||||||
|
|
||||||
## Table of content
|
|
||||||
|
|
||||||
<!-- TOC -->
|
|
||||||
|
|
||||||
- [Docker compose guide](#docker-compose-guide)
|
|
||||||
- [Table of content](#table-of-content)
|
|
||||||
- [Setup](#setup)
|
|
||||||
- [Labels](#labels)
|
|
||||||
- [Syntax](#syntax)
|
|
||||||
- [Fields](#fields)
|
|
||||||
- [Key-value mapping example](#key-value-mapping-example)
|
|
||||||
- [List example](#list-example)
|
|
||||||
- [Troubleshooting](#troubleshooting)
|
|
||||||
- [Docker compose examples](#docker-compose-examples)
|
|
||||||
- [Services URLs for above examples](#services-urls-for-above-examples)
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
1. Install `wget` if not already
|
|
||||||
|
|
||||||
- Ubuntu based: `sudo apt install -y wget`
|
|
||||||
- Fedora based: `sudo yum install -y wget`
|
|
||||||
- Arch based: `sudo pacman -Sy wget`
|
|
||||||
|
|
||||||
2. Run setup script
|
|
||||||
|
|
||||||
`bash <(wget -qO- https://github.com/yusing/go-proxy/raw/main/setup-docker.sh)`
|
|
||||||
|
|
||||||
It will setup folder structure and required config files
|
|
||||||
|
|
||||||
3. Verify folder structure and then `cd go-proxy`
|
|
||||||
|
|
||||||
```plain
|
|
||||||
go-proxy
|
|
||||||
├── certs
|
|
||||||
├── compose.yml
|
|
||||||
└── config
|
|
||||||
├── config.yml
|
|
||||||
└── providers.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Enable HTTPs _(optional)_
|
|
||||||
|
|
||||||
Mount a folder (to store obtained certs) or (containing existing cert)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
go-proxy:
|
|
||||||
...
|
|
||||||
volumes:
|
|
||||||
- ./certs:/app/certs
|
|
||||||
```
|
|
||||||
|
|
||||||
To use **autocert**, complete that section in `config.yml`, e.g.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
autocert:
|
|
||||||
email: john.doe@x.y.z # ACME Email
|
|
||||||
domains: # a list of domains for cert registration
|
|
||||||
- x.y.z
|
|
||||||
provider: cloudflare
|
|
||||||
options:
|
|
||||||
- auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
|
||||||
```
|
|
||||||
|
|
||||||
To use **existing certificate**, set path for cert and key in `config.yml`, e.g.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
autocert:
|
|
||||||
cert_path: /app/certs/cert.crt
|
|
||||||
key_path: /app/certs/priv.key
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Modify `compose.yml` to fit your needs
|
|
||||||
|
|
||||||
6. Run `docker compose up -d` to start the container
|
|
||||||
|
|
||||||
7. Navigate to Web panel `http://gp.yourdomain.com` or use **Visual Studio Code (provides schema check)** to edit proxy config
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
|
||||||
|
|
||||||
## Labels
|
|
||||||
|
|
||||||
### Syntax
|
|
||||||
|
|
||||||
| Label | Description | Example | Default | Accepted values |
|
|
||||||
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | --------------------------- | ------------------------------------------------------------------------- |
|
|
||||||
| `proxy.aliases` | comma separated aliases for subdomain and label matching | `gitlab,gitlab-reg,gitlab-ssh` | `container_name` | any |
|
|
||||||
| `proxy.exclude` | to be excluded from `go-proxy` | | false | boolean |
|
|
||||||
| `proxy.idle_timeout` | time for idle (no traffic) before put it into sleep **(http/s only)**<br> _**NOTE: idlewatcher will only be enabled containers that has non-empty `idle_timeout`**_ | `1h` | empty or `0` **(disabled)** | `number[unit]...`, e.g. `1m30s` |
|
|
||||||
| `proxy.wake_timeout` | time to wait for target site to be ready | | `10s` | `number[unit]...` |
|
|
||||||
| `proxy.stop_method` | method to stop after `idle_timeout` | | `stop` | `stop`, `pause`, `kill` |
|
|
||||||
| `proxy.stop_timeout` | time to wait for stop command | | `10s` | `number[unit]...` |
|
|
||||||
| `proxy.stop_signal` | signal sent to container for `stop` and `kill` methods | | docker's default | `SIGINT`, `SIGTERM`, `SIGHUP`, `SIGQUIT` and those without **SIG** prefix |
|
|
||||||
| `proxy.<alias>.<field>` | set field for specific alias | `proxy.gitlab-ssh.scheme` | N/A | N/A |
|
|
||||||
| `proxy.$<index>.<field>` | set field for specific alias at index (starting from **1**) | `proxy.$3.port` | N/A | N/A |
|
|
||||||
| `proxy.*.<field>` | set field for all aliases | `proxy.*.set_headers` | N/A | N/A |
|
|
||||||
|
|
||||||
### Fields
|
|
||||||
|
|
||||||
| Field | Description | Default | Allowed Values / Syntax |
|
|
||||||
| --------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `scheme` | proxy protocol | <ul><li>`http` for numeric port</li><li>`tcp` for `x:y` port</li></ul> | `http`, `https`, `tcp`, `udp` |
|
|
||||||
| `host` | proxy host | <ul><li>Docker: docker client IP / hostname </li><li>File: `localhost`</li></ul> | IP address, hostname |
|
|
||||||
| `port` | proxy port **(http/s)** | first port returned from docker | number in range of `1 - 65535` |
|
|
||||||
| `port` **(required)** | proxy port **(tcp/udp)** | N/A | `x:y` <br><ul><li>**x**: port for `go-proxy` to listen on.<br>**x** can be 0, which means listen on a random port</li><li>**y**: port or [_service name_](../src/common/constants.go#L55) of target container</li></ul> |
|
|
||||||
| `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean |
|
|
||||||
| `path_patterns` | proxy path patterns **(http/s only)**<br> only requests that matched a pattern will be proxied | empty **(proxy all requests)** | yaml style list[<sup>1</sup>](#list-example) of ([path patterns](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)) |
|
|
||||||
| `set_headers` | header to set **(http/s only)** | empty | yaml style key-value mapping[<sup>2</sup>](#key-value-mapping-example) of header-value pairs |
|
|
||||||
| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[<sup>1</sup>](#list-example) of headers |
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
|
||||||
|
|
||||||
#### Key-value mapping example
|
|
||||||
|
|
||||||
Docker Compose
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
nginx:
|
|
||||||
...
|
|
||||||
labels:
|
|
||||||
# values from duplicated header keys will be combined
|
|
||||||
proxy.nginx.set_headers: | # remember to add the '|'
|
|
||||||
X-Custom-Header1: value1, value2
|
|
||||||
X-Custom-Header2: value3
|
|
||||||
X-Custom-Header2: value4
|
|
||||||
# X-Custom-Header2 will be "value3, value4"
|
|
||||||
```
|
|
||||||
|
|
||||||
File Provider
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service_a:
|
|
||||||
host: service_a.internal
|
|
||||||
set_headers:
|
|
||||||
# do not duplicate header keys, as it is not allowed in YAML
|
|
||||||
X-Custom-Header1: value1, value2
|
|
||||||
X-Custom-Header2: value3
|
|
||||||
```
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
|
||||||
|
|
||||||
#### List example
|
|
||||||
|
|
||||||
Docker Compose
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
nginx:
|
|
||||||
...
|
|
||||||
labels:
|
|
||||||
proxy.nginx.path_patterns: | # remember to add the '|'
|
|
||||||
- GET /
|
|
||||||
- POST /auth
|
|
||||||
proxy.nginx.hide_headers: | # remember to add the '|'
|
|
||||||
- X-Custom-Header1
|
|
||||||
- X-Custom-Header2
|
|
||||||
```
|
|
||||||
|
|
||||||
File Provider
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service_a:
|
|
||||||
host: service_a.internal
|
|
||||||
path_patterns:
|
|
||||||
- GET /
|
|
||||||
- POST /auth
|
|
||||||
hide_headers:
|
|
||||||
- X-Custom-Header1
|
|
||||||
- X-Custom-Header2
|
|
||||||
```
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
- Container not showing up in proxies list
|
|
||||||
|
|
||||||
Please check that either `ports` or label `proxy.<alias>.port` is declared, e.g.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
nginx-1: # Option 1
|
|
||||||
...
|
|
||||||
ports:
|
|
||||||
- 80
|
|
||||||
nginx-2: # Option 2
|
|
||||||
...
|
|
||||||
container_name: nginx-2
|
|
||||||
network_mode: host
|
|
||||||
labels:
|
|
||||||
proxy.nginx-2.port: 80
|
|
||||||
```
|
|
||||||
|
|
||||||
- Firewall issues
|
|
||||||
|
|
||||||
If you are using `ufw` with vpn that drop all inbound traffic except vpn, run below:
|
|
||||||
|
|
||||||
`sudo ufw allow from 172.16.0.0/16 to 100.64.0.0/10`
|
|
||||||
|
|
||||||
Explaination:
|
|
||||||
|
|
||||||
Docker network is usually `172.16.0.0/16`
|
|
||||||
|
|
||||||
Tailscale is used as an example, `100.64.0.0/10` will be the CIDR
|
|
||||||
|
|
||||||
You can also list CIDRs of all docker bridge networks by:
|
|
||||||
|
|
||||||
`docker network inspect $(docker network ls | awk '$3 == "bridge" { print $1}') | jq -r '.[] | .Name + " " + .IPAM.Config[0].Subnet' -`
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
|
||||||
|
|
||||||
## Docker compose examples
|
|
||||||
|
|
||||||
More examples in [here](examples/)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
volumes:
|
|
||||||
adg-work:
|
|
||||||
adg-conf:
|
|
||||||
mc-data:
|
|
||||||
palworld:
|
|
||||||
nginx:
|
|
||||||
services:
|
|
||||||
adg:
|
|
||||||
image: adguard/adguardhome
|
|
||||||
restart: unless-stopped
|
|
||||||
labels:
|
|
||||||
- proxy.aliases=adg,adg-dns,adg-setup
|
|
||||||
- proxy.$1.port=80
|
|
||||||
- proxy.$2.scheme=udp
|
|
||||||
- proxy.$2.port=20000:dns
|
|
||||||
- proxy.$3.port=3000
|
|
||||||
volumes:
|
|
||||||
- adg-work:/opt/adguardhome/work
|
|
||||||
- adg-conf:/opt/adguardhome/conf
|
|
||||||
ports:
|
|
||||||
- 80
|
|
||||||
- 3000
|
|
||||||
- 53/udp
|
|
||||||
mc:
|
|
||||||
image: itzg/minecraft-server
|
|
||||||
tty: true
|
|
||||||
stdin_open: true
|
|
||||||
container_name: mc
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- 25565
|
|
||||||
labels:
|
|
||||||
- proxy.mc.port=20001:25565
|
|
||||||
environment:
|
|
||||||
- EULA=TRUE
|
|
||||||
volumes:
|
|
||||||
- mc-data:/data
|
|
||||||
palworld:
|
|
||||||
image: thijsvanloef/palworld-server-docker:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
container_name: pal
|
|
||||||
stop_grace_period: 30s
|
|
||||||
ports:
|
|
||||||
- 8211/udp
|
|
||||||
- 27015/udp
|
|
||||||
labels:
|
|
||||||
- proxy.aliases=pal1,pal2
|
|
||||||
- proxy.*.scheme=udp
|
|
||||||
- proxy.$1.port=20002:8211
|
|
||||||
- proxy.$2.port=20003:27015
|
|
||||||
environment: ...
|
|
||||||
volumes:
|
|
||||||
- palworld:/palworld
|
|
||||||
nginx:
|
|
||||||
image: nginx
|
|
||||||
container_name: nginx
|
|
||||||
volumes:
|
|
||||||
- nginx:/usr/share/nginx/html
|
|
||||||
ports:
|
|
||||||
- 80
|
|
||||||
labels:
|
|
||||||
proxy.idle_timeout: 1m
|
|
||||||
go-proxy:
|
|
||||||
image: ghcr.io/yusing/go-proxy:latest
|
|
||||||
container_name: go-proxy
|
|
||||||
restart: always
|
|
||||||
network_mode: host
|
|
||||||
volumes:
|
|
||||||
- ./config:/app/config
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
go-proxy-frontend:
|
|
||||||
image: ghcr.io/yusing/go-proxy-frontend:latest
|
|
||||||
container_name: go-proxy-frontend
|
|
||||||
restart: unless-stopped
|
|
||||||
network_mode: host
|
|
||||||
labels:
|
|
||||||
- proxy.aliases=gp
|
|
||||||
- proxy.gp.port=3000
|
|
||||||
depends_on:
|
|
||||||
- go-proxy
|
|
||||||
```
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
|
||||||
|
|
||||||
### Services URLs for above examples
|
|
||||||
|
|
||||||
- `gp.yourdomain.com`: go-proxy web panel
|
|
||||||
- `adg-setup.yourdomain.com`: adguard setup (first time setup)
|
|
||||||
- `adg.yourdomain.com`: adguard dashboard
|
|
||||||
- `nginx.yourdomain.com`: nginx
|
|
||||||
- `yourdomain.com:2000`: adguard dns (udp)
|
|
||||||
- `yourdomain.com:20001`: minecraft server
|
|
||||||
- `yourdomain.com:20002`: palworld server
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
## Docker Socket Proxy
|
|
||||||
|
|
||||||
For docker client on other machine, set this up, then add `name: tcp://<machine_ip>:2375` to `config.yml` under `docker` section
|
|
||||||
|
|
||||||
```yml
|
|
||||||
# compose.yml on remote machine (e.g. server1)
|
|
||||||
docker-proxy:
|
|
||||||
container_name: docker-proxy
|
|
||||||
image: tecnativa/docker-socket-proxy
|
|
||||||
privileged: true
|
|
||||||
environment:
|
|
||||||
- ALLOW_START=1
|
|
||||||
- ALLOW_STOP=1
|
|
||||||
- ALLOW_RESTARTS=1
|
|
||||||
- CONTAINERS=1
|
|
||||||
- EVENTS=1
|
|
||||||
- PING=1
|
|
||||||
- POST=1
|
|
||||||
- VERSION=1
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- 2375:2375
|
|
||||||
# or more secure
|
|
||||||
- <machine_ip>:2375:2375
|
|
||||||
```
|
|
||||||
|
|
||||||
```yml
|
|
||||||
# config.yml on go-proxy machine
|
|
||||||
autocert:
|
|
||||||
... # your config
|
|
||||||
|
|
||||||
providers:
|
|
||||||
include:
|
|
||||||
...
|
|
||||||
docker:
|
|
||||||
...
|
|
||||||
server1: tcp://<machine_ip>:2375
|
|
||||||
```
|
|
||||||
27
examples/docker-compose/n8n.yml
Normal file
27
examples/docker-compose/n8n.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
services:
|
||||||
|
n8n:
|
||||||
|
image: n8nio/n8n
|
||||||
|
container_name: n8n
|
||||||
|
restart: always
|
||||||
|
expose:
|
||||||
|
- 5678
|
||||||
|
labels:
|
||||||
|
proxy.n8n.middlewares.request.set_headers: |
|
||||||
|
SSLRedirect: true
|
||||||
|
STSSeconds: 315360000
|
||||||
|
browserXSSFilter: true
|
||||||
|
contentTypeNosniff: true
|
||||||
|
forceSTSHeader: true
|
||||||
|
SSLHost: ${DOMAIN_NAME}
|
||||||
|
STSIncludeSubdomains: true
|
||||||
|
STSPreload: true
|
||||||
|
environment:
|
||||||
|
- N8N_HOST=${SUBDOMAIN}.${DOMAIN_NAME}
|
||||||
|
- N8N_PORT=5678
|
||||||
|
- N8N_PROTOCOL=https
|
||||||
|
- NODE_ENV=production
|
||||||
|
- WEBHOOK_URL=https://${SUBDOMAIN}.${DOMAIN_NAME}/
|
||||||
|
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
|
||||||
|
volumes:
|
||||||
|
- ./data:/home/node/.n8n
|
||||||
288
examples/error_pages/404.css
Normal file
288
examples/error_pages/404.css
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
@import url("https://fonts.googleapis.com/css?family=Audiowide&display=swap");
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
position: absolute;
|
||||||
|
top: 0%;
|
||||||
|
left: 0%;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0px;
|
||||||
|
background: radial-gradient(circle, #240015 0%, #12000b 100%);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: 150px;
|
||||||
|
font-size: 32px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
display: block;
|
||||||
|
color: #12000a;
|
||||||
|
font-weight: 300;
|
||||||
|
font-family: Audiowide;
|
||||||
|
text-shadow: 0px 0px 4px #12000a;
|
||||||
|
animation: fadeInText 3s ease-in 3.5s forwards,
|
||||||
|
flicker4 5s linear 7.5s infinite, hueRotate 6s ease-in-out 3s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#svgWrap_1,
|
||||||
|
#svgWrap_2 {
|
||||||
|
position: absolute;
|
||||||
|
height: auto;
|
||||||
|
width: 600px;
|
||||||
|
max-width: 100%;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#svgWrap_1,
|
||||||
|
#svgWrap_2,
|
||||||
|
div {
|
||||||
|
animation: hueRotate 6s ease-in-out 3s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#id1_1,
|
||||||
|
#id2_1,
|
||||||
|
#id3_1 {
|
||||||
|
stroke: #ff005d;
|
||||||
|
stroke-width: 3px;
|
||||||
|
fill: transparent;
|
||||||
|
filter: url(#glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
#id1_2,
|
||||||
|
#id2_2,
|
||||||
|
#id3_2 {
|
||||||
|
stroke: #12000a;
|
||||||
|
stroke-width: 3px;
|
||||||
|
fill: transparent;
|
||||||
|
filter: url(#glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
#id3_1 {
|
||||||
|
stroke-dasharray: 940px;
|
||||||
|
stroke-dashoffset: -940px;
|
||||||
|
animation: drawLine3 2.5s ease-in-out 0s forwards,
|
||||||
|
flicker3 4s linear 4s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#id2_1 {
|
||||||
|
stroke-dasharray: 735px;
|
||||||
|
stroke-dashoffset: -735px;
|
||||||
|
animation: drawLine2 2.5s ease-in-out 0.5s forwards,
|
||||||
|
flicker2 4s linear 4.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#id1_1 {
|
||||||
|
stroke-dasharray: 940px;
|
||||||
|
stroke-dashoffset: -940px;
|
||||||
|
animation: drawLine1 2.5s ease-in-out 1s forwards,
|
||||||
|
flicker1 4s linear 5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes drawLine1 {
|
||||||
|
0% {
|
||||||
|
stroke-dashoffset: -940px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes drawLine2 {
|
||||||
|
0% {
|
||||||
|
stroke-dashoffset: -735px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes drawLine3 {
|
||||||
|
0% {
|
||||||
|
stroke-dashoffset: -940px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flicker1 {
|
||||||
|
0% {
|
||||||
|
stroke: #ff005d;
|
||||||
|
}
|
||||||
|
1% {
|
||||||
|
stroke: transparent;
|
||||||
|
}
|
||||||
|
3% {
|
||||||
|
stroke: transparent;
|
||||||
|
}
|
||||||
|
4% {
|
||||||
|
stroke: #ff005d;
|
||||||
|
}
|
||||||
|
6% {
|
||||||
|
stroke: #ff005d;
|
||||||
|
}
|
||||||
|
7% {
|
||||||
|
stroke: transparent;
|
||||||
|
}
|
||||||
|
13% {
|
||||||
|
stroke: transparent;
|
||||||
|
}
|
||||||
|
14% {
|
||||||
|
stroke: #ff005d;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke: #ff005d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flicker2 {
|
||||||
|
0% {
|
||||||
|
stroke: #ff005d;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
stroke: #ff005d;
|
||||||
|
}
|
||||||
|
51% {
|
||||||
|
stroke: transparent;
|
||||||
|
}
|
||||||
|
61% {
|
||||||
|
stroke: transparent;
|
||||||
|
}
|
||||||
|
62% {
|
||||||
|
stroke: #ff005d;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke: #ff005d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flicker3 {
|
||||||
|
0% {
|
||||||
|
stroke: #ff005d;
|
||||||
|
}
|
||||||
|
1% {
|
||||||
|
stroke: transparent;
|
||||||
|
}
|
||||||
|
10% {
|
||||||
|
stroke: transparent;
|
||||||
|
}
|
||||||
|
11% {
|
||||||
|
stroke: #ff005d;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
stroke: #ff005d;
|
||||||
|
}
|
||||||
|
41% {
|
||||||
|
stroke: transparent;
|
||||||
|
}
|
||||||
|
45% {
|
||||||
|
stroke: transparent;
|
||||||
|
}
|
||||||
|
46% {
|
||||||
|
stroke: #ff005d;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke: #ff005d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flicker4 {
|
||||||
|
0% {
|
||||||
|
color: #ff005d;
|
||||||
|
text-shadow: 0px 0px 4px #ff005d;
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
color: #ff005d;
|
||||||
|
text-shadow: 0px 0px 4px #ff005d;
|
||||||
|
}
|
||||||
|
31% {
|
||||||
|
color: #12000a;
|
||||||
|
text-shadow: 0px 0px 4px #12000a;
|
||||||
|
}
|
||||||
|
32% {
|
||||||
|
color: #ff005d;
|
||||||
|
text-shadow: 0px 0px 4px #ff005d;
|
||||||
|
}
|
||||||
|
36% {
|
||||||
|
color: #ff005d;
|
||||||
|
text-shadow: 0px 0px 4px #ff005d;
|
||||||
|
}
|
||||||
|
37% {
|
||||||
|
color: #12000a;
|
||||||
|
text-shadow: 0px 0px 4px #12000a;
|
||||||
|
}
|
||||||
|
41% {
|
||||||
|
color: #12000a;
|
||||||
|
text-shadow: 0px 0px 4px #12000a;
|
||||||
|
}
|
||||||
|
42% {
|
||||||
|
color: #ff005d;
|
||||||
|
text-shadow: 0px 0px 4px #ff005d;
|
||||||
|
}
|
||||||
|
85% {
|
||||||
|
color: #ff005d;
|
||||||
|
text-shadow: 0px 0px 4px #ff005d;
|
||||||
|
}
|
||||||
|
86% {
|
||||||
|
color: #12000a;
|
||||||
|
text-shadow: 0px 0px 4px #12000a;
|
||||||
|
}
|
||||||
|
95% {
|
||||||
|
color: #12000a;
|
||||||
|
text-shadow: 0px 0px 4px #12000a;
|
||||||
|
}
|
||||||
|
96% {
|
||||||
|
color: #ff005d;
|
||||||
|
text-shadow: 0px 0px 4px #ff005d;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
color: #ff005d;
|
||||||
|
text-shadow: 0px 0px 4px #ff005d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInText {
|
||||||
|
1% {
|
||||||
|
color: #12000a;
|
||||||
|
text-shadow: 0px 0px 4px #12000a;
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
color: #ff005d;
|
||||||
|
text-shadow: 0px 0px 14px #ff005d;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
color: #ff005d;
|
||||||
|
text-shadow: 0px 0px 4px #ff005d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hueRotate {
|
||||||
|
0% {
|
||||||
|
filter: hue-rotate(0deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
filter: hue-rotate(-120deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
filter: hue-rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
examples/error_pages/404.html
Normal file
51
examples/error_pages/404.html
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{{/* Credit: https://codepen.io/code2rithik/pen/XWpVvYL */}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Page Not Found</title>
|
||||||
|
<link rel="stylesheet" href="/$gperrorpage/404.css" type="text/css">
|
||||||
|
<!-- <script src="/$gperrorpage/404.js"> </script> -->
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<script>0</script>
|
||||||
|
<div></div>
|
||||||
|
<svg id="svgWrap_2" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 700 250">
|
||||||
|
<g>
|
||||||
|
<path id="id3_2"
|
||||||
|
d="M195.7 232.67h-37.1V149.7H27.76c-2.64 0-5.1-.5-7.36-1.49-2.27-.99-4.23-2.31-5.88-3.96-1.65-1.65-2.95-3.61-3.89-5.88s-1.42-4.67-1.42-7.22V29.62h36.82v82.98H158.6V29.62h37.1v203.05z" />
|
||||||
|
<path id="id2_2"
|
||||||
|
d="M470.69 147.71c0 8.31-1.06 16.17-3.19 23.58-2.12 7.41-5.12 14.28-8.99 20.6-3.87 6.33-8.45 11.99-13.74 16.99-5.29 5-11.07 9.28-17.35 12.81a85.146 85.146 0 0 1-20.04 8.14 83.637 83.637 0 0 1-21.67 2.83H319.3c-7.46 0-14.73-.94-21.81-2.83-7.08-1.89-13.76-4.6-20.04-8.14a88.292 88.292 0 0 1-17.35-12.81c-5.29-5-9.84-10.67-13.66-16.99-3.82-6.32-6.8-13.19-8.92-20.6-2.12-7.41-3.19-15.27-3.19-23.58v-33.13c0-12.46 2.34-23.88 7.01-34.27 4.67-10.38 10.92-19.33 18.76-26.83 7.83-7.5 16.87-13.36 27.12-17.56 10.24-4.2 20.93-6.3 32.07-6.3h66.41c7.36 0 14.58.94 21.67 2.83 7.08 1.89 13.76 4.6 20.04 8.14a88.292 88.292 0 0 1 17.35 12.81c5.29 5 9.86 10.67 13.74 16.99 3.87 6.33 6.87 13.19 8.99 20.6 2.13 7.41 3.19 15.27 3.19 23.58v33.14zm-37.1-33.13c0-7.27-1.32-13.88-3.96-19.82-2.64-5.95-6.16-11.04-10.55-15.29-4.39-4.25-9.46-7.5-15.22-9.77-5.76-2.27-11.8-3.35-18.13-3.26h-66.41c-6.14-.09-12.11.97-17.91 3.19-5.81 2.22-10.95 5.43-15.44 9.63-4.48 4.2-8.07 9.3-10.76 15.29-2.69 6-4.04 12.67-4.04 20.04v33.13c0 7.36 1.32 14.02 3.96 19.97 2.64 5.95 6.18 11.02 10.62 15.22 4.44 4.2 9.56 7.43 15.36 9.7 5.8 2.27 11.87 3.35 18.2 3.26h66.41c7.27 0 13.85-1.2 19.75-3.61s10.93-5.73 15.08-9.98 7.36-9.32 9.63-15.22c2.27-5.9 3.4-12.34 3.4-19.33v-33.15zm-16-26.91a17.89 17.89 0 0 1 2.83 6.73c.47 2.41.47 4.77 0 7.08-.47 2.31-1.39 4.48-2.76 6.51-1.37 2.03-3.14 3.75-5.31 5.17l-99.4 66.41c-1.61 1.23-3.26 2.08-4.96 2.55-1.7.47-3.45.71-5.24.71-3.02 0-5.9-.71-8.64-2.12-2.74-1.42-4.96-3.44-6.66-6.09a17.89 17.89 0 0 1-2.83-6.73c-.47-2.41-.5-4.77-.07-7.08.43-2.31 1.3-4.48 2.62-6.51 1.32-2.03 3.07-3.75 5.24-5.17l99.69-66.41a17.89 17.89 0 0 1 6.73-2.83c2.41-.47 4.77-.47 7.08 0 2.31.47 4.48 1.37 6.51 2.69 2.03 1.32 3.75 3.02 5.17 5.09z" />
|
||||||
|
<path id="id1_2"
|
||||||
|
d="M688.33 232.67h-37.1V149.7H520.39c-2.64 0-5.1-.5-7.36-1.49-2.27-.99-4.23-2.31-5.88-3.96-1.65-1.65-2.95-3.61-3.89-5.88s-1.42-4.67-1.42-7.22V29.62h36.82v82.98h112.57V29.62h37.1v203.05z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<svg id="svgWrap_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 700 250">
|
||||||
|
<g>
|
||||||
|
<path id="id3_1"
|
||||||
|
d="M195.7 232.67h-37.1V149.7H27.76c-2.64 0-5.1-.5-7.36-1.49-2.27-.99-4.23-2.31-5.88-3.96-1.65-1.65-2.95-3.61-3.89-5.88s-1.42-4.67-1.42-7.22V29.62h36.82v82.98H158.6V29.62h37.1v203.05z" />
|
||||||
|
<path id="id2_1"
|
||||||
|
d="M470.69 147.71c0 8.31-1.06 16.17-3.19 23.58-2.12 7.41-5.12 14.28-8.99 20.6-3.87 6.33-8.45 11.99-13.74 16.99-5.29 5-11.07 9.28-17.35 12.81a85.146 85.146 0 0 1-20.04 8.14 83.637 83.637 0 0 1-21.67 2.83H319.3c-7.46 0-14.73-.94-21.81-2.83-7.08-1.89-13.76-4.6-20.04-8.14a88.292 88.292 0 0 1-17.35-12.81c-5.29-5-9.84-10.67-13.66-16.99-3.82-6.32-6.8-13.19-8.92-20.6-2.12-7.41-3.19-15.27-3.19-23.58v-33.13c0-12.46 2.34-23.88 7.01-34.27 4.67-10.38 10.92-19.33 18.76-26.83 7.83-7.5 16.87-13.36 27.12-17.56 10.24-4.2 20.93-6.3 32.07-6.3h66.41c7.36 0 14.58.94 21.67 2.83 7.08 1.89 13.76 4.6 20.04 8.14a88.292 88.292 0 0 1 17.35 12.81c5.29 5 9.86 10.67 13.74 16.99 3.87 6.33 6.87 13.19 8.99 20.6 2.13 7.41 3.19 15.27 3.19 23.58v33.14zm-37.1-33.13c0-7.27-1.32-13.88-3.96-19.82-2.64-5.95-6.16-11.04-10.55-15.29-4.39-4.25-9.46-7.5-15.22-9.77-5.76-2.27-11.8-3.35-18.13-3.26h-66.41c-6.14-.09-12.11.97-17.91 3.19-5.81 2.22-10.95 5.43-15.44 9.63-4.48 4.2-8.07 9.3-10.76 15.29-2.69 6-4.04 12.67-4.04 20.04v33.13c0 7.36 1.32 14.02 3.96 19.97 2.64 5.95 6.18 11.02 10.62 15.22 4.44 4.2 9.56 7.43 15.36 9.7 5.8 2.27 11.87 3.35 18.2 3.26h66.41c7.27 0 13.85-1.2 19.75-3.61s10.93-5.73 15.08-9.98 7.36-9.32 9.63-15.22c2.27-5.9 3.4-12.34 3.4-19.33v-33.15zm-16-26.91a17.89 17.89 0 0 1 2.83 6.73c.47 2.41.47 4.77 0 7.08-.47 2.31-1.39 4.48-2.76 6.51-1.37 2.03-3.14 3.75-5.31 5.17l-99.4 66.41c-1.61 1.23-3.26 2.08-4.96 2.55-1.7.47-3.45.71-5.24.71-3.02 0-5.9-.71-8.64-2.12-2.74-1.42-4.96-3.44-6.66-6.09a17.89 17.89 0 0 1-2.83-6.73c-.47-2.41-.5-4.77-.07-7.08.43-2.31 1.3-4.48 2.62-6.51 1.32-2.03 3.07-3.75 5.24-5.17l99.69-66.41a17.89 17.89 0 0 1 6.73-2.83c2.41-.47 4.77-.47 7.08 0 2.31.47 4.48 1.37 6.51 2.69 2.03 1.32 3.75 3.02 5.17 5.09z" />
|
||||||
|
<path id="id1_1"
|
||||||
|
d="M688.33 232.67h-37.1V149.7H520.39c-2.64 0-5.1-.5-7.36-1.49-2.27-.99-4.23-2.31-5.88-3.96-1.65-1.65-2.95-3.61-3.89-5.88s-1.42-4.67-1.42-7.22V29.62h36.82v82.98h112.57V29.62h37.1v203.05z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg>
|
||||||
|
<defs>
|
||||||
|
<filter id="glow">
|
||||||
|
<fegaussianblur class="blur" result="coloredBlur" stddeviation="4"></fegaussianblur>
|
||||||
|
<femerge>
|
||||||
|
<femergenode in="coloredBlur"></femergenode>
|
||||||
|
<femergenode in="SourceGraphic"></femergenode>
|
||||||
|
</femerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<h2>Page Not Found</h2>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
1002
examples/grafana_template.json
Normal file
1002
examples/grafana_template.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,16 +0,0 @@
|
|||||||
services:
|
|
||||||
app:
|
|
||||||
container_name: microbin
|
|
||||||
cpu_shares: 10
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 256M
|
|
||||||
env_file: .env
|
|
||||||
image: docker.i.sh/danielszabo99/microbin:latest
|
|
||||||
ports:
|
|
||||||
- 8080
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- ./data:/app/microbin_data
|
|
||||||
# microbin.domain.tld
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
services:
|
|
||||||
main:
|
|
||||||
image: b3log/siyuan:v3.1.0
|
|
||||||
container_name: siyuan
|
|
||||||
command:
|
|
||||||
- --workspace=/siyuan/workspace/
|
|
||||||
- --accessAuthCode=<some password>
|
|
||||||
user: 1000:1000
|
|
||||||
volumes:
|
|
||||||
- ./workspace:/siyuan/workspace
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
- TZ=Asia/Hong_Kong
|
|
||||||
ports:
|
|
||||||
- 6806
|
|
||||||
# siyuan.domain.tld
|
|
||||||
1
frontend
1
frontend
Submodule frontend deleted from 441fd708db
190
go.mod
Normal file
190
go.mod
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
module github.com/yusing/godoxy
|
||||||
|
|
||||||
|
go 1.25.6
|
||||||
|
|
||||||
|
replace (
|
||||||
|
github.com/coreos/go-oidc/v3 => ./internal/go-oidc
|
||||||
|
github.com/shirou/gopsutil/v4 => ./internal/gopsutil
|
||||||
|
github.com/yusing/godoxy/agent => ./agent
|
||||||
|
github.com/yusing/godoxy/internal/dnsproviders => ./internal/dnsproviders
|
||||||
|
github.com/yusing/goutils => ./goutils
|
||||||
|
github.com/yusing/goutils/http/reverseproxy => ./goutils/http/reverseproxy
|
||||||
|
github.com/yusing/goutils/http/websocket => ./goutils/http/websocket
|
||||||
|
github.com/yusing/goutils/server => ./goutils/server
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon; modify_html middleware
|
||||||
|
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
||||||
|
github.com/gin-gonic/gin v1.11.0 // api server
|
||||||
|
github.com/go-acme/lego/v4 v4.31.0 // acme client
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 // validator
|
||||||
|
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
||||||
|
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
|
||||||
|
github.com/gotify/server/v2 v2.8.0 // reference the Message struct for json response
|
||||||
|
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
||||||
|
github.com/pires/go-proxyproto v0.8.1 // proxy protocol support
|
||||||
|
github.com/puzpuzpuz/xsync/v4 v4.3.0 // lock free map for concurrent operations
|
||||||
|
github.com/rs/zerolog v1.34.0 // logging
|
||||||
|
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
||||||
|
golang.org/x/crypto v0.47.0 // encrypting password with bcrypt
|
||||||
|
golang.org/x/net v0.49.0 // HTTP header utilities
|
||||||
|
golang.org/x/oauth2 v0.34.0 // oauth2 authentication
|
||||||
|
golang.org/x/sync v0.19.0 // errgroup and singleflight for concurrent operations
|
||||||
|
golang.org/x/time v0.14.0 // time utilities
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
|
||||||
|
github.com/bytedance/sonic v1.14.2 // fast json parsing
|
||||||
|
github.com/docker/cli v29.1.4+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
|
||||||
|
github.com/goccy/go-yaml v1.19.2 // yaml parsing for different config files
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 // jwt authentication
|
||||||
|
github.com/luthermonson/go-proxmox v0.3.2 // proxmox API client
|
||||||
|
github.com/moby/moby/api v1.52.0 // docker API
|
||||||
|
github.com/moby/moby/client v0.2.1 // docker client
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database
|
||||||
|
github.com/quic-go/quic-go v0.59.0 // http3 support
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.12 // system information
|
||||||
|
github.com/spf13/afero v1.15.0 // afero for file system operations
|
||||||
|
github.com/stretchr/testify v1.11.1 // testing framework
|
||||||
|
github.com/valyala/fasthttp v1.69.0 // fast http for health check
|
||||||
|
github.com/yusing/ds v0.4.1 // data structures and algorithms
|
||||||
|
github.com/yusing/godoxy/agent v0.0.0-20260116020954-edcde00dcc3a
|
||||||
|
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260116020954-edcde00dcc3a
|
||||||
|
github.com/yusing/gointernals v0.1.16
|
||||||
|
github.com/yusing/goutils v0.7.0
|
||||||
|
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260116021320-b12ef77f3743
|
||||||
|
github.com/yusing/goutils/http/websocket v0.0.0-20260116021320-b12ef77f3743
|
||||||
|
github.com/yusing/goutils/server v0.0.0-20260116021320-b12ef77f3743
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cloud.google.com/go/auth v0.18.0 // indirect
|
||||||
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
|
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
|
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||||
|
github.com/buger/goterm v1.0.4 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/diskfs/go-diskfs v1.7.0 // indirect
|
||||||
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/djherbis/times v1.6.0 // indirect
|
||||||
|
github.com/docker/go-connections v0.6.0
|
||||||
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
|
github.com/ebitengine/purego v0.9.1 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/gofrs/flock v0.13.0 // indirect
|
||||||
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||||
|
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||||
|
github.com/jinzhu/copier v0.4.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
||||||
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/magefile/mage v1.15.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/miekg/dns v1.1.70 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
||||||
|
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
|
github.com/ovh/go-ovh v1.9.0 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
|
github.com/samber/lo v1.52.0 // indirect
|
||||||
|
github.com/samber/slog-common v0.19.0 // indirect
|
||||||
|
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
|
||||||
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||||
|
github.com/sony/gobreaker v1.0.0 // indirect
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0
|
||||||
|
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||||
|
go.uber.org/atomic v1.11.0
|
||||||
|
go.uber.org/ratelimit v0.3.1 // indirect
|
||||||
|
golang.org/x/mod v0.32.0 // indirect
|
||||||
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
|
golang.org/x/text v0.33.0 // indirect
|
||||||
|
golang.org/x/tools v0.41.0 // indirect
|
||||||
|
google.golang.org/api v0.260.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 // indirect
|
||||||
|
google.golang.org/grpc v1.78.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
|
github.com/boombuler/barcode v1.1.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||||
|
github.com/go-resty/resty/v2 v2.17.1 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/google/go-querystring v1.2.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.3 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||||
|
github.com/linode/linodego v1.64.0 // indirect
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||||
|
github.com/nrdcg/goinwx v0.12.0 // indirect
|
||||||
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.0 // indirect
|
||||||
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.0 // indirect
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||||
|
github.com/pion/dtls/v3 v3.0.10 // indirect
|
||||||
|
github.com/pion/logging v0.2.4 // indirect
|
||||||
|
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
|
github.com/pquerna/otp v1.5.0 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
|
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/vultr/govultr/v3 v3.26.1 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
golang.org/x/arch v0.23.0 // indirect
|
||||||
|
)
|
||||||
478
go.sum
Normal file
478
go.sum
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
|
||||||
|
cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
|
||||||
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||||
|
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||||
|
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||||
|
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||||
|
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 h1:yzrctSl9GMIQ5lHu7jc8olOsGjWDCsBpJhWqfGa/YIM=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0/go.mod h1:GE4m0rnnfwLGX0Y9A9A25Zx5N/90jneT5ABevqzhuFQ=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 h1:zLzoX5+W2l95UJoVwiyNS4dX8vHyQ6x2xRLoBBL9wMk=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
|
||||||
|
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||||
|
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||||
|
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
||||||
|
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 h1:h/33OxYLqBk0BYmEbSUy7MlvgQR/m1w1/7OJFKoPL1I=
|
||||||
|
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0/go.mod h1:rvh3imDA6EaQi+oM/GQHkQAOHbXPKJ7EWJvfjuw141Q=
|
||||||
|
github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs=
|
||||||
|
github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=
|
||||||
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||||
|
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||||
|
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
|
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
||||||
|
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
|
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
||||||
|
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
|
||||||
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
|
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
||||||
|
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
||||||
|
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
||||||
|
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/diskfs/go-diskfs v1.7.0 h1:vonWmt5CMowXwUc79jWyGrf2DIMeoOjkLlMnQYGVOs8=
|
||||||
|
github.com/diskfs/go-diskfs v1.7.0/go.mod h1:LhQyXqOugWFRahYUSw47NyZJPezFzB9UELwhpszLP/k=
|
||||||
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||||
|
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||||
|
github.com/docker/cli v29.1.4+incompatible h1:AI8fwZhqsAsrqZnVv9h6lbexeW/LzNTasf6A4vcNN8M=
|
||||||
|
github.com/docker/cli v29.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
|
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||||
|
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
|
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=
|
||||||
|
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=
|
||||||
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
|
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||||
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
|
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
|
||||||
|
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
||||||
|
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
|
github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
|
||||||
|
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
|
||||||
|
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||||
|
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||||
|
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||||
|
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||||
|
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
|
||||||
|
github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA=
|
||||||
|
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
||||||
|
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
|
||||||
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||||
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
|
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||||
|
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||||
|
github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=
|
||||||
|
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
|
||||||
|
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||||
|
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||||
|
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
||||||
|
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
||||||
|
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||||
|
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||||
|
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||||
|
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
|
||||||
|
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/linode/linodego v1.64.0 h1:If6pULIwHuQytgogtpQaBdVLX7z2TTHUF5u1tj2TPiY=
|
||||||
|
github.com/linode/linodego v1.64.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
|
||||||
|
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||||
|
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
|
github.com/luthermonson/go-proxmox v0.3.2 h1:/zUg6FCl9cAABx0xU3OIgtDtClY0gVXxOCsrceDNylc=
|
||||||
|
github.com/luthermonson/go-proxmox v0.3.2/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||||
|
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||||
|
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
|
||||||
|
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||||
|
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
|
||||||
|
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
|
||||||
|
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||||
|
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
|
||||||
|
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
|
||||||
|
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
||||||
|
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
||||||
|
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
||||||
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.0 h1:4MRzV6spwPHKct+4/ETqkEtr39Hq+0KvxhsgqbgQ2Bo=
|
||||||
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||||
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.0 h1:RxraLVYX3eMUfQ1pDtJVvykEFGheky2YsrUt2HHRDcw=
|
||||||
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.0/go.mod h1:JLMEKMX8IYPZ1TUSVHAVAbtnNSfP/I8OZQkAnfEMA0I=
|
||||||
|
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
||||||
|
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||||
|
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
|
||||||
|
github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
|
github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
|
||||||
|
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
|
||||||
|
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||||
|
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||||
|
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||||
|
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||||
|
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||||
|
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
|
||||||
|
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||||
|
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
|
github.com/puzpuzpuz/xsync/v4 v4.3.0 h1:w/bWkEJdYuRNYhHn5eXnIT8LzDM1O629X1I9MJSkD7Q=
|
||||||
|
github.com/puzpuzpuz/xsync/v4 v4.3.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||||
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
|
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||||
|
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||||
|
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
|
||||||
|
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
||||||
|
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
|
||||||
|
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
||||||
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=
|
||||||
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
|
||||||
|
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||||
|
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||||
|
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
|
||||||
|
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||||
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||||
|
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||||
|
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||||
|
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||||
|
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||||
|
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||||
|
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
||||||
|
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
||||||
|
github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw=
|
||||||
|
github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||||
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
|
||||||
|
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||||
|
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
||||||
|
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||||
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
|
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||||
|
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||||
|
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
|
||||||
|
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
|
||||||
|
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||||
|
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
|
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
|
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
|
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
|
google.golang.org/api v0.260.0 h1:XbNi5E6bOVEj/uLXQRlt6TKuEzMD7zvW/6tNwltE4P4=
|
||||||
|
google.golang.org/api v0.260.0/go.mod h1:Shj1j0Phr/9sloYrKomICzdYgsSDImpTxME8rGLaZ/o=
|
||||||
|
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||||
|
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 h1:C4WAdL+FbjnGlpp2S+HMVhBeCq2Lcib4xZqfPNF6OoQ=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
|
||||||
|
gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
|
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||||
|
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||||
1
goutils
Submodule
1
goutils
Submodule
Submodule goutils added at 326c1f1eb3
282
internal/acl/README.md
Normal file
282
internal/acl/README.md
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
# ACL (Access Control List)
|
||||||
|
|
||||||
|
Access control at the TCP connection level with IP/CIDR, timezone, and country-based filtering.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The ACL package provides network-level access control by wrapping TCP listeners and validating incoming connections against configurable allow/deny rules. It integrates with MaxMind GeoIP for geographic-based filtering and supports access logging with notification batching.
|
||||||
|
|
||||||
|
### Primary consumers
|
||||||
|
|
||||||
|
- `internal/entrypoint` - Wraps the main TCP listener for connection filtering
|
||||||
|
- Operators - Configure rules via YAML configuration
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- HTTP request-level filtering (handled by middleware)
|
||||||
|
- Authentication or authorization (see `internal/auth`)
|
||||||
|
- VPN or tunnel integration
|
||||||
|
|
||||||
|
### Stability
|
||||||
|
|
||||||
|
Stable internal package. The public API is the `Config` struct and its methods.
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
### Exported types
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Config struct {
|
||||||
|
Default string // "allow" or "deny" (default: "allow")
|
||||||
|
AllowLocal *bool // Allow private/loopback IPs (default: true)
|
||||||
|
Allow Matchers // Allow rules
|
||||||
|
Deny Matchers // Deny rules
|
||||||
|
Log *accesslog.ACLLoggerConfig // Access logging configuration
|
||||||
|
|
||||||
|
Notify struct {
|
||||||
|
To []string // Notification providers
|
||||||
|
Interval time.Duration // Notification frequency (default: 1m)
|
||||||
|
IncludeAllowed *bool // Include allowed in notifications (default: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Matcher struct {
|
||||||
|
match MatcherFunc
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Matchers []Matcher
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exported functions and methods
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (c *Config) Validate() gperr.Error
|
||||||
|
```
|
||||||
|
|
||||||
|
Validates configuration and sets defaults. Must be called before `Start`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (c *Config) Start(parent task.Parent) gperr.Error
|
||||||
|
```
|
||||||
|
|
||||||
|
Initializes the ACL, starts the logger and notification goroutines.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (c *Config) IPAllowed(ip net.IP) bool
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns true if the IP is allowed based on configured rules. Performs caching and GeoIP lookup if needed.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (c *Config) WrapTCP(lis net.Listener) net.Listener
|
||||||
|
```
|
||||||
|
|
||||||
|
Wraps a `net.Listener` to filter connections by IP.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (matcher *Matcher) Parse(s string) error
|
||||||
|
```
|
||||||
|
|
||||||
|
Parses a matcher string in the format `{type}:{value}`. Supported types: `ip`, `cidr`, `tz`, `country`.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core components
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[TCP Listener] --> B[TCPListener Wrapper]
|
||||||
|
B --> C{IP Allowed?}
|
||||||
|
C -->|Yes| D[Accept Connection]
|
||||||
|
C -->|No| E[Close Connection]
|
||||||
|
|
||||||
|
F[Config] --> G[Validate]
|
||||||
|
G --> H[Start]
|
||||||
|
H --> I[Matcher Evaluation]
|
||||||
|
I --> C
|
||||||
|
|
||||||
|
J[MaxMind] -.-> K[IP Lookup]
|
||||||
|
K -.-> I
|
||||||
|
|
||||||
|
L[Access Logger] -.-> M[Log & Notify]
|
||||||
|
M -.-> B
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection filtering flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Client
|
||||||
|
participant TCPListener
|
||||||
|
participant Config
|
||||||
|
participant MaxMind
|
||||||
|
participant Logger
|
||||||
|
|
||||||
|
Client->>TCPListener: Connection Request
|
||||||
|
TCPListener->>Config: IPAllowed(clientIP)
|
||||||
|
|
||||||
|
alt Loopback IP
|
||||||
|
Config-->>TCPListener: true
|
||||||
|
else Private IP (allow_local)
|
||||||
|
Config-->>TCPListener: true
|
||||||
|
else Cached Result
|
||||||
|
Config-->>TCPListener: Cached Result
|
||||||
|
else Evaluate Allow Rules
|
||||||
|
Config->>Config: Check Allow list
|
||||||
|
alt Matches
|
||||||
|
Config->>Config: Cache true
|
||||||
|
Config-->>TCPListener: Allowed
|
||||||
|
else Evaluate Deny Rules
|
||||||
|
Config->>Config: Check Deny list
|
||||||
|
alt Matches
|
||||||
|
Config->>Config: Cache false
|
||||||
|
Config-->>TCPListener: Denied
|
||||||
|
else Default Action
|
||||||
|
Config->>MaxMind: Lookup GeoIP
|
||||||
|
MaxMind-->>Config: IPInfo
|
||||||
|
Config->>Config: Apply default rule
|
||||||
|
Config->>Config: Cache result
|
||||||
|
Config-->>TCPListener: Result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
alt Logging enabled
|
||||||
|
Config->>Logger: Log access attempt
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Matcher types
|
||||||
|
|
||||||
|
| Type | Format | Example |
|
||||||
|
| -------- | ----------------- | --------------------- |
|
||||||
|
| IP | `ip:address` | `ip:192.168.1.1` |
|
||||||
|
| CIDR | `cidr:network` | `cidr:192.168.0.0/16` |
|
||||||
|
| TimeZone | `tz:timezone` | `tz:Asia/Shanghai` |
|
||||||
|
| Country | `country:ISOCode` | `country:GB` |
|
||||||
|
|
||||||
|
## Configuration Surface
|
||||||
|
|
||||||
|
### Config sources
|
||||||
|
|
||||||
|
Configuration is loaded from `config/config.yml` under the `acl` key.
|
||||||
|
|
||||||
|
### Schema
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
acl:
|
||||||
|
default: "allow" # "allow" or "deny"
|
||||||
|
allow_local: true # Allow private/loopback IPs
|
||||||
|
log:
|
||||||
|
log_allowed: false # Log allowed connections
|
||||||
|
notify:
|
||||||
|
to: ["gotify"] # Notification providers
|
||||||
|
interval: "1m" # Notification interval
|
||||||
|
include_allowed: false # Include allowed in notifications
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hot-reloading
|
||||||
|
|
||||||
|
Configuration requires restart. The ACL does not support dynamic rule updates.
|
||||||
|
|
||||||
|
## Dependency and Integration Map
|
||||||
|
|
||||||
|
### Internal dependencies
|
||||||
|
|
||||||
|
- `internal/maxmind` - IP geolocation lookup
|
||||||
|
- `internal/logging/accesslog` - Access logging
|
||||||
|
- `internal/notif` - Notifications
|
||||||
|
- `internal/task/task.go` - Lifetime management
|
||||||
|
|
||||||
|
### Integration points
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Entrypoint uses ACL to wrap the TCP listener
|
||||||
|
aclListener := config.ACL.WrapTCP(listener)
|
||||||
|
http.Server.Serve(aclListener, entrypoint)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Observability
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
- `ACL started` - Configuration summary on start
|
||||||
|
- `log_notify_loop` - Access attempts (allowed/denied)
|
||||||
|
|
||||||
|
Log levels: `Info` for startup, `Debug` for client closure.
|
||||||
|
|
||||||
|
### Metrics
|
||||||
|
|
||||||
|
No metrics are currently exposed.
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Loopback and private IPs are always allowed unless explicitly denied
|
||||||
|
- Cache TTL is 1 minute to limit memory usage
|
||||||
|
- Notification channel has a buffer of 100 to prevent blocking
|
||||||
|
- Failed connections are immediately closed without response
|
||||||
|
|
||||||
|
## Failure Modes and Recovery
|
||||||
|
|
||||||
|
| Failure | Behavior | Recovery |
|
||||||
|
| --------------------------------- | ------------------------------------- | --------------------------------------------- |
|
||||||
|
| Invalid matcher syntax | Validation fails on startup | Fix configuration syntax |
|
||||||
|
| MaxMind database unavailable | GeoIP lookups return unknown location | Default action applies; cache hit still works |
|
||||||
|
| Notification provider unavailable | Notification dropped | Error logged, continues operation |
|
||||||
|
| Cache full | No eviction, uses Go map | No action needed |
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic configuration
|
||||||
|
|
||||||
|
```go
|
||||||
|
aclConfig := &acl.Config{
|
||||||
|
Default: "allow",
|
||||||
|
AllowLocal: ptr(true),
|
||||||
|
Allow: acl.Matchers{
|
||||||
|
{match: matchIP(net.ParseIP("192.168.1.0/24"))},
|
||||||
|
},
|
||||||
|
Deny: acl.Matchers{
|
||||||
|
{match: matchISOCode("CN")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := aclConfig.Validate(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := aclConfig.Start(parent); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wrapping a TCP listener
|
||||||
|
|
||||||
|
```go
|
||||||
|
listener, err := net.Listen("tcp", ":443")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap with ACL
|
||||||
|
aclListener := aclConfig.WrapTCP(listener)
|
||||||
|
|
||||||
|
// Use with HTTP server
|
||||||
|
server := &http.Server{}
|
||||||
|
server.Serve(aclListener)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating custom matchers
|
||||||
|
|
||||||
|
```go
|
||||||
|
matcher := &acl.Matcher{}
|
||||||
|
err := matcher.Parse("country:US")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the matcher
|
||||||
|
allowed := matcher.match(ipInfo)
|
||||||
|
```
|
||||||
309
internal/acl/config.go
Normal file
309
internal/acl/config.go
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/puzpuzpuz/xsync/v4"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/yusing/godoxy/internal/common"
|
||||||
|
"github.com/yusing/godoxy/internal/logging/accesslog"
|
||||||
|
"github.com/yusing/godoxy/internal/maxmind"
|
||||||
|
"github.com/yusing/godoxy/internal/notif"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
strutils "github.com/yusing/goutils/strings"
|
||||||
|
"github.com/yusing/goutils/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Default string `json:"default" validate:"omitempty,oneof=allow deny"` // default: allow
|
||||||
|
AllowLocal *bool `json:"allow_local"` // default: true
|
||||||
|
Allow Matchers `json:"allow"`
|
||||||
|
Deny Matchers `json:"deny"`
|
||||||
|
Log *accesslog.ACLLoggerConfig `json:"log"`
|
||||||
|
|
||||||
|
Notify struct {
|
||||||
|
To []string `json:"to"` // list of notification providers
|
||||||
|
Interval time.Duration `json:"interval"` // interval between notifications
|
||||||
|
IncludeAllowed *bool `json:"include_allowed"` // default: false
|
||||||
|
} `json:"notify"`
|
||||||
|
|
||||||
|
config
|
||||||
|
valErr gperr.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultNotifyInterval = 1 * time.Minute
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
defaultAllow bool
|
||||||
|
allowLocal bool
|
||||||
|
ipCache *xsync.Map[string, *checkCache]
|
||||||
|
|
||||||
|
// will be nil if Notify.To is empty
|
||||||
|
// these are per IP, reset every Notify.Interval
|
||||||
|
allowedCount map[string]uint32
|
||||||
|
blockedCount map[string]uint32
|
||||||
|
|
||||||
|
// these are total, never reset
|
||||||
|
totalAllowedCount uint64
|
||||||
|
totalBlockedCount uint64
|
||||||
|
|
||||||
|
logAllowed bool
|
||||||
|
// will be nil if Log is nil
|
||||||
|
logger accesslog.AccessLogger
|
||||||
|
|
||||||
|
// will never tick if Notify.To is empty
|
||||||
|
notifyTicker *time.Ticker
|
||||||
|
notifyAllowed bool
|
||||||
|
|
||||||
|
// will be nil if both Log and Notify.To are empty
|
||||||
|
logNotifyCh chan ipLog
|
||||||
|
}
|
||||||
|
|
||||||
|
type checkCache struct {
|
||||||
|
*maxmind.IPInfo
|
||||||
|
allow bool
|
||||||
|
created time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type ipLog struct {
|
||||||
|
info *maxmind.IPInfo
|
||||||
|
allowed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// could be nil
|
||||||
|
var ActiveConfig atomic.Pointer[Config]
|
||||||
|
|
||||||
|
const cacheTTL = 1 * time.Minute
|
||||||
|
|
||||||
|
func (c *checkCache) Expired() bool {
|
||||||
|
return c.created.Add(cacheTTL).Before(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add stats
|
||||||
|
|
||||||
|
const (
|
||||||
|
ACLAllow = "allow"
|
||||||
|
ACLDeny = "deny"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Config) Validate() gperr.Error {
|
||||||
|
switch c.Default {
|
||||||
|
case "", ACLAllow:
|
||||||
|
c.defaultAllow = true
|
||||||
|
case ACLDeny:
|
||||||
|
c.defaultAllow = false
|
||||||
|
default:
|
||||||
|
c.valErr = gperr.New("invalid default value").Subject(c.Default)
|
||||||
|
return c.valErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.AllowLocal != nil {
|
||||||
|
c.allowLocal = *c.AllowLocal
|
||||||
|
} else {
|
||||||
|
c.allowLocal = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Notify.Interval < 0 {
|
||||||
|
c.Notify.Interval = defaultNotifyInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Log != nil {
|
||||||
|
c.logAllowed = c.Log.LogAllowed
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.allowLocal && !c.defaultAllow && len(c.Allow) == 0 {
|
||||||
|
c.valErr = gperr.New("allow_local is false and default is deny, but no allow rules are configured")
|
||||||
|
return c.valErr
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ipCache = xsync.NewMap[string, *checkCache]()
|
||||||
|
|
||||||
|
if c.Notify.IncludeAllowed != nil {
|
||||||
|
c.notifyAllowed = *c.Notify.IncludeAllowed
|
||||||
|
} else {
|
||||||
|
c.notifyAllowed = false
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Valid() bool {
|
||||||
|
return c != nil && c.valErr == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Start(parent task.Parent) gperr.Error {
|
||||||
|
if c.Log != nil {
|
||||||
|
logger, err := accesslog.NewAccessLogger(parent, c.Log)
|
||||||
|
if err != nil {
|
||||||
|
return gperr.New("failed to start access logger").With(err)
|
||||||
|
}
|
||||||
|
c.logger = logger
|
||||||
|
}
|
||||||
|
if c.valErr != nil {
|
||||||
|
return c.valErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.needLogOrNotify() {
|
||||||
|
c.logNotifyCh = make(chan ipLog, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.needNotify() {
|
||||||
|
c.allowedCount = make(map[string]uint32)
|
||||||
|
c.blockedCount = make(map[string]uint32)
|
||||||
|
c.notifyTicker = time.NewTicker(c.Notify.Interval)
|
||||||
|
} else {
|
||||||
|
c.notifyTicker = time.NewTicker(time.Duration(math.MaxInt64)) // never tick
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.needLogOrNotify() {
|
||||||
|
go c.logNotifyLoop(parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Str("default", c.Default).
|
||||||
|
Bool("allow_local", c.allowLocal).
|
||||||
|
Int("allow_rules", len(c.Allow)).
|
||||||
|
Int("deny_rules", len(c.Deny)).
|
||||||
|
Msg("ACL started")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool) {
|
||||||
|
if common.ForceResolveCountry && info.City == nil {
|
||||||
|
maxmind.LookupCity(info)
|
||||||
|
}
|
||||||
|
c.ipCache.Store(info.Str, &checkCache{
|
||||||
|
IPInfo: info,
|
||||||
|
allow: allow,
|
||||||
|
created: time.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) needLogOrNotify() bool {
|
||||||
|
return c.needLog() || c.needNotify()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) needLog() bool {
|
||||||
|
return c.logger != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) needNotify() bool {
|
||||||
|
return len(c.Notify.To) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) getCachedCity(ip string) string {
|
||||||
|
record, ok := c.ipCache.Load(ip)
|
||||||
|
if ok {
|
||||||
|
if record.City != nil {
|
||||||
|
if record.City.Country.IsoCode != "" {
|
||||||
|
return record.City.Country.IsoCode
|
||||||
|
}
|
||||||
|
return record.City.Location.TimeZone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "unknown location"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) logNotifyLoop(parent task.Parent) {
|
||||||
|
defer c.notifyTicker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-parent.Context().Done():
|
||||||
|
return
|
||||||
|
case log := <-c.logNotifyCh:
|
||||||
|
if c.logger != nil {
|
||||||
|
if !log.allowed || c.logAllowed {
|
||||||
|
c.logger.LogACL(log.info, !log.allowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.needNotify() {
|
||||||
|
if log.allowed {
|
||||||
|
if c.notifyAllowed {
|
||||||
|
c.allowedCount[log.info.Str]++
|
||||||
|
c.totalAllowedCount++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.blockedCount[log.info.Str]++
|
||||||
|
c.totalBlockedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case <-c.notifyTicker.C: // will never tick when notify is disabled
|
||||||
|
total := len(c.allowedCount) + len(c.blockedCount)
|
||||||
|
if total == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
total++
|
||||||
|
fieldsBody := make(notif.ListBody, total)
|
||||||
|
i := 0
|
||||||
|
fieldsBody[i] = fmt.Sprintf("Total: allowed %d, blocked %d", c.totalAllowedCount, c.totalBlockedCount)
|
||||||
|
i++
|
||||||
|
for ip, count := range c.allowedCount {
|
||||||
|
fieldsBody[i] = fmt.Sprintf("%s (%s): allowed %d times", ip, c.getCachedCity(ip), count)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
for ip, count := range c.blockedCount {
|
||||||
|
fieldsBody[i] = fmt.Sprintf("%s (%s): blocked %d times", ip, c.getCachedCity(ip), count)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
notif.Notify(¬if.LogMessage{
|
||||||
|
Level: zerolog.InfoLevel,
|
||||||
|
Title: "ACL Summary for last " + strutils.FormatDuration(c.Notify.Interval),
|
||||||
|
Body: fieldsBody,
|
||||||
|
To: c.Notify.To,
|
||||||
|
})
|
||||||
|
clear(c.allowedCount)
|
||||||
|
clear(c.blockedCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// log and notify if needed
|
||||||
|
func (c *Config) logAndNotify(info *maxmind.IPInfo, allowed bool) {
|
||||||
|
if c.logNotifyCh != nil {
|
||||||
|
c.logNotifyCh <- ipLog{info: info, allowed: allowed}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) IPAllowed(ip net.IP) bool {
|
||||||
|
if ip == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// always allow loopback, not logged
|
||||||
|
if ip.IsLoopback() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.allowLocal && ip.IsPrivate() {
|
||||||
|
c.logAndNotify(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
ipStr := ip.String()
|
||||||
|
record, ok := c.ipCache.Load(ipStr)
|
||||||
|
if ok && !record.Expired() {
|
||||||
|
c.logAndNotify(record.IPInfo, record.allow)
|
||||||
|
return record.allow
|
||||||
|
}
|
||||||
|
|
||||||
|
ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr}
|
||||||
|
if c.Allow.Match(ipAndStr) {
|
||||||
|
c.logAndNotify(ipAndStr, true)
|
||||||
|
c.cacheRecord(ipAndStr, true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if c.Deny.Match(ipAndStr) {
|
||||||
|
c.logAndNotify(ipAndStr, false)
|
||||||
|
c.cacheRecord(ipAndStr, false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logAndNotify(ipAndStr, c.defaultAllow)
|
||||||
|
c.cacheRecord(ipAndStr, c.defaultAllow)
|
||||||
|
return c.defaultAllow
|
||||||
|
}
|
||||||
112
internal/acl/matcher.go
Normal file
112
internal/acl/matcher.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yusing/godoxy/internal/maxmind"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MatcherFunc func(*maxmind.IPInfo) bool
|
||||||
|
|
||||||
|
type Matcher struct {
|
||||||
|
match MatcherFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type Matchers []Matcher
|
||||||
|
|
||||||
|
const (
|
||||||
|
MatcherTypeIP = "ip"
|
||||||
|
MatcherTypeCIDR = "cidr"
|
||||||
|
MatcherTypeTimeZone = "tz"
|
||||||
|
MatcherTypeCountry = "country"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: use this error in the future
|
||||||
|
//
|
||||||
|
//nolint:unused
|
||||||
|
var errMatcherFormat = gperr.Multiline().AddLines(
|
||||||
|
"invalid matcher format, expect {type}:{value}",
|
||||||
|
"Available types: ip|cidr|tz|country",
|
||||||
|
"ip:127.0.0.1",
|
||||||
|
"cidr:127.0.0.0/8",
|
||||||
|
"tz:Asia/Shanghai",
|
||||||
|
"country:GB",
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errSyntax = gperr.New("syntax error")
|
||||||
|
errInvalidIP = gperr.New("invalid IP")
|
||||||
|
errInvalidCIDR = gperr.New("invalid CIDR")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (matcher *Matcher) Parse(s string) error {
|
||||||
|
parts := strings.Split(s, ":")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return errSyntax
|
||||||
|
}
|
||||||
|
|
||||||
|
switch parts[0] {
|
||||||
|
case MatcherTypeIP:
|
||||||
|
ip := net.ParseIP(parts[1])
|
||||||
|
if ip == nil {
|
||||||
|
return errInvalidIP
|
||||||
|
}
|
||||||
|
matcher.match = matchIP(ip)
|
||||||
|
case MatcherTypeCIDR:
|
||||||
|
_, net, err := net.ParseCIDR(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return errInvalidCIDR
|
||||||
|
}
|
||||||
|
matcher.match = matchCIDR(net)
|
||||||
|
case MatcherTypeTimeZone:
|
||||||
|
matcher.match = matchTimeZone(parts[1])
|
||||||
|
case MatcherTypeCountry:
|
||||||
|
matcher.match = matchISOCode(parts[1])
|
||||||
|
default:
|
||||||
|
return errSyntax
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (matchers Matchers) Match(ip *maxmind.IPInfo) bool {
|
||||||
|
for _, m := range matchers {
|
||||||
|
if m.match(ip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchIP(ip net.IP) MatcherFunc {
|
||||||
|
return func(ip2 *maxmind.IPInfo) bool {
|
||||||
|
return ip.Equal(ip2.IP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchCIDR(n *net.IPNet) MatcherFunc {
|
||||||
|
return func(ip *maxmind.IPInfo) bool {
|
||||||
|
return n.Contains(ip.IP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchTimeZone(tz string) MatcherFunc {
|
||||||
|
return func(ip *maxmind.IPInfo) bool {
|
||||||
|
city, ok := maxmind.LookupCity(ip)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return city.Location.TimeZone == tz
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchISOCode(iso string) MatcherFunc {
|
||||||
|
return func(ip *maxmind.IPInfo) bool {
|
||||||
|
city, ok := maxmind.LookupCity(ip)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return city.Country.IsoCode == iso
|
||||||
|
}
|
||||||
|
}
|
||||||
49
internal/acl/matcher_test.go
Normal file
49
internal/acl/matcher_test.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
|
||||||
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMatchers(t *testing.T) {
|
||||||
|
strMatchers := []string{
|
||||||
|
"ip:127.0.0.1",
|
||||||
|
"cidr:10.0.0.0/8",
|
||||||
|
}
|
||||||
|
|
||||||
|
var mathers Matchers
|
||||||
|
err := serialization.Convert(reflect.ValueOf(strMatchers), reflect.ValueOf(&mathers), false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
ip string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"127.0.0.1", true},
|
||||||
|
{"10.0.0.1", true},
|
||||||
|
{"127.0.0.2", false},
|
||||||
|
{"192.168.0.1", false},
|
||||||
|
{"11.0.0.1", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
ip := net.ParseIP(test.ip)
|
||||||
|
if ip == nil {
|
||||||
|
t.Fatalf("invalid ip: %s", test.ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := mathers.Match(&maxmind.IPInfo{
|
||||||
|
IP: ip,
|
||||||
|
Str: test.ip,
|
||||||
|
})
|
||||||
|
if got != test.want {
|
||||||
|
t.Errorf("mathers.Match(%s) = %v, want %v", test.ip, got, test.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
internal/acl/tcp_listener.go
Normal file
75
internal/acl/tcp_listener.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TCPListener struct {
|
||||||
|
acl *Config
|
||||||
|
lis net.Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
type noConn struct{}
|
||||||
|
|
||||||
|
func (noConn) Read(b []byte) (int, error) { return 0, io.EOF }
|
||||||
|
func (noConn) Write(b []byte) (int, error) { return 0, io.EOF }
|
||||||
|
func (noConn) Close() error { return nil }
|
||||||
|
func (noConn) LocalAddr() net.Addr { return nil }
|
||||||
|
func (noConn) RemoteAddr() net.Addr { return nil }
|
||||||
|
func (noConn) SetDeadline(t time.Time) error { return nil }
|
||||||
|
func (noConn) SetReadDeadline(t time.Time) error { return nil }
|
||||||
|
func (noConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||||
|
|
||||||
|
func (c *Config) WrapTCP(lis net.Listener) net.Listener {
|
||||||
|
if c == nil {
|
||||||
|
return lis
|
||||||
|
}
|
||||||
|
return &TCPListener{
|
||||||
|
acl: c,
|
||||||
|
lis: lis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TCPListener) Addr() net.Addr {
|
||||||
|
return s.lis.Addr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TCPListener) Accept() (net.Conn, error) {
|
||||||
|
c, err := s.lis.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
addr, ok := c.RemoteAddr().(*net.TCPAddr)
|
||||||
|
if !ok {
|
||||||
|
// Not a TCPAddr, drop
|
||||||
|
c.Close()
|
||||||
|
return noConn{}, nil
|
||||||
|
}
|
||||||
|
if !s.acl.IPAllowed(addr.IP) {
|
||||||
|
c.Close()
|
||||||
|
return noConn{}, nil
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type tcpListener interface {
|
||||||
|
SetDeadline(t time.Time) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ tcpListener = (*net.TCPListener)(nil)
|
||||||
|
|
||||||
|
func (s *TCPListener) SetDeadline(t time.Time) error {
|
||||||
|
switch lis := s.lis.(type) {
|
||||||
|
case tcpListener:
|
||||||
|
return lis.SetDeadline(t)
|
||||||
|
default:
|
||||||
|
return errors.New("not a TCPListener")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TCPListener) Close() error {
|
||||||
|
return s.lis.Close()
|
||||||
|
}
|
||||||
105
internal/acl/udp_listener.go
Normal file
105
internal/acl/udp_listener.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UDPListener struct {
|
||||||
|
acl *Config
|
||||||
|
lis net.PacketConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) WrapUDP(lis net.PacketConn) net.PacketConn {
|
||||||
|
if c == nil {
|
||||||
|
return lis
|
||||||
|
}
|
||||||
|
return &UDPListener{
|
||||||
|
acl: c,
|
||||||
|
lis: lis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) LocalAddr() net.Addr {
|
||||||
|
return s.lis.LocalAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||||
|
for {
|
||||||
|
n, addr, err := s.lis.ReadFrom(p)
|
||||||
|
if err != nil {
|
||||||
|
return n, addr, err
|
||||||
|
}
|
||||||
|
udpAddr, ok := addr.(*net.UDPAddr)
|
||||||
|
if !ok {
|
||||||
|
// Not a UDPAddr, drop
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !s.acl.IPAllowed(udpAddr.IP) {
|
||||||
|
// Drop packet from disallowed IP
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return n, addr, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) WriteTo(p []byte, addr net.Addr) (int, error) {
|
||||||
|
for {
|
||||||
|
n, err := s.lis.WriteTo(p, addr)
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
udpAddr, ok := addr.(*net.UDPAddr)
|
||||||
|
if !ok {
|
||||||
|
// Not a UDPAddr, drop
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !s.acl.IPAllowed(udpAddr.IP) {
|
||||||
|
// Drop packet to disallowed IP
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) SetDeadline(t time.Time) error {
|
||||||
|
return s.lis.SetDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) SetReadDeadline(t time.Time) error {
|
||||||
|
return s.lis.SetReadDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) SetWriteDeadline(t time.Time) error {
|
||||||
|
return s.lis.SetWriteDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
type udpListener interface {
|
||||||
|
SetReadBuffer(bytes int) error
|
||||||
|
SetWriteBuffer(bytes int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ udpListener = (*net.UDPConn)(nil)
|
||||||
|
|
||||||
|
func (s *UDPListener) SetReadBuffer(bytes int) error {
|
||||||
|
switch lis := s.lis.(type) {
|
||||||
|
case udpListener:
|
||||||
|
return lis.SetReadBuffer(bytes)
|
||||||
|
default:
|
||||||
|
return errors.New("not a UDPConn")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) SetWriteBuffer(bytes int) error {
|
||||||
|
switch lis := s.lis.(type) {
|
||||||
|
case udpListener:
|
||||||
|
return lis.SetWriteBuffer(bytes)
|
||||||
|
default:
|
||||||
|
return errors.New("not a UDPConn")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) Close() error {
|
||||||
|
return s.lis.Close()
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user