diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index 1abc57c..32a83c4 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -24,8 +24,24 @@ jobs: ⏱️ ${{ github.event.head_commit.timestamp }} - build-docker-image: + build-docker-images: + name: Build ${{ matrix.image.name }} runs-on: ubuntu-latest + strategy: + matrix: + image: + - name: 'Main Image' + tags: | + remnawave/node:latest + remnawave/node:${{github.ref_name}} + ghcr.io/remnawave/node:latest + ghcr.io/remnawave/node:${{github.ref_name}} + build_args: '' + - name: 'SNI Image' + tags: | + remnawave/node:sni-latest + remnawave/node:sni-${{github.ref_name}} + build_args: 'UPSTREAM_REPO=kastov' steps: - name: Checkout uses: actions/checkout@v3 @@ -54,20 +70,17 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.TOKEN_GH_DEPLOY }} - - name: Build and push + - name: Build and push ${{ matrix.image.name }} uses: docker/build-push-action@v3 with: context: . platforms: linux/amd64,linux/arm64 push: true - tags: | - remnawave/node:latest - remnawave/node:${{github.ref_name}} - ghcr.io/remnawave/node:latest - ghcr.io/remnawave/node:${{github.ref_name}} + build-args: ${{ matrix.image.build_args }} + tags: ${{ matrix.image.tags }} create-release: - needs: [build-docker-image] + needs: [build-docker-images] runs-on: ubuntu-latest steps: - name: NewTag @@ -101,7 +114,7 @@ jobs: send-telegram-message: name: Send Telegram message - needs: [build-docker-image, create-release] + needs: [build-docker-images, create-release] runs-on: ubuntu-latest steps: - name: Checkout source code @@ -120,7 +133,7 @@ jobs: notify-on-error: runs-on: ubuntu-latest - needs: [build-docker-image] + needs: [build-docker-images] if: failure() steps: - name: Checkout source code diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml index 951464b..2b0b21b 100644 --- a/.github/workflows/build-dev.yml +++ b/.github/workflows/build-dev.yml @@ -21,10 +21,20 @@ jobs: thread_id: ${{ secrets.TELEGRAM_TOPIC_ID }} status: pending notify_fields: 'repo_with_tag,commit,workflow' - title: 'Building docker image.' + title: 'Building docker images.' - build-docker-image: + build-docker-images: + name: Build ${{ matrix.image.name }} runs-on: ubuntu-latest + strategy: + matrix: + image: + - name: 'Main Image' + tag: 'remnawave/node:dev' + build_args: '' + - name: 'SNI Image' + tag: 'remnawave/node:sni-dev' + build_args: 'UPSTREAM_REPO=kastov' steps: - name: Checkout uses: actions/checkout@v3 @@ -46,18 +56,18 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push + - name: Build and push ${{ matrix.image.name }} uses: docker/build-push-action@v3 with: context: . platforms: linux/amd64,linux/arm64 push: true - tags: | - remnawave/node:dev + build-args: ${{ matrix.image.build_args }} + tags: ${{ matrix.image.tag }} send-finish-tg-msg: name: Send TG message - needs: [build-docker-image] + needs: [build-docker-images] runs-on: ubuntu-latest steps: - name: Checkout source code @@ -75,7 +85,7 @@ jobs: notify-on-error: runs-on: ubuntu-latest - needs: [build-docker-image] + needs: [build-docker-images] if: failure() steps: - name: Checkout source code diff --git a/Dockerfile b/Dockerfile index 22ede36..dadd715 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,9 +7,11 @@ RUN npm run build --omit=dev FROM node:22-alpine -RUN mkdir -p /var/log/supervisor /var/lib/rnode/xray \ - && echo '{}' > /var/lib/rnode/xray/xray-config.json +ARG XRAY_CORE_VERSION=v25.8.3 +ARG UPSTREAM_REPO=XTLS +ARG XRAY_CORE_INSTALL_SCRIPT=https://raw.githubusercontent.com/remnawave/scripts/main/scripts/install-xray.sh +RUN mkdir -p /var/log/supervisor WORKDIR /opt/app COPY --from=build /opt/app/dist ./dist @@ -23,7 +25,7 @@ RUN apk add --no-cache \ python3 \ py3-pip \ && pip3 install --break-system-packages git+https://github.com/Supervisor/supervisor.git@4bf1e57cbf292ce988dc128e0d2c8917f18da9be \ - && curl -L https://raw.githubusercontent.com/remnawave/scripts/main/scripts/install-latest-xray.sh | bash -s -- v25.6.8 \ + && curl -L ${XRAY_CORE_INSTALL_SCRIPT} | bash -s -- ${XRAY_CORE_VERSION} ${UPSTREAM_REPO} \ && apk del curl git COPY supervisord.conf /etc/supervisord.conf diff --git a/DockerfileLegacy b/DockerfileLegacy deleted file mode 100644 index d56064e..0000000 --- a/DockerfileLegacy +++ /dev/null @@ -1,39 +0,0 @@ -FROM node:22-alpine AS build -WORKDIR /opt/app -ADD . . -RUN npm ci --legacy-peer-deps -RUN npm run build --omit=dev - - -FROM node:22-alpine - -RUN mkdir -p /var/log/supervisor /var/lib/rnode/xray \ - && echo '{}' > /var/lib/rnode/xray/xray-config.json - - -WORKDIR /opt/app -COPY --from=build /opt/app/dist ./dist - - -RUN apk add --no-cache \ - curl \ - unzip \ - bash \ - supervisor \ - && curl -L https://raw.githubusercontent.com/remnawave/scripts/main/scripts/install-latest-xray.sh | bash -s -- v1.8.23 \ - && apk del curl - -COPY supervisord.conf /etc/supervisord.conf -COPY docker-entrypoint.sh /usr/local/bin/ -RUN chmod +x /usr/local/bin/docker-entrypoint.sh - - -COPY package*.json ./ -COPY ./libs ./libs - -RUN npm ci --omit=dev --legacy-peer-deps \ - && npm cache clean --force - -ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] - -CMD ["npm", "run", "start:prod"] \ No newline at end of file diff --git a/libs/contract/commands/handler/add-user.command.ts b/libs/contract/commands/handler/add-user.command.ts index 2281ee7..49aee14 100644 --- a/libs/contract/commands/handler/add-user.command.ts +++ b/libs/contract/commands/handler/add-user.command.ts @@ -79,6 +79,10 @@ export namespace AddUserCommand { BaseHttpUser, ]), ), + hashData: z.object({ + vlessUuid: z.string().uuid(), + prevVlessUuid: z.optional(z.string().uuid()), + }), }); export type Request = z.infer; diff --git a/libs/contract/commands/handler/remove-user.command.ts b/libs/contract/commands/handler/remove-user.command.ts index f6dfbdb..cc1e75c 100644 --- a/libs/contract/commands/handler/remove-user.command.ts +++ b/libs/contract/commands/handler/remove-user.command.ts @@ -7,6 +7,9 @@ export namespace RemoveUserCommand { export const RequestSchema = z.object({ username: z.string(), + hashData: z.object({ + vlessUuid: z.string().uuid(), + }), }); export type Request = z.infer; diff --git a/libs/contract/constants/hashes/hash-payload.ts b/libs/contract/constants/hashes/hash-payload.ts new file mode 100644 index 0000000..f58982d --- /dev/null +++ b/libs/contract/constants/hashes/hash-payload.ts @@ -0,0 +1,8 @@ +export interface IHashPayload { + emptyConfig: string; + inbounds: { + usersCount: number; + hash: string; + tag: string; + }[]; +} diff --git a/libs/contract/constants/hashes/index.ts b/libs/contract/constants/hashes/index.ts new file mode 100644 index 0000000..8b113eb --- /dev/null +++ b/libs/contract/constants/hashes/index.ts @@ -0,0 +1 @@ +export * from './hash-payload'; diff --git a/libs/contract/constants/headers/headers.contants.ts b/libs/contract/constants/headers/headers.contants.ts new file mode 100644 index 0000000..5401533 --- /dev/null +++ b/libs/contract/constants/headers/headers.contants.ts @@ -0,0 +1 @@ +export const X_HASH_PAYLOAD = 'X-Hash-Payload'; diff --git a/libs/contract/constants/headers/index.ts b/libs/contract/constants/headers/index.ts new file mode 100644 index 0000000..2be1a2f --- /dev/null +++ b/libs/contract/constants/headers/index.ts @@ -0,0 +1 @@ +export * from './headers.contants'; diff --git a/libs/contract/constants/index.ts b/libs/contract/constants/index.ts index 04448ba..a77b81e 100644 --- a/libs/contract/constants/index.ts +++ b/libs/contract/constants/index.ts @@ -1,4 +1,6 @@ export * from './errors'; +export * from './hashes'; +export * from './headers'; export * from './internal'; export * from './roles'; export * from './xray'; diff --git a/libs/contract/package.json b/libs/contract/package.json index daa525c..96a5713 100644 --- a/libs/contract/package.json +++ b/libs/contract/package.json @@ -1,6 +1,6 @@ { "name": "@remnawave/node-contract", - "version": "0.5.3", + "version": "0.5.8", "description": "A node-contract library for Remnawave Panel", "main": "build/index.js", "types": "build/index.d.ts", diff --git a/package-lock.json b/package-lock.json index 73bd2ef..c99d214 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,24 @@ { "name": "@remnawave/node", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@remnawave/node", - "version": "2.0.0", + "version": "2.1.0", "license": "AGPL-3.0-only", "dependencies": { "@cjs-exporter/execa": "9.5.2", - "@nestjs/common": "11.1.5", + "@nestjs/common": "11.1.6", "@nestjs/config": "4.0.2", - "@nestjs/core": "11.1.5", + "@nestjs/core": "11.1.6", "@nestjs/jwt": "11.0.0", "@nestjs/passport": "11.0.5", - "@nestjs/platform-express": "11.1.5", + "@nestjs/platform-express": "11.1.6", + "@remnawave/hashed-set": "^0.0.4", "@remnawave/supervisord-nestjs": "0.1.1", - "@remnawave/xtls-sdk": "0.4.1", + "@remnawave/xtls-sdk": "0.5.0", "@remnawave/xtls-sdk-nestjs": "0.4.0", "compression": "^1.8.1", "enhanced-ms": "^4.1.0", @@ -45,8 +46,8 @@ "zod": "^3.24.2" }, "devDependencies": { - "@nestjs/cli": "11.0.9", - "@nestjs/schematics": "11.0.6", + "@nestjs/cli": "11.0.10", + "@nestjs/schematics": "11.0.7", "@types/compression": "^1.8.1", "@types/express": "^5.0.3", "@types/js-yaml": "^4.0.9", @@ -74,9 +75,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "20.1.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.1.3.tgz", - "integrity": "sha512-23neiDOsq9cprozgBbnWo2nRTE4xYMjcAN59QcS4yYPccDkxbr3AazFHhlTSZWLp63hhTlT+B2AA47W7cUqhUQ==", + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.15.tgz", + "integrity": "sha512-pU2RZYX6vhd7uLSdLwPnuBcr0mXJSjp3EgOXKsrlQFQZevc+Qs+2JdXgIElnOT/aDqtRtriDmLlSbtdE8n3ZbA==", "dev": true, "license": "MIT", "dependencies": { @@ -84,11 +85,11 @@ "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.2", - "rxjs": "7.8.2", + "rxjs": "7.8.1", "source-map": "0.7.4" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, @@ -101,64 +102,75 @@ } } }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/@angular-devkit/schematics": { - "version": "20.1.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.1.3.tgz", - "integrity": "sha512-VPwCeKsJE6FEwjIWoUL221Iqh/0Lbml/c+xjISIMXf58qinFlQj1k/5LNLlVrn56QLSHUpxoXIsVek/ME3x6/A==", + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.15.tgz", + "integrity": "sha512-kNOJ+3vekJJCQKWihNmxBkarJzNW09kP5a9E1SRNiQVNOUEeSwcRR0qYotM65nx821gNzjjhJXnAZ8OazWldrg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.1.3", + "@angular-devkit/core": "19.2.15", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", - "ora": "8.2.0", - "rxjs": "7.8.2" + "ora": "5.4.1", + "rxjs": "7.8.1" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, "node_modules/@angular-devkit/schematics-cli": { - "version": "20.1.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-20.1.3.tgz", - "integrity": "sha512-pUnd3LRCMTsRsNeOi1xm9QImPGbB7pfy7XT8rHoamrinQxOe8G6Dz8qhKnInsxGCWsXKjmLPbeDFy3lG6yiiCg==", + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.15.tgz", + "integrity": "sha512-1ESFmFGMpGQmalDB3t2EtmWDGv6gOFYBMxmHO2f1KI/UDl8UmZnCGL4mD3EWo8Hv0YIsZ9wOH9Q7ZHNYjeSpzg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.1.3", - "@angular-devkit/schematics": "20.1.3", - "@inquirer/prompts": "7.6.0", + "@angular-devkit/core": "19.2.15", + "@angular-devkit/schematics": "19.2.15", + "@inquirer/prompts": "7.3.2", "ansi-colors": "4.1.3", - "yargs-parser": "22.0.0" + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" }, "bin": { "schematics": "bin/schematics.js" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/prompts": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.6.0.tgz", - "integrity": "sha512-jAhL7tyMxB3Gfwn4HIJ0yuJ5pvcB5maYUcouGcgd/ub79f9MqZ+aVnBtuFf+VC2GTkCBF+R+eo7Vi63w5VZlzw==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", + "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.1.9", - "@inquirer/confirm": "^5.1.13", - "@inquirer/editor": "^4.2.14", - "@inquirer/expand": "^4.0.16", - "@inquirer/input": "^4.2.0", - "@inquirer/number": "^3.0.16", - "@inquirer/password": "^4.0.16", - "@inquirer/rawlist": "^4.1.4", - "@inquirer/search": "^3.0.16", - "@inquirer/select": "^4.2.4" + "@inquirer/checkbox": "^4.1.2", + "@inquirer/confirm": "^5.1.6", + "@inquirer/editor": "^4.2.7", + "@inquirer/expand": "^4.0.9", + "@inquirer/input": "^4.1.6", + "@inquirer/number": "^3.0.9", + "@inquirer/password": "^4.0.9", + "@inquirer/rawlist": "^4.0.9", + "@inquirer/search": "^3.0.9", + "@inquirer/select": "^4.0.9" }, "engines": { "node": ">=18" @@ -172,197 +184,14 @@ } } }, - "node_modules/@angular-devkit/schematics-cli/node_modules/yargs-parser": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@angular-devkit/schematics/node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/ora": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "tslib": "^2.1.0" } }, "node_modules/@babel/code-frame": { @@ -1296,15 +1125,15 @@ } }, "node_modules/@nestjs/cli": { - "version": "11.0.9", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.9.tgz", - "integrity": "sha512-pSxiAl5eE4CnobEB4+pBoqHoTpXeQLwZh3Iig22v8IZBSQHHik9aZMWqm/fvIJjqK5qClPvLiiCJ5AIEBW/86Q==", + "version": "11.0.10", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.10.tgz", + "integrity": "sha512-4waDT0yGWANg0pKz4E47+nUrqIJv/UqrZ5wLPkCqc7oMGRMWKAaw1NDZ9rKsaqhqvxb2LfI5+uXOWr4yi94DOQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.1.3", - "@angular-devkit/schematics": "20.1.3", - "@angular-devkit/schematics-cli": "20.1.3", + "@angular-devkit/core": "19.2.15", + "@angular-devkit/schematics": "19.2.15", + "@angular-devkit/schematics-cli": "19.2.15", "@inquirer/prompts": "7.8.0", "@nestjs/schematics": "^11.0.1", "ansis": "4.1.0", @@ -1342,9 +1171,9 @@ } }, "node_modules/@nestjs/common": { - "version": "11.1.5", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.5.tgz", - "integrity": "sha512-DQpWdr3ShO0BHWkHl3I4W/jR6R3pDtxyBlmrpTuZF+PXxQyBXNvsUne0Wyo6QHPEDi+pAz9XchBFoKbqOhcdTg==", + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.6.tgz", + "integrity": "sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==", "license": "MIT", "dependencies": { "file-type": "21.0.0", @@ -1388,9 +1217,9 @@ } }, "node_modules/@nestjs/core": { - "version": "11.1.5", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.5.tgz", - "integrity": "sha512-Qr25MEY9t8VsMETy7eXQ0cNXqu0lzuFrrTr+f+1G57ABCtV5Pogm7n9bF71OU2bnkDD32Bi4hQLeFR90cku3Tw==", + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.6.tgz", + "integrity": "sha512-siWX7UDgErisW18VTeJA+x+/tpNZrJewjTBsRPF3JVxuWRuAB1kRoiJcxHgln8Lb5UY9NdvklITR84DUEXD0Cg==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -1461,9 +1290,9 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "11.1.5", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.5.tgz", - "integrity": "sha512-OsoiUBY9Shs5IG3uvDIt9/IDfY5OlvWBESuB/K4Eun8xILw1EK5d5qMfC3d2sIJ+kA3l+kBR1d/RuzH7VprLIg==", + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.6.tgz", + "integrity": "sha512-HErwPmKnk+loTq8qzu1up+k7FC6Kqa8x6lJ4cDw77KnTxLzsCaPt+jBvOq6UfICmfqcqCCf3dKXg+aObQp+kIQ==", "license": "MIT", "dependencies": { "cors": "2.8.5", @@ -1482,14 +1311,14 @@ } }, "node_modules/@nestjs/schematics": { - "version": "11.0.6", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.6.tgz", - "integrity": "sha512-vrrC6Znlv3JNisR0YPaNX30vLkM00Pydc6L7KgcC6mOplkJ/8r1t++BIdQLeWmGSj+jXQ6YWhaHT6kz+5UayMw==", + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.7.tgz", + "integrity": "sha512-t8dNYYMwEeEsrlwc2jbkfwCfXczq4AeNEgx1KVQuJ6wYibXk0ZbXbPdfp8scnEAaQv1grpncNV5gWgzi7ZwbvQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.1.3", - "@angular-devkit/schematics": "20.1.3", + "@angular-devkit/core": "19.2.15", + "@angular-devkit/schematics": "19.2.15", "comment-json": "4.2.5", "jsonc-parser": "3.3.1", "pluralize": "8.0.0" @@ -1629,6 +1458,12 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, + "node_modules/@remnawave/hashed-set": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@remnawave/hashed-set/-/hashed-set-0.0.4.tgz", + "integrity": "sha512-YFIpbEbdxxC/2ReEKeRyiO7Vb6Zwkr6BooczsE0dNMXj1+nPtPNDrYui6/wmGLXHgzL9E8tgmqArkR4zbJ5+ng==", + "license": "AGPL-3.0-only" + }, "node_modules/@remnawave/supervisord-nestjs": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@remnawave/supervisord-nestjs/-/supervisord-nestjs-0.1.1.tgz", @@ -1643,9 +1478,9 @@ } }, "node_modules/@remnawave/xtls-sdk": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@remnawave/xtls-sdk/-/xtls-sdk-0.4.1.tgz", - "integrity": "sha512-2aBGIFTrRe7ahzERLWNITiMyS2q6Mg5togHE9paMF1c2o1zZ+w3mFfoq7qvzhATcLPkjNKnGADHrNyjqJ5Pvkg==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@remnawave/xtls-sdk/-/xtls-sdk-0.5.0.tgz", + "integrity": "sha512-QYqJDVDuidss6CTyzxjNf3SHYrKyvF1JHI58QAjXJ2KMqtIG+1D/gEA2V7MlyywlLnNdaMdeWAdwiABape4UIQ==", "license": "AGPL-3.0-only", "dependencies": { "@bufbuild/protobuf": "^2.2.2", @@ -4349,19 +4184,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-intrinsic": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", @@ -5342,19 +5164,6 @@ "node": ">=6" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -6818,19 +6627,6 @@ "node": ">= 0.8" } }, - "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -6974,6 +6770,16 @@ "node": ">=8" } }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/synckit": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.3.tgz", diff --git a/package.json b/package.json index bf4dd37..1809630 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@remnawave/node", - "version": "2.0.0", + "version": "2.1.0", "description": "Remnawave Node", "private": false, "type": "commonjs", @@ -28,15 +28,16 @@ }, "dependencies": { "@cjs-exporter/execa": "9.5.2", - "@nestjs/common": "11.1.5", + "@nestjs/common": "11.1.6", "@nestjs/config": "4.0.2", - "@nestjs/core": "11.1.5", + "@nestjs/core": "11.1.6", "@nestjs/jwt": "11.0.0", "@nestjs/passport": "11.0.5", - "@nestjs/platform-express": "11.1.5", + "@nestjs/platform-express": "11.1.6", + "@remnawave/hashed-set": "^0.0.4", "@remnawave/supervisord-nestjs": "0.1.1", + "@remnawave/xtls-sdk": "0.5.0", "@remnawave/xtls-sdk-nestjs": "0.4.0", - "@remnawave/xtls-sdk": "0.4.1", "compression": "^1.8.1", "enhanced-ms": "^4.1.0", "helmet": "^8.1.0", @@ -63,8 +64,8 @@ "zod": "^3.24.2" }, "devDependencies": { - "@nestjs/cli": "11.0.9", - "@nestjs/schematics": "11.0.6", + "@nestjs/cli": "11.0.10", + "@nestjs/schematics": "11.0.7", "@types/compression": "^1.8.1", "@types/express": "^5.0.3", "@types/js-yaml": "^4.0.9", diff --git a/src/common/config/app-config/config.schema.ts b/src/common/config/app-config/config.schema.ts index 108ebfb..e720761 100644 --- a/src/common/config/app-config/config.schema.ts +++ b/src/common/config/app-config/config.schema.ts @@ -13,6 +13,10 @@ export const configSchema = z JWT_PUBLIC_KEY: z.string().optional(), XTLS_IP: z.string().default('127.0.0.1'), XTLS_PORT: z.string().default('61000'), + DISABLE_HASHED_SET_CHECK: z + .string() + .default('false') + .transform((val) => val === 'true'), }) .superRefine((data, ctx) => { if (data.SSL_CERT) { diff --git a/src/common/decorators/get-hash-payload/get-hash-payload.ts b/src/common/decorators/get-hash-payload/get-hash-payload.ts new file mode 100644 index 0000000..9fe6297 --- /dev/null +++ b/src/common/decorators/get-hash-payload/get-hash-payload.ts @@ -0,0 +1,22 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +import { IHashPayload, X_HASH_PAYLOAD } from '@libs/contracts/constants'; + +export const HashPayload = createParamDecorator((_, ctx: ExecutionContext): IHashPayload | null => { + const request = ctx.switchToHttp().getRequest(); + + const hashPayload = request.headers[X_HASH_PAYLOAD.toLowerCase()]; + + if (hashPayload) { + try { + const decodedPayload = Buffer.from(hashPayload as string, 'base64').toString('utf-8'); + const hashPayloadJson = JSON.parse(decodedPayload); + + return hashPayloadJson as IHashPayload; + } catch { + return null; + } + } + + return null; +}); diff --git a/src/common/exception/httpException.filter.ts b/src/common/exception/httpException.filter.ts index 5057469..ac0d1e1 100644 --- a/src/common/exception/httpException.filter.ts +++ b/src/common/exception/httpException.filter.ts @@ -30,12 +30,12 @@ export class HttpExceptionFilter implements ExceptionFilter { this.logger.error(exception.getResponse()); response.status(status).json(exception.getResponse()); } else { - this.logger.error({ - timestamp: new Date().toISOString(), - code: errorCode, - path: request.url, - message: errorMessage, - }); + // this.logger.error({ + // timestamp: new Date().toISOString(), + // code: errorCode, + // path: request.url, + // message: errorMessage, + // }); response.status(status).json({ timestamp: new Date().toISOString(), path: request.url, diff --git a/src/modules/handler/handler.service.ts b/src/modules/handler/handler.service.ts index 0d36056..c67992c 100644 --- a/src/modules/handler/handler.service.ts +++ b/src/modules/handler/handler.service.ts @@ -10,9 +10,9 @@ import { ICommandResponse } from '@common/types/command-response.type'; import { ERRORS } from '@libs/contracts/constants/errors'; import { AddUserResponseModel, RemoveUserResponseModel } from './models'; +import { InternalService } from '../internal/internal.service'; import { GetInboundUsersCountResponseModel } from './models'; import { GetInboundUsersResponseModel } from './models'; -import { XrayService } from '../xray-core/xray.service'; import { IRemoveUserRequest } from './interfaces'; import { TAddUserRequest } from './interfaces'; @@ -22,19 +22,24 @@ export class HandlerService { constructor( @InjectXtls() private readonly xtlsApi: XtlsApi, - private readonly xrayService: XrayService, + private readonly internalService: InternalService, ) {} public async addUser(data: TAddUserRequest): Promise> { try { - const { data: requestData } = data; + const { data: requestData, hashData } = data; const response: Array> = []; - const inboundsTags = this.xrayService.getSavedInboundsTags(); + const inboundsTags = this.internalService.getXtlsConfigInbounds(); for (const tag of inboundsTags) { this.logger.debug(`Removing user: ${requestData[0].username} from tag: ${tag}`); await this.xtlsApi.handler.removeUser(tag, requestData[0].username); + if (hashData.prevVlessUuid) { + this.internalService.removeUserFromInbound(tag, hashData.prevVlessUuid); + } else { + this.internalService.removeUserFromInbound(tag, hashData.vlessUuid); + } } for (const item of requestData) { @@ -50,6 +55,9 @@ export class HandlerService { password: item.password, level: item.level, }); + if (tempRes.isOk) { + this.internalService.addUserToInbound(item.tag, hashData.vlessUuid); + } response.push(tempRes); break; case 'vless': @@ -60,6 +68,9 @@ export class HandlerService { flow: item.flow, level: item.level, }); + if (tempRes.isOk) { + this.internalService.addUserToInbound(item.tag, hashData.vlessUuid); + } response.push(tempRes); break; case 'shadowsocks': @@ -71,6 +82,9 @@ export class HandlerService { ivCheck: item.ivCheck, level: item.level, }); + if (tempRes.isOk) { + this.internalService.addUserToInbound(item.tag, hashData.vlessUuid); + } response.push(tempRes); break; } @@ -138,13 +152,14 @@ export class HandlerService { data: IRemoveUserRequest, ): Promise> { try { - const { username } = data; + const { username, hashData } = data; const response: Array> = []; - const inboundsTags = this.xrayService.getSavedInboundsTags(); + const inboundsTags = this.internalService.getXtlsConfigInbounds(); for (const tag of inboundsTags) { const tempRes = await this.xtlsApi.handler.removeUser(tag, username); + this.internalService.removeUserFromInbound(tag, hashData.vlessUuid); response.push(tempRes); } diff --git a/src/modules/handler/interfaces/add-user.interface.ts b/src/modules/handler/interfaces/add-user.interface.ts index 04ec559..1765b77 100644 --- a/src/modules/handler/interfaces/add-user.interface.ts +++ b/src/modules/handler/interfaces/add-user.interface.ts @@ -1,6 +1,10 @@ import { CipherType } from '@remnawave/xtls-sdk/build/src/xray-protos/proxy/shadowsocks/config'; export interface TAddUserRequest { + hashData: { + vlessUuid: string; + prevVlessUuid?: string; + }; data: Array< | { cipherType: CipherType; diff --git a/src/modules/handler/interfaces/remove-user.interface.ts b/src/modules/handler/interfaces/remove-user.interface.ts index b9129fd..24ca0c9 100644 --- a/src/modules/handler/interfaces/remove-user.interface.ts +++ b/src/modules/handler/interfaces/remove-user.interface.ts @@ -1,3 +1,6 @@ export interface IRemoveUserRequest { username: string; + hashData: { + vlessUuid: string; + }; } diff --git a/src/modules/internal/internal.service.ts b/src/modules/internal/internal.service.ts index 4271583..290a759 100644 --- a/src/modules/internal/internal.service.ts +++ b/src/modules/internal/internal.service.ts @@ -1,9 +1,18 @@ +import ems from 'enhanced-ms'; + import { Injectable, Logger } from '@nestjs/common'; +import { HashedSet } from '@remnawave/hashed-set'; + +import { IHashPayload } from '@libs/contracts/constants'; + @Injectable() export class InternalService { private readonly logger = new Logger(InternalService.name); private xrayConfig: null | Record = null; + private emptyConfigHash: null | string = null; + private inboundsHashMap: Map = new Map(); + private xtlsConfigInbounds: string[] = []; constructor() {} @@ -19,4 +28,140 @@ export class InternalService { this.logger.debug('Setting new xray config'); this.xrayConfig = config; } + + public extractUsersFromConfig( + hashPayload: IHashPayload, + newConfig: Record, + ): void { + this.cleanup(); + + this.emptyConfigHash = hashPayload.emptyConfig; + this.xrayConfig = newConfig; + + this.logger.log( + `Starting user extraction from inbounds... Hash payload: ${JSON.stringify(hashPayload)}`, + ); + + const start = performance.now(); + if (newConfig.inbounds && Array.isArray(newConfig.inbounds)) { + for (const inbound of newConfig.inbounds) { + const inboundTag: string = inbound.tag; + + if (!inboundTag || !hashPayload.inbounds.find((item) => item.tag === inboundTag)) { + continue; + } + + const usersSet = new HashedSet(); + + if ( + inbound.settings && + inbound.settings.clients && + Array.isArray(inbound.settings.clients) + ) { + for (const client of inbound.settings.clients) { + if (client.id) { + usersSet.add(client.id); + } + } + } + + this.inboundsHashMap.set(inboundTag, usersSet); + } + + for (const [inboundTag, usersSet] of this.inboundsHashMap) { + this.xtlsConfigInbounds.push(inboundTag); + this.logger.log(`Inbound ${inboundTag} contains ${usersSet.size} user(s)`); + } + } + + const result = ems(performance.now() - start, { + extends: 'short', + includeMs: true, + }); + + this.logger.log(`User extraction completed in ${result ? result : '0ms'}`); + } + + public isNeedRestartCore(incomingHashPayload: IHashPayload): boolean { + const start = performance.now(); + try { + if (!this.emptyConfigHash) { + return true; + } + + if (incomingHashPayload.emptyConfig !== this.emptyConfigHash) { + this.logger.log('Detected changes in Xray Core base configuration'); + return true; + } + + if (incomingHashPayload.inbounds.length !== this.inboundsHashMap.size) { + this.logger.log('Number of Xray Core inbounds has changed'); + return true; + } + + for (const [inboundTag, usersSet] of this.inboundsHashMap) { + const incomingInbound = incomingHashPayload.inbounds.find( + (item) => item.tag === inboundTag, + ); + + if (!incomingInbound) { + this.logger.log( + `Inbound ${inboundTag} no longer exists in Xray Core configuration`, + ); + return true; + } + + if (usersSet.hash64String !== incomingInbound.hash) { + this.logger.log( + `User configuration changed for inbound ${inboundTag} (${usersSet.hash64String} → ${incomingInbound.hash})`, + ); + return true; + } + } + + this.logger.log('Xray Core configuration is up-to-date - no restart required'); + + return false; + } catch (error) { + this.logger.error(`Failed to check if Xray Core restart is needed: ${error}`); + return true; + } finally { + const result = ems(performance.now() - start, { + extends: 'short', + includeMs: true, + }); + this.logger.log(`Configuration hash check completed in ${result ? result : '0ms'}`); + } + } + + public addUserToInbound(inboundTag: string, user: string): void { + const usersSet = this.inboundsHashMap.get(inboundTag); + + if (!usersSet) { + return; + } + + usersSet.add(user); + } + + public removeUserFromInbound(inboundTag: string, user: string): void { + const usersSet = this.inboundsHashMap.get(inboundTag); + + if (!usersSet) { + return; + } + + usersSet.delete(user); + } + + public getXtlsConfigInbounds(): string[] { + return this.xtlsConfigInbounds; + } + + public cleanup(): void { + this.inboundsHashMap.clear(); + this.xtlsConfigInbounds = []; + this.xrayConfig = null; + this.emptyConfigHash = null; + } } diff --git a/src/modules/vision/vision.controller.ts b/src/modules/vision/vision.controller.ts index b382e2c..9368426 100644 --- a/src/modules/vision/vision.controller.ts +++ b/src/modules/vision/vision.controller.ts @@ -1,20 +1,20 @@ import { Body, Controller, Post, UseFilters, UseGuards } from '@nestjs/common'; -import { VISION_CONTROLLER, VISION_ROUTES } from '@libs/contracts/api/controllers/vision'; import { PortGuard } from '@common/guards/request-port-guard/request-port.guard'; import { HttpExceptionFilter } from '@common/exception/httpException.filter'; import { errorHandler } from '@common/helpers/error-handler.helper'; -import { XRAY_INTERNAL_API_PORT } from '@libs/contracts/constants'; import { OnPort } from '@common/decorators/port/port.decorator'; +import { VISION_CONTROLLER, VISION_ROUTES } from '@libs/contracts/api/controllers/vision'; +import { XRAY_INTERNAL_API_PORT } from '@libs/contracts/constants'; import { UnblockIpRequestDto, UnblockIpResponseDto } from './dtos/unblock-ip.dto'; import { BlockIpRequestDto, BlockIpResponseDto } from './dtos/block-ip.dto'; import { VisionService } from './vision.service'; -@Controller(VISION_CONTROLLER) @OnPort(XRAY_INTERNAL_API_PORT) @UseFilters(HttpExceptionFilter) @UseGuards(PortGuard) +@Controller(VISION_CONTROLLER) export class VisionController { constructor(private readonly visionService: VisionService) {} diff --git a/src/modules/xray-core/xray.controller.ts b/src/modules/xray-core/xray.controller.ts index 4343142..f4aa218 100644 --- a/src/modules/xray-core/xray.controller.ts +++ b/src/modules/xray-core/xray.controller.ts @@ -1,9 +1,11 @@ import { Body, Controller, Get, Ip, Logger, Post, UseFilters, UseGuards } from '@nestjs/common'; +import { HashPayload } from '@common/decorators/get-hash-payload/get-hash-payload'; import { HttpExceptionFilter } from '@common/exception/httpException.filter'; import { JwtDefaultGuard } from '@common/guards/jwt-guards/def-jwt-guard'; import { errorHandler } from '@common/helpers/error-handler.helper'; import { XRAY_CONTROLLER, XRAY_ROUTES } from '@libs/contracts/api/controllers/xray'; +import { IHashPayload } from '@libs/contracts/constants'; import { GetNodeHealthCheckResponseDto, @@ -26,8 +28,9 @@ export class XrayController { public async startXray( @Body() body: StartXrayRequestDto, @Ip() ip: string, + @HashPayload() hashPayload: IHashPayload | null, ): Promise { - const response = await this.xrayService.startXray(body, ip); + const response = await this.xrayService.startXray(body, ip, hashPayload); const data = errorHandler(response); return { diff --git a/src/modules/xray-core/xray.service.ts b/src/modules/xray-core/xray.service.ts index e88442a..73ce3d3 100644 --- a/src/modules/xray-core/xray.service.ts +++ b/src/modules/xray-core/xray.service.ts @@ -8,6 +8,7 @@ import pRetry from 'p-retry'; import semver from 'semver'; import { Injectable, Logger, OnApplicationBootstrap, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { InjectSupervisord } from '@remnawave/supervisord-nestjs'; import { InjectXtls } from '@remnawave/xtls-sdk-nestjs'; @@ -17,7 +18,7 @@ import { ISystemStats } from '@common/utils/get-system-stats/get-system-stats.in import { ICommandResponse } from '@common/types/command-response.type'; import { generateApiConfig } from '@common/utils/generate-api-config'; import { getSystemStats } from '@common/utils/get-system-stats'; -import { KNOWN_ERRORS, REMNAWAVE_NODE_KNOWN_ERROR } from '@libs/contracts/constants'; +import { IHashPayload, KNOWN_ERRORS, REMNAWAVE_NODE_KNOWN_ERROR } from '@libs/contracts/constants'; import { GetNodeHealthCheckResponseModel, @@ -32,6 +33,7 @@ const XRAY_PROCESS_NAME = 'xray' as const; @Injectable() export class XrayService implements OnApplicationBootstrap, OnModuleInit { private readonly logger = new Logger(XrayService.name); + private readonly disableHashedSetCheck: boolean; private readonly xrayPath: string; @@ -39,19 +41,21 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { private isXrayOnline: boolean = false; private systemStats: ISystemStats | null = null; private isXrayStartedProccesing: boolean = false; - private xtlsConfigInbounds: Array = []; private nodeVersion: string | null = null; constructor( @InjectXtls() private readonly xtlsSdk: XtlsApi, @InjectSupervisord() private readonly supervisordApi: SupervisordClient, private readonly internalService: InternalService, + private readonly configService: ConfigService, ) { this.xrayPath = '/usr/local/bin/xray'; this.xrayVersion = null; this.systemStats = null; this.isXrayStartedProccesing = false; this.nodeVersion = null; - this.xtlsConfigInbounds = []; + this.disableHashedSetCheck = this.configService.getOrThrow( + 'DISABLE_HASHED_SET_CHECK', + ); } async onModuleInit() { @@ -76,10 +80,24 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { public async startXray( config: Record, ip: string, + hashPayload: IHashPayload | null, ): Promise> { const tm = performance.now(); try { + if (!hashPayload) { + const errMessage = + 'Hash payload is null. Update Remnawave to version 2.1.0 or downgrade @remnawave/node to 2.0.0.'; + this.logger.error(errMessage); + + return { + isOk: false, + response: new StartXrayResponseModel(false, null, errMessage, null, { + version: this.nodeVersion, + }), + }; + } + if (this.isXrayStartedProccesing) { this.logger.warn('Request already in progress'); return { @@ -100,9 +118,25 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { const fullConfig = generateApiConfig(config); - this.xtlsConfigInbounds = await this.extractInboundTags(fullConfig); + if (this.isXrayOnline && !this.disableHashedSetCheck) { + const isNeedRestart = this.internalService.isNeedRestartCore(hashPayload); + if (!isNeedRestart) { + return { + isOk: true, + response: new StartXrayResponseModel( + true, + this.xrayVersion, + null, + this.systemStats, + { + version: this.nodeVersion, + }, + ), + }; + } + } - this.internalService.setXrayConfig(fullConfig); + this.internalService.extractUsersFromConfig(hashPayload, fullConfig); const xrayProcess = await this.restartXrayProcess(); @@ -228,7 +262,7 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { await this.killAllXrayProcesses(); this.isXrayOnline = false; - this.internalService.setXrayConfig({}); + this.internalService.cleanup(); return { isOk: true, @@ -408,16 +442,4 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { }; } } - - private async extractInboundTags(config: Record): Promise { - if (!config.inbounds || !Array.isArray(config.inbounds)) { - return []; - } - - return config.inbounds.map((inbound: { tag: string }) => inbound.tag); - } - - public getSavedInboundsTags(): string[] { - return this.xtlsConfigInbounds; - } }