psinet.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  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 psiphon/server/psinet implements psinet database services. The psinet
  20. // database is a JSON-format file containing information about the Psiphon network,
  21. // including sponsors, home pages, stats regexes, available upgrades, and other
  22. // servers for discovery. This package also implements the Psiphon discovery algorithm.
  23. package psinet
  24. import (
  25. "encoding/hex"
  26. "encoding/json"
  27. "fmt"
  28. "io/ioutil"
  29. "math"
  30. "math/rand"
  31. "strconv"
  32. "strings"
  33. "time"
  34. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
  35. )
  36. // Database serves Psiphon API data requests. It's safe for
  37. // concurrent usage. The Reload function supports hot reloading
  38. // of Psiphon network data while the server is running.
  39. type Database struct {
  40. common.ReloadableFile
  41. AlternateMeekFrontingAddresses map[string][]string `json:"alternate_meek_fronting_addresses"`
  42. AlternateMeekFrontingAddressesRegex map[string]string `json:"alternate_meek_fronting_addresses_regex"`
  43. Hosts map[string]Host `json:"hosts"`
  44. MeekFrontingDisableSNI map[string]bool `json:"meek_fronting_disable_SNI"`
  45. Servers []Server `json:"servers"`
  46. Sponsors map[string]Sponsor `json:"sponsors"`
  47. Versions map[string][]ClientVersion `json:"client_versions"`
  48. }
  49. type Host struct {
  50. AlternateMeekServerFrontingHosts []string `json:"alternate_meek_server_fronting_hosts"`
  51. DatacenterName string `json:"datacenter_name"`
  52. Id string `json:"id"`
  53. IpAddress string `json:"ip_address"`
  54. MeekCookieEncryptionPublicKey string `json:"meek_cookie_encryption_public_key"`
  55. MeekServerFrontingDomain string `json:"meek_server_fronting_domain"`
  56. MeekServerFrontingHost string `json:"meek_server_fronting_host"`
  57. MeekServerObfuscatedKey string `json:"meek_server_obfuscated_key"`
  58. MeekServerPort int `json:"meek_server_port"`
  59. Region string `json:"region"`
  60. }
  61. type Server struct {
  62. AlternateSshObfuscatedPorts []string `json:"alternate_ssh_obfuscated_ports"`
  63. Capabilities map[string]bool `json:"capabilities"`
  64. DiscoveryDateRange []string `json:"discovery_date_range"`
  65. EgressIpAddress string `json:"egress_ip_address"`
  66. HostId string `json:"host_id"`
  67. Id string `json:"id"`
  68. InternalIpAddress string `json:"internal_ip_address"`
  69. IpAddress string `json:"ip_address"`
  70. IsEmbedded bool `json:"is_embedded"`
  71. IsPermanent bool `json:"is_permanent"`
  72. PropogationChannelId string `json:"propagation_channel_id"`
  73. SshHostKey string `json:"ssh_host_key"`
  74. SshObfuscatedKey string `json:"ssh_obfuscated_key"`
  75. SshObfuscatedPort int `json:"ssh_obfuscated_port"`
  76. SshPassword string `json:"ssh_password"`
  77. SshPort string `json:"ssh_port"`
  78. SshUsername string `json:"ssh_username"`
  79. WebServerCertificate string `json:"web_server_certificate"`
  80. WebServerPort string `json:"web_server_port"`
  81. WebServerSecret string `json:"web_server_secret"`
  82. }
  83. type Sponsor struct {
  84. Banner string
  85. HomePages map[string][]HomePage `json:"home_pages"`
  86. HttpsRequestRegexes []HttpsRequestRegex `json:"https_request_regexes"`
  87. Id string `json:"id"`
  88. MobileHomePages map[string][]HomePage `json:"mobile_home_pages"`
  89. Name string `json:"name"`
  90. PageViewRegexes []PageViewRegex `json:"page_view_regexes"`
  91. WebsiteBanner string `json:"website_banner"`
  92. WebsiteBannerLink string `json:"website_banner_link"`
  93. }
  94. type ClientVersion struct {
  95. Version string `json:"version"`
  96. }
  97. type HomePage struct {
  98. Region string `json:"region"`
  99. Url string `json:"url"`
  100. }
  101. type HttpsRequestRegex struct {
  102. Regex string `json:"regex"`
  103. Replace string `json:"replace"`
  104. }
  105. type MobileHomePage struct {
  106. Region string `json:"region"`
  107. Url string `json:"url"`
  108. }
  109. type PageViewRegex struct {
  110. Regex string `json:"regex"`
  111. Replace string `json:"replace"`
  112. }
  113. // NewDatabase initializes a Database, calling Reload on the specified
  114. // filename.
  115. func NewDatabase(filename string) (*Database, error) {
  116. database := &Database{}
  117. database.ReloadableFile = common.NewReloadableFile(
  118. filename,
  119. func(filename string) error {
  120. psinetJSON, err := ioutil.ReadFile(filename)
  121. if err != nil {
  122. // On error, state remains the same
  123. return common.ContextError(err)
  124. }
  125. err = json.Unmarshal(psinetJSON, &database)
  126. if err != nil {
  127. // On error, state remains the same
  128. // (Unmarshal first validates the provided
  129. // JOSN and then populates the interface)
  130. return common.ContextError(err)
  131. }
  132. return nil
  133. })
  134. _, err := database.Reload()
  135. if err != nil {
  136. return nil, common.ContextError(err)
  137. }
  138. return database, nil
  139. }
  140. // GetHomepages returns a list of home pages for the specified sponsor,
  141. // region, and platform.
  142. func (db *Database) GetHomepages(sponsorID, clientRegion string, isMobilePlatform bool) []string {
  143. db.ReloadableFile.RLock()
  144. defer db.ReloadableFile.RUnlock()
  145. sponsorHomePages := make([]string, 0)
  146. // Sponsor id does not exist: fail gracefully
  147. sponsor, ok := db.Sponsors[sponsorID]
  148. if !ok {
  149. return nil
  150. }
  151. homePages := sponsor.HomePages
  152. if isMobilePlatform {
  153. if sponsor.MobileHomePages != nil {
  154. homePages = sponsor.MobileHomePages
  155. }
  156. }
  157. // Case: lookup succeeded and corresponding homepages found for region
  158. homePagesByRegion, ok := homePages[clientRegion]
  159. if ok {
  160. for _, homePage := range homePagesByRegion {
  161. sponsorHomePages = append(sponsorHomePages, strings.Replace(homePage.Url, "client_region=XX", "client_region="+clientRegion, 1))
  162. }
  163. }
  164. // Case: lookup failed or no corresponding homepages found for region --> use default
  165. if sponsorHomePages == nil {
  166. defaultHomePages, ok := homePages["None"]
  167. if ok {
  168. for _, homePage := range defaultHomePages {
  169. // client_region query parameter substitution
  170. sponsorHomePages = append(sponsorHomePages, strings.Replace(homePage.Url, "client_region=XX", "client_region="+clientRegion, 1))
  171. }
  172. }
  173. }
  174. return sponsorHomePages
  175. }
  176. // GetUpgradeClientVersion returns a new client version when an upgrade is
  177. // indicated for the specified client current version. The result is "" when
  178. // no upgrade is available. Caller should normalize clientPlatform.
  179. func (db *Database) GetUpgradeClientVersion(clientVersion, clientPlatform string) string {
  180. db.ReloadableFile.RLock()
  181. defer db.ReloadableFile.RUnlock()
  182. // Check lastest version number against client version number
  183. clientVersions, ok := db.Versions[clientPlatform]
  184. if !ok {
  185. return ""
  186. }
  187. if len(clientVersions) == 0 {
  188. return ""
  189. }
  190. // NOTE: Assumes versions list is in ascending version order
  191. lastVersion := clientVersions[len(clientVersions)-1].Version
  192. lastVersionInt, err := strconv.Atoi(lastVersion)
  193. if err != nil {
  194. return ""
  195. }
  196. clientVersionInt, err := strconv.Atoi(clientVersion)
  197. if err != nil {
  198. return ""
  199. }
  200. // Return latest version if upgrade needed
  201. if lastVersionInt > clientVersionInt {
  202. return lastVersion
  203. }
  204. return ""
  205. }
  206. // GetHttpsRequestRegexes returns bytes transferred stats regexes for the
  207. // specified sponsor. The result is nil when an unknown sponsorID is provided.
  208. func (db *Database) GetHttpsRequestRegexes(sponsorID string) []map[string]string {
  209. db.ReloadableFile.RLock()
  210. defer db.ReloadableFile.RUnlock()
  211. regexes := make([]map[string]string, 0)
  212. for i := range db.Sponsors[sponsorID].HttpsRequestRegexes {
  213. regex := make(map[string]string)
  214. regex["replace"] = db.Sponsors[sponsorID].HttpsRequestRegexes[i].Replace
  215. regex["regex"] = db.Sponsors[sponsorID].HttpsRequestRegexes[i].Regex
  216. regexes = append(regexes, regex)
  217. }
  218. return regexes
  219. }
  220. // DiscoverServers selects new encoded server entries to be "discovered" by
  221. // the client, using the discoveryValue as the input into the discovery algorithm.
  222. // The server list (db.Servers) loaded from JSON is stored as an array instead of
  223. // a map to ensure servers are discovered deterministically. Each iteration over a
  224. // map in go is seeded with a random value which causes non-deterministic ordering.
  225. func (db *Database) DiscoverServers(discoveryValue int) []string {
  226. db.ReloadableFile.RLock()
  227. defer db.ReloadableFile.RUnlock()
  228. var servers []Server
  229. discoveryDate := time.Now().UTC()
  230. candidateServers := make([]Server, 0)
  231. for _, server := range db.Servers {
  232. var start time.Time
  233. var end time.Time
  234. var err error
  235. // All servers that are discoverable on this day are eligable for discovery
  236. if len(server.DiscoveryDateRange) != 0 {
  237. start, err = time.Parse("2006-01-02T15:04:05", server.DiscoveryDateRange[0])
  238. if err != nil {
  239. continue
  240. }
  241. end, err = time.Parse("2006-01-02T15:04:05", server.DiscoveryDateRange[1])
  242. if err != nil {
  243. continue
  244. }
  245. if discoveryDate.After(start) && discoveryDate.Before(end) {
  246. candidateServers = append(candidateServers, server)
  247. }
  248. }
  249. }
  250. servers = selectServers(candidateServers, discoveryValue)
  251. encodedServerEntries := make([]string, 0)
  252. for _, server := range servers {
  253. encodedServerEntries = append(encodedServerEntries, db.getEncodedServerEntry(server))
  254. }
  255. return encodedServerEntries
  256. }
  257. // Combine client IP address and time-of-day strategies to give out different
  258. // discovery servers to different clients. The aim is to achieve defense against
  259. // enumerability. We also want to achieve a degree of load balancing clients
  260. // and these strategies are expected to have reasonably random distribution,
  261. // even for a cluster of users coming from the same network.
  262. //
  263. // We only select one server: multiple results makes enumeration easier; the
  264. // strategies have a built-in load balancing effect; and date range discoverability
  265. // means a client will actually learn more servers later even if they happen to
  266. // always pick the same result at this point.
  267. //
  268. // This is a blended strategy: as long as there are enough servers to pick from,
  269. // both aspects determine which server is selected. IP address is given the
  270. // priority: if there are only a couple of servers, for example, IP address alone
  271. // determines the outcome.
  272. func selectServers(servers []Server, discoveryValue int) []Server {
  273. TIME_GRANULARITY := 3600
  274. if len(servers) == 0 {
  275. return nil
  276. }
  277. // Current time truncated to an hour
  278. timeInSeconds := int(time.Now().Unix())
  279. timeStrategyValue := timeInSeconds / TIME_GRANULARITY
  280. // Divide servers into buckets. The bucket count is chosen such that the number
  281. // of buckets and the number of items in each bucket are close (using sqrt).
  282. // IP address selects the bucket, time selects the item in the bucket.
  283. // NOTE: this code assumes that the range of possible timeStrategyValues
  284. // and discoveryValues are sufficient to index to all bucket items.
  285. bucketCount := calculateBucketCount(len(servers))
  286. buckets := bucketizeServerList(servers, bucketCount)
  287. bucket := buckets[discoveryValue%len(buckets)]
  288. server := bucket[timeStrategyValue%len(bucket)]
  289. serverList := make([]Server, 1)
  290. serverList[0] = server
  291. return serverList
  292. }
  293. // Number of buckets such that first strategy picks among about the same number
  294. // of choices as the second strategy. Gives an edge to the "outer" strategy.
  295. func calculateBucketCount(length int) int {
  296. return int(math.Ceil(math.Sqrt(float64(length))))
  297. }
  298. // Create bucketCount buckets.
  299. // Each bucket will be of size division or divison-1.
  300. func bucketizeServerList(servers []Server, bucketCount int) [][]Server {
  301. division := float64(len(servers)) / float64(bucketCount)
  302. buckets := make([][]Server, bucketCount)
  303. var currentBucketIndex int = 0
  304. var serverIndex int = 0
  305. for _, server := range servers {
  306. bucketEndIndex := int(math.Floor(division * (float64(currentBucketIndex) + 1)))
  307. buckets[currentBucketIndex] = append(buckets[currentBucketIndex], server)
  308. serverIndex++
  309. if serverIndex > bucketEndIndex {
  310. currentBucketIndex++
  311. }
  312. }
  313. return buckets
  314. }
  315. // Return hex encoded server entry string for comsumption by client.
  316. // Newer clients ignore the legacy fields and only utilize the extended (new) config.
  317. func (db *Database) getEncodedServerEntry(server Server) string {
  318. // Double-check that we're not giving our blank server credentials
  319. if len(server.IpAddress) <= 1 || len(server.WebServerPort) <= 1 || len(server.WebServerSecret) <= 1 || len(server.WebServerCertificate) <= 1 {
  320. return ""
  321. }
  322. // Extended (new) entry fields are in a JSON string
  323. var extendedConfig struct {
  324. IpAddress string
  325. WebServerPort string
  326. WebServerSecret string
  327. WebServerCertificate string
  328. SshPort int
  329. SshUsername string
  330. SshPassword string
  331. SshHostKey string
  332. SshObfuscatedPort int
  333. SshObfuscatedKey string
  334. Region string
  335. MeekServerPort int
  336. MeekObfuscatedKey string
  337. MeekFrontingDomain string
  338. MeekFrontingHost string
  339. MeekCookieEncryptionPublicKey string
  340. meekFrontingAddresses []string
  341. meekFrontingAddressesRegex string
  342. meekFrontingDisableSNI bool
  343. meekFrontingHosts []string
  344. capabilities []string
  345. }
  346. // NOTE: also putting original values in extended config for easier parsing by new clients
  347. extendedConfig.IpAddress = server.IpAddress
  348. extendedConfig.WebServerPort = server.WebServerPort
  349. extendedConfig.WebServerSecret = server.WebServerSecret
  350. extendedConfig.WebServerCertificate = server.WebServerCertificate
  351. sshPort, err := strconv.Atoi(server.SshPort)
  352. if err != nil {
  353. extendedConfig.SshPort = 0
  354. } else {
  355. extendedConfig.SshPort = sshPort
  356. }
  357. extendedConfig.SshUsername = server.SshUsername
  358. extendedConfig.SshPassword = server.SshPassword
  359. sshHostKeyType, sshHostKey := parseSshKeyString(server.SshHostKey)
  360. if strings.Compare(sshHostKeyType, "ssh-rsa") == 0 {
  361. extendedConfig.SshHostKey = sshHostKey
  362. } else {
  363. extendedConfig.SshHostKey = ""
  364. }
  365. extendedConfig.SshObfuscatedPort = server.SshObfuscatedPort
  366. // Use the latest alternate port unless tunneling through meek
  367. if len(server.AlternateSshObfuscatedPorts) > 0 && !(server.Capabilities["FRONTED-MEEK"] || server.Capabilities["UNFRONTED-MEEK"]) {
  368. port, err := strconv.Atoi(server.AlternateSshObfuscatedPorts[len(server.AlternateSshObfuscatedPorts)-1])
  369. if err == nil {
  370. extendedConfig.SshObfuscatedPort = port
  371. }
  372. }
  373. extendedConfig.SshObfuscatedKey = server.SshObfuscatedKey
  374. host := db.Hosts[server.HostId]
  375. extendedConfig.Region = host.Region
  376. extendedConfig.MeekServerPort = host.MeekServerPort
  377. extendedConfig.MeekObfuscatedKey = host.MeekServerObfuscatedKey
  378. extendedConfig.MeekFrontingDomain = host.MeekServerFrontingDomain
  379. extendedConfig.MeekFrontingHost = host.MeekServerFrontingHost
  380. extendedConfig.MeekCookieEncryptionPublicKey = host.MeekCookieEncryptionPublicKey
  381. serverCapabilities := make(map[string]bool, 0)
  382. for capability, enabled := range server.Capabilities {
  383. serverCapabilities[capability] = enabled
  384. }
  385. if serverCapabilities["UNFRONTED-MEEK"] && host.MeekServerPort == 443 {
  386. serverCapabilities["UNFRONTED-MEEK"] = false
  387. serverCapabilities["UNFRONTED-MEEK-HTTPS"] = true
  388. }
  389. if host.MeekServerFrontingDomain != "" {
  390. alternateMeekFrontingAddresses := db.AlternateMeekFrontingAddresses[host.MeekServerFrontingDomain]
  391. if len(alternateMeekFrontingAddresses) > 0 {
  392. // Choose 3 addresses randomly
  393. perm := rand.Perm(len(alternateMeekFrontingAddresses))[:int(math.Min(float64(len(alternateMeekFrontingAddresses)), float64(3)))]
  394. for i := range perm {
  395. extendedConfig.meekFrontingAddresses = append(extendedConfig.meekFrontingAddresses, alternateMeekFrontingAddresses[perm[i]])
  396. }
  397. }
  398. extendedConfig.meekFrontingAddressesRegex = db.AlternateMeekFrontingAddressesRegex[host.MeekServerFrontingDomain]
  399. extendedConfig.meekFrontingDisableSNI = db.MeekFrontingDisableSNI[host.MeekServerFrontingDomain]
  400. }
  401. if host.AlternateMeekServerFrontingHosts != nil {
  402. // Choose 3 addresses randomly
  403. perm := rand.Perm(len(host.AlternateMeekServerFrontingHosts))[:int(math.Min(float64(len(host.AlternateMeekServerFrontingHosts)), float64(3)))]
  404. for i := range perm {
  405. extendedConfig.meekFrontingHosts = append(extendedConfig.meekFrontingHosts, host.AlternateMeekServerFrontingHosts[i])
  406. }
  407. if serverCapabilities["FRONTED-MEEK"] == true {
  408. serverCapabilities["FRONTED-MEEK-HTTP"] = true
  409. }
  410. }
  411. for capability, enabled := range serverCapabilities {
  412. if enabled == true {
  413. extendedConfig.capabilities = append(extendedConfig.capabilities, capability)
  414. }
  415. }
  416. jsonDump, err := json.Marshal(extendedConfig)
  417. if err != nil {
  418. return ""
  419. }
  420. // Legacy format + extended (new) config
  421. prefixString := fmt.Sprintf("%s %s %s %s ", server.IpAddress, server.WebServerPort, server.WebServerSecret, server.WebServerCertificate)
  422. return hex.EncodeToString(append([]byte(prefixString)[:], []byte(jsonDump)[:]...))
  423. }
  424. // Parse string of format "ssh-key-type ssh-key".
  425. func parseSshKeyString(sshKeyString string) (keyType string, key string) {
  426. sshKeyArr := strings.Split(sshKeyString, " ")
  427. if len(sshKeyArr) != 2 {
  428. return "", ""
  429. }
  430. return sshKeyArr[0], sshKeyArr[1]
  431. }