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
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.detectIndentation": false,
"[javascript]": {
"editor.tabSize": 2
}
}
2 changes: 1 addition & 1 deletion biz/webui/htdocs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
</head>
<body style="overscroll-behavior-x: none;">
<div id="container" class="main"></div>
<script src="js/index.js?v=2.10.1"></script>
<script src="js/index.js?v=2.10.2"></script>
</body>
</html>
4 changes: 2 additions & 2 deletions biz/webui/htdocs/js/index.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion biz/webui/htdocs/src/js/protocols.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var PROTOCOLS = [
'plugin',
'sniCallback',
'host',
'dns',
'xhost',
'proxy',
'xproxy',
Expand Down Expand Up @@ -115,7 +116,7 @@ var allRules = allInnerRules;
var groupRules = [
['Map Local', ['file://', 'xfile://', 'tpl://', 'xtpl://', 'rawfile://', 'xrawfile://']],
['Map Remote', ['https://', 'http://', 'wss://', 'ws://', 'tunnel://']],
['DNS Spoofing', ['host://', 'xhost://', 'proxy://', 'xproxy://', 'http-proxy://', 'xhttp-proxy://',
['DNS Spoofing', ['host://', 'dns://', 'xhost://', 'proxy://', 'xproxy://', 'http-proxy://', 'xhttp-proxy://',
'https-proxy://', 'xhttps-proxy://', 'socks://', 'xsocks://', 'pac://']],
['Rewrite Request', ['urlParams://', 'pathReplace://','sniCallback://', 'method://', 'tlsOptions://', 'reqHeaders://', 'forwardedFor://',
'ua://', 'auth://', 'cache://', 'referer://', 'reqType://', 'reqCharset://', 'reqCookies://',
Expand Down
5 changes: 5 additions & 0 deletions biz/webui/htdocs/src/js/rules-mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ CodeMirror.defineMode('rules', function () {
function isHost(str) {
return /^x?hosts?:\/\//.test(str);
}
function isDns(str) {
return /^dns:\/\//.test(str);
}
function isHead(str) {
return /^head:\/\//.test(str);
}
Expand Down Expand Up @@ -220,6 +223,8 @@ CodeMirror.defineMode('rules', function () {
if (!type && ch == '/' && pre == '/') {
if (isHost(str)) {
type = 'number js-number js-type';
} else if (isDns(str)) {
type = 'number js-number js-type';
} else if (isHead(str)) {
type = 'header js-head js-type';
} else if (isWeinre(str)) {
Expand Down
43 changes: 43 additions & 0 deletions docs/docs/rules/dns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# dns
对匹配的请求使用指定的 DNS 服务器进行域名解析,支持传统 DNS 和安全 DNS (DoH)。

## 规则语法
``` txt
pattern dns://dnsServer [filters...]
```

| 参数 | 描述 | 详细文档 |
| ------- | ------------------------------------------------------------ | ------------------------- |
| pattern | 匹配请求 URL 的表达式 | [匹配模式文档](./pattern) |
| value | DNS 服务器地址,支持两种形式:<br/>• **传统 DNS**:IP 或 IP:端口<br/>• **安全 DNS (DoH)**:`https://` 开头的 DoH 服务 URL | [操作指令文档](./operation) |
| filters | 可选过滤器,支持匹配:<br/>• 请求URL/方法/头部/内容<br/>• 响应状态码/头部 | [过滤器文档](./filters) |

## value 参数说明

| 类型 | 格式 | 示例 |
| ------------------ | ------------ | ----------------------------------------------------------------------- |
| 传统 DNS | IP 或 IP:端口 | `8.8.8.8`、`8.8.8.8:53`、`[::1]:53` |
| **安全 DNS (DoH)** | HTTPS URL | `https://dns.google/dns-query`、`https://cloudflare-dns.com/dns-query` |

## 配置示例

``` txt
# 使用传统 DNS
example.com dns://8.8.8.8
*.google.com dns://8.8.8.8:53

# 使用安全 DNS (DoH) - Google
*.google.com dns://https://dns.google/dns-query

# 使用安全 DNS (DoH) - Cloudflare
example.com dns://https://cloudflare-dns.com/dns-query
```

## 与 host 的关系

- **host**:直接映射域名到 IP,相当于修改 hosts 文件
- **dns**:指定使用哪个 DNS 服务器来解析域名

当请求同时匹配 `host` 和 `dns` 时:
- 若 `host` 直接映射到 IP,则无需 DNS 解析,`dns` 规则不生效
- 若 `host` 映射到域名(CNAME),则使用 `dns` 规则指定的服务器解析该域名
43 changes: 43 additions & 0 deletions docs/en/docs/rules/dns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# dns
Use the specified DNS server for domain resolution of matching requests. Supports traditional DNS and secure DNS (DoH).

## Rule Syntax
``` txt
pattern dns://dnsServer [filters...]
```

| Parameters | Description | Detailed Documentation |
| ------- | ------------------------------------------------------------ | ------------------------- |
| pattern | Expression to match the request URL | [Match Pattern Documentation](./pattern) |
| value | DNS server address. Two formats supported:<br/>• **Traditional DNS**: IP or IP:port<br/>• **Secure DNS (DoH)**: DoH service URL starting with `https://` | [Operation Instruction Documentation](./operation) |
| filters | Optional filters, supporting matching:<br/>• Request URL/Method/Headers/Content<br/>• Response Status Code/Headers | [Filters Documentation](./filters) |

## Value Parameter

| Type | Format | Examples |
| ------------------ | ------------ | ----------------------------------------------------------------------- |
| Traditional DNS | IP or IP:port | `8.8.8.8`, `8.8.8.8:53`, `[::1]:53` |
| **Secure DNS (DoH)** | HTTPS URL | `https://dns.google/dns-query`, `https://cloudflare-dns.com/dns-query` |

## Configuration Example

``` txt
# Traditional DNS
example.com dns://8.8.8.8
*.google.com dns://8.8.8.8:53

# Secure DNS (DoH) - Google
*.google.com dns://https://dns.google/dns-query

# Secure DNS (DoH) - Cloudflare
example.com dns://https://cloudflare-dns.com/dns-query
```

## Relationship with host

- **host**: Directly maps domain to IP, equivalent to modifying hosts file
- **dns**: Specifies which DNS server to use for domain resolution

When a request matches both `host` and `dns`:
- If `host` maps directly to an IP, no DNS resolution is needed; `dns` rule has no effect
- If `host` maps to a domain (CNAME), the domain is resolved using the server specified by the `dns` rule
86 changes: 72 additions & 14 deletions lib/rules/dns.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ var dnsServer = config.dnsServer;
var dnsOverHttps = config.dnsOverHttps;
var resolve6 = config.resolve6;
var dnsResolve = config.dnsResolve;
var dnsResolve4= config.dnsResolve4;
var dnsResolve4 = config.dnsResolve4;
var dnsResolve6 = config.dnsResolve6;
var dnsOptional = config.dnsOptional;
var dnsFallback = config.dnsFallback;
var dnsCache = {};
var callbacks = {};
var TIMEOUT = 10000;
var CACHE_TIME = dnsCacheTime >= 0 ? dnsCacheTime : 60000;
var CACHE_TIME = dnsCacheTime >= 0 ? dnsCacheTime : 5000;
var MAX_CACHE_TIME = Math.max(CACHE_TIME * 3, 600000);
var IPV6_OPTIONS = {family: 6};
var IPV4_FIRST = {verbatim: false};
var IPV6_OPTIONS = { family: 6 };
var IPV4_FIRST = { verbatim: false };

if (dnsOverHttps) {
dnsOverHttps += (dnsOverHttps.indexOf('?') === -1 ? '?' : '&') + 'name=';
Expand All @@ -38,8 +38,11 @@ function getIpFromAnswer(data) {
}
}

function lookDnsOverHttps(hostname, callback) {
util.request({ url: dnsOverHttps + hostname }, function (err, data) {
function lookDnsOverHttps(hostname, callback, dohUrl) {
var url = dohUrl
? dohUrl + (dohUrl.indexOf('?') === -1 ? '?' : '&') + 'name=' + hostname
: dnsOverHttps + hostname;
util.request({ url: url }, function (err, data) {
if (err) {
return callback(err);
}
Expand Down Expand Up @@ -108,7 +111,7 @@ function lookupDNS(hostname, callback) {
};
try {
if (dnsOverHttps) {
return lookDnsOverHttps(hostname, handleCallback);
return lookDnsOverHttps(hostname, handleCallback);
}
if (fn === 'lookup') {
useLookup = true;
Expand Down Expand Up @@ -157,23 +160,78 @@ function getDefaultIp(type) {
}

function checkCacheData(host) {
return host && (config.ipv6Only ? host.ipv6Only : !host.ipv6Only) && (config.dnsOrder === host.order) ? host: null;
return host && (config.ipv6Only ? host.ipv6Only : !host.ipv6Only) && (config.dnsOrder === host.order) ? host : null;
}

module.exports = function lookup(hostname, callback, allowDnsCache) {
var host = allowDnsCache ? dnsCache[hostname] : null;
function getOptCacheKey(hostname, opts) {
return hostname + '\n' + (opts.dnsServer || opts.dnsOverHttps);
}

function resolveWithSpecifiedDns(hostname, callback, opts, cacheKey) {
var timer = setTimeout(function () {
if (callback) {
callback(util.TIMEOUT_ERR);
}
}, TIMEOUT);
var handleCallback = function (err, ip, type) {
clearTimeout(timer);
if (!err) {
ip = Array.isArray(ip) ? ip[0] : ip;
ip = ip || getDefaultIp(type);
dnsCache[cacheKey] = {
ip: ip,
hostname: hostname,
ipv6Only: config.ipv6Only,
order: config.dnsOrder,
time: Date.now()
};
}
if (callback) {
callback(err, err ? undefined : ip);
}
};
if (opts.dnsOverHttps) {
return lookDnsOverHttps(hostname, handleCallback, opts.dnsOverHttps);
}
if (opts.dnsServer) {
var resolver = new dns.Resolver();
resolver.setServers([opts.dnsServer]);
var fn = config.ipv6Only ? 'resolve6' : 'resolve4';
return resolver[fn](hostname, handleCallback);
}
clearTimeout(timer);
if (callback) {
callback(new Error('Invalid DNS options'));
}
}

module.exports = function lookup(hostname, callback, allowDnsCache, options) {
if (net.isIP(hostname)) {
return callback(null, hostname);
}
var hasOpts = options && (options.dnsServer || options.dnsOverHttps);
var cacheKey = hasOpts ? getOptCacheKey(hostname, options) : hostname;
var host = allowDnsCache ? dnsCache[cacheKey] : null;
var cacheTime;
if (checkCacheData(host)) {
cacheTime = Date.now() - host.time;
}
if (host && cacheTime < MAX_CACHE_TIME) {
callback(null, host.ip);
if (cacheTime > CACHE_TIME) {
lookupDNS(host.hostname);
if (hasOpts) {
resolveWithSpecifiedDns(hostname, null, options, cacheKey);
} else {
lookupDNS(hostname);
}
}
return host.ip;
}
lookupDNS(hostname, function (err, ip) {
err ? lookupDNS(hostname, callback) : callback(err, ip);
});
if (hasOpts) {
resolveWithSpecifiedDns(hostname, callback, options, cacheKey);
} else {
lookupDNS(hostname, function (err, ip) {
err ? lookupDNS(hostname, callback) : callback(err, ip);
});
}
};
4 changes: 4 additions & 0 deletions lib/rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ exports.getProxy = function (url, req, callback) {
var proxy;
var pacRule;
var host = rules.getHost(req, pRules, fRules, hRules);
var dns = rules.getDns(req, pRules, fRules, hRules);
if (dns) {
reqRules.dns = dns;
}
var hostValue = util.getMatcherValue(host) || '';
if (config.multiEnv || req._headerRulesFirst) {
proxy = resolveRulesFromList([pRules, hRules, rules, fRules], 'resolveProxy', req, hostValue);
Expand Down
1 change: 1 addition & 0 deletions lib/rules/protocols.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ var protocols = [
'G',
'style',
'host',
'dns',
'rule',
'pipe',
'weinre',
Expand Down
78 changes: 77 additions & 1 deletion lib/rules/rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -2119,13 +2119,40 @@ proto.resolveHost = function (
this.lookupHost(req, callback);
};

function parseDnsValue(value) {
if (!value || typeof value !== 'string') {
return null;
}
value = value.trim();
if (util.isUrl(value)) {
return { dnsOverHttps: value };
}
var ip;
if (IP_WITH_PORT_RE.test(value)) {
ip = RegExp.$1;
return net.isIP(ip) ? { dnsServer: value } : null;
}
if (IPV4_RE.test(value)) {
ip = RegExp.$1;
var port = RegExp.$2;
return net.isIP(ip) ? { dnsServer: port ? ip + ':' + port : ip } : null;
}
return net.isIP(value) ? { dnsServer: value } : null;
}

proto.lookupHost = function (req, callback) {
req.curUrl = util.formatUrl(util.setProtocol(req.curUrl));
var options = parseUrl(req.curUrl);
var dnsOpts;
var dnsRule = req.rules && req.rules.dns;
if (dnsRule) {
dnsOpts = parseDnsValue(util.getMatcherValue(dnsRule));
}
lookup(
options.hostname,
callback,
allowDnsCache && !this.resolveDisable(req).dnsCache
allowDnsCache && !this.resolveDisable(req).dnsCache,
dnsOpts
);
};

Expand All @@ -2143,6 +2170,15 @@ var ignoreHost = function (req, rulesMgr, filter) {
);
};

var ignoreDns = function (req, rulesMgr, filter) {
if (!rulesMgr) {
return false;
}
var ignore = rulesMgr.resolveFilter(req);
extend(filter, ignore);
return ignore.dns || ignore['ignore|dns'];
};

function getHostFromList(list, req) {
var host;
for (var i = 0, len = list.length; i < len; i++) {
Expand Down Expand Up @@ -2188,6 +2224,46 @@ proto.getHost = function (req, pluginRulesMgr, rulesFileMgr, headerRulesMgr) {
return host;
};

function getDnsFromList(list, req) {
var dnsRule;
for (var i = 0, len = list.length; i < len; i++) {
var mgr = list[i];
var _dns = mgr && mgr.getRule(req, mgr._rules.dns, mgr._values);
if (_dns) {
if (util.isImportant(_dns)) {
return _dns;
}
dnsRule = dnsRule || _dns;
}
}
return dnsRule;
}

proto.getDns = function (req, pluginRulesMgr, rulesFileMgr, headerRulesMgr) {
var curUrl = util.formatUrl(util.setProtocol(req.curUrl));
req.curUrl = curUrl;
var filter = {};
var filterDns =
ignoreDns(req, this, filter) ||
ignoreDns(req, pluginRulesMgr, filter) ||
ignoreDns(req, rulesFileMgr, filter) ||
ignoreDns(req, headerRulesMgr, filter);
if (filterDns) {
return;
}
var list;
if (config.multiEnv) {
list = [pluginRulesMgr, headerRulesMgr, this, rulesFileMgr];
} else {
list = [pluginRulesMgr, this, rulesFileMgr, headerRulesMgr];
}
var dnsRule = getDnsFromList(list, req);
if (!dnsRule || util.exactIgnore(filter, dnsRule)) {
return;
}
return dnsRule;
};

proto.resolveFilter = function (req) {
var filter = resolveProps.call(this, req, this._rules.filter, this._values);
var ignore = resolveProps.call(this, req, this._rules.ignore, this._values, true);
Expand Down