geoip.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. /*
  2. * Copyright (c) 2016, 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 server
  20. import (
  21. "crypto/hmac"
  22. "crypto/sha256"
  23. "net"
  24. "time"
  25. cache "github.com/Psiphon-Inc/go-cache"
  26. maxminddb "github.com/Psiphon-Inc/maxminddb-golang"
  27. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
  28. )
  29. const (
  30. GEOIP_SESSION_CACHE_TTL = 60 * time.Minute
  31. GEOIP_UNKNOWN_VALUE = "None"
  32. )
  33. // GeoIPData is GeoIP data for a client session. Individual client
  34. // IP addresses are neither logged nor explicitly referenced during a session.
  35. // The GeoIP country, city, and ISP corresponding to a client IP address are
  36. // resolved and then logged along with usage stats. The DiscoveryValue is
  37. // a special value derived from the client IP that's used to compartmentalize
  38. // discoverable servers (see calculateDiscoveryValue for details).
  39. type GeoIPData struct {
  40. Country string
  41. City string
  42. ISP string
  43. DiscoveryValue int
  44. }
  45. // NewGeoIPData returns a GeoIPData initialized with the expected
  46. // GEOIP_UNKNOWN_VALUE values to be used when GeoIP lookup fails.
  47. func NewGeoIPData() GeoIPData {
  48. return GeoIPData{
  49. Country: GEOIP_UNKNOWN_VALUE,
  50. City: GEOIP_UNKNOWN_VALUE,
  51. ISP: GEOIP_UNKNOWN_VALUE,
  52. }
  53. }
  54. // GeoIPService implements GeoIP lookup and session/GeoIP caching.
  55. // Lookup is via a MaxMind database; the ReloadDatabase function
  56. // supports hot reloading of MaxMind data while the server is
  57. // running.
  58. type GeoIPService struct {
  59. databases []*geoIPDatabase
  60. sessionCache *cache.Cache
  61. discoveryValueHMACKey string
  62. }
  63. type geoIPDatabase struct {
  64. common.ReloadableFile
  65. maxMindReader *maxminddb.Reader
  66. }
  67. // NewGeoIPService initializes a new GeoIPService.
  68. func NewGeoIPService(
  69. databaseFilenames []string,
  70. discoveryValueHMACKey string) (*GeoIPService, error) {
  71. geoIP := &GeoIPService{
  72. databases: make([]*geoIPDatabase, len(databaseFilenames)),
  73. sessionCache: cache.New(GEOIP_SESSION_CACHE_TTL, 1*time.Minute),
  74. discoveryValueHMACKey: discoveryValueHMACKey,
  75. }
  76. for i, filename := range databaseFilenames {
  77. database := &geoIPDatabase{}
  78. database.ReloadableFile = common.NewReloadableFile(
  79. filename,
  80. func(fileContent []byte) error {
  81. maxMindReader, err := maxminddb.FromBytes(fileContent)
  82. if err != nil {
  83. // On error, database state remains the same
  84. return common.ContextError(err)
  85. }
  86. if database.maxMindReader != nil {
  87. database.maxMindReader.Close()
  88. }
  89. database.maxMindReader = maxMindReader
  90. return nil
  91. })
  92. _, err := database.Reload()
  93. if err != nil {
  94. return nil, common.ContextError(err)
  95. }
  96. geoIP.databases[i] = database
  97. }
  98. return geoIP, nil
  99. }
  100. // Reloaders gets the list of reloadable databases in use
  101. // by the GeoIPService. This list is used to hot reload
  102. // these databases.
  103. func (geoIP *GeoIPService) Reloaders() []common.Reloader {
  104. reloaders := make([]common.Reloader, len(geoIP.databases))
  105. for i, database := range geoIP.databases {
  106. reloaders[i] = database
  107. }
  108. return reloaders
  109. }
  110. // Lookup determines a GeoIPData for a given client IP address.
  111. func (geoIP *GeoIPService) Lookup(ipAddress string) GeoIPData {
  112. result := NewGeoIPData()
  113. ip := net.ParseIP(ipAddress)
  114. if ip == nil || len(geoIP.databases) == 0 {
  115. return result
  116. }
  117. var geoIPFields struct {
  118. Country struct {
  119. ISOCode string `maxminddb:"iso_code"`
  120. } `maxminddb:"country"`
  121. City struct {
  122. Names map[string]string `maxminddb:"names"`
  123. } `maxminddb:"city"`
  124. ISP string `maxminddb:"isp"`
  125. }
  126. // Each database will populate geoIPFields with the values it contains. In the
  127. // currnt MaxMind deployment, the City database populates Country and City and
  128. // the separate ISP database populates ISP.
  129. for _, database := range geoIP.databases {
  130. database.ReloadableFile.RLock()
  131. err := database.maxMindReader.Lookup(ip, &geoIPFields)
  132. database.ReloadableFile.RUnlock()
  133. if err != nil {
  134. log.WithContextFields(LogFields{"error": err}).Warning("GeoIP lookup failed")
  135. }
  136. }
  137. if geoIPFields.Country.ISOCode != "" {
  138. result.Country = geoIPFields.Country.ISOCode
  139. }
  140. name, ok := geoIPFields.City.Names["en"]
  141. if ok && name != "" {
  142. result.City = name
  143. }
  144. if geoIPFields.ISP != "" {
  145. result.ISP = geoIPFields.ISP
  146. }
  147. result.DiscoveryValue = calculateDiscoveryValue(
  148. geoIP.discoveryValueHMACKey, ipAddress)
  149. return result
  150. }
  151. // SetSessionCache adds the sessionID/geoIPData pair to the
  152. // session cache. This value will not expire; the caller must
  153. // call MarkSessionCacheToExpire to initiate expiry.
  154. // Calling SetSessionCache for an existing sessionID will
  155. // replace the previous value and reset any expiry.
  156. func (geoIP *GeoIPService) SetSessionCache(sessionID string, geoIPData GeoIPData) {
  157. geoIP.sessionCache.Set(sessionID, geoIPData, cache.NoExpiration)
  158. }
  159. // MarkSessionCacheToExpire initiates expiry for an existing
  160. // session cache entry, if the session ID is found in the cache.
  161. // Concurrency note: SetSessionCache and MarkSessionCacheToExpire
  162. // should not be called concurrently for a single session ID.
  163. func (geoIP *GeoIPService) MarkSessionCacheToExpire(sessionID string) {
  164. geoIPData, found := geoIP.sessionCache.Get(sessionID)
  165. // Note: potential race condition between Get and Set. In practice,
  166. // the tunnel server won't clobber a SetSessionCache value by calling
  167. // MarkSessionCacheToExpire concurrently.
  168. if found {
  169. geoIP.sessionCache.Set(sessionID, geoIPData, cache.DefaultExpiration)
  170. }
  171. }
  172. // GetSessionCache returns the cached GeoIPData for the
  173. // specified session ID; a blank GeoIPData is returned
  174. // if the session ID is not found in the cache.
  175. func (geoIP *GeoIPService) GetSessionCache(sessionID string) GeoIPData {
  176. geoIPData, found := geoIP.sessionCache.Get(sessionID)
  177. if !found {
  178. return NewGeoIPData()
  179. }
  180. return geoIPData.(GeoIPData)
  181. }
  182. // calculateDiscoveryValue derives a value from the client IP address to be
  183. // used as input in the server discovery algorithm. Since we do not explicitly
  184. // store the client IP address, we must derive the value here and store it for
  185. // later use by the discovery algorithm.
  186. // See https://bitbucket.org/psiphon/psiphon-circumvention-system/src/tip/Automation/psi_ops_discovery.py
  187. // for full details.
  188. func calculateDiscoveryValue(discoveryValueHMACKey, ipAddress string) int {
  189. // From: psi_ops_discovery.calculate_ip_address_strategy_value:
  190. // # Mix bits from all octets of the client IP address to determine the
  191. // # bucket. An HMAC is used to prevent pre-calculation of buckets for IPs.
  192. // return ord(hmac.new(HMAC_KEY, ip_address, hashlib.sha256).digest()[0])
  193. // TODO: use 3-octet algorithm?
  194. hash := hmac.New(sha256.New, []byte(discoveryValueHMACKey))
  195. hash.Write([]byte(ipAddress))
  196. return int(hash.Sum(nil)[0])
  197. }