diff --git a/app/dns/nameserver_quic.go b/app/dns/nameserver_quic.go index 435cd0a3..0896b62d 100644 --- a/app/dns/nameserver_quic.go +++ b/app/dns/nameserver_quic.go @@ -9,7 +9,7 @@ import ( "sync/atomic" "time" - "github.com/quic-go/quic-go" + "github.com/refraction-networking/uquic" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" @@ -400,7 +400,9 @@ func (s *QUICNameServer) openConnection() (quic.Connection, error) { HandshakeIdleTimeout: handshakeTimeout, } tlsConfig.ServerName = s.destination.Address.String() - conn, err := quic.DialAddr(context.Background(), s.destination.NetAddr(), tlsConfig.GetTLSConfig(tls.WithNextProto("http/1.1", http2.NextProtoTLS, NextProtoDQ)), quicConfig) + utlsConf := tls.CopyConfig(tlsConfig.GetTLSConfig()) + utlsConf.NextProtos = []string{ "http/1.1", http2.NextProtoTLS, NextProtoDQ } + conn, err := quic.DialAddr(context.Background(), s.destination.NetAddr(), utlsConf, quicConfig) log.Record(&log.AccessMessage{ From: "DNS", To: s.destination, diff --git a/common/protocol/quic/sniff.go b/common/protocol/quic/sniff.go index 8719a085..b5a7517d 100644 --- a/common/protocol/quic/sniff.go +++ b/common/protocol/quic/sniff.go @@ -7,7 +7,7 @@ import ( "encoding/binary" "io" - "github.com/quic-go/quic-go/quicvarint" + "github.com/refraction-networking/uquic/quicvarint" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/bytespool" diff --git a/go.mod b/go.mod index 7f3cba9f..27ffdd78 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/miekg/dns v1.1.62 github.com/pelletier/go-toml v1.9.5 github.com/pires/go-proxyproto v0.8.0 - github.com/quic-go/quic-go v0.46.0 + github.com/refraction-networking/uquic v0.0.6 github.com/refraction-networking/utls v1.6.7 github.com/sagernet/sing v0.4.3 github.com/sagernet/sing-shadowsocks v0.2.7 @@ -38,11 +38,15 @@ require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect + github.com/gaukas/clienthellod v0.4.2 // indirect + github.com/gaukas/godicttls v0.0.4 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/btree v1.1.2 // indirect + github.com/google/gopacket v1.1.19 // indirect github.com/google/pprof v0.0.0-20240528025155-186aa0362fba // indirect github.com/klauspost/compress v1.17.8 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/onsi/ginkgo/v2 v2.19.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect diff --git a/go.sum b/go.sum index 8fa32abd..4952bcd2 100644 --- a/go.sum +++ b/go.sum @@ -4,12 +4,17 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY= github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0= github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= +github.com/gaukas/clienthellod v0.4.2 h1:LPJ+LSeqt99pqeCV4C0cllk+pyWmERisP7w6qWr7eqE= +github.com/gaukas/clienthellod v0.4.2/go.mod h1:M57+dsu0ZScvmdnNxaxsDPM46WhSEdPYAOdNgfL7IKA= +github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= +github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= @@ -22,6 +27,8 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 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/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/pprof v0.0.0-20240528025155-186aa0362fba h1:ql1qNgCyOB7iAEk8JTNM+zJrgIbnyCKX/wdlyPufP5g= github.com/google/pprof v0.0.0-20240528025155-186aa0362fba/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= @@ -32,6 +39,10 @@ github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0N github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= @@ -44,16 +55,19 @@ github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoU github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pires/go-proxyproto v0.8.0 h1:5unRmEAPbHXHuLjDg01CxJWf91cw3lKHc/0xzKpXEe0= github.com/pires/go-proxyproto v0.8.0/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/quic-go v0.46.0 h1:uuwLClEEyk1DNvchH8uCByQVjo3yKL9opKulExNDs7Y= -github.com/quic-go/quic-go v0.46.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI= +github.com/refraction-networking/uquic v0.0.6 h1:9ol1oOaOpHDeeDlBY7u228jK+T5oic35QrFimHVaCMM= +github.com/refraction-networking/uquic v0.0.6/go.mod h1:TFgTmV/yqVCMEXVwP7z7PMAhzye02rFHLV6cRAg59jc= github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM= github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= 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.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/sagernet/sing v0.4.3 h1:Ty/NAiNnVd6844k7ujlL5lkzydhcTH5Psc432jXA4Y8= github.com/sagernet/sing v0.4.3/go.mod h1:ieZHA/+Y9YZfXs2I3WtuwgyCZ6GPsIR7HdKb1SdEnls= github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= @@ -83,6 +97,8 @@ golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg= golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= @@ -115,6 +131,7 @@ 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.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= @@ -131,8 +148,9 @@ google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/transport/internet/http/dialer.go b/transport/internet/http/dialer.go index d0a86e8b..6cb27894 100644 --- a/transport/internet/http/dialer.go +++ b/transport/internet/http/dialer.go @@ -9,8 +9,9 @@ import ( "sync" "time" - "github.com/quic-go/quic-go" - "github.com/quic-go/quic-go/http3" + "github.com/refraction-networking/uquic" + "github.com/refraction-networking/uquic/http3" + utls "github.com/refraction-networking/utls" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" c "github.com/xtls/xray-core/common/ctx" @@ -79,9 +80,9 @@ func getHTTPClient(ctx context.Context, dest net.Destination, streamSettings *in KeepAlivePeriod: h3KeepalivePeriod, } roundTripper := &http3.RoundTripper{ - QUICConfig: quicConfig, - TLSClientConfig: tlsConfigs.GetTLSConfig(tls.WithDestination(dest)), - Dial: func(ctx context.Context, addr string, tlsCfg *gotls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + QuicConfig: quicConfig, + TLSClientConfig: tls.CopyConfig(tlsConfigs.GetTLSConfig(tls.WithDestination(dest))), + Dial: func(ctx context.Context, addr string, tlsCfg *utls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings) if err != nil { return nil, err diff --git a/transport/internet/http/hub.go b/transport/internet/http/hub.go index 96fe8f62..7368985b 100644 --- a/transport/internet/http/hub.go +++ b/transport/internet/http/hub.go @@ -8,8 +8,8 @@ import ( "strings" "time" - "github.com/quic-go/quic-go" - "github.com/quic-go/quic-go/http3" + "github.com/refraction-networking/uquic" + "github.com/refraction-networking/uquic/http3" goreality "github.com/xtls/reality" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" @@ -170,7 +170,7 @@ func Listen(ctx context.Context, address net.Address, port net.Port, streamSetti if err != nil { return nil, errors.New("failed to listen UDP(for SH3) on ", address, ":", port).Base(err) } - h3listener, err := quic.ListenEarly(Conn, tlsConfig, nil) + h3listener, err := quic.ListenEarly(Conn, config.GetUTLSConfig(), nil) if err != nil { return nil, errors.New("failed to listen QUIC(for SH3) on ", address, ":", port).Base(err) } diff --git a/transport/internet/splithttp/dialer.go b/transport/internet/splithttp/dialer.go index c43783ec..f5bf88c3 100644 --- a/transport/internet/splithttp/dialer.go +++ b/transport/internet/splithttp/dialer.go @@ -9,8 +9,9 @@ import ( "sync" "time" - "github.com/quic-go/quic-go" - "github.com/quic-go/quic-go/http3" + "github.com/refraction-networking/uquic" + "github.com/refraction-networking/uquic/http3" + utls "github.com/refraction-networking/utls" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" @@ -145,9 +146,9 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea KeepAlivePeriod: h3KeepalivePeriod, } transport = &http3.RoundTripper{ - QUICConfig: quicConfig, - TLSClientConfig: gotlsConfig, - Dial: func(ctx context.Context, addr string, tlsCfg *gotls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + QuicConfig: quicConfig, + TLSClientConfig: tls.CopyConfig(gotlsConfig), + Dial: func(ctx context.Context, addr string, tlsCfg *utls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings) if err != nil { return nil, err diff --git a/transport/internet/splithttp/hub.go b/transport/internet/splithttp/hub.go index b7244f10..39c191d2 100644 --- a/transport/internet/splithttp/hub.go +++ b/transport/internet/splithttp/hub.go @@ -11,8 +11,8 @@ import ( "sync" "time" - "github.com/quic-go/quic-go" - "github.com/quic-go/quic-go/http3" + "github.com/refraction-networking/uquic" + "github.com/refraction-networking/uquic/http3" goreality "github.com/xtls/reality" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" @@ -308,7 +308,8 @@ func ListenSH(ctx context.Context, address net.Address, port net.Port, streamSet if err != nil { return nil, errors.New("failed to listen UDP(for SH3) on ", address, ":", port).Base(err) } - h3listener, err := quic.ListenEarly(Conn, tlsConfig, nil) + config := v2tls.ConfigFromStreamSettings(streamSettings) + h3listener, err := quic.ListenEarly(Conn, config.GetUTLSConfig(), nil) if err != nil { return nil, errors.New("failed to listen QUIC(for SH3) on ", address, ":", port).Base(err) } diff --git a/transport/internet/tls/config.go b/transport/internet/tls/config.go index b6c55c00..ba517a13 100644 --- a/transport/internet/tls/config.go +++ b/transport/internet/tls/config.go @@ -1,6 +1,7 @@ package tls import ( + "bytes" "context" "crypto/hmac" "crypto/tls" @@ -10,8 +11,8 @@ import ( "strings" "sync" "time" - "bytes" + utls "github.com/refraction-networking/utls" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/ocsp" @@ -21,6 +22,7 @@ import ( ) var globalSessionCache = tls.NewLRUClientSessionCache(128) +var globalSessionCacheU = utls.NewLRUClientSessionCache(128) // ParseCertificate converts a cert.Certificate to Certificate. func ParseCertificate(c *cert.Certificate) *Certificate { @@ -92,6 +94,54 @@ func (c *Config) BuildCertificates() []*tls.Certificate { return certs } +// BuildCertificates builds a list of UTLS certificates from proto definition. +func (c *Config) BuildCertificatesU() []*utls.Certificate { + certs := make([]*utls.Certificate, 0, len(c.Certificate)) + for _, entry := range c.Certificate { + if entry.Usage != Certificate_ENCIPHERMENT { + continue + } + getX509KeyPair := func() *utls.Certificate { + keyPair, err := utls.X509KeyPair(entry.Certificate, entry.Key) + if err != nil { + errors.LogWarningInner(context.Background(), err, "ignoring invalid X509 key pair") + return nil + } + keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) + if err != nil { + errors.LogWarningInner(context.Background(), err, "ignoring invalid certificate") + return nil + } + return &keyPair + } + if keyPair := getX509KeyPair(); keyPair != nil { + certs = append(certs, keyPair) + } else { + continue + } + index := len(certs) - 1 + setupOcspTicker(entry, func(isReloaded, isOcspstapling bool){ + cert := certs[index] + if isReloaded { + if newKeyPair := getX509KeyPair(); newKeyPair != nil { + cert = newKeyPair + } else { + return + } + } + if isOcspstapling { + if newOCSPData, err := ocsp.GetOCSPForCert(cert.Certificate); err != nil { + errors.LogWarningInner(context.Background(), err, "ignoring invalid OCSP") + } else if string(newOCSPData) != string(cert.OCSPStaple) { + cert.OCSPStaple = newOCSPData + } + } + certs[index] = cert + }) + } + return certs +} + func setupOcspTicker(entry *Certificate, callback func(isReloaded, isOcspstapling bool)) { go func() { if entry.OneTimeLoading { @@ -140,6 +190,17 @@ func isCertificateExpired(c *tls.Certificate) bool { return c.Leaf != nil && c.Leaf.NotAfter.Before(time.Now().Add(time.Minute*2)) } +func isCertificateExpiredU(c *utls.Certificate) bool { + if c.Leaf == nil && len(c.Certificate) > 0 { + if pc, err := x509.ParseCertificate(c.Certificate[0]); err == nil { + c.Leaf = pc + } + } + + // If leaf is not there, the certificate is probably not used yet. We trust user to provide a valid certificate. + return c.Leaf != nil && c.Leaf.NotAfter.Before(time.Now().Add(time.Minute*2)) +} + func issueCertificate(rawCA *Certificate, domain string) (*tls.Certificate, error) { parent, err := cert.ParseCertificate(rawCA.Certificate, rawCA.Key) if err != nil { @@ -157,6 +218,23 @@ func issueCertificate(rawCA *Certificate, domain string) (*tls.Certificate, erro return &cert, err } +func issueCertificateU(rawCA *Certificate, domain string) (*utls.Certificate, error) { + parent, err := cert.ParseCertificate(rawCA.Certificate, rawCA.Key) + if err != nil { + return nil, errors.New("failed to parse raw certificate").Base(err) + } + newCert, err := cert.Generate(parent, cert.CommonName(domain), cert.DNSNames(domain)) + if err != nil { + return nil, errors.New("failed to generate new certificate for ", domain).Base(err) + } + newCertPEM, newKeyPEM := newCert.ToPEM() + if rawCA.BuildChain { + newCertPEM = bytes.Join([][]byte{newCertPEM, rawCA.Certificate}, []byte("\n")) + } + cert, err := utls.X509KeyPair(newCertPEM, newKeyPEM) + return &cert, err +} + func (c *Config) getCustomCA() []*Certificate { certs := make([]*Certificate, 0, len(c.Certificate)) for _, certificate := range c.Certificate { @@ -272,6 +350,110 @@ func getNewGetCertificateFunc(certs []*tls.Certificate, rejectUnknownSNI bool) f } } +func getGetCertificateUFunc(c *utls.Config, ca []*Certificate) func(hello *utls.ClientHelloInfo) (*utls.Certificate, error) { + var access sync.RWMutex + + return func(hello *utls.ClientHelloInfo) (*utls.Certificate, error) { + domain := hello.ServerName + certExpired := false + + access.RLock() + certificate, found := c.NameToCertificate[domain] + access.RUnlock() + + if found { + if !isCertificateExpiredU(certificate) { + return certificate, nil + } + certExpired = true + } + + if certExpired { + newCerts := make([]utls.Certificate, 0, len(c.Certificates)) + + access.Lock() + for _, certificate := range c.Certificates { + if !isCertificateExpiredU(&certificate) { + newCerts = append(newCerts, certificate) + } else if certificate.Leaf != nil { + expTime := certificate.Leaf.NotAfter.Format(time.RFC3339) + errors.LogInfo(context.Background(), "old certificate for ", domain, " (expire on ", expTime, ") discarded") + } + } + + c.Certificates = newCerts + access.Unlock() + } + + var issuedCertificate *utls.Certificate + + // Create a new certificate from existing CA if possible + for _, rawCert := range ca { + if rawCert.Usage == Certificate_AUTHORITY_ISSUE { + newCert, err := issueCertificateU(rawCert, domain) + if err != nil { + errors.LogInfoInner(context.Background(), err, "failed to issue new certificate for ", domain) + continue + } + parsed, err := x509.ParseCertificate(newCert.Certificate[0]) + if err == nil { + newCert.Leaf = parsed + expTime := parsed.NotAfter.Format(time.RFC3339) + errors.LogInfo(context.Background(), "new certificate for ", domain, " (expire on ", expTime, ") issued") + } else { + errors.LogInfoInner(context.Background(), err, "failed to parse new certificate for ", domain) + } + + access.Lock() + c.Certificates = append(c.Certificates, *newCert) + issuedCertificate = &c.Certificates[len(c.Certificates)-1] + access.Unlock() + break + } + } + + if issuedCertificate == nil { + return nil, errors.New("failed to create a new certificate for ", domain) + } + + access.Lock() + c.BuildNameToCertificate() + access.Unlock() + + return issuedCertificate, nil + } +} + +func getNewGetCertificateUFunc(certs []*utls.Certificate, rejectUnknownSNI bool) func(hello *utls.ClientHelloInfo) (*utls.Certificate, error) { + return func(hello *utls.ClientHelloInfo) (*utls.Certificate, error) { + if len(certs) == 0 { + return nil, errNoCertificates + } + sni := strings.ToLower(hello.ServerName) + if !rejectUnknownSNI && (len(certs) == 1 || sni == "") { + return certs[0], nil + } + gsni := "*" + if index := strings.IndexByte(sni, '.'); index != -1 { + gsni += sni[index:] + } + for _, keyPair := range certs { + if keyPair.Leaf.Subject.CommonName == sni || keyPair.Leaf.Subject.CommonName == gsni { + return keyPair, nil + } + for _, name := range keyPair.Leaf.DNSNames { + if name == sni || name == gsni { + return keyPair, nil + } + } + } + if rejectUnknownSNI { + return nil, errNoCertificates + } + return certs[0], nil + } +} + func (c *Config) parseServerName() string { return c.ServerName } @@ -394,6 +576,97 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { return config } +// GetUTLSConfig converts this Config into utls.Config. +func (c *Config) GetUTLSConfig() *utls.Config { + root, err := c.getCertPool() + if err != nil { + errors.LogErrorInner(context.Background(), err, "failed to load system root certificate") + } + + if c == nil { + return &utls.Config{ + ClientSessionCache: globalSessionCacheU, + RootCAs: root, + InsecureSkipVerify: false, + NextProtos: nil, + SessionTicketsDisabled: true, + } + } + + config := &utls.Config{ + ClientSessionCache: globalSessionCacheU, + RootCAs: root, + InsecureSkipVerify: c.AllowInsecure, + NextProtos: c.NextProtocol, + SessionTicketsDisabled: !c.EnableSessionResumption, + VerifyPeerCertificate: c.verifyPeerCert, + } + + // for _, opt := range opts { + // opt(config) + // } + + caCerts := c.getCustomCA() + if len(caCerts) > 0 { + config.GetCertificate = getGetCertificateUFunc(config, caCerts) + } else { + config.GetCertificate = getNewGetCertificateUFunc(c.BuildCertificatesU(), c.RejectUnknownSni) + } + + if sn := c.parseServerName(); len(sn) > 0 { + config.ServerName = sn + } + + if len(config.NextProtos) == 0 { + config.NextProtos = []string{"h2", "http/1.1"} + } + + switch c.MinVersion { + case "1.0": + config.MinVersion = tls.VersionTLS10 + case "1.1": + config.MinVersion = tls.VersionTLS11 + case "1.2": + config.MinVersion = tls.VersionTLS12 + case "1.3": + config.MinVersion = tls.VersionTLS13 + } + + switch c.MaxVersion { + case "1.0": + config.MaxVersion = tls.VersionTLS10 + case "1.1": + config.MaxVersion = tls.VersionTLS11 + case "1.2": + config.MaxVersion = tls.VersionTLS12 + case "1.3": + config.MaxVersion = tls.VersionTLS13 + } + + if len(c.CipherSuites) > 0 { + id := make(map[string]uint16) + for _, s := range tls.CipherSuites() { + id[s.Name] = s.ID + } + for _, n := range strings.Split(c.CipherSuites, ":") { + if id[n] != 0 { + config.CipherSuites = append(config.CipherSuites, id[n]) + } + } + } + + if len(c.MasterKeyLog) > 0 && c.MasterKeyLog != "none" { + writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644) + if err != nil { + errors.LogErrorInner(context.Background(), err, "failed to open ", c.MasterKeyLog, " as master key log") + } else { + config.KeyLogWriter = writer + } + } + + return config +} + // Option for building TLS config. type Option func(*tls.Config) diff --git a/transport/internet/tls/tls.go b/transport/internet/tls/tls.go index 38b603c0..24eb9c69 100644 --- a/transport/internet/tls/tls.go +++ b/transport/internet/tls/tls.go @@ -129,11 +129,11 @@ func (c *UConn) NegotiatedProtocol() string { } func UClient(c net.Conn, config *tls.Config, fingerprint *utls.ClientHelloID) net.Conn { - utlsConn := utls.UClient(c, copyConfig(config), *fingerprint) + utlsConn := utls.UClient(c, CopyConfig(config), *fingerprint) return &UConn{UConn: utlsConn} } -func copyConfig(c *tls.Config) *utls.Config { +func CopyConfig(c *tls.Config) *utls.Config { return &utls.Config{ RootCAs: c.RootCAs, ServerName: c.ServerName,