Skip to content

Commit 4865e2a

Browse files
neildgopherbot
authored andcommitted
internal/quic/cmd/interop: add interop test runner
The QUIC interop tests at https://interop.seemann.io/ invoke a program and instruct it to perform some set of operations (mostly serve files from a directory, or download a set of files). The cmd/interop binary executes test cases for our implementation. For golang/go#58547 Change-Id: Ic1c8be2f3f49a30464650d9eaa5ded74c92fa5a7 Reviewed-on: https://go-review.googlesource.com/c/net/+/532435 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Jonathan Amsterdam <[email protected]> Auto-Submit: Damien Neil <[email protected]>
1 parent 770149e commit 4865e2a

File tree

5 files changed

+473
-0
lines changed

5 files changed

+473
-0
lines changed

internal/quic/cmd/interop/Dockerfile

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
FROM martenseemann/quic-network-simulator-endpoint:latest AS builder
2+
3+
ARG TARGETPLATFORM
4+
RUN echo "TARGETPLATFORM: ${TARGETPLATFORM}"
5+
6+
RUN apt-get update && apt-get install -y wget tar git
7+
8+
ENV GOVERSION=1.21.1
9+
10+
RUN platform=$(echo ${TARGETPLATFORM} | tr '/' '-') && \
11+
filename="go${GOVERSION}.${platform}.tar.gz" && \
12+
wget https://dl.google.com/go/${filename} && \
13+
tar xfz ${filename} && \
14+
rm ${filename}
15+
16+
ENV PATH="/go/bin:${PATH}"
17+
18+
RUN git clone https://go.googlesource.com/net
19+
20+
WORKDIR /net
21+
RUN go build -o /interop ./internal/quic/cmd/interop
22+
23+
FROM martenseemann/quic-network-simulator-endpoint:latest
24+
25+
WORKDIR /go-x-net
26+
27+
COPY --from=builder /interop ./
28+
29+
# copy run script and run it
30+
COPY run_endpoint.sh .
31+
RUN chmod +x run_endpoint.sh
32+
ENTRYPOINT [ "./run_endpoint.sh" ]

internal/quic/cmd/interop/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
This directory contains configuration and programs used to
2+
integrate with the QUIC Interop Test Runner.
3+
4+
The QUIC Interop Test Runner executes a variety of test cases
5+
against a matrix of clients and servers.
6+
7+
https://github.com/marten-seemann/quic-interop-runner

