diff --git a/client/certificates.go b/client/certificates.go index 96c493d..87bf15d 100644 --- a/client/certificates.go +++ b/client/certificates.go @@ -8,13 +8,11 @@ import ( "encoding/hex" "errors" "fmt" - "net" "net/http" "os" "path/filepath" "strings" "sync" - "time" ) type PinnedCertificate struct { @@ -135,31 +133,31 @@ func CertificateFingerprint(certificate *x509.Certificate) string { return fmt.Sprintf("%s-%s", algo, hex.EncodeToString(sum[:])) } -func PreFetchServerCertificate(store *CertificateStore, addr string) error { - conn, err := tls.DialWithDialer(&net.Dialer{ - Timeout: 5 * time.Second, - }, "tcp", addr, &tls.Config{ - InsecureSkipVerify: true, - }) +func PreFetchServerCertificate(store *CertificateStore, hostname string, useHttp3 bool) error { + addr, err := EnsurePort(hostname) + if err != nil { + return err + } + + var ( + name string + certificate *x509.Certificate + ) + + if useHttp3 { + certificate, name, err = ResolveTLSCertificateHttp3(addr) + } else { + certificate, name, err = ResolveTLSCertificateHttp2(addr) + } if err != nil { return err } - defer conn.Close() - - state := conn.ConnectionState() - if len(state.PeerCertificates) == 0 { - return fmt.Errorf("no peer certificates") - } - - certificate := state.PeerCertificates[0] - if certificate.Subject.CommonName != "up" { return errors.New("invalid certificate subject") } - name := state.ServerName fingerprint := CertificateFingerprint(certificate) if store.IsPinned(name, fingerprint) { @@ -180,12 +178,8 @@ func PreFetchServerCertificate(store *CertificateStore, addr string) error { return store.Pin(name, fingerprint) } -func NewPinnedClient(store *CertificateStore) *http.Client { - config := &tls.Config{ - InsecureSkipVerify: true, - } - - config.VerifyConnection = func(cs tls.ConnectionState) error { +func NewPinnedClient(store *CertificateStore, useHttp3 bool) *http.Client { + return NewHttpClient(func(cs tls.ConnectionState) error { if len(cs.PeerCertificates) == 0 { return errors.New("missing certificate") } @@ -204,16 +198,5 @@ func NewPinnedClient(store *CertificateStore) *http.Client { } return nil - } - - return &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: config, - Dial: (&net.Dialer{ - Timeout: 5 * time.Second, - }).Dial, - TLSHandshakeTimeout: 5 * time.Second, - IdleConnTimeout: 10 * time.Second, - }, - } + }, useHttp3) } diff --git a/client/client.go b/client/client.go index bacf9c4..5dbb94e 100644 --- a/client/client.go +++ b/client/client.go @@ -75,10 +75,9 @@ func Run(_ context.Context, cmd *cli.Command) error { } if found, _ := cfg.Get(hostname, "HostName"); found != "" { - hostname = found - - if index := strings.Index(hostname, ":"); index != -1 { - hostname = hostname[:index] + hostname, err = StripPort(found) + if err != nil { + return err } } @@ -119,11 +118,19 @@ func Run(_ context.Context, cmd *cli.Command) error { return fmt.Errorf("failed to load certificate store: %v", err) } - if err = PreFetchServerCertificate(store, hostname); err != nil { + useHttp3 := cmd.Bool("http3") + + if useHttp3 { + log.Println("Using http3 over udp") + } else { + log.Println("Using http2 over tcp") + } + + if err = PreFetchServerCertificate(store, hostname, useHttp3); err != nil { return err } - client := NewPinnedClient(store) + client := NewPinnedClient(store, useHttp3) log.Println("Requesting challenge...") diff --git a/client/http.go b/client/http.go new file mode 100644 index 0000000..adc71d3 --- /dev/null +++ b/client/http.go @@ -0,0 +1,118 @@ +package client + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "net" + "net/http" + "time" + + "github.com/quic-go/quic-go" + "github.com/quic-go/quic-go/http3" +) + +func GetHttp2Transport(verify func(tls.ConnectionState) error) *http.Transport { + return &http.Transport{ + TLSClientConfig: &tls.Config{ + MinVersion: tls.VersionTLS13, + NextProtos: []string{"h2"}, + InsecureSkipVerify: true, + VerifyConnection: verify, + }, + Dial: (&net.Dialer{ + Timeout: 5 * time.Second, + }).Dial, + TLSHandshakeTimeout: 5 * time.Second, + IdleConnTimeout: 10 * time.Second, + ForceAttemptHTTP2: true, + } +} + +func GetHttp3Transport(verify func(tls.ConnectionState) error) *http3.Transport { + return &http3.Transport{ + TLSClientConfig: &tls.Config{ + MinVersion: tls.VersionTLS13, + NextProtos: []string{http3.NextProtoH3}, + InsecureSkipVerify: true, + VerifyConnection: verify, + }, + QUICConfig: &quic.Config{ + HandshakeIdleTimeout: 5 * time.Second, + MaxIdleTimeout: 10 * time.Second, + }, + } +} + +func NewHttpClient(verify func(tls.ConnectionState) error, useHttp3 bool) *http.Client { + var transport http.RoundTripper + + if useHttp3 { + transport = GetHttp2Transport(verify) + } else { + transport = GetHttp3Transport(verify) + } + + return &http.Client{ + Transport: transport, + } +} + +func ResolveTLSCertificateHttp2(addr string) (*x509.Certificate, string, error) { + transport := GetHttp2Transport(nil) + + conn, err := tls.DialWithDialer(&net.Dialer{ + Timeout: 5 * time.Second, + }, "tcp", addr, transport.TLSClientConfig) + if err != nil { + return nil, "", err + } + + defer conn.Close() + + state := conn.ConnectionState() + if len(state.PeerCertificates) == 0 { + return nil, "", errors.New("no peer certificates") + } + + return state.PeerCertificates[0], state.ServerName, nil +} + +func ResolveTLSCertificateHttp3(addr string) (*x509.Certificate, string, error) { + transport := GetHttp3Transport(nil) + + conn, err := quic.DialAddr(context.Background(), addr, transport.TLSClientConfig, transport.QUICConfig) + if err != nil { + return nil, "", err + } + + defer conn.CloseWithError(quic.ApplicationErrorCode(0), "") + + state := conn.ConnectionState().TLS + if len(state.PeerCertificates) == 0 { + return nil, "", errors.New("no peer certificates") + } + + return state.PeerCertificates[0], state.ServerName, nil +} + +func StripPort(hostname string) (string, error) { + host, _, err := net.SplitHostPort(hostname) + + return host, err +} + +func EnsurePort(hostname string) (string, error) { + host, port, err := net.SplitHostPort(hostname) + if err != nil { + return "", err + } + + if port == "" { + port = "443" + } + + return fmt.Sprintf("%s:%s", host, port), err +} diff --git a/go.mod b/go.mod index 7425e86..aeac7e3 100644 --- a/go.mod +++ b/go.mod @@ -7,15 +7,26 @@ require ( github.com/go-chi/chi/v5 v5.2.2 github.com/kevinburke/ssh_config v1.2.0 github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/quic-go/quic-go v0.52.0 github.com/urfave/cli/v3 v3.3.8 github.com/vmihailenco/msgpack/v5 v5.4.1 golang.org/x/crypto v0.39.0 ) require ( + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/gookit/color v1.5.4 // indirect + github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/quic-go/qpack v0.5.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sync v0.15.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/tools v0.33.0 // indirect ) diff --git a/go.sum b/go.sum index 5cff3f4..1160b05 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,42 @@ +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/coalaura/logger v1.4.5 h1:xXazOab4qXaltUbD4TrQdSs2TtLB+k6t0t6y/M8LR3Q= github.com/coalaura/logger v1.4.5/go.mod h1:3HCYCWmsWmYW175e2/fZL9BWjJutr2W+7adeh1BPHkg= +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/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +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/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +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/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 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.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA= +github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ= +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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E= @@ -22,13 +47,30 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +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.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/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/main.go b/main.go index aa8e472..f4ff71b 100644 --- a/main.go +++ b/main.go @@ -50,6 +50,13 @@ func main() { Action: server.Run, }, }, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "http3", + Aliases: []string{"h3"}, + Usage: "use experimental http3", + }, + }, EnableShellCompletion: true, UseShortOptionHandling: true, Suggest: true, diff --git a/server/server.go b/server/server.go index 5fbd3a6..e0c260e 100644 --- a/server/server.go +++ b/server/server.go @@ -11,6 +11,7 @@ import ( "github.com/coalaura/up/internal" "github.com/go-chi/chi/v5" "github.com/patrickmn/go-cache" + "github.com/quic-go/quic-go/http3" "github.com/urfave/cli/v3" ) @@ -77,15 +78,33 @@ func Run(_ context.Context, cmd *cli.Command) error { r.Post("/receive", HandleReceiveRequest) - srv := &http.Server{ - Addr: fmt.Sprintf(":%d", port), - Handler: r, - TLSConfig: &tls.Config{ - MinVersion: tls.VersionTLS13, - }, + tlsCfg := &tls.Config{ + MinVersion: tls.VersionTLS13, } - log.Printf("Server listening on :%d\n", port) + if cmd.Bool("http3") { + tlsCfg.NextProtos = []string{http3.NextProtoH3} - return srv.ListenAndServeTLS("cert.pem", "key.pem") + srv := &http3.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: r, + TLSConfig: tlsCfg, + } + + log.Printf("Server listening at :%d (udp/http3)...\n", port) + + return srv.ListenAndServeTLS("cert.pem", "key.pem") + } else { + tlsCfg.NextProtos = []string{"h2"} + + srv := &http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: r, + TLSConfig: tlsCfg, + } + + log.Printf("Server listening at :%d (tcp/http2)\n", port) + + return srv.ListenAndServeTLS("cert.pem", "key.pem") + } }