diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 050fc07a..740523eb 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -6,3 +6,4 @@ export * from './mask-string'; export * from './md5'; export * from './superjson'; export * from './truncate-header.util'; +export * from './parse-int-range.util'; diff --git a/src/common/utils/parse-int-range.util.ts b/src/common/utils/parse-int-range.util.ts new file mode 100644 index 00000000..80cf1f14 --- /dev/null +++ b/src/common/utils/parse-int-range.util.ts @@ -0,0 +1,26 @@ +export interface IntRange { + from: number | null; + to: number | null; +} + +export function parseIntRangeUtil(value: string | number | undefined): IntRange { + if (value === undefined || value === '') return { from: null, to: null }; + + const [left, right = left] = String(value).split('-', 2); + const from = parseIntPart(left); + const to = parseIntPart(right); + + if (from !== null && to !== null && from > to) { + return { from: to, to: from }; + } + + return { from, to }; +} + +function parseIntPart(part: string): number | null { + if (part === '') return null; + + const parsed = Number(part); + + return Number.isInteger(parsed) && parsed >= 0 ? parsed : null; +} diff --git a/src/modules/subscription-template/generators/mihomo.generator.service.ts b/src/modules/subscription-template/generators/mihomo.generator.service.ts index 67232d43..e359f36d 100644 --- a/src/modules/subscription-template/generators/mihomo.generator.service.ts +++ b/src/modules/subscription-template/generators/mihomo.generator.service.ts @@ -3,7 +3,7 @@ import yaml from 'yaml'; import { Injectable, Logger } from '@nestjs/common'; -import { isNonEmptyObject } from '@common/utils'; +import { isNonEmptyObject, parseIntRangeUtil } from '@common/utils'; import { FINGERPRINTS } from '@libs/contracts/constants'; import { SubscriptionTemplateService } from '@modules/subscription-template/subscription-template.service'; @@ -41,12 +41,25 @@ interface Hysteria2FinalMask { bbrProfile?: string; congestion?: string; }; - udp?: Array<{ - type?: string; - settings?: { password?: string }; - }>; } +interface Hysteria2Mask { + type: 'salamander'; + settings: { + packetSize?: string | number; + password: string; + }; +} + +interface Hysteria2PacketSizeFields { + 'obfs-max-packet-size': number; + 'obfs-min-packet-size': number; +} + +type Hysteria2ObfsFields = + | ({ obfs: 'gecko'; 'obfs-password': string } & Partial) + | { obfs: 'salamander'; 'obfs-password': string }; + interface ProxyNode { [key: string]: unknown; alpn?: string[]; @@ -646,13 +659,25 @@ export class MihomoGeneratorService { private buildHysteria2ObfsFields( finalMask: Record | null, - ): Record { - const password = (finalMask as Hysteria2FinalMask | null)?.udp?.find( - (m) => m?.type === 'salamander', - )?.settings?.password; + ): Hysteria2ObfsFields | Record { + const mask = this.findHysteria2Mask(finalMask); - if (!password) return {}; - return { obfs: 'salamander', 'obfs-password': password }; + if (!mask) return {}; + + const { password, packetSize } = mask.settings; + + if (!packetSize) { + return { obfs: 'salamander', 'obfs-password': password }; + } + + const { from, to } = parseIntRangeUtil(packetSize); + + return { + obfs: 'gecko', + 'obfs-password': password, + ...(from && { 'obfs-min-packet-size': from }), + ...(to && { 'obfs-max-packet-size': to }), + }; } private buildHysteria2TlsFields(host: ResolvedProxyConfig): Record { @@ -678,4 +703,43 @@ export class MihomoGeneratorService { } } } + + private findHysteria2Mask(finalMask: Record | null): Hysteria2Mask | null { + const udp = finalMask?.udp; + + if (!Array.isArray(udp)) return null; + + return udp.find((mask): mask is Hysteria2Mask => this.isHysteria2Mask(mask)) ?? null; + } + + private isHysteria2Mask(value: unknown): value is Hysteria2Mask { + if ( + typeof value !== 'object' || + value === null || + !('type' in value) || + !('settings' in value) + ) { + return false; + } + + if (value.type !== 'salamander') { + return false; + } + + const settings: unknown = value.settings; + if (typeof settings !== 'object' || settings === null || !('password' in settings)) { + return false; + } + + if (typeof settings.password !== 'string' || settings.password.length === 0) { + return false; + } + + const packetSize: unknown = 'packetSize' in settings ? settings.packetSize : undefined; + return ( + packetSize === undefined || + typeof packetSize === 'string' || + typeof packetSize === 'number' + ); + } }