classic.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. /*
  2. * Copyright (c) 2024, 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 discovery
  20. import (
  21. "crypto/hmac"
  22. "crypto/sha256"
  23. "math"
  24. "net"
  25. "sync"
  26. "time"
  27. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server/psinet"
  28. )
  29. type classicDiscovery struct {
  30. clk clock
  31. buckets [][]*psinet.DiscoveryServer
  32. discoveryValueHMACKey string
  33. sync.RWMutex
  34. }
  35. func NewClassicDiscovery(discoveryValueHMACKey string) (*classicDiscovery, error) {
  36. return newClassicDiscovery(discoveryValueHMACKey, realClock{})
  37. }
  38. func newClassicDiscovery(discoveryValueHMACKey string, clk clock) (*classicDiscovery, error) {
  39. return &classicDiscovery{
  40. clk: clk,
  41. discoveryValueHMACKey: discoveryValueHMACKey,
  42. }, nil
  43. }
  44. func (c *classicDiscovery) serversChanged(servers []*psinet.DiscoveryServer) {
  45. var buckets [][]*psinet.DiscoveryServer
  46. if len(servers) != 0 {
  47. // Divide servers into buckets. The bucket count is chosen such that the number
  48. // of buckets and the number of items in each bucket are close (using sqrt).
  49. // IP address selects the bucket, time selects the item in the bucket.
  50. bucketCount := calculateBucketCount(len(servers))
  51. buckets = bucketizeServerList(servers, bucketCount)
  52. }
  53. c.RWMutex.Lock()
  54. c.buckets = buckets
  55. c.RWMutex.Unlock()
  56. }
  57. func calculateDiscoveryValue(discoveryValueHMACKey string, ipAddress net.IP) int {
  58. // From: psi_ops_discovery.calculate_ip_address_strategy_value:
  59. // # Mix bits from all octets of the client IP address to determine the
  60. // # bucket. An HMAC is used to prevent pre-calculation of buckets for IPs.
  61. // return ord(hmac.new(HMAC_KEY, ip_address, hashlib.sha256).digest()[0])
  62. // TODO: use 3-octet algorithm?
  63. hash := hmac.New(sha256.New, []byte(discoveryValueHMACKey))
  64. hash.Write([]byte(ipAddress.String()))
  65. return int(hash.Sum(nil)[0])
  66. }
  67. func (c *classicDiscovery) selectServers(clientIP net.IP) []*psinet.DiscoveryServer {
  68. discoveryValue := calculateDiscoveryValue(c.discoveryValueHMACKey, clientIP)
  69. return c.discoverServers(discoveryValue)
  70. }
  71. // discoverServers selects new encoded server entries to be "discovered" by
  72. // the client, using the discoveryValue -- a function of the client's IP
  73. // address -- as the input into the discovery algorithm.
  74. func (c *classicDiscovery) discoverServers(discoveryValue int) []*psinet.DiscoveryServer {
  75. discoveryDate := c.clk.Now().UTC()
  76. c.RWMutex.RLock()
  77. buckets := c.buckets
  78. c.RWMutex.RUnlock()
  79. if len(buckets) == 0 {
  80. return nil
  81. }
  82. timeInSeconds := int(discoveryDate.Unix())
  83. servers := selectServers(buckets, timeInSeconds, discoveryValue, discoveryDate)
  84. return servers
  85. }
  86. // Combine client IP address and time-of-day strategies to give out different
  87. // discovery servers to different clients. The aim is to achieve defense against
  88. // enumerability. We also want to achieve a degree of load balancing clients
  89. // and these strategies are expected to have reasonably random distribution,
  90. // even for a cluster of users coming from the same network.
  91. //
  92. // We only select one server: multiple results makes enumeration easier; the
  93. // strategies have a built-in load balancing effect; and date range discoverability
  94. // means a client will actually learn more servers later even if they happen to
  95. // always pick the same result at this point.
  96. //
  97. // This is a blended strategy: as long as there are enough servers to pick from,
  98. // both aspects determine which server is selected. IP address is given the
  99. // priority: if there are only a couple of servers, for example, IP address alone
  100. // determines the outcome.
  101. func selectServers(
  102. buckets [][]*psinet.DiscoveryServer,
  103. timeInSeconds,
  104. discoveryValue int,
  105. discoveryDate time.Time) []*psinet.DiscoveryServer {
  106. TIME_GRANULARITY := 3600
  107. // Time truncated to an hour
  108. timeStrategyValue := timeInSeconds / TIME_GRANULARITY
  109. // NOTE: this code assumes that the range of possible timeStrategyValues
  110. // and discoveryValues are sufficient to index to all bucket items.
  111. if len(buckets) == 0 {
  112. return nil
  113. }
  114. bucket := buckets[discoveryValue%len(buckets)]
  115. if len(bucket) == 0 {
  116. return nil
  117. }
  118. server := bucket[timeStrategyValue%len(bucket)]
  119. // Double check that server is discoverable at this time.
  120. if discoveryDate.Before(server.DiscoveryDateRange[0]) ||
  121. !discoveryDate.Before(server.DiscoveryDateRange[1]) {
  122. return nil
  123. }
  124. serverList := make([]*psinet.DiscoveryServer, 1)
  125. serverList[0] = server
  126. return serverList
  127. }
  128. // Number of buckets such that first strategy picks among about the same number
  129. // of choices as the second strategy. Gives an edge to the "outer" strategy.
  130. func calculateBucketCount(length int) int {
  131. return int(math.Ceil(math.Sqrt(float64(length))))
  132. }
  133. // bucketizeServerList creates nearly equal sized slices of the input list.
  134. func bucketizeServerList(servers []*psinet.DiscoveryServer, bucketCount int) [][]*psinet.DiscoveryServer {
  135. // This code creates the same partitions as legacy servers:
  136. // https://github.com/Psiphon-Inc/psiphon-automation/blob/685f91a85bcdb33a75a200d936eadcb0686eadd7/Automation/psi_ops_discovery.py
  137. //
  138. // Both use the same algorithm from:
  139. // http://stackoverflow.com/questions/2659900/python-slicing-a-list-into-n-nearly-equal-length-partitions
  140. buckets := make([][]*psinet.DiscoveryServer, bucketCount)
  141. division := float64(len(servers)) / float64(bucketCount)
  142. for i := 0; i < bucketCount; i++ {
  143. start := int((division * float64(i)) + 0.5)
  144. end := int((division * (float64(i) + 1)) + 0.5)
  145. buckets[i] = servers[start:end]
  146. }
  147. return buckets
  148. }