From 86f9f078fd103c1b92e196442c918266f3fd1a43 Mon Sep 17 00:00:00 2001 From: nadoo <287492+nadoo@users.noreply.github.com> Date: Thu, 25 Jun 2026 22:10:53 +0800 Subject: [PATCH] anytls: support udp --- README.md | 10 ++--- proxy/anytls/client.go | 48 ++++++++++++++++++++- proxy/anytls/packet.go | 95 ++++++++++++++++++++++++++++++++++++++++++ proxy/anytls/server.go | 35 ++++++++++++++++ 4 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 proxy/anytls/packet.go diff --git a/README.md b/README.md index 0df6e68..3c9c60b 100644 --- a/README.md +++ b/README.md @@ -52,11 +52,11 @@ we can set up local listeners as proxy servers, and forward requests to internet |HTTP |√| |√| |client & server |SOCKS5 |√|√|√|√|client & server |SS |√|√|√|√|client & server -|Trojan |√|√|√|√|client & server -|Trojanc |√|√|√|√|trojan cleartext(without tls) -|AnyTLS |√| |√| |client & server -|AnyTLSc |√| |√| |anytls cleartext(without tls) -|VLESS |√|√|√|√|client & server +|Trojan |√| |√|√|client & server +|Trojanc |√| |√|√|trojan cleartext(without tls) +|AnyTLS |√| |√|√|client & server +|AnyTLSc |√| |√|√|anytls cleartext(without tls) +|VLESS |√| |√|√|client & server |VMess | | |√|√|client only |SSR | | |√| |client only |SSH | | |√| |client only diff --git a/proxy/anytls/client.go b/proxy/anytls/client.go index 7ca4c76..610a1ae 100644 --- a/proxy/anytls/client.go +++ b/proxy/anytls/client.go @@ -64,7 +64,42 @@ func (s *AnyTLS) Dial(network, addr string) (net.Conn, error) { } func (s *AnyTLS) DialUDP(network, addr string) (net.PacketConn, error) { - return nil, proxy.ErrNotSupported + if network != "udp" && network != "udp4" && network != "udp6" { + return nil, proxy.ErrNotSupported + } + target := socks.ParseAddr(addr) + if target == nil { + return nil, fmt.Errorf("[anytls] invalid target address: %s", addr) + } + raw := socks.ParseAddr(net.JoinHostPort(uotV2MagicHost, "0")) + if raw == nil { + return nil, fmt.Errorf("[anytls] invalid udp-over-tcp target address") + } + 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 := writeUOTV2Request(st, target); 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 &clientPacketConn{PacketConn: newUOTPacketConn(st, target), session: ss}, nil } func (s *AnyTLS) newClientSession() (*session, error) { @@ -105,3 +140,14 @@ func (c *clientConn) Close() error { _ = c.session.Close() return err } + +type clientPacketConn struct { + net.PacketConn + session *session +} + +func (c *clientPacketConn) Close() error { + err := c.PacketConn.Close() + _ = c.session.Close() + return err +} diff --git a/proxy/anytls/packet.go b/proxy/anytls/packet.go new file mode 100644 index 0000000..bbaecfd --- /dev/null +++ b/proxy/anytls/packet.go @@ -0,0 +1,95 @@ +package anytls + +import ( + "encoding/binary" + "errors" + "io" + "net" + "time" + + "github.com/nadoo/glider/pkg/pool" + "github.com/nadoo/glider/pkg/socks" +) + +const uotV2MagicHost = "sp.v2.udp-over-tcp.arpa" + +// uotPacketConn carries UDP packets over an AnyTLS stream using sing-box +// udp-over-tcp v2 connect format. +type uotPacketConn struct { + net.Conn + target socks.Addr +} + +func newUOTPacketConn(c net.Conn, target socks.Addr) *uotPacketConn { + return &uotPacketConn{Conn: c, target: target} +} + +func (pc *uotPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + if len(b) < 2 { + return 0, pc.target, errors.New("buf size is not enough") + } + + if _, err := io.ReadFull(pc.Conn, b[:2]); err != nil { + return 0, pc.target, err + } + length := int(binary.BigEndian.Uint16(b[:2])) + if len(b) < length { + return 0, pc.target, errors.New("buf size is not enough") + } + + n, err := io.ReadFull(pc.Conn, b[:length]) + return n, pc.target, err +} + +func (pc *uotPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { + buf := pool.GetBytesBuffer() + defer pool.PutBytesBuffer(buf) + + var head [2]byte + binary.BigEndian.PutUint16(head[:], uint16(len(b))) + buf.Write(head[:]) + buf.Write(b) + + n, err := pc.Write(buf.Bytes()) + if n > 2 { + return n - 2, err + } + return 0, err +} + +func (pc *uotPacketConn) SetDeadline(t time.Time) error { + return pc.Conn.SetDeadline(t) +} + +func (pc *uotPacketConn) SetReadDeadline(t time.Time) error { + return pc.Conn.SetReadDeadline(t) +} + +func (pc *uotPacketConn) SetWriteDeadline(t time.Time) error { + return pc.Conn.SetWriteDeadline(t) +} + +func writeUOTV2Request(w io.Writer, target socks.Addr) error { + if target == nil { + return errors.New("invalid target address") + } + + buf := pool.GetBytesBuffer() + defer pool.PutBytesBuffer(buf) + + buf.WriteByte(1) // connect stream format + buf.Write(target) + _, err := w.Write(buf.Bytes()) + return err +} + +func readUOTV2Request(r io.Reader) (socks.Addr, error) { + var connect [1]byte + if _, err := io.ReadFull(r, connect[:]); err != nil { + return nil, err + } + if connect[0] != 1 { + return nil, errors.New("udp-over-tcp v2 non-connect format is not supported") + } + return socks.ReadAddr(r) +} diff --git a/proxy/anytls/server.go b/proxy/anytls/server.go index 6a456a1..789b75f 100644 --- a/proxy/anytls/server.go +++ b/proxy/anytls/server.go @@ -8,6 +8,7 @@ import ( "io" "net" "strings" + "time" "github.com/nadoo/glider/pkg/log" "github.com/nadoo/glider/pkg/socks" @@ -127,6 +128,11 @@ func (s *AnyTLS) serveStream(ss *session, st *stream) { log.F("[anytls] read target error: %v", err) return } + host, _, err := net.SplitHostPort(target.String()) + if err == nil && host == uotV2MagicHost { + s.serveUoT(ss, st) + return + } rc, dialer, err := s.proxy.Dial("tcp", target.String()) if err != nil { @@ -145,3 +151,32 @@ func (s *AnyTLS) serveStream(ss *session, st *stream) { } } } + +func (s *AnyTLS) serveUoT(ss *session, st *stream) { + target, err := readUOTV2Request(st) + if err != nil { + _ = ss.writeFrame(frame{command: cmdSYNACK, streamID: st.id, data: []byte(err.Error())}) + log.F("[anytls] read udp-over-tcp request error: %v", err) + return + } + + dstPC, dialer, err := s.proxy.DialUDP("udp", target.String()) + if err != nil { + _ = ss.writeFrame(frame{command: cmdSYNACK, streamID: st.id, data: []byte(err.Error())}) + log.F("[anytls] %s <-UoT-> %s via %s, error in dial: %v", st.RemoteAddr(), target, dialer.Addr(), err) + return + } + defer dstPC.Close() + + _ = ss.writeFrame(frame{command: cmdSYNACK, streamID: st.id}) + pc := newUOTPacketConn(st, target) + log.F("[anytls] %s <-UoT-> %s via %s", st.RemoteAddr(), target, dialer.Addr()) + + go proxy.CopyUDP(dstPC, nil, pc, 2*time.Minute, 5*time.Second) + if err := proxy.CopyUDP(pc, nil, dstPC, 2*time.Minute, 5*time.Second); err != nil { + log.F("[anytls] %s <-UoT-> %s via %s, relay error: %v", st.RemoteAddr(), target, dialer.Addr(), err) + if d, ok := dialer.(proxy.Dialer); ok && !strings.Contains(err.Error(), s.addr) { + s.proxy.Record(d, false) + } + } +}