psinet.go 17 KB

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