anytls: support anytls

This commit is contained in:
nadoo 2026-06-23 23:17:09 +08:00
parent bb439c9345
commit cb6f2d1e1a
12 changed files with 958 additions and 80 deletions

View File

@ -14,7 +14,7 @@ builds:
- darwin - darwin
- freebsd - freebsd
goarch: goarch:
- 386 - "386"
- amd64 - amd64
- arm - arm
- arm64 - arm64
@ -27,8 +27,8 @@ builds:
- v1 - v1
- v3 - v3
goarm: goarm:
- 6 - "6"
- 7 - "7"
gomips: gomips:
- hardfloat - hardfloat
- softfloat - softfloat
@ -49,13 +49,13 @@ archives:
- systemd/* - systemd/*
snapshot: snapshot:
version_template: '{{ incpatch .Version }}-dev-{{.ShortCommit}}' version_template: "{{ incpatch .Version }}-dev-{{.ShortCommit}}"
checksum: checksum:
name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt" name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt"
release: release:
prerelease: true prerelease: "true"
draft: true draft: true
nfpms: nfpms:
@ -73,8 +73,8 @@ nfpms:
dependencies: dependencies:
- libsystemd0 - libsystemd0
bindir: /usr/bin bindir: /usr/bin
release: 1 release: "1"
epoch: 1 epoch: "1"
version_metadata: git version_metadata: git
section: default section: default
priority: extra priority: extra

View File

@ -54,6 +54,7 @@ we can set up local listeners as proxy servers, and forward requests to internet
|SS |√|√|√|√|client & server |SS |√|√|√|√|client & server
|Trojan |√|√|√|√|client & server |Trojan |√|√|√|√|client & server
|Trojanc |√|√|√|√|trojan cleartext(without tls) |Trojanc |√|√|√|√|trojan cleartext(without tls)
|AnyTLS |√| |√| |client & server
|VLESS |√|√|√|√|client & server |VLESS |√|√|√|√|client & server
|VMess | | |√|√|client only |VMess | | |√|√|client only
|SSR | | |√| |client only |SSR | | |√| |client only
@ -197,8 +198,8 @@ URL:
-forward socks5://serverA:1080,socks5://serverB:1080 (proxy chain) -forward socks5://serverA:1080,socks5://serverB:1080 (proxy chain)
SCHEME: SCHEME:
listen : http kcp mixed pxyproto redir redir6 smux sni socks5 ss tcp tls tproxy trojan trojanc udp unix vless 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: direct http kcp reject simple-obfs smux socks4 socks4a socks5 ss ssh ssr tcp tls trojan trojanc udp unix vless vmess 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. 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] 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) 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 domain socket scheme:
unix://path unix://path

View File

@ -85,6 +85,9 @@ listen=127.0.0.1:8443
# trojanc server (trojan without tls) # trojanc server (trojan without tls)
# listen=trojanc://PASSWORD@:1234?fallback=127.0.0.1 # 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
# ---------- # ----------
# Forwarders, we can setup multiple forwarders. # Forwarders, we can setup multiple forwarders.
@ -127,6 +130,9 @@ listen=127.0.0.1:8443
# trojanc as forwarder # trojanc as forwarder
# forward=trojanc://PASSWORD@1.1.1.1:8080 # 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 # vless forwarder
# forward=vless://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@1.1.1.1:443 # forward=vless://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@1.1.1.1:443

View File

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

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

@ -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
`)
}

93
proxy/anytls/client.go Normal file
View File

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

70
proxy/anytls/padding.go Normal file
View File

@ -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[:])
}

97
proxy/anytls/protocol.go Normal file
View File

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

105
proxy/anytls/server.go Normal file
View File

@ -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)
}
}
}

233
proxy/anytls/session.go Normal file
View File

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

59
proxy/anytls/settings.go Normal file
View File

@ -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)})
}

96
proxy/anytls/stream.go Normal file
View File

@ -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) }