mirror of
https://github.com/go-gost/gost.git
synced 2026-10-08 12:05:46 +00:00
Add an e2e suite covering go-gost/x#125: a malformed UDP datagram sent to a cipherKey-enabled QUIC listener must not close the shared transport. Uses the canonical http-over-quic chaining pattern and verifies the forward still works after the invalid datagram. Also bumps go.mod/go.sum (plugin v0.6.1, otel, x/net, etc.).
136 lines
4.6 KiB
Go
136 lines
4.6 KiB
Go
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/suite"
|
|
"github.com/testcontainers/testcontainers-go"
|
|
)
|
|
|
|
// QUICSuite covers the QUIC listener/dialer pair (listener type: quic, dialer
|
|
// type: quic) with the cipherKey obfuscation enabled. It uses the canonical
|
|
// GOST chaining pattern: a client container exposes a plain HTTP proxy that
|
|
// tunnels through a quic chain to a quic server (HTTP over QUIC).
|
|
type QUICSuite struct {
|
|
suite.Suite
|
|
ctx context.Context
|
|
echoC testcontainers.Container
|
|
echoIP string
|
|
}
|
|
|
|
func (s *QUICSuite) SetupSuite() {
|
|
s.ctx = context.Background()
|
|
|
|
s.T().Logf("start tcp echo container...")
|
|
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
|
|
s.Require().NoError(err)
|
|
s.echoC = echoC
|
|
|
|
echoIP, err := echoC.ContainerIP(s.ctx)
|
|
s.Require().NoError(err)
|
|
s.echoIP = echoIP
|
|
}
|
|
|
|
func (s *QUICSuite) TearDownSuite() {
|
|
if s.echoC != nil {
|
|
s.echoC.Terminate(s.ctx)
|
|
}
|
|
}
|
|
|
|
// startChain brings up a quic server (alias "quic-server", cipherKey enabled,
|
|
// http handler) and a client container that chains to it through a quic
|
|
// dialer. The client exposes port 8080 for curl.
|
|
func (s *QUICSuite) startChain() (testcontainers.Container, testcontainers.Container) {
|
|
s.T().Helper()
|
|
|
|
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
|
|
"testdata/quic/server.yaml", []string{"quic-server"}, []string{"18843/udp"})
|
|
s.Require().NoError(err)
|
|
|
|
rendered, err := RenderConfig("testdata/quic/client.yaml", ConfigData{ServerAddr: "quic-server:18843"})
|
|
s.Require().NoError(err)
|
|
s.T().Cleanup(func() { os.Remove(rendered) })
|
|
|
|
clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, rendered, "8080/tcp")
|
|
s.Require().NoError(err)
|
|
|
|
return serverC, clientC
|
|
}
|
|
|
|
// curlEcho runs curl through the client's local http proxy and returns the
|
|
// exit code plus the captured body.
|
|
func (s *QUICSuite) curlEcho(clientC testcontainers.Container) (int, string) {
|
|
s.T().Helper()
|
|
cmd := []string{"curl", "-s", "--max-time", "20", "-x", "http://127.0.0.1:8080",
|
|
fmt.Sprintf("http://%s:5678/", s.echoIP)}
|
|
code, out, _ := clientC.Exec(s.ctx, cmd)
|
|
body, _ := io.ReadAll(out)
|
|
return code, string(body)
|
|
}
|
|
|
|
// sendInvalidDatagram sends a single malformed (non-encrypted) UDP datagram to
|
|
// the quic server's listener, reproducing the issue's repro packet.
|
|
func (s *QUICSuite) sendInvalidDatagram(clientC testcontainers.Container) {
|
|
s.T().Helper()
|
|
cmd := []string{"python3", "-c",
|
|
"import socket; s=socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.sendto(b\"x\", (\"quic-server\", 18843))"}
|
|
code, out, err := clientC.Exec(s.ctx, cmd)
|
|
s.Require().NoError(err)
|
|
if code != 0 {
|
|
b, _ := io.ReadAll(out)
|
|
s.T().Fatalf("send invalid datagram failed (%d): %s", code, string(b))
|
|
}
|
|
}
|
|
|
|
// TestQUICForwardCipherKeySurvivesInvalidDatagram verifies that a malformed
|
|
// UDP datagram does not close the shared QUIC transport (go-gost/x#125).
|
|
//
|
|
// With the bug, cipherConn.ReadFrom returned the decrypt error straight to
|
|
// quic-go, which treats any packet-socket error as fatal and closes the shared
|
|
// transport: a single invalid datagram kills the listener and correct clients
|
|
// subsequently time out. With the fix the invalid datagram is discarded and
|
|
// the listener keeps serving.
|
|
//
|
|
// Sequence: baseline forward works -> malformed datagram -> forward still works.
|
|
func (s *QUICSuite) TestQUICForwardCipherKeySurvivesInvalidDatagram() {
|
|
serverC, clientC := s.startChain()
|
|
defer serverC.Terminate(s.ctx)
|
|
defer clientC.Terminate(s.ctx)
|
|
|
|
// Baseline: a valid QUIC forward must work before we poke the listener.
|
|
code, body := s.curlEcho(clientC)
|
|
if code != 0 || !strings.Contains(body, "hello-gost") {
|
|
s.dump("quic-baseline logs", clientC, serverC)
|
|
}
|
|
s.Require().Equal(0, code, "baseline QUIC forward should work")
|
|
s.Require().Contains(body, "hello-gost")
|
|
|
|
// Send one malformed UDP datagram to the quic listener's cipher socket.
|
|
s.sendInvalidDatagram(clientC)
|
|
|
|
// The listener must still be alive: a fresh QUIC forward must succeed.
|
|
code, body = s.curlEcho(clientC)
|
|
if code != 0 || !strings.Contains(body, "hello-gost") {
|
|
s.dump("quic-after-invalid logs", clientC, serverC)
|
|
}
|
|
s.Require().Equal(0, code,
|
|
"QUIC forward should still work after an invalid datagram (issue #125)")
|
|
s.Require().Contains(body, "hello-gost",
|
|
"listener must not close on an invalid datagram (issue #125)")
|
|
}
|
|
|
|
func (s *QUICSuite) dump(label string, cs ...testcontainers.Container) {
|
|
for _, c := range cs {
|
|
DumpLogs(s.T(), s.ctx, fmt.Sprintf("%s: %s", label, c.GetContainerID()), c)
|
|
}
|
|
}
|
|
|
|
func TestQUICSuite(t *testing.T) {
|
|
suite.Run(t, new(QUICSuite))
|
|
}
|