psinet.go 19 KB

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