diff --git a/conn.go b/conn.go index 22afb78..be4b811 100644 --- a/conn.go +++ b/conn.go @@ -4,7 +4,9 @@ import ( "encoding/binary" "io" "net" + "reflect" "sync" + "sync/atomic" "time" ) @@ -14,23 +16,30 @@ var DefaultFirstFragmentLen = 4 // Conn remote: real server; local: relay type Conn struct { relay relay + isold uintptr init *sync.Once conn *net.TCPConn - isold bool } // NewConn wraps *net.TCPConn (net.Conn must be *net.TCPConn) func NewConn(conn net.Conn) *Conn { - return &Conn{ + c := &Conn{ relay: newrelay(), init: &sync.Once{}, conn: conn.(*net.TCPConn), } + switch o := conn.(type) { + case *net.TCPConn: + c.conn = o + default: + panic("unsupported conn type: " + reflect.TypeOf(conn).String()) + } + return c } // Write is send func (conn *Conn) Write(b []byte) (int, error) { - if conn.isold || DefaultFirstFragmentLen == 0 { + if atomic.LoadUintptr(&conn.isold) != 0 || DefaultFirstFragmentLen == 0 { return conn.conn.Write(b) } go conn.init.Do(func() { @@ -44,10 +53,12 @@ func (conn *Conn) Write(b []byte) (int, error) { // ReadFrom when client want to send to server, detect and split. func (conn *Conn) ReadFrom(r io.Reader) (n int64, err error) { - if conn.isold || DefaultFirstFragmentLen == 0 { + if atomic.LoadUintptr(&conn.isold) != 0 || DefaultFirstFragmentLen == 0 { return conn.conn.ReadFrom(r) } + defer atomic.StoreUintptr(&conn.isold, 1) + // ContentType [0:1] // Version [1:3] // Length [3:5] @@ -61,10 +72,6 @@ func (conn *Conn) ReadFrom(r io.Reader) (n int64, err error) { var b []byte bd := newbuilder() - defer func() { - conn.isold = true - }() - // ContentType [0:1] Version [1:2] 0x03 _, err = io.ReadFull(r, header[:2]) if err != nil { diff --git a/dns/dns.go b/dns/dns.go index 2289904..dcc5f5c 100644 --- a/dns/dns.go +++ b/dns/dns.go @@ -89,6 +89,14 @@ type List struct { b map[string][]string } +// NewEmptyList ... +func NewEmptyList() *List { + return &List{ + m: make(map[string][]*dnsstat, 64), + b: make(map[string][]string, 64), + } +} + // Config is the user config type Config struct { Servers map[string][]string `yaml:"Servers"` // Servers map[dot.com]ip:ports diff --git a/go.mod b/go.mod index b482806..02bfbe7 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,23 @@ go 1.22 require ( github.com/FloatTech/ttl v0.0.0-20250224045156-012b1463287d + github.com/fumiama/go-base16384 v1.7.1 + github.com/quic-go/quic-go v0.49.1-0.20250213113345-5af39164b9fe github.com/sirupsen/logrus v1.9.3 - golang.org/x/net v0.24.0 + golang.org/x/net v0.28.0 ) require ( - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.org/x/tools v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index 6d0797c..3318cb0 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,69 @@ github.com/FloatTech/ttl v0.0.0-20250224045156-012b1463287d h1:mUQ/c3wXKsUGa4Sg9DBy01APXKB68PmobhxOyaJI7lY= github.com/FloatTech/ttl v0.0.0-20250224045156-012b1463287d/go.mod h1:fHZFWGquNXuHttu9dUYoKuNbm3dzLETnIOnm1muSfDs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fumiama/go-base16384 v1.7.1 h1:1P1x6FWRvd7PtbH4idDAGWAjKKcVxggxlROYKRXbw58= +github.com/fumiama/go-base16384 v1.7.1/go.mod h1:OEn+947GV5gsbTAnyuUW/SrfxJYUdYupSIQXOuGOcXM= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.49.1-0.20250213113345-5af39164b9fe h1:gaj0z36YmZoU5opm6XIK20Vn18jDB6AxbjUXCd732k0= +github.com/quic-go/quic-go v0.49.1-0.20250213113345-5af39164b9fe/go.mod h1:s2wDnmCdooUQBmQfpUSTCYBl1/D4FcqbULMMkASvR6s= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/http2/http2.go b/http2/http.go similarity index 100% rename from http2/http2.go rename to http2/http.go diff --git a/http2/http2_test.go b/http2/http_test.go similarity index 100% rename from http2/http2_test.go rename to http2/http_test.go diff --git a/http3/http.go b/http3/http.go new file mode 100644 index 0000000..014d652 --- /dev/null +++ b/http3/http.go @@ -0,0 +1,117 @@ +// Package http3 is the same as the standard http lib with HTTP3 client support +package http3 + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/tls" + "errors" + "io" + mrand "math/rand" + "net" + "net/http" + "net/netip" + "net/url" + "time" + + base14 "github.com/fumiama/go-base16384" + "github.com/fumiama/terasu/dns" + "github.com/quic-go/quic-go" + "github.com/quic-go/quic-go/http3" +) + +// ErrEmptyHostAddress is returned when DNS lookup for a host returns no addresses +var ErrEmptyHostAddress = errors.New("empty host addr") + +// defaultDialer is the default dialer used for establishing TCP connections +var defaultDialer = net.Dialer{ + Timeout: 10 * time.Second, +} + +// SetDefaultClientTimeout sets the default timeout for all HTTP2 client connections +func SetDefaultClientTimeout(t time.Duration) { + defaultDialer.Timeout = t +} + +// DefaultClient is the default HTTP2 client that supports HTTP/2 and DNS resolution +var DefaultClient = http.Client{ + Transport: &http3.Transport{ + Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + addrs, err := dns.LookupHost(ctx, host) + if err != nil { + return nil, err + } + if len(addrs) == 0 { + return nil, ErrEmptyHostAddress + } + var conn net.Conn + var qConn quic.EarlyConnection + for _, a := range addrs { + if defaultDialer.Timeout != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), defaultDialer.Timeout) + defer cancel() + } else if !defaultDialer.Deadline.IsZero() { + var cancel context.CancelFunc + ctx, cancel = context.WithDeadline(context.Background(), defaultDialer.Deadline) + defer cancel() + } + conn, err = net.ListenUDP("udp", nil) + if err != nil { + continue + } + ucon := conn.(*net.UDPConn) + raddr := net.UDPAddrFromAddrPort( + netip.MustParseAddrPort(net.JoinHostPort(a, port)), + ) + n := (mrand.Intn(128) + 128) / 7 * 14 + w := bytes.NewBuffer(make([]byte, 0, base14.EncodeLen(n))) + e := base14.NewEncoder(w) + _, _ = io.CopyN(e, rand.Reader, int64(n)) + _ = e.Close() + _, _ = ucon.WriteToUDP(w.Bytes(), raddr) + // re-init ctx due to deadline settings in tcp dial + if defaultDialer.Timeout != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), defaultDialer.Timeout) + defer cancel() + } else if !defaultDialer.Deadline.IsZero() { + var cancel context.CancelFunc + ctx, cancel = context.WithDeadline(context.Background(), defaultDialer.Deadline) + defer cancel() + } + qConn, err = quic.DialEarly(ctx, ucon, raddr, tlsCfg, cfg) + if err == nil { + break + } + panic(err) + } + return qConn, err + }, + }, +} + +// Get sends an HTTP GET request to the specified URL using the default HTTP2 client +func Get(url string) (resp *http.Response, err error) { + return DefaultClient.Get(url) +} + +// Head sends an HTTP HEAD request to the specified URL using the default HTTP2 client +func Head(url string) (resp *http.Response, err error) { + return DefaultClient.Head(url) +} + +// Post sends an HTTP POST request to the specified URL with the given content type and body using the default HTTP2 client +func Post(url string, contentType string, body io.Reader) (resp *http.Response, err error) { + return DefaultClient.Post(url, contentType, body) +} + +// PostForm sends an HTTP POST request with form data to the specified URL using the default HTTP2 client +func PostForm(url string, data url.Values) (resp *http.Response, err error) { + return DefaultClient.PostForm(url, data) +} diff --git a/http3/http_test.go b/http3/http_test.go new file mode 100644 index 0000000..d228b59 --- /dev/null +++ b/http3/http_test.go @@ -0,0 +1,37 @@ +package http3 + +import ( + "io" + "testing" + + "github.com/fumiama/terasu/dns" +) + +func TestClientGet(t *testing.T) { + dns.IPv4Servers = *dns.NewEmptyList() + dns.IPv6Servers = *dns.NewEmptyList() + dns.IPv4Servers.Add(&dns.Config{ + Fallbacks: map[string][]string{ + "huggingface.co": {"52.222.136.117"}, + }, + }) + resp, err := Get("https://huggingface.co/") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + t.Log("[T] response code", resp.StatusCode) + for k, vs := range resp.Header { + for _, v := range vs { + t.Log("[T] response header", k+":", v) + } + } + data, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if len(data) == 0 { + t.Fail() + } + t.Log(string(data)) +}