diff --git a/dialer/http3/metadata.go b/dialer/http3/metadata.go index 41ed700..5215f24 100644 --- a/dialer/http3/metadata.go +++ b/dialer/http3/metadata.go @@ -29,32 +29,27 @@ type metadata struct { func (d *http3Dialer) parseMetadata(md mdata.Metadata) (err error) { const ( - authorizePath = "authorizePath" - pushPath = "pushPath" - pullPath = "pullPath" - host = "host" - - keepAlive = "keepAlive" + keepAlive = "keepalive" keepAlivePeriod = "ttl" handshakeTimeout = "handshakeTimeout" maxIdleTimeout = "maxIdleTimeout" maxStreams = "maxStreams" ) - d.md.authorizePath = mdutil.GetString(md, authorizePath) + d.md.authorizePath = mdutil.GetString(md, "pht.authorizePath", "authorizePath") if !strings.HasPrefix(d.md.authorizePath, "/") { d.md.authorizePath = defaultAuthorizePath } - d.md.pushPath = mdutil.GetString(md, pushPath) + d.md.pushPath = mdutil.GetString(md, "pht.pushPath", "pushPath") if !strings.HasPrefix(d.md.pushPath, "/") { d.md.pushPath = defaultPushPath } - d.md.pullPath = mdutil.GetString(md, pullPath) + d.md.pullPath = mdutil.GetString(md, "pht.pullPath", "pullPath") if !strings.HasPrefix(d.md.pullPath, "/") { d.md.pullPath = defaultPullPath } - d.md.host = mdutil.GetString(md, host) + d.md.host = mdutil.GetString(md, "host") if !md.IsExists(keepAlive) || mdutil.GetBool(md, keepAlive) { d.md.keepAlivePeriod = mdutil.GetDuration(md, keepAlivePeriod) if d.md.keepAlivePeriod <= 0 { diff --git a/dialer/http3/wt/client.go b/dialer/http3/wt/client.go new file mode 100644 index 0000000..f0ef197 --- /dev/null +++ b/dialer/http3/wt/client.go @@ -0,0 +1,58 @@ +package wt + +import ( + "context" + "net" + "net/http" + "net/http/httputil" + "net/url" + + "github.com/go-gost/core/logger" + wt_util "github.com/go-gost/x/internal/util/wt" + wt "github.com/quic-go/webtransport-go" +) + +type Client struct { + host string + path string + header http.Header + dialer *wt.Dialer + session *wt.Session + log logger.Logger +} + +func (c *Client) Dial(ctx context.Context, addr string) (net.Conn, error) { + ok := false + if c.session != nil { + select { + case <-c.session.Context().Done(): + default: + ok = true + } + } + if !ok { + url := url.URL{ + Scheme: "https", + Host: c.host, + Path: c.path, + } + resp, session, err := c.dialer.Dial(ctx, url.String(), c.header) + if err != nil { + return nil, err + } + + if c.log.IsLevelEnabled(logger.TraceLevel) { + dump, _ := httputil.DumpResponse(resp, false) + c.log.Trace(string(dump)) + } + + c.session = session + } + + stream, err := c.session.OpenStream() + if err != nil { + return nil, err + } + + return wt_util.Conn(c.session, stream), nil +} diff --git a/dialer/http3/wt/dialer.go b/dialer/http3/wt/dialer.go new file mode 100644 index 0000000..7df98f6 --- /dev/null +++ b/dialer/http3/wt/dialer.go @@ -0,0 +1,111 @@ +package wt + +import ( + "context" + "crypto/tls" + "net" + "sync" + + "github.com/go-gost/core/dialer" + md "github.com/go-gost/core/metadata" + "github.com/go-gost/x/registry" + "github.com/quic-go/quic-go" + "github.com/quic-go/quic-go/http3" + wt "github.com/quic-go/webtransport-go" +) + +func init() { + registry.DialerRegistry().Register("wt", NewDialer) +} + +type wtDialer struct { + clients map[string]*Client + clientMutex sync.Mutex + md metadata + options dialer.Options +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &wtDialer{ + clients: make(map[string]*Client), + options: options, + } +} + +func (d *wtDialer) Init(md md.Metadata) (err error) { + if err = d.parseMetadata(md); err != nil { + return + } + return +} + +// Multiplex implements dialer.Multiplexer interface. +func (d *wtDialer) Multiplex() bool { + return true +} + +func (d *wtDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (conn net.Conn, err error) { + d.clientMutex.Lock() + defer d.clientMutex.Unlock() + + client := d.clients[addr] + if client == nil { + var options dialer.DialOptions + for _, opt := range opts { + opt(&options) + } + + host := d.md.host + if host == "" { + host = options.Host + } + if h, _, _ := net.SplitHostPort(host); h != "" { + host = h + } + + client = &Client{ + log: d.options.Logger, + host: host, + path: d.md.path, + header: d.md.header, + dialer: &wt.Dialer{ + RoundTripper: &http3.RoundTripper{ + TLSClientConfig: d.options.TLSConfig, + Dial: func(ctx context.Context, adr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + // d.options.Logger.Infof("dial: %s, %s, %s", addr, adr, host) + udpAddr, err := net.ResolveUDPAddr("udp", addr) + if err != nil { + return nil, err + } + + udpConn, err := options.NetDialer.Dial(ctx, "udp", "") + if err != nil { + return nil, err + } + + return quic.DialEarly(ctx, udpConn.(net.PacketConn), udpAddr, tlsCfg, cfg) + }, + QuicConfig: &quic.Config{ + KeepAlivePeriod: d.md.keepAlivePeriod, + HandshakeIdleTimeout: d.md.handshakeTimeout, + MaxIdleTimeout: d.md.maxIdleTimeout, + /* + Versions: []quic.VersionNumber{ + quic.Version1, + }, + */ + MaxIncomingStreams: int64(d.md.maxStreams), + }, + }, + }, + } + d.clients[addr] = client + } + + return client.Dial(ctx, addr) +} diff --git a/dialer/http3/wt/metadata.go b/dialer/http3/wt/metadata.go new file mode 100644 index 0000000..f2408ac --- /dev/null +++ b/dialer/http3/wt/metadata.go @@ -0,0 +1,62 @@ +package wt + +import ( + "net/http" + "time" + + mdata "github.com/go-gost/core/metadata" + mdutil "github.com/go-gost/core/metadata/util" +) + +const ( + defaultPath = "/wt" + defaultKeepalivePeriod = 15 * time.Second +) + +type metadata struct { + host string + path string + header http.Header + + // QUIC config options + keepAlivePeriod time.Duration + maxIdleTimeout time.Duration + handshakeTimeout time.Duration + maxStreams int +} + +func (d *wtDialer) parseMetadata(md mdata.Metadata) (err error) { + const ( + keepAlive = "keepalive" + keepAlivePeriod = "ttl" + handshakeTimeout = "handshakeTimeout" + maxIdleTimeout = "maxIdleTimeout" + maxStreams = "maxStreams" + ) + + d.md.host = mdutil.GetString(md, "wt.host", "host") + d.md.path = mdutil.GetString(md, "wt.path", "path") + if d.md.path == "" { + d.md.path = defaultPath + } + + if !md.IsExists(keepAlive) || mdutil.GetBool(md, keepAlive) { + d.md.keepAlivePeriod = mdutil.GetDuration(md, keepAlivePeriod) + if d.md.keepAlivePeriod <= 0 { + d.md.keepAlivePeriod = 10 * time.Second + } + } + d.md.handshakeTimeout = mdutil.GetDuration(md, handshakeTimeout) + d.md.maxIdleTimeout = mdutil.GetDuration(md, maxIdleTimeout) + d.md.maxStreams = mdutil.GetInt(md, maxStreams) + + if m := mdutil.GetStringMapString(md, "wt.header", "header"); len(m) > 0 { + h := http.Header{} + for k, v := range m { + h.Add(k, v) + } + d.md.header = h + } + + return +} diff --git a/go.mod b/go.mod index 40a13a1..d1f4e8d 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,8 @@ require ( github.com/pion/dtls/v2 v2.2.6 github.com/pires/go-proxyproto v0.7.0 github.com/prometheus/client_golang v1.17.0 - github.com/quic-go/quic-go v0.38.1 + github.com/quic-go/quic-go v0.39.0 + github.com/quic-go/webtransport-go v0.6.0 github.com/rs/xid v1.3.0 github.com/shadowsocks/go-shadowsocks2 v0.1.5 github.com/shadowsocks/shadowsocks-go v0.0.0-20200409064450-3e585ff90601 @@ -61,7 +62,6 @@ require ( github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gopacket v1.1.19 // indirect github.com/google/pprof v0.0.0-20230912144702-c363fe2c2ed8 // indirect @@ -102,6 +102,7 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect + go.uber.org/mock v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/mod v0.12.0 // indirect diff --git a/go.sum b/go.sum index 1c228df..f26f424 100644 --- a/go.sum +++ b/go.sum @@ -75,6 +75,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= @@ -91,16 +93,12 @@ github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SU github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gost/core v0.0.0-20231107150907-7f581cb668b1 h1:hzxZgut10J3Rm0meINWB5yal3gIV9IkThKLbshsd/Mk= -github.com/go-gost/core v0.0.0-20231107150907-7f581cb668b1/go.mod h1:ndkgWVYRLwupVaFFWv8ML1Nr8tD3xhHK245PLpUDg4E= github.com/go-gost/core v0.0.0-20231109123312-8e4fc06cf1b7 h1:sDsPtmP51qf8zN/RbZZj/3vNLCoH0sdvpIRwV6TfzvY= github.com/go-gost/core v0.0.0-20231109123312-8e4fc06cf1b7/go.mod h1:ndkgWVYRLwupVaFFWv8ML1Nr8tD3xhHK245PLpUDg4E= github.com/go-gost/gosocks4 v0.0.1 h1:+k1sec8HlELuQV7rWftIkmy8UijzUt2I6t+iMPlGB2s= github.com/go-gost/gosocks4 v0.0.1/go.mod h1:3B6L47HbU/qugDg4JnoFPHgJXE43Inz8Bah1QaN9qCc= github.com/go-gost/gosocks5 v0.4.0 h1:EIrOEkpJez4gwHrMa33frA+hHXJyevjp47thpMQsJzI= github.com/go-gost/gosocks5 v0.4.0/go.mod h1:1G6I7HP7VFVxveGkoK8mnprnJqSqJjdcASKsdUn4Pp4= -github.com/go-gost/plugin v0.0.0-20231102125124-a1cc7a13e066 h1:/pDM9JP9ESSRuAr237yAXB6WiDdjEeulDkaLa9Gw0ss= -github.com/go-gost/plugin v0.0.0-20231102125124-a1cc7a13e066/go.mod h1:qXr2Zm9Ex2ATqnWuNUzVZqySPMnuIihvblYZt4MlZLw= github.com/go-gost/plugin v0.0.0-20231109123346-0ae4157b9d25 h1:sOarC0xAJij4VtEhkJRng5okZW23KlXprxhb5XFZ+pw= github.com/go-gost/plugin v0.0.0-20231109123346-0ae4157b9d25/go.mod h1:qXr2Zm9Ex2ATqnWuNUzVZqySPMnuIihvblYZt4MlZLw= github.com/go-gost/relay v0.4.1-0.20230916134211-828f314ddfe7 h1:qAG1OyjvdA5h221CfFSS3J359V3d2E7dJWyP29QoDSI= @@ -138,8 +136,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -292,8 +288,10 @@ github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg= github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= -github.com/quic-go/quic-go v0.38.1 h1:M36YWA5dEhEeT+slOu/SwMEucbYd0YFidxG3KlGPZaE= -github.com/quic-go/quic-go v0.38.1/go.mod h1:ijnZM7JsFIkp4cRyjxJNIzdSfCLmUMg9wdyhGmg+SN4= +github.com/quic-go/quic-go v0.39.0 h1:AgP40iThFMY0bj8jGxROhw3S0FMGa8ryqsmi9tBH3So= +github.com/quic-go/quic-go v0.39.0/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q= +github.com/quic-go/webtransport-go v0.6.0 h1:CvNsKqc4W2HljHJnoT+rMmbRJybShZ0YPFDD3NxaZLY= +github.com/quic-go/webtransport-go v0.6.0/go.mod h1:9KjU4AEBqEQidGHNDkZrb8CAa1abRaosM2yGOyiikEc= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -367,7 +365,6 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -375,6 +372,8 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= +go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/arch v0.0.0-20190909030613-46d78d1859ac/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= @@ -428,7 +427,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -463,7 +461,6 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -488,7 +485,6 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= @@ -528,9 +524,7 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -613,7 +607,6 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= diff --git a/internal/util/wt/conn.go b/internal/util/wt/conn.go new file mode 100644 index 0000000..02d4d46 --- /dev/null +++ b/internal/util/wt/conn.go @@ -0,0 +1,52 @@ +package wt + +import ( + "net" + "time" + + wt "github.com/quic-go/webtransport-go" +) + +type conn struct { + session *wt.Session + stream wt.Stream +} + +func Conn(session *wt.Session, stream wt.Stream) net.Conn { + return &conn{ + session: session, + stream: stream, + } +} + +func (c *conn) Read(b []byte) (n int, err error) { + return c.stream.Read(b) +} + +func (c *conn) Write(b []byte) (n int, err error) { + return c.stream.Write(b) +} + +func (c *conn) Close() error { + return c.stream.Close() +} + +func (c *conn) LocalAddr() net.Addr { + return c.session.LocalAddr() +} + +func (c *conn) RemoteAddr() net.Addr { + return c.session.RemoteAddr() +} + +func (c *conn) SetDeadline(t time.Time) error { + return c.stream.SetDeadline(t) +} + +func (c *conn) SetReadDeadline(t time.Time) error { + return c.stream.SetReadDeadline(t) +} + +func (c *conn) SetWriteDeadline(t time.Time) error { + return c.stream.SetWriteDeadline(t) +} diff --git a/listener/http3/h3/metadata.go b/listener/http3/h3/metadata.go index ab63ded..8e69974 100644 --- a/listener/http3/h3/metadata.go +++ b/listener/http3/h3/metadata.go @@ -30,11 +30,7 @@ type metadata struct { func (l *http3Listener) parseMetadata(md mdata.Metadata) (err error) { const ( - authorizePath = "authorizePath" - pushPath = "pushPath" - pullPath = "pullPath" - - keepAlive = "keepAlive" + keepAlive = "keepalive" keepAlivePeriod = "ttl" handshakeTimeout = "handshakeTimeout" maxIdleTimeout = "maxIdleTimeout" @@ -43,15 +39,15 @@ func (l *http3Listener) parseMetadata(md mdata.Metadata) (err error) { backlog = "backlog" ) - l.md.authorizePath = mdutil.GetString(md, authorizePath) + l.md.authorizePath = mdutil.GetString(md, "pht.authorizePath", "authorizePath") if !strings.HasPrefix(l.md.authorizePath, "/") { l.md.authorizePath = defaultAuthorizePath } - l.md.pushPath = mdutil.GetString(md, pushPath) + l.md.pushPath = mdutil.GetString(md, "pht.pushPath", "pushPath") if !strings.HasPrefix(l.md.pushPath, "/") { l.md.pushPath = defaultPushPath } - l.md.pullPath = mdutil.GetString(md, pullPath) + l.md.pullPath = mdutil.GetString(md, "pht.pullPath", "pullPath") if !strings.HasPrefix(l.md.pullPath, "/") { l.md.pullPath = defaultPullPath } diff --git a/listener/http3/wt/listener.go b/listener/http3/wt/listener.go new file mode 100644 index 0000000..3e0d110 --- /dev/null +++ b/listener/http3/wt/listener.go @@ -0,0 +1,158 @@ +package wt + +import ( + "net" + "net/http" + "net/http/httputil" + + "github.com/go-gost/core/listener" + "github.com/go-gost/core/logger" + md "github.com/go-gost/core/metadata" + xnet "github.com/go-gost/x/internal/net" + wt_util "github.com/go-gost/x/internal/util/wt" + "github.com/go-gost/x/registry" + "github.com/quic-go/quic-go" + "github.com/quic-go/quic-go/http3" + wt "github.com/quic-go/webtransport-go" +) + +func init() { + registry.ListenerRegistry().Register("wt", NewListener) +} + +type wtListener struct { + addr net.Addr + srv *wt.Server + cqueue chan net.Conn + errChan chan error + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &wtListener{ + logger: options.Logger, + options: options, + } +} + +func (l *wtListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + network := "udp" + if xnet.IsIPv4(l.options.Addr) { + network = "udp4" + } + l.addr, err = net.ResolveUDPAddr(network, l.options.Addr) + if err != nil { + return + } + + mux := http.NewServeMux() + mux.Handle(l.md.path, http.HandlerFunc(l.upgrade)) + + l.srv = &wt.Server{ + H3: http3.Server{ + Addr: l.options.Addr, + TLSConfig: l.options.TLSConfig, + QuicConfig: &quic.Config{ + KeepAlivePeriod: l.md.keepAlivePeriod, + HandshakeIdleTimeout: l.md.handshakeTimeout, + MaxIdleTimeout: l.md.maxIdleTimeout, + /* + Versions: []quic.VersionNumber{ + quic.Version1, + quic.Version2, + }, + */ + MaxIncomingStreams: int64(l.md.maxStreams), + }, + Handler: mux, + }, + CheckOrigin: func(r *http.Request) bool { return true }, + } + + l.cqueue = make(chan net.Conn, l.md.backlog) + l.errChan = make(chan error, 1) + + go func() { + if err := l.srv.ListenAndServe(); err != nil { + l.logger.Error(err) + } + }() + + return +} + +func (l *wtListener) Accept() (conn net.Conn, err error) { + var ok bool + select { + case conn = <-l.cqueue: + case err, ok = <-l.errChan: + if !ok { + err = listener.ErrClosed + } + } + return +} + +func (l *wtListener) Addr() net.Addr { + return l.addr +} + +func (l *wtListener) Close() (err error) { + return l.srv.Close() +} + +func (l *wtListener) upgrade(w http.ResponseWriter, r *http.Request) { + log := l.logger.WithFields(map[string]any{ + "local": l.addr.String(), + "remote": r.RemoteAddr, + }) + if l.logger.IsLevelEnabled(logger.TraceLevel) { + dump, _ := httputil.DumpRequest(r, false) + log.Trace(string(dump)) + } + + s, err := l.srv.Upgrade(w, r) + if err != nil { + l.logger.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + l.mux(s, log) +} + +func (l *wtListener) mux(s *wt.Session, log logger.Logger) (err error) { + defer func() { + if err != nil { + s.CloseWithError(1, err.Error()) + } else { + s.CloseWithError(0, "") + } + }() + + for { + var stream wt.Stream + stream, err = s.AcceptStream(s.Context()) + if err != nil { + log.Errorf("accept stream: %v", err) + return + } + + select { + case l.cqueue <- wt_util.Conn(s, stream): + default: + stream.Close() + l.logger.Warnf("connection queue is full, stream %v discarded", stream.StreamID()) + } + } +} diff --git a/listener/http3/wt/metadata.go b/listener/http3/wt/metadata.go new file mode 100644 index 0000000..6188ef1 --- /dev/null +++ b/listener/http3/wt/metadata.go @@ -0,0 +1,58 @@ +package wt + +import ( + "time" + + mdata "github.com/go-gost/core/metadata" + mdutil "github.com/go-gost/core/metadata/util" +) + +const ( + defaultPath = "/wt" + defaultBacklog = 128 +) + +type metadata struct { + path string + backlog int + + // QUIC config options + keepAlivePeriod time.Duration + maxIdleTimeout time.Duration + handshakeTimeout time.Duration + maxStreams int +} + +func (l *wtListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + keepAlive = "keepalive" + keepAlivePeriod = "ttl" + handshakeTimeout = "handshakeTimeout" + maxIdleTimeout = "maxIdleTimeout" + maxStreams = "maxStreams" + + backlog = "backlog" + ) + + l.md.path = mdutil.GetString(md, "wt.path", "path") + if l.md.path == "" { + l.md.path = defaultPath + } + + l.md.backlog = mdutil.GetInt(md, backlog) + if l.md.backlog <= 0 { + l.md.backlog = defaultBacklog + } + + if mdutil.GetBool(md, keepAlive) { + l.md.keepAlivePeriod = mdutil.GetDuration(md, keepAlivePeriod) + if l.md.keepAlivePeriod <= 0 { + l.md.keepAlivePeriod = 10 * time.Second + } + } + l.md.handshakeTimeout = mdutil.GetDuration(md, handshakeTimeout) + l.md.maxIdleTimeout = mdutil.GetDuration(md, maxIdleTimeout) + l.md.maxStreams = mdutil.GetInt(md, maxStreams) + + return +} diff --git a/listener/mws/listener.go b/listener/mws/listener.go index a4c9402..4a12789 100644 --- a/listener/mws/listener.go +++ b/listener/mws/listener.go @@ -160,6 +160,7 @@ func (l *mwsListener) upgrade(w http.ResponseWriter, r *http.Request) { conn, err := l.upgrader.Upgrade(w, r, l.md.header) if err != nil { + w.WriteHeader(http.StatusInternalServerError) log.Error(err) return } diff --git a/listener/ws/listener.go b/listener/ws/listener.go index 6958251..16ce03d 100644 --- a/listener/ws/listener.go +++ b/listener/ws/listener.go @@ -156,6 +156,7 @@ func (l *wsListener) upgrade(w http.ResponseWriter, r *http.Request) { conn, err := l.upgrader.Upgrade(w, r, l.md.header) if err != nil { l.logger.Error(err) + w.WriteHeader(http.StatusInternalServerError) return }