From cb6f2d1e1af7f6d29dddadb8437686ea253f5f8a Mon Sep 17 00:00:00 2001 From: nadoo <287492+nadoo@users.noreply.github.com> Date: Tue, 23 Jun 2026 23:17:09 +0800 Subject: [PATCH] anytls: support anytls --- .goreleaser.yml | 156 ++++++++++++------------- README.md | 12 +- config/glider.conf.example | 6 + feature.go | 1 + proxy/anytls/anytls.go | 110 +++++++++++++++++ proxy/anytls/client.go | 93 +++++++++++++++ proxy/anytls/padding.go | 70 +++++++++++ proxy/anytls/protocol.go | 97 +++++++++++++++ proxy/anytls/server.go | 105 +++++++++++++++++ proxy/anytls/session.go | 233 +++++++++++++++++++++++++++++++++++++ proxy/anytls/settings.go | 59 ++++++++++ proxy/anytls/stream.go | 96 +++++++++++++++ 12 files changed, 958 insertions(+), 80 deletions(-) create mode 100644 proxy/anytls/anytls.go create mode 100644 proxy/anytls/client.go create mode 100644 proxy/anytls/padding.go create mode 100644 proxy/anytls/protocol.go create mode 100644 proxy/anytls/server.go create mode 100644 proxy/anytls/session.go create mode 100644 proxy/anytls/settings.go create mode 100644 proxy/anytls/stream.go diff --git a/.goreleaser.yml b/.goreleaser.yml index 1dc3426..8658e6f 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,96 +1,96 @@ version: 2 before: - hooks: - - go mod tidy + hooks: + - go mod tidy builds: - - id: default - env: - - CGO_ENABLED=0 - goos: - - windows - - linux - - darwin - - freebsd - goarch: - - 386 - - amd64 - - arm - - arm64 - - mips - - mipsle - - mips64 - - mips64le - - riscv64 - goamd64: - - v1 - - v3 - goarm: - - 6 - - 7 - gomips: - - hardfloat - - softfloat + - id: default + env: + - CGO_ENABLED=0 + goos: + - windows + - linux + - darwin + - freebsd + goarch: + - "386" + - amd64 + - arm + - arm64 + - mips + - mipsle + - mips64 + - mips64le + - riscv64 + goamd64: + - v1 + - v3 + goarm: + - "6" + - "7" + gomips: + - hardfloat + - softfloat archives: - - id: default - builds: - - default - wrap_in_directory: true - formats: tar.gz - format_overrides: - - goos: windows - formats: zip - files: - - LICENSE - - README.md - - config/**/* - - systemd/* + - id: default + builds: + - default + wrap_in_directory: true + formats: tar.gz + format_overrides: + - goos: windows + formats: zip + files: + - LICENSE + - README.md + - config/**/* + - systemd/* snapshot: - version_template: '{{ incpatch .Version }}-dev-{{.ShortCommit}}' + version_template: "{{ incpatch .Version }}-dev-{{.ShortCommit}}" checksum: - name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt" + name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt" release: - prerelease: true - draft: true + prerelease: "true" + draft: true nfpms: - - id: glider - package_name: glider - vendor: nadoo - homepage: https://github.com/nadoo/glider - maintainer: nadoo - description: Glider is a forward proxy with multiple protocols support, and also a dns/dhcp server with ipset management features(like dnsmasq). - license: GPL-3.0 License - formats: - # - apk - - deb - # - rpm - dependencies: - - libsystemd0 - bindir: /usr/bin - release: 1 - epoch: 1 - version_metadata: git - section: default - priority: extra - contents: - - src: systemd/glider@.service - dst: /etc/systemd/system/glider@.service + - id: glider + package_name: glider + vendor: nadoo + homepage: https://github.com/nadoo/glider + maintainer: nadoo + description: Glider is a forward proxy with multiple protocols support, and also a dns/dhcp server with ipset management features(like dnsmasq). + license: GPL-3.0 License + formats: + # - apk + - deb + # - rpm + dependencies: + - libsystemd0 + bindir: /usr/bin + release: "1" + epoch: "1" + version_metadata: git + section: default + priority: extra + contents: + - src: systemd/glider@.service + dst: /etc/systemd/system/glider@.service - - src: config/glider.conf.example - dst: /etc/glider/glider.conf.example + - src: config/glider.conf.example + dst: /etc/glider/glider.conf.example - scripts: - postinstall: "systemd/postinstall.sh" - preremove: "systemd/preremove.sh" - postremove: "systemd/postremove.sh" + scripts: + postinstall: "systemd/postinstall.sh" + preremove: "systemd/preremove.sh" + postremove: "systemd/postremove.sh" - deb: - triggers: - interest_noawait: - - /lib/systemd/systemd + deb: + triggers: + interest_noawait: + - /lib/systemd/systemd diff --git a/README.md b/README.md index fa794da..27de1a5 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ we can set up local listeners as proxy servers, and forward requests to internet |SS |√|√|√|√|client & server |Trojan |√|√|√|√|client & server |Trojanc |√|√|√|√|trojan cleartext(without tls) +|AnyTLS |√| |√| |client & server |VLESS |√|√|√|√|client & server |VMess | | |√|√|client only |SSR | | |√| |client only @@ -197,8 +198,8 @@ URL: -forward socks5://serverA:1080,socks5://serverB:1080 (proxy chain) SCHEME: - listen : http kcp mixed pxyproto redir redir6 smux sni socks5 ss tcp tls tproxy trojan trojanc udp unix vless vsock ws wss - forward: direct http kcp reject simple-obfs smux socks4 socks4a socks5 ss ssh ssr tcp tls trojan trojanc udp unix vless vmess vsock ws wss + listen : anytls http kcp mixed pxyproto redir redir6 smux sni socks5 ss tcp tls tproxy trojan trojanc udp unix vless vsock ws wss + forward: anytls direct http kcp reject simple-obfs smux socks4 socks4a socks5 ss ssh ssr tcp tls trojan trojanc udp unix vless vmess vsock ws wss Note: use 'glider -scheme all' or 'glider -scheme SCHEME' to see help info for the scheme. @@ -334,6 +335,13 @@ Trojan server scheme: trojan://pass@host:port?cert=PATH&key=PATH[&fallback=127.0.0.1] trojanc://pass@host:port[?fallback=127.0.0.1] (cleartext, without TLS) +-- +AnyTLS client scheme: + anytls://password@host:port[?serverName=SERVERNAME][&skipVerify=true][&cert=PATH][&synackTimeout=10s] + +AnyTLS server scheme: + anytls://password@host:port?cert=PATH&key=PATH + -- Unix domain socket scheme: unix://path diff --git a/config/glider.conf.example b/config/glider.conf.example index af4a40d..98ad36f 100644 --- a/config/glider.conf.example +++ b/config/glider.conf.example @@ -85,6 +85,9 @@ listen=127.0.0.1:8443 # trojanc server (trojan without tls) # listen=trojanc://PASSWORD@:1234?fallback=127.0.0.1 +# anytls server +# listen=anytls://PASSWORD@:8443?cert=/path/to/cert&key=/path/to/key + # FORWARDERS # ---------- # Forwarders, we can setup multiple forwarders. @@ -127,6 +130,9 @@ listen=127.0.0.1:8443 # trojanc as forwarder # forward=trojanc://PASSWORD@1.1.1.1:8080 +# anytls as forwarder +# forward=anytls://PASSWORD@1.1.1.1:8443[?serverName=SERVERNAME][&skipVerify=true] + # vless forwarder # forward=vless://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@1.1.1.1:443 diff --git a/feature.go b/feature.go index 21b49fc..9489a92 100644 --- a/feature.go +++ b/feature.go @@ -5,6 +5,7 @@ import ( // _ "github.com/nadoo/glider/service/xxx" // comment out the protocols you don't need to make the compiled binary smaller. + _ "github.com/nadoo/glider/proxy/anytls" _ "github.com/nadoo/glider/proxy/http" _ "github.com/nadoo/glider/proxy/kcp" _ "github.com/nadoo/glider/proxy/mixed" diff --git a/proxy/anytls/anytls.go b/proxy/anytls/anytls.go new file mode 100644 index 0000000..dff28d7 --- /dev/null +++ b/proxy/anytls/anytls.go @@ -0,0 +1,110 @@ +package anytls + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "net" + "net/url" + "os" + "strings" + "time" + + "github.com/nadoo/glider/proxy" +) + +type AnyTLS struct { + dialer proxy.Dialer + proxy proxy.Proxy + + addr string + password string + serverName string + skipVerify bool + certFile string + keyFile string + tlsConfig *tls.Config + + synackTimeout time.Duration + padding paddingScheme +} + +func NewAnyTLS(s string, d proxy.Dialer, p proxy.Proxy) (*AnyTLS, error) { + u, err := url.Parse(s) + if err != nil { + return nil, fmt.Errorf("[anytls] parse url err: %s", err) + } + query := u.Query() + a := &AnyTLS{ + dialer: d, + proxy: p, + addr: u.Host, + password: u.User.Username(), + serverName: query.Get("serverName"), + skipVerify: query.Get("skipVerify") == "true", + certFile: query.Get("cert"), + keyFile: query.Get("key"), + synackTimeout: 10 * time.Second, + } + if a.password == "" { + return nil, errors.New("[anytls] password must be specified") + } + if a.addr != "" { + if _, port, _ := net.SplitHostPort(a.addr); port == "" { + a.addr = net.JoinHostPort(a.addr, "443") + } + if a.serverName == "" { + a.serverName = a.addr[:strings.LastIndex(a.addr, ":")] + } + } + if timeout := query.Get("synackTimeout"); timeout != "" { + d, err := time.ParseDuration(timeout) + if err != nil { + return nil, fmt.Errorf("[anytls] invalid synackTimeout: %s", err) + } + a.synackTimeout = d + } + if scheme := query.Get("paddingScheme"); scheme != "" { + a.padding, err = parsePaddingScheme(scheme) + } else { + a.padding, err = parsePaddingScheme(defaultPaddingScheme) + } + if err != nil { + return nil, fmt.Errorf("[anytls] invalid padding scheme: %s", err) + } + return a, nil +} + +func (s *AnyTLS) Addr() string { + if s.addr == "" && s.dialer != nil { + return s.dialer.Addr() + } + return s.addr +} + +func loadClientTLSConfig(serverName, certFile string, skipVerify bool) (*tls.Config, error) { + conf := &tls.Config{ServerName: serverName, InsecureSkipVerify: skipVerify, MinVersion: tls.VersionTLS12} + if certFile != "" { + certData, err := os.ReadFile(certFile) + if err != nil { + return nil, fmt.Errorf("[anytls] read cert file error: %s", err) + } + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(certData) { + return nil, fmt.Errorf("[anytls] can not append cert file: %s", certFile) + } + conf.RootCAs = certPool + } + return conf, nil +} + +func init() { + proxy.AddUsage("anytls", ` +AnyTLS client scheme: + anytls://password@host:port[?serverName=SERVERNAME][&skipVerify=true][&cert=PATH][&synackTimeout=10s] + +AnyTLS server scheme: + anytls://password@host:port?cert=PATH&key=PATH +`) +} diff --git a/proxy/anytls/client.go b/proxy/anytls/client.go new file mode 100644 index 0000000..bcc2615 --- /dev/null +++ b/proxy/anytls/client.go @@ -0,0 +1,93 @@ +package anytls + +import ( + "crypto/tls" + "fmt" + "net" + + "github.com/nadoo/glider/pkg/log" + "github.com/nadoo/glider/pkg/socks" + "github.com/nadoo/glider/proxy" +) + +func init() { + proxy.RegisterDialer("anytls", NewAnyTLSDialer) +} + +func NewAnyTLSDialer(s string, d proxy.Dialer) (proxy.Dialer, error) { + a, err := NewAnyTLS(s, d, nil) + if err != nil { + return nil, fmt.Errorf("[anytls] create instance error: %s", err) + } + a.tlsConfig, err = loadClientTLSConfig(a.serverName, a.certFile, a.skipVerify) + return a, err +} + +func (s *AnyTLS) Dial(network, addr string) (net.Conn, error) { + if network != "tcp" && network != "tcp4" && network != "tcp6" { + return nil, proxy.ErrNotSupported + } + raw := socks.ParseAddr(addr) + if raw == nil { + return nil, fmt.Errorf("[anytls] invalid target address: %s", addr) + } + ss, err := s.newClientSession() + if err != nil { + return nil, err + } + st, err := ss.openStream() + if err != nil { + _ = ss.Close() + return nil, err + } + if _, err := st.Write(raw); err != nil { + _ = st.Close() + _ = ss.Close() + return nil, err + } + if err := ss.waitSYNACK(st.id, s.synackTimeout); err != nil { + _ = st.Close() + _ = ss.Close() + return nil, err + } + return &clientConn{Conn: st, session: ss}, nil +} + +func (s *AnyTLS) DialUDP(network, addr string) (net.PacketConn, error) { + return nil, proxy.ErrNotSupported +} + +func (s *AnyTLS) newClientSession() (*session, error) { + rc, err := s.dialer.Dial("tcp", s.addr) + if err != nil { + log.F("[anytls] dial to %s error: %s", s.addr, err) + return nil, err + } + tc := tls.Client(rc, s.tlsConfig) + if err := tc.Handshake(); err != nil { + _ = rc.Close() + return nil, err + } + if err := writeAuth(tc, s.password, s.padding.authPaddingLen()); err != nil { + _ = tc.Close() + return nil, err + } + ss := newSession(tc) + if err := ss.writeFrame(frame{command: cmdSettings, data: clientSettings(s.padding)}); err != nil { + _ = tc.Close() + return nil, err + } + ss.start() + return ss, nil +} + +type clientConn struct { + net.Conn + session *session +} + +func (c *clientConn) Close() error { + err := c.Conn.Close() + _ = c.session.Close() + return err +} diff --git a/proxy/anytls/padding.go b/proxy/anytls/padding.go new file mode 100644 index 0000000..c636612 --- /dev/null +++ b/proxy/anytls/padding.go @@ -0,0 +1,70 @@ +package anytls + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "strconv" + "strings" +) + +const defaultPaddingScheme = "stop=8\n0=30-30\n1=100-400\n2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000\n3=9-9,500-1000\n4=500-1000\n5=500-1000\n6=500-1000\n7=500-1000" + +type paddingScheme struct { + raw string + authRange [2]int +} + +func parsePaddingScheme(raw string) (paddingScheme, error) { + if strings.TrimSpace(raw) == "" { + raw = defaultPaddingScheme + } + ps := paddingScheme{raw: raw, authRange: [2]int{0, 0}} + for _, line := range strings.Split(raw, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + key, value, ok := strings.Cut(line, "=") + if !ok { + return ps, fmt.Errorf("invalid padding scheme line %q", line) + } + if key == "0" { + r, err := parseRange(value) + if err != nil { + return ps, err + } + ps.authRange = r + } + } + return ps, nil +} + +func parseRange(s string) ([2]int, error) { + part := strings.SplitN(s, ",", 2)[0] + lo, hi, ok := strings.Cut(part, "-") + if !ok { + return [2]int{}, fmt.Errorf("invalid range %q", s) + } + min, err := strconv.Atoi(lo) + if err != nil { + return [2]int{}, err + } + max, err := strconv.Atoi(hi) + if err != nil { + return [2]int{}, err + } + if min < 0 || max < min || max > 65535 { + return [2]int{}, fmt.Errorf("invalid range %q", s) + } + return [2]int{min, max}, nil +} + +func (p paddingScheme) authPaddingLen() int { + return p.authRange[0] +} + +func (p paddingScheme) md5() string { + sum := md5.Sum([]byte(p.raw)) + return hex.EncodeToString(sum[:]) +} diff --git a/proxy/anytls/protocol.go b/proxy/anytls/protocol.go new file mode 100644 index 0000000..0c8f3e7 --- /dev/null +++ b/proxy/anytls/protocol.go @@ -0,0 +1,97 @@ +package anytls + +import ( + "crypto/sha256" + "crypto/subtle" + "encoding/binary" + "errors" + "fmt" + "io" +) + +const ( + cmdWaste byte = iota + cmdSYN + cmdPSH + cmdFIN + cmdSettings + cmdAlert + cmdUpdatePaddingScheme + cmdSYNACK + cmdHeartRequest + cmdHeartResponse + cmdServerSettings +) + +const ( + protocolVersion = 2 + maxFrameData = 65535 +) + +type frame struct { + command byte + streamID uint32 + data []byte +} + +func passwordHash(password string) [32]byte { + return sha256.Sum256([]byte(password)) +} + +func readAuth(r io.Reader, password string) error { + var head [34]byte + if _, err := io.ReadFull(r, head[:]); err != nil { + return err + } + want := passwordHash(password) + if subtle.ConstantTimeCompare(head[:32], want[:]) != 1 { + return errors.New("authentication failed") + } + paddingLen := binary.BigEndian.Uint16(head[32:34]) + if paddingLen > 0 { + _, err := io.CopyN(io.Discard, r, int64(paddingLen)) + return err + } + return nil +} + +func writeAuth(w io.Writer, password string, paddingLen int) error { + if paddingLen < 0 || paddingLen > 65535 { + return fmt.Errorf("invalid auth padding length %d", paddingLen) + } + hash := passwordHash(password) + buf := make([]byte, 34+paddingLen) + copy(buf, hash[:]) + binary.BigEndian.PutUint16(buf[32:34], uint16(paddingLen)) + _, err := w.Write(buf) + return err +} + +func readFrame(r io.Reader) (frame, error) { + var head [7]byte + if _, err := io.ReadFull(r, head[:]); err != nil { + return frame{}, err + } + n := binary.BigEndian.Uint16(head[5:7]) + var data []byte + if n > 0 { + data = make([]byte, n) + if _, err := io.ReadFull(r, data); err != nil { + return frame{}, err + } + } + return frame{command: head[0], streamID: binary.BigEndian.Uint32(head[1:5]), data: data}, nil +} + +func writeFrame(w io.Writer, f frame) error { + if len(f.data) > maxFrameData { + return errors.New("frame data too large") + } + buf := make([]byte, 7+len(f.data)) + buf[0] = f.command + binary.BigEndian.PutUint32(buf[1:5], f.streamID) + binary.BigEndian.PutUint16(buf[5:7], uint16(len(f.data))) + copy(buf[7:], f.data) + _, err := w.Write(buf) + return err +} diff --git a/proxy/anytls/server.go b/proxy/anytls/server.go new file mode 100644 index 0000000..3c45406 --- /dev/null +++ b/proxy/anytls/server.go @@ -0,0 +1,105 @@ +package anytls + +import ( + "crypto/tls" + "errors" + "fmt" + "net" + "strings" + + "github.com/nadoo/glider/pkg/log" + "github.com/nadoo/glider/pkg/socks" + "github.com/nadoo/glider/proxy" +) + +func init() { + proxy.RegisterServer("anytls", NewAnyTLSServer) +} + +func NewAnyTLSServer(s string, p proxy.Proxy) (proxy.Server, error) { + a, err := NewAnyTLS(s, nil, p) + if err != nil { + return nil, fmt.Errorf("[anytls] create instance error: %s", err) + } + if a.certFile == "" || a.keyFile == "" { + return nil, errors.New("[anytls] cert and key file path must be specified") + } + cert, err := tls.LoadX509KeyPair(a.certFile, a.keyFile) + if err != nil { + return nil, fmt.Errorf("[anytls] unable to load cert: %s, key %s, error: %s", a.certFile, a.keyFile, err) + } + a.tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12} + return a, nil +} + +func (s *AnyTLS) ListenAndServe() { + l, err := net.Listen("tcp", s.addr) + if err != nil { + log.Fatalf("[anytls] failed to listen on %s: %v", s.addr, err) + return + } + defer l.Close() + + log.F("[anytls] listening TCP on %s", s.addr) + for { + c, err := l.Accept() + if err != nil { + log.F("[anytls] failed to accept: %v", err) + continue + } + go s.Serve(c) + } +} + +func (s *AnyTLS) Serve(c net.Conn) { + tlsConn := tls.Server(c, s.tlsConfig) + if err := tlsConn.Handshake(); err != nil { + _ = tlsConn.Close() + log.F("[anytls] error in tls handshake: %s", err) + return + } + if err := readAuth(tlsConn, s.password); err != nil { + _ = tlsConn.Close() + log.F("[anytls] auth error from %s: %s", c.RemoteAddr(), err) + return + } + + ss := newSession(tlsConn) + ss.start() + for { + st, err := ss.acceptStream() + if err != nil { + _ = ss.Close() + return + } + go s.serveStream(ss, st) + } +} + +func (s *AnyTLS) serveStream(ss *session, st *stream) { + defer st.Close() + + target, err := socks.ReadAddr(st) + if err != nil { + _ = ss.writeFrame(frame{command: cmdSYNACK, streamID: st.id, data: []byte(err.Error())}) + log.F("[anytls] read target error: %v", err) + return + } + + rc, dialer, err := s.proxy.Dial("tcp", target.String()) + if err != nil { + _ = ss.writeFrame(frame{command: cmdSYNACK, streamID: st.id, data: []byte(err.Error())}) + log.F("[anytls] %s <-> %s via %s, error in dial: %v", st.RemoteAddr(), target, dialer.Addr(), err) + return + } + defer rc.Close() + + _ = ss.writeFrame(frame{command: cmdSYNACK, streamID: st.id}) + log.F("[anytls] %s <-> %s via %s", st.RemoteAddr(), target, dialer.Addr()) + if err = proxy.Relay(st, rc); err != nil { + log.F("[anytls] %s <-> %s via %s, relay error: %v", st.RemoteAddr(), target, dialer.Addr(), err) + if !strings.Contains(err.Error(), s.addr) { + s.proxy.Record(dialer, false) + } + } +} diff --git a/proxy/anytls/session.go b/proxy/anytls/session.go new file mode 100644 index 0000000..43b61cd --- /dev/null +++ b/proxy/anytls/session.go @@ -0,0 +1,233 @@ +package anytls + +import ( + "errors" + "fmt" + "io" + "net" + "sync" + "sync/atomic" + "time" +) + +type session struct { + conn net.Conn + + writeMu sync.Mutex + mu sync.Mutex + streams map[uint32]*stream + synack map[uint32]chan synackResult + nextID uint32 + + incoming chan *stream + done chan struct{} + closeOnce sync.Once + err atomic.Value + + settingsSeen bool +} + +type synackResult struct { + data []byte +} + +func newSession(conn net.Conn) *session { + return &session{ + conn: conn, + streams: map[uint32]*stream{}, + synack: map[uint32]chan synackResult{}, + nextID: 1, + incoming: make(chan *stream, 32), + done: make(chan struct{}), + } +} + +func (s *session) start() { + go s.readLoop() +} + +func (s *session) acceptStream() (*stream, error) { + select { + case st, ok := <-s.incoming: + if !ok { + return nil, s.Err() + } + return st, nil + case <-s.done: + return nil, s.Err() + } +} + +func (s *session) openStream() (*stream, error) { + id := atomic.AddUint32(&s.nextID, 1) - 1 + st := newStream(id, s) + s.mu.Lock() + s.streams[id] = st + s.synack[id] = make(chan synackResult, 1) + s.mu.Unlock() + if err := s.writeFrame(frame{command: cmdSYN, streamID: id}); err != nil { + s.removeStream(id) + return nil, err + } + return st, nil +} + +func (s *session) waitSYNACK(id uint32, timeout time.Duration) error { + s.mu.Lock() + ch := s.synack[id] + s.mu.Unlock() + if ch == nil { + return nil + } + var timer <-chan time.Time + if timeout > 0 { + t := time.NewTimer(timeout) + defer t.Stop() + timer = t.C + } + select { + case r, ok := <-ch: + if !ok { + return s.Err() + } + if len(r.data) > 0 { + return fmt.Errorf("stream open failed: %s", string(r.data)) + } + return nil + case <-timer: + return errors.New("timeout waiting for SYNACK") + case <-s.done: + return s.Err() + } +} + +func (s *session) writeFrame(f frame) error { + s.writeMu.Lock() + defer s.writeMu.Unlock() + return writeFrame(s.conn, f) +} + +func (s *session) readLoop() { + for { + f, err := readFrame(s.conn) + if err != nil { + if !errors.Is(err, io.EOF) { + s.setErr(err) + } + s.Close() + return + } + if err := s.handleFrame(f); err != nil { + s.setErr(err) + s.Close() + return + } + } +} + +func (s *session) handleFrame(f frame) error { + switch f.command { + case cmdWaste: + return nil + case cmdHeartRequest: + return s.writeFrame(frame{command: cmdHeartResponse, streamID: f.streamID}) + case cmdHeartResponse: + return nil + case cmdSettings: + m := parseSettings(f.data) + s.settingsSeen = true + if settingsVersion(m) >= 2 { + return s.writeFrame(frame{command: cmdServerSettings, data: serverSettings()}) + } + case cmdServerSettings: + return nil + case cmdAlert: + return errors.New("alert: " + string(f.data)) + case cmdUpdatePaddingScheme: + return nil + case cmdSYN: + if !s.settingsSeen { + _ = s.writeFrame(frame{command: cmdAlert, data: []byte("cmdSYN received before cmdSettings")}) + return errors.New("cmdSYN received before cmdSettings") + } + st := newStream(f.streamID, s) + s.mu.Lock() + s.streams[f.streamID] = st + s.mu.Unlock() + select { + case s.incoming <- st: + case <-s.done: + } + case cmdSYNACK: + s.mu.Lock() + ch := s.synack[f.streamID] + delete(s.synack, f.streamID) + s.mu.Unlock() + if ch != nil { + ch <- synackResult{data: f.data} + close(ch) + } + case cmdPSH: + st := s.getStream(f.streamID) + if st != nil { + st.push(f.data) + } + case cmdFIN: + st := s.getStream(f.streamID) + if st != nil { + st.closeRead() + s.removeStream(f.streamID) + } + default: + return errors.New("unknown command") + } + return nil +} + +func (s *session) getStream(id uint32) *stream { + s.mu.Lock() + defer s.mu.Unlock() + return s.streams[id] +} + +func (s *session) removeStream(id uint32) { + s.mu.Lock() + delete(s.streams, id) + if ch := s.synack[id]; ch != nil { + delete(s.synack, id) + close(ch) + } + s.mu.Unlock() +} + +func (s *session) setErr(err error) { + if err != nil && s.err.Load() == nil { + s.err.Store(err) + } +} + +func (s *session) Err() error { + if v := s.err.Load(); v != nil { + return v.(error) + } + return net.ErrClosed +} + +func (s *session) Close() error { + s.closeOnce.Do(func() { + close(s.done) + _ = s.conn.Close() + s.mu.Lock() + for _, st := range s.streams { + st.closeRead() + } + s.streams = map[uint32]*stream{} + for id, ch := range s.synack { + delete(s.synack, id) + close(ch) + } + close(s.incoming) + s.mu.Unlock() + }) + return nil +} diff --git a/proxy/anytls/settings.go b/proxy/anytls/settings.go new file mode 100644 index 0000000..917d305 --- /dev/null +++ b/proxy/anytls/settings.go @@ -0,0 +1,59 @@ +package anytls + +import ( + "fmt" + "strconv" + "strings" +) + +func encodeSettings(m map[string]string) []byte { + keys := []string{"v", "client", "padding-md5"} + lines := make([]string, 0, len(m)) + seen := map[string]bool{} + for _, k := range keys { + if v, ok := m[k]; ok { + lines = append(lines, k+"="+v) + seen[k] = true + } + } + for k, v := range m { + if !seen[k] { + lines = append(lines, k+"="+v) + } + } + return []byte(strings.Join(lines, "\n")) +} + +func parseSettings(data []byte) map[string]string { + out := map[string]string{} + for _, line := range strings.Split(string(data), "\n") { + if line == "" { + continue + } + k, v, ok := strings.Cut(line, "=") + if ok { + out[k] = v + } + } + return out +} + +func settingsVersion(m map[string]string) int { + v, err := strconv.Atoi(m["v"]) + if err != nil { + return 1 + } + return v +} + +func clientSettings(ps paddingScheme) []byte { + return encodeSettings(map[string]string{ + "v": fmt.Sprint(protocolVersion), + "client": "glider-anytls", + "padding-md5": ps.md5(), + }) +} + +func serverSettings() []byte { + return encodeSettings(map[string]string{"v": fmt.Sprint(protocolVersion)}) +} diff --git a/proxy/anytls/stream.go b/proxy/anytls/stream.go new file mode 100644 index 0000000..4490e2b --- /dev/null +++ b/proxy/anytls/stream.go @@ -0,0 +1,96 @@ +package anytls + +import ( + "errors" + "io" + "net" + "sync" + "time" +) + +var errStreamClosed = errors.New("stream closed") + +type stream struct { + id uint32 + s *session + + in chan []byte + readBuf []byte + closeIn sync.Once + closeOut sync.Once + + mu sync.Mutex + closed bool +} + +func newStream(id uint32, s *session) *stream { + return &stream{id: id, s: s, in: make(chan []byte, 32)} +} + +func (st *stream) Read(p []byte) (int, error) { + for len(st.readBuf) == 0 { + b, ok := <-st.in + if !ok { + return 0, io.EOF + } + st.readBuf = b + } + n := copy(p, st.readBuf) + st.readBuf = st.readBuf[n:] + return n, nil +} + +func (st *stream) Write(p []byte) (int, error) { + st.mu.Lock() + closed := st.closed + st.mu.Unlock() + if closed { + return 0, errStreamClosed + } + written := 0 + for len(p) > 0 { + n := len(p) + if n > maxFrameData { + n = maxFrameData + } + if err := st.s.writeFrame(frame{command: cmdPSH, streamID: st.id, data: p[:n]}); err != nil { + return written, err + } + written += n + p = p[n:] + } + return written, nil +} + +func (st *stream) Close() error { + st.mu.Lock() + already := st.closed + st.closed = true + st.mu.Unlock() + if !already { + st.closeOut.Do(func() { + _ = st.s.writeFrame(frame{command: cmdFIN, streamID: st.id}) + }) + st.s.removeStream(st.id) + } + return nil +} + +func (st *stream) closeRead() { + st.closeIn.Do(func() { close(st.in) }) +} + +func (st *stream) push(data []byte) { + cp := make([]byte, len(data)) + copy(cp, data) + select { + case st.in <- cp: + case <-st.s.done: + } +} + +func (st *stream) LocalAddr() net.Addr { return st.s.conn.LocalAddr() } +func (st *stream) RemoteAddr() net.Addr { return st.s.conn.RemoteAddr() } +func (st *stream) SetDeadline(t time.Time) error { return st.s.conn.SetDeadline(t) } +func (st *stream) SetReadDeadline(t time.Time) error { return st.s.conn.SetReadDeadline(t) } +func (st *stream) SetWriteDeadline(t time.Time) error { return st.s.conn.SetWriteDeadline(t) }