// 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" "log/slog" "net" "net/url" "os" "path/filepath" "sync" "golang.org/x/net/internal/quic" "golang.org/x/net/internal/quic/qlog" ) 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") qlogdir = flag.String("qlog", "", "directory to write qlog output 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, QLogLogger: slog.New(qlog.NewJSONHandler(qlog.HandlerOptions{ Level: quic.QLogLevelFrame, Dir: *qlogdir, })), } 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.Endpoint) 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.Endpoint, 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 }