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;
- }
}