mirror of
https://github.com/nadoo/glider.git
synced 2026-06-26 16:40:12 +08:00
anytls: support anytls
This commit is contained in:
parent
bb439c9345
commit
cb6f2d1e1a
156
.goreleaser.yml
156
.goreleaser.yml
@ -1,96 +1,96 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
- go mod tidy
|
- go mod tidy
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
- id: default
|
- id: default
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
goos:
|
goos:
|
||||||
- windows
|
- windows
|
||||||
- linux
|
- linux
|
||||||
- darwin
|
- darwin
|
||||||
- freebsd
|
- freebsd
|
||||||
goarch:
|
goarch:
|
||||||
- 386
|
- "386"
|
||||||
- amd64
|
- amd64
|
||||||
- arm
|
- arm
|
||||||
- arm64
|
- arm64
|
||||||
- mips
|
- mips
|
||||||
- mipsle
|
- mipsle
|
||||||
- mips64
|
- mips64
|
||||||
- mips64le
|
- mips64le
|
||||||
- riscv64
|
- riscv64
|
||||||
goamd64:
|
goamd64:
|
||||||
- v1
|
- v1
|
||||||
- v3
|
- v3
|
||||||
goarm:
|
goarm:
|
||||||
- 6
|
- "6"
|
||||||
- 7
|
- "7"
|
||||||
gomips:
|
gomips:
|
||||||
- hardfloat
|
- hardfloat
|
||||||
- softfloat
|
- softfloat
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- id: default
|
- id: default
|
||||||
builds:
|
builds:
|
||||||
- default
|
- default
|
||||||
wrap_in_directory: true
|
wrap_in_directory: true
|
||||||
formats: tar.gz
|
formats: tar.gz
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
formats: zip
|
formats: zip
|
||||||
files:
|
files:
|
||||||
- LICENSE
|
- LICENSE
|
||||||
- README.md
|
- README.md
|
||||||
- config/**/*
|
- config/**/*
|
||||||
- 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:
|
||||||
- id: glider
|
- id: glider
|
||||||
package_name: glider
|
package_name: glider
|
||||||
vendor: nadoo
|
vendor: nadoo
|
||||||
homepage: https://github.com/nadoo/glider
|
homepage: https://github.com/nadoo/glider
|
||||||
maintainer: nadoo
|
maintainer: nadoo
|
||||||
description: Glider is a forward proxy with multiple protocols support, and also a dns/dhcp server with ipset management features(like dnsmasq).
|
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
|
license: GPL-3.0 License
|
||||||
formats:
|
formats:
|
||||||
# - apk
|
# - apk
|
||||||
- deb
|
- deb
|
||||||
# - rpm
|
# - rpm
|
||||||
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
|
||||||
contents:
|
contents:
|
||||||
- src: systemd/glider@.service
|
- src: systemd/glider@.service
|
||||||
dst: /etc/systemd/system/glider@.service
|
dst: /etc/systemd/system/glider@.service
|
||||||
|
|
||||||
- src: config/glider.conf.example
|
- src: config/glider.conf.example
|
||||||
dst: /etc/glider/glider.conf.example
|
dst: /etc/glider/glider.conf.example
|
||||||
|
|
||||||
scripts:
|
scripts:
|
||||||
postinstall: "systemd/postinstall.sh"
|
postinstall: "systemd/postinstall.sh"
|
||||||
preremove: "systemd/preremove.sh"
|
preremove: "systemd/preremove.sh"
|
||||||
postremove: "systemd/postremove.sh"
|
postremove: "systemd/postremove.sh"
|
||||||
|
|
||||||
deb:
|
deb:
|
||||||
triggers:
|
triggers:
|
||||||
interest_noawait:
|
interest_noawait:
|
||||||
- /lib/systemd/systemd
|
- /lib/systemd/systemd
|
||||||
|
|||||||
12
README.md
12
README.md
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
110
proxy/anytls/anytls.go
Normal 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
93
proxy/anytls/client.go
Normal 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
70
proxy/anytls/padding.go
Normal 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
97
proxy/anytls/protocol.go
Normal 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
105
proxy/anytls/server.go
Normal 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
233
proxy/anytls/session.go
Normal 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
59
proxy/anytls/settings.go
Normal 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
96
proxy/anytls/stream.go
Normal 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) }
|
||||||
Loading…
Reference in New Issue
Block a user