geoip.go 6.1 KB

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