mirror of
https://github.com/nadoo/glider.git
synced 2026-04-24 21:20:12 +08:00
add support for anytls
This commit is contained in:
parent
bb439c9345
commit
8fcc41eda2
2
.gitignore
vendored
2
.gitignore
vendored
@ -38,3 +38,5 @@ config/rules.d/*.list
|
|||||||
glider
|
glider
|
||||||
/bak/
|
/bak/
|
||||||
/rules.d/
|
/rules.d/
|
||||||
|
|
||||||
|
CLAUDE.md
|
||||||
@ -5,6 +5,7 @@ import (
|
|||||||
// _ "github.com/nadoo/glider/service/xxx"
|
// _ "github.com/nadoo/glider/service/xxx"
|
||||||
|
|
||||||
// comment out the protocols you don't need to make the compiled binary smaller.
|
// 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/http"
|
||||||
_ "github.com/nadoo/glider/proxy/kcp"
|
_ "github.com/nadoo/glider/proxy/kcp"
|
||||||
_ "github.com/nadoo/glider/proxy/mixed"
|
_ "github.com/nadoo/glider/proxy/mixed"
|
||||||
|
|||||||
191
proxy/anytls/anytls.go
Normal file
191
proxy/anytls/anytls.go
Normal 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
|
||||||
|
}
|
||||||
196
proxy/anytls/session/client.go
Normal file
196
proxy/anytls/session/client.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
36
proxy/anytls/session/frame.go
Normal file
36
proxy/anytls/session/frame.go
Normal 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:]) }
|
||||||
325
proxy/anytls/session/session.go
Normal file
325
proxy/anytls/session/session.go
Normal 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
|
||||||
|
}
|
||||||
88
proxy/anytls/session/stream.go
Normal file
88
proxy/anytls/session/stream.go
Normal 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 }
|
||||||
Loading…
Reference in New Issue
Block a user