fix(websocket): keep upgraded WebSocket tunnels on dedicated lifecycle timeouts

This commit is contained in:
2026-05-29 05:27:42 +00:00
parent 72be691668
commit d02b5a45d1
5 changed files with 127 additions and 150 deletions
+7
View File
@@ -2,6 +2,13 @@
## Pending ## Pending
### Fixes
- keep upgraded WebSocket tunnels on dedicated WebSocket lifecycle timeouts instead of the HTTP socket timeout (rustproxy)
- keep upgraded WebSocket tunnels on dedicated lifecycle timeouts (websocket)
- Track active upgraded tunnels so HTTP idle and max-lifetime watchdogs do not terminate WebSocket connections
- Use dedicated default WebSocket inactivity and max-lifetime timeouts in rustproxy passthrough listeners
- Add end-to-end coverage for idle WebSockets surviving short HTTP socket timeouts
## 2026-05-24 - 27.11.0 ## 2026-05-24 - 27.11.0
+20 -137
View File
@@ -1008,9 +1008,6 @@ packages:
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
'@push.rocks/consolecolor@2.0.3':
resolution: {integrity: sha512-hA+m0BMqEwZNSAS7c2aQFfoPkpX/dNdsHzkdLdeERUOy7BLacb9ItTUofGtjtginP0yDj4NSpqSjNYyX3Y8Y/w==}
'@push.rocks/consolecolor@2.0.4': '@push.rocks/consolecolor@2.0.4':
resolution: {integrity: sha512-rQJfuSJLzm117PBpsfyemX8Q/rpKh8ZVc2AqDVu6RXJMJkmGkKsADe0/rnttuHZYss8IP7yJIN9E6Vnx+jyy0A==} resolution: {integrity: sha512-rQJfuSJLzm117PBpsfyemX8Q/rpKh8ZVc2AqDVu6RXJMJkmGkKsADe0/rnttuHZYss8IP7yJIN9E6Vnx+jyy0A==}
@@ -1023,9 +1020,6 @@ packages:
'@push.rocks/levelcache@3.2.2': '@push.rocks/levelcache@3.2.2':
resolution: {integrity: sha512-g44xp3XmtSPlcTHQ8qoaNV0AK7w4cuLd6h7sGXXxldN3NLgjOUpUqnnyDBU9i5hpIIxqssxe8WRQz10bi9W+tA==} resolution: {integrity: sha512-g44xp3XmtSPlcTHQ8qoaNV0AK7w4cuLd6h7sGXXxldN3NLgjOUpUqnnyDBU9i5hpIIxqssxe8WRQz10bi9W+tA==}
'@push.rocks/lik@6.3.1':
resolution: {integrity: sha512-UWDwGBaVx5yPtAFXqDDBtQZCzETUOA/7myQIXb+YBsuiIw4yQuhNZ23uY2ChQH2Zn6DLqdNSgQcYC0WywMZBNQ==}
'@push.rocks/lik@6.4.1': '@push.rocks/lik@6.4.1':
resolution: {integrity: sha512-W5M2zoJWUxYnCVqUB7jaxMB4W1kfhs1P6SXvWGqwDpJAjMjCnZeAXD+w0akECgSBY1zCCT2qMj7YK4Gza0t25g==} resolution: {integrity: sha512-W5M2zoJWUxYnCVqUB7jaxMB4W1kfhs1P6SXvWGqwDpJAjMjCnZeAXD+w0akECgSBY1zCCT2qMj7YK4Gza0t25g==}
@@ -1062,9 +1056,6 @@ packages:
'@push.rocks/smartdata@7.1.7': '@push.rocks/smartdata@7.1.7':
resolution: {integrity: sha512-HDI/Q9dKybfsJ68oCzlE+S63Xpij9qXnMfi28yznKP0Li1ECVZZMDDGIW5IjsXlHjO+Q+RJMcVd72Pjt3QLY5Q==} resolution: {integrity: sha512-HDI/Q9dKybfsJ68oCzlE+S63Xpij9qXnMfi28yznKP0Li1ECVZZMDDGIW5IjsXlHjO+Q+RJMcVd72Pjt3QLY5Q==}
'@push.rocks/smartdelay@3.0.5':
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
'@push.rocks/smartdelay@3.1.0': '@push.rocks/smartdelay@3.1.0':
resolution: {integrity: sha512-59xveBMbWmbFhh/rqhQnYG/klg/VONG9hV8+RQ7ftqsNRkcmUT+VM5etAbODgAUvsF4lxK+xVR0tbZOo0kGhRQ==} resolution: {integrity: sha512-59xveBMbWmbFhh/rqhQnYG/klg/VONG9hV8+RQ7ftqsNRkcmUT+VM5etAbODgAUvsF4lxK+xVR0tbZOo0kGhRQ==}
@@ -1074,9 +1065,6 @@ packages:
'@push.rocks/smartenv@5.0.13': '@push.rocks/smartenv@5.0.13':
resolution: {integrity: sha512-ACXmUcHZHl2CF2jnVuRw9saRRrZvJblCRs2d+K5aLR1DfkYFX3eA21kcMlKeLisI3aGNbIj9vz/rowN5qkRkfA==} resolution: {integrity: sha512-ACXmUcHZHl2CF2jnVuRw9saRRrZvJblCRs2d+K5aLR1DfkYFX3eA21kcMlKeLisI3aGNbIj9vz/rowN5qkRkfA==}
'@push.rocks/smartenv@6.0.0':
resolution: {integrity: sha512-ktW5MqOFs0492sB4vrvl4lgRFQ/sQ4AyREgB+sCIzGqszHWGVvGXR95Y2a3z66jkLPYML2CUWHzmMlfv8fkG+A==}
'@push.rocks/smartenv@6.1.0': '@push.rocks/smartenv@6.1.0':
resolution: {integrity: sha512-pKm5knYEkcHHc9XaYJ41Ya8/WfZB6fy1ZDB+TSLC85lvMrrRFLSsujjDehdDXl/mJr3MqecauTh2QzQIszTrjQ==} resolution: {integrity: sha512-pKm5knYEkcHHc9XaYJ41Ya8/WfZB6fy1ZDB+TSLC85lvMrrRFLSsujjDehdDXl/mJr3MqecauTh2QzQIszTrjQ==}
@@ -1098,9 +1086,6 @@ packages:
'@push.rocks/smartguard@3.1.0': '@push.rocks/smartguard@3.1.0':
resolution: {integrity: sha512-J23q84f1O+TwFGmd4lrO9XLHUh2DaLXo9PN/9VmTWYzTkQDv5JehmifXVI0esophXcCIfbdIu6hbt7/aHlDF4A==} resolution: {integrity: sha512-J23q84f1O+TwFGmd4lrO9XLHUh2DaLXo9PN/9VmTWYzTkQDv5JehmifXVI0esophXcCIfbdIu6hbt7/aHlDF4A==}
'@push.rocks/smarthash@3.2.6':
resolution: {integrity: sha512-Mq/WNX0Tjjes3X1gHd/ZBwOOKSrAG/Z3Xoc0OcCm3P20WKpniihkMpsnlE7wGjvpHLi/ZRe/XkB3KC3d5r9X4g==}
'@push.rocks/smarthash@3.2.7': '@push.rocks/smarthash@3.2.7':
resolution: {integrity: sha512-y6iyu9l8Hslsa8W4e8UktX5d0yFZqipNgxxIik6NT0yHUM1zagx2cjemUtdV49uq1u+086Wr7nvrzLROWDzReA==} resolution: {integrity: sha512-y6iyu9l8Hslsa8W4e8UktX5d0yFZqipNgxxIik6NT0yHUM1zagx2cjemUtdV49uq1u+086Wr7nvrzLROWDzReA==}
@@ -1110,9 +1095,6 @@ packages:
'@push.rocks/smartjimp@1.2.1': '@push.rocks/smartjimp@1.2.1':
resolution: {integrity: sha512-tIVS2sEqBjZTPX5U7a+dDBSZ+kfz7CdQwkEIhW6DEl6cuJ9uz2eH+pnPY0oZhw4g3q8hyW9Lf6lb8+nMmTyudw==} resolution: {integrity: sha512-tIVS2sEqBjZTPX5U7a+dDBSZ+kfz7CdQwkEIhW6DEl6cuJ9uz2eH+pnPY0oZhw4g3q8hyW9Lf6lb8+nMmTyudw==}
'@push.rocks/smartjson@5.2.0':
resolution: {integrity: sha512-710e8UwovRfPgUtaBHcd6unaODUjV5fjxtGcGCqtaTcmvOV6VpasdVfT66xMDzQmWH2E9ZfHDJeso9HdDQzNQA==}
'@push.rocks/smartjson@6.0.1': '@push.rocks/smartjson@6.0.1':
resolution: {integrity: sha512-iIw860jpjBcl83bLtq97QrjJxQkgxIKkhrX53EnpsVsZVNBgPCymLp0xNqY2jMpak5MKCEIWUVXkrmWVXj/TlQ==} resolution: {integrity: sha512-iIw860jpjBcl83bLtq97QrjJxQkgxIKkhrX53EnpsVsZVNBgPCymLp0xNqY2jMpak5MKCEIWUVXkrmWVXj/TlQ==}
@@ -1158,9 +1140,6 @@ packages:
'@push.rocks/smartpdf@4.2.2': '@push.rocks/smartpdf@4.2.2':
resolution: {integrity: sha512-xQWRChCLcM/sUrRuanvIcND/dKrnCYfL8Rr3kzSIPgSoDSmdDbd4kz7lLAHEPTsCezIwg2VqxFidW+zMNZ5Z1Q==} resolution: {integrity: sha512-xQWRChCLcM/sUrRuanvIcND/dKrnCYfL8Rr3kzSIPgSoDSmdDbd4kz7lLAHEPTsCezIwg2VqxFidW+zMNZ5Z1Q==}
'@push.rocks/smartpromise@4.2.3':
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
'@push.rocks/smartpromise@4.2.4': '@push.rocks/smartpromise@4.2.4':
resolution: {integrity: sha512-8FUyYt94hOIY9mqHjitn4h69u0jbEtTF2RKKw2DpiTVFjpDTk9gXbVHZ/V+xEcBrN4mrzdQES0OiDmkNPoddEQ==} resolution: {integrity: sha512-8FUyYt94hOIY9mqHjitn4h69u0jbEtTF2RKKw2DpiTVFjpDTk9gXbVHZ/V+xEcBrN4mrzdQES0OiDmkNPoddEQ==}
@@ -1200,9 +1179,6 @@ packages:
'@push.rocks/smartstream@3.4.2': '@push.rocks/smartstream@3.4.2':
resolution: {integrity: sha512-JsjFjaNIlCBUglciM/IrXH0mH+oOQTLYQ6UMwqsew2XSUTXxER3ev2NeKMDBV6ONf2HF21EPnOZuKfgvtNGnUg==} resolution: {integrity: sha512-JsjFjaNIlCBUglciM/IrXH0mH+oOQTLYQ6UMwqsew2XSUTXxER3ev2NeKMDBV6ONf2HF21EPnOZuKfgvtNGnUg==}
'@push.rocks/smartstring@4.1.0':
resolution: {integrity: sha512-Q4py/Nm3KTDhQ9EiC75yBtSTLR0KLMwhKM+8gGcutgKotZT6wJ3gncjmtD8LKFfNhb4lSaFMgPJgLrCHTOH6Iw==}
'@push.rocks/smartstring@4.1.1': '@push.rocks/smartstring@4.1.1':
resolution: {integrity: sha512-FlEpp2PcQ819ymmxjWb5/2gD8uPic/+IvOrSP2+KTdXLHOI4GSyK9YW/YBF541LVGl0GC3VGFmypcPNUzkPfYw==} resolution: {integrity: sha512-FlEpp2PcQ819ymmxjWb5/2gD8uPic/+IvOrSP2+KTdXLHOI4GSyK9YW/YBF541LVGl0GC3VGFmypcPNUzkPfYw==}
@@ -1234,9 +1210,6 @@ packages:
'@push.rocks/websetup@3.0.20': '@push.rocks/websetup@3.0.20':
resolution: {integrity: sha512-7TJ2ryFEpuSocGQwhhdEL6x8d7H0q3N4MJIJS46nc7r5XM5oXAXaIj/8gX2/TSNQWUt35CNSpJPkznoLpp95Jw==} resolution: {integrity: sha512-7TJ2ryFEpuSocGQwhhdEL6x8d7H0q3N4MJIJS46nc7r5XM5oXAXaIj/8gX2/TSNQWUt35CNSpJPkznoLpp95Jw==}
'@push.rocks/webstore@2.0.20':
resolution: {integrity: sha512-Z3L4OHGcw/Gs9aXpMUwebEPTh0nK/C7R6YwPfCLcGVu9yd/ZShaQ8QZEYE243Cu9J1Mn+CEtz4jpPLnHiizHQA==}
'@push.rocks/webstore@2.0.22': '@push.rocks/webstore@2.0.22':
resolution: {integrity: sha512-EdWfcNo0m6adSgTq7NtZusvmubUtRiCRADfFIbbgGZhCr9xLxmyB1nCtO/wzUrWZEbnR+Q9+fYkJFnDFOmZ4wA==} resolution: {integrity: sha512-EdWfcNo0m6adSgTq7NtZusvmubUtRiCRADfFIbbgGZhCr9xLxmyB1nCtO/wzUrWZEbnR+Q9+fYkJFnDFOmZ4wA==}
@@ -1580,9 +1553,6 @@ packages:
resolution: {integrity: sha512-G/gWDykZNL0NVcd1qXkoKm45jxJECp6q53DSomM5QKMsyAMEsGksVq+HwgonqYxfFJEzzHi6ljtWKXVS1pl0/Q==} resolution: {integrity: sha512-G/gWDykZNL0NVcd1qXkoKm45jxJECp6q53DSomM5QKMsyAMEsGksVq+HwgonqYxfFJEzzHi6ljtWKXVS1pl0/Q==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@tempfix/idb@8.0.3':
resolution: {integrity: sha512-hPJQKO7+oAIY+pDNImrZ9QAINbz9KmwT+yO4iRVwdPanok2YKpaUxdJzIvCUwY0YgAawlvYdffbLvRLV5hbs2g==}
'@tempfix/lenis@1.3.20': '@tempfix/lenis@1.3.20':
resolution: {integrity: sha512-ypeB0FuHLHOCQXW4d0RQ69txPJJH+1CHcpsZIUdcv2t1vR0IVyQr2vHihtde9UOXhjzqEnUphWon/UcJNsa0YA==} resolution: {integrity: sha512-ypeB0FuHLHOCQXW4d0RQ69txPJJH+1CHcpsZIUdcv2t1vR0IVyQr2vHihtde9UOXhjzqEnUphWon/UcJNsa0YA==}
peerDependencies: peerDependencies:
@@ -1637,9 +1607,6 @@ packages:
'@types/mime-types@2.1.4': '@types/mime-types@2.1.4':
resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==}
'@types/minimatch@5.1.2':
resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
'@types/ms@2.1.0': '@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
@@ -2092,10 +2059,6 @@ packages:
engines: {node: '>= 10.17.0'} engines: {node: '>= 10.17.0'}
hasBin: true hasBin: true
fake-indexeddb@5.0.2:
resolution: {integrity: sha512-cB507r5T3D55DfclY01GLkninZLfU7HXV/mhVRTnTRm5k2u+fY7Fof2dBkr80p5t7G7dlA/G5dI87QiMdPpMCQ==}
engines: {node: '>=18'}
fake-indexeddb@6.2.5: fake-indexeddb@6.2.5:
resolution: {integrity: sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==} resolution: {integrity: sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -2230,10 +2193,6 @@ packages:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hasown@2.0.3: hasown@2.0.3:
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2376,9 +2335,6 @@ packages:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'} engines: {node: '>=8'}
lodash.clonedeep@4.5.0:
resolution: {integrity: sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=}
longest-streak@3.1.0: longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
@@ -2650,10 +2606,6 @@ packages:
no-case@2.3.2: no-case@2.3.2:
resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==}
node-forge@1.3.3:
resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==}
engines: {node: '>= 6.13.0'}
node-forge@1.4.0: node-forge@1.4.0:
resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==}
engines: {node: '>= 6.13.0'} engines: {node: '>= 6.13.0'}
@@ -4698,10 +4650,6 @@ snapshots:
- react-native-b4a - react-native-b4a
- supports-color - supports-color
'@push.rocks/consolecolor@2.0.3':
dependencies:
ansi-256-colors: 1.1.0
'@push.rocks/consolecolor@2.0.4': '@push.rocks/consolecolor@2.0.4':
dependencies: dependencies:
ansi-256-colors: 1.1.0 ansi-256-colors: 1.1.0
@@ -4725,17 +4673,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- aws-crt - aws-crt
'@push.rocks/lik@6.3.1':
dependencies:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartmatch': 2.0.0
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10
'@push.rocks/smarttime': 4.2.3
'@types/minimatch': 5.1.2
'@types/symbol-tree': 3.2.5
symbol-tree: 3.2.4
'@push.rocks/lik@6.4.1': '@push.rocks/lik@6.4.1':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.1.0 '@push.rocks/smartdelay': 3.1.0
@@ -4838,9 +4775,9 @@ snapshots:
'@push.rocks/smartclickhouse@2.2.0': '@push.rocks/smartclickhouse@2.2.0':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.1.0
'@push.rocks/smartobject': 1.0.12 '@push.rocks/smartobject': 1.0.12
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.4
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smarturl': 3.1.0 '@push.rocks/smarturl': 3.1.0
'@push.rocks/webrequest': 4.0.5 '@push.rocks/webrequest': 4.0.5
@@ -4864,9 +4801,9 @@ snapshots:
'@push.rocks/smartcrypto@2.0.4': '@push.rocks/smartcrypto@2.0.4':
dependencies: dependencies:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.4
'@types/node-forge': 1.3.14 '@types/node-forge': 1.3.14
node-forge: 1.3.3 node-forge: 1.4.0
'@push.rocks/smartdata@7.1.7(socks@2.8.9)': '@push.rocks/smartdata@7.1.7(socks@2.8.9)':
dependencies: dependencies:
@@ -4898,10 +4835,6 @@ snapshots:
- supports-color - supports-color
- vue - vue
'@push.rocks/smartdelay@3.0.5':
dependencies:
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartdelay@3.1.0': '@push.rocks/smartdelay@3.1.0':
dependencies: dependencies:
'@push.rocks/smartpromise': 4.2.4 '@push.rocks/smartpromise': 4.2.4
@@ -4920,11 +4853,7 @@ snapshots:
'@push.rocks/smartenv@5.0.13': '@push.rocks/smartenv@5.0.13':
dependencies: dependencies:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.4
'@push.rocks/smartenv@6.0.0':
dependencies:
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartenv@6.1.0': '@push.rocks/smartenv@6.1.0':
dependencies: dependencies:
@@ -4970,14 +4899,6 @@ snapshots:
'@push.rocks/smartpromise': 4.2.4 '@push.rocks/smartpromise': 4.2.4
'@push.rocks/smartrequest': 2.1.0 '@push.rocks/smartrequest': 2.1.0
'@push.rocks/smarthash@3.2.6':
dependencies:
'@push.rocks/smartenv': 5.0.13
'@push.rocks/smartjson': 5.2.0
'@push.rocks/smartpromise': 4.2.3
'@types/through2': 2.0.41
through2: 4.0.2
'@push.rocks/smarthash@3.2.7': '@push.rocks/smarthash@3.2.7':
dependencies: dependencies:
'@push.rocks/smartenv': 6.1.0 '@push.rocks/smartenv': 6.1.0
@@ -5006,17 +4927,10 @@ snapshots:
- aws-crt - aws-crt
- supports-color - supports-color
'@push.rocks/smartjson@5.2.0':
dependencies:
'@push.rocks/smartenv': 5.0.13
'@push.rocks/smartstring': 4.1.0
fast-json-stable-stringify: 2.1.0
lodash.clonedeep: 4.5.0
'@push.rocks/smartjson@6.0.1': '@push.rocks/smartjson@6.0.1':
dependencies: dependencies:
'@push.rocks/smartenv': 6.0.0 '@push.rocks/smartenv': 6.1.0
'@push.rocks/smartstring': 4.1.0 '@push.rocks/smartstring': 4.1.1
fast-json-stable-stringify: 2.1.0 fast-json-stable-stringify: 2.1.0
'@push.rocks/smartlog-destination-local@9.0.2': '@push.rocks/smartlog-destination-local@9.0.2':
@@ -5033,11 +4947,11 @@ snapshots:
'@push.rocks/smartlog@3.2.2': '@push.rocks/smartlog@3.2.2':
dependencies: dependencies:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/consolecolor': 2.0.3 '@push.rocks/consolecolor': 2.0.4
'@push.rocks/isounique': 1.0.5 '@push.rocks/isounique': 1.0.5
'@push.rocks/smartclickhouse': 2.2.0 '@push.rocks/smartclickhouse': 2.2.0
'@push.rocks/smarthash': 3.2.6 '@push.rocks/smarthash': 3.2.7
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.4
'@push.rocks/smarttime': 4.2.3 '@push.rocks/smarttime': 4.2.3
'@push.rocks/webrequest': 4.0.5 '@push.rocks/webrequest': 4.0.5
'@tsclass/tsclass': 9.5.1 '@tsclass/tsclass': 9.5.1
@@ -5128,7 +5042,7 @@ snapshots:
'@push.rocks/smartnftables@1.2.0': '@push.rocks/smartnftables@1.2.0':
dependencies: dependencies:
'@push.rocks/smartlog': 3.2.2 '@push.rocks/smartlog': 3.2.2
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.4
'@push.rocks/smartnpm@2.1.0': '@push.rocks/smartnpm@2.1.0':
dependencies: dependencies:
@@ -5182,8 +5096,6 @@ snapshots:
- typescript - typescript
- utf-8-validate - utf-8-validate
'@push.rocks/smartpromise@4.2.3': {}
'@push.rocks/smartpromise@4.2.4': {} '@push.rocks/smartpromise@4.2.4': {}
'@push.rocks/smartpuppeteer@2.0.6(typescript@6.0.3)': '@push.rocks/smartpuppeteer@2.0.6(typescript@6.0.3)':
@@ -5229,7 +5141,7 @@ snapshots:
'@push.rocks/smartrx@3.0.10': '@push.rocks/smartrx@3.0.10':
dependencies: dependencies:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.4
rxjs: 7.8.2 rxjs: 7.8.2
'@push.rocks/smartserve@2.0.4': '@push.rocks/smartserve@2.0.4':
@@ -5283,19 +5195,15 @@ snapshots:
'@push.rocks/smartpromise': 4.2.4 '@push.rocks/smartpromise': 4.2.4
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smartstring@4.1.0':
dependencies:
'@push.rocks/isounique': 1.0.5
'@push.rocks/smartstring@4.1.1': '@push.rocks/smartstring@4.1.1':
dependencies: dependencies:
'@push.rocks/isounique': 1.0.5 '@push.rocks/isounique': 1.0.5
'@push.rocks/smarttime@4.2.3': '@push.rocks/smarttime@4.2.3':
dependencies: dependencies:
'@push.rocks/lik': 6.3.1 '@push.rocks/lik': 6.4.1
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.1.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.4
croner: 10.0.1 croner: 10.0.1
date-fns: 4.1.0 date-fns: 4.1.0
dayjs: 1.11.20 dayjs: 1.11.20
@@ -5347,28 +5255,17 @@ snapshots:
'@push.rocks/webrequest@4.0.5': '@push.rocks/webrequest@4.0.5':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.1.0
'@push.rocks/smartenv': 6.0.0 '@push.rocks/smartenv': 6.1.0
'@push.rocks/smartjson': 6.0.1 '@push.rocks/smartjson': 6.0.1
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.4
'@push.rocks/webstore': 2.0.20 '@push.rocks/webstore': 2.0.22
'@push.rocks/websetup@3.0.20': '@push.rocks/websetup@3.0.20':
dependencies: dependencies:
'@push.rocks/smartpromise': 4.2.4 '@push.rocks/smartpromise': 4.2.4
'@tsclass/tsclass': 9.5.1 '@tsclass/tsclass': 9.5.1
'@push.rocks/webstore@2.0.20':
dependencies:
'@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/lik': 6.3.1
'@push.rocks/smartenv': 5.0.13
'@push.rocks/smartjson': 5.2.0
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10
'@tempfix/idb': 8.0.3
fake-indexeddb: 5.0.2
'@push.rocks/webstore@2.0.22': '@push.rocks/webstore@2.0.22':
dependencies: dependencies:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
@@ -5702,8 +5599,6 @@ snapshots:
'@smithy/core': 3.24.1 '@smithy/core': 3.24.1
tslib: 2.8.1 tslib: 2.8.1
'@tempfix/idb@8.0.3': {}
'@tempfix/lenis@1.3.20': {} '@tempfix/lenis@1.3.20': {}
'@tokenizer/inflate@0.4.1': '@tokenizer/inflate@0.4.1':
@@ -5757,8 +5652,6 @@ snapshots:
'@types/mime-types@2.1.4': {} '@types/mime-types@2.1.4': {}
'@types/minimatch@5.1.2': {}
'@types/ms@2.1.0': {} '@types/ms@2.1.0': {}
'@types/mute-stream@0.0.4': '@types/mute-stream@0.0.4':
@@ -6221,8 +6114,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
fake-indexeddb@5.0.2: {}
fake-indexeddb@6.2.5: {} fake-indexeddb@6.2.5: {}
fast-deep-equal@3.1.3: {} fast-deep-equal@3.1.3: {}
@@ -6329,7 +6220,7 @@ snapshots:
get-proto: 1.0.1 get-proto: 1.0.1
gopd: 1.2.0 gopd: 1.2.0
has-symbols: 1.1.0 has-symbols: 1.1.0
hasown: 2.0.2 hasown: 2.0.3
math-intrinsics: 1.1.0 math-intrinsics: 1.1.0
get-proto@1.0.1: get-proto@1.0.1:
@@ -6383,10 +6274,6 @@ snapshots:
dependencies: dependencies:
has-symbols: 1.1.0 has-symbols: 1.1.0
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
hasown@2.0.3: hasown@2.0.3:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
@@ -6570,8 +6457,6 @@ snapshots:
dependencies: dependencies:
p-locate: 4.1.0 p-locate: 4.1.0
lodash.clonedeep@4.5.0: {}
longest-streak@3.1.0: {} longest-streak@3.1.0: {}
lower-case@1.1.4: {} lower-case@1.1.4: {}
@@ -7027,8 +6912,6 @@ snapshots:
dependencies: dependencies:
lower-case: 1.1.4 lower-case: 1.1.4
node-forge@1.3.3: {}
node-forge@1.4.0: {} node-forge@1.4.0: {}
object-keys@1.1.1: {} object-keys@1.1.1: {}
@@ -45,6 +45,9 @@ pub struct ConnActivity {
/// increments on creation and decrements on Drop, keeping the watchdog aware that /// increments on creation and decrements on Drop, keeping the watchdog aware that
/// a response body is still streaming after the request handler has returned. /// a response body is still streaming after the request handler has returned.
active_requests: Option<Arc<AtomicU64>>, active_requests: Option<Arc<AtomicU64>>,
/// Active upgraded tunnel counter. When set, upgraded WebSocket streams keep the
/// HTTP keep-alive/lifetime watchdog out of the tunnel lifecycle.
active_upgrades: Option<Arc<AtomicU64>>,
/// Protocol cache key for Alt-Svc discovery. When set, `build_streaming_response` /// Protocol cache key for Alt-Svc discovery. When set, `build_streaming_response`
/// checks the backend's original response headers for Alt-Svc before our /// checks the backend's original response headers for Alt-Svc before our
/// ResponseFilter injects its own. None when not in auto-detect mode or after H3 failure. /// ResponseFilter injects its own. None when not in auto-detect mode or after H3 failure.
@@ -61,6 +64,7 @@ impl ConnActivity {
last_activity: Arc::new(AtomicU64::new(0)), last_activity: Arc::new(AtomicU64::new(0)),
start: std::time::Instant::now(), start: std::time::Instant::now(),
active_requests: None, active_requests: None,
active_upgrades: None,
alt_svc_cache_key: None, alt_svc_cache_key: None,
alt_svc_request_url: None, alt_svc_request_url: None,
} }
@@ -488,6 +492,7 @@ impl HttpProxyService {
// (no request in progress and none started recently). // (no request in progress and none started recently).
let last_activity = Arc::new(AtomicU64::new(0)); let last_activity = Arc::new(AtomicU64::new(0));
let active_requests = Arc::new(AtomicU64::new(0)); let active_requests = Arc::new(AtomicU64::new(0));
let active_upgrades = Arc::new(AtomicU64::new(0));
let start = std::time::Instant::now(); let start = std::time::Instant::now();
// Connection-level frontend protocol tracker: the first request detects // Connection-level frontend protocol tracker: the first request detects
@@ -498,6 +503,7 @@ impl HttpProxyService {
let la_inner = Arc::clone(&last_activity); let la_inner = Arc::clone(&last_activity);
let ar_inner = Arc::clone(&active_requests); let ar_inner = Arc::clone(&active_requests);
let au_inner = Arc::clone(&active_upgrades);
let cancel_inner = cancel.clone(); let cancel_inner = cancel.clone();
let vpn_info = Arc::new(vpn_info); let vpn_info = Arc::new(vpn_info);
let service = hyper::service::service_fn(move |req: Request<Incoming>| { let service = hyper::service::service_fn(move |req: Request<Incoming>| {
@@ -522,6 +528,7 @@ impl HttpProxyService {
last_activity: Arc::clone(&la_inner), last_activity: Arc::clone(&la_inner),
start, start,
active_requests: Some(Arc::clone(&ar_inner)), active_requests: Some(Arc::clone(&ar_inner)),
active_upgrades: Some(Arc::clone(&au_inner)),
alt_svc_cache_key: None, alt_svc_cache_key: None,
alt_svc_request_url: None, alt_svc_request_url: None,
}; };
@@ -572,8 +579,14 @@ impl HttpProxyService {
loop { loop {
tokio::time::sleep(check_interval).await; tokio::time::sleep(check_interval).await;
// Check max connection lifetime (unconditional — even active connections // Upgraded tunnels have their own WebSocket watchdog and lifetime.
// must eventually be recycled to prevent resource accumulation). if active_upgrades.load(Ordering::Relaxed) > 0 {
last_seen = last_activity.load(Ordering::Relaxed);
continue;
}
// Check max connection lifetime (unconditional for regular HTTP — even active
// connections must eventually be recycled to prevent resource accumulation).
if start.elapsed() >= max_lifetime { if start.elapsed() >= max_lifetime {
debug!("HTTP connection exceeded max lifetime ({}s) from {}", debug!("HTTP connection exceeded max lifetime ({}s) from {}",
max_lifetime.as_secs(), peer_addr); max_lifetime.as_secs(), peer_addr);
@@ -789,11 +802,7 @@ impl HttpProxyService {
cancel, cancel,
&ip_str, &ip_str,
is_h2_websocket, is_h2_websocket,
if is_h2_websocket { Some(conn_activity.clone()),
Some(conn_activity.clone())
} else {
None
},
) )
.await; .await;
// Note: for WebSocket, connection_ended is called inside // Note: for WebSocket, connection_ended is called inside
@@ -3286,8 +3295,19 @@ impl HttpProxyService {
let upstream_key_owned = upstream_key.to_string(); let upstream_key_owned = upstream_key.to_string();
let ws_inactivity_timeout = self.ws_inactivity_timeout; let ws_inactivity_timeout = self.ws_inactivity_timeout;
let ws_max_lifetime = self.ws_max_lifetime; let ws_max_lifetime = self.ws_max_lifetime;
let ws_request_guard = conn_activity
.as_ref()
.and_then(|ca| ca.active_requests.as_ref())
.map(|counter| ActiveRequestGuard::new(Arc::clone(counter)));
let ws_upgrade_guard = conn_activity
.as_ref()
.and_then(|ca| ca.active_upgrades.as_ref())
.map(|counter| ActiveRequestGuard::new(Arc::clone(counter)));
tokio::spawn(async move { tokio::spawn(async move {
let _ws_request_guard = ws_request_guard;
let _ws_upgrade_guard = ws_upgrade_guard;
// RAII guard: ensures connection_ended is called even if this task panics // RAII guard: ensures connection_ended is called even if this task panics
struct WsUpstreamGuard { struct WsUpstreamGuard {
selector: UpstreamSelector, selector: UpstreamSelector,
@@ -154,6 +154,9 @@ pub struct ConnectionConfig {
pub max_connections: u64, pub max_connections: u64,
} }
const DEFAULT_WS_INACTIVITY_TIMEOUT_MS: u64 = 3_600_000;
const DEFAULT_WS_MAX_LIFETIME_MS: u64 = 86_400_000;
impl Default for ConnectionConfig { impl Default for ConnectionConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
@@ -225,8 +228,8 @@ impl TcpListenerManager {
http_proxy_svc.set_connection_timeouts( http_proxy_svc.set_connection_timeouts(
std::time::Duration::from_millis(conn_config.socket_timeout_ms), std::time::Duration::from_millis(conn_config.socket_timeout_ms),
std::time::Duration::from_millis(conn_config.max_connection_lifetime_ms), std::time::Duration::from_millis(conn_config.max_connection_lifetime_ms),
std::time::Duration::from_millis(conn_config.socket_timeout_ms), std::time::Duration::from_millis(DEFAULT_WS_INACTIVITY_TIMEOUT_MS),
std::time::Duration::from_millis(conn_config.max_connection_lifetime_ms), std::time::Duration::from_millis(DEFAULT_WS_MAX_LIFETIME_MS),
); );
let http_proxy = Arc::new(http_proxy_svc); let http_proxy = Arc::new(http_proxy_svc);
let conn_tracker = Arc::new(ConnectionTracker::new( let conn_tracker = Arc::new(ConnectionTracker::new(
@@ -266,8 +269,8 @@ impl TcpListenerManager {
http_proxy_svc.set_connection_timeouts( http_proxy_svc.set_connection_timeouts(
std::time::Duration::from_millis(conn_config.socket_timeout_ms), std::time::Duration::from_millis(conn_config.socket_timeout_ms),
std::time::Duration::from_millis(conn_config.max_connection_lifetime_ms), std::time::Duration::from_millis(conn_config.max_connection_lifetime_ms),
std::time::Duration::from_millis(conn_config.socket_timeout_ms), std::time::Duration::from_millis(DEFAULT_WS_INACTIVITY_TIMEOUT_MS),
std::time::Duration::from_millis(conn_config.max_connection_lifetime_ms), std::time::Duration::from_millis(DEFAULT_WS_MAX_LIFETIME_MS),
); );
let http_proxy = Arc::new(http_proxy_svc); let http_proxy = Arc::new(http_proxy_svc);
let conn_tracker = Arc::new(ConnectionTracker::new( let conn_tracker = Arc::new(ConnectionTracker::new(
@@ -313,8 +316,8 @@ impl TcpListenerManager {
http_proxy_svc.set_connection_timeouts( http_proxy_svc.set_connection_timeouts(
std::time::Duration::from_millis(config.socket_timeout_ms), std::time::Duration::from_millis(config.socket_timeout_ms),
std::time::Duration::from_millis(config.max_connection_lifetime_ms), std::time::Duration::from_millis(config.max_connection_lifetime_ms),
std::time::Duration::from_millis(config.socket_timeout_ms), std::time::Duration::from_millis(DEFAULT_WS_INACTIVITY_TIMEOUT_MS),
std::time::Duration::from_millis(config.max_connection_lifetime_ms), std::time::Duration::from_millis(DEFAULT_WS_MAX_LIFETIME_MS),
); );
self.http_proxy = Arc::new(http_proxy_svc); self.http_proxy = Arc::new(http_proxy_svc);
+64
View File
@@ -415,4 +415,68 @@ tap.test('should handle large WebSocket messages', async () => {
await assertPortsFree([PROXY_PORT, BACKEND_PORT]); await assertPortsFree([PROXY_PORT, BACKEND_PORT]);
}); });
// ─── Test 7: Idle WebSocket outlives short HTTP socket timeout ───
tap.test('should keep idle WebSocket open beyond HTTP socket timeout', async (tools) => {
tools.timeout(15000);
const [PROXY_PORT, BACKEND_PORT] = await findFreePorts(2);
let proxy: SmartProxy | undefined;
let ws: WebSocket | undefined;
const backendServer = http.createServer();
const wss = new WebSocketServer({ server: backendServer });
wss.on('connection', (backendWs) => {
backendWs.on('message', (data) => {
backendWs.send(`echo: ${data.toString()}`);
});
});
try {
await new Promise<void>((resolve) => {
backendServer.listen(BACKEND_PORT, '127.0.0.1', () => resolve());
});
proxy = new SmartProxy({
socketTimeout: 1000,
maxConnectionLifetime: 1000,
routes: [{
name: 'ws-idle-timeout-route',
match: { ports: PROXY_PORT },
action: {
type: 'forward',
targets: [{ host: '127.0.0.1', port: BACKEND_PORT }],
websocket: { enabled: true },
},
}],
});
await proxy.start();
const connection = connectWs(
`ws://127.0.0.1:${PROXY_PORT}/`,
{ Host: 'test.local' },
);
ws = connection.ws;
await connection.opened;
await new Promise((resolve) => setTimeout(resolve, 6500));
expect(ws.readyState).toEqual(WebSocket.OPEN);
ws.send('still open');
await waitFor(() => connection.messages.length >= 1);
expect(connection.messages[0]).toEqual('echo: still open');
} finally {
if (ws && ws.readyState !== WebSocket.CLOSED) {
await closeWs(ws);
}
if (proxy) {
await proxy.stop();
}
wss.close();
await new Promise<void>((resolve) => backendServer.close(() => resolve()));
await new Promise((r) => setTimeout(r, 500));
await assertPortsFree([PROXY_PORT, BACKEND_PORT]);
}
});
export default tap.start(); export default tap.start();