From 6ac929a397dc2b2f83d79fbb32ae8986d8bd018c Mon Sep 17 00:00:00 2001 From: hahahrfool <75717694+hahahrfool@users.noreply.github.com> Date: Fri, 11 Mar 2022 19:57:12 +0800 Subject: [PATCH] update tls lazy encrypt code. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 目前该特性尚不稳定,会导致一些网页无法访问(不是速度慢,是有些tls连接因为检测tls措施不够好的问题, 给搞丢了,导致加载不出来,需要进一步调试;实测一般刷新一下页面就能加载出来,也不知道什么情况) --- README.md | 13 +-- main.go | 79 +++++++++-------- tlsLayer/conn.go | 3 +- tlsLayer/detect.go | 208 +++++++++++++++++++++++++-------------------- 4 files changed, 166 insertions(+), 137 deletions(-) diff --git a/README.md b/README.md index 48d9453..e505ccc 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,10 @@ verysimple项目大大简化了 转发机制,能提高运行速度。 在本项目里 制定 并实现了 vless v1标准。 +在最新代码里,我还实现了 双向 tls lazy encrypt, 即另一种 xtls的 splice的实现。因为是双向的,而xtls的splice是单向,所以 理论上 tls lazy encrypt 比xtls 还快。 + +目前该特性尚不稳定,会导致一些网页无法访问(不是速度慢,是有些tls连接因为检测tls措施不够好的问题, 给搞丢了,导致加载不出来,需要进一步调试;实测一般刷新一下页面就能加载出来,也不知道什么情况),运行时可以用 -lazy 参数打开(服务端客户端都要打开),然后可以用 -pdd 参数 打印 tls 探测输出 + 安装方式: @@ -101,15 +105,6 @@ verysimple 继承 v2simple的一个优点,就是服务端的配置也可以用 对于功能的golang test,请使用 `go test ./...` 命令。如果要详细的打印出test的过程,可以添加 -v 参数 -## 腾讯视频问题 -如果用此代理打开腾讯视频网页的话,会发现视频加载不出来。bilibili是没问题的。而且按理说和udp没关系因为websocket还是基于tcp的。 - -经过我测试, verysimple 返回的错误是: -``` - failed to dail 'apd-9d8f8b192cbf63303ebad8d58b51293f.v.sm:443': dial tcp: lookup apd-9d8f8b192cbf63303ebad8d58b51293f.v.sm: no such host -``` - -此问题有待考证解决。也不知道是不是只有我自己有这个问题。。 ## 交叉编译 diff --git a/main.go b/main.go index 98de7be..19ee76d 100644 --- a/main.go +++ b/main.go @@ -33,13 +33,13 @@ var ( conf *Config directClient proxy.Client - tls_lazy_encryptPtr = flag.Bool("lazy", false, "tls lazy encrypt (splice)") - tls_lazy_encrypt bool + tls_lazy_encrypt bool ) func init() { directClient, _ = proxy.ClientFromURL("direct://") + flag.BoolVar(&tls_lazy_encrypt, "lazy", false, "tls lazy encrypt (splice)") } func printVersion() { @@ -78,7 +78,6 @@ func main() { printVersion() flag.Parse() - tls_lazy_encrypt = *tls_lazy_encryptPtr var err error @@ -109,7 +108,7 @@ func main() { log.Println(localServer.Name(), "is listening TCP on ", localServer.AddrStr()) // 后台运行主代码,而main函数只监听中断信号 - // TODO: 未来main函数可以推出 交互模式,未来推出动态增删用户、查询流量等功能时就有用 + // TODO: 未来main函数可以推出 交互模式,等未来推出动态增删用户、查询流量等功能时就有用 go func() { for { lc, err := listener.Accept() @@ -142,7 +141,7 @@ func handleNewIncomeConnection(localServer proxy.Server, remoteClient proxy.Clie baseLocalConn := thisLocalConnectionInstance - log.Println("got new", thisLocalConnectionInstance.RemoteAddr().String()) + //log.Println("got new", thisLocalConnectionInstance.RemoteAddr().String()) var err error @@ -166,26 +165,24 @@ func handleNewIncomeConnection(localServer proxy.Server, remoteClient proxy.Clie wlc, targetAddr, err := localServer.Handshake(thisLocalConnectionInstance) if err != nil { - log.Printf("failed in handshake from %v: %v", localServer.AddrStr(), err) + log.Println("failed in handshake from", localServer.AddrStr(), err) thisLocalConnectionInstance.Close() return } - // 我们在客户端 lazy_encrypt 探测时,读取socks5 传来的信息,因为这个和要发送到tls的信息时一模一样的,所以就不需要等包上vless、tls后再判断了, 直接解包 socks5进行判断 + // 我们在客户端 lazy_encrypt 探测时,读取socks5 传来的信息,因为这个和要发送到tls的信息是一模一样的,所以就不需要等包上vless、tls后再判断了, 直接解包 socks5进行判断 // - // 而在服务端探测时,因为包了 tls,所以要在tls解包后, vless 解包后,再进行判断; - // 所以总之都是要在 localServer判断 wlc,只不过理由不一样 - - var thecc *tlsLayer.CopyConn + // 而在服务端探测时,因为 客户端传来的连接 包了 tls,所以要在tls解包后, vless 解包后,再进行判断; + // 所以总之都是要在 localServer判断 wlc;总之,含义就是,去检索“用户承载数据”的来源 if tls_lazy_encrypt { - thecc = tlsLayer.NewDetectConn(baseLocalConn, wlc) + wlc = tlsLayer.NewDetectConn(baseLocalConn, wlc, tlsLayer.OnlyTest) - wlc = thecc //clientConn = cc } + //如果目标是udp则要分情况讨论 if targetAddr.IsUDP { switch localServer.Name() { @@ -250,9 +247,9 @@ func handleNewIncomeConnection(localServer proxy.Server, remoteClient proxy.Clie var client proxy.Client - client = remoteClient + client = remoteClient //如果加了白名单等过滤方式,则client可能会等于direct等,再说 - log.Printf("%s want to dial %s", client.Name(), targetAddr.UrlString()) + log.Println(client.Name(), " want to dial ", targetAddr.UrlString()) // 如果目标是udp 则我们单独处理。这里为了简便,不考虑多级串联的关系,直接只考虑 直接转发到direct // 根据 vless_v1的讨论,vless_v1 的udp转发的 通信方式 也是与tcp连接类似的分离信道方式 @@ -338,7 +335,7 @@ func handleNewIncomeConnection(localServer proxy.Server, remoteClient proxy.Clie */ if tls_lazy_encrypt { - tryRaw(wrc, wlc, client.IsUseTLS()) + tryRawCopy(wrc, wlc, client.IsUseTLS()) return } @@ -347,15 +344,17 @@ func handleNewIncomeConnection(localServer proxy.Server, remoteClient proxy.Clie } +// tryRawCopy 尝试能否直接对拷,对拷 直接使用 原始 TCPConn //和 xtls的splice 含义相同 -func tryRaw(wrc, wlc io.ReadWriter, isclient bool) { +func tryRawCopy(wrc, wlc io.ReadWriter, isclient bool) { //如果用了 lazy_encrypt, 则不直接利用Copy,因为有两个阶段:判断阶段和直连阶段 // 在判断阶段,因为还没确定是否是 tls,所以是要继续用tls加密的, - // 而直连阶段,只要能让 Copy使用 ReadFrom, 就能一步一步最终使用splice了 + // 而直连阶段,只要能让 Copy使用 net.TCPConn的 ReadFrom, 就不用管了, golang最终就会使用splice + // 之所以可以对拷直连,是因为无论是 socks5 还是vless,只是在最开始的部分 加了目标头,后面的所有tcp连接都是直接传输的数据,就是说,一开始握手什么的是不能直接对拷的,等到后期就可以了 + // 而且之所以能对拷,还有个原因就是,远程服务器 与 客户端 总是源源不断地 为 我们的 原始 TCP 连接 提供数据,我们只是一个中间商而已,左手倒右手 - //首先判断我们的wlc(*tlsLayer.CopyConn) 是否得出来 IsTLS - wlccc := wlc.(*tlsLayer.CopyConn) + wlccc := wlc.(*tlsLayer.DetectConn) wlccc_raw := wlccc.RawConn var rawWRC *net.TCPConn @@ -363,21 +362,23 @@ func tryRaw(wrc, wlc io.ReadWriter, isclient bool) { //wrc 有两种情况,如果客户端那就是tls,服务端那就是direct。我们不讨论服务端 处于中间层的情况 if isclient { - // 不过实际上 wrc 是 vless的 UserConn, 而UserConn的底层连接才是TLS + // 不过实际上客户端 wrc 是 vless的 UserConn, 而UserConn的底层连接才是TLS + // 很明显,目前我们只支持vless所以才这么操作,以后再说。 wrcVless := wrc.(*vless.UserConn) - tlsConn := wrcVless.Conn.(*tlsLayer.Conn) - rawWRC = tlsConn.GetRaw() } else { - rawWRC = wrc.(*net.TCPConn) + rawWRC = wrc.(*net.TCPConn) //因为是direct } if rawWRC == nil { - log.Println("splice fail reason 3 ") - io.Copy(wrc, wlc) + log.Println("splice fail reason 1 ") + + //退化回原始状态 + go io.Copy(wrc, wlc) + io.Copy(wlc, wrc) return } @@ -385,6 +386,7 @@ func tryRaw(wrc, wlc io.ReadWriter, isclient bool) { //从 wlccc 读取,向 wrc 写入 // 此时如果ReadFrom,那就是 wrc.ReadFrom(wlccc) //wrc 要实现 ReaderFrom才行, 或者把最底层TCPConn暴露,然后 wlccc 也要把最底层 TCPConn暴露出来 + // 这里就直接采取底层方式 p := common.GetPacket() isgood := false @@ -396,20 +398,24 @@ func tryRaw(wrc, wlc io.ReadWriter, isclient bool) { n, err := wlccc.Read(p) if err != nil { - break } wrc.Write(p[:n]) if wlccc.R.IsTls && wlccc.RawConn != nil { isgood = true - } } + common.PutPacket(p) if isgood { - log.Println("成功SpliceRead 方向1") - rawWRC.ReadFrom(wlccc_raw) + if tlsLayer.PDD { + log.Println("成功SpliceRead 方向1") + num, _ := rawWRC.ReadFrom(wlccc_raw) + log.Println("SpliceRead 方向1 读完,", num) + } else { + rawWRC.ReadFrom(wlccc_raw) + } } }() @@ -424,17 +430,22 @@ func tryRaw(wrc, wlc io.ReadWriter, isclient bool) { } n, err := wrc.Read(p) if err != nil { - break } wlccc.Write(p[:n]) if wlccc.W.IsTls && wlccc.RawConn != nil { isgood2 = true - } } + common.PutPacket(p) if isgood2 { - log.Println("成功SpliceRead 方向2") - wlccc_raw.ReadFrom(rawWRC) + if tlsLayer.PDD { + log.Println("成功SpliceRead 方向2") + num, _ := wlccc_raw.ReadFrom(rawWRC) + log.Println("SpliceRead 方向2 读完,", num) + } else { + wlccc_raw.ReadFrom(rawWRC) + } + } } diff --git a/tlsLayer/conn.go b/tlsLayer/conn.go index 0180eb4..0da49da 100644 --- a/tlsLayer/conn.go +++ b/tlsLayer/conn.go @@ -2,7 +2,6 @@ package tlsLayer import ( "crypto/tls" - "log" "net" "unsafe" ) @@ -21,7 +20,7 @@ func (c *Conn) GetRaw() *net.TCPConn { rc := (*faketlsconn)(unsafe.Pointer(uintptr(unsafe.Pointer(c.Conn)))) if rc != nil { if rc.conn != nil { - log.Println("成功获取到 *net.TCPConn!", rc.conn.(*net.TCPConn)) + //log.Println("成功获取到 *net.TCPConn!", rc.conn.(*net.TCPConn)) //经测试,是毫无问题的 return rc.conn.(*net.TCPConn) } } diff --git a/tlsLayer/detect.go b/tlsLayer/detect.go index 68c8d62..6bd611c 100644 --- a/tlsLayer/detect.go +++ b/tlsLayer/detect.go @@ -1,6 +1,7 @@ package tlsLayer import ( + "flag" "io" "log" "net" @@ -8,40 +9,55 @@ import ( "strings" ) -type CopyConn struct { - net.Conn //这个 Conn本包中不会用到,只是为了能让CopyConn支持 net.Conn - io.ReadWriter - W *DetectWriter - R *DetectReader +var PDD bool //print tls detect detail +var OnlyTest bool + +func init() { + log.SetOutput(os.Stdout) //主要是日志太多,如果都能直接用管道放到文件中就好了,默认不是Stdout所以优点尴尬,操作麻烦点 + + flag.BoolVar(&PDD, "pdd", false, "print tls detect detail") + flag.BoolVar(&OnlyTest, "ot", false, "only detect tls, doesn't actually mark tls") - RawConn *net.TCPConn // 这个是为了让外界能够直接拿到底层的连接 } -func (cc *CopyConn) Read(p []byte) (int, error) { +// 用于 探测 承载数据是否使用了tls +// 可以参考 https://www.baeldung.com/linux/tcpdump-capture-ssl-handshake +type DetectConn struct { + net.Conn //这个 Conn本DetectConn 中不会用到,只是为了能让CopyConn支持 net.Conn + W *DetectWriter + R *DetectReader + + RawConn *net.TCPConn // 这个是为了让外界能够直接拿到底层的连接 + + onlyTest bool //如果此开关打开,则不会去真实修改IsTLS,而只是去过滤所有的包。 +} + +func (cc *DetectConn) Read(p []byte) (int, error) { return cc.R.Read(p) } -func (cc *CopyConn) Write(p []byte) (int, error) { +func (cc *DetectConn) Write(p []byte) (int, error) { return cc.W.Write(p) } -func (cc *CopyConn) ReadFrom(r io.Reader) (int64, error) { +//这个暂时没用到,先留着 +func (cc *DetectConn) ReadFrom(r io.Reader) (int64, error) { if cc.RawConn != nil { return cc.RawConn.ReadFrom(r) } return 0, io.EOF } -func NewDetectConn(oldConn net.Conn, rw io.ReadWriter) *CopyConn { +//可选两个参数传入,优先使用rw ,为nil的话 再使用oldConn,作为 底层Read 和Write的 主体 +func NewDetectConn(oldConn net.Conn, rw io.ReadWriter, onlyTest bool) *DetectConn { var validOne io.ReadWriter = rw if rw == nil { validOne = oldConn } - cc := &CopyConn{ - Conn: oldConn, - ReadWriter: rw, + cc := &DetectConn{ + Conn: oldConn, W: &DetectWriter{ Writer: validOne, }, @@ -50,6 +66,9 @@ func NewDetectConn(oldConn net.Conn, rw io.ReadWriter) *CopyConn { }, } + cc.W.onlyTest = onlyTest + cc.R.onlyTest = onlyTest + if netConn := oldConn.(*net.TCPConn); netConn != nil { //log.Println("get netConn!") // 如果是客户端的socks5,网页浏览的话这里一定能转成 TCPConn cc.RawConn = netConn @@ -58,81 +77,107 @@ func NewDetectConn(oldConn net.Conn, rw io.ReadWriter) *CopyConn { return cc } -// DetectReader 对每个Read的数据进行分析,判断是否是tls流量 -type DetectReader struct { - io.Reader - IsTls bool +type ComDetectStruct struct { + IsTls bool + onlyTest bool packetCount int } -func init() { - log.SetOutput(os.Stdout) +// DetectReader 对每个Read的数据进行分析,判断是否是tls流量 +type DetectReader struct { + io.Reader + + ComDetectStruct +} + +func commonDetect(dr *ComDetectStruct, p []byte, isRead bool) { + + dr.packetCount++ + p0 := p[0] + p1 := p[1] + p2 := p[2] + + /* + if p0 == 22 || p0 == 23 || p0 == 20 || (p0 == 21 && n == 31) { + //客户端Read 时 少数情况首部会有21,首部为 [21 3 3 0 26 0 0 0 0 0], 一般总长度为31 + // 其它都是 能被捕捉到的。 + if p[1] == 3 { + min := 5 + if n < 5 { + min = n + } + log.Println(" TLS R,", n, err, p[:min]) + dr.IsTls = true + return + } + }*/ + + if p0 == 23 && p1 == 3 && p2 == 3 { + if PDD { + str := "W" + if isRead { + str = "R" + } + log.Println(str, "got TLS!") + } + + if !dr.onlyTest { + dr.IsTls = true + } + + return + } + n := len(p) + + // 打印没过滤到的数据 + if PDD || dr.onlyTest { + min := 10 + if n < 10 { + min = n + } + + str := "Write," + if isRead { + str = "Read," + } + log.Println(" ======== ", str, n, p[:min]) + } + } // 总之,我们在客户端的 Read 操作,就是 我们试图使用 Read 读取客户的请求,然后试图发往 外界 // 所以在socks5后面 使用的这个 Read,是读取客户端发送的请求,比如 clienthello之类 +// 服务端的 Read 操作,是把 远程目标服务器 发来的 数据 发送到 客户端,比如 serverhello 之类 // // 我们直接判断23 3 3字节,然后直接推定tls!不管三七二十一,判断错误就错误吧!快就得了! func (dr *DetectReader) Read(p []byte) (n int, err error) { n, err = dr.Reader.Read(p) - if dr.IsTls { + if !dr.onlyTest && dr.IsTls { return } - - if dr.packetCount > 8 { - //都8个包了还没断定tls?直接推定不是! - return - } - - if n > 3 { - dr.packetCount++ - p0 := p[0] - p1 := p[1] - p2 := p[2] - - /* - if p0 == 22 || p0 == 23 || p0 == 20 || (p0 == 21 && n == 31) { - //少数情况首部会有21,首部为 [21 3 3 0 26 0 0 0 0 0], 一般总长度为31 - // 其它都是 能被捕捉到的。 - if p[1] == 3 { - min := 5 - if n < 5 { - min = n - } - log.Println(" TLS R,", n, err, p[:min]) - dr.IsTls = true - return - } - }*/ - - if p0 == 23 && p1 == 3 && p2 == 3 { - log.Println("R got TLS!") - dr.IsTls = true - return - } - } if err != nil { eStr := err.Error() if strings.Contains(eStr, "use of closed") || strings.Contains(eStr, "reset by peer") || strings.Contains(eStr, "EOF") { return } } - - min := 10 - if n < 10 { - min = n + if !dr.onlyTest && dr.packetCount > 8 { + //都8个包了还没断定tls?直接推定不是! + return } - log.Println(" ======== Read,", n, err, p[:min], string(p[:min])) + + if n > 3 { + commonDetect(&dr.ComDetectStruct, p, true) + } + return } // DetectReader 对每个Read的数据进行分析,判断是否是tls流量 type DetectWriter struct { io.Writer - IsTls bool - - packetCount int + ComDetectStruct } //我发现,数据基本就是 23 3 3, 22 3 3,22 3 1 , 20 3 3 @@ -144,6 +189,7 @@ type DetectWriter struct { // // 总之,我们在客户端的 Write 操作,就是 外界试图使用我们的 Write 写入数据 // 所以在socks5后面 使用的这个Write,应该是把 服务端的响应 写回 socks5,比如 serverhello之类 +// 服务端的 Write操作,是把客户端发来的 数据 发送到远程目标服务器,比如 clienthello之类 // // 根据之前讨论,23 3 3 就是 数据部分,TLSCiphertext // https://halfrost.com/https_record_layer/ @@ -151,45 +197,23 @@ type DetectWriter struct { // 进入direct模式; 目前从简,连握手包都不检测!测错就测错! func (dr *DetectWriter) Write(p []byte) (n int, err error) { n, err = dr.Writer.Write(p) - if dr.IsTls { + if !dr.onlyTest && dr.IsTls { return } - - if dr.packetCount > 8 { + if err != nil { + eStr := err.Error() + if strings.Contains(eStr, "use of closed") || strings.Contains(eStr, "reset by peer") || strings.Contains(eStr, "EOF") { + return + } + } + if !dr.onlyTest && dr.packetCount > 8 { //都8个包了还没断定tls?直接推定不是! return } if n > 3 { - dr.packetCount++ - - p0 := p[0] - p1 := p[1] - p2 := p[2] - - /* - if p0 == 22 || p0 == 23 || p0 == 20 { - if p[1] == 3 { - min := 5 - if n < 5 { - min = n - } - log.Println(" TLS W,", n, err, p[:min]) - return - } - }*/ - - if p0 == 23 && p1 == 3 && p2 == 3 { - log.Println("W got TLS!") - dr.IsTls = true - return - } + commonDetect(&dr.ComDetectStruct, p, false) } - min := 10 - if n < 10 { - min = n - } - log.Println(" ======== Write,", n, err, p[:min], string(p[:min])) return }