classic.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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. //
  75. // Warning: if discoverServers is called as the set of discoverable servers
  76. // changes, i.e. a new server becomes un/discoverable, then there's a remote
  77. // possibility that discoverServers returns nil because of a race between
  78. // the timer that updates c.buckets firing and discoverServers obtaining a
  79. // reference to the value of c.buckets.
  80. func (c *classicDiscovery) discoverServers(discoveryValue int) []*psinet.DiscoveryServer {
  81. discoveryDate := c.clk.Now().UTC()
  82. c.RWMutex.RLock()
  83. buckets := c.buckets
  84. c.RWMutex.RUnlock()
  85. if len(buckets) == 0 {
  86. return nil
  87. }
  88. timeInSeconds := int(discoveryDate.Unix())
  89. // TODO: ensure that each server in buckets is discoverable on discoveryDate.
  90. servers := selectServers(buckets, timeInSeconds, discoveryValue, discoveryDate)
  91. return servers
  92. }
  93. // Combine client IP address and time-of-day strategies to give out different
  94. // discovery servers to different clients. The aim is to achieve defense against
  95. // enumerability. We also want to achieve a degree of load balancing clients
  96. // and these strategies are expected to have reasonably random distribution,
  97. // even for a cluster of users coming from the same network.
  98. //
  99. // We only select one server: multiple results makes enumeration easier; the
  100. // strategies have a built-in load balancing effect; and date range discoverability
  101. // means a client will actually learn more servers later even if they happen to
  102. // always pick the same result at this point.
  103. //
  104. // This is a blended strategy: as long as there are enough servers to pick from,
  105. // both aspects determine which server is selected. IP address is given the
  106. // priority: if there are only a couple of servers, for example, IP address alone
  107. // determines the outcome.
  108. //
  109. // Warning: If discoveryDate does not fall within the discovery date range of the
  110. // selected server, then nil will be returned. For this reason, an attempt should
  111. // be made to ensure that buckets only contains discovery servers that are
  112. // discoverable on discoveryDate.
  113. func selectServers(
  114. buckets [][]*psinet.DiscoveryServer,
  115. timeInSeconds,
  116. discoveryValue int,
  117. discoveryDate time.Time) []*psinet.DiscoveryServer {
  118. TIME_GRANULARITY := 3600
  119. // Time truncated to an hour
  120. timeStrategyValue := timeInSeconds / TIME_GRANULARITY
  121. // NOTE: this code assumes that the range of possible timeStrategyValues
  122. // and discoveryValues are sufficient to index to all bucket items.
  123. if len(buckets) == 0 {
  124. return nil
  125. }
  126. bucket := buckets[discoveryValue%len(buckets)]
  127. if len(bucket) == 0 {
  128. return nil
  129. }
  130. server := bucket[timeStrategyValue%len(bucket)]
  131. // Double check that server is discoverable at this time.
  132. if discoveryDate.Before(server.DiscoveryDateRange[0]) ||
  133. !discoveryDate.Before(server.DiscoveryDateRange[1]) {
  134. return nil
  135. }
  136. serverList := make([]*psinet.DiscoveryServer, 1)
  137. serverList[0] = server
  138. return serverList
  139. }
  140. // Number of buckets such that first strategy picks among about the same number
  141. // of choices as the second strategy. Gives an edge to the "outer" strategy.
  142. func calculateBucketCount(length int) int {
  143. return int(math.Ceil(math.Sqrt(float64(length))))
  144. }
  145. // bucketizeServerList creates nearly equal sized slices of the input list.
  146. func bucketizeServerList(servers []*psinet.DiscoveryServer, bucketCount int) [][]*psinet.DiscoveryServer {
  147. // This code creates the same partitions as legacy servers:
  148. // https://github.com/Psiphon-Inc/psiphon-automation/blob/685f91a85bcdb33a75a200d936eadcb0686eadd7/Automation/psi_ops_discovery.py
  149. //
  150. // Both use the same algorithm from:
  151. // http://stackoverflow.com/questions/2659900/python-slicing-a-list-into-n-nearly-equal-length-partitions
  152. buckets := make([][]*psinet.DiscoveryServer, bucketCount)
  153. division := float64(len(servers)) / float64(bucketCount)
  154. for i := 0; i < bucketCount; i++ {
  155. start := int((division * float64(i)) + 0.5)
  156. end := int((division * (float64(i) + 1)) + 0.5)
  157. buckets[i] = servers[start:end]
  158. }
  159. return buckets
  160. }