diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml index 2b0b21b..e89ab44 100644 --- a/.github/workflows/build-dev.yml +++ b/.github/workflows/build-dev.yml @@ -31,10 +31,16 @@ jobs: image: - name: 'Main Image' tag: 'remnawave/node:dev' + dockerfile: 'Dockerfile' build_args: '' - name: 'SNI Image' tag: 'remnawave/node:sni-dev' + dockerfile: 'Dockerfile' build_args: 'UPSTREAM_REPO=kastov' + - name: 'Bun Image' + tag: 'remnawave/node:bun-dev' + dockerfile: 'Dockerfile.Bun' + build_args: '' steps: - name: Checkout uses: actions/checkout@v3 @@ -60,6 +66,7 @@ jobs: uses: docker/build-push-action@v3 with: context: . + file: ${{ matrix.image.dockerfile }} platforms: linux/amd64,linux/arm64 push: true build-args: ${{ matrix.image.build_args }} diff --git a/Dockerfile.Bun b/Dockerfile.Bun new file mode 100644 index 0000000..1286189 --- /dev/null +++ b/Dockerfile.Bun @@ -0,0 +1,44 @@ +FROM node:22-alpine AS build +WORKDIR /opt/app +ADD . . +RUN npm ci --legacy-peer-deps +RUN npm run build --omit=dev + +RUN npm ci --omit=dev --legacy-peer-deps \ + && npm cache clean --force + +FROM oven/bun:1.2.20-alpine AS base + +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 +COPY --from=build /opt/app/node_modules ./node_modules + + +RUN apk add --no-cache \ + curl \ + unzip \ + bash \ + git \ + python3 \ + py3-pip \ + && pip3 install --break-system-packages git+https://github.com/Supervisor/supervisor.git@4bf1e57cbf292ce988dc128e0d2c8917f18da9be \ + && curl -L ${XRAY_CORE_INSTALL_SCRIPT} | bash -s -- ${XRAY_CORE_VERSION} ${UPSTREAM_REPO} \ + && apk del curl git + +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 + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] + +CMD ["bun", "run", "start:bun"] diff --git a/package-lock.json b/package-lock.json index c99d214..63a241a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@remnawave/node", - "version": "2.1.0", + "version": "2.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@remnawave/node", - "version": "2.1.0", + "version": "2.1.1", "license": "AGPL-3.0-only", "dependencies": { "@cjs-exporter/execa": "9.5.2", diff --git a/package.json b/package.json index 1809630..ec759db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@remnawave/node", - "version": "2.1.0", + "version": "2.1.1", "description": "Remnawave Node", "private": false, "type": "commonjs", @@ -21,6 +21,7 @@ "start:dev": "NODE_ENV=development nest start --watch", "start:debug": "NODE_ENV=development nest start --debug --watch", "start:prod": "NODE_ENV=production node dist/src/main", + "start:bun": "NODE_ENV=production bun run dist/src/main", "prepare": "husky", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", diff --git a/src/modules/handler/handler.service.ts b/src/modules/handler/handler.service.ts index c67992c..f516923 100644 --- a/src/modules/handler/handler.service.ts +++ b/src/modules/handler/handler.service.ts @@ -30,11 +30,15 @@ export class HandlerService { const { data: requestData, hashData } = data; const response: Array> = []; - const inboundsTags = this.internalService.getXtlsConfigInbounds(); + for (const item of requestData) { + this.internalService.addXtlsConfigInbound(item.tag); + } - for (const tag of inboundsTags) { + for (const tag of this.internalService.getXtlsConfigInbounds()) { 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 { @@ -119,35 +123,6 @@ export class HandlerService { } } - public async getInboundUsers( - tag: string, - ): Promise> { - try { - // TODO: add a better way to return users (trojan, vless, etc) - const response = await this.xtlsApi.handler.getInboundUsers(tag); - - if (!response.isOk || !response.data) { - return { - isOk: false, - code: ERRORS.FAILED_TO_GET_INBOUND_USERS.code, - response: new GetInboundUsersResponseModel([]), - }; - } - - return { - isOk: true, - response: new GetInboundUsersResponseModel(response.data.users), - }; - } catch (error) { - this.logger.error(error); - return { - isOk: false, - code: ERRORS.FAILED_TO_GET_INBOUND_USERS.code, - response: new GetInboundUsersResponseModel([]), - }; - } - } - public async removeUser( data: IRemoveUserRequest, ): Promise> { @@ -155,10 +130,11 @@ export class HandlerService { const { username, hashData } = data; const response: Array> = []; - const inboundsTags = this.internalService.getXtlsConfigInbounds(); + for (const tag of this.internalService.getXtlsConfigInbounds()) { + this.logger.debug(`Removing user: ${username} from tag: ${tag}`); - for (const tag of inboundsTags) { const tempRes = await this.xtlsApi.handler.removeUser(tag, username); + this.internalService.removeUserFromInbound(tag, hashData.vlessUuid); response.push(tempRes); } @@ -192,6 +168,35 @@ export class HandlerService { } } + public async getInboundUsers( + tag: string, + ): Promise> { + try { + // TODO: add a better way to return users (trojan, vless, etc) + const response = await this.xtlsApi.handler.getInboundUsers(tag); + + if (!response.isOk || !response.data) { + return { + isOk: false, + code: ERRORS.FAILED_TO_GET_INBOUND_USERS.code, + response: new GetInboundUsersResponseModel([]), + }; + } + + return { + isOk: true, + response: new GetInboundUsersResponseModel(response.data.users), + }; + } catch (error) { + this.logger.error(error); + return { + isOk: false, + code: ERRORS.FAILED_TO_GET_INBOUND_USERS.code, + response: new GetInboundUsersResponseModel([]), + }; + } + } + public async getInboundUsersCount( tag: string, ): Promise> { diff --git a/src/modules/internal/internal.service.ts b/src/modules/internal/internal.service.ts index 290a759..bf7ff33 100644 --- a/src/modules/internal/internal.service.ts +++ b/src/modules/internal/internal.service.ts @@ -12,7 +12,7 @@ export class InternalService { private xrayConfig: null | Record = null; private emptyConfigHash: null | string = null; private inboundsHashMap: Map = new Map(); - private xtlsConfigInbounds: string[] = []; + private xtlsConfigInbounds: Set = new Set(); constructor() {} @@ -69,7 +69,7 @@ export class InternalService { } for (const [inboundTag, usersSet] of this.inboundsHashMap) { - this.xtlsConfigInbounds.push(inboundTag); + this.xtlsConfigInbounds.add(inboundTag); this.logger.log(`Inbound ${inboundTag} contains ${usersSet.size} user(s)`); } } @@ -138,6 +138,12 @@ export class InternalService { const usersSet = this.inboundsHashMap.get(inboundTag); if (!usersSet) { + this.logger.warn( + `Inbound ${inboundTag} not found in inboundsHashMap, creating new one`, + ); + + this.inboundsHashMap.set(inboundTag, new HashedSet([user])); + return; } @@ -152,15 +158,26 @@ export class InternalService { } usersSet.delete(user); + + if (usersSet.size === 0) { + this.xtlsConfigInbounds.delete(inboundTag); + this.inboundsHashMap.delete(inboundTag); + + this.logger.warn(`Inbound ${inboundTag} has no users, clearing inboundsHashMap.`); + } } - public getXtlsConfigInbounds(): string[] { + public getXtlsConfigInbounds(): Set { return this.xtlsConfigInbounds; } + public addXtlsConfigInbound(inboundTag: string): void { + this.xtlsConfigInbounds.add(inboundTag); + } + public cleanup(): void { this.inboundsHashMap.clear(); - this.xtlsConfigInbounds = []; + this.xtlsConfigInbounds.clear(); this.xrayConfig = null; this.emptyConfigHash = null; } diff --git a/src/modules/xray-core/xray.service.ts b/src/modules/xray-core/xray.service.ts index 73ce3d3..1bb8bd7 100644 --- a/src/modules/xray-core/xray.service.ts +++ b/src/modules/xray-core/xray.service.ts @@ -91,7 +91,7 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { this.logger.error(errMessage); return { - isOk: false, + isOk: true, response: new StartXrayResponseModel(false, null, errMessage, null, { version: this.nodeVersion, }), @@ -116,11 +116,21 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { this.isXrayStartedProccesing = true; - const fullConfig = generateApiConfig(config); - if (this.isXrayOnline && !this.disableHashedSetCheck) { - const isNeedRestart = this.internalService.isNeedRestartCore(hashPayload); - if (!isNeedRestart) { + const { isOk } = await this.xtlsSdk.stats.getSysStats(); + + let shouldRestart = false; + + if (isOk) { + shouldRestart = this.internalService.isNeedRestartCore(hashPayload); + } else { + this.isXrayOnline = false; + shouldRestart = true; + + this.logger.warn(`Xray Core health check failed, restarting...`); + } + + if (!shouldRestart) { return { isOk: true, response: new StartXrayResponseModel( @@ -136,6 +146,8 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { } } + const fullConfig = generateApiConfig(config); + this.internalService.extractUsersFromConfig(hashPayload, fullConfig); const xrayProcess = await this.restartXrayProcess(); @@ -152,7 +164,7 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { } return { - isOk: false, + isOk: true, response: new StartXrayResponseModel(false, null, xrayProcess.error, null, { version: this.nodeVersion, }),