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 nil
  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 nil
  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
  345. WebServerPort string
  346. WebServerSecret string
  347. WebServerCertificate string
  348. SshPort int
  349. SshUsername string
  350. SshPassword string
  351. SshHostKey string
  352. SshObfuscatedPort int
  353. SshObfuscatedKey string
  354. Region string
  355. MeekCookieEncryptionPublicKey string
  356. MeekObfuscatedKey string
  357. MeekServerPort int
  358. capabilities []string
  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. }