arblant
firefox-vpn-client / core / internal / proxy / saturation_test.go
saturation_test.go141 lines · 4.2 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
package proxy

import (
	"crypto/tls"
	"errors"
	"io"
	"net"
	"net/http"
	"net/http/httptest"
	"sync/atomic"
	"testing"
	"time"

	"golang.org/x/net/http2"
)

type flushWriter struct{ http.ResponseWriter }

func (w flushWriter) Write(p []byte) (int, error) {
	n, err := w.ResponseWriter.Write(p)
	w.ResponseWriter.(http.Flusher).Flush()
	return n, err
}

// pipeH2Session builds an h2ProxySession as dialH2ProxySession does, over
// an in-process pipe to a CONNECT echo server that allows maxStreams
// streams at a time.
func pipeH2Session(t *testing.T, timeout time.Duration, maxStreams uint32) *h2ProxySession {
	t.Helper()
	fixture := httptest.NewUnstartedServer(http.NotFoundHandler())
	fixture.EnableHTTP2 = true
	fixture.StartTLS()
	t.Cleanup(fixture.Close)
	clientCfg := fixture.Client().Transport.(*http.Transport).TLSClientConfig.Clone()
	clientCfg.NextProtos, clientCfg.ServerName = []string{"h2"}, "127.0.0.1"
	serverCfg := fixture.TLS.Clone()
	serverCfg.NextProtos = []string{"h2"}

	clientPipe, serverPipe := net.Pipe()
	t.Cleanup(func() { _ = serverPipe.Close(); _ = clientPipe.Close() })
	echo := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.(http.Flusher).Flush()
		_, _ = io.Copy(flushWriter{w}, r.Body)
	})
	go func() {
		serverTLS := tls.Server(serverPipe, serverCfg)
		if serverTLS.Handshake() != nil {
			return
		}
		(&http2.Server{MaxConcurrentStreams: maxStreams}).ServeConn(serverTLS, &http2.ServeConnOpts{Handler: echo})
	}()
	raw := tls.Client(clientPipe, clientCfg)
	if err := raw.Handshake(); err != nil {
		t.Fatal(err)
	}
	cc, err := (&http2.Transport{StrictMaxConcurrentStreams: true}).NewClientConn(raw)
	if err != nil {
		t.Fatal(err)
	}
	s := &h2ProxySession{raw: raw, cc: cc, proxyHost: "127.0.0.1:443", token: "pass", timeout: timeout, stopKeepAlive: func() {}}
	s.alive.Store(true)
	t.Cleanup(func() { _ = s.Close() })
	return s
}

// A session whose streams are all in use is busy, not dead: it stays
// alive, a waiting tunnel takes the stream that frees up, and tunnels that
// find none in time neither mark the session unhealthy nor rebuild it (a
// rebuild would fetch a new pass for nothing).
func TestSaturatedSessionIsBusyNotDead(t *testing.T) {
	s := pipeH2Session(t, 500*time.Millisecond, 2)
	a, err := s.OpenTunnel("a.example:443")
	if err != nil {
		t.Fatal(err)
	}
	b, err := s.OpenTunnel("b.example:443")
	if err != nil {
		t.Fatal(err)
	}
	defer b.Close()
	if !s.IsAlive() {
		t.Fatal("a saturated session must stay alive")
	}

	waited := make(chan error, 1)
	go func() {
		c, err := s.OpenTunnel("c.example:443")
		if err == nil {
			_ = c.Close()
		}
		waited <- err
	}()
	time.Sleep(100 * time.Millisecond)
	_ = a.Close()
	if err := <-waited; err != nil {
		t.Fatalf("a waiting tunnel must take the freed stream: %v", err)
	}

	a, err = s.OpenTunnel("a.example:443")
	if err != nil {
		t.Fatal(err)
	}
	defer a.Close()
	time.Sleep(50 * time.Millisecond)
	for _, target := range []string{"d.example:443", "e.example:443", "f.example:443", "g.example:443"} {
		_, err := s.OpenTunnel(target)
		if !errors.Is(err, errSessionSaturated) {
			t.Fatalf("%s: want errSessionSaturated, got %v", target, err)
		}
		if errors.Is(err, errProxySessionUnhealthy) || shouldRebuildProxySession(err) {
			t.Fatalf("%s: saturation must not count as a broken session: %v", target, err)
		}
	}
	if !s.IsAlive() {
		t.Fatal("the session must still be alive after saturated opens")
	}
}

// Through the controller, saturation never rebuilds the session.
func TestControllerDoesNotRebuildSaturatedSession(t *testing.T) {
	s := pipeH2Session(t, 300*time.Millisecond, 1)
	var rebuilds atomic.Int32
	c := &Controller{current: &managedSession{session: s, expiresAt: time.Now().Add(10 * time.Minute), accepting: true}}
	c.refreshSession = func() error {
		rebuilds.Add(1)
		return errors.New("a new pass")
	}
	held, err := c.OpenTunnel("a.example:443")
	if err != nil {
		t.Fatal(err)
	}
	defer held.Close()
	time.Sleep(50 * time.Millisecond)
	if _, err := c.OpenTunnel("b.example:443"); !errors.Is(err, errSessionSaturated) {
		t.Fatalf("want errSessionSaturated, got %v", err)
	}
	if n := rebuilds.Load(); n != 0 {
		t.Fatalf("saturation rebuilt the session %d times", n)
	}
}