feat(nftables): move NFTables forwarding management from the Rust engine to @push.rocks/smartnftables
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-26 - 26.3.0 - feat(nftables)
|
||||||
|
move NFTables forwarding management from the Rust engine to @push.rocks/smartnftables
|
||||||
|
|
||||||
|
- add @push.rocks/smartnftables as a runtime dependency and export it via the plugin layer
|
||||||
|
- remove the internal rustproxy-nftables crate along with Rust-side NFTables rule application and status management
|
||||||
|
- apply and clean up NFTables port-forwarding rules in the TypeScript SmartProxy lifecycle and route update flow
|
||||||
|
- change getNfTablesStatus to return local smartnftables status instead of querying the Rust bridge
|
||||||
|
- update README documentation to describe NFTables support as provided through @push.rocks/smartnftables
|
||||||
|
|
||||||
## 2026-03-26 - 26.2.4 - fix(rustproxy-http)
|
## 2026-03-26 - 26.2.4 - fix(rustproxy-http)
|
||||||
improve HTTP/3 connection reuse and clean up stale proxy state
|
improve HTTP/3 connection reuse and clean up stale proxy state
|
||||||
|
|
||||||
|
|||||||
10
deno.lock
generated
10
deno.lock
generated
@@ -7,6 +7,7 @@
|
|||||||
"npm:@git.zone/tstest@^3.6.0": "3.6.0_typescript@6.0.2",
|
"npm:@git.zone/tstest@^3.6.0": "3.6.0_typescript@6.0.2",
|
||||||
"npm:@push.rocks/smartcrypto@^2.0.4": "2.0.4",
|
"npm:@push.rocks/smartcrypto@^2.0.4": "2.0.4",
|
||||||
"npm:@push.rocks/smartlog@^3.2.1": "3.2.1",
|
"npm:@push.rocks/smartlog@^3.2.1": "3.2.1",
|
||||||
|
"npm:@push.rocks/smartnftables@^1.0.1": "1.0.1",
|
||||||
"npm:@push.rocks/smartrust@^1.3.2": "1.3.2",
|
"npm:@push.rocks/smartrust@^1.3.2": "1.3.2",
|
||||||
"npm:@push.rocks/smartserve@^2.0.3": "2.0.3",
|
"npm:@push.rocks/smartserve@^2.0.3": "2.0.3",
|
||||||
"npm:@tsclass/tsclass@^9.5.0": "9.5.0",
|
"npm:@tsclass/tsclass@^9.5.0": "9.5.0",
|
||||||
@@ -2298,6 +2299,14 @@
|
|||||||
],
|
],
|
||||||
"tarball": "https://verdaccio.lossless.digital/@push.rocks/smartnetwork/-/smartnetwork-4.4.0.tgz"
|
"tarball": "https://verdaccio.lossless.digital/@push.rocks/smartnetwork/-/smartnetwork-4.4.0.tgz"
|
||||||
},
|
},
|
||||||
|
"@push.rocks/smartnftables@1.0.1": {
|
||||||
|
"integrity": "sha512-o822GH4J8dlEBvNLbm+CwU4h6isMUEh03tf2ZnOSWXc5iewRDdKdOCDwI/e+WdnGYWyv7gvH0DHztCmne6rTCg==",
|
||||||
|
"dependencies": [
|
||||||
|
"@push.rocks/smartlog",
|
||||||
|
"@push.rocks/smartpromise"
|
||||||
|
],
|
||||||
|
"tarball": "https://verdaccio.lossless.digital/@push.rocks/smartnftables/-/smartnftables-1.0.1.tgz"
|
||||||
|
},
|
||||||
"@push.rocks/smartnpm@2.0.6": {
|
"@push.rocks/smartnpm@2.0.6": {
|
||||||
"integrity": "sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==",
|
"integrity": "sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -6729,6 +6738,7 @@
|
|||||||
"npm:@git.zone/tstest@^3.6.0",
|
"npm:@git.zone/tstest@^3.6.0",
|
||||||
"npm:@push.rocks/smartcrypto@^2.0.4",
|
"npm:@push.rocks/smartcrypto@^2.0.4",
|
||||||
"npm:@push.rocks/smartlog@^3.2.1",
|
"npm:@push.rocks/smartlog@^3.2.1",
|
||||||
|
"npm:@push.rocks/smartnftables@^1.0.1",
|
||||||
"npm:@push.rocks/smartrust@^1.3.2",
|
"npm:@push.rocks/smartrust@^1.3.2",
|
||||||
"npm:@push.rocks/smartserve@^2.0.3",
|
"npm:@push.rocks/smartserve@^2.0.3",
|
||||||
"npm:@tsclass/tsclass@^9.5.0",
|
"npm:@tsclass/tsclass@^9.5.0",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/smartcrypto": "^2.0.4",
|
"@push.rocks/smartcrypto": "^2.0.4",
|
||||||
"@push.rocks/smartlog": "^3.2.1",
|
"@push.rocks/smartlog": "^3.2.1",
|
||||||
|
"@push.rocks/smartnftables": "^1.0.1",
|
||||||
"@push.rocks/smartrust": "^1.3.2",
|
"@push.rocks/smartrust": "^1.3.2",
|
||||||
"@tsclass/tsclass": "^9.5.0",
|
"@tsclass/tsclass": "^9.5.0",
|
||||||
"minimatch": "^10.2.4"
|
"minimatch": "^10.2.4"
|
||||||
|
|||||||
37
pnpm-lock.yaml
generated
37
pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
|||||||
'@push.rocks/smartlog':
|
'@push.rocks/smartlog':
|
||||||
specifier: ^3.2.1
|
specifier: ^3.2.1
|
||||||
version: 3.2.1
|
version: 3.2.1
|
||||||
|
'@push.rocks/smartnftables':
|
||||||
|
specifier: ^1.0.1
|
||||||
|
version: 1.0.1
|
||||||
'@push.rocks/smartrust':
|
'@push.rocks/smartrust':
|
||||||
specifier: ^1.3.2
|
specifier: ^1.3.2
|
||||||
version: 1.3.2
|
version: 1.3.2
|
||||||
@@ -468,89 +471,105 @@ packages:
|
|||||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-linux-arm64@0.34.5':
|
'@img/sharp-linux-arm64@0.34.5':
|
||||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-arm@0.34.5':
|
'@img/sharp-linux-arm@0.34.5':
|
||||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-ppc64@0.34.5':
|
'@img/sharp-linux-ppc64@0.34.5':
|
||||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-riscv64@0.34.5':
|
'@img/sharp-linux-riscv64@0.34.5':
|
||||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-s390x@0.34.5':
|
'@img/sharp-linux-s390x@0.34.5':
|
||||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-x64@0.34.5':
|
'@img/sharp-linux-x64@0.34.5':
|
||||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-wasm32@0.34.5':
|
'@img/sharp-wasm32@0.34.5':
|
||||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||||
@@ -978,6 +997,9 @@ packages:
|
|||||||
'@push.rocks/smartnetwork@4.4.0':
|
'@push.rocks/smartnetwork@4.4.0':
|
||||||
resolution: {integrity: sha512-OvFtz41cvQ7lcXwaIOhghNUUlNoMxvwKDctbDvMyuZyEH08SpLjhyv2FuKbKL/mgwA/WxakTbohoC8SW7t+kiw==}
|
resolution: {integrity: sha512-OvFtz41cvQ7lcXwaIOhghNUUlNoMxvwKDctbDvMyuZyEH08SpLjhyv2FuKbKL/mgwA/WxakTbohoC8SW7t+kiw==}
|
||||||
|
|
||||||
|
'@push.rocks/smartnftables@1.0.1':
|
||||||
|
resolution: {integrity: sha512-o822GH4J8dlEBvNLbm+CwU4h6isMUEh03tf2ZnOSWXc5iewRDdKdOCDwI/e+WdnGYWyv7gvH0DHztCmne6rTCg==}
|
||||||
|
|
||||||
'@push.rocks/smartnpm@2.0.6':
|
'@push.rocks/smartnpm@2.0.6':
|
||||||
resolution: {integrity: sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==}
|
resolution: {integrity: sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==}
|
||||||
|
|
||||||
@@ -1121,36 +1143,42 @@ packages:
|
|||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.11':
|
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==}
|
resolution: {integrity: sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11':
|
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==}
|
resolution: {integrity: sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11':
|
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==}
|
resolution: {integrity: sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.11':
|
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==}
|
resolution: {integrity: sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-musl@1.0.0-rc.11':
|
'@rolldown/binding-linux-x64-musl@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==}
|
resolution: {integrity: sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rolldown/binding-openharmony-arm64@1.0.0-rc.11':
|
'@rolldown/binding-openharmony-arm64@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==}
|
resolution: {integrity: sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==}
|
||||||
@@ -1192,21 +1220,25 @@ packages:
|
|||||||
resolution: {integrity: sha512-Z4reus7UxGM4+JuhiIht8KuGP1KgM7nNhOlXUHcQCMswP/Rymj5oJQN3TDWgijFUZs09ULl8t3T+AQAVTd/WvA==}
|
resolution: {integrity: sha512-Z4reus7UxGM4+JuhiIht8KuGP1KgM7nNhOlXUHcQCMswP/Rymj5oJQN3TDWgijFUZs09ULl8t3T+AQAVTd/WvA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rspack/binding-linux-arm64-musl@1.7.10':
|
'@rspack/binding-linux-arm64-musl@1.7.10':
|
||||||
resolution: {integrity: sha512-LYaoVmWizG4oQ3g+St3eM5qxsyfH07kLirP7NJcDMgvu3eQ29MeyTZ3ugkgW6LvlmJue7eTQyf6CZlanoF5SSg==}
|
resolution: {integrity: sha512-LYaoVmWizG4oQ3g+St3eM5qxsyfH07kLirP7NJcDMgvu3eQ29MeyTZ3ugkgW6LvlmJue7eTQyf6CZlanoF5SSg==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rspack/binding-linux-x64-gnu@1.7.10':
|
'@rspack/binding-linux-x64-gnu@1.7.10':
|
||||||
resolution: {integrity: sha512-aIm2G4Kcm3qxDTNqKarK0oaLY2iXnCmpRQQhAcMlR0aS2LmxL89XzVeRr9GFA1MzGrAsZONWCLkxQvn3WUbm4Q==}
|
resolution: {integrity: sha512-aIm2G4Kcm3qxDTNqKarK0oaLY2iXnCmpRQQhAcMlR0aS2LmxL89XzVeRr9GFA1MzGrAsZONWCLkxQvn3WUbm4Q==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rspack/binding-linux-x64-musl@1.7.10':
|
'@rspack/binding-linux-x64-musl@1.7.10':
|
||||||
resolution: {integrity: sha512-SIHQbAgB9IPH0H3H+i5rN5jo9yA/yTMq8b7XfRkTMvZ7P7MXxJ0dE8EJu3BmCLM19sqnTc2eX+SVfE8ZMDzghA==}
|
resolution: {integrity: sha512-SIHQbAgB9IPH0H3H+i5rN5jo9yA/yTMq8b7XfRkTMvZ7P7MXxJ0dE8EJu3BmCLM19sqnTc2eX+SVfE8ZMDzghA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rspack/binding-wasm32-wasi@1.7.10':
|
'@rspack/binding-wasm32-wasi@1.7.10':
|
||||||
resolution: {integrity: sha512-J9HDXHD1tj+9FmX4+K3CTkO7dCE2bootlR37YuC2Owc0Lwl1/i2oGT71KHnMqI9faF/hipAaQM5OywkiiuNB7w==}
|
resolution: {integrity: sha512-J9HDXHD1tj+9FmX4+K3CTkO7dCE2bootlR37YuC2Owc0Lwl1/i2oGT71KHnMqI9faF/hipAaQM5OywkiiuNB7w==}
|
||||||
@@ -5130,6 +5162,11 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@push.rocks/smartnftables@1.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/smartlog': 3.2.1
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
|
||||||
'@push.rocks/smartnpm@2.0.6':
|
'@push.rocks/smartnpm@2.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/consolecolor': 2.0.3
|
'@push.rocks/consolecolor': 2.0.3
|
||||||
|
|||||||
38
readme.md
38
readme.md
@@ -1,6 +1,6 @@
|
|||||||
# @push.rocks/smartproxy 🚀
|
# @push.rocks/smartproxy 🚀
|
||||||
|
|
||||||
**A high-performance, Rust-powered proxy toolkit for Node.js** — unified route-based configuration for SSL/TLS termination, HTTP/HTTPS reverse proxying, WebSocket support, UDP/QUIC/HTTP3, load balancing, custom protocol handlers, and kernel-level NFTables forwarding.
|
**A high-performance, Rust-powered proxy toolkit for Node.js** — unified route-based configuration for SSL/TLS termination, HTTP/HTTPS reverse proxying, WebSocket support, UDP/QUIC/HTTP3, load balancing, custom protocol handlers, and kernel-level NFTables forwarding via [`@push.rocks/smartnftables`](https://code.foss.global/push.rocks/smartnftables).
|
||||||
|
|
||||||
## 📦 Installation
|
## 📦 Installation
|
||||||
|
|
||||||
@@ -384,7 +384,7 @@ const dualStackRoute: IRouteConfig = {
|
|||||||
|
|
||||||
### ⚡ High-Performance NFTables Forwarding
|
### ⚡ High-Performance NFTables Forwarding
|
||||||
|
|
||||||
For ultra-low latency on Linux, use kernel-level forwarding (requires root):
|
For ultra-low latency on Linux, use kernel-level forwarding via [`@push.rocks/smartnftables`](https://code.foss.global/push.rocks/smartnftables) (requires root):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { SmartProxy, createNfTablesTerminateRoute } from '@push.rocks/smartproxy';
|
import { SmartProxy, createNfTablesTerminateRoute } from '@push.rocks/smartproxy';
|
||||||
@@ -694,22 +694,26 @@ SmartProxy uses a hybrid **Rust + TypeScript** architecture:
|
|||||||
│ │ Listener│ │ Reverse │ │ Matcher │ │ Cert Mgr │ │
|
│ │ Listener│ │ Reverse │ │ Matcher │ │ Cert Mgr │ │
|
||||||
│ │ │ │ Proxy │ │ │ │ │ │
|
│ │ │ │ Proxy │ │ │ │ │ │
|
||||||
│ └─────────┘ └─────────┘ └─────────┘ └──────────┘ │
|
│ └─────────┘ └─────────┘ └─────────┘ └──────────┘ │
|
||||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐ │
|
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||||
│ │ UDP │ │ Security│ │ Metrics │ │ NFTables │ │
|
│ │ UDP │ │ Security│ │ Metrics │ │
|
||||||
│ │ QUIC │ │ Enforce │ │ Collect │ │ Mgr │ │
|
│ │ QUIC │ │ Enforce │ │ Collect │ │
|
||||||
│ │ HTTP/3 │ │ │ │ │ │ │ │
|
│ │ HTTP/3 │ │ │ │ │ │
|
||||||
│ └─────────┘ └─────────┘ └─────────┘ └──────────┘ │
|
│ └─────────┘ └─────────┘ └─────────┘ │
|
||||||
└──────────────────┬──────────────────────────────────┘
|
└──────────────────┬──────────────────────────────────┘
|
||||||
│ Unix Socket Relay
|
│ Unix Socket Relay
|
||||||
┌──────────────────▼──────────────────────────────────┐
|
┌──────────────────▼──────────────────────────────────┐
|
||||||
│ TypeScript Socket & Datagram Handler Servers │
|
│ TypeScript Socket & Datagram Handler Servers │
|
||||||
│ (for JS socket handlers, datagram handlers, │
|
│ (for JS socket handlers, datagram handlers, │
|
||||||
│ and dynamic routes) │
|
│ and dynamic routes) │
|
||||||
|
├─────────────────────────────────────────────────────┤
|
||||||
|
│ @push.rocks/smartnftables (kernel-level NFTables) │
|
||||||
|
│ (DNAT/SNAT, firewall, rate limiting via nft CLI) │
|
||||||
└─────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Rust Engine** handles all networking: TCP, UDP, TLS, QUIC, HTTP proxying, connection management, security, and metrics
|
- **Rust Engine** handles all networking: TCP, UDP, TLS, QUIC, HTTP proxying, connection management, security, and metrics
|
||||||
- **TypeScript** provides the npm API, configuration types, route helpers, validation, and handler callbacks
|
- **TypeScript** provides the npm API, configuration types, route helpers, validation, and handler callbacks
|
||||||
|
- **NFTables** managed by [`@push.rocks/smartnftables`](https://code.foss.global/push.rocks/smartnftables) — kernel-level DNAT/SNAT forwarding, firewall rules, and rate limiting via the `nft` CLI
|
||||||
- **IPC** — The TypeScript wrapper uses JSON commands/events over stdin/stdout to communicate with the Rust binary
|
- **IPC** — The TypeScript wrapper uses JSON commands/events over stdin/stdout to communicate with the Rust binary
|
||||||
- **Socket/Datagram Relay** — Unix domain socket servers for routes requiring TypeScript-side handling (socket handlers, datagram handlers, dynamic host/port functions)
|
- **Socket/Datagram Relay** — Unix domain socket servers for routes requiring TypeScript-side handling (socket handlers, datagram handlers, dynamic host/port functions)
|
||||||
|
|
||||||
@@ -938,8 +942,8 @@ class SmartProxy extends EventEmitter {
|
|||||||
getCertificateStatus(routeName: string): Promise<any>;
|
getCertificateStatus(routeName: string): Promise<any>;
|
||||||
getEligibleDomainsForCertificates(): string[];
|
getEligibleDomainsForCertificates(): string[];
|
||||||
|
|
||||||
// NFTables
|
// NFTables (managed by @push.rocks/smartnftables)
|
||||||
getNfTablesStatus(): Promise<Record<string, any>>;
|
getNfTablesStatus(): INftStatus | null;
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
on(event: 'error', handler: (err: Error) => void): this;
|
on(event: 'error', handler: (err: Error) => void): this;
|
||||||
@@ -991,11 +995,11 @@ interface ISmartProxyOptions {
|
|||||||
sendProxyProtocol?: boolean; // Send PROXY protocol to targets
|
sendProxyProtocol?: boolean; // Send PROXY protocol to targets
|
||||||
|
|
||||||
// Timeouts
|
// Timeouts
|
||||||
connectionTimeout?: number; // Backend connection timeout (default: 30s)
|
connectionTimeout?: number; // Backend connection timeout (default: 60s)
|
||||||
initialDataTimeout?: number; // Initial data/SNI timeout (default: 120s)
|
initialDataTimeout?: number; // Initial data/SNI timeout (default: 60s)
|
||||||
socketTimeout?: number; // Socket inactivity timeout (default: 1h)
|
socketTimeout?: number; // Socket inactivity timeout (default: 60s)
|
||||||
maxConnectionLifetime?: number; // Max connection lifetime (default: 24h)
|
maxConnectionLifetime?: number; // Max connection lifetime (default: 1h)
|
||||||
inactivityTimeout?: number; // Inactivity timeout (default: 4h)
|
inactivityTimeout?: number; // Inactivity timeout (default: 75s)
|
||||||
gracefulShutdownTimeout?: number; // Shutdown grace period (default: 30s)
|
gracefulShutdownTimeout?: number; // Shutdown grace period (default: 30s)
|
||||||
|
|
||||||
// Connection limits
|
// Connection limits
|
||||||
@@ -1004,8 +1008,8 @@ interface ISmartProxyOptions {
|
|||||||
|
|
||||||
// Keep-alive
|
// Keep-alive
|
||||||
keepAliveTreatment?: 'standard' | 'extended' | 'immortal';
|
keepAliveTreatment?: 'standard' | 'extended' | 'immortal';
|
||||||
keepAliveInactivityMultiplier?: number; // (default: 6)
|
keepAliveInactivityMultiplier?: number; // (default: 4)
|
||||||
extendedKeepAliveLifetime?: number; // (default: 7 days)
|
extendedKeepAliveLifetime?: number; // (default: 1h)
|
||||||
|
|
||||||
// Metrics
|
// Metrics
|
||||||
metrics?: {
|
metrics?: {
|
||||||
@@ -1137,7 +1141,7 @@ SmartProxy searches for the Rust binary in this order:
|
|||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
|
||||||
|
|
||||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
|||||||
15
rust/Cargo.lock
generated
15
rust/Cargo.lock
generated
@@ -1238,7 +1238,6 @@ dependencies = [
|
|||||||
"rustproxy-config",
|
"rustproxy-config",
|
||||||
"rustproxy-http",
|
"rustproxy-http",
|
||||||
"rustproxy-metrics",
|
"rustproxy-metrics",
|
||||||
"rustproxy-nftables",
|
|
||||||
"rustproxy-passthrough",
|
"rustproxy-passthrough",
|
||||||
"rustproxy-routing",
|
"rustproxy-routing",
|
||||||
"rustproxy-security",
|
"rustproxy-security",
|
||||||
@@ -1304,20 +1303,6 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustproxy-nftables"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"libc",
|
|
||||||
"rustproxy-config",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror 2.0.18",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustproxy-passthrough"
|
name = "rustproxy-passthrough"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ members = [
|
|||||||
"crates/rustproxy-tls",
|
"crates/rustproxy-tls",
|
||||||
"crates/rustproxy-passthrough",
|
"crates/rustproxy-passthrough",
|
||||||
"crates/rustproxy-http",
|
"crates/rustproxy-http",
|
||||||
"crates/rustproxy-nftables",
|
|
||||||
"crates/rustproxy-metrics",
|
"crates/rustproxy-metrics",
|
||||||
"crates/rustproxy-security",
|
"crates/rustproxy-security",
|
||||||
]
|
]
|
||||||
@@ -107,6 +106,5 @@ rustproxy-routing = { path = "crates/rustproxy-routing" }
|
|||||||
rustproxy-tls = { path = "crates/rustproxy-tls" }
|
rustproxy-tls = { path = "crates/rustproxy-tls" }
|
||||||
rustproxy-passthrough = { path = "crates/rustproxy-passthrough" }
|
rustproxy-passthrough = { path = "crates/rustproxy-passthrough" }
|
||||||
rustproxy-http = { path = "crates/rustproxy-http" }
|
rustproxy-http = { path = "crates/rustproxy-http" }
|
||||||
rustproxy-nftables = { path = "crates/rustproxy-nftables" }
|
|
||||||
rustproxy-metrics = { path = "crates/rustproxy-metrics" }
|
rustproxy-metrics = { path = "crates/rustproxy-metrics" }
|
||||||
rustproxy-security = { path = "crates/rustproxy-security" }
|
rustproxy-security = { path = "crates/rustproxy-security" }
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ pub fn create_http_route(
|
|||||||
load_balancing: None,
|
load_balancing: None,
|
||||||
advanced: None,
|
advanced: None,
|
||||||
options: None,
|
options: None,
|
||||||
forwarding_engine: None,
|
|
||||||
nftables: None,
|
|
||||||
send_proxy_protocol: None,
|
send_proxy_protocol: None,
|
||||||
udp: None,
|
udp: None,
|
||||||
},
|
},
|
||||||
@@ -138,8 +136,6 @@ pub fn create_http_to_https_redirect(
|
|||||||
url_rewrite: None,
|
url_rewrite: None,
|
||||||
}),
|
}),
|
||||||
options: None,
|
options: None,
|
||||||
forwarding_engine: None,
|
|
||||||
nftables: None,
|
|
||||||
send_proxy_protocol: None,
|
send_proxy_protocol: None,
|
||||||
udp: None,
|
udp: None,
|
||||||
},
|
},
|
||||||
@@ -222,8 +218,6 @@ pub fn create_load_balancer_route(
|
|||||||
}),
|
}),
|
||||||
advanced: None,
|
advanced: None,
|
||||||
options: None,
|
options: None,
|
||||||
forwarding_engine: None,
|
|
||||||
nftables: None,
|
|
||||||
send_proxy_protocol: None,
|
send_proxy_protocol: None,
|
||||||
udp: None,
|
udp: None,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -60,16 +60,6 @@ pub enum RouteActionType {
|
|||||||
SocketHandler,
|
SocketHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Forwarding Engine ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Forwarding engine specification.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum ForwardingEngine {
|
|
||||||
Node,
|
|
||||||
Nftables,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Route Match ─────────────────────────────────────────────────────
|
// ─── Route Match ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Domain specification: single string or array.
|
/// Domain specification: single string or array.
|
||||||
@@ -341,38 +331,6 @@ pub struct RouteAdvanced {
|
|||||||
pub url_rewrite: Option<RouteUrlRewrite>,
|
pub url_rewrite: Option<RouteUrlRewrite>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── NFTables Options ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// NFTables protocol type.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum NfTablesProtocol {
|
|
||||||
Tcp,
|
|
||||||
Udp,
|
|
||||||
All,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NFTables-specific configuration options.
|
|
||||||
/// Matches TypeScript: `INfTablesOptions`
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct NfTablesOptions {
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub preserve_source_ip: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub protocol: Option<NfTablesProtocol>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub max_rate: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub priority: Option<i32>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub table_name: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub use_ip_sets: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub use_advanced_nat: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Backend Protocol ────────────────────────────────────────────────
|
// ─── Backend Protocol ────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Backend protocol.
|
/// Backend protocol.
|
||||||
@@ -541,14 +499,6 @@ pub struct RouteAction {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub options: Option<ActionOptions>,
|
pub options: Option<ActionOptions>,
|
||||||
|
|
||||||
/// Forwarding engine specification
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub forwarding_engine: Option<ForwardingEngine>,
|
|
||||||
|
|
||||||
/// NFTables-specific options
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub nftables: Option<NfTablesOptions>,
|
|
||||||
|
|
||||||
/// PROXY protocol support (default for all targets)
|
/// PROXY protocol support (default for all targets)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub send_proxy_protocol: Option<bool>,
|
pub send_proxy_protocol: Option<bool>,
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "rustproxy-nftables"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
authors.workspace = true
|
|
||||||
description = "NFTables kernel-level forwarding for RustProxy"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rustproxy-config = { workspace = true }
|
|
||||||
tokio = { workspace = true }
|
|
||||||
tracing = { workspace = true }
|
|
||||||
thiserror = { workspace = true }
|
|
||||||
anyhow = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
|
||||||
libc = { workspace = true }
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
//! # rustproxy-nftables
|
|
||||||
//!
|
|
||||||
//! NFTables kernel-level forwarding for RustProxy.
|
|
||||||
//! Generates and manages nft CLI rules for DNAT/SNAT.
|
|
||||||
|
|
||||||
pub mod nft_manager;
|
|
||||||
pub mod rule_builder;
|
|
||||||
|
|
||||||
pub use nft_manager::*;
|
|
||||||
pub use rule_builder::*;
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
use thiserror::Error;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use tracing::{debug, info, warn};
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum NftError {
|
|
||||||
#[error("nft command failed: {0}")]
|
|
||||||
CommandFailed(String),
|
|
||||||
#[error("IO error: {0}")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
#[error("Not running as root")]
|
|
||||||
NotRoot,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Manager for nftables rules.
|
|
||||||
///
|
|
||||||
/// Executes `nft` CLI commands to manage kernel-level packet forwarding.
|
|
||||||
/// Requires root privileges; operations are skipped gracefully if not root.
|
|
||||||
pub struct NftManager {
|
|
||||||
table_name: String,
|
|
||||||
/// Active rules indexed by route ID
|
|
||||||
active_rules: HashMap<String, Vec<String>>,
|
|
||||||
/// Whether the table has been initialized
|
|
||||||
table_initialized: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NftManager {
|
|
||||||
pub fn new(table_name: Option<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
table_name: table_name.unwrap_or_else(|| "rustproxy".to_string()),
|
|
||||||
active_rules: HashMap::new(),
|
|
||||||
table_initialized: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if we are running as root.
|
|
||||||
fn is_root() -> bool {
|
|
||||||
unsafe { libc::geteuid() == 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute a single nft command via the CLI.
|
|
||||||
async fn exec_nft(command: &str) -> Result<String, NftError> {
|
|
||||||
// The command starts with "nft ", strip it to get the args
|
|
||||||
let args = if command.starts_with("nft ") {
|
|
||||||
&command[4..]
|
|
||||||
} else {
|
|
||||||
command
|
|
||||||
};
|
|
||||||
|
|
||||||
let output = tokio::process::Command::new("nft")
|
|
||||||
.args(args.split_whitespace())
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.map_err(NftError::Io)?;
|
|
||||||
|
|
||||||
if output.status.success() {
|
|
||||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
|
||||||
} else {
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
Err(NftError::CommandFailed(format!(
|
|
||||||
"Command '{}' failed: {}",
|
|
||||||
command, stderr
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensure the nftables table and chains are set up.
|
|
||||||
async fn ensure_table(&mut self) -> Result<(), NftError> {
|
|
||||||
if self.table_initialized {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let setup_commands = crate::rule_builder::build_table_setup(&self.table_name);
|
|
||||||
for cmd in &setup_commands {
|
|
||||||
Self::exec_nft(cmd).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.table_initialized = true;
|
|
||||||
info!("NFTables table '{}' initialized", self.table_name);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply rules for a route.
|
|
||||||
///
|
|
||||||
/// Executes the nft commands via the CLI. If not running as root,
|
|
||||||
/// the rules are stored locally but not applied to the kernel.
|
|
||||||
pub async fn apply_rules(&mut self, route_id: &str, rules: Vec<String>) -> Result<(), NftError> {
|
|
||||||
if !Self::is_root() {
|
|
||||||
warn!("Not running as root, nftables rules will not be applied to kernel");
|
|
||||||
self.active_rules.insert(route_id.to_string(), rules);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.ensure_table().await?;
|
|
||||||
|
|
||||||
for cmd in &rules {
|
|
||||||
Self::exec_nft(cmd).await?;
|
|
||||||
debug!("Applied nft rule: {}", cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Applied {} nftables rules for route '{}'", rules.len(), route_id);
|
|
||||||
self.active_rules.insert(route_id.to_string(), rules);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove rules for a route.
|
|
||||||
///
|
|
||||||
/// Currently removes the route from tracking. To fully remove specific
|
|
||||||
/// rules would require handle-based tracking; for now, cleanup() removes
|
|
||||||
/// the entire table.
|
|
||||||
pub async fn remove_rules(&mut self, route_id: &str) -> Result<(), NftError> {
|
|
||||||
if let Some(rules) = self.active_rules.remove(route_id) {
|
|
||||||
info!("Removed {} tracked nft rules for route '{}'", rules.len(), route_id);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clean up all managed rules by deleting the entire nftables table.
|
|
||||||
pub async fn cleanup(&mut self) -> Result<(), NftError> {
|
|
||||||
if !Self::is_root() {
|
|
||||||
warn!("Not running as root, skipping nftables cleanup");
|
|
||||||
self.active_rules.clear();
|
|
||||||
self.table_initialized = false;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.table_initialized {
|
|
||||||
let cleanup_commands = crate::rule_builder::build_table_cleanup(&self.table_name);
|
|
||||||
for cmd in &cleanup_commands {
|
|
||||||
match Self::exec_nft(cmd).await {
|
|
||||||
Ok(_) => debug!("Cleanup: {}", cmd),
|
|
||||||
Err(e) => warn!("Cleanup command failed (may be ok): {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
info!("NFTables table '{}' cleaned up", self.table_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.active_rules.clear();
|
|
||||||
self.table_initialized = false;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the table name.
|
|
||||||
pub fn table_name(&self) -> &str {
|
|
||||||
&self.table_name
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether the table has been initialized in the kernel.
|
|
||||||
pub fn is_initialized(&self) -> bool {
|
|
||||||
self.table_initialized
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the number of active route rule sets.
|
|
||||||
pub fn active_route_count(&self) -> usize {
|
|
||||||
self.active_rules.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the status of all active rules.
|
|
||||||
pub fn status(&self) -> HashMap<String, serde_json::Value> {
|
|
||||||
let mut status = HashMap::new();
|
|
||||||
for (route_id, rules) in &self.active_rules {
|
|
||||||
status.insert(
|
|
||||||
route_id.clone(),
|
|
||||||
serde_json::json!({
|
|
||||||
"ruleCount": rules.len(),
|
|
||||||
"rules": rules,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_new_default_table_name() {
|
|
||||||
let mgr = NftManager::new(None);
|
|
||||||
assert_eq!(mgr.table_name(), "rustproxy");
|
|
||||||
assert!(!mgr.is_initialized());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_new_custom_table_name() {
|
|
||||||
let mgr = NftManager::new(Some("custom".to_string()));
|
|
||||||
assert_eq!(mgr.table_name(), "custom");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_apply_rules_non_root() {
|
|
||||||
let mut mgr = NftManager::new(None);
|
|
||||||
// When not root, rules are stored but not applied to kernel
|
|
||||||
let rules = vec!["nft add rule ip rustproxy prerouting tcp dport 443 dnat to 10.0.0.1:8443".to_string()];
|
|
||||||
mgr.apply_rules("route-1", rules).await.unwrap();
|
|
||||||
assert_eq!(mgr.active_route_count(), 1);
|
|
||||||
|
|
||||||
let status = mgr.status();
|
|
||||||
assert!(status.contains_key("route-1"));
|
|
||||||
assert_eq!(status["route-1"]["ruleCount"], 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_remove_rules() {
|
|
||||||
let mut mgr = NftManager::new(None);
|
|
||||||
let rules = vec!["nft add rule test".to_string()];
|
|
||||||
mgr.apply_rules("route-1", rules).await.unwrap();
|
|
||||||
assert_eq!(mgr.active_route_count(), 1);
|
|
||||||
|
|
||||||
mgr.remove_rules("route-1").await.unwrap();
|
|
||||||
assert_eq!(mgr.active_route_count(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_cleanup_non_root() {
|
|
||||||
let mut mgr = NftManager::new(None);
|
|
||||||
let rules = vec!["nft add rule test".to_string()];
|
|
||||||
mgr.apply_rules("route-1", rules).await.unwrap();
|
|
||||||
mgr.apply_rules("route-2", vec!["nft add rule test2".to_string()]).await.unwrap();
|
|
||||||
|
|
||||||
mgr.cleanup().await.unwrap();
|
|
||||||
assert_eq!(mgr.active_route_count(), 0);
|
|
||||||
assert!(!mgr.is_initialized());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_status_multiple_routes() {
|
|
||||||
let mut mgr = NftManager::new(None);
|
|
||||||
mgr.apply_rules("web", vec!["rule1".to_string(), "rule2".to_string()]).await.unwrap();
|
|
||||||
mgr.apply_rules("api", vec!["rule3".to_string()]).await.unwrap();
|
|
||||||
|
|
||||||
let status = mgr.status();
|
|
||||||
assert_eq!(status.len(), 2);
|
|
||||||
assert_eq!(status["web"]["ruleCount"], 2);
|
|
||||||
assert_eq!(status["api"]["ruleCount"], 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
use rustproxy_config::{NfTablesOptions, NfTablesProtocol};
|
|
||||||
|
|
||||||
/// Build nftables DNAT rule for port forwarding.
|
|
||||||
pub fn build_dnat_rule(
|
|
||||||
table_name: &str,
|
|
||||||
chain_name: &str,
|
|
||||||
source_port: u16,
|
|
||||||
target_host: &str,
|
|
||||||
target_port: u16,
|
|
||||||
options: &NfTablesOptions,
|
|
||||||
) -> Vec<String> {
|
|
||||||
let protocols: Vec<&str> = match options.protocol.as_ref().unwrap_or(&NfTablesProtocol::Tcp) {
|
|
||||||
NfTablesProtocol::Tcp => vec!["tcp"],
|
|
||||||
NfTablesProtocol::Udp => vec!["udp"],
|
|
||||||
NfTablesProtocol::All => vec!["tcp", "udp"],
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut rules = Vec::new();
|
|
||||||
|
|
||||||
for protocol in &protocols {
|
|
||||||
// DNAT rule
|
|
||||||
rules.push(format!(
|
|
||||||
"nft add rule ip {} {} {} dport {} dnat to {}:{}",
|
|
||||||
table_name, chain_name, protocol, source_port, target_host, target_port,
|
|
||||||
));
|
|
||||||
|
|
||||||
// SNAT rule if preserving source IP is not enabled
|
|
||||||
if !options.preserve_source_ip.unwrap_or(false) {
|
|
||||||
rules.push(format!(
|
|
||||||
"nft add rule ip {} postrouting {} dport {} masquerade",
|
|
||||||
table_name, protocol, target_port,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rate limiting
|
|
||||||
if let Some(max_rate) = &options.max_rate {
|
|
||||||
rules.push(format!(
|
|
||||||
"nft add rule ip {} {} {} dport {} limit rate {} accept",
|
|
||||||
table_name, chain_name, protocol, source_port, max_rate,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rules
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build the initial table and chain setup commands.
|
|
||||||
pub fn build_table_setup(table_name: &str) -> Vec<String> {
|
|
||||||
vec![
|
|
||||||
format!("nft add table ip {}", table_name),
|
|
||||||
format!("nft add chain ip {} prerouting {{ type nat hook prerouting priority 0 \\; }}", table_name),
|
|
||||||
format!("nft add chain ip {} postrouting {{ type nat hook postrouting priority 100 \\; }}", table_name),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build cleanup commands to remove the table.
|
|
||||||
pub fn build_table_cleanup(table_name: &str) -> Vec<String> {
|
|
||||||
vec![format!("nft delete table ip {}", table_name)]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn make_options() -> NfTablesOptions {
|
|
||||||
NfTablesOptions {
|
|
||||||
preserve_source_ip: None,
|
|
||||||
protocol: None,
|
|
||||||
max_rate: None,
|
|
||||||
priority: None,
|
|
||||||
table_name: None,
|
|
||||||
use_ip_sets: None,
|
|
||||||
use_advanced_nat: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_basic_dnat_rule() {
|
|
||||||
let options = make_options();
|
|
||||||
let rules = build_dnat_rule("rustproxy", "prerouting", 443, "10.0.0.1", 8443, &options);
|
|
||||||
assert!(rules.len() >= 1);
|
|
||||||
assert!(rules[0].contains("dnat to 10.0.0.1:8443"));
|
|
||||||
assert!(rules[0].contains("dport 443"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_preserve_source_ip() {
|
|
||||||
let mut options = make_options();
|
|
||||||
options.preserve_source_ip = Some(true);
|
|
||||||
let rules = build_dnat_rule("rustproxy", "prerouting", 443, "10.0.0.1", 8443, &options);
|
|
||||||
// When preserving source IP, no masquerade rule
|
|
||||||
assert!(rules.iter().all(|r| !r.contains("masquerade")));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_without_preserve_source_ip() {
|
|
||||||
let options = make_options();
|
|
||||||
let rules = build_dnat_rule("rustproxy", "prerouting", 443, "10.0.0.1", 8443, &options);
|
|
||||||
assert!(rules.iter().any(|r| r.contains("masquerade")));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_rate_limited_rule() {
|
|
||||||
let mut options = make_options();
|
|
||||||
options.max_rate = Some("100/second".to_string());
|
|
||||||
let rules = build_dnat_rule("rustproxy", "prerouting", 80, "10.0.0.1", 8080, &options);
|
|
||||||
assert!(rules.iter().any(|r| r.contains("limit rate 100/second")));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_table_setup_commands() {
|
|
||||||
let commands = build_table_setup("rustproxy");
|
|
||||||
assert_eq!(commands.len(), 3);
|
|
||||||
assert!(commands[0].contains("add table ip rustproxy"));
|
|
||||||
assert!(commands[1].contains("prerouting"));
|
|
||||||
assert!(commands[2].contains("postrouting"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_table_cleanup() {
|
|
||||||
let commands = build_table_cleanup("rustproxy");
|
|
||||||
assert_eq!(commands.len(), 1);
|
|
||||||
assert!(commands[0].contains("delete table ip rustproxy"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_protocol_all_generates_tcp_and_udp_rules() {
|
|
||||||
let mut options = make_options();
|
|
||||||
options.protocol = Some(NfTablesProtocol::All);
|
|
||||||
let rules = build_dnat_rule("rustproxy", "prerouting", 53, "10.0.0.53", 53, &options);
|
|
||||||
// Should have TCP DNAT + masquerade + UDP DNAT + masquerade = 4 rules
|
|
||||||
assert_eq!(rules.len(), 4);
|
|
||||||
assert!(rules.iter().any(|r| r.contains("tcp dport 53 dnat")));
|
|
||||||
assert!(rules.iter().any(|r| r.contains("udp dport 53 dnat")));
|
|
||||||
assert!(rules.iter().filter(|r| r.contains("masquerade")).count() == 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_protocol_udp() {
|
|
||||||
let mut options = make_options();
|
|
||||||
options.protocol = Some(NfTablesProtocol::Udp);
|
|
||||||
let rules = build_dnat_rule("rustproxy", "prerouting", 53, "10.0.0.53", 53, &options);
|
|
||||||
assert!(rules.iter().all(|r| !r.contains("tcp")));
|
|
||||||
assert!(rules.iter().any(|r| r.contains("udp dport 53 dnat")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -355,8 +355,6 @@ mod tests {
|
|||||||
load_balancing: None,
|
load_balancing: None,
|
||||||
advanced: None,
|
advanced: None,
|
||||||
options: None,
|
options: None,
|
||||||
forwarding_engine: None,
|
|
||||||
nftables: None,
|
|
||||||
send_proxy_protocol: None,
|
send_proxy_protocol: None,
|
||||||
udp: None,
|
udp: None,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ rustproxy-routing = { workspace = true }
|
|||||||
rustproxy-tls = { workspace = true }
|
rustproxy-tls = { workspace = true }
|
||||||
rustproxy-passthrough = { workspace = true }
|
rustproxy-passthrough = { workspace = true }
|
||||||
rustproxy-http = { workspace = true }
|
rustproxy-http = { workspace = true }
|
||||||
rustproxy-nftables = { workspace = true }
|
|
||||||
rustproxy-metrics = { workspace = true }
|
rustproxy-metrics = { workspace = true }
|
||||||
rustproxy-security = { workspace = true }
|
rustproxy-security = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|||||||
@@ -41,16 +41,14 @@ pub use rustproxy_routing;
|
|||||||
pub use rustproxy_passthrough;
|
pub use rustproxy_passthrough;
|
||||||
pub use rustproxy_tls;
|
pub use rustproxy_tls;
|
||||||
pub use rustproxy_http;
|
pub use rustproxy_http;
|
||||||
pub use rustproxy_nftables;
|
|
||||||
pub use rustproxy_metrics;
|
pub use rustproxy_metrics;
|
||||||
pub use rustproxy_security;
|
pub use rustproxy_security;
|
||||||
|
|
||||||
use rustproxy_config::{RouteConfig, RustProxyOptions, TlsMode, CertificateSpec, ForwardingEngine};
|
use rustproxy_config::{RouteConfig, RustProxyOptions, TlsMode, CertificateSpec};
|
||||||
use rustproxy_routing::RouteManager;
|
use rustproxy_routing::RouteManager;
|
||||||
use rustproxy_passthrough::{TcpListenerManager, UdpListenerManager, TlsCertConfig, ConnectionConfig};
|
use rustproxy_passthrough::{TcpListenerManager, UdpListenerManager, TlsCertConfig, ConnectionConfig};
|
||||||
use rustproxy_metrics::{MetricsCollector, Metrics, Statistics};
|
use rustproxy_metrics::{MetricsCollector, Metrics, Statistics};
|
||||||
use rustproxy_tls::{CertManager, CertStore, CertBundle, CertMetadata, CertSource};
|
use rustproxy_tls::{CertManager, CertStore, CertBundle, CertMetadata, CertSource};
|
||||||
use rustproxy_nftables::{NftManager, rule_builder};
|
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
/// Certificate status.
|
/// Certificate status.
|
||||||
@@ -74,7 +72,6 @@ pub struct RustProxy {
|
|||||||
challenge_server: Option<challenge_server::ChallengeServer>,
|
challenge_server: Option<challenge_server::ChallengeServer>,
|
||||||
renewal_handle: Option<tokio::task::JoinHandle<()>>,
|
renewal_handle: Option<tokio::task::JoinHandle<()>>,
|
||||||
sampling_handle: Option<tokio::task::JoinHandle<()>>,
|
sampling_handle: Option<tokio::task::JoinHandle<()>>,
|
||||||
nft_manager: Option<NftManager>,
|
|
||||||
started: bool,
|
started: bool,
|
||||||
started_at: Option<Instant>,
|
started_at: Option<Instant>,
|
||||||
/// Shared path to a Unix domain socket for relaying socket-handler connections back to TypeScript.
|
/// Shared path to a Unix domain socket for relaying socket-handler connections back to TypeScript.
|
||||||
@@ -121,7 +118,6 @@ impl RustProxy {
|
|||||||
challenge_server: None,
|
challenge_server: None,
|
||||||
renewal_handle: None,
|
renewal_handle: None,
|
||||||
sampling_handle: None,
|
sampling_handle: None,
|
||||||
nft_manager: None,
|
|
||||||
started: false,
|
started: false,
|
||||||
started_at: None,
|
started_at: None,
|
||||||
socket_handler_relay: Arc::new(std::sync::RwLock::new(None)),
|
socket_handler_relay: Arc::new(std::sync::RwLock::new(None)),
|
||||||
@@ -387,9 +383,6 @@ impl RustProxy {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Apply NFTables rules for routes using nftables forwarding engine
|
|
||||||
self.apply_nftables_rules(&self.options.routes.clone()).await;
|
|
||||||
|
|
||||||
// Start renewal timer if ACME is enabled
|
// Start renewal timer if ACME is enabled
|
||||||
self.start_renewal_timer();
|
self.start_renewal_timer();
|
||||||
|
|
||||||
@@ -616,14 +609,6 @@ impl RustProxy {
|
|||||||
}
|
}
|
||||||
self.challenge_server = None;
|
self.challenge_server = None;
|
||||||
|
|
||||||
// Clean up NFTables rules
|
|
||||||
if let Some(ref mut nft) = self.nft_manager {
|
|
||||||
if let Err(e) = nft.cleanup().await {
|
|
||||||
warn!("NFTables cleanup failed: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.nft_manager = None;
|
|
||||||
|
|
||||||
if let Some(ref mut listener) = self.listener_manager {
|
if let Some(ref mut listener) = self.listener_manager {
|
||||||
listener.graceful_stop().await;
|
listener.graceful_stop().await;
|
||||||
}
|
}
|
||||||
@@ -825,9 +810,6 @@ impl RustProxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update NFTables rules: remove old, apply new
|
|
||||||
self.update_nftables_rules(&routes).await;
|
|
||||||
|
|
||||||
self.options.routes = routes;
|
self.options.routes = routes;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1149,99 +1131,6 @@ impl RustProxy {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get NFTables status.
|
|
||||||
pub async fn get_nftables_status(&self) -> Result<HashMap<String, serde_json::Value>> {
|
|
||||||
match &self.nft_manager {
|
|
||||||
Some(nft) => Ok(nft.status()),
|
|
||||||
None => Ok(HashMap::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply NFTables rules for routes using the nftables forwarding engine.
|
|
||||||
async fn apply_nftables_rules(&mut self, routes: &[RouteConfig]) {
|
|
||||||
let nft_routes: Vec<&RouteConfig> = routes.iter()
|
|
||||||
.filter(|r| r.action.forwarding_engine.as_ref() == Some(&ForwardingEngine::Nftables))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if nft_routes.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Applying NFTables rules for {} routes", nft_routes.len());
|
|
||||||
|
|
||||||
let table_name = nft_routes.iter()
|
|
||||||
.find_map(|r| r.action.nftables.as_ref()?.table_name.clone())
|
|
||||||
.unwrap_or_else(|| "rustproxy".to_string());
|
|
||||||
|
|
||||||
let mut nft = NftManager::new(Some(table_name));
|
|
||||||
|
|
||||||
for route in &nft_routes {
|
|
||||||
let route_id = route.id.as_deref()
|
|
||||||
.or(route.name.as_deref())
|
|
||||||
.unwrap_or("unnamed");
|
|
||||||
|
|
||||||
let nft_options = match &route.action.nftables {
|
|
||||||
Some(opts) => opts.clone(),
|
|
||||||
None => rustproxy_config::NfTablesOptions {
|
|
||||||
preserve_source_ip: None,
|
|
||||||
protocol: None,
|
|
||||||
max_rate: None,
|
|
||||||
priority: None,
|
|
||||||
table_name: None,
|
|
||||||
use_ip_sets: None,
|
|
||||||
use_advanced_nat: None,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let targets = match &route.action.targets {
|
|
||||||
Some(targets) => targets,
|
|
||||||
None => {
|
|
||||||
warn!("NFTables route '{}' has no targets, skipping", route_id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let source_ports = route.route_match.ports.to_ports();
|
|
||||||
for target in targets {
|
|
||||||
let target_host = target.host.first().to_string();
|
|
||||||
let target_port_spec = &target.port;
|
|
||||||
|
|
||||||
for &source_port in &source_ports {
|
|
||||||
let resolved_port = target_port_spec.resolve(source_port);
|
|
||||||
let rules = rule_builder::build_dnat_rule(
|
|
||||||
nft.table_name(),
|
|
||||||
"prerouting",
|
|
||||||
source_port,
|
|
||||||
&target_host,
|
|
||||||
resolved_port,
|
|
||||||
&nft_options,
|
|
||||||
);
|
|
||||||
|
|
||||||
let rule_id = format!("{}-{}-{}", route_id, source_port, resolved_port);
|
|
||||||
if let Err(e) = nft.apply_rules(&rule_id, rules).await {
|
|
||||||
error!("Failed to apply NFTables rules for route '{}': {}", route_id, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.nft_manager = Some(nft);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update NFTables rules when routes change.
|
|
||||||
async fn update_nftables_rules(&mut self, new_routes: &[RouteConfig]) {
|
|
||||||
// Clean up old rules
|
|
||||||
if let Some(ref mut nft) = self.nft_manager {
|
|
||||||
if let Err(e) = nft.cleanup().await {
|
|
||||||
warn!("NFTables cleanup during update failed: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.nft_manager = None;
|
|
||||||
|
|
||||||
// Apply new rules
|
|
||||||
self.apply_nftables_rules(new_routes).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract TLS configurations from route configs.
|
/// Extract TLS configurations from route configs.
|
||||||
fn extract_tls_configs(routes: &[RouteConfig]) -> HashMap<String, TlsCertConfig> {
|
fn extract_tls_configs(routes: &[RouteConfig]) -> HashMap<String, TlsCertConfig> {
|
||||||
let mut configs = HashMap::new();
|
let mut configs = HashMap::new();
|
||||||
|
|||||||
@@ -147,7 +147,6 @@ async fn handle_request(
|
|||||||
"renewCertificate" => handle_renew_certificate(&id, &request.params, proxy).await,
|
"renewCertificate" => handle_renew_certificate(&id, &request.params, proxy).await,
|
||||||
"getCertificateStatus" => handle_get_certificate_status(&id, &request.params, proxy).await,
|
"getCertificateStatus" => handle_get_certificate_status(&id, &request.params, proxy).await,
|
||||||
"getListeningPorts" => handle_get_listening_ports(&id, proxy),
|
"getListeningPorts" => handle_get_listening_ports(&id, proxy),
|
||||||
"getNftablesStatus" => handle_get_nftables_status(&id, proxy).await,
|
|
||||||
"setSocketHandlerRelay" => handle_set_socket_handler_relay(&id, &request.params, proxy).await,
|
"setSocketHandlerRelay" => handle_set_socket_handler_relay(&id, &request.params, proxy).await,
|
||||||
"setDatagramHandlerRelay" => handle_set_datagram_handler_relay(&id, &request.params, proxy).await,
|
"setDatagramHandlerRelay" => handle_set_datagram_handler_relay(&id, &request.params, proxy).await,
|
||||||
"addListeningPort" => handle_add_listening_port(&id, &request.params, proxy).await,
|
"addListeningPort" => handle_add_listening_port(&id, &request.params, proxy).await,
|
||||||
@@ -352,26 +351,6 @@ fn handle_get_listening_ports(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_get_nftables_status(
|
|
||||||
id: &str,
|
|
||||||
proxy: &Option<RustProxy>,
|
|
||||||
) -> ManagementResponse {
|
|
||||||
match proxy.as_ref() {
|
|
||||||
Some(p) => {
|
|
||||||
match p.get_nftables_status().await {
|
|
||||||
Ok(status) => {
|
|
||||||
match serde_json::to_value(&status) {
|
|
||||||
Ok(v) => ManagementResponse::ok(id.to_string(), v),
|
|
||||||
Err(e) => ManagementResponse::err(id.to_string(), format!("Failed to serialize: {}", e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => ManagementResponse::err(id.to_string(), format!("Failed to get status: {}", e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => ManagementResponse::ok(id.to_string(), serde_json::json!({})),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_set_socket_handler_relay(
|
async fn handle_set_socket_handler_relay(
|
||||||
id: &str,
|
id: &str,
|
||||||
params: &serde_json::Value,
|
params: &serde_json::Value,
|
||||||
|
|||||||
@@ -297,8 +297,6 @@ pub fn make_test_route(
|
|||||||
load_balancing: None,
|
load_balancing: None,
|
||||||
advanced: None,
|
advanced: None,
|
||||||
options: None,
|
options: None,
|
||||||
forwarding_engine: None,
|
|
||||||
nftables: None,
|
|
||||||
send_proxy_protocol: None,
|
send_proxy_protocol: None,
|
||||||
udp: None,
|
udp: None,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '26.2.4',
|
version: '26.3.0',
|
||||||
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,14 @@ export { tsclass };
|
|||||||
import * as smartcrypto from '@push.rocks/smartcrypto';
|
import * as smartcrypto from '@push.rocks/smartcrypto';
|
||||||
import * as smartlog from '@push.rocks/smartlog';
|
import * as smartlog from '@push.rocks/smartlog';
|
||||||
import * as smartlogDestinationLocal from '@push.rocks/smartlog/destination-local';
|
import * as smartlogDestinationLocal from '@push.rocks/smartlog/destination-local';
|
||||||
|
import * as smartnftables from '@push.rocks/smartnftables';
|
||||||
import * as smartrust from '@push.rocks/smartrust';
|
import * as smartrust from '@push.rocks/smartrust';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
smartcrypto,
|
smartcrypto,
|
||||||
smartlog,
|
smartlog,
|
||||||
smartlogDestinationLocal,
|
smartlogDestinationLocal,
|
||||||
|
smartnftables,
|
||||||
smartrust,
|
smartrust,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ type TSmartProxyCommands = {
|
|||||||
renewCertificate: { params: { routeName: string }; result: void };
|
renewCertificate: { params: { routeName: string }; result: void };
|
||||||
getCertificateStatus: { params: { routeName: string }; result: any };
|
getCertificateStatus: { params: { routeName: string }; result: any };
|
||||||
getListeningPorts: { params: Record<string, never>; result: { ports: number[] } };
|
getListeningPorts: { params: Record<string, never>; result: { ports: number[] } };
|
||||||
getNftablesStatus: { params: Record<string, never>; result: any };
|
|
||||||
setSocketHandlerRelay: { params: { socketPath: string }; result: void };
|
setSocketHandlerRelay: { params: { socketPath: string }; result: void };
|
||||||
addListeningPort: { params: { port: number }; result: void };
|
addListeningPort: { params: { port: number }; result: void };
|
||||||
removeListeningPort: { params: { port: number }; result: void };
|
removeListeningPort: { params: { port: number }; result: void };
|
||||||
@@ -159,10 +158,6 @@ export class RustProxyBridge extends plugins.EventEmitter {
|
|||||||
return result?.ports ?? [];
|
return result?.ports ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getNftablesStatus(): Promise<any> {
|
|
||||||
return this.bridge.sendCommand('getNftablesStatus', {} as Record<string, never>);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setSocketHandlerRelay(socketPath: string): Promise<void> {
|
public async setSocketHandlerRelay(socketPath: string): Promise<void> {
|
||||||
await this.bridge.sendCommand('setSocketHandlerRelay', { socketPath });
|
await this.bridge.sendCommand('setSocketHandlerRelay', { socketPath });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ import type { IMetrics } from './models/metrics-types.js';
|
|||||||
/**
|
/**
|
||||||
* SmartProxy - Rust-backed proxy engine with TypeScript configuration API.
|
* SmartProxy - Rust-backed proxy engine with TypeScript configuration API.
|
||||||
*
|
*
|
||||||
* All networking (TCP, TLS, HTTP reverse proxy, connection management, security,
|
* All networking (TCP, TLS, HTTP reverse proxy, connection management, security)
|
||||||
* NFTables) is handled by the Rust binary. TypeScript is only:
|
* is handled by the Rust binary. TypeScript is only:
|
||||||
* - The npm module interface (types, route helpers)
|
* - The npm module interface (types, route helpers)
|
||||||
* - The thin IPC wrapper (this class)
|
* - The thin IPC wrapper (this class)
|
||||||
* - Socket-handler callback relay (for JS-defined handlers)
|
* - Socket-handler callback relay (for JS-defined handlers)
|
||||||
@@ -39,6 +39,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
private socketHandlerServer: SocketHandlerServer | null = null;
|
private socketHandlerServer: SocketHandlerServer | null = null;
|
||||||
private datagramHandlerServer: DatagramHandlerServer | null = null;
|
private datagramHandlerServer: DatagramHandlerServer | null = null;
|
||||||
private metricsAdapter: RustMetricsAdapter;
|
private metricsAdapter: RustMetricsAdapter;
|
||||||
|
private nftablesManager: InstanceType<typeof plugins.smartnftables.SmartNftables> | null = null;
|
||||||
private routeUpdateLock: Mutex;
|
private routeUpdateLock: Mutex;
|
||||||
private stopping = false;
|
private stopping = false;
|
||||||
private certProvisionPromise: Promise<void> | null = null;
|
private certProvisionPromise: Promise<void> | null = null;
|
||||||
@@ -211,6 +212,9 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply NFTables rules for routes using nftables forwarding engine
|
||||||
|
await this.applyNftablesRules(this.settings.routes);
|
||||||
|
|
||||||
// Start metrics polling BEFORE cert provisioning — the Rust engine is already
|
// Start metrics polling BEFORE cert provisioning — the Rust engine is already
|
||||||
// running and accepting connections, so metrics should be available immediately.
|
// running and accepting connections, so metrics should be available immediately.
|
||||||
// Cert provisioning can hang indefinitely (e.g. DNS-01 ACME timeouts) and must
|
// Cert provisioning can hang indefinitely (e.g. DNS-01 ACME timeouts) and must
|
||||||
@@ -238,6 +242,12 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
this.certProvisionPromise = null;
|
this.certProvisionPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up NFTables rules
|
||||||
|
if (this.nftablesManager) {
|
||||||
|
await this.nftablesManager.cleanup();
|
||||||
|
this.nftablesManager = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Stop metrics polling
|
// Stop metrics polling
|
||||||
this.metricsAdapter.stopPolling();
|
this.metricsAdapter.stopPolling();
|
||||||
|
|
||||||
@@ -319,6 +329,13 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
this.datagramHandlerServer = null;
|
this.datagramHandlerServer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update NFTables rules
|
||||||
|
if (this.nftablesManager) {
|
||||||
|
await this.nftablesManager.cleanup();
|
||||||
|
this.nftablesManager = null;
|
||||||
|
}
|
||||||
|
await this.applyNftablesRules(newRoutes);
|
||||||
|
|
||||||
// Update stored routes
|
// Update stored routes
|
||||||
this.settings.routes = newRoutes;
|
this.settings.routes = newRoutes;
|
||||||
|
|
||||||
@@ -411,14 +428,59 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get NFTables status (async - calls Rust).
|
* Get NFTables status.
|
||||||
*/
|
*/
|
||||||
public async getNfTablesStatus(): Promise<Record<string, any>> {
|
public getNfTablesStatus(): plugins.smartnftables.INftStatus | null {
|
||||||
return this.bridge.getNftablesStatus();
|
return this.nftablesManager?.status() ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Private helpers ---
|
// --- Private helpers ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply NFTables rules for routes using the nftables forwarding engine.
|
||||||
|
*/
|
||||||
|
private async applyNftablesRules(routes: IRouteConfig[]): Promise<void> {
|
||||||
|
const nftRoutes = routes.filter(r => r.action.forwardingEngine === 'nftables');
|
||||||
|
if (nftRoutes.length === 0) return;
|
||||||
|
|
||||||
|
const tableName = nftRoutes.find(r => r.action.nftables?.tableName)?.action.nftables?.tableName ?? 'smartproxy';
|
||||||
|
const nft = new plugins.smartnftables.SmartNftables({ tableName });
|
||||||
|
await nft.initialize();
|
||||||
|
|
||||||
|
for (const route of nftRoutes) {
|
||||||
|
const routeId = route.name || 'unnamed';
|
||||||
|
const targets = route.action.targets;
|
||||||
|
if (!targets) continue;
|
||||||
|
|
||||||
|
const nftOpts = route.action.nftables;
|
||||||
|
const protocol: plugins.smartnftables.TNftProtocol = (nftOpts?.protocol as any) ?? 'tcp';
|
||||||
|
const preserveSourceIP = nftOpts?.preserveSourceIP ?? false;
|
||||||
|
|
||||||
|
const ports = Array.isArray(route.match.ports)
|
||||||
|
? route.match.ports.flatMap(p => typeof p === 'number' ? [p] : [])
|
||||||
|
: typeof route.match.ports === 'number' ? [route.match.ports] : [];
|
||||||
|
|
||||||
|
for (const target of targets) {
|
||||||
|
const targetHost = Array.isArray(target.host) ? target.host[0] : target.host;
|
||||||
|
if (typeof targetHost !== 'string') continue;
|
||||||
|
|
||||||
|
for (const sourcePort of ports) {
|
||||||
|
const targetPort = typeof target.port === 'number' ? target.port : sourcePort;
|
||||||
|
await nft.nat.addPortForwarding(`${routeId}-${sourcePort}-${targetPort}`, {
|
||||||
|
sourcePort,
|
||||||
|
targetHost,
|
||||||
|
targetPort,
|
||||||
|
protocol,
|
||||||
|
preserveSourceIP,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nftablesManager = nft;
|
||||||
|
logger.log('info', `Applied NFTables rules for ${nftRoutes.length} route(s)`, { component: 'smart-proxy' });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the Rust configuration object from TS settings.
|
* Build the Rust configuration object from TS settings.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user