internal/quic/cmd/interop/main.go

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.21
6+
7+
// The interop command is the client and server used by QUIC interoperability tests.
8+
//
9+
// https://github.com/marten-seemann/quic-interop-runner
10+
package main
11+
12+
import (
13+
"bytes"
14+
"context"
15+
"crypto/tls"
16+
"errors"
17+
"flag"
18+
"fmt"
19+
"io"
20+
"log"
21+
"net"
22+
"net/url"
23+
"os"
24+
"path/filepath"
25+
"sync"
26+
27+
"golang.org/x/net/internal/quic"
28+
)
29+
30+
var (
31+
listen = flag.String("listen", "", "listen address")
32+
cert = flag.String("cert", "", "certificate")
33+
pkey = flag.String("key", "", "private key")
34+
root = flag.String("root", "", "serve files from this root")
35+
output = flag.String("output", "", "directory to write files to")
36+
)
37+
38+
func main() {
39+
ctx := context.Background()
40+
flag.Parse()
41+
urls := flag.Args()
42+
43+
config := &quic.Config{
44+
TLSConfig: &tls.Config{
45+
InsecureSkipVerify: true,
46+
MinVersion: tls.VersionTLS13,
47+
NextProtos: []string{"hq-interop"},
48+
},
49+
MaxBidiRemoteStreams: -1,
50+
MaxUniRemoteStreams: -1,
51+
}
52+
if *cert != "" {
53+
c, err := tls.LoadX509KeyPair(*cert, *pkey)
54+
if err != nil {
55+
log.Fatal(err)
56+
}
57+
config.TLSConfig.Certificates = []tls.Certificate{c}
58+
}
59+
if *root != "" {
60+
config.MaxBidiRemoteStreams = 100
61+
}
62+
if keylog := os.Getenv("SSLKEYLOGFILE"); keylog != "" {
63+
f, err := os.Create(keylog)
64+
if err != nil {
65+
log.Fatal(err)
66+
}
67+
defer f.Close()
68+
config.TLSConfig.KeyLogWriter = f
69+
}
70+
71+
testcase := os.Getenv("TESTCASE")
72+
switch testcase {
73+
case "handshake", "keyupdate":
74+
basicTest(ctx, config, urls)
75+
return
76+
case "chacha20":
77+
// "[...] offer only ChaCha20 as a ciphersuite."
78+
//
79+
// crypto/tls does not support configuring TLS 1.3 ciphersuites,
80+
// so we can't support this test.
81+
case "transfer":
82+
// "The client should use small initial flow control windows
83+
// for both stream- and connection-level flow control
84+
// such that the during the transfer of files on the order of 1 MB
85+
// the flow control window needs to be increased."
86+
config.MaxStreamReadBufferSize = 64 << 10
87+
config.MaxConnReadBufferSize = 64 << 10
88+
basicTest(ctx, config, urls)
89+
return
90+
case "http3":
91+
// TODO
92+
case "multiconnect":
93+
// TODO
94+
case "resumption":
95+
// TODO
96+
case "retry":
97+
// TODO
98+
case "versionnegotiation":
99+
// "The client should start a connection using
100+
// an unsupported version number [...]"
101+
//
102+
// We don't support setting the client's version,
103+
// so only run this test as a server.
104+
if *listen != "" && len(urls) == 0 {
105+
basicTest(ctx, config, urls)
106+
return
107+
}
108+
case "v2":
109+
// We do not support QUIC v2.
110+
case "zerortt":
111+
// TODO
112+
}
113+
fmt.Printf("unsupported test case %q\n", testcase)
114+
os.Exit(127)
115+
}
116+
117+
// basicTest runs the standard test setup.
118+
//
119+
// As a server, it serves the contents of the -root directory.
120+
// As a client, it downloads all the provided URLs in parallel,
121+
// making one connection to each destination server.
122+
func basicTest(ctx context.Context, config *quic.Config, urls []string) {
123+
l, err := quic.Listen("udp", *listen, config)
124+
if err != nil {
125+
log.Fatal(err)
126+
}
127+
log.Printf("listening on %v", l.LocalAddr())
128+
129+
byAuthority := map[string][]*url.URL{}
130+
for _, s := range urls {
131+
u, addr, err := parseURL(s)
132+
if err != nil {
133+
log.Fatal(err)
134+
}
135+
byAuthority[addr] = append(byAuthority[addr], u)
136+
}
137+
var g sync.WaitGroup
138+
defer g.Wait()
139+
for addr, u := range byAuthority {
140+
addr, u := addr, u
141+
g.Add(1)
142+
go func() {
143+
defer g.Done()
144+
fetchFrom(ctx, l, addr, u)
145+
}()
146+
}
147+
148+
if config.MaxBidiRemoteStreams >= 0 {
149+
serve(ctx, l)
150+
}
151+
}
152+
153+
func serve(ctx context.Context, l *quic.Listener) error {
154+
for {
155+
c, err := l.Accept(ctx)
156+
if err != nil {
157+
return err
158+
}
159+
go serveConn(ctx, c)
160+
}
161+
}
162+
163+
func serveConn(ctx context.Context, c *quic.Conn) {
164+
for {
165+
s, err := c.AcceptStream(ctx)
166+
if err != nil {
167+
return
168+
}
169+
go func() {
170+
if err := serveReq(ctx, s); err != nil {
171+
log.Print("serveReq:", err)
172+
}
173+
}()
174+
}
175+
}
176+
177+
func serveReq(ctx context.Context, s *quic.Stream) error {
178+
defer s.Close()
179+
req, err := io.ReadAll(s)
180+
if err != nil {
181+
return err
182+
}
183+
if !bytes.HasSuffix(req, []byte("\r\n")) {
184+
return errors.New("invalid request")
185+
}
186+
req = bytes.TrimSuffix(req, []byte("\r\n"))
187+
if !bytes.HasPrefix(req, []byte("GET /")) {
188+
return errors.New("invalid request")
189+
}
190+
req = bytes.TrimPrefix(req, []byte("GET /"))
191+
if !filepath.IsLocal(string(req)) {
192+
return errors.New("invalid request")
193+
}
194+
f, err := os.Open(filepath.Join(*root, string(req)))
195+
if err != nil {
196+
return err
197+
}
198+
defer f.Close()
199+
_, err = io.Copy(s, f)
200+
return err
201+
}
202+
203+
func parseURL(s string) (u *url.URL, authority string, err error) {
204+
u, err = url.Parse(s)
205+
if err != nil {
206+
return nil, "", err
207+
}
208+
host := u.Hostname()
209+
port := u.Port()
210+
if port == "" {
211+
port = "443"
212+
}
213+
authority = net.JoinHostPort(host, port)
214+
return u, authority, nil
215+
}
216+
217+
func fetchFrom(ctx context.Context, l *quic.Listener, addr string, urls []*url.URL) {
218+
conn, err := l.Dial(ctx, "udp", addr)
219+
if err != nil {
220+
log.Printf("%v: %v", addr, err)
221+
return
222+
}
223+
log.Printf("connected to %v", addr)
224+
defer conn.Close()
225+
var g sync.WaitGroup
226+
for _, u := range urls {
227+
u := u
228+
g.Add(1)
229+
go func() {
230+
defer g.Done()
231+
if err := fetchOne(ctx, conn, u); err != nil {
232+
log.Printf("fetch %v: %v", u, err)
233+
} else {
234+
log.Printf("fetched %v", u)
235+
}
236+
}()
237+
}
238+
g.Wait()
239+
}
240+
241+
func fetchOne(ctx context.Context, conn *quic.Conn, u *url.URL) error {
242+
if len(u.Path) == 0 || u.Path[0] != '/' || !filepath.IsLocal(u.Path[1:]) {
243+
return errors.New("invalid path")
244+
}
245+
file, err := os.Create(filepath.Join(*output, u.Path[1:]))
246+
if err != nil {
247+
return err
248+
}
249+
s, err := conn.NewStream(ctx)
250+
if err != nil {
251+
return err
252+
}
253+
defer s.Close()
254+
if _, err := s.Write([]byte("GET " + u.Path + "\r\n")); err != nil {
255+
return err
256+
}
257+
s.CloseWrite()
258+
if _, err := io.Copy(file, s); err != nil {
259+
return err
260+
}
261+
return nil
262+
}

0 commit comments

Comments
 (0)