matcher_test.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. /*
  2. * Copyright (c) 2023, 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. package inproxy
  20. import (
  21. "context"
  22. "fmt"
  23. "strings"
  24. "testing"
  25. "time"
  26. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
  27. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
  28. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
  29. )
  30. func TestMatcher(t *testing.T) {
  31. err := runTestMatcher()
  32. if err != nil {
  33. t.Errorf(errors.Trace(err).Error())
  34. }
  35. }
  36. func runTestMatcher() error {
  37. limitEntryCount := 50
  38. rateLimitQuantity := 100
  39. rateLimitInterval := 500 * time.Millisecond
  40. logger := newTestLogger()
  41. m := NewMatcher(
  42. &MatcherConfig{
  43. Logger: logger,
  44. AnnouncementLimitEntryCount: limitEntryCount,
  45. AnnouncementRateLimitQuantity: rateLimitQuantity,
  46. AnnouncementRateLimitInterval: rateLimitInterval,
  47. OfferLimitEntryCount: limitEntryCount,
  48. OfferRateLimitQuantity: rateLimitQuantity,
  49. OfferRateLimitInterval: rateLimitInterval,
  50. })
  51. err := m.Start()
  52. if err != nil {
  53. return errors.Trace(err)
  54. }
  55. defer m.Stop()
  56. makeID := func() ID {
  57. ID, err := MakeID()
  58. if err != nil {
  59. panic(err)
  60. }
  61. return ID
  62. }
  63. makeAnnouncement := func(properties *MatchProperties) *MatchAnnouncement {
  64. return &MatchAnnouncement{
  65. Properties: *properties,
  66. ProxyID: makeID(),
  67. ConnectionID: makeID(),
  68. }
  69. }
  70. makeOffer := func(properties *MatchProperties) *MatchOffer {
  71. return &MatchOffer{
  72. Properties: *properties,
  73. ClientProxyProtocolVersion: ProxyProtocolVersion1,
  74. }
  75. }
  76. proxyIP := randomIPAddress()
  77. proxyFunc := func(
  78. resultChan chan error,
  79. proxyIP string,
  80. matchProperties *MatchProperties,
  81. timeout time.Duration,
  82. waitBeforeAnswer chan struct{},
  83. answerSuccess bool) {
  84. ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
  85. defer cancelFunc()
  86. announcement := makeAnnouncement(matchProperties)
  87. offer, err := m.Announce(ctx, proxyIP, announcement)
  88. if err != nil {
  89. resultChan <- errors.Trace(err)
  90. return
  91. }
  92. if waitBeforeAnswer != nil {
  93. <-waitBeforeAnswer
  94. }
  95. if answerSuccess {
  96. err = m.Answer(
  97. &MatchAnswer{
  98. ProxyID: announcement.ProxyID,
  99. ConnectionID: announcement.ConnectionID,
  100. SelectedProxyProtocolVersion: offer.ClientProxyProtocolVersion,
  101. })
  102. } else {
  103. m.AnswerError(announcement.ProxyID, announcement.ConnectionID)
  104. }
  105. resultChan <- errors.Trace(err)
  106. }
  107. clientIP := randomIPAddress()
  108. clientFunc := func(
  109. resultChan chan error,
  110. clientIP string,
  111. matchProperties *MatchProperties,
  112. timeout time.Duration) {
  113. ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
  114. defer cancelFunc()
  115. offer := makeOffer(matchProperties)
  116. answer, _, err := m.Offer(ctx, clientIP, offer)
  117. if err != nil {
  118. resultChan <- errors.Trace(err)
  119. return
  120. }
  121. if answer.SelectedProxyProtocolVersion != offer.ClientProxyProtocolVersion {
  122. resultChan <- errors.TraceNew("unexpected selected proxy protocol version")
  123. return
  124. }
  125. resultChan <- nil
  126. }
  127. // Test: announce timeout
  128. proxyResultChan := make(chan error)
  129. go proxyFunc(proxyResultChan, proxyIP, &MatchProperties{}, 1*time.Microsecond, nil, true)
  130. err = <-proxyResultChan
  131. if err == nil || !strings.HasSuffix(err.Error(), "context deadline exceeded") {
  132. return errors.Tracef("unexpected result: %v", err)
  133. }
  134. if m.announcementQueue.Len() != 0 {
  135. return errors.TraceNew("unexpected queue size")
  136. }
  137. // Test: limit announce entries by IP
  138. time.Sleep(rateLimitInterval)
  139. maxEntries := limitEntryCount
  140. maxEntriesProxyResultChan := make(chan error, maxEntries)
  141. // fill the queue with max entries for one IP; the first one will timeout sooner
  142. go proxyFunc(maxEntriesProxyResultChan, proxyIP, &MatchProperties{}, 10*time.Millisecond, nil, true)
  143. for i := 0; i < maxEntries-1; i++ {
  144. go proxyFunc(maxEntriesProxyResultChan, proxyIP, &MatchProperties{}, 100*time.Millisecond, nil, true)
  145. }
  146. // await goroutines filling queue
  147. for {
  148. time.Sleep(10 * time.Microsecond)
  149. m.announcementQueueMutex.Lock()
  150. queueLen := m.announcementQueue.Len()
  151. m.announcementQueueMutex.Unlock()
  152. if queueLen == maxEntries {
  153. break
  154. }
  155. }
  156. // the next enqueue should fail with "max entries"
  157. go proxyFunc(proxyResultChan, proxyIP, &MatchProperties{}, 10*time.Millisecond, nil, true)
  158. err = <-proxyResultChan
  159. if err == nil || !strings.HasSuffix(err.Error(), "max entries for IP") {
  160. return errors.Tracef("unexpected result: %v", err)
  161. }
  162. // wait for first entry to timeout
  163. err = <-maxEntriesProxyResultChan
  164. if err == nil || !strings.HasSuffix(err.Error(), "context deadline exceeded") {
  165. return errors.Tracef("unexpected result: %v", err)
  166. }
  167. // now another enqueue succeeds as expected
  168. go proxyFunc(proxyResultChan, proxyIP, &MatchProperties{}, 10*time.Millisecond, nil, true)
  169. err = <-proxyResultChan
  170. if err == nil || !strings.HasSuffix(err.Error(), "context deadline exceeded") {
  171. return errors.Tracef("unexpected result: %v", err)
  172. }
  173. // drain remaining entries
  174. for i := 0; i < maxEntries-1; i++ {
  175. err = <-maxEntriesProxyResultChan
  176. if err == nil || !strings.HasSuffix(err.Error(), "context deadline exceeded") {
  177. return errors.Tracef("unexpected result: %v", err)
  178. }
  179. }
  180. // Test: offer timeout
  181. clientResultChan := make(chan error)
  182. go clientFunc(clientResultChan, clientIP, &MatchProperties{}, 1*time.Microsecond)
  183. err = <-clientResultChan
  184. if err == nil || !strings.HasSuffix(err.Error(), "context deadline exceeded") {
  185. return errors.Tracef("unexpected result: %v", err)
  186. }
  187. if m.offerQueue.Len() != 0 {
  188. return errors.TraceNew("unexpected queue size")
  189. }
  190. // Test: limit offer entries by IP
  191. time.Sleep(rateLimitInterval)
  192. maxEntries = limitEntryCount
  193. maxEntriesClientResultChan := make(chan error, maxEntries)
  194. // fill the queue with max entries for one IP; the first one will timeout sooner
  195. go clientFunc(maxEntriesClientResultChan, clientIP, &MatchProperties{}, 10*time.Millisecond)
  196. for i := 0; i < maxEntries-1; i++ {
  197. go clientFunc(maxEntriesClientResultChan, clientIP, &MatchProperties{}, 100*time.Millisecond)
  198. }
  199. // await goroutines filling queue
  200. for {
  201. time.Sleep(10 * time.Microsecond)
  202. m.offerQueueMutex.Lock()
  203. queueLen := m.offerQueue.Len()
  204. m.offerQueueMutex.Unlock()
  205. if queueLen == maxEntries {
  206. break
  207. }
  208. }
  209. // enqueue should fail with "max entries"
  210. go clientFunc(clientResultChan, clientIP, &MatchProperties{}, 10*time.Millisecond)
  211. err = <-clientResultChan
  212. if err == nil || !strings.HasSuffix(err.Error(), "max entries for IP") {
  213. return errors.Tracef("unexpected result: %v", err)
  214. }
  215. // wait for first entry to timeout
  216. err = <-maxEntriesClientResultChan
  217. if err == nil || !strings.HasSuffix(err.Error(), "context deadline exceeded") {
  218. return errors.Tracef("unexpected result: %v", err)
  219. }
  220. // now another enqueue succeeds as expected
  221. go clientFunc(clientResultChan, clientIP, &MatchProperties{}, 10*time.Millisecond)
  222. err = <-clientResultChan
  223. if err == nil || !strings.HasSuffix(err.Error(), "context deadline exceeded") {
  224. return errors.Tracef("unexpected result: %v", err)
  225. }
  226. // drain remaining entries
  227. for i := 0; i < maxEntries-1; i++ {
  228. err = <-maxEntriesClientResultChan
  229. if err == nil || !strings.HasSuffix(err.Error(), "context deadline exceeded") {
  230. return errors.Tracef("unexpected result: %v", err)
  231. }
  232. }
  233. // Test: announcement rate limit
  234. m.SetLimits(
  235. 0, rateLimitQuantity, rateLimitInterval, []ID{},
  236. 0, rateLimitQuantity, rateLimitInterval)
  237. time.Sleep(rateLimitInterval)
  238. maxEntries = rateLimitQuantity
  239. maxEntriesProxyResultChan = make(chan error, maxEntries)
  240. for i := 0; i < maxEntries; i++ {
  241. go proxyFunc(maxEntriesProxyResultChan, proxyIP, &MatchProperties{}, 1*time.Microsecond, nil, true)
  242. }
  243. time.Sleep(rateLimitInterval / 2)
  244. // the next enqueue should fail with "rate exceeded"
  245. go proxyFunc(proxyResultChan, proxyIP, &MatchProperties{}, 10*time.Millisecond, nil, true)
  246. err = <-proxyResultChan
  247. if err == nil || !strings.HasSuffix(err.Error(), "rate exceeded for IP") {
  248. return errors.Tracef("unexpected result: %v", err)
  249. }
  250. // Test: offer rate limit
  251. maxEntries = rateLimitQuantity
  252. maxEntriesClientResultChan = make(chan error, maxEntries)
  253. for i := 0; i < rateLimitQuantity; i++ {
  254. go clientFunc(maxEntriesClientResultChan, clientIP, &MatchProperties{}, 1*time.Microsecond)
  255. }
  256. time.Sleep(rateLimitInterval / 2)
  257. // enqueue should fail with "rate exceeded"
  258. go clientFunc(clientResultChan, clientIP, &MatchProperties{}, 10*time.Millisecond)
  259. err = <-clientResultChan
  260. if err == nil || !strings.HasSuffix(err.Error(), "rate exceeded for IP") {
  261. return errors.Tracef("unexpected result: %v", err)
  262. }
  263. time.Sleep(rateLimitInterval)
  264. m.SetLimits(
  265. limitEntryCount, rateLimitQuantity, rateLimitInterval, []ID{},
  266. limitEntryCount, rateLimitQuantity, rateLimitInterval)
  267. // Test: basic match
  268. basicCommonCompartmentIDs := []ID{makeID()}
  269. geoIPData1 := &MatchProperties{
  270. GeoIPData: common.GeoIPData{Country: "C1", ASN: "A1"},
  271. CommonCompartmentIDs: basicCommonCompartmentIDs,
  272. }
  273. geoIPData2 := &MatchProperties{
  274. GeoIPData: common.GeoIPData{Country: "C2", ASN: "A2"},
  275. CommonCompartmentIDs: basicCommonCompartmentIDs,
  276. }
  277. go proxyFunc(proxyResultChan, proxyIP, geoIPData1, 10*time.Millisecond, nil, true)
  278. go clientFunc(clientResultChan, clientIP, geoIPData2, 10*time.Millisecond)
  279. err = <-proxyResultChan
  280. if err != nil {
  281. return errors.Trace(err)
  282. }
  283. err = <-clientResultChan
  284. if err != nil {
  285. return errors.Trace(err)
  286. }
  287. // Test: answer error
  288. go proxyFunc(proxyResultChan, proxyIP, geoIPData1, 10*time.Millisecond, nil, false)
  289. go clientFunc(clientResultChan, clientIP, geoIPData2, 10*time.Millisecond)
  290. err = <-proxyResultChan
  291. if err != nil {
  292. return errors.Trace(err)
  293. }
  294. err = <-clientResultChan
  295. if err == nil || !strings.HasSuffix(err.Error(), "no answer") {
  296. return errors.Tracef("unexpected result: %v", err)
  297. }
  298. // Test: client is gone
  299. waitBeforeAnswer := make(chan struct{})
  300. go proxyFunc(proxyResultChan, proxyIP, geoIPData1, 100*time.Millisecond, waitBeforeAnswer, true)
  301. go clientFunc(clientResultChan, clientIP, geoIPData2, 10*time.Millisecond)
  302. err = <-clientResultChan
  303. if err == nil || !strings.HasSuffix(err.Error(), "context deadline exceeded") {
  304. return errors.Tracef("unexpected result: %v", err)
  305. }
  306. close(waitBeforeAnswer)
  307. err = <-proxyResultChan
  308. if err == nil || !strings.HasSuffix(err.Error(), "no client") {
  309. return errors.Tracef("unexpected result: %v", err)
  310. }
  311. // Test: no compartment match
  312. compartment1 := &MatchProperties{
  313. GeoIPData: geoIPData1.GeoIPData,
  314. CommonCompartmentIDs: []ID{makeID()},
  315. PersonalCompartmentIDs: []ID{makeID()},
  316. }
  317. compartment2 := &MatchProperties{
  318. GeoIPData: geoIPData2.GeoIPData,
  319. CommonCompartmentIDs: []ID{makeID()},
  320. PersonalCompartmentIDs: []ID{makeID()},
  321. }
  322. go proxyFunc(proxyResultChan, proxyIP, compartment1, 10*time.Millisecond, nil, true)
  323. go clientFunc(clientResultChan, clientIP, compartment2, 10*time.Millisecond)
  324. err = <-proxyResultChan
  325. if err == nil || !strings.HasSuffix(err.Error(), "context deadline exceeded") {
  326. return errors.Tracef("unexpected result: %v", err)
  327. }
  328. err = <-clientResultChan
  329. if err == nil || !strings.HasSuffix(err.Error(), "context deadline exceeded") {
  330. return errors.Tracef("unexpected result: %v", err)
  331. }
  332. // Test: common compartment match
  333. compartment1And2 := &MatchProperties{
  334. GeoIPData: geoIPData2.GeoIPData,
  335. CommonCompartmentIDs: []ID{compartment1.CommonCompartmentIDs[0], compartment2.CommonCompartmentIDs[0]},
  336. }
  337. go proxyFunc(proxyResultChan, proxyIP, compartment1, 10*time.Millisecond, nil, true)
  338. go clientFunc(clientResultChan, clientIP, compartment1And2, 10*time.Millisecond)
  339. err = <-proxyResultChan
  340. if err != nil {
  341. return errors.Trace(err)
  342. }
  343. err = <-clientResultChan
  344. if err != nil {
  345. return errors.Trace(err)
  346. }
  347. // Test: personal compartment match
  348. compartment1And2 = &MatchProperties{
  349. GeoIPData: geoIPData2.GeoIPData,
  350. PersonalCompartmentIDs: []ID{compartment1.PersonalCompartmentIDs[0], compartment2.PersonalCompartmentIDs[0]},
  351. }
  352. go proxyFunc(proxyResultChan, proxyIP, compartment1, 10*time.Millisecond, nil, true)
  353. go clientFunc(clientResultChan, clientIP, compartment1And2, 10*time.Millisecond)
  354. err = <-proxyResultChan
  355. if err != nil {
  356. return errors.Trace(err)
  357. }
  358. err = <-clientResultChan
  359. if err != nil {
  360. return errors.Trace(err)
  361. }
  362. // Test: personal compartment preferred match
  363. compartment1Common := &MatchProperties{
  364. GeoIPData: geoIPData1.GeoIPData,
  365. CommonCompartmentIDs: []ID{compartment1.CommonCompartmentIDs[0]},
  366. }
  367. compartment1Personal := &MatchProperties{
  368. GeoIPData: geoIPData1.GeoIPData,
  369. PersonalCompartmentIDs: []ID{compartment1.PersonalCompartmentIDs[0]},
  370. }
  371. compartment1CommonAndPersonal := &MatchProperties{
  372. GeoIPData: geoIPData2.GeoIPData,
  373. CommonCompartmentIDs: []ID{compartment1.CommonCompartmentIDs[0]},
  374. PersonalCompartmentIDs: []ID{compartment1.PersonalCompartmentIDs[0]},
  375. }
  376. client1ResultChan := make(chan error)
  377. client2ResultChan := make(chan error)
  378. proxy1ResultChan := make(chan error)
  379. proxy2ResultChan := make(chan error)
  380. go proxyFunc(proxy1ResultChan, proxyIP, compartment1Common, 10*time.Millisecond, nil, true)
  381. go proxyFunc(proxy2ResultChan, proxyIP, compartment1Personal, 10*time.Millisecond, nil, true)
  382. time.Sleep(5 * time.Millisecond) // Hack to ensure both proxies are enqueued
  383. go clientFunc(client1ResultChan, clientIP, compartment1CommonAndPersonal, 10*time.Millisecond)
  384. err = <-proxy1ResultChan
  385. if err == nil || !strings.HasSuffix(err.Error(), "context deadline exceeded") {
  386. return errors.Tracef("unexpected result: %v", err)
  387. }
  388. // proxy2 should match since it has the preferred personal compartment ID
  389. err = <-proxy2ResultChan
  390. if err != nil {
  391. return errors.Trace(err)
  392. }
  393. err = <-client1ResultChan
  394. if err != nil {
  395. return errors.Trace(err)
  396. }
  397. // Test: no same-ASN match
  398. go proxyFunc(proxyResultChan, proxyIP, geoIPData1, 10*time.Millisecond, nil, true)
  399. go clientFunc(clientResultChan, clientIP, geoIPData1, 10*time.Millisecond)
  400. err = <-proxyResultChan
  401. if err == nil || !strings.HasSuffix(err.Error(), "context deadline exceeded") {
  402. return errors.Tracef("unexpected result: %v", err)
  403. }
  404. err = <-clientResultChan
  405. if err == nil || !strings.HasSuffix(err.Error(), "context deadline exceeded") {
  406. return errors.Tracef("unexpected result: %v", err)
  407. }
  408. // Test: proxy preferred NAT match
  409. client1Properties := &MatchProperties{
  410. GeoIPData: common.GeoIPData{Country: "C1", ASN: "A1"},
  411. NATType: NATTypeFullCone,
  412. CommonCompartmentIDs: basicCommonCompartmentIDs,
  413. }
  414. client2Properties := &MatchProperties{
  415. GeoIPData: common.GeoIPData{Country: "C2", ASN: "A2"},
  416. NATType: NATTypeSymmetric,
  417. CommonCompartmentIDs: basicCommonCompartmentIDs,
  418. }
  419. proxy1Properties := &MatchProperties{
  420. GeoIPData: common.GeoIPData{Country: "C3", ASN: "A3"},
  421. NATType: NATTypeNone,
  422. CommonCompartmentIDs: basicCommonCompartmentIDs,
  423. }
  424. proxy2Properties := &MatchProperties{
  425. GeoIPData: common.GeoIPData{Country: "C4", ASN: "A4"},
  426. NATType: NATTypeSymmetric,
  427. CommonCompartmentIDs: basicCommonCompartmentIDs,
  428. }
  429. go proxyFunc(proxy1ResultChan, proxyIP, proxy1Properties, 10*time.Millisecond, nil, true)
  430. go proxyFunc(proxy2ResultChan, proxyIP, proxy2Properties, 10*time.Millisecond, nil, true)
  431. time.Sleep(5 * time.Millisecond) // Hack to ensure both proxies are enqueued
  432. go clientFunc(client1ResultChan, clientIP, client1Properties, 10*time.Millisecond)
  433. err = <-proxy1ResultChan
  434. if err == nil || !strings.HasSuffix(err.Error(), "context deadline exceeded") {
  435. return errors.Tracef("unexpected result: %v", err)
  436. }
  437. // proxy2 should match since it's the preferred NAT match
  438. err = <-proxy2ResultChan
  439. if err != nil {
  440. return errors.Trace(err)
  441. }
  442. err = <-client1ResultChan
  443. if err != nil {
  444. return errors.Trace(err)
  445. }
  446. // Test: client preferred NAT match
  447. // Limitation: the current Matcher.matchAllOffers logic matches the first
  448. // enqueued client against the best proxy match, regardless of whether
  449. // there is another client in the queue that's a better match for that
  450. // proxy. As a result, this test only passes when the preferred matching
  451. // client is enqueued first, and the test is currently of limited utility.
  452. go clientFunc(client2ResultChan, clientIP, client2Properties, 20*time.Millisecond)
  453. time.Sleep(5 * time.Millisecond) // Hack to client is enqueued
  454. go clientFunc(client1ResultChan, clientIP, client1Properties, 20*time.Millisecond)
  455. time.Sleep(5 * time.Millisecond) // Hack to client is enqueued
  456. go proxyFunc(proxy1ResultChan, proxyIP, proxy1Properties, 20*time.Millisecond, nil, true)
  457. err = <-proxy1ResultChan
  458. if err != nil {
  459. return errors.Trace(err)
  460. }
  461. err = <-client1ResultChan
  462. if err == nil || !strings.HasSuffix(err.Error(), "context deadline exceeded") {
  463. return errors.Tracef("unexpected result: %v", err)
  464. }
  465. // client2 should match since it's the preferred NAT match
  466. err = <-client2ResultChan
  467. if err != nil {
  468. return errors.Trace(err)
  469. }
  470. // Test: many matches
  471. // Reduce test log noise for this phase of the test
  472. logger.SetLogLevelDebug(false)
  473. matchCount := 10000
  474. proxyCount := matchCount
  475. clientCount := matchCount
  476. // Buffered so no goroutine will block reporting result
  477. proxyResultChan = make(chan error, matchCount)
  478. clientResultChan = make(chan error, matchCount)
  479. for proxyCount > 0 || clientCount > 0 {
  480. // Don't simply alternate enqueuing a proxy and a client
  481. if proxyCount > 0 && (clientCount == 0 || prng.FlipCoin()) {
  482. go proxyFunc(proxyResultChan, randomIPAddress(), geoIPData1, 10*time.Second, nil, true)
  483. proxyCount -= 1
  484. } else if clientCount > 0 {
  485. go clientFunc(clientResultChan, randomIPAddress(), geoIPData2, 10*time.Second)
  486. clientCount -= 1
  487. }
  488. }
  489. for i := 0; i < matchCount; i++ {
  490. err = <-proxyResultChan
  491. if err != nil {
  492. return errors.Trace(err)
  493. }
  494. err = <-clientResultChan
  495. if err != nil {
  496. return errors.Trace(err)
  497. }
  498. }
  499. return nil
  500. }
  501. func randomIPAddress() string {
  502. return fmt.Sprintf("%d.%d.%d.%d",
  503. prng.Range(0, 255),
  504. prng.Range(0, 255),
  505. prng.Range(0, 255),
  506. prng.Range(0, 255))
  507. }