├── .gitignore ├── .goreleaser.yaml ├── cmd ├── client │ ├── inbound.go │ ├── main.go │ └── myclient.go └── server │ ├── inbound_tcp.go │ ├── main.go │ ├── myserver.go │ └── outbound_tcp.go ├── docs ├── faq.md ├── protocol.md └── uri_scheme.md ├── go.mod ├── go.sum ├── proxy ├── padding │ └── padding.go ├── pipe │ ├── deadline.go │ └── io_pipe.go ├── session │ ├── client.go │ ├── frame.go │ ├── session.go │ └── stream.go └── system_dialer.go ├── readme.md └── util ├── deadline.go ├── mkcert.go ├── routine.go ├── string_map.go ├── type.go └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /cmd/client/client 3 | /cmd/server/server 4 | *.exe 5 | *.zip 6 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | project_name: anytls 4 | builds: 5 | - id: anytls-client 6 | binary: anytls-client 7 | dir: cmd/client 8 | env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - linux 12 | - windows 13 | - darwin 14 | goarch: 15 | - amd64 16 | - arm64 17 | flags: 18 | - -trimpath 19 | - -buildvcs=false 20 | ldflags: 21 | - -s -w 22 | - id: anytls-server 23 | binary: anytls-server 24 | dir: cmd/server 25 | env: 26 | - CGO_ENABLED=0 27 | goos: 28 | - linux 29 | - windows 30 | - darwin 31 | goarch: 32 | - amd64 33 | - arm64 34 | flags: 35 | - -trimpath 36 | - -buildvcs=false 37 | ldflags: 38 | - -s -w 39 | archives: 40 | - id: anytls 41 | builds: 42 | - anytls-client 43 | - anytls-server 44 | name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 45 | format: zip 46 | -------------------------------------------------------------------------------- /cmd/client/inbound.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "runtime/debug" 7 | 8 | "github.com/sagernet/sing/common/bufio" 9 | M "github.com/sagernet/sing/common/metadata" 10 | "github.com/sagernet/sing/common/network" 11 | "github.com/sagernet/sing/common/uot" 12 | "github.com/sagernet/sing/protocol/socks" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func handleTcpConnection(ctx context.Context, c net.Conn, s *myClient) { 17 | defer func() { 18 | if r := recover(); r != nil { 19 | logrus.Errorln("[BUG]", r, string(debug.Stack())) 20 | } 21 | }() 22 | defer c.Close() 23 | 24 | socks.HandleConnection(ctx, c, nil, s, M.Metadata{ 25 | Source: M.SocksaddrFromNet(c.RemoteAddr()), 26 | Destination: M.SocksaddrFromNet(c.LocalAddr()), 27 | }) 28 | } 29 | 30 | // sing socks inbound 31 | 32 | func (c *myClient) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { 33 | proxyC, err := c.CreateProxy(ctx, metadata.Destination) 34 | if err != nil { 35 | logrus.Errorln("CreateProxy:", err) 36 | return err 37 | } 38 | defer proxyC.Close() 39 | 40 | return bufio.CopyConn(ctx, conn, proxyC) 41 | } 42 | 43 | func (c *myClient) NewPacketConnection(ctx context.Context, conn network.PacketConn, metadata M.Metadata) error { 44 | proxyC, err := c.CreateProxy(ctx, uot.RequestDestination(2)) 45 | if err != nil { 46 | logrus.Errorln("CreateProxy:", err) 47 | return err 48 | } 49 | defer proxyC.Close() 50 | 51 | request := uot.Request{ 52 | Destination: metadata.Destination, 53 | } 54 | uotC := uot.NewLazyConn(proxyC, request) 55 | 56 | return bufio.CopyPacketConn(ctx, conn, uotC) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "anytls/proxy" 5 | "anytls/util" 6 | "context" 7 | "crypto/sha256" 8 | "crypto/tls" 9 | "flag" 10 | "net" 11 | "os" 12 | "strings" 13 | 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var passwordSha256 []byte 18 | 19 | func main() { 20 | listen := flag.String("l", "127.0.0.1:1080", "socks5 listen port") 21 | serverAddr := flag.String("s", "127.0.0.1:8443", "server address") 22 | sni := flag.String("sni", "", "SNI") 23 | password := flag.String("p", "", "password") 24 | flag.Parse() 25 | 26 | if *password == "" { 27 | logrus.Fatalln("please set password") 28 | } 29 | 30 | logLevel, err := logrus.ParseLevel(os.Getenv("LOG_LEVEL")) 31 | if err != nil { 32 | logLevel = logrus.InfoLevel 33 | } 34 | logrus.SetLevel(logLevel) 35 | 36 | var sum = sha256.Sum256([]byte(*password)) 37 | passwordSha256 = sum[:] 38 | 39 | logrus.Infoln("[Client]", util.ProgramVersionName) 40 | logrus.Infoln("[Client] socks5", *listen, "=>", *serverAddr) 41 | 42 | listener, err := net.Listen("tcp", *listen) 43 | if err != nil { 44 | logrus.Fatalln("listen socks5 tcp:", err) 45 | } 46 | 47 | tlsConfig := &tls.Config{ 48 | ServerName: *sni, 49 | InsecureSkipVerify: true, 50 | } 51 | if tlsConfig.ServerName == "" { 52 | // disable the SNI 53 | tlsConfig.ServerName = "127.0.0.1" 54 | } 55 | path := strings.TrimSpace(os.Getenv("TLS_KEY_LOG")) 56 | if path != "" { 57 | f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644) 58 | if err == nil { 59 | tlsConfig.KeyLogWriter = f 60 | } 61 | } 62 | 63 | ctx := context.Background() 64 | client := NewMyClient(ctx, func(ctx context.Context) (net.Conn, error) { 65 | conn, err := proxy.SystemDialer.DialContext(ctx, "tcp", *serverAddr) 66 | if err != nil { 67 | return nil, err 68 | } 69 | conn = tls.Client(conn, tlsConfig) 70 | return conn, nil 71 | }) 72 | 73 | for { 74 | c, err := listener.Accept() 75 | if err != nil { 76 | logrus.Fatalln("accept:", err) 77 | } 78 | go handleTcpConnection(ctx, c, client) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /cmd/client/myclient.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "anytls/proxy/padding" 5 | "anytls/proxy/session" 6 | "anytls/util" 7 | "context" 8 | "encoding/binary" 9 | "net" 10 | "time" 11 | 12 | "github.com/sagernet/sing/common/buf" 13 | M "github.com/sagernet/sing/common/metadata" 14 | ) 15 | 16 | type myClient struct { 17 | dialOut util.DialOutFunc 18 | sessionClient *session.Client 19 | } 20 | 21 | func NewMyClient(ctx context.Context, dialOut util.DialOutFunc) *myClient { 22 | s := &myClient{ 23 | dialOut: dialOut, 24 | } 25 | s.sessionClient = session.NewClient(ctx, s.createOutboundConnection, &padding.DefaultPaddingFactory, time.Second*30, time.Second*30, 5) 26 | return s 27 | } 28 | 29 | func (c *myClient) CreateProxy(ctx context.Context, destination M.Socksaddr) (net.Conn, error) { 30 | conn, err := c.sessionClient.CreateStream(ctx) 31 | if err != nil { 32 | return nil, err 33 | } 34 | err = M.SocksaddrSerializer.WriteAddrPort(conn, destination) 35 | if err != nil { 36 | conn.Close() 37 | return nil, err 38 | } 39 | return conn, nil 40 | } 41 | 42 | func (c *myClient) createOutboundConnection(ctx context.Context) (net.Conn, error) { 43 | conn, err := c.dialOut(ctx) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | b := buf.NewPacket() 49 | defer b.Release() 50 | 51 | b.Write(passwordSha256) 52 | var paddingLen int 53 | if pad := padding.DefaultPaddingFactory.Load().GenerateRecordPayloadSizes(0); len(pad) > 0 { 54 | paddingLen = pad[0] 55 | } 56 | binary.BigEndian.PutUint16(b.Extend(2), uint16(paddingLen)) 57 | if paddingLen > 0 { 58 | b.WriteZeroN(paddingLen) 59 | } 60 | 61 | _, err = b.WriteTo(conn) 62 | if err != nil { 63 | conn.Close() 64 | return nil, err 65 | } 66 | 67 | return conn, nil 68 | } 69 | -------------------------------------------------------------------------------- /cmd/server/inbound_tcp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "anytls/proxy/padding" 5 | "anytls/proxy/session" 6 | "bytes" 7 | "context" 8 | "crypto/tls" 9 | "encoding/binary" 10 | "net" 11 | "runtime/debug" 12 | "strings" 13 | 14 | "github.com/sagernet/sing/common/buf" 15 | "github.com/sagernet/sing/common/bufio" 16 | M "github.com/sagernet/sing/common/metadata" 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | func handleTcpConnection(ctx context.Context, c net.Conn, s *myServer) { 21 | defer func() { 22 | if r := recover(); r != nil { 23 | logrus.Errorln("[BUG]", r, string(debug.Stack())) 24 | } 25 | }() 26 | 27 | c = tls.Server(c, s.tlsConfig) 28 | defer c.Close() 29 | 30 | b := buf.NewPacket() 31 | defer b.Release() 32 | 33 | n, err := b.ReadOnceFrom(c) 34 | if err != nil { 35 | logrus.Debugln("ReadOnceFrom:", err) 36 | return 37 | } 38 | c = bufio.NewCachedConn(c, b) 39 | 40 | by, err := b.ReadBytes(32) 41 | if err != nil || !bytes.Equal(by, passwordSha256) { 42 | b.Resize(0, n) 43 | fallback(ctx, c) 44 | return 45 | } 46 | by, err = b.ReadBytes(2) 47 | if err != nil { 48 | b.Resize(0, n) 49 | fallback(ctx, c) 50 | return 51 | } 52 | paddingLen := binary.BigEndian.Uint16(by) 53 | if paddingLen > 0 { 54 | _, err = b.ReadBytes(int(paddingLen)) 55 | if err != nil { 56 | b.Resize(0, n) 57 | fallback(ctx, c) 58 | return 59 | } 60 | } 61 | 62 | session := session.NewServerSession(c, func(stream *session.Stream) { 63 | defer func() { 64 | if r := recover(); r != nil { 65 | logrus.Errorln("[BUG]", r, string(debug.Stack())) 66 | } 67 | }() 68 | defer stream.Close() 69 | 70 | destination, err := M.SocksaddrSerializer.ReadAddrPort(stream) 71 | if err != nil { 72 | logrus.Debugln("ReadAddrPort:", err) 73 | return 74 | } 75 | 76 | if strings.Contains(destination.String(), "udp-over-tcp.arpa") { 77 | proxyOutboundUoT(ctx, stream, destination) 78 | } else { 79 | proxyOutboundTCP(ctx, stream, destination) 80 | } 81 | }, &padding.DefaultPaddingFactory) 82 | session.Run() 83 | session.Close() 84 | } 85 | 86 | func fallback(ctx context.Context, c net.Conn) { 87 | // 暂未实现 88 | logrus.Debugln("fallback:", c.RemoteAddr()) 89 | } 90 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "anytls/proxy/padding" 5 | "anytls/util" 6 | "context" 7 | "crypto/sha256" 8 | "crypto/tls" 9 | "flag" 10 | "io" 11 | "net" 12 | "os" 13 | "time" 14 | 15 | "github.com/sirupsen/logrus" 16 | ) 17 | 18 | var passwordSha256 []byte 19 | 20 | func main() { 21 | listen := flag.String("l", "0.0.0.0:8443", "server listen port") 22 | password := flag.String("p", "", "password") 23 | paddingScheme := flag.String("padding-scheme", "", "padding-scheme") 24 | flag.Parse() 25 | 26 | if *password == "" { 27 | logrus.Fatalln("please set password") 28 | } 29 | if *paddingScheme != "" { 30 | if f, err := os.Open(*paddingScheme); err == nil { 31 | b, err := io.ReadAll(f) 32 | if err != nil { 33 | logrus.Fatalln(err) 34 | } 35 | if padding.UpdatePaddingScheme(b) { 36 | logrus.Infoln("loaded padding scheme file:", *paddingScheme) 37 | } else { 38 | logrus.Errorln("wrong format padding scheme file:", *paddingScheme) 39 | } 40 | f.Close() 41 | } else { 42 | logrus.Fatalln(err) 43 | } 44 | } 45 | 46 | logLevel, err := logrus.ParseLevel(os.Getenv("LOG_LEVEL")) 47 | if err != nil { 48 | logLevel = logrus.InfoLevel 49 | } 50 | logrus.SetLevel(logLevel) 51 | 52 | var sum = sha256.Sum256([]byte(*password)) 53 | passwordSha256 = sum[:] 54 | 55 | logrus.Infoln("[Server]", util.ProgramVersionName) 56 | logrus.Infoln("[Server] Listening TCP", *listen) 57 | 58 | listener, err := net.Listen("tcp", *listen) 59 | if err != nil { 60 | logrus.Fatalln("listen server tcp:", err) 61 | } 62 | 63 | tlsCert, _ := util.GenerateKeyPair(time.Now, "") 64 | tlsConfig := &tls.Config{ 65 | GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) { 66 | return tlsCert, nil 67 | }, 68 | } 69 | 70 | ctx := context.Background() 71 | server := NewMyServer(tlsConfig) 72 | 73 | for { 74 | c, err := listener.Accept() 75 | if err != nil { 76 | logrus.Fatalln("accept:", err) 77 | } 78 | go handleTcpConnection(ctx, c, server) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /cmd/server/myserver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | ) 6 | 7 | type myServer struct { 8 | tlsConfig *tls.Config 9 | } 10 | 11 | func NewMyServer(tlsConfig *tls.Config) *myServer { 12 | s := &myServer{ 13 | tlsConfig: tlsConfig, 14 | } 15 | return s 16 | } 17 | -------------------------------------------------------------------------------- /cmd/server/outbound_tcp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "anytls/proxy" 5 | "context" 6 | "net" 7 | 8 | "github.com/sagernet/sing/common/bufio" 9 | E "github.com/sagernet/sing/common/exceptions" 10 | M "github.com/sagernet/sing/common/metadata" 11 | N "github.com/sagernet/sing/common/network" 12 | "github.com/sagernet/sing/common/uot" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func proxyOutboundTCP(ctx context.Context, conn net.Conn, destination M.Socksaddr) error { 17 | c, err := proxy.SystemDialer.DialContext(ctx, "tcp", destination.String()) 18 | if err != nil { 19 | logrus.Debugln("proxyOutboundTCP DialContext:", err) 20 | err = E.Errors(err, N.ReportHandshakeFailure(conn, err)) 21 | return err 22 | } 23 | 24 | err = N.ReportHandshakeSuccess(conn) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | return bufio.CopyConn(ctx, conn, c) 30 | } 31 | 32 | func proxyOutboundUoT(ctx context.Context, conn net.Conn, destination M.Socksaddr) error { 33 | request, err := uot.ReadRequest(conn) 34 | if err != nil { 35 | logrus.Debugln("proxyOutboundUoT ReadRequest:", err) 36 | return err 37 | } 38 | 39 | c, err := net.ListenPacket("udp", "") 40 | if err != nil { 41 | logrus.Debugln("proxyOutboundUoT ListenPacket:", err) 42 | err = E.Errors(err, N.ReportHandshakeFailure(conn, err)) 43 | return err 44 | } 45 | 46 | err = N.ReportHandshakeSuccess(conn) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | return bufio.CopyPacketConn(ctx, uot.NewConn(conn, *request), bufio.NewPacketConn(c)) 52 | } 53 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # 用户常见疑问 2 | 3 | ## ERR_CONNECTION_CLOSED / 代理关闭且没有日志 4 | 5 | 常见原因: 6 | 7 | - 密码错误,请检查您的密码是否正确。 8 | 9 | ## 好慢 10 | 11 | 网络速度与线路质量有关,升级更优质的线路可以提升速度。 12 | 13 | ## 为什么选项这么少 / 为什么是自签证书 14 | 15 | 本项目只是提供一个简洁的 Any in TLS 代理的示例,并不旨在成为“通用代理工具”。 16 | 17 | 作为参考实现,不对 TLS 协议本身做过多的处理。 18 | 19 | 当然,如果你把这个协议集成到某些代理平台中,你将能够更好地控制 TLS ClientHello/ServerHello。 20 | 21 | ## FingerPrint 之类的选项呢 22 | 23 | TLS 本身(ClientHello/ServerHello)的特征不是本项目关注的重点,现有的工具很容易改变这些特征。 24 | 25 | - 某些 Golang 灵车代理早已标配 uTLS,为什么还被墙? 26 | - 某些 Golang 灵车代理即使不用 uTLS,直接使用 Golang TLS 栈,为什么不被墙? 27 | 28 | ## 关于默认的 PaddingScheme 29 | 30 | 默认 PaddingScheme 只是一个示例。本项目无法确保默认参数不会被墙,因此设计了更新参数改变流量特征的机制。我们相信,实现成本低廉且易于改变的流量特征更有可能达到阻碍审查研究的目的。 31 | 32 | ## 如何更改 PaddingScheme 33 | 34 | 服务器设置 `--padding-scheme ./padding.txt` 参数。 35 | 36 | ## 还有别的 PaddingScheme 吗 37 | 38 | 模拟 XTLS-Vision: 39 | 40 | ``` 41 | stop=3 42 | 0=900-1400 43 | 1=900-1400 44 | 2=900-1400 45 | ``` 46 | 47 | 模仿的不是特别像,但可以说明 XTLS-Vision 的弊端:写死的长度处理逻辑,只要 GFW 更新特征库就能识别。 48 | 49 | ## 命名疑问 / 更换传输层 50 | 51 | 事实上,如果您愿意,您可以将协议放置在其他传输加密层上,这需要一些代码,但不太多。 52 | 53 | 本协议主要负责的工作: 54 | 55 | 1. 合理的 TCP 连接复用与性能表现 (`proxy/session`) 56 | 2. 控制数据包长度模式,缓解“嵌套的TLS握手指纹识别” (`proxy/session` `proxy/padding`) 57 | 58 | 但是仅完成以上工作,仍然无法提供一个“好用”的代理。其他不得不完成的工作,例如加密,目前是依赖 TLS 完成的,因此协议取名为 AnyTLS。 59 | 60 | 更换其他传输层,您可能会失去 TLS 提供的安全保护。如果用于翻墙,还可能会触发不同的防火墙规则。 61 | 62 | **除了“过 CDN”或“牺牲安全性来减少延迟”外,我想不出更换传输层的理由。如果你想尝试,请自行承担风险。** 63 | 64 | ## 参考过的项目 65 | 66 | https://github.com/xtaci/smux 67 | 68 | https://github.com/3andne/restls 69 | 70 | https://github.com/SagerNet/sing-box 71 | 72 | https://github.com/klzgrad/naiveproxy 73 | 74 | ## 已知弱点 75 | 76 | 以下弱点目前可能不会轻易引发“被墙”(甚至可能在大规模 DPI 下很难被利用),且修复可能引发协议不兼容,因此 anytls v1 没有对这些弱点进行处理。 77 | 78 | - TLS over TLS 需要比普通 h2 请求更多的握手往返,也就是说,没有 h2 请求需要这么多的来回握手。除了进行 MITM 代理、破坏 E2E 加密之外,没有简单的方法可以避免这种情况。 79 | - anytls 没有处理下行流量。虽然处理下行包特征会损失性能,但总的来说这一点很容易修复且不会破坏兼容性。 80 | - anytls 现有的 PaddingScheme 语法对单个包的长度指定只有“单一固定长度”和“单一范围内随机”两种模式。此外 PaddingScheme 处理完毕后的剩余数据也只能直接发送。要修复这点,需要重新设计一套更复杂的 PaddingScheme 语法。 81 | - anytls 几乎同时地发送三个或更多的数据包,特别是在 TLS 握手后的第一个 RTT 之内。即使单个数据包的长度符合某种被豁免的特征,也仍有可能被用于 `到达时间 - 包长 - 包数量` 和 `到达时间 - 通信数据量` 等统计。要修复这点,需要设置更多的缓冲区,实现将 auth 和 cmdSettings 等包合并发送,这会破坏现有 PaddingScheme 的语义。 82 | - 即使修复了上一条所述的问题,包计数器仍然不一定能代表发包的时机,因为不可能预测被代理方的发送时机。 83 | - 目前不清楚客户端初始化时使用默认 PaddingScheme 发起的少量连接,以及某些机械性的测试连接是否会对整体统计造成影响? 84 | - 目前不清楚 GFW 对 TLS 连接会持续跟踪多久。 85 | - TLS over TLS 开销导致可见的数据包长度增大和小数据包的缺失。消除这种开销还需要 MITM 代理。 86 | - TLS over TLS 开销还会导致数据包持续超过 MTU 限制,这对于原始用户代理来说不应该发生。 87 | - 由于这不是 HTTP 服务器,仍然可能存在主动探测问题,即使 gfw 对翻墙协议的主动探测似乎已不常见。 88 | -------------------------------------------------------------------------------- /docs/protocol.md: -------------------------------------------------------------------------------- 1 | # 协议说明 2 | 3 | ## 客户端 4 | 5 | ### 认证 6 | 7 | 本协议基于 TLS 协议,TLS 握手完成后客户端立即发送认证请求: 8 | 9 | | sha256(password) | padding0 length | padding0 | 10 | |--|--|--| 11 | | 32 Bytes | Big-Endian uint16 | 可变长度 | 12 | 13 | 认证成功服务器会进入会话循环,认证失败服务器会关闭连接(或 fallback 到 http 服务)。 14 | 15 | ### 会话 16 | 17 | 认证完成后,客户端&服务器在 TLS 协议之上开启会话层事件循环,会话层 frame 格式如下: 18 | 19 | | command | streamId | data length | data | 20 | |--|--|--|--| 21 | | uint8 | Big-Endian uint32 | Big-Endian uint16 | 可变长度 | 22 | 23 | **客户端每次开启新会话必须立即发送 `cmdSettings`。** 24 | 25 | #### command 26 | 27 | ``` 28 | // Since version 1 29 | 30 | cmdWaste = 0 // Paddings 31 | cmdSYN = 1 // stream open 32 | cmdPSH = 2 // data push 33 | cmdFIN = 3 // stream close, a.k.a EOF mark 34 | cmdSettings = 4 // Settings(客户端向服务器发送) 35 | cmdAlert = 5 // Alert(服务器向客户端发送) 36 | cmdUpdatePaddingScheme = 6 // update padding scheme(服务器向客户端发送) 37 | 38 | // Since version 2 39 | 40 | cmdSYNACK = 7 // Server reports to the client that the stream has been opened 41 | cmdHeartRequest = 8 // Keep alive command 42 | cmdHeartResponse = 9 // Keep alive command 43 | cmdServerSettings = 10 // Settings (Server send to client) 44 | ``` 45 | 46 | 对于不同类型的 command,除非下方说明有提到,否则该类型 command 不应也不能携带 data。 47 | 48 | #### cmdWaste 49 | 50 | 任意一方收到 cmdWaste 后都应将其 data 完整读出并无声丢弃。 51 | 52 | #### cmdHeartRequest 53 | 54 | 任意一方收到 cmdHeartRequest 后,应向对方发送 cmdHeartResponse 55 | 56 | #### cmdSYN 57 | 58 | 客户端通知服务器打开一条新的 Stream。客户端应为每个 Stream 生成在 Session 内单调递增的 streamId。 59 | 60 | #### cmdSYNACK 61 | 62 | 若客户端上报的版本 `v` >= 2,服务器收到 cmdSYN 后应在代理出站连接 TCP 握手完成后,发送带有对应 streamId 的 cmdSYNACK 回包。 63 | 64 | 如果您的服务器软件架构不支持回报出站连接状态,也可以在收到 cmdSYN 后直接发送 cmdSYNACK。 65 | 66 | cmdSYNACK 若不带有 data,则表示代理 stream 握手成功。若带有 data,则 data 代表错误信息。客户端收到错误信息后必须关闭对应 stream。 67 | 68 | #### cmdPSH 69 | 70 | 本命令的 data 承载 Stream 的传输数据。 71 | 72 | #### cmdFIN 73 | 74 | 通知对方关闭对应 streamId 的 Stream。 75 | 76 | #### cmdSettings 77 | 78 | 其 data 目前为: 79 | 80 | ``` 81 | v=2 82 | client=anytls/0.0.1 83 | padding-md5=(md5) 84 | ``` 85 | 86 | > 采用 UTF-8 编码,key 与 value 之间用 `=` 连接,两者均为 string 类型。不同项目之间用 `\n` 分割。 87 | 88 | - `v` 是客户端实现的协议版本号 (目前为 `2`) 89 | - `client` 是客户端软件名称与版本号(第三方实现请填写真实的软件名称与版本号,伪装没有任何意义) 90 | - `padding-md5` 是客户端当前 `paddingScheme` 的 md5 (小写 hex 编码) 91 | 92 | #### cmdServerSettings 93 | 94 | 其 data 目前为: 95 | 96 | ``` 97 | v=2 98 | ``` 99 | 100 | - `v` 是服务器实现的协议版本号 (目前为 `2`) 101 | 102 | #### cmdAlert 103 | 104 | 其 data 为服务器发送的警告文本信息,客户端需要将其读出并打印到日志,然后双方关闭会话。 105 | 106 | #### cmdUpdatePaddingScheme 107 | 108 | 当服务器收到客户端的 `padding-md5` 不同于服务器时,会发送 `cmdUpdatePaddingScheme` 向客户端请求更新,其 data 目前格式如下: 109 | 110 | > Default Padding Schme 111 | 112 | ``` 113 | stop=8 114 | 0=30-30 115 | 1=100-400 116 | 2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000 117 | 3=9-9,500-1000 118 | 4=500-1000 119 | 5=500-1000 120 | 6=500-1000 121 | 7=500-1000 122 | ``` 123 | 124 | - 客户端应在 Client 对象存储 `paddingScheme`,即服务器下发的 `paddingScheme` 只作用于连接到该服务器的 Client 125 | - 客户端第一次会话连接使用默认的 `paddingScheme`,如果收到 `cmdUpdatePaddingScheme` 后续新建会话则必须使用服务器下发的 `paddingScheme` 126 | 127 | > 有了这个设计,当默认 paddingScheme 产生的流量特征被 GFW 列入黑名单时,理论上每个客户端启动时只需要发送少量数据(理想情况下只有第一个连接的 pkt 0~2),在收到服务器的首个 `cmdUpdatePaddingScheme` 后就能更新为服务器指定的特征。因此,理论上可以被 GFW 捕获的已知特征的连接的比例将非常低。 128 | 129 | #### paddingScheme 具体含义与实现 130 | 131 | > stop 132 | 133 | `stop` 表示在第几个包停止处理 padding 比如: `stop=8` 代表只处理第 `0~7` 个包。 134 | 135 | > padding0 136 | 137 | `padding0` 也就是第 `0` 个包,处于认证部分,不支持分包。客户端应将该长度的 padding 与 sha265(password) 一并发送。 138 | 139 | 提示:认证部分的开销为 34 字节。 140 | 141 | > padding1 开始 142 | 143 | - padding1 开始处于会话部分,采用策略分包和/或填充:如果分包发送完之后,用户数据仍然有剩余,则直接发送剩余数据。如果分包发送完之前,用户数据已发送完毕,则发送 `cmdWaste` 携带数据(建议用 0)做填充。 144 | - 策略示例:上述 paddingScheme 将包 `2` 将分成 5 个尺寸在 400-500 / 500-1000 的包发送(这里的尺寸指 TLS PlainText 的尺寸,不计算 TLS 加密等开销)。 145 | - 策略中的 `c` 是检查符号,含义:若上一个分包发送完毕后,用户数据已无剩余,则直接对本次 Write TLS 返回,不再发送后续的填充包。 146 | - 包计数器以 Write TLS 的次数为准,包 `1` 应该包括:`cmdSettings` 和首个 Stream 的 `cmdSYN + cmdPSH(代理目标地址)` 147 | - 包 `2` 应该是代理自用户的第一个数据包,比如 TLS ClientHello。 148 | - 假如在 stop 之前的某个包的发送策略没有被 PaddingScheme 定义,那么直接发送该包。 149 | 150 | 参考处理逻辑在 `func (s *Session) writeConn()` 151 | 152 | ### 复用 153 | 154 | **客户端必须实现会话层复用功能。** 总体架构为: 155 | 156 | > TCP Proxy -> Stream -> Session -> TLS -> TCP 157 | 158 | 复用的具体逻辑: 159 | 160 | 创建新的会话层之前必须检查是否有“空闲”的会话,如果有则取 `Seq` 最大的会话,在该 Session 上开启 Stream 承载用户代理请求。 161 | 162 | 如果没有空闲的会话,则创建新的会话,Session 的序号 `Seq` 在一个 Client 内应单调递增。 163 | 164 | Stream 在代理中继完毕被关闭时,如果对应 Session 的事件循环未遇到错误,则将 Session 放入“空闲会话池”,并且设置 Session 的空闲起始时间为 now。 165 | 166 | 定期(如 30s)检查会话池,关闭并删除持续空闲超过一定时间(如 60s)的会话。 167 | 168 | > 以上复用策略高度概括:优先复用最新的会话,优先清理最老的会话。 169 | 170 | ### 代理 171 | 172 | 对于 TCP,每个 Stream 打开后,客户端向服务器发送 [SocksAddr](https://tools.ietf.org/html/rfc1928#section-5) 格式表示代理请求的目标地址,然后开始双向代理中继。 173 | 174 | 对于 UDP,现在使用 sing-box 的 [udp-over-tcp 2](https://sing-box.sagernet.org/configuration/shared/udp-over-tcp/#protocol-version-2) 协议,相当于代理请求 TCP `sp.v2.udp-over-tcp.arpa`。 175 | 176 | ## 服务器 177 | 178 | ### 认证 179 | 180 | 服务器基于 TLS Server 运行,对于每个 Accpted TLS Connection 认证的方式为: 181 | 182 | 读出第一个数据包,校验认证请求(包括完整读出 padding0),如果符合,则开始会话循环。如果不符合,则直接关闭连接或 "[fallback](https://trojan-gfw.github.io/trojan/protocol.html#:~:text=Anti%2Ddetection-,Active%20Detection,-All%20connection%20without)" 到任意 "合法" L7 应用。 183 | 184 | ### 会话 185 | 186 | 会话层格式和命令见客户端。 187 | 188 | 对于一个新 Session,如果服务器在收到客户端的 `cmdSettings` 之前收到 `cmdSYN`,必须拒绝此次会话。 189 | 190 | 服务器有权拒绝未正确实现本协议(包括但不限于 `cmdUpdatePaddingScheme` 和连接复用)、版本过旧(有已知问题)的客户端连接。 191 | 192 | 当服务器拒绝这类客户端时,必须发送 `cmdAlert` 说明原因,然后关闭 Session。 193 | 194 | 当客户端上报的版本 `v` >= 2,服务器收到 cmdSettings 后应立即发送 cmdServerSettings。 195 | 196 | ### 代理 197 | 198 | 代理中继完毕后,服务器关闭 Stream 但不要关闭 Session。 199 | 200 | 服务器可以定期清理长期无上下行的 Session。 201 | 202 | 对于目标地址为 `sp.v2.udp-over-tcp.arpa` 的请求,则应该使用 sing-box udp-over-tcp 协议处理。 203 | 204 | ## 协议参数 205 | 206 | anytls 协议参数不包括 TLS 的参数。应该在另外的配置分区中指定 TLS 参数。 207 | 208 | ### 客户端 209 | 210 | - `password` 必选,string 类型,协议认证的密码。 211 | - `idleSessionCheckInterval` 可选,time.Duration 类型,检查空闲会话的间隔时间。 212 | - `idleSessionTimeout` 可选,time.Duration 类型,在检查中,关闭空闲时间超过此时长的会话。 213 | - `minIdleSession` 可选,int 类型,在检查中,至少保留前 n 个空闲会话不关闭,即为后续代理保留一定数量的“预备会话”。 214 | 215 | ### 服务器 216 | 217 | - `paddingScheme` 可选,string 类型,填充方案。 218 | 219 | ## 更新记录 220 | 221 | ### 协议版本 2 222 | 223 | > `anytls-go` v0.0.7+ 224 | 225 | > `sing-anytls` v0.0.7+ ( `sing-box` 1.12.0-alpha.21+ ) 226 | 227 | > `mihomo` Prerelease-Alpha 2025.3.27+ 228 | 229 | 本次协议更新主要是为了应对隧道连接卡住的问题,实现更好的超时处理。 230 | 231 | 仅当您的服务器和客户端都支持版本 2 时,才应该启用以下特性。否则,两端都将按照版本 1 运行。 232 | 233 | - 可以使用 cmdSYNACK 回报服务器出站连接状态,同时检测并恢复卡住的隧道连接 234 | - 可以使用主动心跳包 (cmdHeartRequest cmdHeartResponse) 检测并恢复卡住的隧道连接 235 | - 服务器可以向客户端发送协商信息 (cmdServerSettings) 236 | 237 | 版本协商原理: 238 | 239 | - v2 服务器 + v1 客户端:由于客户端发送的版本为 1,服务器直接禁用版本 2 特性。 240 | - v1 服务器 + v2 客户端:由于客户端发送的版本为 2,服务器不认识,也不会向客户端发送 cmdServerSettings。客户端没有收到 cmdServerSettings 提示的版本,默认版本为 1,则不启用版本 2 特性。 241 | 242 | #### cmdSYNACK 243 | 244 | 当隧道连接意外断开且客户端未收到 RST 时,协议版本 1 的行为在极端情况下可能会导致很长的超时(取决于系统设置)。 245 | 246 | 由于在版本 2 客户端打开 stream 时可以期待来自服务器的回复,如果长时间未收到回复,则代表可能网络出现问题,客户端可以提前关闭卡住的连接。 247 | -------------------------------------------------------------------------------- /docs/uri_scheme.md: -------------------------------------------------------------------------------- 1 | # URI 格式 2 | 3 | AnyTLS 的 URI 格式旨在提供一种简洁的方式来表示连接到 AnyTLS 服务器所需的必要信息。它包括各种参数,如服务器地址、验证密码,TLS 设置等。 4 | 5 | 本格式参考了 [Hysteria2](https://v2.hysteria.network/zh/docs/developers/URI-Scheme/) 6 | 7 | ## 结构 8 | 9 | ``` 10 | anytls://[auth@]hostname[:port]/?[key=value]&[key=value]... 11 | ``` 12 | 13 | ## 组件 14 | 15 | ### 协议名 16 | 17 | `anytls` 18 | 19 | ### 验证 20 | 21 | 验证密码应在 URI 的 `auth` 中指定。这部分实际上就是标准 URI 格式中的用户名部分,因此如果包含特殊字符,需要进行 [百分号编码](https://datatracker.ietf.org/doc/html/rfc3986#section-2.1)。 22 | 23 | ### 地址 24 | 25 | 服务器的地址和可选端口。如果省略端口,则默认为 443。 26 | 27 | ### 参数 28 | 29 | - `sni`:用于 TLS 连接的服务器 SNI。(特殊情况:当 `sni` 的值为 [IP 地址](https://datatracker.ietf.org/doc/html/rfc6066#section-3:~:text=Literal%20IPv4%20and%20IPv6%20addresses%20are%20not%20permitted%20in%20%22HostName%22.)时,客户端必须不发送 SNI) 30 | 31 | - `insecure`:是否允许不安全的 TLS 连接。接受 `1` 表示 `true`,`0` 表示 `false`。 32 | 33 | ## 示例 34 | 35 | ``` 36 | anytls://letmein@example.com/?sni=real.example.com 37 | anytls://letmein@example.com/?sni=127.0.0.1&insecure=1 38 | anytls://0fdf77d7-d4ba-455e-9ed9-a98dd6d5489a@[2409:8a71:6a00:1953::615]:8964/?insecure=1 39 | ``` 40 | 41 | ## 注意事项 42 | 43 | 这个 URI 故意只包含连接到 AnyTLS 服务器所需的基础信息。尽管第三方实现可以根据需要添加额外的参数,但它们不应假设其他实现能理解这些额外参数。 44 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module anytls 2 | 3 | go 1.23.6 4 | 5 | require ( 6 | github.com/chen3feng/stl4go v0.1.1 7 | github.com/sagernet/sing v0.5.1 8 | github.com/sirupsen/logrus v1.9.3 9 | ) 10 | 11 | require golang.org/x/sys v0.29.0 // indirect 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chen3feng/stl4go v0.1.1 h1:0L1+mDw7pomftKDruM23f1mA7miavOj6C6MZeadzN2Q= 2 | github.com/chen3feng/stl4go v0.1.1/go.mod h1:5ml3psLgETJjRJnMbPE+JiHLrCpt+Ajc2weeTECXzWU= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y= 9 | github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= 10 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 11 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 12 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 14 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 15 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 16 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 17 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 18 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 20 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 21 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 22 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 23 | -------------------------------------------------------------------------------- /proxy/padding/padding.go: -------------------------------------------------------------------------------- 1 | package padding 2 | 3 | import ( 4 | "anytls/util" 5 | "crypto/md5" 6 | "crypto/rand" 7 | "fmt" 8 | "math/big" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/sagernet/sing/common/atomic" 13 | ) 14 | 15 | const CheckMark = -1 16 | 17 | var defaultPaddingScheme = []byte(`stop=8 18 | 0=30-30 19 | 1=100-400 20 | 2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000 21 | 3=9-9,500-1000 22 | 4=500-1000 23 | 5=500-1000 24 | 6=500-1000 25 | 7=500-1000`) 26 | 27 | type PaddingFactory struct { 28 | scheme util.StringMap 29 | RawScheme []byte 30 | Stop uint32 31 | Md5 string 32 | } 33 | 34 | var DefaultPaddingFactory atomic.TypedValue[*PaddingFactory] 35 | 36 | func init() { 37 | UpdatePaddingScheme(defaultPaddingScheme) 38 | } 39 | 40 | func UpdatePaddingScheme(rawScheme []byte) bool { 41 | if p := NewPaddingFactory(rawScheme); p != nil { 42 | DefaultPaddingFactory.Store(p) 43 | return true 44 | } 45 | return false 46 | } 47 | 48 | func NewPaddingFactory(rawScheme []byte) *PaddingFactory { 49 | p := &PaddingFactory{ 50 | RawScheme: rawScheme, 51 | Md5: fmt.Sprintf("%x", md5.Sum(rawScheme)), 52 | } 53 | scheme := util.StringMapFromBytes(rawScheme) 54 | if len(scheme) == 0 { 55 | return nil 56 | } 57 | if stop, err := strconv.Atoi(scheme["stop"]); err == nil { 58 | p.Stop = uint32(stop) 59 | } else { 60 | return nil 61 | } 62 | p.scheme = scheme 63 | return p 64 | } 65 | 66 | func (p *PaddingFactory) GenerateRecordPayloadSizes(pkt uint32) (pktSizes []int) { 67 | if s, ok := p.scheme[strconv.Itoa(int(pkt))]; ok { 68 | sRanges := strings.Split(s, ",") 69 | for _, sRange := range sRanges { 70 | sRangeMinMax := strings.Split(sRange, "-") 71 | if len(sRangeMinMax) == 2 { 72 | _min, err := strconv.ParseInt(sRangeMinMax[0], 10, 64) 73 | if err != nil { 74 | continue 75 | } 76 | _max, err := strconv.ParseInt(sRangeMinMax[1], 10, 64) 77 | if err != nil { 78 | continue 79 | } 80 | _min, _max = min(_min, _max), max(_min, _max) 81 | if _min <= 0 || _max <= 0 { 82 | continue 83 | } 84 | if _min == _max { 85 | pktSizes = append(pktSizes, int(_min)) 86 | } else { 87 | i, _ := rand.Int(rand.Reader, big.NewInt(_max-_min)) 88 | pktSizes = append(pktSizes, int(i.Int64()+_min)) 89 | } 90 | } else if sRange == "c" { 91 | pktSizes = append(pktSizes, CheckMark) 92 | } 93 | } 94 | } 95 | return 96 | } 97 | -------------------------------------------------------------------------------- /proxy/pipe/deadline.go: -------------------------------------------------------------------------------- 1 | package pipe 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // PipeDeadline is an abstraction for handling timeouts. 9 | type PipeDeadline struct { 10 | mu sync.Mutex // Guards timer and cancel 11 | timer *time.Timer 12 | cancel chan struct{} // Must be non-nil 13 | } 14 | 15 | func MakePipeDeadline() PipeDeadline { 16 | return PipeDeadline{cancel: make(chan struct{})} 17 | } 18 | 19 | // Set sets the point in time when the deadline will time out. 20 | // A timeout event is signaled by closing the channel returned by waiter. 21 | // Once a timeout has occurred, the deadline can be refreshed by specifying a 22 | // t value in the future. 23 | // 24 | // A zero value for t prevents timeout. 25 | func (d *PipeDeadline) Set(t time.Time) { 26 | d.mu.Lock() 27 | defer d.mu.Unlock() 28 | 29 | if d.timer != nil && !d.timer.Stop() { 30 | <-d.cancel // Wait for the timer callback to finish and close cancel 31 | } 32 | d.timer = nil 33 | 34 | // Time is zero, then there is no deadline. 35 | closed := isClosedChan(d.cancel) 36 | if t.IsZero() { 37 | if closed { 38 | d.cancel = make(chan struct{}) 39 | } 40 | return 41 | } 42 | 43 | // Time in the future, setup a timer to cancel in the future. 44 | if dur := time.Until(t); dur > 0 { 45 | if closed { 46 | d.cancel = make(chan struct{}) 47 | } 48 | d.timer = time.AfterFunc(dur, func() { 49 | close(d.cancel) 50 | }) 51 | return 52 | } 53 | 54 | // Time in the past, so close immediately. 55 | if !closed { 56 | close(d.cancel) 57 | } 58 | } 59 | 60 | // Wait returns a channel that is closed when the deadline is exceeded. 61 | func (d *PipeDeadline) Wait() chan struct{} { 62 | d.mu.Lock() 63 | defer d.mu.Unlock() 64 | return d.cancel 65 | } 66 | 67 | func isClosedChan(c <-chan struct{}) bool { 68 | select { 69 | case <-c: 70 | return true 71 | default: 72 | return false 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /proxy/pipe/io_pipe.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Pipe adapter to connect code expecting an io.Reader 6 | // with code expecting an io.Writer. 7 | 8 | package pipe 9 | 10 | import ( 11 | "io" 12 | "os" 13 | "sync" 14 | "time" 15 | ) 16 | 17 | // onceError is an object that will only store an error once. 18 | type onceError struct { 19 | sync.Mutex // guards following 20 | err error 21 | } 22 | 23 | func (a *onceError) Store(err error) { 24 | a.Lock() 25 | defer a.Unlock() 26 | if a.err != nil { 27 | return 28 | } 29 | a.err = err 30 | } 31 | func (a *onceError) Load() error { 32 | a.Lock() 33 | defer a.Unlock() 34 | return a.err 35 | } 36 | 37 | // A pipe is the shared pipe structure underlying PipeReader and PipeWriter. 38 | type pipe struct { 39 | wrMu sync.Mutex // Serializes Write operations 40 | wrCh chan []byte 41 | rdCh chan int 42 | 43 | once sync.Once // Protects closing done 44 | done chan struct{} 45 | rerr onceError 46 | werr onceError 47 | 48 | readDeadline PipeDeadline 49 | writeDeadline PipeDeadline 50 | } 51 | 52 | func (p *pipe) read(b []byte) (n int, err error) { 53 | select { 54 | case <-p.done: 55 | return 0, p.readCloseError() 56 | case <-p.readDeadline.Wait(): 57 | return 0, os.ErrDeadlineExceeded 58 | default: 59 | } 60 | 61 | select { 62 | case bw := <-p.wrCh: 63 | nr := copy(b, bw) 64 | p.rdCh <- nr 65 | return nr, nil 66 | case <-p.done: 67 | return 0, p.readCloseError() 68 | case <-p.readDeadline.Wait(): 69 | return 0, os.ErrDeadlineExceeded 70 | } 71 | } 72 | 73 | func (p *pipe) closeRead(err error) error { 74 | if err == nil { 75 | err = io.ErrClosedPipe 76 | } 77 | p.rerr.Store(err) 78 | p.once.Do(func() { close(p.done) }) 79 | return nil 80 | } 81 | 82 | func (p *pipe) write(b []byte) (n int, err error) { 83 | select { 84 | case <-p.done: 85 | return 0, p.writeCloseError() 86 | case <-p.writeDeadline.Wait(): 87 | return 0, os.ErrDeadlineExceeded 88 | default: 89 | p.wrMu.Lock() 90 | defer p.wrMu.Unlock() 91 | } 92 | 93 | for once := true; once || len(b) > 0; once = false { 94 | select { 95 | case p.wrCh <- b: 96 | nw := <-p.rdCh 97 | b = b[nw:] 98 | n += nw 99 | case <-p.done: 100 | return n, p.writeCloseError() 101 | case <-p.writeDeadline.Wait(): 102 | return n, os.ErrDeadlineExceeded 103 | } 104 | } 105 | return n, nil 106 | } 107 | 108 | func (p *pipe) closeWrite(err error) error { 109 | if err == nil { 110 | err = io.EOF 111 | } 112 | p.werr.Store(err) 113 | p.once.Do(func() { close(p.done) }) 114 | return nil 115 | } 116 | 117 | // readCloseError is considered internal to the pipe type. 118 | func (p *pipe) readCloseError() error { 119 | rerr := p.rerr.Load() 120 | if werr := p.werr.Load(); rerr == nil && werr != nil { 121 | return werr 122 | } 123 | return io.ErrClosedPipe 124 | } 125 | 126 | // writeCloseError is considered internal to the pipe type. 127 | func (p *pipe) writeCloseError() error { 128 | werr := p.werr.Load() 129 | if rerr := p.rerr.Load(); werr == nil && rerr != nil { 130 | return rerr 131 | } 132 | return io.ErrClosedPipe 133 | } 134 | 135 | // A PipeReader is the read half of a pipe. 136 | type PipeReader struct{ pipe } 137 | 138 | // Read implements the standard Read interface: 139 | // it reads data from the pipe, blocking until a writer 140 | // arrives or the write end is closed. 141 | // If the write end is closed with an error, that error is 142 | // returned as err; otherwise err is EOF. 143 | func (r *PipeReader) Read(data []byte) (n int, err error) { 144 | return r.pipe.read(data) 145 | } 146 | 147 | // Close closes the reader; subsequent writes to the 148 | // write half of the pipe will return the error [ErrClosedPipe]. 149 | func (r *PipeReader) Close() error { 150 | return r.CloseWithError(nil) 151 | } 152 | 153 | // CloseWithError closes the reader; subsequent writes 154 | // to the write half of the pipe will return the error err. 155 | // 156 | // CloseWithError never overwrites the previous error if it exists 157 | // and always returns nil. 158 | func (r *PipeReader) CloseWithError(err error) error { 159 | return r.pipe.closeRead(err) 160 | } 161 | 162 | // A PipeWriter is the write half of a pipe. 163 | type PipeWriter struct{ r PipeReader } 164 | 165 | // Write implements the standard Write interface: 166 | // it writes data to the pipe, blocking until one or more readers 167 | // have consumed all the data or the read end is closed. 168 | // If the read end is closed with an error, that err is 169 | // returned as err; otherwise err is [ErrClosedPipe]. 170 | func (w *PipeWriter) Write(data []byte) (n int, err error) { 171 | return w.r.pipe.write(data) 172 | } 173 | 174 | // Close closes the writer; subsequent reads from the 175 | // read half of the pipe will return no bytes and EOF. 176 | func (w *PipeWriter) Close() error { 177 | return w.CloseWithError(nil) 178 | } 179 | 180 | // CloseWithError closes the writer; subsequent reads from the 181 | // read half of the pipe will return no bytes and the error err, 182 | // or EOF if err is nil. 183 | // 184 | // CloseWithError never overwrites the previous error if it exists 185 | // and always returns nil. 186 | func (w *PipeWriter) CloseWithError(err error) error { 187 | return w.r.pipe.closeWrite(err) 188 | } 189 | 190 | // Pipe creates a synchronous in-memory pipe. 191 | // It can be used to connect code expecting an [io.Reader] 192 | // with code expecting an [io.Writer]. 193 | // 194 | // Reads and Writes on the pipe are matched one to one 195 | // except when multiple Reads are needed to consume a single Write. 196 | // That is, each Write to the [PipeWriter] blocks until it has satisfied 197 | // one or more Reads from the [PipeReader] that fully consume 198 | // the written data. 199 | // The data is copied directly from the Write to the corresponding 200 | // Read (or Reads); there is no internal buffering. 201 | // 202 | // It is safe to call Read and Write in parallel with each other or with Close. 203 | // Parallel calls to Read and parallel calls to Write are also safe: 204 | // the individual calls will be gated sequentially. 205 | // 206 | // Added SetReadDeadline and SetWriteDeadline methods based on `io.Pipe`. 207 | func Pipe() (*PipeReader, *PipeWriter) { 208 | pw := &PipeWriter{r: PipeReader{pipe: pipe{ 209 | wrCh: make(chan []byte), 210 | rdCh: make(chan int), 211 | done: make(chan struct{}), 212 | readDeadline: MakePipeDeadline(), 213 | writeDeadline: MakePipeDeadline(), 214 | }}} 215 | return &pw.r, pw 216 | } 217 | 218 | func (p *PipeReader) SetReadDeadline(t time.Time) error { 219 | if isClosedChan(p.done) { 220 | return io.ErrClosedPipe 221 | } 222 | p.readDeadline.Set(t) 223 | return nil 224 | } 225 | 226 | func (p *PipeWriter) SetWriteDeadline(t time.Time) error { 227 | if isClosedChan(p.r.done) { 228 | return io.ErrClosedPipe 229 | } 230 | p.r.writeDeadline.Set(t) 231 | return nil 232 | } 233 | -------------------------------------------------------------------------------- /proxy/session/client.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "anytls/proxy/padding" 5 | "anytls/util" 6 | "context" 7 | "fmt" 8 | "io" 9 | "math" 10 | "net" 11 | "sync" 12 | "time" 13 | 14 | "github.com/chen3feng/stl4go" 15 | "github.com/sagernet/sing/common/atomic" 16 | ) 17 | 18 | type Client struct { 19 | die context.Context 20 | dieCancel context.CancelFunc 21 | 22 | dialOut util.DialOutFunc 23 | 24 | sessionCounter atomic.Uint64 25 | 26 | idleSession *stl4go.SkipList[uint64, *Session] 27 | idleSessionLock sync.Mutex 28 | 29 | sessions map[uint64]*Session 30 | sessionsLock sync.Mutex 31 | 32 | padding *atomic.TypedValue[*padding.PaddingFactory] 33 | 34 | idleSessionTimeout time.Duration 35 | minIdleSession int 36 | } 37 | 38 | func NewClient(ctx context.Context, dialOut util.DialOutFunc, 39 | _padding *atomic.TypedValue[*padding.PaddingFactory], idleSessionCheckInterval, idleSessionTimeout time.Duration, minIdleSession int, 40 | ) *Client { 41 | c := &Client{ 42 | sessions: make(map[uint64]*Session), 43 | dialOut: dialOut, 44 | padding: _padding, 45 | idleSessionTimeout: idleSessionTimeout, 46 | minIdleSession: minIdleSession, 47 | } 48 | if idleSessionCheckInterval <= time.Second*5 { 49 | idleSessionCheckInterval = time.Second * 30 50 | } 51 | if c.idleSessionTimeout <= time.Second*5 { 52 | c.idleSessionTimeout = time.Second * 30 53 | } 54 | c.die, c.dieCancel = context.WithCancel(ctx) 55 | c.idleSession = stl4go.NewSkipList[uint64, *Session]() 56 | util.StartRoutine(c.die, idleSessionCheckInterval, c.idleCleanup) 57 | return c 58 | } 59 | 60 | func (c *Client) CreateStream(ctx context.Context) (net.Conn, error) { 61 | select { 62 | case <-c.die.Done(): 63 | return nil, io.ErrClosedPipe 64 | default: 65 | } 66 | 67 | var session *Session 68 | var stream *Stream 69 | var err error 70 | 71 | for i := 0; i < 3; i++ { 72 | session, err = c.findSession(ctx) 73 | if session == nil { 74 | return nil, fmt.Errorf("failed to create session: %w", err) 75 | } 76 | stream, err = session.OpenStream() 77 | if err != nil { 78 | session.Close() 79 | continue 80 | } 81 | break 82 | } 83 | if session == nil || stream == nil { 84 | return nil, fmt.Errorf("too many closed session: %w", err) 85 | } 86 | 87 | stream.dieHook = func() { 88 | if !session.IsClosed() { 89 | select { 90 | case <-c.die.Done(): 91 | // Now client has been closed 92 | go session.Close() 93 | default: 94 | c.idleSessionLock.Lock() 95 | session.idleSince = time.Now() 96 | c.idleSession.Insert(math.MaxUint64-session.seq, session) 97 | c.idleSessionLock.Unlock() 98 | } 99 | } 100 | } 101 | 102 | return stream, nil 103 | } 104 | 105 | func (c *Client) findSession(ctx context.Context) (*Session, error) { 106 | var idle *Session 107 | 108 | c.idleSessionLock.Lock() 109 | if !c.idleSession.IsEmpty() { 110 | it := c.idleSession.Iterate() 111 | idle = it.Value() 112 | c.idleSession.Remove(it.Key()) 113 | } 114 | c.idleSessionLock.Unlock() 115 | 116 | if idle == nil { 117 | s, err := c.createSession(ctx) 118 | return s, err 119 | } 120 | return idle, nil 121 | } 122 | 123 | func (c *Client) createSession(ctx context.Context) (*Session, error) { 124 | underlying, err := c.dialOut(ctx) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | session := NewClientSession(underlying, &padding.DefaultPaddingFactory) 130 | session.seq = c.sessionCounter.Add(1) 131 | session.dieHook = func() { 132 | //logrus.Debugln("session died", session) 133 | c.idleSessionLock.Lock() 134 | c.idleSession.Remove(math.MaxUint64 - session.seq) 135 | c.idleSessionLock.Unlock() 136 | 137 | c.sessionsLock.Lock() 138 | delete(c.sessions, session.seq) 139 | c.sessionsLock.Unlock() 140 | } 141 | 142 | c.sessionsLock.Lock() 143 | c.sessions[session.seq] = session 144 | c.sessionsLock.Unlock() 145 | 146 | session.Run() 147 | return session, nil 148 | } 149 | 150 | func (c *Client) Close() error { 151 | c.dieCancel() 152 | 153 | c.sessionsLock.Lock() 154 | sessionToClose := make([]*Session, 0, len(c.sessions)) 155 | for _, session := range c.sessions { 156 | sessionToClose = append(sessionToClose, session) 157 | } 158 | c.sessions = make(map[uint64]*Session) 159 | c.sessionsLock.Unlock() 160 | 161 | for _, session := range sessionToClose { 162 | session.Close() 163 | } 164 | 165 | return nil 166 | } 167 | 168 | func (c *Client) idleCleanup() { 169 | c.idleCleanupExpTime(time.Now().Add(-c.idleSessionTimeout)) 170 | } 171 | 172 | func (c *Client) idleCleanupExpTime(expTime time.Time) { 173 | activeCount := 0 174 | var sessionToClose []*Session 175 | 176 | c.idleSessionLock.Lock() 177 | it := c.idleSession.Iterate() 178 | for it.IsNotEnd() { 179 | session := it.Value() 180 | key := it.Key() 181 | it.MoveToNext() 182 | 183 | if !session.idleSince.Before(expTime) { 184 | activeCount++ 185 | continue 186 | } 187 | 188 | if activeCount < c.minIdleSession { 189 | session.idleSince = time.Now() 190 | activeCount++ 191 | continue 192 | } 193 | 194 | sessionToClose = append(sessionToClose, session) 195 | c.idleSession.Remove(key) 196 | } 197 | c.idleSessionLock.Unlock() 198 | 199 | for _, session := range sessionToClose { 200 | session.Close() 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /proxy/session/frame.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "encoding/binary" 5 | ) 6 | 7 | const ( // cmds 8 | cmdWaste = 0 // Paddings 9 | cmdSYN = 1 // stream open 10 | cmdPSH = 2 // data push 11 | cmdFIN = 3 // stream close, a.k.a EOF mark 12 | cmdSettings = 4 // Settings (Client send to Server) 13 | cmdAlert = 5 // Alert 14 | cmdUpdatePaddingScheme = 6 // update padding scheme 15 | // Since version 2 16 | cmdSYNACK = 7 // Server reports to the client that the stream has been opened 17 | cmdHeartRequest = 8 // Keep alive command 18 | cmdHeartResponse = 9 // Keep alive command 19 | cmdServerSettings = 10 // Settings (Server send to client) 20 | ) 21 | 22 | const ( 23 | headerOverHeadSize = 1 + 4 + 2 24 | ) 25 | 26 | // frame defines a packet from or to be multiplexed into a single connection 27 | type frame struct { 28 | cmd byte // 1 29 | sid uint32 // 4 30 | data []byte // 2 + len(data) 31 | } 32 | 33 | func newFrame(cmd byte, sid uint32) frame { 34 | return frame{cmd: cmd, sid: sid} 35 | } 36 | 37 | type rawHeader [headerOverHeadSize]byte 38 | 39 | func (h rawHeader) Cmd() byte { 40 | return h[0] 41 | } 42 | 43 | func (h rawHeader) StreamID() uint32 { 44 | return binary.BigEndian.Uint32(h[1:]) 45 | } 46 | 47 | func (h rawHeader) Length() uint16 { 48 | return binary.BigEndian.Uint16(h[5:]) 49 | } 50 | -------------------------------------------------------------------------------- /proxy/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "anytls/proxy/padding" 5 | "anytls/util" 6 | "crypto/md5" 7 | "encoding/binary" 8 | "fmt" 9 | "io" 10 | "net" 11 | "os" 12 | "runtime/debug" 13 | "slices" 14 | "strconv" 15 | "sync" 16 | "time" 17 | 18 | "github.com/sagernet/sing/common/atomic" 19 | "github.com/sagernet/sing/common/buf" 20 | "github.com/sirupsen/logrus" 21 | ) 22 | 23 | var clientDebugPaddingScheme = os.Getenv("CLIENT_DEBUG_PADDING_SCHEME") == "1" 24 | 25 | type Session struct { 26 | conn net.Conn 27 | connLock sync.Mutex 28 | 29 | streams map[uint32]*Stream 30 | streamId atomic.Uint32 31 | streamLock sync.RWMutex 32 | 33 | dieOnce sync.Once 34 | die chan struct{} 35 | dieHook func() 36 | 37 | synDone func() 38 | synDoneLock sync.Mutex 39 | 40 | // pool 41 | seq uint64 42 | idleSince time.Time 43 | padding *atomic.TypedValue[*padding.PaddingFactory] 44 | 45 | peerVersion byte 46 | 47 | // client 48 | isClient bool 49 | sendPadding bool 50 | buffering bool 51 | buffer []byte 52 | pktCounter atomic.Uint32 53 | 54 | // server 55 | onNewStream func(stream *Stream) 56 | } 57 | 58 | func NewClientSession(conn net.Conn, _padding *atomic.TypedValue[*padding.PaddingFactory]) *Session { 59 | s := &Session{ 60 | conn: conn, 61 | isClient: true, 62 | sendPadding: true, 63 | padding: _padding, 64 | } 65 | s.die = make(chan struct{}) 66 | s.streams = make(map[uint32]*Stream) 67 | return s 68 | } 69 | 70 | func NewServerSession(conn net.Conn, onNewStream func(stream *Stream), _padding *atomic.TypedValue[*padding.PaddingFactory]) *Session { 71 | s := &Session{ 72 | conn: conn, 73 | onNewStream: onNewStream, 74 | padding: _padding, 75 | } 76 | s.die = make(chan struct{}) 77 | s.streams = make(map[uint32]*Stream) 78 | return s 79 | } 80 | 81 | func (s *Session) Run() { 82 | if !s.isClient { 83 | s.recvLoop() 84 | return 85 | } 86 | 87 | settings := util.StringMap{ 88 | "v": "2", 89 | "client": util.ProgramVersionName, 90 | "padding-md5": s.padding.Load().Md5, 91 | } 92 | f := newFrame(cmdSettings, 0) 93 | f.data = settings.ToBytes() 94 | s.buffering = true 95 | s.writeFrame(f) 96 | 97 | go s.recvLoop() 98 | } 99 | 100 | // IsClosed does a safe check to see if we have shutdown 101 | func (s *Session) IsClosed() bool { 102 | select { 103 | case <-s.die: 104 | return true 105 | default: 106 | return false 107 | } 108 | } 109 | 110 | // Close is used to close the session and all streams. 111 | func (s *Session) Close() error { 112 | var once bool 113 | s.dieOnce.Do(func() { 114 | close(s.die) 115 | once = true 116 | }) 117 | if once { 118 | if s.dieHook != nil { 119 | s.dieHook() 120 | s.dieHook = nil 121 | } 122 | s.streamLock.Lock() 123 | for _, stream := range s.streams { 124 | stream.Close() 125 | } 126 | s.streams = make(map[uint32]*Stream) 127 | s.streamLock.Unlock() 128 | return s.conn.Close() 129 | } else { 130 | return io.ErrClosedPipe 131 | } 132 | } 133 | 134 | // OpenStream is used to create a new stream for CLIENT 135 | func (s *Session) OpenStream() (*Stream, error) { 136 | if s.IsClosed() { 137 | return nil, io.ErrClosedPipe 138 | } 139 | 140 | sid := s.streamId.Add(1) 141 | stream := newStream(sid, s) 142 | 143 | //logrus.Debugln("stream open", sid, s.streams) 144 | 145 | if sid >= 2 && s.peerVersion >= 2 { 146 | s.synDoneLock.Lock() 147 | if s.synDone != nil { 148 | s.synDone() 149 | } 150 | s.synDone = util.NewDeadlineWatcher(time.Second*3, func() { 151 | s.Close() 152 | }) 153 | s.synDoneLock.Unlock() 154 | } 155 | 156 | if _, err := s.writeFrame(newFrame(cmdSYN, sid)); err != nil { 157 | return nil, err 158 | } 159 | 160 | s.buffering = false // proxy Write it's SocksAddr to flush the buffer 161 | 162 | s.streamLock.Lock() 163 | defer s.streamLock.Unlock() 164 | select { 165 | case <-s.die: 166 | return nil, io.ErrClosedPipe 167 | default: 168 | s.streams[sid] = stream 169 | return stream, nil 170 | } 171 | } 172 | 173 | func (s *Session) recvLoop() error { 174 | defer func() { 175 | if r := recover(); r != nil { 176 | logrus.Errorln("[BUG]", r, string(debug.Stack())) 177 | } 178 | }() 179 | defer s.Close() 180 | 181 | var receivedSettingsFromClient bool 182 | var hdr rawHeader 183 | 184 | for { 185 | if s.IsClosed() { 186 | return io.ErrClosedPipe 187 | } 188 | // read header first 189 | if _, err := io.ReadFull(s.conn, hdr[:]); err == nil { 190 | sid := hdr.StreamID() 191 | switch hdr.Cmd() { 192 | case cmdPSH: 193 | if hdr.Length() > 0 { 194 | buffer := buf.Get(int(hdr.Length())) 195 | if _, err := io.ReadFull(s.conn, buffer); err == nil { 196 | s.streamLock.RLock() 197 | stream, ok := s.streams[sid] 198 | s.streamLock.RUnlock() 199 | if ok { 200 | stream.pipeW.Write(buffer) 201 | } 202 | buf.Put(buffer) 203 | } else { 204 | buf.Put(buffer) 205 | return err 206 | } 207 | } 208 | case cmdSYN: // should be server only 209 | if !s.isClient && !receivedSettingsFromClient { 210 | f := newFrame(cmdAlert, 0) 211 | f.data = []byte("client did not send its settings") 212 | s.writeFrame(f) 213 | return nil 214 | } 215 | s.streamLock.Lock() 216 | if _, ok := s.streams[sid]; !ok { 217 | stream := newStream(sid, s) 218 | s.streams[sid] = stream 219 | go func() { 220 | if s.onNewStream != nil { 221 | s.onNewStream(stream) 222 | } else { 223 | stream.Close() 224 | } 225 | }() 226 | } 227 | s.streamLock.Unlock() 228 | case cmdSYNACK: // should be client only 229 | s.synDoneLock.Lock() 230 | if s.synDone != nil { 231 | s.synDone() 232 | s.synDone = nil 233 | } 234 | s.synDoneLock.Unlock() 235 | if hdr.Length() > 0 { 236 | buffer := buf.Get(int(hdr.Length())) 237 | if _, err := io.ReadFull(s.conn, buffer); err != nil { 238 | buf.Put(buffer) 239 | return err 240 | } 241 | // report error 242 | s.streamLock.RLock() 243 | stream, ok := s.streams[sid] 244 | s.streamLock.RUnlock() 245 | if ok { 246 | stream.CloseWithError(fmt.Errorf("remote: %s", string(buffer))) 247 | } 248 | buf.Put(buffer) 249 | } 250 | case cmdFIN: 251 | s.streamLock.RLock() 252 | stream, ok := s.streams[sid] 253 | s.streamLock.RUnlock() 254 | if ok { 255 | stream.Close() 256 | } 257 | //logrus.Debugln("stream fin", sid, s.streams) 258 | case cmdWaste: 259 | if hdr.Length() > 0 { 260 | buffer := buf.Get(int(hdr.Length())) 261 | if _, err := io.ReadFull(s.conn, buffer); err != nil { 262 | buf.Put(buffer) 263 | return err 264 | } 265 | buf.Put(buffer) 266 | } 267 | case cmdSettings: 268 | if hdr.Length() > 0 { 269 | buffer := buf.Get(int(hdr.Length())) 270 | if _, err := io.ReadFull(s.conn, buffer); err != nil { 271 | buf.Put(buffer) 272 | return err 273 | } 274 | if !s.isClient { 275 | receivedSettingsFromClient = true 276 | m := util.StringMapFromBytes(buffer) 277 | paddingF := s.padding.Load() 278 | if m["padding-md5"] != paddingF.Md5 { 279 | // logrus.Debugln("remote md5 is", m["padding-md5"]) 280 | f := newFrame(cmdUpdatePaddingScheme, 0) 281 | f.data = paddingF.RawScheme 282 | _, err = s.writeFrame(f) 283 | if err != nil { 284 | buf.Put(buffer) 285 | return err 286 | } 287 | } 288 | // check client's version 289 | if v, err := strconv.Atoi(m["v"]); err == nil && v >= 2 { 290 | s.peerVersion = byte(v) 291 | // send cmdServerSettings 292 | f := newFrame(cmdServerSettings, 0) 293 | f.data = util.StringMap{ 294 | "v": "2", 295 | }.ToBytes() 296 | _, err = s.writeFrame(f) 297 | if err != nil { 298 | buf.Put(buffer) 299 | return err 300 | } 301 | } 302 | } 303 | buf.Put(buffer) 304 | } 305 | case cmdAlert: 306 | if hdr.Length() > 0 { 307 | buffer := buf.Get(int(hdr.Length())) 308 | if _, err := io.ReadFull(s.conn, buffer); err != nil { 309 | buf.Put(buffer) 310 | return err 311 | } 312 | if s.isClient { 313 | logrus.Errorln("[Alert from server]", string(buffer)) 314 | } 315 | buf.Put(buffer) 316 | return nil 317 | } 318 | case cmdUpdatePaddingScheme: 319 | if hdr.Length() > 0 { 320 | // `rawScheme` Do not use buffer to prevent subsequent misuse 321 | rawScheme := make([]byte, int(hdr.Length())) 322 | if _, err := io.ReadFull(s.conn, rawScheme); err != nil { 323 | return err 324 | } 325 | if s.isClient && !clientDebugPaddingScheme { 326 | if padding.UpdatePaddingScheme(rawScheme) { 327 | logrus.Infof("[Update padding succeed] %x\n", md5.Sum(rawScheme)) 328 | } else { 329 | logrus.Warnf("[Update padding failed] %x\n", md5.Sum(rawScheme)) 330 | } 331 | } 332 | } 333 | case cmdHeartRequest: 334 | if _, err := s.writeFrame(newFrame(cmdHeartResponse, sid)); err != nil { 335 | return err 336 | } 337 | case cmdHeartResponse: 338 | // Active keepalive checking is not implemented yet 339 | break 340 | case cmdServerSettings: 341 | if hdr.Length() > 0 { 342 | buffer := buf.Get(int(hdr.Length())) 343 | if _, err := io.ReadFull(s.conn, buffer); err != nil { 344 | buf.Put(buffer) 345 | return err 346 | } 347 | if s.isClient { 348 | // check server's version 349 | m := util.StringMapFromBytes(buffer) 350 | if v, err := strconv.Atoi(m["v"]); err == nil { 351 | s.peerVersion = byte(v) 352 | } 353 | } 354 | buf.Put(buffer) 355 | } 356 | default: 357 | // I don't know what command it is (can't have data) 358 | } 359 | } else { 360 | return err 361 | } 362 | } 363 | } 364 | 365 | func (s *Session) streamClosed(sid uint32) error { 366 | if s.IsClosed() { 367 | return io.ErrClosedPipe 368 | } 369 | _, err := s.writeFrame(newFrame(cmdFIN, sid)) 370 | s.streamLock.Lock() 371 | delete(s.streams, sid) 372 | s.streamLock.Unlock() 373 | return err 374 | } 375 | 376 | func (s *Session) writeFrame(frame frame) (int, error) { 377 | dataLen := len(frame.data) 378 | 379 | buffer := buf.NewSize(dataLen + headerOverHeadSize) 380 | buffer.WriteByte(frame.cmd) 381 | binary.BigEndian.PutUint32(buffer.Extend(4), frame.sid) 382 | binary.BigEndian.PutUint16(buffer.Extend(2), uint16(dataLen)) 383 | buffer.Write(frame.data) 384 | _, err := s.writeConn(buffer.Bytes()) 385 | buffer.Release() 386 | if err != nil { 387 | return 0, err 388 | } 389 | 390 | return dataLen, nil 391 | } 392 | 393 | func (s *Session) writeConn(b []byte) (n int, err error) { 394 | s.connLock.Lock() 395 | defer s.connLock.Unlock() 396 | 397 | if s.buffering { 398 | s.buffer = slices.Concat(s.buffer, b) 399 | return len(b), nil 400 | } else if len(s.buffer) > 0 { 401 | b = slices.Concat(s.buffer, b) 402 | s.buffer = nil 403 | } 404 | 405 | // calulate & send padding 406 | if s.sendPadding { 407 | pkt := s.pktCounter.Add(1) 408 | paddingF := s.padding.Load() 409 | if pkt < paddingF.Stop { 410 | pktSizes := paddingF.GenerateRecordPayloadSizes(pkt) 411 | for _, l := range pktSizes { 412 | remainPayloadLen := len(b) 413 | if l == padding.CheckMark { 414 | if remainPayloadLen == 0 { 415 | break 416 | } else { 417 | continue 418 | } 419 | } 420 | // logrus.Debugln(pkt, "write", l, "len", remainPayloadLen, "remain", remainPayloadLen-l) 421 | if remainPayloadLen > l { // this packet is all payload 422 | _, err = s.conn.Write(b[:l]) 423 | if err != nil { 424 | return 0, err 425 | } 426 | n += l 427 | b = b[l:] 428 | } else if remainPayloadLen > 0 { // this packet contains padding and the last part of payload 429 | paddingLen := l - remainPayloadLen - headerOverHeadSize 430 | if paddingLen > 0 { 431 | padding := make([]byte, headerOverHeadSize+paddingLen) 432 | padding[0] = cmdWaste 433 | binary.BigEndian.PutUint32(padding[1:5], 0) 434 | binary.BigEndian.PutUint16(padding[5:7], uint16(paddingLen)) 435 | b = slices.Concat(b, padding) 436 | } 437 | _, err = s.conn.Write(b) 438 | if err != nil { 439 | return 0, err 440 | } 441 | n += remainPayloadLen 442 | b = nil 443 | } else { // this packet is all padding 444 | padding := make([]byte, headerOverHeadSize+l) 445 | padding[0] = cmdWaste 446 | binary.BigEndian.PutUint32(padding[1:5], 0) 447 | binary.BigEndian.PutUint16(padding[5:7], uint16(l)) 448 | _, err = s.conn.Write(padding) 449 | if err != nil { 450 | return 0, err 451 | } 452 | b = nil 453 | } 454 | } 455 | // maybe still remain payload to write 456 | if len(b) == 0 { 457 | return 458 | } else { 459 | n2, err := s.conn.Write(b) 460 | return n + n2, err 461 | } 462 | } else { 463 | s.sendPadding = false 464 | } 465 | } 466 | 467 | return s.conn.Write(b) 468 | } 469 | -------------------------------------------------------------------------------- /proxy/session/stream.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "anytls/proxy/pipe" 5 | "io" 6 | "net" 7 | "os" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | // Stream implements net.Conn 13 | type Stream struct { 14 | id uint32 15 | 16 | sess *Session 17 | 18 | pipeR *pipe.PipeReader 19 | pipeW *pipe.PipeWriter 20 | writeDeadline pipe.PipeDeadline 21 | 22 | dieOnce sync.Once 23 | dieHook func() 24 | dieErr error 25 | 26 | reportOnce sync.Once 27 | } 28 | 29 | // newStream initiates a Stream struct 30 | func newStream(id uint32, sess *Session) *Stream { 31 | s := new(Stream) 32 | s.id = id 33 | s.sess = sess 34 | s.pipeR, s.pipeW = pipe.Pipe() 35 | s.writeDeadline = pipe.MakePipeDeadline() 36 | return s 37 | } 38 | 39 | // Read implements net.Conn 40 | func (s *Stream) Read(b []byte) (n int, err error) { 41 | n, err = s.pipeR.Read(b) 42 | if n == 0 && s.dieErr != nil { 43 | err = s.dieErr 44 | } 45 | return 46 | } 47 | 48 | // Write implements net.Conn 49 | func (s *Stream) Write(b []byte) (n int, err error) { 50 | select { 51 | case <-s.writeDeadline.Wait(): 52 | return 0, os.ErrDeadlineExceeded 53 | default: 54 | } 55 | f := newFrame(cmdPSH, s.id) 56 | f.data = b 57 | n, err = s.sess.writeFrame(f) 58 | return 59 | } 60 | 61 | // Close implements net.Conn 62 | func (s *Stream) Close() error { 63 | return s.CloseWithError(io.ErrClosedPipe) 64 | } 65 | 66 | func (s *Stream) CloseWithError(err error) error { 67 | // if err != io.ErrClosedPipe { 68 | // logrus.Debugln(err) 69 | // } 70 | var once bool 71 | s.dieOnce.Do(func() { 72 | s.dieErr = err 73 | s.pipeR.Close() 74 | once = true 75 | }) 76 | if once { 77 | if s.dieHook != nil { 78 | s.dieHook() 79 | s.dieHook = nil 80 | } 81 | return s.sess.streamClosed(s.id) 82 | } else { 83 | return s.dieErr 84 | } 85 | } 86 | 87 | func (s *Stream) SetReadDeadline(t time.Time) error { 88 | return s.pipeR.SetReadDeadline(t) 89 | } 90 | 91 | func (s *Stream) SetWriteDeadline(t time.Time) error { 92 | s.writeDeadline.Set(t) 93 | return nil 94 | } 95 | 96 | func (s *Stream) SetDeadline(t time.Time) error { 97 | s.SetWriteDeadline(t) 98 | return s.SetReadDeadline(t) 99 | } 100 | 101 | // LocalAddr satisfies net.Conn interface 102 | func (s *Stream) LocalAddr() net.Addr { 103 | if ts, ok := s.sess.conn.(interface { 104 | LocalAddr() net.Addr 105 | }); ok { 106 | return ts.LocalAddr() 107 | } 108 | return nil 109 | } 110 | 111 | // RemoteAddr satisfies net.Conn interface 112 | func (s *Stream) RemoteAddr() net.Addr { 113 | if ts, ok := s.sess.conn.(interface { 114 | RemoteAddr() net.Addr 115 | }); ok { 116 | return ts.RemoteAddr() 117 | } 118 | return nil 119 | } 120 | 121 | // HandshakeFailure should be called when Server fail to create outbound proxy 122 | func (s *Stream) HandshakeFailure(err error) error { 123 | var once bool 124 | s.reportOnce.Do(func() { 125 | once = true 126 | }) 127 | if once && err != nil && s.sess.peerVersion >= 2 { 128 | f := newFrame(cmdSYNACK, s.id) 129 | f.data = []byte(err.Error()) 130 | if _, err := s.sess.writeFrame(f); err != nil { 131 | return err 132 | } 133 | } 134 | return nil 135 | } 136 | 137 | // HandshakeSuccess should be called when Server success to create outbound proxy 138 | func (s *Stream) HandshakeSuccess() error { 139 | var once bool 140 | s.reportOnce.Do(func() { 141 | once = true 142 | }) 143 | if once && s.sess.peerVersion >= 2 { 144 | if _, err := s.sess.writeFrame(newFrame(cmdSYNACK, s.id)); err != nil { 145 | return err 146 | } 147 | } 148 | return nil 149 | } 150 | -------------------------------------------------------------------------------- /proxy/system_dialer.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | var SystemDialer = &net.Dialer{ 9 | Timeout: time.Second * 5, 10 | } 11 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # AnyTLS 2 | 3 | 一个试图缓解 嵌套的TLS握手指纹(TLS in TLS) 问题的代理协议。`anytls-go` 是该协议的参考实现。 4 | 5 | - 灵活的分包和填充策略 6 | - 连接复用,降低代理延迟 7 | - 简洁的配置 8 | 9 | [用户常见问题](./docs/faq.md) 10 | 11 | [协议文档](./docs/protocol.md) 12 | 13 | [URI 格式](./docs/uri_scheme.md) 14 | 15 | ## 快速食用方法 16 | 17 | ### 服务器 18 | 19 | ``` 20 | ./anytls-server -l 0.0.0.0:8443 -p 密码 21 | ``` 22 | 23 | `0.0.0.0:8443` 为服务器监听的地址和端口。 24 | 25 | ### 客户端 26 | 27 | ``` 28 | ./anytls-client -l 127.0.0.1:1080 -s 服务器ip:端口 -p 密码 29 | ``` 30 | 31 | `127.0.0.1:1080` 为本机 Socks5 代理监听地址,理论上支持 TCP 和 UDP(通过 udp over tcp 传输)。 32 | 33 | ### sing-box 34 | 35 | https://github.com/SagerNet/sing-box 36 | 37 | 已合并至 dev-next 分支。它包含了 anytls 协议的服务器和客户端。 38 | 39 | ### mihomo 40 | 41 | https://github.com/MetaCubeX/mihomo 42 | 43 | 已合并至 Alpha 分支。它包含了 anytls 协议的服务器和客户端。 44 | 45 | ### Shadowrocket 46 | 47 | Shadowrocket 2.2.65+ 实现了 anytls 协议的客户端。 48 | -------------------------------------------------------------------------------- /util/deadline.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | func NewDeadlineWatcher(ddl time.Duration, timeOut func()) (done func()) { 9 | t := time.NewTimer(ddl) 10 | closeCh := make(chan struct{}) 11 | go func() { 12 | defer t.Stop() 13 | select { 14 | case <-closeCh: 15 | case <-t.C: 16 | timeOut() 17 | } 18 | }() 19 | var once sync.Once 20 | return func() { 21 | once.Do(func() { 22 | close(closeCh) 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /util/mkcert.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "crypto/x509/pkix" 9 | "encoding/pem" 10 | "math/big" 11 | "time" 12 | ) 13 | 14 | func GenerateKeyPair(timeFunc func() time.Time, serverName string) (*tls.Certificate, error) { 15 | if timeFunc == nil { 16 | timeFunc = time.Now 17 | } 18 | key, err := rsa.GenerateKey(rand.Reader, 2048) 19 | if err != nil { 20 | return nil, err 21 | } 22 | serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) 23 | if err != nil { 24 | return nil, err 25 | } 26 | template := &x509.Certificate{ 27 | SerialNumber: serialNumber, 28 | NotBefore: timeFunc().Add(time.Hour * -1), 29 | NotAfter: timeFunc().Add(time.Hour), 30 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 31 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 32 | BasicConstraintsValid: true, 33 | Subject: pkix.Name{ 34 | CommonName: serverName, 35 | }, 36 | DNSNames: []string{serverName}, 37 | } 38 | publicDer, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key) 39 | if err != nil { 40 | return nil, err 41 | } 42 | privateDer, err := x509.MarshalPKCS8PrivateKey(key) 43 | if err != nil { 44 | return nil, err 45 | } 46 | publicPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}) 47 | privPem := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateDer}) 48 | keyPair, err := tls.X509KeyPair(publicPem, privPem) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return &keyPair, err 53 | } 54 | -------------------------------------------------------------------------------- /util/routine.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "runtime/debug" 6 | "time" 7 | 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func StartRoutine(ctx context.Context, d time.Duration, f func()) { 12 | go func() { 13 | defer func() { 14 | if r := recover(); r != nil { 15 | logrus.Errorln("[BUG]", r, string(debug.Stack())) 16 | } 17 | }() 18 | for { 19 | time.Sleep(d) 20 | f() 21 | select { 22 | case <-ctx.Done(): 23 | return 24 | default: 25 | } 26 | } 27 | }() 28 | } 29 | -------------------------------------------------------------------------------- /util/string_map.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type StringMap map[string]string 8 | 9 | func (s StringMap) ToBytes() []byte { 10 | var lines []string 11 | for k, v := range s { 12 | lines = append(lines, k+"="+v) 13 | } 14 | return []byte(strings.Join(lines, "\n")) 15 | } 16 | 17 | func StringMapFromBytes(b []byte) StringMap { 18 | var m = make(StringMap) 19 | var lines = strings.Split(string(b), "\n") 20 | for _, line := range lines { 21 | v := strings.SplitN(line, "=", 2) 22 | if len(v) == 2 { 23 | m[v[0]] = v[1] 24 | } 25 | } 26 | return m 27 | } 28 | -------------------------------------------------------------------------------- /util/type.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "net" 6 | ) 7 | 8 | type DialOutFunc func(ctx context.Context) (net.Conn, error) 9 | -------------------------------------------------------------------------------- /util/version.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | var ProgramVersionName = "anytls/0.0.8" 4 | --------------------------------------------------------------------------------