mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-14 20:34:19 +08:00
371 lines
14 KiB
TypeScript
371 lines
14 KiB
TypeScript
import './protocols/registry'
|
|
import uci from 'uci'
|
|
import {XrayEnum} from '../../enum/xray'
|
|
import {ShuntUtils} from '../../utils/feature/shunt/section'
|
|
import {
|
|
Balancer, BurstObservatory,
|
|
DNS, DnsOutbound, DnsRule, DnsServer,
|
|
Freedom, Inbound, Log, Outbound, Route,
|
|
RouteRule, Rule, Socks, BlockOutbound
|
|
} from './models'
|
|
import {LuciFlied, PROXY_TYPE, ShuntTag} from '../../enum/hijpass'
|
|
import { CoreAdapter, LoadBalanceProxyNode, ProxyNode, RuleDTO } from '../../module/luci'
|
|
import {xrayRegistry} from './protocols/protocol'
|
|
import { LOAD_BALANCE_MEMBER_TAG_PREFIX, ProxyChainUtils } from '../../utils/feature/proxy/chain'
|
|
|
|
export const XrayUtils: CoreAdapter = {
|
|
genShuntConf: genConf,
|
|
genProxyConf
|
|
}
|
|
|
|
function genConf() {
|
|
const shuntSection = ShuntUtils.getShuntSection()
|
|
|
|
const dnsServerMap = new Map<string, DnsServer>()
|
|
const route = new Route<RouteRule | DnsRule>()
|
|
|
|
const dnsConf = shuntSection.dns
|
|
const dns = new DNS(transDnsStrategy(dnsConf.defaultStrategy), dnsConf.useCache === '1')
|
|
dnsConf.nodes.forEach(function (dnsNode) {
|
|
const server = new DnsServer(dnsNode.type, dnsNode.tag, dnsNode.server, Number(dnsNode.port))
|
|
dnsServerMap.set(dnsNode.tag, server)
|
|
})
|
|
|
|
const routeConf = shuntSection.route
|
|
const outbounds: Outbound[] = []
|
|
uci.sections(LuciFlied.CONF_NAME, LuciFlied.PROXY_NODE_TYPE, function (section) {
|
|
if (section.enabled === '1') {
|
|
const proxyNode = new Socks('127.0.0.1', Number(section.socks_port), transOutTag(section.name))
|
|
if (section.name === routeConf.defaultProxyNode) {
|
|
outbounds.unshift(proxyNode)
|
|
} else {
|
|
outbounds.push(proxyNode)
|
|
}
|
|
}
|
|
})
|
|
if (routeConf.defaultProxyNode === ShuntTag.DIRECT_OUTBOUND_TAG) {
|
|
outbounds.unshift(new Freedom(XrayEnum.DIRECT_OUTBOUND_TAG))
|
|
} else {
|
|
outbounds.push(new Freedom(XrayEnum.DIRECT_OUTBOUND_TAG))
|
|
}
|
|
outbounds.push(new BlockOutbound(XrayEnum.BLOCK_OUTBOUND_TAG))
|
|
outbounds.push(new DnsOutbound(XrayEnum.DNS_OUTBOUND_TAG))
|
|
|
|
const inbounds = [
|
|
new Inbound('tproxy', parseInt(shuntSection.shuntListenPort, 10), XrayEnum.TPROXY_INBOUND_TAG),
|
|
new Inbound('direct', parseInt(shuntSection.dnsListenPort, 10), XrayEnum.DNS_INBOUND_TAG, '::')
|
|
]
|
|
|
|
const dnsRules = [new DnsRule([XrayEnum.DNS_INBOUND_TAG], XrayEnum.DNS_OUTBOUND_TAG)]
|
|
const routeRules: RouteRule[] = []
|
|
const defaultDnsTmpl = dnsServerMap.get(dnsConf.defaultDnsNode)
|
|
if (!defaultDnsTmpl) {
|
|
throw new Error(_('Default global DNS "%s" does not exist').format(dnsConf.defaultDnsNode || ''))
|
|
}
|
|
const dnsInOutMap = new Map<string, string[]>()
|
|
|
|
routeConf.rules.forEach(function (rule) {
|
|
if (rule.enabled !== '1') return
|
|
const isIpVersionSplit = rule.ipVersionSplit === '1'
|
|
let splitDnsRuleDTO: RuleDTO = undefined
|
|
let splitDnsNeeded = false
|
|
|
|
ShuntUtils.getRouteRuleVariants(rule).forEach(function (variant) {
|
|
const ruleOutBoundTag = transOutTag(variant.proxyNode) || outbounds[0].tag
|
|
|
|
const luciRuleDTO = ShuntUtils.parseRuleList(variant.rule)
|
|
const hasDnsTarget = hasXrayDnsTargetRule(luciRuleDTO)
|
|
luciRuleDTO.ipVersion = variant.ipVersion
|
|
if (shouldSkipXrayRule(luciRuleDTO)) return
|
|
|
|
buildXrayRouteRules(variant.rule.name, ruleOutBoundTag, luciRuleDTO).forEach((routeRule) => {
|
|
routeRules.push(routeRule)
|
|
})
|
|
|
|
if (ruleOutBoundTag === XrayEnum.BLOCK_OUTBOUND_TAG) return
|
|
|
|
if (isIpVersionSplit) {
|
|
if (hasDnsTarget) {
|
|
splitDnsRuleDTO = splitDnsRuleDTO || luciRuleDTO
|
|
splitDnsNeeded = true
|
|
}
|
|
return
|
|
}
|
|
|
|
if (hasDnsTarget) {
|
|
const outboundTag = transOutTag(rule.dnsProxyNode) || ruleOutBoundTag
|
|
appendXrayDnsServer(rule, variant.rule.name, outboundTag, luciRuleDTO, dnsServerMap, defaultDnsTmpl, dns, dnsInOutMap)
|
|
}
|
|
})
|
|
|
|
if (isIpVersionSplit && splitDnsNeeded && splitDnsRuleDTO) {
|
|
const outboundTag = transOutTag(rule.dnsProxyNode)
|
|
if (!outboundTag) {
|
|
throw new Error(_('Route rule "%s" uses IPv4/IPv6 split and must specify a DNS exit node').format(rule.name))
|
|
}
|
|
appendXrayDnsServer(rule, rule.name, outboundTag, splitDnsRuleDTO, dnsServerMap, defaultDnsTmpl, dns, dnsInOutMap)
|
|
}
|
|
})
|
|
|
|
const defaultDnsServer = structuredClone(defaultDnsTmpl)
|
|
defaultDnsServer.tag = transDnsTag('default')
|
|
defaultDnsServer.skipFallback = false
|
|
dns.servers.push(defaultDnsServer)
|
|
|
|
dnsInOutMap.forEach((v, k) => dnsRules.push(new DnsRule(v, k)))
|
|
|
|
route.rules = route.rules.concat(dnsRules)
|
|
route.rules = route.rules.concat(routeRules)
|
|
|
|
const log = new Log(transXrayLogLevel(shuntSection.logLevel))
|
|
|
|
return {
|
|
log,
|
|
inbounds,
|
|
outbounds,
|
|
dns,
|
|
routing: route
|
|
}
|
|
}
|
|
|
|
function genProxyConf(config: ProxyNode): {} {
|
|
if (config.type === PROXY_TYPE.LOAD_BALANCE) {
|
|
return genLoadBalanceProxyConf(config as LoadBalanceProxyNode)
|
|
}
|
|
|
|
const outbound = xrayRegistry.build(config.type, config)
|
|
return buildBaseProxyConf(config, outbound)
|
|
}
|
|
|
|
function buildBaseProxyConf(config: ProxyNode, outbound: Outbound): {} {
|
|
const inbounds = buildProxyInbounds(config)
|
|
|
|
const outbounds = [outbound]
|
|
appendProxyChainOutbounds(config, outbound, outbounds)
|
|
|
|
return {
|
|
log: new Log(transXrayLogLevel(config.log_level)),
|
|
inbounds,
|
|
outbounds
|
|
}
|
|
}
|
|
|
|
function buildProxyInbounds(config: ProxyNode): Inbound[] {
|
|
const tproxyInbound = new Inbound('tproxy', Number(config.listen_port), 'tproxy-in')
|
|
tproxyInbound.sniffing = undefined
|
|
|
|
const socksInbound = new Inbound('direct', Number(config.socks_port), 'socks-in')
|
|
socksInbound.settings = {udp: true}
|
|
socksInbound.protocol = 'socks'
|
|
|
|
return [tproxyInbound, socksInbound]
|
|
}
|
|
|
|
function genLoadBalanceProxyConf(config: LoadBalanceProxyNode): {} {
|
|
const members = ProxyChainUtils.resolveLoadBalanceMembers(config)
|
|
const route = new Route<Rule>()
|
|
route.rules.push(new Rule(undefined, ['tproxy-in', 'socks-in'], XrayEnum.LOAD_BALANCE_OUTBOUND_TAG))
|
|
|
|
const balancer = new Balancer(XrayEnum.LOAD_BALANCE_OUTBOUND_TAG)
|
|
balancer.selector = [LOAD_BALANCE_MEMBER_TAG_PREFIX]
|
|
const fallback = members.find(member => member.name === config.fallback_tag) || members[0]
|
|
balancer.fallbackTag = fallback.tag
|
|
|
|
const strategy = config.strategy || 'leastLoad'
|
|
balancer.strategy.type = strategy
|
|
if (strategy === 'leastLoad') {
|
|
if (config.strategy_expected) balancer.strategy.settings.expected = parseInt(config.strategy_expected, 10)
|
|
if (config.strategy_max_rtt) balancer.strategy.settings.maxRTT = config.strategy_max_rtt
|
|
if (config.strategy_tolerance) balancer.strategy.settings.tolerance = parseFloat(config.strategy_tolerance)
|
|
} else {
|
|
balancer.strategy.settings = undefined
|
|
}
|
|
route.balancers.push(balancer)
|
|
|
|
const burstObservatory = new BurstObservatory(
|
|
config.probe_url || undefined,
|
|
config.probe_connectivity || undefined,
|
|
config.probe_interval || undefined,
|
|
config.probe_timeout || undefined,
|
|
config.probe_sampling ? parseInt(config.probe_sampling, 10) : undefined,
|
|
config.probe_http_method || undefined
|
|
)
|
|
burstObservatory.subjectSelector = [LOAD_BALANCE_MEMBER_TAG_PREFIX]
|
|
|
|
const outbounds = members.map(member => new Socks('127.0.0.1', member.socksPort, member.tag))
|
|
|
|
return {
|
|
log: new Log(transXrayLogLevel(config.log_level)),
|
|
inbounds: buildProxyInbounds(config),
|
|
outbounds,
|
|
routing: route,
|
|
burstObservatory
|
|
}
|
|
}
|
|
|
|
function transXrayLogLevel(logLevel: string) {
|
|
return logLevel === 'warn' ? 'warning' : logLevel
|
|
}
|
|
|
|
function appendProxyChainOutbounds(config: ProxyNode, outbound: Outbound, outbounds: Outbound[]) {
|
|
const upstream = ProxyChainUtils.resolveUpstreamProxy(config)
|
|
if (!upstream) return
|
|
|
|
setDialerProxy(outbound, upstream.tag)
|
|
outbounds.push(new Socks('127.0.0.1', upstream.socksPort, upstream.tag))
|
|
}
|
|
|
|
function setDialerProxy(outbound: Outbound, tag: string) {
|
|
outbound.streamSettings = outbound.streamSettings || {}
|
|
outbound.streamSettings.sockopt = outbound.streamSettings.sockopt || {}
|
|
outbound.streamSettings.sockopt.dialerProxy = tag
|
|
outbound.streamSettings.sockopt.domainStrategy = 'UseIP'
|
|
}
|
|
|
|
function transDnsTag(name: string) {
|
|
return name + '-dns'
|
|
}
|
|
|
|
function shouldSkipXrayRule(luciRuleDTO: RuleDTO) {
|
|
if (luciRuleDTO.isEmpty('geo')) return true
|
|
return luciRuleDTO.containRuleSet()
|
|
&& !luciRuleDTO.containDomain()
|
|
&& !luciRuleDTO.containIp()
|
|
&& !luciRuleDTO.containGeo()
|
|
}
|
|
|
|
function hasXrayDnsTargetRule(luciRuleDTO: RuleDTO) {
|
|
return luciRuleDTO.containDomain() || luciRuleDTO.geoSite.length > 0
|
|
}
|
|
|
|
function appendXrayDnsServer(rule: any, ruleName: string, outboundTag: string, luciRuleDTO: RuleDTO, dnsServerMap: Map<string, DnsServer>, defaultDnsTmpl: DnsServer, dns: DNS, dnsInOutMap: Map<string, string[]>) {
|
|
const dnsTag = transDnsTag(ruleName)
|
|
if (dnsInOutMap.has(outboundTag)) {
|
|
dnsInOutMap.get(outboundTag).push(dnsTag)
|
|
} else {
|
|
dnsInOutMap.set(outboundTag, [dnsTag])
|
|
}
|
|
const dnsTmpl = rule.dnsNode ? dnsServerMap.get(rule.dnsNode) : defaultDnsTmpl
|
|
if (!dnsTmpl) {
|
|
throw new Error(_('Route rule "%s" DNS node "%s" does not exist').format(rule.name || ruleName, rule.dnsNode || ''))
|
|
}
|
|
const server = structuredClone(dnsTmpl)
|
|
server.tag = dnsTag
|
|
server.queryStrategy = transDnsStrategy(rule.dnsStrategy)
|
|
server.domains = buildXrayDomains(luciRuleDTO)
|
|
dns.servers.push(server)
|
|
}
|
|
|
|
function buildXrayRouteRules(name: string, outboundTag: string, luciRuleDTO: RuleDTO): RouteRule[] {
|
|
const routeRule = new RouteRule(name, outboundTag)
|
|
routeRule.network = luciRuleDTO.network
|
|
routeRule.protocol = transRouteProtocol(luciRuleDTO.protocol)
|
|
|
|
let allPorts: string[] = luciRuleDTO.port.map(p => String(p))
|
|
if (luciRuleDTO.portRange && luciRuleDTO.portRange.length > 0) {
|
|
allPorts = allPorts.concat(luciRuleDTO.portRange)
|
|
}
|
|
routeRule.port = allPorts.join(',')
|
|
|
|
routeRule.domain = buildXrayDomains(luciRuleDTO)
|
|
routeRule.domain = routeRule.domain.length > 0 ? routeRule.domain : undefined
|
|
routeRule.ip = buildXrayIps(luciRuleDTO)
|
|
routeRule.ip = routeRule.ip.length > 0 ? routeRule.ip : undefined
|
|
|
|
const rules: RouteRule[] = []
|
|
const ipVersionRange = transXrayIpVersion(luciRuleDTO.ipVersion)
|
|
if (routeRule.domain) {
|
|
const domainRule = structuredClone(routeRule)
|
|
if (ipVersionRange) domainRule.ip = [ipVersionRange]
|
|
else domainRule.ip = undefined
|
|
rules.push(domainRule)
|
|
}
|
|
if (routeRule.ip) {
|
|
const ipRule = structuredClone(routeRule)
|
|
ipRule.domain = undefined
|
|
rules.push(ipRule)
|
|
}
|
|
if (!routeRule.domain && !routeRule.ip) {
|
|
if (ipVersionRange) routeRule.ip = [ipVersionRange]
|
|
rules.push(routeRule)
|
|
}
|
|
return rules
|
|
}
|
|
|
|
function buildXrayDomains(luciRuleDTO: RuleDTO): string[] {
|
|
const domainArrays = [
|
|
addPrefixToElements(luciRuleDTO.domain, 'full:'),
|
|
addPrefixToElements(luciRuleDTO.domainSuffix, 'domain:'),
|
|
luciRuleDTO.domainKeyword,
|
|
addPrefixToElements(luciRuleDTO.domainRegex, 'regexp:'),
|
|
luciRuleDTO.geoSite
|
|
]
|
|
return ([] as string[]).concat(...domainArrays.filter(a => Array.isArray(a) && a.length > 0))
|
|
}
|
|
|
|
function buildXrayIps(luciRuleDTO: RuleDTO): string[] {
|
|
const ipArrays = [
|
|
filterIpRulesByVersion(luciRuleDTO.ipCidr, luciRuleDTO.ipVersion),
|
|
luciRuleDTO.geoIp
|
|
]
|
|
return ([] as string[]).concat(...ipArrays.filter(a => Array.isArray(a) && a.length > 0))
|
|
}
|
|
|
|
function filterIpRulesByVersion(rules: string[], ipVersion?: 4 | 6): string[] {
|
|
if (!ipVersion || !Array.isArray(rules)) return rules || []
|
|
return rules.filter((rule) => {
|
|
const normalized = rule.replace(/^!/, '')
|
|
if (normalized.startsWith('geoip:') || normalized.startsWith('ext:')) return true
|
|
if (normalized.indexOf(':') >= 0) return ipVersion === 6
|
|
if (/^\d{1,3}(\.\d{1,3}){3}(\/\d+)?$/.test(normalized)) return ipVersion === 4
|
|
return true
|
|
})
|
|
}
|
|
|
|
function transXrayIpVersion(ipVersion?: 4 | 6) {
|
|
if (ipVersion === 4) return '0.0.0.0/0'
|
|
if (ipVersion === 6) return '::/0'
|
|
return undefined
|
|
}
|
|
|
|
function transOutTag(tag: string) {
|
|
if (tag === ShuntTag.BLOCK_OUTBOUND_TAG) return XrayEnum.BLOCK_OUTBOUND_TAG
|
|
if (tag === ShuntTag.DIRECT_OUTBOUND_TAG) return XrayEnum.DIRECT_OUTBOUND_TAG
|
|
return tag
|
|
}
|
|
|
|
function transDnsStrategy(strategy: string) {
|
|
switch (strategy) {
|
|
case '4':
|
|
return 'UseIPv4'
|
|
case '6':
|
|
return 'UseIPv6'
|
|
case '64':
|
|
case '46':
|
|
return 'UseIP'
|
|
default:
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
function transRouteProtocol(protocol: string[]) {
|
|
if (!protocol) return undefined
|
|
const res = protocol.map((pro: string) => {
|
|
switch (pro) {
|
|
case 'http':
|
|
case 'tls':
|
|
case 'quic':
|
|
case 'bittorrent':
|
|
return pro
|
|
default:
|
|
throw new Error('Unsupported xray rule protocol: ' + pro)
|
|
}
|
|
}).filter(p => p !== undefined)
|
|
return res.length === 0 ? undefined : res
|
|
}
|
|
|
|
function addPrefixToElements(arr: string[], prefix: string) {
|
|
if (!Array.isArray(arr)) return []
|
|
return arr.map(item => prefix + item)
|
|
}
|