-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactoring: DNS App #169
Refactoring: DNS App #169
Conversation
// Client is the interface for DNS client. | ||
type Client interface { | ||
// Server is the interface for Name Server. | ||
type Server interface { | ||
// Name of the Client. | ||
Name() string | ||
|
||
// QueryIP sends IP queries to its configured server. | ||
QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error) | ||
QueryIP(ctx context.Context, domain string, clientIP net.IP, option IPOption) ([]net.IP, error) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
原Client
接口更名为Server
接口。Server
不再保存clientIP
,而是在调用QueryIP
时经过参数clientIP
传递,以更强调该对象是远程服务器的抽象的概念。
原Client
接口的几个实现是:LocalNameServer
,ClassicNameServer
, DOHNameServer
。这几个NameServer类实现的却是Client接口,容易令人混淆。
由于本PR里Client
有了新的定义,因此将原Client
更名为Server
接口。不选择NameServer
是因为该名字被proto文件里的Config类占用了。
app/dns/nameserver.go
Outdated
// NewServer creates a name server object according to the network destination url. | ||
func NewServer(dest net.Destination, dispatcher routing.Dispatcher) (Server, error) { | ||
if address := dest.Address; address.Family().IsDomain() { | ||
u, err := url.Parse(address.Domain()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
switch { | ||
case u.String() == "localhost": | ||
return NewLocalNameServer(), nil | ||
case u.Scheme == "https": // DOH Remote mode | ||
return NewDoHNameServer(u, dispatcher) | ||
case u.Scheme == "https+local": // DOH Local mode | ||
return NewDoHLocalNameServer(u), nil | ||
} | ||
} | ||
|
||
if option.IPv6Enable { | ||
return s.client.LookupIPv6(domain) | ||
if dest.Network == net.Network_Unknown { | ||
dest.Network = net.Network_UDP | ||
} | ||
if dest.Network == net.Network_UDP { // UDP classic DNS mode | ||
return NewClassicNameServer(dest, dispatcher), nil | ||
} | ||
return nil, newError("No available name server could be created from ", dest).AtWarning() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
server.go
中的New -> addNameServer
函数实现被移入了NewServer
中并得到了简化,以增强可读性和维护性。
// Client is the interface for DNS client. | ||
type Client struct { | ||
server Server | ||
clientIP net.IP | ||
domains []string | ||
expectIPs []*router.GeoIPMatcher | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
新的Client
类将原server.go
中与单个Client
相关的代码抽象了出来,且成员布局与DNS JSON配置中的ServerObject
一致。也即每个ServerObject
的信息会对应一个Client
对象。
server.go
中与config.NameServers
, config.NameServer
有关的处理逻辑,queryIPTimeout
与match
函数的实现被移入了Client
中。
每个Client中独立保存了clientIP
,因此未来可以支持为每个ServerObject
独立配置一个Client IP(在TG群里看过这个需求)
app/dns/hosts.go
Outdated
func (h *StaticHosts) lookup(domain string, option IPOption, maxDepth int) []net.Address { | ||
switch addrs := h.lookupInternal(domain, option); { | ||
case len(addrs) == 0: // Not recorded in static hosts, return nil | ||
return nil | ||
case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Try to unwrap domain | ||
if maxDepth > 0 { | ||
unwrapped := h.lookup(addrs[0].Domain(), option, maxDepth-1) | ||
if unwrapped != nil { | ||
return unwrapped | ||
} | ||
} | ||
return addrs | ||
default: // IP record found, return a non-nil IP array | ||
return filterIP(addrs, option) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
server.go
中的lookupStatic
迁移到了hosts.go
中,并作为StaticHosts
提供的公开接口使用。
同时严谨定义StaticHosts.Lookup
的行为:
Lookup
可能返回nil,此时代表域名没有被收录进StaticHosts
中。Lookup
可能返回一个且最多一个域名,此时代表查询到了一个域名替换。Lookup
会默认以最多5次展开这个域名,返回展开到的IP或最后无法继续展开的域名。Lookup
可能返回零至多个IP。返回的IP数组一定不为nil
,返回空数组代表Lookup
有查询结果,但只是被全过滤掉了。lookupInternal
返回了多于一个domain
也会在这里被处理,经过filterIP
后被过滤掉,仍然只有IP返回。关于StaticHosts仍有一段内容需要更新,但不适合放在该重构PR里,会在后面PR补上。
希望新增两个配置:
|
目前的 Refactoring 在 App 这边功能上已经有了支持,细节问题有:
这个可以在App端这样实现:Client 维护一个新成员 以上是两者在 App 端实现方案的大致设计,Config 端如何设计按你的想法来(面向用户一端应该需要更有经验的人来给提案)。 |
个人认为,DNS log 是否打印 clientIP 无所谓,因为目前已有对应的 idx。其余实现怎么方便怎么来。 IPOption 似乎是 freedom outbound 用于设置 domainStrategy 的,默认为 AsIs,另外可选项为 UseIP、UseIPv4、UseIPv6。我觉得可以仿照这个作为选项。默认什么都不设置(或设置 UseIP),同时查询 A 和 AAAA;设置 UseIPv4,查询 A 记录;设置 UseIPv6 查询 AAAA 记录;用 "dns": {
"servers": [{
"address": "https+local://223.5.5.5/dns-query",
"domains": ["geosite:cn", "geosite:icloud"],
"expectIPs": ["geoip:cn"],
"queryStrategy": "UseIPv4",
"clientip": "123.123.123.123"
}]
} 优先使用 DNS 内设置的 clientIP,否则使用统一设置的 clientIP(如果外层设置了的话)。这样应该能在不破坏已有配置可用性的情况下增加更多细致的选项。 |
// DNS is a DNS rely server. | ||
type DNS struct { | ||
sync.Mutex | ||
tag string | ||
hosts *StaticHosts | ||
clients []*Client | ||
|
||
domainMatcher strmatcher.IndexMatcher | ||
matcherInfos []DomainMatcherInfo | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
因为nameserver.go
中实现了Server
接口,原server.go
中的Server
类直接改名为DNS
并迁移到dns.go
中。这样与JSON 配置的对应关系也更清晰些。
原Server
类中的许多成员也合并到了Client
类中,降低了耦合度。这避免了如 #146 这样的问题,其中ipMatcher
本应是和Client
一比一绑定的关系,但却分成了两个数组,又因两个数组的元素对应不一致而出错。
app/dns/dns.go
Outdated
for _, endpoint := range config.NameServers { | ||
features.PrintDeprecatedFeatureWarning("simple DNS server") | ||
client, err := NewSimpleClient(ctx, endpoint, clientIP) | ||
if err != nil { | ||
return nil, newError("failed to create client").Base(err) | ||
} | ||
clients = append(clients, client) | ||
} | ||
|
||
for _, ns := range config.NameServer { | ||
clientIdx := len(clients) | ||
updateDomain := func(domainRule strmatcher.Matcher, originalRuleIdx int) error { | ||
midx := domainMatcher.Add(domainRule) | ||
matcherInfos[midx] = DomainMatcherInfo{ | ||
clientIdx: uint16(clientIdx), | ||
domainRuleIdx: uint16(originalRuleIdx), | ||
} | ||
return nil | ||
} | ||
client, err := NewClient(ctx, ns, clientIP, geoipContainer, updateDomain) | ||
if err != nil { | ||
return nil, newError("failed to create client").Base(err) | ||
} | ||
clients = append(clients, client) | ||
} | ||
|
||
if len(clients) == 0 { | ||
clients = append(clients, NewLocalDNSClient()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DNS.New()
通过以下几种函数创建Client对象:
NewClient(...)
以新版NameServer
配置创建;NewSimpleClient(...)
以旧版Endpoint
配置创建;NewLocalDNSClient()
创建一个维护LocalNameServer
的Client。(实现于nameserver_local.go
中)
// Static host lookup | ||
switch addrs := s.hosts.Lookup(domain, option); { | ||
case addrs == nil: // Domain not recorded in static host | ||
break | ||
case len(addrs) == 0: // Domain recorded, but no valid IP returned (e.g. IPv4 address with only IPv6 enabled) | ||
return nil, dns.ErrEmptyResponse | ||
case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Domain replacement | ||
newError("domain replaced: ", domain, " -> ", addrs[0].Domain()).WriteToLog() | ||
domain = addrs[0].Domain() | ||
default: // Successfully found ip records in static host | ||
newError("returning ", len(addrs), " IPs for domain ", domain).WriteToLog() | ||
return toNetIP(addrs) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Static Hosts 匹配行为改动。参考 #169 (comment) ,对匹配结果addrs
的处理流程如下:
addrs
为nil
时,代表 Static Hosts 查询失败,domain
未被收录,继续向下使用 Name Servers 查询。addrs
不为nil
,但为空数组时,代表 Static Hosts 收录了domain
,但返回了空结果。此时直接返回dns.ErrEmptyResponse
,不继续向下查询 Name Servers。addrs
仅有一个结果且为域名时,代表 Static Hosts 返回了一个替换域名(像是CNAME?),用新域名替换原domain
继续向下查询 Name Servers。- 默认情况下,均认为 Staic Hosts 返回了 IP 记录,尝试将
addrs
转换为 IP 数组并返回。若转换失败则返回对应的错误。
其中主要的行为变化是条件 2,将nil
与空数组作了区分处理。非nil
的空数组可能是因为 Static Hosts 中有添加一条 IPv4 记录,但在只有 option.IPv6Enable
为真的情况下被过滤了。这种时候应该认为 Static Hosts 有对应 domain
的记录,但没有收录 IPv6 IP,因此对 IPv6 的查询请求应该返回 Empty Response。这解决了 v2ray/domain-list-community#487 (comment) 中碰到的问题。
// Name servers lookup | ||
errs := []error{} | ||
ctx := session.ContextWithInbound(context.Background(), &session.Inbound{Tag: s.tag}) | ||
for _, client := range s.sortClients(domain) { | ||
ips, err := client.QueryIP(ctx, domain, option) | ||
if len(ips) > 0 { | ||
return ips, nil | ||
} | ||
if err != nil { | ||
newError("failed to lookup ip for domain ", domain, " at server ", client.Name()).Base(err).WriteToLog() | ||
errs = append(errs, err) | ||
} | ||
if err != context.Canceled && err != context.DeadlineExceeded && err != errExpectedIPNonMatch { | ||
return nil, err | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Name Servers匹配行为改动。原来的先优先查询,后默认轮询
被整合为了以下流程:
- 按照优先级顺序对
Clients
进行排序。也即待查的Clients
总数不变,且不会重复查询。 - 按照排序后的
Clients
对其进行轮询。
流程 1 解决了 #156 (comment) 中的问题:由于现在查询的是 Clients
经过优先级重排后的结果,因此自然一个 Client
只会被使用一次。
流程 2 解决了 #156 (comment) 的其中一个问题:由于现在是统一在重排后的 Clients
上作轮询了,优先查询与默认轮询的处理逻辑自然地被合并了,而不是使用两份一样的代码。
对于 #156 的本来问题:如何控制允许DNS继续查询的逻辑,这个重构PR没有改动。这部分应留给原 Issue 继续进行讨论,得出共识方案后由他人开PR修正。
func (s *DNS) sortClients(domain string) []*Client { | ||
clients := make([]*Client, 0, len(s.clients)) | ||
clientUsed := make([]bool, len(s.clients)) | ||
clientNames := make([]string, 0, len(s.clients)) | ||
domainRules := []string{} | ||
|
||
// Priority domain matching | ||
for _, match := range s.domainMatcher.Match(domain) { | ||
info := s.matcherInfos[match] | ||
client := s.clients[info.clientIdx] | ||
domainRule := client.domains[info.domainRuleIdx] | ||
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx)) | ||
if clientUsed[info.clientIdx] { | ||
continue | ||
} | ||
clientUsed[info.clientIdx] = true | ||
clients = append(clients, client) | ||
clientNames = append(clientNames, client.Name()) | ||
} | ||
|
||
// Default round-robin query | ||
for idx, client := range s.clients { | ||
if clientUsed[idx] { | ||
continue | ||
} | ||
clientUsed[idx] = true | ||
clients = append(clients, client) | ||
clientNames = append(clientNames, client.Name()) | ||
} | ||
|
||
if len(domainRules) > 0 { | ||
newError("domain ", domain, " matches following rules: ", domainRules).AtDebug().WriteToLog() | ||
} | ||
if len(clientNames) > 0 { | ||
newError("domain ", domain, " will use DNS in order: ", clientNames).AtDebug().WriteToLog() | ||
} | ||
return clients | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sortClients
的主要逻辑:
- 先处理优先匹配,后对
Clients
遍历一遍 - 使用
clientUsed
数组判断是否重复使用
从而构造一个按优先级排序后的 Clients
数组。
这里对于 domainRules
与 matchedDNS
日志赋予了新的定义:
domainRules
数组会打印出匹配的所有规则,尽管某些规则可能对应了同一个 DNS。matchedDNS
变为clientNames
数组,打印重排后将用于查询的所有 DNS 顺序。若采用了 per-DNS client IP, 这里有必要为 DNS 额外记录clientIP
以消歧义。
@kslr @Loyalsoldier 等到周五新版本发布后再考虑这个PR?这样避免合进去后可能会出的问题,也方便跟后面的DNS更新算到一起去。 |
好的,另外在什么情况下会需要 queryStrategy |
就是只希望 DNS 查询 A 记录或者 AAAA 记录 |
这有什么具体场景吗 |
没有 IPv6 网络,没必要查 AAAA 记录,仅此而已 😂 |
提个请求 假设将 V2Ray 放在一台具有公网 IP 的服务器上作为 DNS 服务器使用,那么能否将连接 V2Ray 用户的 IP "自动添加" 到 |
|
@Vigilans 那 access.log 中的用户的公网 IP 是怎么获取的,不能直接用吗 虽然用户大多都是内网 IP,但是家庭宽带连接带有 V2Ray 的服务器的时候的 IP 是公网 IP 呀 |
@CalmLog 我的access.log是这样的(透明代理):
|
我感觉你没明白我的意思 假设本机 V2Ray 开个的任意门协议,然后监听 53 端口用来接收 DNS 查询的流量,假设本机 IP 是 192.168.1.1 但是目前 clientIp 只能设置一个,在 DNS 查询的时候起作用 假设我用另一台 IP 为 192.168.1.2 的机器 DNS 地址设置为 192.168.1.1 这时候 V2Ray 可以知道是 192.168.1.2 这台机器连接了,然后把 192.168.1.2 这个 IP 当作 clientIp 来用 这里把上述的IP改为公网 IP 就是我想表达的意思 |
源IP |
比如上面的
对于访问来源是内网 IP 的这种,我觉得反正内网 IP 是有限的,可以识别然后跳过 |
这个实在太魔法了,你还是单独开一个 issue 再讨论吧 |
@CalmLong 了解你的意思了……确实我会错意了,之前只往Client角度去想了。 意思是使用了V2Ray的VPS,同时使用V2Ray开放了一个DNS服务,对于从公网送来的查询请求,VPS以请求方的公网IP填入clientIP送给第三方公共DNS查询。 这个想法感觉挺合理的,相对Client开DNS只需要一个Public IP作clientIP,Server的这种可变的clientIP更有意义。内网使用V2Ray DNS查询时,可以fallback到JSON配置里设置的client IP。 在实现上DNS Feature需要重构,调用DNS Lookup时需要能拿到source ip,这个不放在这个DNS App PR做。 @Loyalsoldier 你感觉这个Feature request怎么样?从App角度来说可以实现,需不需要以及面向用户该如何设计配置看你们的讨论了(在另开的Issue回吧) |
没意见 |
我认为不应该把 core 放到公网上当dns服务,太简单了,延伸出的问题更多。 p.s 你准备要合并了吗 |
@kslr 我想把Routing Stats的几个PR推进了再来考虑DNS这边的PR,因为在此之后DNS Stats或许也可以考虑加上了…… |
8f293f9
to
9e97919
Compare
9e97919
to
0df05a1
Compare
@Vigilans 我有个建议,能否添加并行DNS查询? |
发现有些域名仅有AAAA,没有A,遇到这种情况若DNS仅查询A的话将没有返回值,此时如何匹配路由规则? |
在路由透明代理里,查询到AAAA记录是不通的,因为ipv6没法不开net的情况下代理。或许可以?但我不知道怎么做 |
这个PR还不打算合并吗 |
This is an unfinished PR. |
301675e
to
043d78c
Compare
043d78c
to
380c018
Compare
别啊,怎么给合并了? 这没有做完也没有充分测试怎么就进master了 |
😱 |
在本地测试了几回,没啥毛病,就先合了(心急如焚 |
StaticHosts 可能需要测试一下 |
DNS模块的实现似乎由于历史原因与Go自身的问题,在耦合度、代码复用上有一些不足之处。这个PR的目标是重构DNS模块,以:
server.go
中DNS核心代码,避免过长的函数,将功能独立的代码抽出,降低耦合度。主要改动介绍如下,详细的改动内容与改动理由将在Review中给出:
Name Server
File Name Change
LocalNameServer
从nameserver.go
中抽出来,放入nameserver_local.go
。udpns.go
->nameserver_udp.go
dohdns.go
->nameserver_doh.go
Name Server & Client
Client
接口更名为Server
接口:见Review Refactoring: DNS App #169 (comment), Refactoring: DNS App #169 (comment) 。Client
类,用以解耦原server.go
中纯Client
相关的功能:见Review Refactoring: DNS App #169 (comment) 。Hosts
server.go
中lookupStatic
代码迁至hosts.go
中:见Review Refactoring: DNS App #169 (comment) 。DNS
Server
类改名为DNS
类,代码由server.go
迁至dns.go
:见Review Refactoring: DNS App #169 (comment), Refactoring: DNS App #169 (comment) 。