geoip.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  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(filename string) error {
  81. maxMindReader, err := maxminddb.Open(filename)
  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. func (geoIP *GeoIPService) SetSessionCache(sessionID string, geoIPData GeoIPData) {
  152. geoIP.sessionCache.Set(sessionID, geoIPData, cache.DefaultExpiration)
  153. }
  154. func (geoIP *GeoIPService) GetSessionCache(
  155. sessionID string) GeoIPData {
  156. geoIPData, found := geoIP.sessionCache.Get(sessionID)
  157. if !found {
  158. return NewGeoIPData()
  159. }
  160. return geoIPData.(GeoIPData)
  161. }
  162. // calculateDiscoveryValue derives a value from the client IP address to be
  163. // used as input in the server discovery algorithm. Since we do not explicitly
  164. // store the client IP address, we must derive the value here and store it for
  165. // later use by the discovery algorithm.
  166. // See https://bitbucket.org/psiphon/psiphon-circumvention-system/src/tip/Automation/psi_ops_discovery.py
  167. // for full details.
  168. func calculateDiscoveryValue(discoveryValueHMACKey, ipAddress string) int {
  169. // From: psi_ops_discovery.calculate_ip_address_strategy_value:
  170. // # Mix bits from all octets of the client IP address to determine the
  171. // # bucket. An HMAC is used to prevent pre-calculation of buckets for IPs.
  172. // return ord(hmac.new(HMAC_KEY, ip_address, hashlib.sha256).digest()[0])
  173. // TODO: use 3-octet algorithm?
  174. hash := hmac.New(sha256.New, []byte(discoveryValueHMACKey))
  175. hash.Write([]byte(ipAddress))
  176. return int(hash.Sum(nil)[0])
  177. }