github.com/Psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/server/meek_test.go (about)

     1  /*
     2   * Copyright (c) 2017, Psiphon Inc.
     3   * All rights reserved.
     4   *
     5   * This program is free software: you can redistribute it and/or modify
     6   * it under the terms of the GNU General Public License as published by
     7   * the Free Software Foundation, either version 3 of the License, or
     8   * (at your option) any later version.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package server
    21  
    22  import (
    23  	"bytes"
    24  	"context"
    25  	crypto_rand "crypto/rand"
    26  	"encoding/base64"
    27  	"fmt"
    28  	"io/ioutil"
    29  	"math/rand"
    30  	"net"
    31  	"path/filepath"
    32  	"sync"
    33  	"sync/atomic"
    34  	"syscall"
    35  	"testing"
    36  	"time"
    37  
    38  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
    39  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
    40  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
    41  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
    42  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
    43  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics"
    44  	"golang.org/x/crypto/nacl/box"
    45  )
    46  
    47  var KB = 1024
    48  var MB = KB * KB
    49  
    50  func TestCachedResponse(t *testing.T) {
    51  
    52  	rand.Seed(time.Now().Unix())
    53  
    54  	testCases := []struct {
    55  		concurrentResponses int
    56  		responseSize        int
    57  		bufferSize          int
    58  		extendedBufferSize  int
    59  		extendedBufferCount int
    60  		minBytesPerWrite    int
    61  		maxBytesPerWrite    int
    62  		copyPosition        int
    63  		expectedSuccess     bool
    64  	}{
    65  		{1, 16, 16, 0, 0, 1, 1, 0, true},
    66  
    67  		{1, 31, 16, 0, 0, 1, 1, 15, true},
    68  
    69  		{1, 16, 2, 2, 7, 1, 1, 0, true},
    70  
    71  		{1, 31, 15, 3, 5, 1, 1, 1, true},
    72  
    73  		{1, 16, 16, 0, 0, 1, 1, 16, true},
    74  
    75  		{1, 64*KB + 1, 64 * KB, 64 * KB, 1, 1, 1 * KB, 64 * KB, true},
    76  
    77  		{1, 10 * MB, 64 * KB, 64 * KB, 158, 1, 32 * KB, 0, false},
    78  
    79  		{1, 10 * MB, 64 * KB, 64 * KB, 159, 1, 32 * KB, 0, true},
    80  
    81  		{1, 10 * MB, 64 * KB, 64 * KB, 160, 1, 32 * KB, 0, true},
    82  
    83  		{1, 128 * KB, 64 * KB, 0, 0, 1, 1 * KB, 64 * KB, true},
    84  
    85  		{1, 128 * KB, 64 * KB, 0, 0, 1, 1 * KB, 63 * KB, false},
    86  
    87  		{1, 200 * KB, 64 * KB, 0, 0, 1, 1 * KB, 136 * KB, true},
    88  
    89  		{10, 10 * MB, 64 * KB, 64 * KB, 1589, 1, 32 * KB, 0, false},
    90  
    91  		{10, 10 * MB, 64 * KB, 64 * KB, 1590, 1, 32 * KB, 0, true},
    92  	}
    93  
    94  	for _, testCase := range testCases {
    95  		description := fmt.Sprintf("test case: %+v", testCase)
    96  		t.Run(description, func(t *testing.T) {
    97  
    98  			pool := NewCachedResponseBufferPool(testCase.extendedBufferSize, testCase.extendedBufferCount)
    99  
   100  			responses := make([]*CachedResponse, testCase.concurrentResponses)
   101  			for i := 0; i < testCase.concurrentResponses; i++ {
   102  				responses[i] = NewCachedResponse(testCase.bufferSize, pool)
   103  			}
   104  
   105  			// Repeats exercise CachedResponse.Reset() and CachedResponseBufferPool replacement
   106  			for repeat := 0; repeat < 2; repeat++ {
   107  
   108  				t.Logf("repeat %d", repeat)
   109  
   110  				responseData := make([]byte, testCase.responseSize)
   111  				_, _ = rand.Read(responseData)
   112  
   113  				waitGroup := new(sync.WaitGroup)
   114  
   115  				// Goroutines exercise concurrent access to CachedResponseBufferPool
   116  				for _, response := range responses {
   117  					waitGroup.Add(1)
   118  					go func(response *CachedResponse) {
   119  						defer waitGroup.Done()
   120  
   121  						remainingSize := testCase.responseSize
   122  						for remainingSize > 0 {
   123  
   124  							writeSize := testCase.minBytesPerWrite
   125  							writeSize += rand.Intn(testCase.maxBytesPerWrite - testCase.minBytesPerWrite + 1)
   126  							if writeSize > remainingSize {
   127  								writeSize = remainingSize
   128  							}
   129  
   130  							offset := len(responseData) - remainingSize
   131  							response.Write(responseData[offset : offset+writeSize])
   132  							remainingSize -= writeSize
   133  						}
   134  					}(response)
   135  				}
   136  
   137  				waitGroup.Wait()
   138  
   139  				atLeastOneFailure := false
   140  
   141  				for i, response := range responses {
   142  
   143  					cachedResponseData := new(bytes.Buffer)
   144  
   145  					n, err := response.CopyFromPosition(testCase.copyPosition, cachedResponseData)
   146  
   147  					if testCase.expectedSuccess {
   148  						if err != nil {
   149  							t.Fatalf("CopyFromPosition unexpectedly failed for response %d: %s", i, err)
   150  						}
   151  						if n != cachedResponseData.Len() || n > response.Available() {
   152  							t.Fatalf("cached response size mismatch for response %d", i)
   153  						}
   154  						if !bytes.Equal(responseData[testCase.copyPosition:], cachedResponseData.Bytes()) {
   155  							t.Fatalf("cached response data mismatch for response %d", i)
   156  						}
   157  					} else {
   158  						atLeastOneFailure = true
   159  					}
   160  				}
   161  
   162  				if !testCase.expectedSuccess && !atLeastOneFailure {
   163  					t.Fatalf("CopyFromPosition unexpectedly succeeded for all responses")
   164  				}
   165  
   166  				for _, response := range responses {
   167  					response.Reset()
   168  				}
   169  			}
   170  		})
   171  	}
   172  }
   173  
   174  func TestMeekResiliency(t *testing.T) {
   175  
   176  	upstreamData := make([]byte, 5*MB)
   177  	_, _ = rand.Read(upstreamData)
   178  
   179  	downstreamData := make([]byte, 5*MB)
   180  	_, _ = rand.Read(downstreamData)
   181  
   182  	minWrite, maxWrite := 1, 128*KB
   183  	minRead, maxRead := 1, 128*KB
   184  	minWait, maxWait := 1*time.Millisecond, 500*time.Millisecond
   185  
   186  	sendFunc := func(name string, conn net.Conn, data []byte) {
   187  		for sent := 0; sent < len(data); {
   188  			wait := minWait + time.Duration(rand.Int63n(int64(maxWait-minWait)+1))
   189  			time.Sleep(wait)
   190  			writeLen := minWrite + rand.Intn(maxWrite-minWrite+1)
   191  			writeLen = min(writeLen, len(data)-sent)
   192  			_, err := conn.Write(data[sent : sent+writeLen])
   193  			if err != nil {
   194  				t.Errorf("conn.Write failed: %s", err)
   195  				return
   196  			}
   197  			sent += writeLen
   198  			fmt.Printf("%s sent %d/%d...\n", name, sent, len(data))
   199  		}
   200  		fmt.Printf("%s send complete\n", name)
   201  	}
   202  
   203  	recvFunc := func(name string, conn net.Conn, expectedData []byte) {
   204  		data := make([]byte, len(expectedData))
   205  		for received := 0; received < len(data); {
   206  			wait := minWait + time.Duration(rand.Int63n(int64(maxWait-minWait)+1))
   207  			time.Sleep(wait)
   208  			readLen := minRead + rand.Intn(maxRead-minRead+1)
   209  			readLen = min(readLen, len(data)-received)
   210  			n, err := conn.Read(data[received : received+readLen])
   211  			if err != nil {
   212  				t.Errorf("conn.Read failed: %s", err)
   213  				return
   214  			}
   215  			received += n
   216  			if !bytes.Equal(data[0:received], expectedData[0:received]) {
   217  				fmt.Printf("%s data check has failed...\n", name)
   218  				additionalInfo := ""
   219  				index := bytes.Index(expectedData, data[received-n:received])
   220  				if index != -1 {
   221  					// Helpful for debugging missing or repeated data...
   222  					additionalInfo = fmt.Sprintf(
   223  						" (last read of %d appears at %d)", n, index)
   224  				}
   225  				t.Errorf("%s got unexpected data with %d/%d%s",
   226  					name, received, len(expectedData), additionalInfo)
   227  				return
   228  			}
   229  			fmt.Printf("%s received %d/%d...\n", name, received, len(expectedData))
   230  		}
   231  		fmt.Printf("%s receive complete\n", name)
   232  	}
   233  
   234  	// Run meek server
   235  
   236  	rawMeekCookieEncryptionPublicKey, rawMeekCookieEncryptionPrivateKey, err := box.GenerateKey(crypto_rand.Reader)
   237  	if err != nil {
   238  		t.Fatalf("box.GenerateKey failed: %s", err)
   239  	}
   240  	meekCookieEncryptionPublicKey := base64.StdEncoding.EncodeToString(rawMeekCookieEncryptionPublicKey[:])
   241  	meekCookieEncryptionPrivateKey := base64.StdEncoding.EncodeToString(rawMeekCookieEncryptionPrivateKey[:])
   242  	meekObfuscatedKey := prng.HexString(SSH_OBFUSCATED_KEY_BYTE_LENGTH)
   243  
   244  	mockSupport := &SupportServices{
   245  		Config: &Config{
   246  			MeekObfuscatedKey:              meekObfuscatedKey,
   247  			MeekCookieEncryptionPrivateKey: meekCookieEncryptionPrivateKey,
   248  		},
   249  		TrafficRulesSet: &TrafficRulesSet{},
   250  	}
   251  	mockSupport.GeoIPService, _ = NewGeoIPService([]string{})
   252  
   253  	listener, err := net.Listen("tcp", "127.0.0.1:0")
   254  	if err != nil {
   255  		t.Fatalf("net.Listen failed: %s", err)
   256  	}
   257  	defer listener.Close()
   258  
   259  	serverAddress := listener.Addr().String()
   260  
   261  	relayWaitGroup := new(sync.WaitGroup)
   262  
   263  	var serverClientConn atomic.Value
   264  
   265  	clientHandler := func(_ string, conn net.Conn) {
   266  		serverClientConn.Store(conn)
   267  		name := "server"
   268  		relayWaitGroup.Add(1)
   269  		go func() {
   270  			defer relayWaitGroup.Done()
   271  			sendFunc(name, conn, downstreamData)
   272  		}()
   273  		relayWaitGroup.Add(1)
   274  		go func() {
   275  			defer relayWaitGroup.Done()
   276  			recvFunc(name, conn, upstreamData)
   277  		}()
   278  	}
   279  
   280  	stopBroadcast := make(chan struct{})
   281  
   282  	useTLS := false
   283  	isFronted := false
   284  	useObfuscatedSessionTickets := false
   285  
   286  	server, err := NewMeekServer(
   287  		mockSupport,
   288  		listener,
   289  		"",
   290  		0,
   291  		useTLS,
   292  		isFronted,
   293  		useObfuscatedSessionTickets,
   294  		clientHandler,
   295  		stopBroadcast)
   296  	if err != nil {
   297  		t.Fatalf("NewMeekServer failed: %s", err)
   298  	}
   299  
   300  	serverWaitGroup := new(sync.WaitGroup)
   301  
   302  	serverWaitGroup.Add(1)
   303  	go func() {
   304  		defer serverWaitGroup.Done()
   305  		err := server.Run()
   306  		select {
   307  		case <-stopBroadcast:
   308  			return
   309  		default:
   310  		}
   311  		if err != nil {
   312  			t.Errorf("MeekServer.Run failed: %s", err)
   313  		}
   314  	}()
   315  
   316  	// Run meek client
   317  
   318  	dialConfig := &psiphon.DialConfig{
   319  		DeviceBinder: new(fileDescriptorInterruptor),
   320  		ResolveIP: func(_ context.Context, host string) ([]net.IP, error) {
   321  			return []net.IP{net.ParseIP(host)}, nil
   322  		},
   323  	}
   324  
   325  	params, err := parameters.NewParameters(nil)
   326  	if err != nil {
   327  		t.Fatalf("NewParameters failed: %s", err)
   328  	}
   329  
   330  	meekObfuscatorPaddingSeed, err := prng.NewSeed()
   331  	if err != nil {
   332  		t.Fatalf("prng.NewSeed failed: %s", err)
   333  	}
   334  
   335  	meekConfig := &psiphon.MeekConfig{
   336  		Parameters:                    params,
   337  		DialAddress:                   serverAddress,
   338  		UseHTTPS:                      useTLS,
   339  		UseObfuscatedSessionTickets:   useObfuscatedSessionTickets,
   340  		HostHeader:                    "example.com",
   341  		MeekCookieEncryptionPublicKey: meekCookieEncryptionPublicKey,
   342  		MeekObfuscatedKey:             meekObfuscatedKey,
   343  		MeekObfuscatorPaddingSeed:     meekObfuscatorPaddingSeed,
   344  	}
   345  
   346  	ctx, cancelFunc := context.WithTimeout(
   347  		context.Background(), time.Second*5)
   348  	defer cancelFunc()
   349  
   350  	clientConn, err := psiphon.DialMeek(ctx, meekConfig, dialConfig)
   351  	if err != nil {
   352  		t.Fatalf("psiphon.DialMeek failed: %s", err)
   353  	}
   354  
   355  	// Relay data through meek while interrupting underlying TCP connections
   356  
   357  	name := "client"
   358  	relayWaitGroup.Add(1)
   359  	go func() {
   360  		defer relayWaitGroup.Done()
   361  		sendFunc(name, clientConn, upstreamData)
   362  	}()
   363  
   364  	relayWaitGroup.Add(1)
   365  	go func() {
   366  		defer relayWaitGroup.Done()
   367  		recvFunc(name, clientConn, downstreamData)
   368  	}()
   369  
   370  	relayWaitGroup.Wait()
   371  
   372  	// Check for multiple underlying connections
   373  
   374  	metrics := serverClientConn.Load().(common.MetricsSource).GetMetrics()
   375  	count := metrics["meek_underlying_connection_count"].(int64)
   376  	if count <= 1 {
   377  		t.Fatalf("unexpected meek_underlying_connection_count: %d", count)
   378  	}
   379  
   380  	// Graceful shutdown
   381  
   382  	clientConn.Close()
   383  
   384  	listener.Close()
   385  	close(stopBroadcast)
   386  
   387  	// This wait will hang if shutdown is broken, and the test will ultimately panic
   388  	serverWaitGroup.Wait()
   389  }
   390  
   391  type fileDescriptorInterruptor struct {
   392  }
   393  
   394  func (interruptor *fileDescriptorInterruptor) BindToDevice(fileDescriptor int) (string, error) {
   395  	fdDup, err := syscall.Dup(fileDescriptor)
   396  	if err != nil {
   397  		return "", err
   398  	}
   399  	minAfter := 500 * time.Millisecond
   400  	maxAfter := 1 * time.Second
   401  	after := minAfter + time.Duration(rand.Int63n(int64(maxAfter-minAfter)+1))
   402  	time.AfterFunc(after, func() {
   403  		syscall.Shutdown(fdDup, syscall.SHUT_RDWR)
   404  		syscall.Close(fdDup)
   405  		fmt.Printf("interrupted TCP connection\n")
   406  	})
   407  	return "", nil
   408  }
   409  
   410  func TestMeekRateLimiter(t *testing.T) {
   411  	runTestMeekAccessControl(t, true, false)
   412  	runTestMeekAccessControl(t, false, false)
   413  }
   414  
   415  func TestMeekRestrictFrontingProviders(t *testing.T) {
   416  	runTestMeekAccessControl(t, false, true)
   417  	runTestMeekAccessControl(t, false, false)
   418  }
   419  
   420  func runTestMeekAccessControl(t *testing.T, rateLimit, restrictProvider bool) {
   421  
   422  	attempts := 10
   423  
   424  	allowedConnections := 5
   425  
   426  	if !rateLimit {
   427  		allowedConnections = 10
   428  	}
   429  
   430  	if restrictProvider {
   431  		allowedConnections = 0
   432  	}
   433  
   434  	// Configure tactics
   435  
   436  	frontingProviderID := prng.HexString(8)
   437  
   438  	tacticsConfigJSONFormat := `
   439      {
   440        "RequestPublicKey" : "%s",
   441        "RequestPrivateKey" : "%s",
   442        "RequestObfuscatedKey" : "%s",
   443        "DefaultTactics" : {
   444          "TTL" : "60s",
   445          "Probability" : 1.0,
   446          "Parameters" : {
   447            "RestrictFrontingProviderIDs" : ["%s"],
   448            "RestrictFrontingProviderIDsServerProbability" : 1.0
   449          }
   450        }
   451      }
   452      `
   453  
   454  	tacticsRequestPublicKey, tacticsRequestPrivateKey, tacticsRequestObfuscatedKey, err :=
   455  		tactics.GenerateKeys()
   456  	if err != nil {
   457  		t.Fatalf("error generating tactics keys: %s", err)
   458  	}
   459  
   460  	restrictFrontingProviderID := ""
   461  
   462  	if restrictProvider {
   463  		restrictFrontingProviderID = frontingProviderID
   464  	}
   465  
   466  	tacticsConfigJSON := fmt.Sprintf(
   467  		tacticsConfigJSONFormat,
   468  		tacticsRequestPublicKey, tacticsRequestPrivateKey, tacticsRequestObfuscatedKey,
   469  		restrictFrontingProviderID)
   470  
   471  	tacticsConfigFilename := filepath.Join(testDataDirName, "tactics_config.json")
   472  
   473  	err = ioutil.WriteFile(tacticsConfigFilename, []byte(tacticsConfigJSON), 0600)
   474  	if err != nil {
   475  		t.Fatalf("error paving tactics config file: %s", err)
   476  	}
   477  
   478  	// Run meek server
   479  
   480  	rawMeekCookieEncryptionPublicKey, rawMeekCookieEncryptionPrivateKey, err := box.GenerateKey(crypto_rand.Reader)
   481  	if err != nil {
   482  		t.Fatalf("box.GenerateKey failed: %s", err)
   483  	}
   484  	meekCookieEncryptionPublicKey := base64.StdEncoding.EncodeToString(rawMeekCookieEncryptionPublicKey[:])
   485  	meekCookieEncryptionPrivateKey := base64.StdEncoding.EncodeToString(rawMeekCookieEncryptionPrivateKey[:])
   486  	meekObfuscatedKey := prng.HexString(SSH_OBFUSCATED_KEY_BYTE_LENGTH)
   487  
   488  	tunnelProtocol := protocol.TUNNEL_PROTOCOL_FRONTED_MEEK
   489  
   490  	meekRateLimiterTunnelProtocols := []string{tunnelProtocol}
   491  	if !rateLimit {
   492  		meekRateLimiterTunnelProtocols = []string{protocol.TUNNEL_PROTOCOL_FRONTED_MEEK}
   493  	}
   494  
   495  	mockSupport := &SupportServices{
   496  		Config: &Config{
   497  			MeekObfuscatedKey:              meekObfuscatedKey,
   498  			MeekCookieEncryptionPrivateKey: meekCookieEncryptionPrivateKey,
   499  			TunnelProtocolPorts:            map[string]int{tunnelProtocol: 0},
   500  			frontingProviderID:             frontingProviderID,
   501  		},
   502  		TrafficRulesSet: &TrafficRulesSet{
   503  			MeekRateLimiterHistorySize:                   allowedConnections,
   504  			MeekRateLimiterThresholdSeconds:              attempts,
   505  			MeekRateLimiterTunnelProtocols:               meekRateLimiterTunnelProtocols,
   506  			MeekRateLimiterGarbageCollectionTriggerCount: 1,
   507  			MeekRateLimiterReapHistoryFrequencySeconds:   1,
   508  		},
   509  	}
   510  	mockSupport.GeoIPService, _ = NewGeoIPService([]string{})
   511  
   512  	tacticsServer, err := tactics.NewServer(nil, nil, nil, tacticsConfigFilename)
   513  	if err != nil {
   514  		t.Fatalf("tactics.NewServer failed: %s", err)
   515  	}
   516  
   517  	mockSupport.TacticsServer = tacticsServer
   518  	mockSupport.ServerTacticsParametersCache = NewServerTacticsParametersCache(mockSupport)
   519  
   520  	listener, err := net.Listen("tcp", "127.0.0.1:0")
   521  	if err != nil {
   522  		t.Fatalf("net.Listen failed: %s", err)
   523  	}
   524  	defer listener.Close()
   525  
   526  	serverAddress := listener.Addr().String()
   527  
   528  	stopBroadcast := make(chan struct{})
   529  
   530  	useTLS := false
   531  	isFronted := false
   532  	useObfuscatedSessionTickets := false
   533  
   534  	server, err := NewMeekServer(
   535  		mockSupport,
   536  		listener,
   537  		tunnelProtocol,
   538  		0,
   539  		useTLS,
   540  		isFronted,
   541  		useObfuscatedSessionTickets,
   542  		func(_ string, conn net.Conn) {
   543  			go func() {
   544  				for {
   545  					buffer := make([]byte, 1)
   546  					n, err := conn.Read(buffer)
   547  					if err == nil && n == 1 {
   548  						_, err = conn.Write(buffer)
   549  					}
   550  					if err != nil {
   551  						conn.Close()
   552  						break
   553  					}
   554  				}
   555  			}()
   556  		},
   557  		stopBroadcast)
   558  	if err != nil {
   559  		t.Fatalf("NewMeekServer failed: %s", err)
   560  	}
   561  
   562  	serverWaitGroup := new(sync.WaitGroup)
   563  
   564  	serverWaitGroup.Add(1)
   565  	go func() {
   566  		defer serverWaitGroup.Done()
   567  		err := server.Run()
   568  		select {
   569  		case <-stopBroadcast:
   570  			return
   571  		default:
   572  		}
   573  		if err != nil {
   574  			t.Errorf("MeekServer.Run failed: %s", err)
   575  		}
   576  	}()
   577  
   578  	// Run meek clients:
   579  	// For 10 attempts, connect once per second vs. rate limit of 5-per-10 seconds,
   580  	// so about half of the connections should be rejected by the rate limiter.
   581  
   582  	totalConnections := 0
   583  	totalFailures := 0
   584  
   585  	for i := 0; i < attempts; i++ {
   586  
   587  		dialConfig := &psiphon.DialConfig{
   588  			ResolveIP: func(_ context.Context, host string) ([]net.IP, error) {
   589  				return []net.IP{net.ParseIP(host)}, nil
   590  			},
   591  		}
   592  
   593  		params, err := parameters.NewParameters(nil)
   594  		if err != nil {
   595  			t.Fatalf("NewParameters failed: %s", err)
   596  		}
   597  
   598  		meekObfuscatorPaddingSeed, err := prng.NewSeed()
   599  		if err != nil {
   600  			t.Fatalf("prng.NewSeed failed: %s", err)
   601  		}
   602  
   603  		meekConfig := &psiphon.MeekConfig{
   604  			Parameters:                    params,
   605  			DialAddress:                   serverAddress,
   606  			HostHeader:                    "example.com",
   607  			MeekCookieEncryptionPublicKey: meekCookieEncryptionPublicKey,
   608  			MeekObfuscatedKey:             meekObfuscatedKey,
   609  			MeekObfuscatorPaddingSeed:     meekObfuscatorPaddingSeed,
   610  		}
   611  
   612  		ctx, cancelFunc := context.WithTimeout(
   613  			context.Background(), 500*time.Millisecond)
   614  		defer cancelFunc()
   615  
   616  		clientConn, err := psiphon.DialMeek(ctx, meekConfig, dialConfig)
   617  
   618  		if err == nil {
   619  			_, err = clientConn.Write([]byte{0})
   620  		}
   621  		if err == nil {
   622  			buffer := make([]byte, 1)
   623  			_, err = clientConn.Read(buffer)
   624  		}
   625  
   626  		if clientConn != nil {
   627  			clientConn.Close()
   628  		}
   629  
   630  		if err != nil {
   631  			totalFailures += 1
   632  		} else {
   633  			totalConnections += 1
   634  		}
   635  
   636  		if i < attempts-1 {
   637  			time.Sleep(1 * time.Second)
   638  		}
   639  	}
   640  
   641  	if totalConnections != allowedConnections ||
   642  		totalFailures != attempts-totalConnections {
   643  
   644  		t.Fatalf(
   645  			"Unexpected results: %d connections, %d failures, %d allowed",
   646  			totalConnections, totalFailures, allowedConnections)
   647  	}
   648  
   649  	// Graceful shutdown
   650  
   651  	listener.Close()
   652  	close(stopBroadcast)
   653  
   654  	// This wait will hang if shutdown is broken, and the test will ultimately panic
   655  	serverWaitGroup.Wait()
   656  }