add support for anytls

This commit is contained in:
philoinovsky 2026-04-09 18:49:23 +08:00
parent bb439c9345
commit 8fcc41eda2
7 changed files with 839 additions and 0 deletions

2
.gitignore vendored
View File

@ -38,3 +38,5 @@ config/rules.d/*.list
glider
/bak/
/rules.d/
CLAUDE.md

View File

@ -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"

191
proxy/anytls/anytls.go Normal file
View File

@ -0,0 +1,191 @@
// Protocol spec:
// https://github.com/anytls/anytls-go/blob/main/docs/protocol.md
package anytls
import (
"context"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/binary"
"errors"
"fmt"
"net"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/nadoo/glider/pkg/log"
"github.com/nadoo/glider/pkg/socks"
"github.com/nadoo/glider/proxy"
"github.com/nadoo/glider/proxy/anytls/session"
)
func init() {
proxy.RegisterDialer("anytls", NewAnyTLSDialer)
proxy.AddUsage("anytls", `
AnyTLS scheme:
anytls://password@host:port[?serverName=SERVERNAME][&skipVerify=true][&cert=PATH][&idleTimeout=SECONDS][&idleCheckInterval=SECONDS]
`)
}
// AnyTLS implements the anytls protocol client.
type AnyTLS struct {
dialer proxy.Dialer
addr string
password []byte // sha256 hash
tlsConfig *tls.Config
serverName string
skipVerify bool
certFile string
idleCheckInterval time.Duration
idleTimeout time.Duration
client *session.Client
}
// NewAnyTLSDialer returns a new anytls dialer.
func NewAnyTLSDialer(s string, d proxy.Dialer) (proxy.Dialer, error) {
u, err := url.Parse(s)
if err != nil {
return nil, fmt.Errorf("[anytls] parse url err: %s", err)
}
pass := u.User.Username()
if pass == "" {
return nil, errors.New("[anytls] password must be specified")
}
query := u.Query()
a := &AnyTLS{
dialer: d,
addr: u.Host,
skipVerify: query.Get("skipVerify") == "true",
serverName: query.Get("serverName"),
certFile: query.Get("cert"),
}
// default port
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, ":")]
}
}
// password sha256
hash := sha256.Sum256([]byte(pass))
a.password = hash[:]
// idle session config
a.idleCheckInterval = 30 * time.Second
a.idleTimeout = 60 * time.Second
if v := query.Get("idleCheckInterval"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
a.idleCheckInterval = time.Duration(n) * time.Second
}
}
if v := query.Get("idleTimeout"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
a.idleTimeout = time.Duration(n) * time.Second
}
}
// tls config
a.tlsConfig = &tls.Config{
ServerName: a.serverName,
InsecureSkipVerify: a.skipVerify,
MinVersion: tls.VersionTLS12,
}
if a.certFile != "" {
certData, err := os.ReadFile(a.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", a.certFile)
}
a.tlsConfig.RootCAs = certPool
}
// session pool client
a.client = session.NewClient(
context.Background(),
a.createOutboundConn,
a.idleCheckInterval,
a.idleTimeout,
)
return a, nil
}
// createOutboundConn dials to the server, performs TLS handshake, and sends authentication.
func (a *AnyTLS) createOutboundConn(ctx context.Context) (net.Conn, error) {
rc, err := a.dialer.Dial("tcp", a.addr)
if err != nil {
return nil, fmt.Errorf("[anytls] dial to %s error: %s", a.addr, err)
}
tlsConn := tls.Client(rc, a.tlsConfig)
if err := tlsConn.Handshake(); err != nil {
rc.Close()
return nil, fmt.Errorf("[anytls] tls handshake error: %s", err)
}
// Send authentication: sha256(password) + padding_length(uint16) + padding
var buf [34]byte // 32 bytes hash + 2 bytes padding length (0)
copy(buf[:32], a.password)
binary.BigEndian.PutUint16(buf[32:34], 0)
if _, err := tlsConn.Write(buf[:]); err != nil {
tlsConn.Close()
return nil, fmt.Errorf("[anytls] auth write error: %s", err)
}
return tlsConn, nil
}
// Addr returns the forwarder's address.
func (a *AnyTLS) Addr() string {
if a.addr == "" {
return a.dialer.Addr()
}
return a.addr
}
// Dial connects to the address addr on the network net via the proxy.
func (a *AnyTLS) Dial(network, addr string) (net.Conn, error) {
stream, err := a.client.CreateStream(context.Background())
if err != nil {
log.F("[anytls] create stream error: %s", err)
return nil, err
}
// Write SOCKS address to indicate the destination
target := socks.ParseAddr(addr)
if target == nil {
stream.Close()
return nil, fmt.Errorf("[anytls] failed to parse target address: %s", addr)
}
if _, err := stream.Write(target); err != nil {
stream.Close()
return nil, fmt.Errorf("[anytls] failed to write target address: %s", err)
}
return stream, nil
}
// DialUDP connects to the given address via the proxy.
func (a *AnyTLS) DialUDP(network, addr string) (net.PacketConn, error) {
return nil, proxy.ErrNotSupported
}

View File

@ -0,0 +1,196 @@
package session
import (
"context"
"io"
"net"
"sync"
"sync/atomic"
"time"
)
// DialFunc is a function that creates a new outbound connection.
type DialFunc func(ctx context.Context) (net.Conn, error)
// Client manages a pool of sessions for stream multiplexing.
type Client struct {
ctx context.Context
cancel context.CancelFunc
dialOut DialFunc
sessionCounter atomic.Uint64
idleSessions []*Session
idleLock sync.Mutex
sessions map[uint64]*Session
sessionsLock sync.Mutex
idleTimeout time.Duration
}
// NewClient creates a new session pool client.
func NewClient(ctx context.Context, dialOut DialFunc, idleCheckInterval, idleTimeout time.Duration) *Client {
if idleCheckInterval < 5*time.Second {
idleCheckInterval = 30 * time.Second
}
if idleTimeout < 5*time.Second {
idleTimeout = 30 * time.Second
}
c := &Client{
dialOut: dialOut,
sessions: make(map[uint64]*Session),
idleTimeout: idleTimeout,
}
c.ctx, c.cancel = context.WithCancel(ctx)
go c.idleCleanupLoop(idleCheckInterval)
return c
}
// CreateStream opens a new stream, reusing an idle session or creating a new one.
func (c *Client) CreateStream(ctx context.Context) (net.Conn, error) {
select {
case <-c.ctx.Done():
return nil, io.ErrClosedPipe
default:
}
sess := c.getIdleSession()
if sess == nil {
var err error
sess, err = c.createSession(ctx)
if err != nil {
return nil, err
}
}
stream, err := sess.OpenStream()
if err != nil {
sess.Close()
return nil, err
}
// When the stream closes, return the session to the idle pool.
stream.CloseFunc = func() error {
err := stream.CloseRemote()
if !sess.IsClosed() {
select {
case <-c.ctx.Done():
go sess.Close()
default:
c.idleLock.Lock()
sess.IdleSince = time.Now()
c.idleSessions = append(c.idleSessions, sess)
c.idleLock.Unlock()
}
}
return err
}
return stream, nil
}
func (c *Client) getIdleSession() *Session {
c.idleLock.Lock()
defer c.idleLock.Unlock()
// Reuse the newest idle session (last in slice).
for len(c.idleSessions) > 0 {
n := len(c.idleSessions)
sess := c.idleSessions[n-1]
c.idleSessions = c.idleSessions[:n-1]
if !sess.IsClosed() {
return sess
}
}
return nil
}
func (c *Client) createSession(ctx context.Context) (*Session, error) {
conn, err := c.dialOut(ctx)
if err != nil {
return nil, err
}
sess := NewClientSession(conn)
sess.Seq = c.sessionCounter.Add(1)
sess.DieHook = func() {
c.idleLock.Lock()
for i, s := range c.idleSessions {
if s == sess {
c.idleSessions = append(c.idleSessions[:i], c.idleSessions[i+1:]...)
break
}
}
c.idleLock.Unlock()
c.sessionsLock.Lock()
delete(c.sessions, sess.Seq)
c.sessionsLock.Unlock()
}
c.sessionsLock.Lock()
c.sessions[sess.Seq] = sess
c.sessionsLock.Unlock()
sess.Run()
return sess, nil
}
// Close shuts down the client and all sessions.
func (c *Client) Close() error {
c.cancel()
c.sessionsLock.Lock()
toClose := make([]*Session, 0, len(c.sessions))
for _, sess := range c.sessions {
toClose = append(toClose, sess)
}
c.sessions = make(map[uint64]*Session)
c.sessionsLock.Unlock()
for _, sess := range toClose {
sess.Close()
}
return nil
}
func (c *Client) idleCleanupLoop(interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-c.ctx.Done():
return
case <-ticker.C:
c.idleCleanup()
}
}
}
func (c *Client) idleCleanup() {
expTime := time.Now().Add(-c.idleTimeout)
var toClose []*Session
c.idleLock.Lock()
remaining := c.idleSessions[:0]
for _, sess := range c.idleSessions {
if sess.IsClosed() {
continue
}
if sess.IdleSince.Before(expTime) {
toClose = append(toClose, sess)
} else {
remaining = append(remaining, sess)
}
}
c.idleSessions = remaining
c.idleLock.Unlock()
for _, sess := range toClose {
sess.Close()
}
}

View File

@ -0,0 +1,36 @@
package session
import "encoding/binary"
// Command types for the anytls session protocol.
const (
cmdWaste byte = 0 // padding, discarded
cmdSYN byte = 1 // stream open
cmdPSH byte = 2 // data push
cmdFIN byte = 3 // stream close (EOF)
cmdSettings byte = 4 // client -> server settings
cmdAlert byte = 5 // alert message
cmdUpdatePaddingScheme byte = 6 // update padding scheme
cmdSYNACK byte = 7 // server -> client stream opened (v2)
cmdHeartRequest byte = 8 // keepalive request (v2)
cmdHeartResponse byte = 9 // keepalive response (v2)
cmdServerSettings byte = 10 // server -> client settings (v2)
)
const headerSize = 1 + 4 + 2 // cmd(1) + streamID(4) + length(2)
type frame struct {
cmd byte
sid uint32
data []byte
}
func newFrame(cmd byte, sid uint32) frame {
return frame{cmd: cmd, sid: sid}
}
type rawHeader [headerSize]byte
func (h rawHeader) Cmd() byte { return h[0] }
func (h rawHeader) StreamID() uint32 { return binary.BigEndian.Uint32(h[1:]) }
func (h rawHeader) Length() uint16 { return binary.BigEndian.Uint16(h[5:]) }

View File

@ -0,0 +1,325 @@
package session
import (
"encoding/binary"
"fmt"
"io"
"net"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/nadoo/glider/pkg/log"
)
const protocolVersion = "2"
const clientName = "anytls/0.0.11"
// Session multiplexes streams over a single connection.
type Session struct {
conn net.Conn
connLock sync.Mutex
streams map[uint32]*Stream
streamID atomic.Uint32
streamLock sync.RWMutex
dieOnce sync.Once
die chan struct{}
DieHook func()
// pool fields
Seq uint64
IdleSince time.Time
peerVersion byte
// buffering: buffer initial frames until first stream opens
buffering bool
buffer []byte
}
// NewClientSession creates a new client-side session.
func NewClientSession(conn net.Conn) *Session {
s := &Session{
conn: conn,
buffering: true,
die: make(chan struct{}),
streams: make(map[uint32]*Stream),
}
return s
}
// Run starts the session. For clients, it sends settings then starts the recv loop.
func (s *Session) Run() {
settings := fmt.Sprintf("v=%s\nclient=%s\n", protocolVersion, clientName)
f := newFrame(cmdSettings, 0)
f.data = []byte(settings)
s.writeControlFrame(f)
go s.recvLoop()
}
// IsClosed returns true if the session is closed.
func (s *Session) IsClosed() bool {
select {
case <-s.die:
return true
default:
return false
}
}
// Close closes the session and all its streams.
func (s *Session) Close() error {
var once bool
s.dieOnce.Do(func() {
close(s.die)
once = true
})
if once {
if s.DieHook != nil {
s.DieHook()
s.DieHook = nil
}
s.streamLock.Lock()
for _, stream := range s.streams {
stream.closeLocally()
}
s.streams = make(map[uint32]*Stream)
s.streamLock.Unlock()
return s.conn.Close()
}
return io.ErrClosedPipe
}
// OpenStream opens a new stream on the session.
func (s *Session) OpenStream() (*Stream, error) {
if s.IsClosed() {
return nil, io.ErrClosedPipe
}
sid := s.streamID.Add(1)
stream := newStream(sid, s)
if _, err := s.writeControlFrame(newFrame(cmdSYN, sid)); err != nil {
return nil, err
}
s.buffering = false
s.streamLock.Lock()
defer s.streamLock.Unlock()
select {
case <-s.die:
return nil, io.ErrClosedPipe
default:
s.streams[sid] = stream
return stream, nil
}
}
func (s *Session) recvLoop() {
defer s.Close()
var hdr rawHeader
for {
if s.IsClosed() {
return
}
if _, err := io.ReadFull(s.conn, hdr[:]); err != nil {
return
}
sid := hdr.StreamID()
dataLen := int(hdr.Length())
switch hdr.Cmd() {
case cmdPSH:
if dataLen > 0 {
buf := make([]byte, dataLen)
if _, err := io.ReadFull(s.conn, buf); err != nil {
return
}
s.streamLock.RLock()
stream, ok := s.streams[sid]
s.streamLock.RUnlock()
if ok {
stream.pw.Write(buf)
}
}
case cmdFIN:
s.streamLock.Lock()
stream, ok := s.streams[sid]
delete(s.streams, sid)
s.streamLock.Unlock()
if ok {
stream.closeLocally()
}
case cmdSYNACK:
if dataLen > 0 {
buf := make([]byte, dataLen)
if _, err := io.ReadFull(s.conn, buf); err != nil {
return
}
// non-empty SYNACK means handshake failure
s.streamLock.RLock()
stream, ok := s.streams[sid]
s.streamLock.RUnlock()
if ok {
stream.dieErr = fmt.Errorf("remote: %s", string(buf))
stream.pr.CloseWithError(stream.dieErr)
}
}
case cmdWaste:
if dataLen > 0 {
buf := make([]byte, dataLen)
if _, err := io.ReadFull(s.conn, buf); err != nil {
return
}
// discard
}
case cmdAlert:
if dataLen > 0 {
buf := make([]byte, dataLen)
if _, err := io.ReadFull(s.conn, buf); err != nil {
return
}
log.F("[anytls] alert from server: %s", string(buf))
return
}
case cmdServerSettings:
if dataLen > 0 {
buf := make([]byte, dataLen)
if _, err := io.ReadFull(s.conn, buf); err != nil {
return
}
m := parseStringMap(string(buf))
if v, err := strconv.Atoi(m["v"]); err == nil {
s.peerVersion = byte(v)
}
}
case cmdUpdatePaddingScheme:
// We don't implement dynamic padding updates for simplicity;
// just consume the data.
if dataLen > 0 {
buf := make([]byte, dataLen)
if _, err := io.ReadFull(s.conn, buf); err != nil {
return
}
}
case cmdHeartRequest:
s.writeControlFrame(newFrame(cmdHeartResponse, sid))
case cmdHeartResponse:
// no-op
case cmdSettings:
// Server shouldn't send this to client, but consume anyway
if dataLen > 0 {
buf := make([]byte, dataLen)
if _, err := io.ReadFull(s.conn, buf); err != nil {
return
}
}
default:
// Unknown command: consume data
if dataLen > 0 {
buf := make([]byte, dataLen)
if _, err := io.ReadFull(s.conn, buf); err != nil {
return
}
}
}
}
}
func (s *Session) streamClosed(sid uint32) error {
if s.IsClosed() {
return io.ErrClosedPipe
}
_, err := s.writeControlFrame(newFrame(cmdFIN, sid))
s.streamLock.Lock()
delete(s.streams, sid)
s.streamLock.Unlock()
return err
}
func (s *Session) writeDataFrame(sid uint32, data []byte) (int, error) {
dataLen := len(data)
buf := make([]byte, headerSize+dataLen)
buf[0] = cmdPSH
binary.BigEndian.PutUint32(buf[1:5], sid)
binary.BigEndian.PutUint16(buf[5:7], uint16(dataLen))
copy(buf[headerSize:], data)
_, err := s.writeConn(buf)
if err != nil {
return 0, err
}
return dataLen, nil
}
func (s *Session) writeControlFrame(f frame) (int, error) {
dataLen := len(f.data)
buf := make([]byte, headerSize+dataLen)
buf[0] = f.cmd
binary.BigEndian.PutUint32(buf[1:5], f.sid)
binary.BigEndian.PutUint16(buf[5:7], uint16(dataLen))
copy(buf[headerSize:], f.data)
s.conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
_, err := s.writeConn(buf)
if err != nil {
s.Close()
return 0, err
}
s.conn.SetWriteDeadline(time.Time{})
return dataLen, nil
}
func (s *Session) writeConn(b []byte) (int, error) {
s.connLock.Lock()
defer s.connLock.Unlock()
if s.buffering {
s.buffer = append(s.buffer, b...)
return len(b), nil
}
if len(s.buffer) > 0 {
b = append(s.buffer, b...)
s.buffer = nil
}
return s.conn.Write(b)
}
// NumStreams returns the number of active streams.
func (s *Session) NumStreams() int {
s.streamLock.RLock()
defer s.streamLock.RUnlock()
return len(s.streams)
}
// parseStringMap parses newline-separated key=value pairs.
func parseStringMap(s string) map[string]string {
m := make(map[string]string)
for _, line := range strings.Split(s, "\n") {
if k, v, ok := strings.Cut(line, "="); ok {
m[strings.TrimSpace(k)] = strings.TrimSpace(v)
}
}
return m
}

View File

@ -0,0 +1,88 @@
package session
import (
"io"
"net"
"sync"
"time"
)
// Stream implements net.Conn over a multiplexed session.
type Stream struct {
id uint32
sess *Session
pr *io.PipeReader
pw *io.PipeWriter
dieOnce sync.Once
dieErr error
CloseFunc func() error // overridable close hook
}
func newStream(id uint32, sess *Session) *Stream {
pr, pw := io.Pipe()
return &Stream{
id: id,
sess: sess,
pr: pr,
pw: pw,
}
}
func (s *Stream) Read(b []byte) (int, error) {
return s.pr.Read(b)
}
func (s *Stream) Write(b []byte) (int, error) {
if s.dieErr != nil {
return 0, s.dieErr
}
return s.sess.writeDataFrame(s.id, b)
}
func (s *Stream) Close() error {
if s.CloseFunc != nil {
return s.CloseFunc()
}
return s.CloseRemote()
}
func (s *Stream) CloseRemote() error {
var once bool
s.dieOnce.Do(func() {
s.dieErr = io.ErrClosedPipe
s.pr.Close()
once = true
})
if once {
return s.sess.streamClosed(s.id)
}
return s.dieErr
}
// closeLocally closes the stream without notifying the remote peer.
func (s *Stream) closeLocally() {
s.dieOnce.Do(func() {
s.dieErr = net.ErrClosed
s.pr.Close()
})
}
func (s *Stream) LocalAddr() net.Addr {
if ts, ok := s.sess.conn.(interface{ LocalAddr() net.Addr }); ok {
return ts.LocalAddr()
}
return nil
}
func (s *Stream) RemoteAddr() net.Addr {
if ts, ok := s.sess.conn.(interface{ RemoteAddr() net.Addr }); ok {
return ts.RemoteAddr()
}
return nil
}
func (s *Stream) SetDeadline(t time.Time) error { return nil }
func (s *Stream) SetReadDeadline(t time.Time) error { return nil }
func (s *Stream) SetWriteDeadline(t time.Time) error { return nil }