Skip to content

Commit

Permalink
internal/quic/cmd/interop: add interop test runner
Browse files Browse the repository at this point in the history
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 <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Auto-Submit: Damien Neil <dneil@google.com>
  • Loading branch information
neild authored and gopherbot committed Oct 31, 2023
1 parent 770149e commit 4865e2a
Show file tree
Hide file tree
Showing 5 changed files with 473 additions and 0 deletions.
32 changes: 32 additions & 0 deletions internal/quic/cmd/interop/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
FROM martenseemann/quic-network-simulator-endpoint:latest AS builder

ARG TARGETPLATFORM
RUN echo "TARGETPLATFORM: ${TARGETPLATFORM}"

RUN apt-get update && apt-get install -y wget tar git

ENV GOVERSION=1.21.1

RUN platform=$(echo ${TARGETPLATFORM} | tr '/' '-') && \
filename="go${GOVERSION}.${platform}.tar.gz" && \
wget https://dl.google.com/go/${filename} && \
tar xfz ${filename} && \
rm ${filename}

ENV PATH="/go/bin:${PATH}"

RUN git clone https://go.googlesource.com/net

WORKDIR /net
RUN go build -o /interop ./internal/quic/cmd/interop

FROM martenseemann/quic-network-simulator-endpoint:latest

WORKDIR /go-x-net

COPY --from=builder /interop ./

# copy run script and run it
COPY run_endpoint.sh .
RUN chmod +x run_endpoint.sh
ENTRYPOINT [ "./run_endpoint.sh" ]
7 changes: 7 additions & 0 deletions internal/quic/cmd/interop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
This directory contains configuration and programs used to
integrate with the QUIC Interop Test Runner.

The QUIC Interop Test Runner executes a variety of test cases
against a matrix of clients and servers.

https://github.com/marten-seemann/quic-interop-runner
262 changes: 262 additions & 0 deletions internal/quic/cmd/interop/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build go1.21

// The interop command is the client and server used by QUIC interoperability tests.
//
// https://github.com/marten-seemann/quic-interop-runner
package main

import (
"bytes"
"context"
"crypto/tls"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"net/url"
"os"
"path/filepath"
"sync"

"golang.org/x/net/internal/quic"
)

var (
listen = flag.String("listen", "", "listen address")
cert = flag.String("cert", "", "certificate")
pkey = flag.String("key", "", "private key")
root = flag.String("root", "", "serve files from this root")
output = flag.String("output", "", "directory to write files to")
)

func main() {
ctx := context.Background()
flag.Parse()
urls := flag.Args()

config := &quic.Config{
TLSConfig: &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
NextProtos: []string{"hq-interop"},
},
MaxBidiRemoteStreams: -1,
MaxUniRemoteStreams: -1,
}
if *cert != "" {
c, err := tls.LoadX509KeyPair(*cert, *pkey)
if err != nil {
log.Fatal(err)
}
config.TLSConfig.Certificates = []tls.Certificate{c}
}
if *root != "" {
config.MaxBidiRemoteStreams = 100
}
if keylog := os.Getenv("SSLKEYLOGFILE"); keylog != "" {
f, err := os.Create(keylog)
if err != nil {
log.Fatal(err)
}
defer f.Close()
config.TLSConfig.KeyLogWriter = f
}

testcase := os.Getenv("TESTCASE")
switch testcase {
case "handshake", "keyupdate":
basicTest(ctx, config, urls)
return
case "chacha20":
// "[...] offer only ChaCha20 as a ciphersuite."
//
// crypto/tls does not support configuring TLS 1.3 ciphersuites,
// so we can't support this test.
case "transfer":
// "The client should use small initial flow control windows
// for both stream- and connection-level flow control
// such that the during the transfer of files on the order of 1 MB
// the flow control window needs to be increased."
config.MaxStreamReadBufferSize = 64 << 10
config.MaxConnReadBufferSize = 64 << 10
basicTest(ctx, config, urls)
return
case "http3":
// TODO
case "multiconnect":
// TODO
case "resumption":
// TODO
case "retry":
// TODO
case "versionnegotiation":
// "The client should start a connection using
// an unsupported version number [...]"
//
// We don't support setting the client's version,
// so only run this test as a server.
if *listen != "" && len(urls) == 0 {
basicTest(ctx, config, urls)
return
}
case "v2":
// We do not support QUIC v2.
case "zerortt":
// TODO
}
fmt.Printf("unsupported test case %q\n", testcase)
os.Exit(127)
}

