psinet.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  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. "math"
  29. "math/rand"
  30. "os"
  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. file, err := os.Open(filename)
  138. if err != nil {
  139. return err
  140. }
  141. defer file.Close()
  142. err = json.NewDecoder(file).Decode(db)
  143. return err
  144. }
  145. // GetHomepages returns a list of home pages for the specified sponsor,
  146. // region, and platform.
  147. func (db *Database) GetHomepages(sponsorID, clientRegion, clientPlatform string) []string {
  148. db.RLock()
  149. defer db.RUnlock()
  150. sponsorHomePages := make([]string, 0)
  151. // Sponsor id does not exist: fail gracefully
  152. sponsor, ok := db.Sponsors[sponsorID]
  153. if !ok {
  154. return nil
  155. }
  156. homePages := sponsor.HomePages
  157. if getClientPlatform(clientPlatform) == CLIENT_PLATFORM_ANDROID {
  158. if sponsor.MobileHomePages != nil {
  159. homePages = sponsor.MobileHomePages
  160. }
  161. }
  162. // Case: lookup succeeded and corresponding homepages found for region
  163. homePagesByRegion, ok := homePages[clientRegion]
  164. if ok {
  165. for _, homePage := range homePagesByRegion {
  166. sponsorHomePages = append(sponsorHomePages, strings.Replace(homePage.Url, "client_region=XX", "client_region="+clientRegion, 1))
  167. }
  168. }
  169. // Case: lookup failed or no corresponding homepages found for region --> use default
  170. if sponsorHomePages == nil {
  171. defaultHomePages, ok := homePages["None"]
  172. if ok {
  173. for _, homePage := range defaultHomePages {
  174. // client_region query parameter substitution
  175. sponsorHomePages = append(sponsorHomePages, strings.Replace(homePage.Url, "client_region=XX", "client_region="+clientRegion, 1))
  176. }
  177. }
  178. }
  179. return sponsorHomePages
  180. }
  181. // GetUpgradeClientVersion returns a new client version when an upgrade is
  182. // indicated for the specified client current version. The result is "" when
  183. // no upgrade is available.
  184. func (db *Database) GetUpgradeClientVersion(clientVersion, clientPlatform string) string {
  185. db.RLock()
  186. defer db.RUnlock()
  187. // Check lastest version number against client version number
  188. platform := getClientPlatform(clientPlatform)
  189. // If no versions exist for this platform
  190. clientVersions, ok := db.Versions[platform]
  191. if !ok {
  192. return ""
  193. }
  194. // NOTE: Assumes versions list is in ascending version order
  195. lastVersion := clientVersions[len(clientVersions)-1].Version
  196. lastVersionInt, err := strconv.Atoi(lastVersion)
  197. if err != nil {
  198. return ""
  199. }
  200. clientVersionInt, err := strconv.Atoi(clientVersion)
  201. if err != nil {
  202. return ""
  203. }
  204. // Return latest version if upgrade needed
  205. if lastVersionInt > clientVersionInt {
  206. return lastVersion
  207. }
  208. return ""
  209. }
  210. // GetHttpsRequestRegexes returns bytes transferred stats regexes for the
  211. // specified sponsor. The result is nil when an unknown sponsorID is provided.
  212. func (db *Database) GetHttpsRequestRegexes(sponsorID string) []HttpsRequestRegex {
  213. db.RLock()
  214. defer db.RUnlock()
  215. return db.Sponsors[sponsorID].HttpsRequestRegexes
  216. }
  217. // DiscoverServers selects new encoded server entries to be "discovered" by
  218. // the client, using the discoveryValue as the input into the discovery algorithm.
  219. func (db *Database) DiscoverServers(discoveryValue int) []string {
  220. db.RLock()
  221. defer db.RUnlock()
  222. var servers []Server
  223. discoveryDate := time.Now().UTC()
  224. candidateServers := make([]Server, 0)
  225. for _, server := range db.Servers {
  226. var start time.Time
  227. var end time.Time
  228. var err error
  229. // All servers that are discoverable on this day are eligable for discovery
  230. if len(server.DiscoveryDateRange) != 0 {
  231. start, err = time.Parse("2006-01-02T15:04:05", server.DiscoveryDateRange[0])
  232. if err != nil {
  233. continue
  234. }
  235. end, err = time.Parse("2006-01-02T15:04:05", server.DiscoveryDateRange[1])
  236. if err != nil {
  237. continue
  238. }
  239. if discoveryDate.After(start) && discoveryDate.Before(end) {
  240. candidateServers = append(candidateServers, server)
  241. }
  242. }
  243. }
  244. servers = selectServers(candidateServers, discoveryValue)
  245. encodedServerEntries := make([]string, 0)
  246. for _, server := range servers {
  247. encodedServerEntries = append(encodedServerEntries, db.getEncodedServerEntry(server))
  248. }
  249. return encodedServerEntries
  250. }
  251. // Combine client IP address and time-of-day strategies to give out different
  252. // discovery servers to different clients. The aim is to achieve defense against
  253. // enumerability. We also want to achieve a degree of load balancing clients
  254. // and these strategies are expected to have reasonably random distribution,
  255. // even for a cluster of users coming from the same network.
  256. //
  257. // We only select one server: multiple results makes enumeration easier; the
  258. // strategies have a built-in load balancing effect; and date range discoverability
  259. // means a client will actually learn more servers later even if they happen to
  260. // always pick the same result at this point.
  261. //
  262. // This is a blended strategy: as long as there are enough servers to pick from,
  263. // both aspects determine which server is selected. IP address is given the
  264. // priority: if there are only a couple of servers, for example, IP address alone
  265. // determines the outcome.
  266. func selectServers(servers []Server, discoveryValue int) []Server {
  267. TIME_GRANULARITY := 3600
  268. if len(servers) == 0 {
  269. return nil
  270. }
  271. // Current time truncated to an hour
  272. timeInSeconds := int(time.Now().Unix())
  273. timeStrategyValue := timeInSeconds / TIME_GRANULARITY
  274. // Divide servers into buckets. The bucket count is chosen such that the number
  275. // of buckets and the number of items in each bucket are close (using sqrt).
  276. // IP address selects the bucket, time selects the item in the bucket.
  277. // NOTE: this code assumes that the range of possible timeStrategyValues
  278. // and discoveryValues are sufficient to index to all bucket items.
  279. bucketCount := calculateBucketCount(len(servers))
  280. buckets := bucketizeServerList(servers, bucketCount)
  281. bucket := buckets[discoveryValue%len(buckets)]
  282. server := bucket[timeStrategyValue%len(bucket)]
  283. serverList := make([]Server, 1)
  284. serverList[0] = server
  285. return serverList
  286. }
  287. // Number of buckets such that first strategy picks among about the same number
  288. // of choices as the second strategy. Gives an edge to the "outer" strategy.
  289. func calculateBucketCount(length int) int {
  290. return int(math.Ceil(math.Sqrt(float64(length))))
  291. }
  292. // Create bucketCount buckets.
  293. // Each bucket will be of size division or divison-1.
  294. func bucketizeServerList(servers []Server, bucketCount int) [][]Server {
  295. division := float64(len(servers)) / float64(bucketCount)
  296. buckets := make([][]Server, bucketCount)
  297. var currentBucketIndex int = 0
  298. var serverIndex int = 0
  299. for _, server := range servers {
  300. bucketEndIndex := int(math.Floor(division * (float64(currentBucketIndex) + 1)))
  301. buckets[currentBucketIndex] = append(buckets[currentBucketIndex], server)
  302. serverIndex++
  303. if serverIndex > bucketEndIndex {
  304. currentBucketIndex++
  305. }
  306. }
  307. return buckets
  308. }
  309. // Return hex encoded server entry string for comsumption by client.
  310. // Newer clients ignore the legacy fields and only utilize the extended (new) config.
  311. func (db *Database) getEncodedServerEntry(server Server) string {
  312. // Double-check that we're not giving our blank server credentials
  313. if len(server.IpAddress) <= 1 || len(server.WebServerPort) <= 1 || len(server.WebServerSecret) <= 1 || len(server.WebServerCertificate) <= 1 {
  314. return ""
  315. }
  316. // Extended (new) entry fields are in a JSON string
  317. var extendedConfig struct {
  318. IpAddress string
  319. WebServerPort string
  320. WebServerSecret string
  321. WebServerCertificate string
  322. SshPort int
  323. SshUsername string
  324. SshPassword string
  325. SshHostKey string
  326. SshObfuscatedPort int
  327. SshObfuscatedKey string
  328. Region string
  329. MeekServerPort int
  330. MeekObfuscatedKey string
  331. MeekFrontingDomain string
  332. MeekFrontingHost string
  333. MeekCookieEncryptionPublicKey string
  334. meekFrontingAddresses []string
  335. meekFrontingAddressesRegex string
  336. meekFrontingDisableSNI bool
  337. meekFrontingHosts []string
  338. capabilities []string
  339. }
  340. // NOTE: also putting original values in extended config for easier parsing by new clients
  341. extendedConfig.IpAddress = server.IpAddress
  342. extendedConfig.WebServerPort = server.WebServerPort
  343. extendedConfig.WebServerSecret = server.WebServerSecret
  344. extendedConfig.WebServerCertificate = server.WebServerCertificate
  345. sshPort, err := strconv.Atoi(server.SshPort)
  346. if err != nil {
  347. extendedConfig.SshPort = 0
  348. } else {
  349. extendedConfig.SshPort = sshPort
  350. }
  351. extendedConfig.SshUsername = server.SshUsername
  352. extendedConfig.SshPassword = server.SshPassword
  353. sshHostKeyType, sshHostKey := parseSshKeyString(server.SshHostKey)
  354. if strings.Compare(sshHostKeyType, "ssh-rsa") == 0 {
  355. extendedConfig.SshHostKey = sshHostKey
  356. } else {
  357. extendedConfig.SshHostKey = ""
  358. }
  359. extendedConfig.SshObfuscatedPort = server.SshObfuscatedPort
  360. // Use the latest alternate port unless tunneling through meek
  361. if len(server.AlternateSshObfuscatedPorts) > 0 && !(server.Capabilities["FRONTED-MEEK"] || server.Capabilities["UNFRONTED-MEEK"]) {
  362. port, err := strconv.Atoi(server.AlternateSshObfuscatedPorts[len(server.AlternateSshObfuscatedPorts)-1])
  363. if err == nil {
  364. extendedConfig.SshObfuscatedPort = port
  365. }
  366. }
  367. extendedConfig.SshObfuscatedKey = server.SshObfuscatedKey
  368. host := db.Hosts[server.HostId]
  369. extendedConfig.Region = host.Region
  370. extendedConfig.MeekServerPort = host.MeekServerPort
  371. extendedConfig.MeekObfuscatedKey = host.MeekServerObfuscatedKey
  372. extendedConfig.MeekFrontingDomain = host.MeekServerFrontingDomain
  373. extendedConfig.MeekFrontingHost = host.MeekServerFrontingHost
  374. extendedConfig.MeekCookieEncryptionPublicKey = host.MeekCookieEncryptionPublicKey
  375. serverCapabilities := make(map[string]bool, 0)
  376. for capability, enabled := range server.Capabilities {
  377. serverCapabilities[capability] = enabled
  378. }
  379. if serverCapabilities["UNFRONTED-MEEK"] && host.MeekServerPort == 443 {
  380. serverCapabilities["UNFRONTED-MEEK"] = false
  381. serverCapabilities["UNFRONTED-MEEK-HTTPS"] = true
  382. }
  383. if host.MeekServerFrontingDomain != "" {
  384. alternateMeekFrontingAddresses := db.AlternateMeekFrontingAddresses[host.MeekServerFrontingDomain]
  385. if len(alternateMeekFrontingAddresses) > 0 {
  386. // Choose 3 addresses randomly
  387. perm := rand.Perm(len(alternateMeekFrontingAddresses))[:int(math.Min(float64(len(alternateMeekFrontingAddresses)), float64(3)))]
  388. for i := range perm {
  389. extendedConfig.meekFrontingAddresses = append(extendedConfig.meekFrontingAddresses, alternateMeekFrontingAddresses[perm[i]])
  390. }
  391. }
  392. extendedConfig.meekFrontingAddressesRegex = db.AlternateMeekFrontingAddressesRegex[host.MeekServerFrontingDomain]
  393. extendedConfig.meekFrontingDisableSNI = db.MeekFrontingDisableSNI[host.MeekServerFrontingDomain]
  394. }
  395. if host.AlternateMeekServerFrontingHosts != nil {
  396. // Choose 3 addresses randomly
  397. perm := rand.Perm(len(host.AlternateMeekServerFrontingHosts))[:int(math.Min(float64(len(host.AlternateMeekServerFrontingHosts)), float64(3)))]
  398. for i := range perm {
  399. extendedConfig.meekFrontingHosts = append(extendedConfig.meekFrontingHosts, host.AlternateMeekServerFrontingHosts[i])
  400. }
  401. if serverCapabilities["FRONTED-MEEK"] == true {
  402. serverCapabilities["FRONTED-MEEK-HTTP"] = true
  403. }
  404. }
  405. for capability, enabled := range serverCapabilities {
  406. if enabled == true {
  407. extendedConfig.capabilities = append(extendedConfig.capabilities, capability)
  408. }
  409. }
  410. jsonDump, err := json.Marshal(extendedConfig)
  411. if err != nil {
  412. return ""
  413. }
  414. // Legacy format + extended (new) config
  415. prefixString := fmt.Sprintf("%s %s %s %s ", server.IpAddress, server.WebServerPort, server.WebServerSecret, server.WebServerCertificate)
  416. return hex.EncodeToString(append([]byte(prefixString)[:], []byte(jsonDump)[:]...))
  417. }
  418. // Parse string of format "ssh-key-type ssh-key".
  419. func parseSshKeyString(sshKeyString string) (keyType string, key string) {
  420. sshKeyArr := strings.Split(sshKeyString, " ")
  421. if len(sshKeyArr) != 2 {
  422. return "", ""
  423. }
  424. return sshKeyArr[0], sshKeyArr[1]
  425. }
  426. // Parse client platform string for platform identifier
  427. // and return corresponding platform.
  428. func getClientPlatform(clientPlatformString string) string {
  429. platform := CLIENT_PLATFORM_WINDOWS
  430. if strings.Contains(strings.ToLower(clientPlatformString), strings.ToLower(CLIENT_PLATFORM_ANDROID)) {
  431. platform = CLIENT_PLATFORM_ANDROID
  432. }
  433. return platform
  434. }