Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Security Policy

## Reporting a Vulnerability

Use this section to tell people how to report a vulnerability.

Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.
Comment on lines +1 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Placeholder SECURITY.md included in feature PR

This file contains GitHub's default template text ("Tell them where to go, how often they can expect to get an update...") and is unrelated to the host-tags migration. It should either be filled in with a real vulnerability-disclosure policy or removed from this PR and tracked separately.

20 changes: 12 additions & 8 deletions libs/contract/commands/hosts/create.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,23 @@ export namespace CreateHostCommand {
})
.nullable(),
),
tag: z
tags: z
.optional(
z
.string()
.regex(
/^[A-Z0-9_:]+$/,
'Tag can only contain uppercase letters, numbers, underscores and colons',
.array(
z
.string()
.regex(
/^[A-Z0-9_:]+$/,
'Tag can only contain uppercase letters, numbers, underscores and colons',
)
.max(32, 'Tag must be less than 32 characters'),
)
.max(32, 'Tag must be less than 32 characters')
.nullable(),
.max(10, 'Maximum 10 tags allowed')
.default([]),
)
.describe(
'Optional. Host tag for categorization. Max 32 characters, uppercase letters, numbers, underscores and colons are allowed.',
'Optional. Host tags for categorization. Max 10 tags, each max 32 characters, uppercase letters, numbers, underscores and colons are allowed.',
),
isHidden: z.optional(z.boolean().default(false)),
overrideSniFromAddress: z.optional(z.boolean().default(false)),
Expand Down
19 changes: 11 additions & 8 deletions libs/contract/commands/hosts/update.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,22 @@ export namespace UpdateHostCommand {
})
.nullable(),
),
tag: z
tags: z
.optional(
z
.string()
.regex(
/^[A-Z0-9_:]+$/,
'Tag can only contain uppercase letters, numbers, underscores and colons',
.array(
z
.string()
.regex(
/^[A-Z0-9_:]+$/,
'Tag can only contain uppercase letters, numbers, underscores and colons',
)
.max(32, 'Tag must be less than 32 characters'),
)
.max(32, 'Tag must be less than 32 characters')
.nullable(),
.max(10, 'Maximum 10 tags allowed'),
)
.describe(
'Optional. Host tag for categorization. Max 32 characters, uppercase letters, numbers, underscores and colons are allowed.',
'Optional. Host tags for categorization. Max 10 tags, each max 32 characters, uppercase letters, numbers, underscores and colons are allowed.',
),
isHidden: z.optional(z.boolean()),
overrideSniFromAddress: z.optional(z.boolean()),
Expand Down
2 changes: 1 addition & 1 deletion libs/contract/models/hosts.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const HostsSchema = z.object({
}),

serverDescription: z.string().max(30).nullable(),
tag: z.string().nullable(),
tags: z.array(z.string()).default([]),
isHidden: z.boolean().default(false),
overrideSniFromAddress: z.boolean().default(false),
keepSniBlank: z.boolean().default(false),
Expand Down
2 changes: 1 addition & 1 deletion libs/contract/models/resolved-proxy-config.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export const SecurityVariantSchema = z.discriminatedUnion('security', [

export const ProxyEntryMetadataSchema = z.object({
uuid: z.string().uuid(),
tag: z.string().nullable(),
tags: z.array(z.string()).default([]),
excludeFromSubscriptionTypes: z.array(z.nativeEnum(SUBSCRIPTION_TEMPLATE_TYPE)),
inboundTag: z.string(),
configProfileUuid: z.string().uuid().nullable(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- AlterTable: migrate host tag (single string) to tags (array of strings)

-- Step 1: Add new tags column with default empty array
ALTER TABLE "hosts" ADD COLUMN "tags" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[];

-- Step 2: Migrate existing tag data to tags array
UPDATE "hosts" SET "tags" = ARRAY["tag"] WHERE "tag" IS NOT NULL;

-- Step 3: Drop old tag column
ALTER TABLE "hosts" DROP COLUMN "tag";
2 changes: 1 addition & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ model Hosts {
keepSniBlank Boolean @default(false) @map("keep_sni_blank")
excludeFromSubscriptionTypes String[] @default([]) @map("exclude_from_subscription_types")

tag String? @map("tag")
tags String[] @default([]) @map("tags")
isHidden Boolean @default(false) @map("is_hidden")

overrideSniFromAddress Boolean @default(false) @map("override_sni_from_address")
Expand Down
2 changes: 1 addition & 1 deletion src/modules/hosts/entities/hosts.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class HostsEntity implements Hosts {
serverDescription: null | string;
allowInsecure: boolean;

tag: null | string;
tags: string[];
isHidden: boolean;

overrideSniFromAddress: boolean;
Expand Down
2 changes: 1 addition & 1 deletion src/modules/hosts/hosts.converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const entityToModel = (entity: HostsEntity): Hosts => {
shuffleHost: entity.shuffleHost,
mihomoX25519: entity.mihomoX25519,

tag: entity.tag,
tags: entity.tags,
isHidden: entity.isHidden,

overrideSniFromAddress: entity.overrideSniFromAddress,
Expand Down
4 changes: 2 additions & 2 deletions src/modules/hosts/models/host.response.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class HostResponseModel {
public shuffleHost: boolean;
public mihomoX25519: boolean;

public tag: null | string;
public tags: string[];
public isHidden: boolean;

public overrideSniFromAddress: boolean;
Expand Down Expand Up @@ -69,7 +69,7 @@ export class HostResponseModel {
this.shuffleHost = data.shuffleHost;
this.mihomoX25519 = data.mihomoX25519;

this.tag = data.tag;
this.tags = data.tags;
this.isHidden = data.isHidden;

this.overrideSniFromAddress = data.overrideSniFromAddress;
Expand Down
17 changes: 10 additions & 7 deletions src/modules/hosts/repositories/hosts.repository.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Prisma } from '@prisma/client';
import { sql } from 'kysely';

import { IReorderHost } from 'src/modules/hosts/interfaces/reorder-host.interface';

Expand Down Expand Up @@ -93,6 +94,7 @@ export class HostsRepository implements ICrud<HostsEntity> {
| 'excludedInternalSquads'
| 'excludeFromSubscriptionTypes'
| 'finalMask'
| 'tags'
>,
): Promise<HostsEntity[]> {
const list = await this.prisma.tx.hosts.findMany({
Expand Down Expand Up @@ -245,14 +247,15 @@ export class HostsRepository implements ICrud<HostsEntity> {
}

public async getAllHostTags(): Promise<string[]> {
const result = await this.prisma.tx.hosts.findMany({
select: {
tag: true,
},
distinct: ['tag'],
});
const result = await this.qb.kysely
.selectFrom('hosts')
.select(sql<string>`unnest(tags)`.as('tag'))
.distinct()
.where('tags', 'is not', null)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant null-check on a NOT NULL column

tags is declared TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[] in the migration, so this WHERE tags IS NOT NULL predicate will always be true and adds no filtering. unnest(ARRAY[]::TEXT[]) already returns zero rows for empty arrays, so there is no functional regression from removing it — but it may mislead future readers into thinking null values are possible.

Or simply omit the .where(...) clause entirely, matching the actual semantics.

.orderBy('tag')
.execute();

return result.map((host) => host.tag).filter((tag) => tag !== null);
return result.map((value) => value.tag);
}

public async addNodesToHost(hostUuid: string, nodes: string[]): Promise<boolean> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,9 @@ export class XrayJsonGeneratorService {
}

if (useHostTagAsTag) {
return hosts.map((h) => this.buildOutbound(h, h.metadata.tag || h.finalRemark));
return hosts.map((h) =>
this.buildOutbound(h, h.metadata.tags[0] || h.finalRemark),
);
}

const proxyTag = tagPrefix ?? 'proxy';
Expand Down Expand Up @@ -404,13 +406,17 @@ export class XrayJsonGeneratorService {
case 'sameTagAsRecipient':
return candidates.filter(
(h) =>
h.metadata.tag && host.metadata.tag && h.metadata.tag === host.metadata.tag,
h.metadata.tags.length > 0 &&
host.metadata.tags.length > 0 &&
h.metadata.tags.some((t) => host.metadata.tags.includes(t)),
);

case 'tagRegex': {
const regex = this.parseRegex(selector.pattern);
if (!regex) return [];
return candidates.filter((h) => h.metadata.tag && regex.test(h.metadata.tag));
return candidates.filter(
(h) => h.metadata.tags.length > 0 && h.metadata.tags.some((t) => regex.test(t)),
);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export type SecurityVariant = TlsSecurity | RealitySecurity | NoneSecurity;

export interface IProxyEntryMetadata {
uuid: string;
tag: string | null;
tags: string[];
excludeFromSubscriptionTypes: TSubscriptionTemplateType[];
inboundTag: string;
configProfileUuid: string | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ export class ResolveProxyConfigService {
},
metadata: {
uuid: inputHost.uuid,
tag: inputHost.tag,
tags: inputHost.tags,
excludeFromSubscriptionTypes: inputHost.excludeFromSubscriptionTypes,
inboundTag: inputHost.inboundTag,
configProfileUuid: inputHost.configProfileUuid,
Expand Down Expand Up @@ -676,7 +676,7 @@ export class ResolveProxyConfigService {
},
metadata: {
uuid: '00000000-0000-0000-0000-000000000000',
tag: null,
tags: [],
excludeFromSubscriptionTypes: [],
inboundTag: '',
configProfileUuid: null,
Expand Down