// basicTest runs the standard test setup.
//
// As a server, it serves the contents of the -root directory.
// As a client, it downloads all the provided URLs in parallel,
// making one connection to each destination server.
func basicTest(ctx context.Context, config *quic.Config, urls []string) {
l, err := quic.Listen("udp", *listen, config)
if err != nil {
log.Fatal(err)
}
log.Printf("listening on %v", l.LocalAddr())

byAuthority := map[string][]*url.URL{}
for _, s := range urls {
u, addr, err := parseURL(s)
if err != nil {
log.Fatal(err)
}
byAuthority[addr] = append(byAuthority[addr], u)
}
var g sync.WaitGroup
defer g.Wait()
for addr, u := range byAuthority {
addr, u := addr, u
g.Add(1)
go func() {
defer g.Done()
fetchFrom(ctx, l, addr, u)
}()
}

if config.MaxBidiRemoteStreams >= 0 {
serve(ctx, l)
}
}

func serve(ctx context.Context, l *quic.Listener) error {
for {
c, err := l.Accept(ctx)
if err != nil {
return err
}
go serveConn(ctx, c)
}
}

func serveConn(ctx context.Context, c *quic.Conn) {
for {
s, err := c.AcceptStream(ctx)
if err != nil {
return
}
go func() {
if err := serveReq(ctx, s); err != nil {
log.Print("serveReq:", err)
}
}()
}
}

func serveReq(ctx context.Context, s *quic.Stream) error {
defer s.Close()
req, err := io.ReadAll(s)
if err != nil {
return err
}
if !bytes.HasSuffix(req, []byte("\r\n")) {
return errors.New("invalid request")
}
req = bytes.TrimSuffix(req, []byte("\r\n"))
if !bytes.HasPrefix(req, []byte("GET /")) {
return errors.New("invalid request")
}
req = bytes.TrimPrefix(req, []byte("GET /"))
if !filepath.IsLocal(string(req)) {
return errors.New("invalid request")
}
f, err := os.Open(filepath.Join(*root, string(req)))
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(s, f)
return err
}

func parseURL(s string) (u *url.URL, authority string, err error) {
u, err = url.Parse(s)
if err != nil {
return nil, "", err
}
host := u.Hostname()
port := u.Port()
if port == "" {
port = "443"
}
authority = net.JoinHostPort(host, port)
return u, authority, nil
}

func fetchFrom(ctx context.Context, l *quic.Listener, addr string, urls []*url.URL) {
conn, err := l.Dial(ctx, "udp", addr)
if err != nil {
log.Printf("%v: %v", addr, err)
return
}
log.Printf("connected to %v", addr)
defer conn.Close()
var g sync.WaitGroup
for _, u := range urls {
u := u
g.Add(1)
go func() {
defer g.Done()
if err := fetchOne(ctx, conn, u); err != nil {
log.Printf("fetch %v: %v", u, err)
} else {
log.Printf("fetched %v", u)
}
}()
}
g.Wait()
}

func fetchOne(ctx context.Context, conn *quic.Conn, u *url.URL) error {
if len(u.Path) == 0 || u.Path[0] != '/' || !filepath.IsLocal(u.Path[1:]) {
return errors.New("invalid path")
}
file, err := os.Create(filepath.Join(*output, u.Path[1:]))
if err != nil {
return err
}
s, err := conn.NewStream(ctx)
if err != nil {
return err
}
defer s.Close()
if _, err := s.Write([]byte("GET " + u.Path + "\r\n")); err != nil {
return err
}
s.CloseWrite()
if _, err := io.Copy(file, s); err != nil {
return err
}
return nil
}
Loading

0 comments on commit 4865e2a

Please sign in to comment.