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
43 changes: 42 additions & 1 deletion frontend/src/stores/subscribes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,42 @@ import {
migrateSubscribes,
} from '@/utils'

const collectDomainResolverTags = (value: any): string[] => {
if (!value || typeof value !== 'object') return []
if (Array.isArray(value)) return value.flatMap(collectDomainResolverTags)

return Object.entries(value).flatMap(([key, val]) => {
if (key === 'domain_resolver' && typeof val === 'string' && val) {
return [val]
}
return collectDomainResolverTags(val)
})
}

const collectReferencedDnsServers = (
outbounds: Recordable[],
dnsServers: Recordable[],
): Recordable[] => {
const serverMap = new Map<string, Recordable>(
dnsServers.flatMap((server) => (server.tag ? [[server.tag, server]] : [])),
)
const result = new Map<string, Recordable>()
const pending = outbounds.flatMap(collectDomainResolverTags)

while (pending.length > 0) {
const tag = pending.shift()!
if (result.has(tag)) continue

const server = serverMap.get(tag)
if (!server) continue

result.set(tag, server)
pending.push(...collectDomainResolverTags(server))
}
Comment on lines +47 to +56

return Array.from(result.values())
}

export const useSubscribesStore = defineStore('subscribes', () => {
const subscribes = ref<App.Subscription[]>([])

Expand Down Expand Up @@ -86,6 +122,7 @@ export const useSubscribesStore = defineStore('subscribes', () => {
const userInfo: Recordable = {}
let body = ''
let proxies: Record<string, any>[] = []
let dnsServers: Recordable[] = []

if (s.type === 'Manual') {
body = await ReadFile(s.path)
Expand Down Expand Up @@ -124,7 +161,9 @@ export const useSubscribesStore = defineStore('subscribes', () => {
}

if (isValidSubJson(body)) {
proxies = JSON.parse(body).outbounds
const config = JSON.parse(body)
proxies = config.outbounds
dnsServers = config.dns?.servers || []
} else if (isValidSubYAML(body)) {
proxies = parse(body).proxies
} else if (isValidBase64(body)) {
Expand Down Expand Up @@ -187,6 +226,7 @@ export const useSubscribesStore = defineStore('subscribes', () => {
const { proxies: _proxies, subscription } = await fn(proxies, s)

Object.assign(s, subscription)
s.dnsServers = collectReferencedDnsServers(_proxies, dnsServers)
s.proxies = _proxies.map(({ tag, type }) => {
// Keep the original ID value of the proxy unchanged
const id = s.proxies.find((v) => v.tag === tag)?.id || sampleID()
Expand Down Expand Up @@ -284,6 +324,7 @@ export const useSubscribesStore = defineStore('subscribes', () => {
response: {},
},
proxies: [],
dnsServers: [],
script: DefaultSubscribeScript,
}
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/types/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ declare namespace App {
response: Recordable
}
script: string
dnsServers: Recordable[]
// Not Config
updating?: boolean
}
Expand Down
146 changes: 98 additions & 48 deletions frontend/src/utils/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,19 @@ const generateInbounds = (inbounds: App.Inbound[]) => {
const generateOutbounds = async (outbounds: App.Outbound[]) => {
const result: Recordable[] = []
const SubscriptionCache: Recordable<any[]> = {}
const SubscriptionDnsServersCache: Recordable<Recordable[]> = {}
const proxiesSet = new Set<any>()
const builtInProxiesSet = new Set<string>()
const dnsServersMap = new Map<string, Recordable>()

const subscribesStore = useSubscribesStore()
const collectSubscriptionDnsServers = (subId: string) => {
SubscriptionDnsServersCache[subId]?.forEach((server) => {
if (server.tag && !dnsServersMap.has(server.tag)) {
dnsServersMap.set(server.tag, server)
}
})
}

for (const outbound of outbounds) {
const _outbound: Recordable = {
Expand Down Expand Up @@ -125,18 +134,21 @@ const generateOutbounds = async (outbounds: App.Outbound[]) => {
const subStr = await ReadFile(sub.path)
const proxies = JSON.parse(subStr)
SubscriptionCache[subId] = proxies
SubscriptionDnsServersCache[subId] = sub.dnsServers || []
}
}
if (proxy.type === 'Subscription') {
_outbound.outbounds.push(
...SubscriptionCache[subId]!.map((v) => v.tag).filter((tag) => isTagMatching(tag)),
)
SubscriptionCache[subId]!.forEach((v) => proxiesSet.add(v))
collectSubscriptionDnsServers(subId)
} else {
const _proxy = SubscriptionCache[subId]!.find((v) => v.tag === proxy.tag)
if (_proxy && isTagMatching(_proxy.tag)) {
_outbound.outbounds.push(_proxy.tag)
proxiesSet.add(_proxy)
collectSubscriptionDnsServers(subId)
}
}
}
Expand All @@ -148,7 +160,24 @@ const generateOutbounds = async (outbounds: App.Outbound[]) => {
result.push(...proxiesSet)
result.push(...Array.from(builtInProxiesSet).map((v) => ({ type: v, tag: v })))

return result
return {
outbounds: result,
dnsServers: Array.from(dnsServersMap.values()),
}
}

const normalizeSubscriptionDnsServers = (
dnsServers: Recordable[],
generatedOutbounds: Recordable[],
) => {
const outboundTags = new Set(generatedOutbounds.map((outbound) => outbound.tag))
return dnsServers.map((server) => {
const normalized = { ...server }
if (normalized.detour && !outboundTags.has(normalized.detour)) {
delete normalized.detour
}
return normalized
})
}

const generateRoute = (route: App.Route, inbounds: App.Inbound[], outbounds: App.Outbound[], dns: App.Dns) => {
Expand Down Expand Up @@ -230,6 +259,7 @@ const generateDns = (
rule_set: App.ProfileRuleSet[],
inbounds: App.Inbound[],
outbounds: App.Outbound[],
subscriptionDnsServers: Recordable[] = [],
) => {
const getOutbound = (id: string) => outbounds.find((v) => v.id === id)
const getDnsServer = (id: string) => dns.servers.find((v) => v.id === id)?.tag
Expand All @@ -240,63 +270,72 @@ const generateDns = (
if (dns.client_subnet) {
extra.client_subnet = dns.client_subnet
}
return {
servers: dns.servers.flatMap((server) => {
const extra: Recordable = {}
const servers: Recordable[] = dns.servers.flatMap((server) => {
const extra: Recordable = {}
if (
[
DnsServer.Local,
DnsServer.Tcp,
DnsServer.Udp,
DnsServer.Tls,
DnsServer.Quic,
DnsServer.Https,
DnsServer.H3,
DnsServer.Dhcp,
].includes(server.type as any)
) {
if (server.detour) {
const outbound = getOutbound(server.detour)
if (outbound?.type !== Outbound.Direct) {
extra.detour = outbound?.tag
}
}
server.domain_resolver && (extra.domain_resolver = getDnsServer(server.domain_resolver))
if (
[
DnsServer.Local,
DnsServer.Tcp,
DnsServer.Udp,
DnsServer.Tls,
DnsServer.Quic,
DnsServer.Https,
DnsServer.H3,
DnsServer.Dhcp,
].includes(server.type as any)
) {
if (server.detour) {
const outbound = getOutbound(server.detour)
if (outbound?.type !== Outbound.Direct) {
extra.detour = outbound?.tag
}
server.server_port && (extra.server_port = Number(server.server_port))
extra.server = server.server
if ([DnsServer.Https, DnsServer.H3].includes(server.type as any)) {
server.path && (extra.path = server.path)
}
server.domain_resolver && (extra.domain_resolver = getDnsServer(server.domain_resolver))
if (
[
DnsServer.Tcp,
DnsServer.Udp,
DnsServer.Tls,
DnsServer.Quic,
DnsServer.Https,
DnsServer.H3,
].includes(server.type as any)
) {
server.server_port && (extra.server_port = Number(server.server_port))
extra.server = server.server
if ([DnsServer.Https, DnsServer.H3].includes(server.type as any)) {
server.path && (extra.path = server.path)
}
}
}
if (server.type === DnsServer.Hosts) {
extra.path = server.hosts_path.reduce((p, c) => p.concat(c.split(',')), [] as string[])
extra.predefined = Object.entries(server.predefined).reduce(
(p, [k, v]) => ({ ...p, [k]: v.split(',') }),
{},
)
} else if (server.type === DnsServer.Dhcp) {
server.interface && (extra.interface = server.interface)
} else if (server.type === DnsServer.FakeIP) {
server.inet4_range && (extra.inet4_range = server.inet4_range)
server.inet6_range && (extra.inet6_range = server.inet6_range)
}
return {
tag: server.tag,
type: server.type,
...extra,
}
}),
}
if (server.type === DnsServer.Hosts) {
extra.path = server.hosts_path.reduce((p, c) => p.concat(c.split(',')), [] as string[])
extra.predefined = Object.entries(server.predefined).reduce(
(p, [k, v]) => ({ ...p, [k]: v.split(',') }),
{},
)
} else if (server.type === DnsServer.Dhcp) {
server.interface && (extra.interface = server.interface)
} else if (server.type === DnsServer.FakeIP) {
server.inet4_range && (extra.inet4_range = server.inet4_range)
server.inet6_range && (extra.inet6_range = server.inet6_range)
}
return {
tag: server.tag,
type: server.type,
...extra,
}
})
const serverTags = new Set(servers.map((server) => server.tag))
subscriptionDnsServers.forEach((server) => {
if (server.tag && !serverTags.has(server.tag)) {
servers.push(server)
serverTags.add(server.tag)
}
})

return {
servers,
rules: dns.rules.flatMap((rule) => {
if (rule.type === RuleType.InsertionPoint || !rule.enable) {
return []
Expand Down Expand Up @@ -385,14 +424,25 @@ export const generateConfig = async (
} = options

const profile = deepClone(originalProfile)
const generatedOutbounds = await generateOutbounds(profile.outbounds)
const subscriptionDnsServers = normalizeSubscriptionDnsServers(
generatedOutbounds.dnsServers,
generatedOutbounds.outbounds,
)
// step 1
let config: Recordable = {
log: profile.log,
experimental: generateExperimental(profile.experimental, profile.outbounds),
inbounds: generateInbounds(profile.inbounds),
outbounds: await generateOutbounds(profile.outbounds),
outbounds: generatedOutbounds.outbounds,
route: generateRoute(profile.route, profile.inbounds, profile.outbounds, profile.dns),
dns: generateDns(profile.dns, profile.route.rule_set, profile.inbounds, profile.outbounds),
dns: generateDns(
profile.dns,
profile.route.rule_set,
profile.inbounds,
profile.outbounds,
subscriptionDnsServers,
),
}

// adapt to stable branch
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/utils/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export const migrateSubscribes = async (
subscribe.customProxy = ''
needSync = true
}
if (!subscribe.dnsServers) {
subscribe.dnsServers = []
needSync = true
}
})

if (needSync) await save()
Expand Down