Skip to content

Commit c14492e

Browse files
[9.3] (backport #20908) Fix HTTP/2 SETTINGS mismatch connection failure in gmux (#20913)
* Fix HTTP/2 SETTINGS mismatch connection failure in gmux (#20908) * Bump gmux to fix http2 settings frame error * make notice * Add http2_test.go * make check-full (cherry picked from commit bbaaf9c) # Conflicts: # go.sum * Fix conflict --------- Co-authored-by: Carson Ip <carsonip@users.noreply.github.com> Co-authored-by: Carson Ip <carson.ip@elastic.co>
1 parent 2d52f35 commit c14492e

5 files changed

Lines changed: 223 additions & 14 deletions

File tree

NOTICE-fips.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,11 +1344,11 @@ Contents of probable licence file $GOMODCACHE/github.com/elastic/elastic-transpo
13441344

13451345
--------------------------------------------------------------------------------
13461346
Dependency : github.com/elastic/gmux
1347-
Version: v0.3.2
1347+
Version: v0.3.3
13481348
Licence type (autodetected): Apache-2.0
13491349
--------------------------------------------------------------------------------
13501350

1351-
Contents of probable licence file $GOMODCACHE/github.com/elastic/gmux@v0.3.2/LICENSE:
1351+
Contents of probable licence file $GOMODCACHE/github.com/elastic/gmux@v0.3.3/LICENSE:
13521352

13531353
Apache License
13541354
Version 2.0, January 2004
@@ -5273,11 +5273,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
52735273

52745274
--------------------------------------------------------------------------------
52755275
Dependency : google.golang.org/grpc
5276-
Version: v1.79.3
5276+
Version: v1.80.0
52775277
Licence type (autodetected): Apache-2.0
52785278
--------------------------------------------------------------------------------
52795279

5280-
Contents of probable licence file $GOMODCACHE/google.golang.org/grpc@v1.79.3/LICENSE:
5280+
Contents of probable licence file $GOMODCACHE/google.golang.org/grpc@v1.80.0/LICENSE:
52815281

52825282

52835283
Apache License

NOTICE.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,11 +1344,11 @@ Contents of probable licence file $GOMODCACHE/github.com/elastic/elastic-transpo
13441344

13451345
--------------------------------------------------------------------------------
13461346
Dependency : github.com/elastic/gmux
1347-
Version: v0.3.2
1347+
Version: v0.3.3
13481348
Licence type (autodetected): Apache-2.0
13491349
--------------------------------------------------------------------------------
13501350

1351-
Contents of probable licence file $GOMODCACHE/github.com/elastic/gmux@v0.3.2/LICENSE:
1351+
Contents of probable licence file $GOMODCACHE/github.com/elastic/gmux@v0.3.3/LICENSE:
13521352

13531353
Apache License
13541354
Version 2.0, January 2004
@@ -5273,11 +5273,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
52735273

52745274
--------------------------------------------------------------------------------
52755275
Dependency : google.golang.org/grpc
5276-
Version: v1.79.3
5276+
Version: v1.80.0
52775277
Licence type (autodetected): Apache-2.0
52785278
--------------------------------------------------------------------------------
52795279

5280-
Contents of probable licence file $GOMODCACHE/google.golang.org/grpc@v1.79.3/LICENSE:
5280+
Contents of probable licence file $GOMODCACHE/google.golang.org/grpc@v1.80.0/LICENSE:
52815281

52825282

52835283
Apache License

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ require (
1414
github.com/elastic/elastic-agent-libs v0.33.3
1515
github.com/elastic/elastic-agent-system-metrics v0.14.3
1616
github.com/elastic/elastic-transport-go/v8 v8.8.0
17-
github.com/elastic/gmux v0.3.2
17+
github.com/elastic/gmux v0.3.3
1818
github.com/elastic/go-docappender/v2 v2.12.1
1919
github.com/elastic/go-freelru v0.16.0
2020
github.com/elastic/go-sysinfo v1.15.4
@@ -46,7 +46,7 @@ require (
4646
golang.org/x/sync v0.20.0
4747
golang.org/x/term v0.40.0
4848
golang.org/x/time v0.14.0
49-
google.golang.org/grpc v1.79.3
49+
google.golang.org/grpc v1.80.0
5050
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
5151
)
5252

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,8 @@ github.com/elastic/elastic-agent-system-metrics v0.14.3 h1:v867kcgCVguOX3AYIHEVn
185185
github.com/elastic/elastic-agent-system-metrics v0.14.3/go.mod h1:JNfnZrC0viAjlJRUzQKKuMpDlXgjXBn4WdWEXQF7jcA=
186186
github.com/elastic/elastic-transport-go/v8 v8.8.0 h1:7k1Ua+qluFr6p1jfJjGDl97ssJS/P7cHNInzfxgBQAo=
187187
github.com/elastic/elastic-transport-go/v8 v8.8.0/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk=
188-
github.com/elastic/gmux v0.3.2 h1:cb721R+fe/rt/jVNyBP5HDQsEwLD2wSKfPD2Sk6adDk=
189-
github.com/elastic/gmux v0.3.2/go.mod h1:OD6oYrno+SV3pyl1ArdWCjlExZ+FJOfoSaFqnFeldBQ=
188+
github.com/elastic/gmux v0.3.3 h1:HEM7HOO10CKJUau/9WcnbQAPlo9EW85f0tWfMEidVaU=
189+
github.com/elastic/gmux v0.3.3/go.mod h1:kRSvlDg0rSzWmBamH/kNRFl+NoMKDU4VufUT98PnGHU=
190190
github.com/elastic/go-docappender/v2 v2.12.1 h1:ROOQyT4bjUTt2y36vJG4269UaDV63jY3pF301+DKCx4=
191191
github.com/elastic/go-docappender/v2 v2.12.1/go.mod h1:3eEqeo9gaXyDYWTXZ0J5n6A07UpfbvogpsUHRu1E+rI=
192192
github.com/elastic/go-elasticsearch/v8 v8.19.1 h1:0iEGt5/Ds9MNVxEp3hqLsXdbe6SjleaVHONg/FuR09Q=
@@ -775,8 +775,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:
775775
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=
776776
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
777777
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
778-
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
779-
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
778+
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
779+
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
780780
google.golang.org/grpc/examples v0.0.0-20231016154744-cb430bed4d27 h1:EB/3dtnYKOItaNPpOI/HmOCGbVZUiXcstRfiuxN+cFg=
781781
google.golang.org/grpc/examples v0.0.0-20231016154744-cb430bed4d27/go.mod h1:Crtq1t+mykyL5d6PR3z8zCxKx/Qjq/mlPWDPoWJANYA=
782782
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=

systemtest/http2_test.go

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package systemtest
19+
20+
import (
21+
"bytes"
22+
"context"
23+
"crypto/tls"
24+
"io"
25+
"net"
26+
"net/http"
27+
"strconv"
28+
"sync"
29+
"testing"
30+
"time"
31+
32+
"github.com/stretchr/testify/require"
33+
"golang.org/x/net/http2"
34+
"golang.org/x/net/http2/hpack"
35+
36+
"github.com/elastic/apm-server/systemtest/apmservertest"
37+
)
38+
39+
// TestHTTP2OverTLS performs a sanity check on http2 request response flow.
40+
func TestHTTP2OverTLS(t *testing.T) {
41+
srv := apmservertest.NewUnstartedServerTB(t)
42+
require.NoError(t, srv.StartTLS())
43+
44+
tlsConf := srv.TLS.Clone()
45+
tlsConf.NextProtos = []string{"h2"}
46+
client := &http.Client{
47+
Transport: &http.Transport{
48+
ForceAttemptHTTP2: true,
49+
TLSClientConfig: tlsConf,
50+
},
51+
Timeout: 5 * time.Second,
52+
}
53+
54+
resp, err := client.Get(srv.URL + "/")
55+
require.NoError(t, err)
56+
defer resp.Body.Close()
57+
58+
_, err = io.Copy(io.Discard, resp.Body)
59+
require.NoError(t, err)
60+
require.Equal(t, 2, resp.ProtoMajor)
61+
require.NotZero(t, resp.StatusCode)
62+
}
63+
64+
// TestHTTP2OverTLSFramingConsistency performs a raw HTTP/2 exchange and asserts
65+
// that SETTINGS remain consistent across the connection while a request/response
66+
// completes without GOAWAY.
67+
// This is to prevent reoccurrence of regression in https://github.com/elastic/apm-server/issues/20887
68+
func TestHTTP2OverTLSFramingConsistency(t *testing.T) {
69+
srv := apmservertest.NewUnstartedServerTB(t)
70+
require.NoError(t, srv.StartTLS())
71+
72+
tlsConf := srv.TLS.Clone()
73+
tlsConf.NextProtos = []string{"h2"}
74+
var (
75+
mu sync.Mutex
76+
recorded *recordingConn
77+
)
78+
h2Transport := &http2.Transport{
79+
TLSClientConfig: tlsConf,
80+
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
81+
conn, err := tls.DialWithDialer(&net.Dialer{}, network, addr, cfg)
82+
if err != nil {
83+
return nil, err
84+
}
85+
rc := &recordingConn{Conn: conn}
86+
mu.Lock()
87+
recorded = rc
88+
mu.Unlock()
89+
return rc, nil
90+
},
91+
}
92+
client := &http.Client{
93+
Transport: h2Transport,
94+
Timeout: 5 * time.Second,
95+
}
96+
resp, err := client.Get(srv.URL + "/")
97+
require.NoError(t, err)
98+
defer resp.Body.Close()
99+
_, err = io.Copy(io.Discard, resp.Body)
100+
require.NoError(t, err)
101+
require.NotZero(t, resp.StatusCode)
102+
103+
mu.Lock()
104+
require.NotNil(t, recorded)
105+
serverFrames := recorded.Bytes()
106+
mu.Unlock()
107+
108+
nonACKSettings, statusCode, sawGoAway := collectSettingsAndStatusFromBytes(t, serverFrames)
109+
110+
require.False(t, sawGoAway, "server should not send GOAWAY for a valid request")
111+
require.NotZero(t, statusCode, "response must include a non-zero :status")
112+
require.GreaterOrEqual(t, len(nonACKSettings), 2, "expected sniffing and backend SETTINGS frames")
113+
114+
first := nonACKSettings[0]
115+
for i := 1; i < len(nonACKSettings); i++ {
116+
require.Equalf(t, first, nonACKSettings[i], "non-ACK SETTINGS #%d differs from first SETTINGS frame", i+1)
117+
}
118+
}
119+
120+
type recordingConn struct {
121+
net.Conn
122+
mu sync.Mutex
123+
buf bytes.Buffer
124+
}
125+
126+
func (c *recordingConn) Read(p []byte) (int, error) {
127+
n, err := c.Conn.Read(p)
128+
if n > 0 {
129+
c.mu.Lock()
130+
_, _ = c.buf.Write(p[:n])
131+
c.mu.Unlock()
132+
}
133+
return n, err
134+
}
135+
136+
func (c *recordingConn) Bytes() []byte {
137+
c.mu.Lock()
138+
defer c.mu.Unlock()
139+
return append([]byte(nil), c.buf.Bytes()...)
140+
}
141+
142+
func collectSettingsAndStatusFromBytes(t *testing.T, data []byte) ([][]http2.Setting, int, bool) {
143+
t.Helper()
144+
145+
var (
146+
nonACKSettings [][]http2.Setting
147+
sawGoAway bool
148+
statusCode int
149+
done bool
150+
headerBlock bytes.Buffer
151+
)
152+
153+
decoder := hpack.NewDecoder(4096, func(hf hpack.HeaderField) {
154+
if hf.Name == ":status" {
155+
code, err := strconv.Atoi(hf.Value)
156+
require.NoError(t, err)
157+
statusCode = code
158+
}
159+
})
160+
161+
decodeHeaders := func(endStream bool) {
162+
_, err := decoder.Write(headerBlock.Bytes())
163+
require.NoError(t, err)
164+
require.NoError(t, decoder.Close())
165+
headerBlock.Reset()
166+
if endStream {
167+
done = true
168+
}
169+
}
170+
framer := http2.NewFramer(io.Discard, bytes.NewReader(data))
171+
172+
for !done && !sawGoAway {
173+
f, err := framer.ReadFrame()
174+
require.NoError(t, err)
175+
176+
switch f := f.(type) {
177+
case *http2.SettingsFrame:
178+
if f.IsAck() {
179+
continue
180+
}
181+
var settings []http2.Setting
182+
require.NoError(t, f.ForeachSetting(func(s http2.Setting) error {
183+
settings = append(settings, s)
184+
return nil
185+
}))
186+
nonACKSettings = append(nonACKSettings, settings)
187+
case *http2.HeadersFrame:
188+
_, err := headerBlock.Write(f.HeaderBlockFragment())
189+
require.NoError(t, err)
190+
if f.HeadersEnded() {
191+
decodeHeaders(f.StreamEnded())
192+
}
193+
case *http2.ContinuationFrame:
194+
_, err := headerBlock.Write(f.HeaderBlockFragment())
195+
require.NoError(t, err)
196+
if f.HeadersEnded() {
197+
decodeHeaders(false)
198+
}
199+
case *http2.DataFrame:
200+
if f.StreamEnded() {
201+
done = true
202+
}
203+
case *http2.GoAwayFrame:
204+
sawGoAway = true
205+
}
206+
}
207+
208+
return nonACKSettings, statusCode, sawGoAway
209+
}

0 commit comments

Comments
 (0)