From 76fb0cfdbb4fcabdea9ca1f447880eec3c8c47ea Mon Sep 17 00:00:00 2001 From: Yuzerion Date: Fri, 4 Apr 2025 00:18:15 +0800 Subject: [PATCH] Feat/http3 (#84) * chore(deps): update go-playground/validator to v10.26.0 * chore(deps): update Go version to 1.24.2 and dependencies, reorganize dependencies into categorized sections * chore(deps): update Go version to 1.24.2 in Dockerfile * refactor(agent): replace deprecated context import with standard context package * feat(http3): add HTTP/3 support and refactor server handling code into utility functions --------- Co-authored-by: yusing --- go.mod | 12 +++- go.sum | 24 +++++++ internal/common/env.go | 2 + internal/net/gphttp/server/server.go | 94 ++++++++++++++++------------ internal/net/gphttp/server/utils.go | 75 ++++++++++++++++++++++ 5 files changed, 165 insertions(+), 42 deletions(-) create mode 100644 internal/net/gphttp/server/utils.go diff --git a/go.mod b/go.mod index dd6ab467..c0e4115e 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( github.com/go-acme/lego/v4 v4.23.1 // acme client github.com/go-playground/validator/v10 v10.26.0 // validator github.com/gobwas/glob v0.2.3 // glob matcher for route rules - github.com/golang-jwt/jwt/v5 v5.2.2 // jwt for default auth github.com/gotify/server/v2 v2.6.1 // reference the Message struct for json response github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics github.com/prometheus/client_golang v1.22.0 // metrics @@ -32,7 +31,10 @@ replace github.com/coreos/go-oidc/v3 => github.com/godoxy-app/go-oidc/v3 v3.14.2 require ( github.com/bytedance/sonic v1.13.2 github.com/docker/cli v28.1.1+incompatible + github.com/golang-jwt/jwt/v5 v5.2.2 github.com/luthermonson/go-proxmox v0.2.2 + github.com/quic-go/quic-go v0.51.0 + github.com/samber/slog-zerolog/v2 v2.7.3 github.com/spf13/afero v1.14.0 github.com/stretchr/testify v1.10.0 go.uber.org/atomic v1.11.0 @@ -62,9 +64,11 @@ require ( github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/jinzhu/copier v0.4.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect @@ -77,6 +81,7 @@ require ( github.com/moby/docker-image-spec v1.3.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nrdcg/porkbun v0.4.0 // indirect + github.com/onsi/ginkgo/v2 v2.23.4 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/ovh/go-ovh v1.7.0 // indirect @@ -86,7 +91,10 @@ require ( github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect + github.com/quic-go/qpack v0.5.1 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/samber/slog-common v0.18.1 // indirect github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect @@ -95,6 +103,8 @@ require ( go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/sdk v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + go.uber.org/mock v0.5.1 // indirect golang.org/x/arch v0.16.0 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/sync v0.13.0 // indirect diff --git a/go.sum b/go.sum index 3eab3a7f..95448687 100644 --- a/go.sum +++ b/go.sum @@ -72,6 +72,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= @@ -93,6 +95,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4= +github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= @@ -158,6 +162,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw= github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54= +github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= +github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= +github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -175,6 +183,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= @@ -185,11 +195,21 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +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.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc= +github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/samber/slog-common v0.18.1 h1:c0EipD/nVY9HG5shgm/XAs67mgpWDMF+MmtptdJNCkQ= +github.com/samber/slog-common v0.18.1/go.mod h1:QNZiNGKakvrfbJ2YglQXLCZauzkI9xZBjOhWFKS3IKk= +github.com/samber/slog-zerolog/v2 v2.7.3 h1:/MkPDl/tJhijN2GvB1MWwBn2FU8RiL3rQ8gpXkQm2EY= +github.com/samber/slog-zerolog/v2 v2.7.3/go.mod h1:oWU7WHof4Xp8VguiNO02r1a4VzkgoOyOZhY5CuRke60= github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE= github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA= github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0= @@ -238,6 +258,10 @@ go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeX go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs= +go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U= golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/internal/common/env.go b/internal/common/env.go index 5fb6a420..09ddf552 100644 --- a/internal/common/env.go +++ b/internal/common/env.go @@ -19,6 +19,8 @@ var ( IsDebug = GetEnvBool("DEBUG", IsTest) IsTrace = GetEnvBool("TRACE", false) && IsDebug + HTTP3Enabled = GetEnvBool("HTTP3_ENABLED", true) + ProxyHTTPAddr, ProxyHTTPHost, ProxyHTTPPort, diff --git a/internal/net/gphttp/server/server.go b/internal/net/gphttp/server/server.go index 61f63c2d..92408af9 100644 --- a/internal/net/gphttp/server/server.go +++ b/internal/net/gphttp/server/server.go @@ -3,11 +3,11 @@ package server import ( "context" "crypto/tls" - "log" "net" "net/http" "time" + "github.com/quic-go/quic-go/http3" "github.com/rs/zerolog" "github.com/yusing/go-proxy/internal/autocert" "github.com/yusing/go-proxy/internal/common" @@ -33,6 +33,11 @@ type Options struct { Handler http.Handler } +type httpServer interface { + *http.Server | *http3.Server + Shutdown(ctx context.Context) error +} + func StartServer(parent task.Parent, opt Options) (s *Server) { s = NewServer(opt) s.Start(parent) @@ -82,67 +87,74 @@ func NewServer(opt Options) (s *Server) { func (s *Server) Start(parent task.Parent) { s.startTime = time.Now() subtask := parent.Subtask("server."+s.Name, false) + + if s.https != nil && common.HTTP3Enabled { + s.https.TLSConfig.NextProtos = []string{http3.NextProtoH3, "h2", "http/1.1"} + h3 := &http3.Server{ + Addr: s.https.Addr, + Handler: s.https.Handler, + TLSConfig: http3.ConfigureTLSConfig(s.https.TLSConfig), + } + Start(subtask, h3, &s.l) + s.http.Handler = advertiseHTTP3(s.http.Handler, h3) + s.https.Handler = advertiseHTTP3(s.https.Handler, h3) + } + Start(subtask, s.http, &s.l) Start(subtask, s.https, &s.l) } -func Start(parent task.Parent, srv *http.Server, logger *zerolog.Logger) { +func Start[Server httpServer](parent task.Parent, srv Server, logger *zerolog.Logger) { if srv == nil { return } - srv.BaseContext = func(l net.Listener) context.Context { - return parent.Context() - } - if common.IsDebug { - srv.ErrorLog = log.New(logger, "", 0) - } - - var proto string - if srv.TLSConfig == nil { - proto = "http" - } else { - proto = "https" - } + setDebugLogger(srv, logger) + proto := proto(srv) task := parent.Subtask(proto, false) var lc net.ListenConfig + var serveFunc func() error - // Serve already closes the listener on return - l, err := lc.Listen(task.Context(), "tcp", srv.Addr) - if err != nil { - HandleError(logger, err, "failed to listen on port") - return - } - - task.OnCancel("stop", func() { - Stop(srv, logger) - }) - - logger.Info().Str("addr", srv.Addr).Msg("server started") - - go func() { - if srv.TLSConfig == nil { - err = srv.Serve(l) - } else { - err = srv.Serve(tls.NewListener(l, srv.TLSConfig)) + switch srv := any(srv).(type) { + case *http.Server: + srv.BaseContext = func(l net.Listener) context.Context { + return parent.Context() } + l, err := lc.Listen(task.Context(), "tcp", srv.Addr) + if err != nil { + HandleError(logger, err, "failed to listen on port") + return + } + if srv.TLSConfig != nil { + l = tls.NewListener(l, srv.TLSConfig) + } + serveFunc = getServeFunc(l, srv.Serve) + case *http3.Server: + l, err := lc.ListenPacket(task.Context(), "udp", srv.Addr) + if err != nil { + HandleError(logger, err, "failed to listen on port") + return + } + serveFunc = getServeFunc(l, srv.Serve) + } + task.OnCancel("stop", func() { + stop(srv, logger) + }) + logStarted(srv, logger) + go func() { + err := serveFunc() HandleError(logger, err, "failed to serve "+proto+" server") }() } -func Stop(srv *http.Server, logger *zerolog.Logger) { +func stop[Server httpServer](srv Server, logger *zerolog.Logger) { if srv == nil { return } - var proto string - if srv.TLSConfig == nil { - proto = "http" - } else { - proto = "https" - } + proto := proto(srv) ctx, cancel := context.WithTimeout(task.RootContext(), 3*time.Second) defer cancel() @@ -150,7 +162,7 @@ func Stop(srv *http.Server, logger *zerolog.Logger) { if err := srv.Shutdown(ctx); err != nil { HandleError(logger, err, "failed to shutdown "+proto+" server") } else { - logger.Info().Str("addr", srv.Addr).Msgf("server stopped") + logger.Info().Str("proto", proto).Str("addr", addr(srv)).Msg("server stopped") } } diff --git a/internal/net/gphttp/server/utils.go b/internal/net/gphttp/server/utils.go new file mode 100644 index 00000000..968f5842 --- /dev/null +++ b/internal/net/gphttp/server/utils.go @@ -0,0 +1,75 @@ +package server + +import ( + "log" + "log/slog" + "net/http" + + "github.com/quic-go/quic-go/http3" + "github.com/rs/zerolog" + slogzerolog "github.com/samber/slog-zerolog/v2" + "github.com/yusing/go-proxy/internal/common" + "github.com/yusing/go-proxy/internal/net/gphttp" +) + +func advertiseHTTP3(handler http.Handler, h3 *http3.Server) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.ProtoMajor < 3 { + err := h3.SetQUICHeaders(w.Header()) + if err != nil { + gphttp.ServerError(w, r, err) + return + } + } + handler.ServeHTTP(w, r) + }) +} + +func proto[Server httpServer](srv Server) string { + var proto string + switch src := any(srv).(type) { + case *http.Server: + if src.TLSConfig == nil { + proto = "http" + } else { + proto = "https" + } + case *http3.Server: + proto = "h3" + } + return proto +} + +func addr[Server httpServer](srv Server) string { + var addr string + switch src := any(srv).(type) { + case *http.Server: + addr = src.Addr + case *http3.Server: + addr = src.Addr + } + return addr +} + +func getServeFunc[listener any](l listener, serve func(listener) error) func() error { + return func() error { + return serve(l) + } +} + +func setDebugLogger[Server httpServer](srv Server, logger *zerolog.Logger) { + if !common.IsDebug { + return + } + switch srv := any(srv).(type) { + case *http.Server: + srv.ErrorLog = log.New(logger, "", 0) + case *http3.Server: + logOpts := slogzerolog.Option{Level: slog.LevelDebug, Logger: logger} + srv.Logger = slog.New(logOpts.NewZerologHandler()) + } +} + +func logStarted[Server httpServer](srv Server, logger *zerolog.Logger) { + logger.Info().Str("proto", proto(srv)).Str("addr", addr(srv)).Msg("server started") +}