Clash 源码解析
前言
Clash 是一款基于规则的跨平台代理软件,支持 Windows,macOS,Linux 操作系统。它可以帮助用户实现网络访问控制等需求。
Clash 使用规则文件来控制流量,规则文件的作用是告诉 Clash 哪些流量需要代理,哪些流量不需要代理。规则文件是文本文件,其中包含了一些规则,可以根据规则的匹配结果来选择是否使用代理。
在 Clash 规则文件中,最重要的是 Proxy 和 Rule 两个关键词。Proxy 定义了代理服务器的信息,Rule 定义了使用代理的规则。在 Rule 中,可以使用不同的匹配方式来匹配请求的目标地址或域名,从而选择是否使用代理。
规则匹配
规则的基础语法
Clash 规则采用 YAML 格式,采用了缩进表示层次关系,rule 分为三个部分,<匹配规则>,<匹配规则的参数>,<代理节点>
。下面是一个简单的 Clash 规则示例:
rules:
- DOMAIN-SUFFIX,google.com,proxy
- DOMAIN,google.com,proxy
- DOMAIN-KEYWORD,google.com,proxy
- GEOIP,CN,DIRECT
Clash 规则匹配的基本原理是将流量的各个部分(如域名、IP、端口等)与规则进行匹配,以确定是否应该代理该流量。例如上述示例中的 DOMAIN 和 GEOIP 就是对域名和 IP 进行匹配。
Clash 规则匹配的源代码在 clash/rule 文件夹下面,例如上面的 DOMAIN-KEYWORD, GEOIP 对应文件是 domain_keyword.go, geoip.go。下面通过源代码来学习规则匹配的原理,首先从 TCP 代理的入口开始看。
源码实现
Clash 规则匹配的基本原理是将流量的各个部分(如域名、IP、端口等)与规则进行匹配,以确定是否应该代理该流量。例如上述示例中的 DOMAIN 和 GEOIP 就是对域名和 IP 进行匹配。
Clash 规则匹配的源代码在 clash/rule 文件夹下面,例如上面的 DOMAIN-KEYWORD, GEOIP 对应文件是 domain_keyword.go, geoip.go。下面通过源代码来学习规则匹配的原理,首先从 TCP 代理的入口开始看。
// tunnel/tunnel.go
func handleTCPConn(connCtx C.ConnContext) {
metadata := connCtx.Metadata()
// ...
// 该行代码最终返回的 proxy 就是匹配中的代理节点,rule 则是使用的代理规则
proxy, rule, err := resolveMetadata(connCtx, metadata)
}
var (
proxies = make(map[string]C.Proxy)
)
func resolveMetadata(ctx C.PlainContext, metadata *C.Metadata) (proxy C.Proxy, rule C.Rule, err error) {
// 元信息中指定了代理则直接走指定的代理
if metadata.SpecialProxy != "" {
var exist bool
proxy, exist = proxies[metadata.SpecialProxy]
if !exist {
err = fmt.Errorf("proxy %s not found", metadata.SpecialProxy)
}
return
}
switch mode {
// 判断当前的模式,如果为 DIRECT 模式则选择 DIRECT 对应的节点
case Direct:
proxy = proxies["DIRECT"]
// 判断当前的模式,如果为 GLOBAL 模式则选择 GLOBAL 对应的节点
case Global:
proxy = proxies["GLOBAL"]
// 如果不是前两种模式则走规则匹配
default:
proxy, rule, err = match(metadata)
}
return
}
func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) {
configMux.RLock()
defer configMux.RUnlock()
var resolved bool
var processFound bool
// 进行 DNS 解析
if node := resolver.DefaultHosts.Search(metadata.Host); node != nil {
ip := node.Data.(net.IP)
metadata.DstIP = ip
resolved = true
}
for _, rule := range rules {
//...
// 下面的 Match 进行规则匹配
if rule.Match(metadata) {
// 匹配上后通过 rule.Adapter() 找到对应的代理节点也就是 adapter 然后返回
adapter, ok := proxies[rule.Adapter()]
if !ok {
continue
}
if metadata.NetWork == C.UDP && !adapter.SupportUDP() && UDPFallbackMatch.Load() {
log.Debugln("[Matcher] %s UDP is not supported, skip match", adapter.Name())
continue
}
return adapter, rule, nil
}
}
// 未匹配上则走直连
return proxies["DIRECT"], nil, nil
}
// rule/domain_keyword.go
// 这里以域名关键字匹配为例
func (dk *DomainKeyword) Match(metadata *C.Metadata) bool {
// 判断传入的域名中是否包含指定的关键字
return strings.Contains(metadata.Host, dk.keyword)
}
上面以 domain_keyword 为例讲解了整个匹配的全过程,剩下的几种匹配方式均大同小异,理解了 domain_keyword 读者就可以自己通过阅读源码理解另外几种匹配机制。
隧道
代理连接是通过 proxy.DialContext 函数来创建的。其中,DialContext 函数会创建一个新的连接并返回一个 net.Conn 接口类型的对象,该对象可以被用于数据传输。在目标连接创建后,通过将源连接和目标连接的数据进行双向拷贝,完成数据传输,从而达到 tunnel 的效果。同时,在实现过程中,还使用了一个巧妙的技巧——通过 ReadOnlyReader 和 WriteOnlyWriter 对连接进行包装,避免使用 net.TCPConn 的 ReadFrom 方法,从而提高了性能。在进行数据传输的过程中,还使用了多个 goroutine 进行调度和优化,提高了程序的运行效率。
// context/conn.go
type ConnContext struct {
id uuid.UUID
metadata *C.Metadata
conn net.Conn
}
// tunnel/tunnel.go
// 连接建立时会调用这个方法,connCtx 中的 conn 就是连接到 clash 的连接
func handleTCPConn(connCtx C.ConnContext) {
metadata := connCtx.Metadata()
// ...
// metadata 中存放着源地址和目标地址,进入该函数后会使用远程地址创建连接并将其返回,也就是 remoteConn
remoteConn, err := proxy.DialContext(ctx, metadata.Pure())
// 远程连接建立后通过调用 handleSocket 方法完成对数据的双向拷贝
handleSocket(connCtx, remoteConn)
}
// tunnel/connection.go
import (
N "github.com/Dreamacro/clash/common/net"
)
func handleSocket(ctx C.ConnContext, outbound net.Conn) {
N.Relay(ctx.Conn(), outbound)
}
// common/net/relay.go
// Relay copies between left and right bidirectionally.
func Relay(leftConn, rightConn net.Conn) {
ch := make(chan error)
// 开启携程从目标服务器读取的数据写入源客户端
go func() {
_, err := io.Copy(WriteOnlyWriter{Writer: leftConn}, ReadOnlyReader{Reader: rightConn})
leftConn.SetReadDeadline(time.Now())
ch <- err
}()
// 从源客户端读取的数据写入目标服务器
io.Copy(WriteOnlyWriter{Writer: rightConn}, ReadOnlyReader{Reader: leftConn})
rightConn.SetReadDeadline(time.Now())
<-ch
}
- 0
- 0
-
分享