tactics.go 64 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137
  1. /*
  2. * Copyright (c) 2018, 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. /*
  20. Package tactics provides dynamic Psiphon client configuration based on GeoIP
  21. attributes, API parameters, and speed test data. The tactics implementation
  22. works in concert with the "parameters" package, allowing contextual
  23. optimization of Psiphon client parameters; for example, customizing
  24. NetworkLatencyMultiplier to adjust timeouts for clients on slow networks; or
  25. customizing LimitTunnelProtocols and ConnectionWorkerPoolSize to circumvent
  26. specific blocking conditions.
  27. Clients obtain tactics from a Psiphon server. Tactics are configured with a hot-
  28. reloadable, JSON format server config file. The config file specifies default
  29. tactics for all clients as well as a list of filtered tactics. For each filter,
  30. if the client's attributes satisfy the filter then additional tactics are merged
  31. into the tactics set provided to the client.
  32. Tactics configuration is optimized for a modest number of filters -- dozens --
  33. and very many GeoIP matches in each filter.
  34. A Psiphon client "tactics request" is an an untunneled, pre-establishment
  35. request to obtain tactics, which will in turn be applied and used in the normal
  36. tunnel establishment sequence; the tactics request may result in custom
  37. timeouts, protocol selection, and other tunnel establishment behavior.
  38. The client will delay its normal establishment sequence and launch a tactics
  39. request only when it has no stored, valid tactics for its current network
  40. context. The normal establishment sequence will begin, regardless of tactics
  41. request outcome, after TacticsWaitPeriod; this ensures that the client will not
  42. stall its establishment process when the tactics request cannot complete.
  43. Tactics are configured with a TTL, which is converted to an expiry time on the
  44. client when tactics are received and stored. When the client starts its
  45. establishment sequence and finds stored, unexpired tactics, no tactics request
  46. is made. The expiry time serves to prevent execess tactics requests and avoid a
  47. fingerprintable network sequence that would result from always performing the
  48. tactics request.
  49. The client calls UseStoredTactics to check for stored tactics; and if none is
  50. found (there is no record or it is expired) the client proceeds to call
  51. FetchTactics to make the tactics request.
  52. In the Psiphon client and server, the tactics request is transported using the
  53. meek protocol. In this case, meek is configured as a simple HTTP round trip
  54. transport and does not relay arbitrary streams of data and does not allocate
  55. resources required for relay mode. On the Psiphon server, the same meek
  56. component handles both tactics requests and tunnel relays. Anti-probing for
  57. tactics endpoints are thus provided as usual by meek. A meek request is routed
  58. based on an routing field in the obfuscated meek cookie.
  59. As meek may be plaintext and as TLS certificate verification is sometimes
  60. skipped, the tactics request payload is wrapped with NaCl box and further
  61. wrapped in a padded obfuscator. Distinct request and response nonces are used to
  62. mitigate replay attacks. Clients generate ephemeral NaCl key pairs and the
  63. server public key is obtained from the server entry. The server entry also
  64. contains capabilities indicating that a Psiphon server supports tactics requests
  65. and which meek protocol is to be used.
  66. The Psiphon client requests, stores, and applies distinct tactics based on its
  67. current network context. The client uses platform-specific APIs to obtain a fine
  68. grain network ID based on, for example BSSID for WiFi or MCC/MNC for mobile.
  69. These values provides accurate detection of network context changes and can be
  70. obtained from the client device without any network activity. As the network ID
  71. is personally identifying, this ID is only used by the client and is never sent
  72. to the Psiphon server. The client obtains the current network ID from a callback
  73. made from tunnel-core to native client code.
  74. Tactics returned to the Psiphon client are accompanied by a "tag" which is a
  75. hash digest of the merged tactics data. This tag uniquely identifies the
  76. tactics. The client reports the tactics it is employing through the
  77. "applied_tactics" common metrics API parameter. When fetching new tactics, the
  78. client reports the stored (and possibly expired) tactics it has through the
  79. "stored_tactics" API parameter. The stored tactics tag is used to avoid
  80. redownloading redundant tactics data; when the tactics response indicates the
  81. tag is unchanged, no tactics data is returned and the client simply extends the
  82. expiry of the data is already has.
  83. The Psiphon handshake API returns tactics in its response. This enabled regular
  84. tactics expiry extension without requiring any distinct tactics request or
  85. tactics data transfer when the tag is unchanged. Psiphon clients that connect
  86. regularly and successfully with make almost no untunnled tactics requests except
  87. for new network IDs. Returning tactics in the handshake reponse also provides
  88. tactics in the case where a client is unable to complete an untunneled tactics
  89. request but can otherwise establish a tunnel. Clients will abort any outstanding
  90. untunneled tactics requests or scheduled retries once a handshake has completed.
  91. The client handshake request component calls SetTacticsAPIParameters to populate
  92. the handshake request parameters with tactics inputs, and calls
  93. HandleTacticsPayload to process the tactics payload in the handshake response.
  94. The core tactics data is custom values for a subset of the parameters in
  95. parameters.Parameters. A client takes the default Parameters, applies any
  96. custom values set in its config file, and then applies any stored or received
  97. tactics. Each time the tactics changes, this process is repeated so that
  98. obsolete tactics parameters are not retained in the client's Parameters
  99. instance.
  100. Speed test data is used in filtered tactics for selection of parameters such as
  101. timeouts.
  102. A speed test sample records the RTT of an application-level round trip to a
  103. Psiphon server -- either a meek HTTP round trip or an SSH request round trip.
  104. The round trip should be preformed after an TCP, TLS, SSH, etc. handshake so
  105. that the RTT includes only the application-level round trip. Each sample also
  106. records the tunnel/meek protocol used, the Psiphon server region, and a
  107. timestamp; these values may be used to filter out outliers or stale samples. The
  108. samples record bytes up/down, although at this time the speed test is focused on
  109. latency and the payload is simply anti-fingerprint padding and should not be
  110. larger than an IP packet.
  111. The Psiphon client records the latest SpeedTestMaxSampleCount speed test samples
  112. for each network context. SpeedTestMaxSampleCount should be a modest size, as
  113. each speed test sample is ~100 bytes when serialzied and all samples (for one
  114. network ID) are loaded into memory and sent as API inputs to tactics and
  115. handshake requests.
  116. When a tactics request is initiated and there are no speed test samples for
  117. current network ID, the tactics request is proceeded by a speed test round trip,
  118. using the same meek round tripper, and that sample is stored and used for the
  119. tactics request. with a speed test The client records additional samples taken
  120. from regular SSH keep alive round trips and calls AddSpeedTestSample to store
  121. these.
  122. The client sends all its speed test samples, for the current network context, to
  123. the server in tactics and handshake requests; this allows the server logic to
  124. handle outliers and aggregation. Currently, filtered tactics support filerting
  125. on speed test RTT maximum, minimum, and median.
  126. */
  127. package tactics
  128. import (
  129. "bytes"
  130. "context"
  131. "crypto/md5"
  132. "crypto/rand"
  133. "encoding/base64"
  134. "encoding/hex"
  135. "encoding/json"
  136. "fmt"
  137. "io/ioutil"
  138. "net/http"
  139. "sort"
  140. "strconv"
  141. "strings"
  142. "sync"
  143. "time"
  144. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
  145. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
  146. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/obfuscator"
  147. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
  148. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
  149. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
  150. lrucache "github.com/cognusion/go-cache-lru"
  151. "github.com/fxamacker/cbor/v2"
  152. "golang.org/x/crypto/nacl/box"
  153. )
  154. // TACTICS_PADDING_MAX_SIZE is used by the client as well as the server. This
  155. // value is not a dynamic client parameter since a tactics request is made
  156. // only when the client has no valid tactics, so no override of
  157. // TACTICS_PADDING_MAX_SIZE can be applied.
  158. const (
  159. SPEED_TEST_END_POINT = "speedtest"
  160. TACTICS_END_POINT = "tactics"
  161. MAX_REQUEST_BODY_SIZE = 65536
  162. SPEED_TEST_PADDING_MIN_SIZE = 0
  163. SPEED_TEST_PADDING_MAX_SIZE = 256
  164. TACTICS_PADDING_MAX_SIZE = 256
  165. TACTICS_OBFUSCATED_KEY_SIZE = 32
  166. SPEED_TEST_SAMPLES_PARAMETER_NAME = "speed_test_samples"
  167. APPLIED_TACTICS_TAG_PARAMETER_NAME = "applied_tactics_tag"
  168. STORED_TACTICS_TAG_PARAMETER_NAME = "stored_tactics_tag"
  169. TACTICS_METRIC_EVENT_NAME = "tactics"
  170. NEW_TACTICS_TAG_LOG_FIELD_NAME = "new_tactics_tag"
  171. IS_TACTICS_REQUEST_LOG_FIELD_NAME = "is_tactics_request"
  172. AGGREGATION_MINIMUM = "Minimum"
  173. AGGREGATION_MAXIMUM = "Maximum"
  174. AGGREGATION_MEDIAN = "Median"
  175. PAYLOAD_CACHE_SIZE = 1024
  176. )
  177. var (
  178. TACTICS_REQUEST_NONCE = []byte{1}
  179. TACTICS_RESPONSE_NONCE = []byte{2}
  180. )
  181. // Server is a tactics server to be integrated with the Psiphon server meek and handshake
  182. // components.
  183. //
  184. // The meek server calls HandleEndPoint to handle untunneled tactics and speed test requests.
  185. // The handshake handler calls GetTacticsPayload to obtain a tactics payload to include with
  186. // the handsake response.
  187. //
  188. // The Server is a reloadable file; its exported fields are read from the tactics configuration
  189. // file.
  190. //
  191. // Each client will receive at least the DefaultTactics. Client GeoIP, API parameter, and speed
  192. // test sample attributes are matched against all filters and the tactics corresponding to any
  193. // matching filter are merged into the client tactics.
  194. //
  195. // The merge operation replaces any existing item in Parameter with a Parameter specified in
  196. // the newest matching tactics. The TTL of the newest matching tactics is taken, although all
  197. // but the DefaultTactics can omit the TTL field.
  198. type Server struct {
  199. common.ReloadableFile
  200. // RequestPublicKey is the Server's tactics request NaCl box public key.
  201. RequestPublicKey []byte
  202. // RequestPublicKey is the Server's tactics request NaCl box private key.
  203. RequestPrivateKey []byte
  204. // RequestObfuscatedKey is the tactics request obfuscation key.
  205. RequestObfuscatedKey []byte
  206. // DefaultTactics is the baseline tactics for all clients. It must include a
  207. // TTL.
  208. DefaultTactics Tactics
  209. // FilteredTactics is an ordered list of filter/tactics pairs. For a client,
  210. // each fltered tactics is checked in order and merged into the clients
  211. // tactics if the client's attributes satisfy the filter.
  212. FilteredTactics []struct {
  213. Filter Filter
  214. Tactics Tactics
  215. }
  216. // When no tactics configuration file is provided, there will be no
  217. // request key material or default tactics, and the server will not
  218. // support tactics. The loaded flag, set to true only when a configuration
  219. // file has been successfully loaded, provides an explict check for this
  220. // condition (vs., say, checking for a zero-value Server).
  221. loaded bool
  222. filterGeoIPScope int
  223. filterRegionScopes map[string]int
  224. logger common.Logger
  225. logFieldFormatter common.APIParameterLogFieldFormatter
  226. apiParameterValidator common.APIParameterValidator
  227. cachedTacticsData *lrucache.Cache
  228. filterMatches *sync.Pool
  229. }
  230. const (
  231. GeoIPScopeRegion = 1
  232. GeoIPScopeISP = 2
  233. GeoIPScopeASN = 4
  234. GeoIPScopeCity = 8
  235. )
  236. // Filter defines a filter to match against client attributes.
  237. // Each field within the filter is optional and may be omitted.
  238. type Filter struct {
  239. // Regions specifies a list of GeoIP regions/countries the client
  240. // must match.
  241. Regions []string
  242. // ISPs specifies a list of GeoIP ISPs the client must match.
  243. ISPs []string
  244. // ASNs specifies a list of GeoIP ASNs the client must match.
  245. ASNs []string
  246. // Cities specifies a list of GeoIP Cities the client must match.
  247. Cities []string
  248. // APIParameters specifies API, e.g. handshake, parameter names and
  249. // a list of values, one of which must be specified to match this
  250. // filter. Only scalar string API parameters may be filtered.
  251. // Values may be patterns containing the '*' wildcard.
  252. APIParameters map[string][]string
  253. // Min/MaxClientVersion specify version constraints the client must match.
  254. MinClientVersion *int
  255. MaxClientVersion *int
  256. // SpeedTestRTTMilliseconds specifies a Range filter field that the
  257. // client speed test samples must satisfy.
  258. SpeedTestRTTMilliseconds *Range
  259. regionLookup map[string]bool
  260. ispLookup map[string]bool
  261. asnLookup map[string]bool
  262. cityLookup map[string]bool
  263. }
  264. // Range is a filter field which specifies that the aggregation of
  265. // the a client attribute is within specified upper and lower bounds.
  266. // At least one bound must be specified.
  267. //
  268. // For example, Range is to aggregate and filter client speed test
  269. // sample RTTs.
  270. type Range struct {
  271. // Aggregation may be "Maximum", "Minimum", or "Median"
  272. Aggregation string
  273. // AtLeast specifies a lower bound for the aggregarted
  274. // client value.
  275. AtLeast *int
  276. // AtMost specifies an upper bound for the aggregarted
  277. // client value.
  278. AtMost *int
  279. }
  280. // Payload is the data to be returned to the client in response to a
  281. // tactics request or in the handshake response.
  282. type Payload struct {
  283. // Tag is the hash tag of the accompanying Tactics. When the Tag
  284. // is the same as the stored tag the client specified in its
  285. // request, the Tactics will be empty as the client already has the
  286. // correct data.
  287. Tag string `cbor:"1,keyasint,omitempty"`
  288. // Tactics is a JSON- or CBOR-encoded Tactics struct and may be nil.
  289. Tactics json.RawMessage `cbor:"2,keyasint,omitempty"`
  290. // TacticsCompression specifies how Tactics is compressed.
  291. TacticsCompression int32 `cbor:"3,keyasint,omitempty"`
  292. }
  293. // Record is the tactics data persisted by the client. There is one
  294. // record for each network ID.
  295. type Record struct {
  296. // The Tag is the hash of the tactics data and is used as the
  297. // stored tag when making requests.
  298. Tag string
  299. // Expiry is the time when this perisisted tactics expires as
  300. // determined by the client applying the TTL against its local
  301. // clock when the tactics was stored.
  302. Expiry time.Time
  303. // Tactics is the core tactics data.
  304. Tactics Tactics
  305. }
  306. // Tactics is the core tactics data. This is both what is set in
  307. // in the server configuration file and what is stored and used
  308. // by the cient.
  309. type Tactics struct {
  310. // TTL is a string duration (e.g., "24h", the syntax supported
  311. // by time.ParseDuration). This specifies how long the client
  312. // should use the accompanying tactics until it expires.
  313. //
  314. // The client stores the TTL to use for extending the tactics
  315. // expiry when a tactics request or handshake response returns
  316. // no tactics data when the tag is unchanged.
  317. TTL string
  318. // Probability is an obsolete field which is no longer used, as overall
  319. // tactics are now applied unconditionally; but it must be present, and
  320. // greater than zero, in marshaled tactics, sent by the server, for
  321. // compatibility with legacy client tactics validation.
  322. Probability float64
  323. // Parameters specify client parameters to override. These must
  324. // be a subset of parameter.ClientParameter values and follow
  325. // the corresponding data type and minimum value constraints.
  326. Parameters map[string]interface{}
  327. }
  328. // Note: the SpeedTestSample json tags are selected to minimize marshaled
  329. // size. In psiphond, for logging metrics, the field names are translated to
  330. // more verbose values. psiphon/server.makeSpeedTestSamplesLogField currently
  331. // hard-codes these same SpeedTestSample json tag values for that translation.
  332. // SpeedTestSample is speed test data for a single RTT event.
  333. type SpeedTestSample struct {
  334. // Timestamp is the speed test event time, and may be used to discard
  335. // stale samples. The server supplies the speed test timestamp. This
  336. // value is truncated to the nearest hour as a privacy measure.
  337. Timestamp time.Time `json:"s"`
  338. // EndPointRegion is the region of the endpoint, the Psiphon server,
  339. // used for the speed test. This may be used to exclude outlier samples
  340. // using remote data centers.
  341. EndPointRegion string `json:"r"`
  342. // EndPointProtocol is the tactics or tunnel protocol use for the
  343. // speed test round trip. The protocol may impact RTT.
  344. EndPointProtocol string `json:"p"`
  345. // All speed test samples should measure RTT as the time to complete
  346. // an application-level round trip on top of a previously established
  347. // tactics or tunnel prococol connection. The RTT should not include
  348. // TCP, TLS, or SSH handshakes.
  349. // This value is truncated to the nearest millisecond as a privacy
  350. // measure.
  351. RTTMilliseconds int `json:"t"`
  352. // BytesUp is the size of the upstream payload in the round trip.
  353. // Currently, the payload is limited to anti-fingerprint padding.
  354. BytesUp int `json:"u"`
  355. // BytesDown is the size of the downstream payload in the round trip.
  356. // Currently, the payload is limited to anti-fingerprint padding.
  357. BytesDown int `json:"d"`
  358. }
  359. // GenerateKeys generates a tactics request key pair and obfuscation key.
  360. func GenerateKeys() (encodedRequestPublicKey, encodedRequestPrivateKey, encodedObfuscatedKey string, err error) {
  361. requestPublicKey, requestPrivateKey, err := box.GenerateKey(rand.Reader)
  362. if err != nil {
  363. return "", "", "", errors.Trace(err)
  364. }
  365. obfuscatedKey, err := common.MakeSecureRandomBytes(TACTICS_OBFUSCATED_KEY_SIZE)
  366. if err != nil {
  367. return "", "", "", errors.Trace(err)
  368. }
  369. return base64.StdEncoding.EncodeToString(requestPublicKey[:]),
  370. base64.StdEncoding.EncodeToString(requestPrivateKey[:]),
  371. base64.StdEncoding.EncodeToString(obfuscatedKey[:]),
  372. nil
  373. }
  374. // NewServer creates Server using the specified tactics configuration file.
  375. //
  376. // The logger and logFieldFormatter callbacks are used to log errors and
  377. // metrics. The apiParameterValidator callback is used to validate client
  378. // API parameters submitted to the tactics request.
  379. //
  380. // The optional requestPublicKey, requestPrivateKey, and requestObfuscatedKey
  381. // base64 encoded string parameters may be used to specify and override the
  382. // corresponding Server config values.
  383. func NewServer(
  384. logger common.Logger,
  385. logFieldFormatter common.APIParameterLogFieldFormatter,
  386. apiParameterValidator common.APIParameterValidator,
  387. configFilename string,
  388. requestPublicKey string,
  389. requestPrivateKey string,
  390. requestObfuscatedKey string) (*Server, error) {
  391. server := &Server{
  392. logger: logger,
  393. logFieldFormatter: logFieldFormatter,
  394. apiParameterValidator: apiParameterValidator,
  395. cachedTacticsData: lrucache.NewWithLRU(
  396. lrucache.NoExpiration, 1*time.Minute, PAYLOAD_CACHE_SIZE),
  397. }
  398. server.ReloadableFile = common.NewReloadableFile(
  399. configFilename,
  400. true,
  401. func(fileContent []byte, _ time.Time) error {
  402. var newServer Server
  403. err := json.Unmarshal(fileContent, &newServer)
  404. if err != nil {
  405. return errors.Trace(err)
  406. }
  407. if requestPublicKey != "" {
  408. newServer.RequestPublicKey, err =
  409. base64.StdEncoding.DecodeString(requestPublicKey)
  410. if err != nil {
  411. return errors.Trace(err)
  412. }
  413. }
  414. if requestPrivateKey != "" {
  415. newServer.RequestPrivateKey, err =
  416. base64.StdEncoding.DecodeString(requestPrivateKey)
  417. if err != nil {
  418. return errors.Trace(err)
  419. }
  420. }
  421. if requestObfuscatedKey != "" {
  422. newServer.RequestObfuscatedKey, err =
  423. base64.StdEncoding.DecodeString(requestObfuscatedKey)
  424. if err != nil {
  425. return errors.Trace(err)
  426. }
  427. }
  428. err = newServer.Validate()
  429. if err != nil {
  430. return errors.Trace(err)
  431. }
  432. // Server.ReloadableFile.RWMutex is the mutex for accessing
  433. // these and other Server fields.
  434. // Modify actual traffic rules only after validation
  435. server.RequestPublicKey = newServer.RequestPublicKey
  436. server.RequestPrivateKey = newServer.RequestPrivateKey
  437. server.RequestObfuscatedKey = newServer.RequestObfuscatedKey
  438. server.DefaultTactics = newServer.DefaultTactics
  439. server.FilteredTactics = newServer.FilteredTactics
  440. // Any cached, merged tactics data is flushed when the
  441. // configuration changes.
  442. server.cachedTacticsData.Flush()
  443. // A pool of filterMatches, used in getTactics, is used to avoid
  444. // allocating a slice for every getTactics call.
  445. //
  446. // A pointer to a slice is used with sync.Pool to avoid an
  447. // allocation on Put, as would happen if passing in a slice
  448. // instead of a pointer; see
  449. // https://github.com/dominikh/go-tools/issues/1042#issuecomment-869064445
  450. server.filterMatches = &sync.Pool{
  451. New: func() any {
  452. b := make([]bool, len(server.FilteredTactics))
  453. return &b
  454. },
  455. }
  456. server.initLookups()
  457. server.loaded = true
  458. return nil
  459. })
  460. _, err := server.Reload()
  461. if err != nil {
  462. return nil, errors.Trace(err)
  463. }
  464. return server, nil
  465. }
  466. // Validate checks for correct tactics configuration values.
  467. func (server *Server) Validate() error {
  468. // Key material must either be entirely omitted, or fully populated.
  469. if len(server.RequestPublicKey) == 0 {
  470. if len(server.RequestPrivateKey) != 0 ||
  471. len(server.RequestObfuscatedKey) != 0 {
  472. return errors.TraceNew("unexpected request key")
  473. }
  474. } else {
  475. if len(server.RequestPublicKey) != 32 ||
  476. len(server.RequestPrivateKey) != 32 ||
  477. len(server.RequestObfuscatedKey) != TACTICS_OBFUSCATED_KEY_SIZE {
  478. return errors.TraceNew("invalid request key")
  479. }
  480. }
  481. // validateTactics validates either the defaultTactics, when filteredTactics
  482. // is nil, or the filteredTactics otherwise. In the second case,
  483. // defaultTactics must be passed in to validate filtered tactics references
  484. // to default tactics parameters, such as CustomTLSProfiles or
  485. // PacketManipulationSpecs.
  486. //
  487. // Limitation: references must point to the default tactics or the filtered
  488. // tactics itself; referring to parameters in a previous filtered tactics is
  489. // not suported.
  490. validateTactics := func(defaultTactics, filteredTactics *Tactics) error {
  491. tactics := defaultTactics
  492. validatingDefault := true
  493. if filteredTactics != nil {
  494. tactics = filteredTactics
  495. validatingDefault = false
  496. }
  497. // Allow "" for 0, even though ParseDuration does not.
  498. var d time.Duration
  499. if tactics.TTL != "" {
  500. var err error
  501. d, err = time.ParseDuration(tactics.TTL)
  502. if err != nil {
  503. return errors.Trace(err)
  504. }
  505. }
  506. if d <= 0 {
  507. if validatingDefault {
  508. return errors.TraceNew("invalid duration")
  509. }
  510. // For merging logic, Normalize any 0 duration to "".
  511. tactics.TTL = ""
  512. }
  513. params, err := parameters.NewParameters(nil)
  514. if err != nil {
  515. return errors.Trace(err)
  516. }
  517. applyParameters := []map[string]interface{}{
  518. defaultTactics.Parameters,
  519. }
  520. if filteredTactics != nil {
  521. applyParameters = append(
  522. applyParameters, filteredTactics.Parameters)
  523. }
  524. _, err = params.Set(
  525. "", parameters.ValidationServerSide, applyParameters...)
  526. if err != nil {
  527. return errors.Trace(err)
  528. }
  529. return nil
  530. }
  531. validateRange := func(r *Range) error {
  532. if r == nil {
  533. return nil
  534. }
  535. if (r.AtLeast == nil && r.AtMost == nil) ||
  536. ((r.AtLeast != nil && r.AtMost != nil) && *r.AtLeast > *r.AtMost) {
  537. return errors.TraceNew("invalid range")
  538. }
  539. switch r.Aggregation {
  540. case AGGREGATION_MINIMUM, AGGREGATION_MAXIMUM, AGGREGATION_MEDIAN:
  541. default:
  542. return errors.TraceNew("invalid aggregation")
  543. }
  544. return nil
  545. }
  546. err := validateTactics(&server.DefaultTactics, nil)
  547. if err != nil {
  548. return errors.Tracef("invalid default tactics: %s", err)
  549. }
  550. for i, filteredTactics := range server.FilteredTactics {
  551. err := validateTactics(&server.DefaultTactics, &filteredTactics.Tactics)
  552. if err == nil {
  553. err = validateRange(filteredTactics.Filter.SpeedTestRTTMilliseconds)
  554. }
  555. // TODO: validate Filter.APIParameters names are valid?
  556. if err != nil {
  557. return errors.Tracef("invalid filtered tactics %d: %s", i, err)
  558. }
  559. }
  560. return nil
  561. }
  562. const stringLookupThreshold = 5
  563. // initLookups creates map lookups for filters where the number
  564. // of string values to compare against exceeds a threshold where
  565. // benchmarks show maps are faster than looping through a string
  566. // slice.
  567. func (server *Server) initLookups() {
  568. server.filterGeoIPScope = 0
  569. server.filterRegionScopes = make(map[string]int)
  570. for _, filteredTactics := range server.FilteredTactics {
  571. if len(filteredTactics.Filter.Regions) >= stringLookupThreshold {
  572. filteredTactics.Filter.regionLookup = make(map[string]bool)
  573. for _, region := range filteredTactics.Filter.Regions {
  574. filteredTactics.Filter.regionLookup[region] = true
  575. }
  576. }
  577. if len(filteredTactics.Filter.ISPs) >= stringLookupThreshold {
  578. filteredTactics.Filter.ispLookup = make(map[string]bool)
  579. for _, ISP := range filteredTactics.Filter.ISPs {
  580. filteredTactics.Filter.ispLookup[ISP] = true
  581. }
  582. }
  583. if len(filteredTactics.Filter.ASNs) >= stringLookupThreshold {
  584. filteredTactics.Filter.asnLookup = make(map[string]bool)
  585. for _, ASN := range filteredTactics.Filter.ASNs {
  586. filteredTactics.Filter.asnLookup[ASN] = true
  587. }
  588. }
  589. if len(filteredTactics.Filter.Cities) >= stringLookupThreshold {
  590. filteredTactics.Filter.cityLookup = make(map[string]bool)
  591. for _, city := range filteredTactics.Filter.Cities {
  592. filteredTactics.Filter.cityLookup[city] = true
  593. }
  594. }
  595. // Initialize the filter GeoIP scope fields used by GetFilterGeoIPScope.
  596. //
  597. // The basic case is, for example, when only Regions appear in filters, then
  598. // only GeoIPScopeRegion is set.
  599. //
  600. // As an optimization, a regional map is populated so that, for example,
  601. // GeoIPScopeRegion&GeoIPScopeISP will be set only for regions for which
  602. // there is a filter with region and ISP, while other regions will set only
  603. // GeoIPScopeRegion.
  604. //
  605. // When any ISP, ASN, or City appears in a filter without a Region,
  606. // the regional map optimization is disabled.
  607. if len(filteredTactics.Filter.Regions) == 0 {
  608. disableRegionScope := false
  609. if len(filteredTactics.Filter.ISPs) > 0 {
  610. server.filterGeoIPScope |= GeoIPScopeISP
  611. disableRegionScope = true
  612. }
  613. if len(filteredTactics.Filter.ASNs) > 0 {
  614. server.filterGeoIPScope |= GeoIPScopeASN
  615. disableRegionScope = true
  616. }
  617. if len(filteredTactics.Filter.Cities) > 0 {
  618. server.filterGeoIPScope |= GeoIPScopeCity
  619. disableRegionScope = true
  620. }
  621. if disableRegionScope && server.filterRegionScopes != nil {
  622. for _, regionScope := range server.filterRegionScopes {
  623. server.filterGeoIPScope |= regionScope
  624. }
  625. server.filterRegionScopes = nil
  626. }
  627. } else {
  628. server.filterGeoIPScope |= GeoIPScopeRegion
  629. if server.filterRegionScopes != nil {
  630. regionScope := 0
  631. if len(filteredTactics.Filter.ISPs) > 0 {
  632. regionScope |= GeoIPScopeISP
  633. }
  634. if len(filteredTactics.Filter.ASNs) > 0 {
  635. regionScope |= GeoIPScopeASN
  636. }
  637. if len(filteredTactics.Filter.Cities) > 0 {
  638. regionScope |= GeoIPScopeCity
  639. }
  640. for _, region := range filteredTactics.Filter.Regions {
  641. server.filterRegionScopes[region] |= regionScope
  642. }
  643. }
  644. }
  645. // TODO: add lookups for APIParameters?
  646. // Not expected to be long lists of values.
  647. }
  648. }
  649. // GetFilterGeoIPScope returns which GeoIP fields are relevent to tactics
  650. // filters. The return value is a bit array containing some combination of
  651. // the GeoIPScopeRegion, GeoIPScopeISP, GeoIPScopeASN, and GeoIPScopeCity
  652. // flags. For the given geoIPData, all tactics filters reference only the
  653. // flagged fields.
  654. func (server *Server) GetFilterGeoIPScope(geoIPData common.GeoIPData) int {
  655. scope := server.filterGeoIPScope
  656. if server.filterRegionScopes != nil {
  657. regionScope, ok := server.filterRegionScopes[geoIPData.Country]
  658. if ok {
  659. scope |= regionScope
  660. }
  661. }
  662. return scope
  663. }
  664. // GetTacticsPayload assembles and returns a tactics payload for a client with
  665. // the specified GeoIP, API parameters, and speed test attributes.
  666. //
  667. // The speed test samples are expected to be in apiParams, as is the stored
  668. // tactics tag.
  669. //
  670. // Unless no tactics configuration was loaded, GetTacticsPayload will always
  671. // return a payload for any client. When the client's stored tactics tag is
  672. // identical to the assembled tactics, the Payload.Tactics is nil.
  673. //
  674. // Elements of the returned Payload, e.g., tactics parameters, will point to
  675. // data in DefaultTactics and FilteredTactics and must not be modifed.
  676. //
  677. // Callers must not mutate returned tactics data, which is cached.
  678. func (server *Server) GetTacticsPayload(
  679. geoIPData common.GeoIPData,
  680. apiParams common.APIParameters,
  681. compressPayload bool) (*Payload, error) {
  682. // includeServerSideOnly is false: server-side only parameters are not
  683. // used by the client, so including them wastes space and unnecessarily
  684. // exposes the values.
  685. tacticsData, err := server.getTactics(false, geoIPData, apiParams)
  686. if err != nil {
  687. return nil, errors.Trace(err)
  688. }
  689. if tacticsData == nil {
  690. return nil, nil
  691. }
  692. payload := &Payload{
  693. Tag: tacticsData.tag,
  694. }
  695. // New clients should always send STORED_TACTICS_TAG_PARAMETER_NAME. When they have no
  696. // stored tactics, the stored tag will be "" and not match payload.Tag and payload.Tactics
  697. // will be sent.
  698. //
  699. // When new clients send a stored tag that matches payload.Tag, the client already has
  700. // the correct data and payload.Tactics is not sent.
  701. //
  702. // Old clients will not send STORED_TACTICS_TAG_PARAMETER_NAME. In this case, do not
  703. // send payload.Tactics as the client will not use it, will not store it, will not send
  704. // back the new tag and so the handshake response will always contain wasteful tactics
  705. // data.
  706. sendPayloadTactics := true
  707. clientStoredTag, err := getStringRequestParam(apiParams, STORED_TACTICS_TAG_PARAMETER_NAME)
  708. // Old client or new client with same tag.
  709. if err != nil || payload.Tag == clientStoredTag {
  710. sendPayloadTactics = false
  711. }
  712. if sendPayloadTactics {
  713. tacticsDataPayload := tacticsData.payload
  714. compression := common.CompressionNone
  715. if compressPayload {
  716. compression = common.CompressionZlib
  717. tacticsDataPayload, err = common.Compress(
  718. compression, tacticsDataPayload)
  719. if err != nil {
  720. return nil, errors.Trace(err)
  721. }
  722. }
  723. payload.Tactics = tacticsDataPayload
  724. payload.TacticsCompression = compression
  725. }
  726. return payload, nil
  727. }
  728. // GetTacticsWithTag returns a GetTactics value along with the associated tag value.
  729. //
  730. // Callers must not mutate returned tactics data, which is cached.
  731. func (server *Server) GetTacticsWithTag(
  732. includeServerSideOnly bool,
  733. geoIPData common.GeoIPData,
  734. apiParams common.APIParameters) (*Tactics, string, error) {
  735. tacticsData, err := server.getTactics(
  736. includeServerSideOnly, geoIPData, apiParams)
  737. if err != nil {
  738. return nil, "", errors.Trace(err)
  739. }
  740. if tacticsData == nil {
  741. return nil, "", nil
  742. }
  743. return tacticsData.tactics, tacticsData.tag, nil
  744. }
  745. // tacticsData is cached tactics data, including the merged Tactics object,
  746. // the JSON marshaled paylod, and hashed tag.
  747. type tacticsData struct {
  748. tactics *Tactics
  749. payload []byte
  750. tag string
  751. }
  752. func newTacticsData(tactics *Tactics) (*tacticsData, error) {
  753. payload, err := json.Marshal(tactics)
  754. if err != nil {
  755. return nil, errors.Trace(err)
  756. }
  757. // MD5 hash is used solely as a data checksum and not for any security
  758. // purpose.
  759. digest := md5.Sum(payload)
  760. tag := hex.EncodeToString(digest[:])
  761. return &tacticsData{
  762. tactics: tactics,
  763. payload: payload,
  764. tag: tag,
  765. }, nil
  766. }
  767. // GetTactics assembles and returns tactics data for a client with the
  768. // specified GeoIP, API parameter, and speed test attributes.
  769. //
  770. // The tactics return value may be nil.
  771. //
  772. // Callers must not mutate returned tactics data, which is cached.
  773. func (server *Server) getTactics(
  774. includeServerSideOnly bool,
  775. geoIPData common.GeoIPData,
  776. apiParams common.APIParameters) (*tacticsData, error) {
  777. server.ReloadableFile.RLock()
  778. defer server.ReloadableFile.RUnlock()
  779. if !server.loaded {
  780. // No tactics configuration was loaded.
  781. return nil, nil
  782. }
  783. // Two passes are performed, one to get the list of matching filters, and
  784. // then, if no merged tactics data is found for that filter match set,
  785. // another pass to merge all the tactics parameters.
  786. var aggregatedValues map[string]int
  787. filterMatchCount := 0
  788. // Use the filterMatches buffer pool to avoid an allocation per getTactics
  789. // call.
  790. b := server.filterMatches.Get().(*[]bool)
  791. filterMatches := *b
  792. clear(filterMatches)
  793. defer server.filterMatches.Put(b)
  794. for filterIndex, filteredTactics := range server.FilteredTactics {
  795. filterMatches[filterIndex] = false
  796. if len(filteredTactics.Filter.Regions) > 0 {
  797. if filteredTactics.Filter.regionLookup != nil {
  798. if !filteredTactics.Filter.regionLookup[geoIPData.Country] {
  799. continue
  800. }
  801. } else {
  802. if !common.Contains(filteredTactics.Filter.Regions, geoIPData.Country) {
  803. continue
  804. }
  805. }
  806. }
  807. if len(filteredTactics.Filter.ISPs) > 0 {
  808. if filteredTactics.Filter.ispLookup != nil {
  809. if !filteredTactics.Filter.ispLookup[geoIPData.ISP] {
  810. continue
  811. }
  812. } else {
  813. if !common.Contains(filteredTactics.Filter.ISPs, geoIPData.ISP) {
  814. continue
  815. }
  816. }
  817. }
  818. if len(filteredTactics.Filter.ASNs) > 0 {
  819. if filteredTactics.Filter.asnLookup != nil {
  820. if !filteredTactics.Filter.asnLookup[geoIPData.ASN] {
  821. continue
  822. }
  823. } else {
  824. if !common.Contains(filteredTactics.Filter.ASNs, geoIPData.ASN) {
  825. continue
  826. }
  827. }
  828. }
  829. if len(filteredTactics.Filter.Cities) > 0 {
  830. if filteredTactics.Filter.cityLookup != nil {
  831. if !filteredTactics.Filter.cityLookup[geoIPData.City] {
  832. continue
  833. }
  834. } else {
  835. if !common.Contains(filteredTactics.Filter.Cities, geoIPData.City) {
  836. continue
  837. }
  838. }
  839. }
  840. if filteredTactics.Filter.APIParameters != nil {
  841. mismatch := false
  842. for name, values := range filteredTactics.Filter.APIParameters {
  843. clientValue, err := getStringRequestParam(apiParams, name)
  844. if err != nil || !common.ContainsWildcard(values, clientValue) {
  845. mismatch = true
  846. break
  847. }
  848. }
  849. if mismatch {
  850. continue
  851. }
  852. }
  853. if filteredTactics.Filter.MinClientVersion != nil ||
  854. filteredTactics.Filter.MaxClientVersion != nil {
  855. clientVersion, err := getIntStringRequestParam(
  856. apiParams, protocol.PSIPHON_API_HANDSHAKE_CLIENT_VERSION)
  857. if err != nil {
  858. continue
  859. }
  860. if filteredTactics.Filter.MinClientVersion != nil &&
  861. clientVersion < *filteredTactics.Filter.MinClientVersion {
  862. continue
  863. }
  864. if filteredTactics.Filter.MaxClientVersion != nil &&
  865. clientVersion > *filteredTactics.Filter.MaxClientVersion {
  866. continue
  867. }
  868. }
  869. if filteredTactics.Filter.SpeedTestRTTMilliseconds != nil {
  870. var speedTestSamples []SpeedTestSample
  871. err := getJSONRequestParam(apiParams, SPEED_TEST_SAMPLES_PARAMETER_NAME, &speedTestSamples)
  872. if err != nil {
  873. // TODO: log speed test parameter errors?
  874. // This API param is not explicitly validated elsewhere.
  875. continue
  876. }
  877. // As there must be at least one Range bound, there must be data to aggregate.
  878. if len(speedTestSamples) == 0 {
  879. continue
  880. }
  881. if aggregatedValues == nil {
  882. aggregatedValues = make(map[string]int)
  883. }
  884. // Note: here we could filter out outliers such as samples that are unusually old
  885. // or client/endPoint region pair too distant.
  886. // aggregate may mutate (sort) the speedTestSamples slice.
  887. value := aggregate(
  888. filteredTactics.Filter.SpeedTestRTTMilliseconds.Aggregation,
  889. speedTestSamples,
  890. aggregatedValues)
  891. if filteredTactics.Filter.SpeedTestRTTMilliseconds.AtLeast != nil &&
  892. value < *filteredTactics.Filter.SpeedTestRTTMilliseconds.AtLeast {
  893. continue
  894. }
  895. if filteredTactics.Filter.SpeedTestRTTMilliseconds.AtMost != nil &&
  896. value > *filteredTactics.Filter.SpeedTestRTTMilliseconds.AtMost {
  897. continue
  898. }
  899. }
  900. filterMatchCount += 1
  901. filterMatches[filterIndex] = true
  902. // Continue to check for more matches. Last matching tactics filter
  903. // has priority for any field.
  904. }
  905. // For any filter match set, the merged tactics parameters are the same,
  906. // so the resulting merge is cached, along with the JSON encoding of the
  907. // payload and hash tag. This cache reduces, for repeated tactics
  908. // requests, heavy allocations from the JSON marshal and CPU load from
  909. // both the marshal and hashing the marshal result.
  910. //
  911. // getCacheKey still allocates a strings.Builder buffer.
  912. //
  913. // TODO: log cache metrics; similar to what is done in
  914. // psiphon/server.ServerTacticsParametersCache.GetMetrics.
  915. cacheKey := getCacheKey(includeServerSideOnly, filterMatchCount > 0, filterMatches)
  916. cacheValue, ok := server.cachedTacticsData.Get(cacheKey)
  917. if ok {
  918. return cacheValue.(*tacticsData), nil
  919. }
  920. tactics := server.DefaultTactics.clone(includeServerSideOnly)
  921. if filterMatchCount > 0 {
  922. for filterIndex, filteredTactics := range server.FilteredTactics {
  923. if filterMatches[filterIndex] {
  924. tactics.merge(includeServerSideOnly, &filteredTactics.Tactics)
  925. }
  926. }
  927. }
  928. // See Tactics.Probability doc comment.
  929. tactics.Probability = 1.0
  930. tacticsData, err := newTacticsData(tactics)
  931. if err != nil {
  932. return nil, errors.Trace(err)
  933. }
  934. server.cachedTacticsData.Set(cacheKey, tacticsData, 0)
  935. return tacticsData, nil
  936. }
  937. func getCacheKey(
  938. includeServerSideOnly bool, hasFilterMatches bool, filterMatches []bool) string {
  939. prefix := "0-"
  940. if includeServerSideOnly {
  941. prefix = "1-"
  942. }
  943. // hasFilterMatches allows for skipping the strings.Builder setup and loop
  944. // entirely.
  945. if !hasFilterMatches {
  946. return prefix
  947. }
  948. var b strings.Builder
  949. _, _ = b.WriteString(prefix)
  950. for filterIndex, match := range filterMatches {
  951. if match {
  952. fmt.Fprintf(&b, "%x-", filterIndex)
  953. }
  954. }
  955. return b.String()
  956. }
  957. // TODO: refactor this copy of psiphon/server.getStringRequestParam into common?
  958. func getStringRequestParam(apiParams common.APIParameters, name string) (string, error) {
  959. if apiParams[name] == nil {
  960. return "", errors.Tracef("missing param: %s", name)
  961. }
  962. value, ok := apiParams[name].(string)
  963. if !ok {
  964. return "", errors.Tracef("invalid param: %s", name)
  965. }
  966. return value, nil
  967. }
  968. // TODO: refactor this copy of psiphon/server.getIntStringRequestParam into common?
  969. func getIntStringRequestParam(params common.APIParameters, name string) (int, error) {
  970. if params[name] == nil {
  971. return 0, errors.Tracef("missing param: %s", name)
  972. }
  973. valueStr, ok := params[name].(string)
  974. if !ok {
  975. return 0, errors.Tracef("invalid param: %s", name)
  976. }
  977. value, err := strconv.Atoi(valueStr)
  978. if !ok {
  979. return 0, errors.Trace(err)
  980. }
  981. return value, nil
  982. }
  983. func getJSONRequestParam(apiParams common.APIParameters, name string, value interface{}) error {
  984. if apiParams[name] == nil {
  985. return errors.Tracef("missing param: %s", name)
  986. }
  987. // Remarshal the parameter from common.APIParameters, as the initial API parameter
  988. // unmarshal will not have known the correct target type. I.e., instead of doing
  989. // unmarshal-into-struct, common.APIParameters will have an unmarshal-into-interface
  990. // value as described here: https://golang.org/pkg/encoding/json/#Unmarshal.
  991. jsonValue, err := json.Marshal(apiParams[name])
  992. if err != nil {
  993. return errors.Trace(err)
  994. }
  995. err = json.Unmarshal(jsonValue, value)
  996. if err != nil {
  997. return errors.Trace(err)
  998. }
  999. return nil
  1000. }
  1001. // aggregate may mutate (sort) the speedTestSamples slice.
  1002. func aggregate(
  1003. aggregation string,
  1004. speedTestSamples []SpeedTestSample,
  1005. aggregatedValues map[string]int) int {
  1006. // Aggregated values are memoized to save recalculating for each filter.
  1007. if value, ok := aggregatedValues[aggregation]; ok {
  1008. return value
  1009. }
  1010. var value int
  1011. switch aggregation {
  1012. case AGGREGATION_MINIMUM:
  1013. value = minimumSampleRTTMilliseconds(speedTestSamples)
  1014. case AGGREGATION_MAXIMUM:
  1015. value = maximumSampleRTTMilliseconds(speedTestSamples)
  1016. case AGGREGATION_MEDIAN:
  1017. value = medianSampleRTTMilliseconds(speedTestSamples)
  1018. default:
  1019. return 0
  1020. }
  1021. aggregatedValues[aggregation] = value
  1022. return value
  1023. }
  1024. func minimumSampleRTTMilliseconds(samples []SpeedTestSample) int {
  1025. if len(samples) == 0 {
  1026. return 0
  1027. }
  1028. min := 0
  1029. for i := 1; i < len(samples); i++ {
  1030. if samples[i].RTTMilliseconds < samples[min].RTTMilliseconds {
  1031. min = i
  1032. }
  1033. }
  1034. return samples[min].RTTMilliseconds
  1035. }
  1036. func maximumSampleRTTMilliseconds(samples []SpeedTestSample) int {
  1037. if len(samples) == 0 {
  1038. return 0
  1039. }
  1040. max := 0
  1041. for i := 1; i < len(samples); i++ {
  1042. if samples[i].RTTMilliseconds > samples[max].RTTMilliseconds {
  1043. max = i
  1044. }
  1045. }
  1046. return samples[max].RTTMilliseconds
  1047. }
  1048. func medianSampleRTTMilliseconds(samples []SpeedTestSample) int {
  1049. if len(samples) == 0 {
  1050. return 0
  1051. }
  1052. // This in-place sort mutates the input slice.
  1053. sort.Slice(
  1054. samples,
  1055. func(i, j int) bool {
  1056. return samples[i].RTTMilliseconds < samples[j].RTTMilliseconds
  1057. })
  1058. // See: https://en.wikipedia.org/wiki/Median#Easy_explanation_of_the_sample_median
  1059. mid := len(samples) / 2
  1060. if len(samples)%2 == 1 {
  1061. return samples[mid].RTTMilliseconds
  1062. }
  1063. return (samples[mid-1].RTTMilliseconds + samples[mid].RTTMilliseconds) / 2
  1064. }
  1065. func (t *Tactics) clone(includeServerSideOnly bool) *Tactics {
  1066. u := &Tactics{
  1067. TTL: t.TTL,
  1068. }
  1069. // Note: there is no deep copy of parameter values; the the returned
  1070. // Tactics shares memory with the original and it individual parameters
  1071. // should not be modified.
  1072. if t.Parameters != nil {
  1073. u.Parameters = make(map[string]interface{})
  1074. for k, v := range t.Parameters {
  1075. if includeServerSideOnly || !parameters.IsServerSideOnly(k) {
  1076. u.Parameters[k] = v
  1077. }
  1078. }
  1079. }
  1080. return u
  1081. }
  1082. func (t *Tactics) merge(includeServerSideOnly bool, u *Tactics) {
  1083. if u.TTL != "" {
  1084. t.TTL = u.TTL
  1085. }
  1086. // Note: there is no deep copy of parameter values; the the returned
  1087. // Tactics shares memory with the original and its individual parameters
  1088. // should not be modified.
  1089. if u.Parameters != nil {
  1090. if t.Parameters == nil {
  1091. t.Parameters = make(map[string]interface{})
  1092. }
  1093. for k, v := range u.Parameters {
  1094. if includeServerSideOnly || !parameters.IsServerSideOnly(k) {
  1095. t.Parameters[k] = v
  1096. }
  1097. }
  1098. }
  1099. }
  1100. // HandleEndPoint routes the request to either handleSpeedTestRequest
  1101. // or handleTacticsRequest; or returns false if not handled.
  1102. func (server *Server) HandleEndPoint(
  1103. endPoint string,
  1104. geoIPData common.GeoIPData,
  1105. w http.ResponseWriter,
  1106. r *http.Request) bool {
  1107. server.ReloadableFile.RLock()
  1108. loaded := server.loaded
  1109. hasRequestKeys := len(server.RequestPublicKey) > 0
  1110. server.ReloadableFile.RUnlock()
  1111. if !loaded || !hasRequestKeys {
  1112. // No tactics configuration was loaded, or the configuration contained
  1113. // no key material for tactics requests.
  1114. return false
  1115. }
  1116. switch endPoint {
  1117. case SPEED_TEST_END_POINT:
  1118. server.handleSpeedTestRequest(geoIPData, w, r)
  1119. return true
  1120. case TACTICS_END_POINT:
  1121. server.handleTacticsRequest(geoIPData, w, r)
  1122. return true
  1123. default:
  1124. return false
  1125. }
  1126. }
  1127. func (server *Server) handleSpeedTestRequest(
  1128. _ common.GeoIPData, w http.ResponseWriter, r *http.Request) {
  1129. _, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, MAX_REQUEST_BODY_SIZE))
  1130. if err != nil {
  1131. server.logger.WithTraceFields(
  1132. common.LogFields{"error": err}).Warning("failed to read request body")
  1133. common.TerminateHTTPConnection(w, r)
  1134. return
  1135. }
  1136. response, err := MakeSpeedTestResponse(
  1137. SPEED_TEST_PADDING_MIN_SIZE, SPEED_TEST_PADDING_MAX_SIZE)
  1138. if err != nil {
  1139. server.logger.WithTraceFields(
  1140. common.LogFields{"error": err}).Warning("failed to make response")
  1141. common.TerminateHTTPConnection(w, r)
  1142. return
  1143. }
  1144. w.WriteHeader(http.StatusOK)
  1145. _, err = w.Write(response)
  1146. if err != nil {
  1147. server.logger.WithTraceFields(
  1148. common.LogFields{"error": err}).Warning("failed to write response")
  1149. common.TerminateHTTPConnection(w, r)
  1150. return
  1151. }
  1152. }
  1153. func (server *Server) handleTacticsRequest(
  1154. geoIPData common.GeoIPData, w http.ResponseWriter, r *http.Request) {
  1155. server.ReloadableFile.RLock()
  1156. requestPrivateKey := server.RequestPrivateKey
  1157. requestObfuscatedKey := server.RequestObfuscatedKey
  1158. server.ReloadableFile.RUnlock()
  1159. // Read, decode, and unbox request payload.
  1160. boxedRequest, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, MAX_REQUEST_BODY_SIZE))
  1161. if err != nil {
  1162. server.logger.WithTraceFields(
  1163. common.LogFields{"error": err}).Warning("failed to read request body")
  1164. common.TerminateHTTPConnection(w, r)
  1165. return
  1166. }
  1167. var apiParams common.APIParameters
  1168. bundledPeerPublicKey, err := unboxPayload(
  1169. TACTICS_REQUEST_NONCE,
  1170. nil,
  1171. requestPrivateKey,
  1172. requestObfuscatedKey,
  1173. boxedRequest,
  1174. &apiParams,
  1175. nil)
  1176. if err != nil {
  1177. server.logger.WithTraceFields(
  1178. common.LogFields{"error": err}).Warning("failed to unbox request")
  1179. common.TerminateHTTPConnection(w, r)
  1180. return
  1181. }
  1182. err = server.apiParameterValidator(apiParams)
  1183. if err != nil {
  1184. server.logger.WithTraceFields(
  1185. common.LogFields{"error": err}).Warning("invalid request parameters")
  1186. common.TerminateHTTPConnection(w, r)
  1187. return
  1188. }
  1189. // When compressed tactics are requested, use CBOR binary encoding for the
  1190. // response.
  1191. compressTactics := protocol.GetCompressTactics(apiParams)
  1192. var responseMarshaler func(any) ([]byte, error)
  1193. if compressTactics {
  1194. responseMarshaler = protocol.CBOREncoding.Marshal
  1195. }
  1196. tacticsPayload, err := server.GetTacticsPayload(geoIPData, apiParams, compressTactics)
  1197. if err == nil && tacticsPayload == nil {
  1198. err = errors.TraceNew("unexpected missing tactics payload")
  1199. }
  1200. if err != nil {
  1201. server.logger.WithTraceFields(
  1202. common.LogFields{"error": err}).Warning("failed to get tactics")
  1203. common.TerminateHTTPConnection(w, r)
  1204. return
  1205. }
  1206. // Marshal, box, and write response payload.
  1207. boxedResponse, err := boxPayload(
  1208. TACTICS_RESPONSE_NONCE,
  1209. bundledPeerPublicKey,
  1210. requestPrivateKey,
  1211. requestObfuscatedKey,
  1212. nil,
  1213. tacticsPayload,
  1214. responseMarshaler)
  1215. if err != nil {
  1216. server.logger.WithTraceFields(
  1217. common.LogFields{"error": err}).Warning("failed to box response")
  1218. common.TerminateHTTPConnection(w, r)
  1219. return
  1220. }
  1221. w.WriteHeader(http.StatusOK)
  1222. _, err = w.Write(boxedResponse)
  1223. if err != nil {
  1224. server.logger.WithTraceFields(
  1225. common.LogFields{"error": err}).Warning("failed to write response")
  1226. common.TerminateHTTPConnection(w, r)
  1227. return
  1228. }
  1229. // Log a metric.
  1230. logFields := server.logFieldFormatter("", geoIPData, apiParams)
  1231. logFields[NEW_TACTICS_TAG_LOG_FIELD_NAME] = tacticsPayload.Tag
  1232. logFields[IS_TACTICS_REQUEST_LOG_FIELD_NAME] = true
  1233. server.logger.LogMetric(TACTICS_METRIC_EVENT_NAME, logFields)
  1234. }
  1235. // ObfuscatedRoundTripper performs a round trip to the specified endpoint,
  1236. // sending the request body and returning the response body, with an
  1237. // obfuscation layer applied to the endpoint value. The context may be used
  1238. // to set a timeout or cancel the round trip.
  1239. //
  1240. // The Psiphon client provides a ObfuscatedRoundTripper using MeekConn. The
  1241. // client will handle connection details including server selection, dialing
  1242. // details including device binding and upstream proxy, etc.
  1243. type ObfuscatedRoundTripper func(
  1244. ctx context.Context,
  1245. endPoint string,
  1246. requestBody []byte) ([]byte, error)
  1247. // Storer provides a facility to persist tactics and speed test data.
  1248. type Storer interface {
  1249. SetTacticsRecord(networkID string, record []byte) error
  1250. GetTacticsRecord(networkID string) ([]byte, error)
  1251. SetSpeedTestSamplesRecord(networkID string, record []byte) error
  1252. GetSpeedTestSamplesRecord(networkID string) ([]byte, error)
  1253. }
  1254. // SetTacticsAPIParameters populates apiParams with the additional
  1255. // parameters for tactics. This is used by the Psiphon client when
  1256. // preparing its handshake request.
  1257. func SetTacticsAPIParameters(
  1258. storer Storer,
  1259. networkID string,
  1260. apiParams common.APIParameters) error {
  1261. // TODO: store the tag in its own record to avoid loading the whole tactics record?
  1262. record, err := getStoredTacticsRecord(storer, networkID)
  1263. if err != nil {
  1264. return errors.Trace(err)
  1265. }
  1266. speedTestSamples, err := getSpeedTestSamples(storer, networkID)
  1267. if err != nil {
  1268. return errors.Trace(err)
  1269. }
  1270. apiParams[STORED_TACTICS_TAG_PARAMETER_NAME] = record.Tag
  1271. apiParams[SPEED_TEST_SAMPLES_PARAMETER_NAME] = speedTestSamples
  1272. return nil
  1273. }
  1274. // HandleTacticsPayload updates the stored tactics with the given payload. If
  1275. // the payload has a new tag/tactics, this is stored and a new expiry time is
  1276. // set. If the payload has the same tag, the existing tactics are retained,
  1277. // the expiry is extended using the previous TTL, and a nil record is
  1278. // returned.
  1279. //
  1280. // HandleTacticsPayload is called by the Psiphon client to handle the tactics
  1281. // payload in the API handshake and inproxy broker responses. As the Psiphon
  1282. // client has already called UseStoredTactics/FetchTactics and applied
  1283. // tactics, the nil record return value allows the caller to skip an
  1284. // unnecessary tactics parameters application.
  1285. func HandleTacticsPayload(
  1286. storer Storer,
  1287. networkID string,
  1288. payload *Payload) (*Record, error) {
  1289. // Note: since, in the client, a tactics request and a handshake
  1290. // request could be in flight concurrently, there exists a possibility
  1291. // that one clobbers the others result, and the clobbered result may
  1292. // be newer.
  1293. //
  1294. // However:
  1295. // - in the Storer, the tactics record is a single key/value, so its
  1296. // elements are updated atomically;
  1297. // - the client Controller typically stops/aborts any outstanding
  1298. // tactics request before the handshake
  1299. // - this would have to be concurrent with a tactics configuration hot
  1300. // reload on the server
  1301. // - old and new tactics should both be valid
  1302. if payload == nil {
  1303. return nil, errors.TraceNew("unexpected nil payload")
  1304. }
  1305. record, err := getStoredTacticsRecord(storer, networkID)
  1306. if err != nil {
  1307. return nil, errors.Trace(err)
  1308. }
  1309. newTactics, err := applyTacticsPayload(storer, networkID, record, payload)
  1310. if err != nil {
  1311. return nil, errors.Trace(err)
  1312. }
  1313. // Store the tactics record, which may contain new tactics, and always
  1314. // contains an extended TTL.
  1315. //
  1316. // TODO: if tags match, just set an expiry record, not the whole tactics
  1317. // record?
  1318. err = setStoredTacticsRecord(storer, networkID, record)
  1319. if err != nil {
  1320. return nil, errors.Trace(err)
  1321. }
  1322. if !newTactics {
  1323. // Don't return a tactics record when the tactics have not changed.
  1324. record = nil
  1325. }
  1326. return record, nil
  1327. }
  1328. // UseStoredTactics checks for an unexpired stored tactics record for the
  1329. // given network ID that may be used immediately. When there is no error
  1330. // and the record is nil, the caller should proceed with FetchTactics.
  1331. //
  1332. // When used, Record.Tag should be reported as the applied tactics tag.
  1333. func UseStoredTactics(
  1334. storer Storer, networkID string) (*Record, error) {
  1335. record, err := getStoredTacticsRecord(storer, networkID)
  1336. if err != nil {
  1337. return nil, errors.Trace(err)
  1338. }
  1339. if record.Tag != "" && record.Expiry.After(time.Now().UTC()) {
  1340. return record, nil
  1341. }
  1342. return nil, nil
  1343. }
  1344. // FetchTactics performs a tactics request. When there are no stored
  1345. // speed test samples for the network ID, a speed test request is
  1346. // performed immediately before the tactics request, using the same
  1347. // ObfuscatedRoundTripper.
  1348. //
  1349. // The ObfuscatedRoundTripper transport should be established in advance, so
  1350. // that calls to ObfuscatedRoundTripper don't take additional time in TCP,
  1351. // TLS, etc. handshakes.
  1352. //
  1353. // The caller should first call UseStoredTactics and skip FetchTactics
  1354. // when there is an unexpired stored tactics record available. The
  1355. // caller is expected to set any overall timeout in the context input.
  1356. //
  1357. // Limitation: it is assumed that the network ID obtained from getNetworkID
  1358. // is the one that is active when the tactics request is received by the
  1359. // server. However, it is remotely possible to switch networks
  1360. // immediately after invoking the GetNetworkID callback and initiating
  1361. // the request. This is partially mitigated by rechecking the network ID
  1362. // after the request and failing if it differs from the initial network ID.
  1363. //
  1364. // FetchTactics modifies the apiParams input.
  1365. func FetchTactics(
  1366. ctx context.Context,
  1367. params *parameters.Parameters,
  1368. storer Storer,
  1369. getNetworkID func() string,
  1370. apiParams common.APIParameters,
  1371. endPointRegion string,
  1372. endPointProtocol string,
  1373. encodedRequestPublicKey string,
  1374. encodedRequestObfuscatedKey string,
  1375. obfuscatedRoundTripper ObfuscatedRoundTripper) (*Record, error) {
  1376. p := params.Get()
  1377. speedTestPaddingMinBytes := p.Int(parameters.SpeedTestPaddingMinBytes)
  1378. speedTestPaddingMaxBytes := p.Int(parameters.SpeedTestPaddingMaxBytes)
  1379. compressTactics := p.Bool(parameters.CompressTactics)
  1380. p.Close()
  1381. networkID := getNetworkID()
  1382. record, err := getStoredTacticsRecord(storer, networkID)
  1383. if err != nil {
  1384. return nil, errors.Trace(err)
  1385. }
  1386. speedTestSamples, err := getSpeedTestSamples(storer, networkID)
  1387. if err != nil {
  1388. return nil, errors.Trace(err)
  1389. }
  1390. // Perform a speed test when there are no samples.
  1391. if len(speedTestSamples) == 0 {
  1392. request := prng.Padding(speedTestPaddingMinBytes, speedTestPaddingMaxBytes)
  1393. startTime := time.Now()
  1394. response, err := obfuscatedRoundTripper(ctx, SPEED_TEST_END_POINT, request)
  1395. elapsedTime := time.Since(startTime)
  1396. if err != nil {
  1397. return nil, errors.Trace(err)
  1398. }
  1399. if networkID != getNetworkID() {
  1400. return nil, errors.TraceNew("network ID changed")
  1401. }
  1402. err = AddSpeedTestSample(
  1403. params,
  1404. storer,
  1405. networkID,
  1406. endPointRegion,
  1407. endPointProtocol,
  1408. elapsedTime,
  1409. request,
  1410. response)
  1411. if err != nil {
  1412. return nil, errors.Trace(err)
  1413. }
  1414. speedTestSamples, err = getSpeedTestSamples(storer, networkID)
  1415. if err != nil {
  1416. return nil, errors.Trace(err)
  1417. }
  1418. }
  1419. // Perform the tactics request.
  1420. apiParams[STORED_TACTICS_TAG_PARAMETER_NAME] = record.Tag
  1421. apiParams[SPEED_TEST_SAMPLES_PARAMETER_NAME] = speedTestSamples
  1422. // When requesting compressed tactics, the response will use CBOR binary
  1423. // encoding.
  1424. var responseUnmarshaler func([]byte, any) error
  1425. if compressTactics {
  1426. protocol.SetCompressTactics(apiParams)
  1427. responseUnmarshaler = cbor.Unmarshal
  1428. }
  1429. requestPublicKey, err := base64.StdEncoding.DecodeString(encodedRequestPublicKey)
  1430. if err != nil {
  1431. return nil, errors.Trace(err)
  1432. }
  1433. requestObfuscatedKey, err := base64.StdEncoding.DecodeString(encodedRequestObfuscatedKey)
  1434. if err != nil {
  1435. return nil, errors.Trace(err)
  1436. }
  1437. ephemeralPublicKey, ephemeralPrivateKey, err := box.GenerateKey(rand.Reader)
  1438. if err != nil {
  1439. return nil, errors.Trace(err)
  1440. }
  1441. boxedRequest, err := boxPayload(
  1442. TACTICS_REQUEST_NONCE,
  1443. requestPublicKey,
  1444. ephemeralPrivateKey[:],
  1445. requestObfuscatedKey,
  1446. ephemeralPublicKey[:],
  1447. &apiParams,
  1448. nil)
  1449. if err != nil {
  1450. return nil, errors.Trace(err)
  1451. }
  1452. boxedResponse, err := obfuscatedRoundTripper(ctx, TACTICS_END_POINT, boxedRequest)
  1453. if err != nil {
  1454. return nil, errors.Trace(err)
  1455. }
  1456. if networkID != getNetworkID() {
  1457. return nil, errors.TraceNew("network ID changed")
  1458. }
  1459. // Process and store the response payload.
  1460. var payload *Payload
  1461. _, err = unboxPayload(
  1462. TACTICS_RESPONSE_NONCE,
  1463. requestPublicKey,
  1464. ephemeralPrivateKey[:],
  1465. requestObfuscatedKey,
  1466. boxedResponse,
  1467. &payload,
  1468. responseUnmarshaler)
  1469. if err != nil {
  1470. return nil, errors.Trace(err)
  1471. }
  1472. _, err = applyTacticsPayload(storer, networkID, record, payload)
  1473. if err != nil {
  1474. return nil, errors.Trace(err)
  1475. }
  1476. err = setStoredTacticsRecord(storer, networkID, record)
  1477. if err != nil {
  1478. return nil, errors.Trace(err)
  1479. }
  1480. return record, nil
  1481. }
  1482. // MakeSpeedTestResponse creates a speed test response prefixed
  1483. // with a timestamp and followed by random padding. The timestamp
  1484. // enables the client performing the speed test to record the
  1485. // sample time with an accurate server clock; the random padding
  1486. // is to frustrate fingerprinting.
  1487. // The speed test timestamp is truncated as a privacy measure.
  1488. func MakeSpeedTestResponse(minPadding, maxPadding int) ([]byte, error) {
  1489. // MarshalBinary encoding (version 1) is 15 bytes:
  1490. // https://github.com/golang/go/blob/release-branch.go1.9/src/time/time.go#L1112
  1491. timestamp, err := time.Now().UTC().Truncate(1 * time.Hour).MarshalBinary()
  1492. if err == nil && len(timestamp) > 255 {
  1493. err = fmt.Errorf("unexpected marshaled time size: %d", len(timestamp))
  1494. }
  1495. if err != nil {
  1496. return nil, errors.Trace(err)
  1497. }
  1498. randomPadding := prng.Padding(minPadding, maxPadding)
  1499. // On error, proceed without random padding.
  1500. // TODO: log error, even if proceeding?
  1501. response := make([]byte, 0, 1+len(timestamp)+len(randomPadding))
  1502. response = append(response, byte(len(timestamp)))
  1503. response = append(response, timestamp...)
  1504. response = append(response, randomPadding...)
  1505. return response, nil
  1506. }
  1507. // AddSpeedTestSample stores a new speed test sample. A maximum of
  1508. // SpeedTestMaxSampleCount samples per network ID are stored, so once
  1509. // that limit is reached, the oldest samples are removed to make room
  1510. // for the new sample.
  1511. func AddSpeedTestSample(
  1512. params *parameters.Parameters,
  1513. storer Storer,
  1514. networkID string,
  1515. endPointRegion string,
  1516. endPointProtocol string,
  1517. elaspedTime time.Duration,
  1518. request []byte,
  1519. response []byte) error {
  1520. if len(response) < 1 {
  1521. return errors.TraceNew("unexpected empty response")
  1522. }
  1523. timestampLength := int(response[0])
  1524. if len(response) < 1+timestampLength {
  1525. return errors.Tracef(
  1526. "unexpected response shorter than timestamp size %d", timestampLength)
  1527. }
  1528. var timestamp time.Time
  1529. err := timestamp.UnmarshalBinary(response[1 : 1+timestampLength])
  1530. if err != nil {
  1531. return errors.Trace(err)
  1532. }
  1533. sample := SpeedTestSample{
  1534. Timestamp: timestamp,
  1535. EndPointRegion: endPointRegion,
  1536. EndPointProtocol: endPointProtocol,
  1537. RTTMilliseconds: int(elaspedTime / time.Millisecond),
  1538. BytesUp: len(request),
  1539. BytesDown: len(response),
  1540. }
  1541. maxCount := params.Get().Int(parameters.SpeedTestMaxSampleCount)
  1542. if maxCount == 0 {
  1543. return errors.TraceNew("speed test max sample count is 0")
  1544. }
  1545. speedTestSamples, err := getSpeedTestSamples(storer, networkID)
  1546. if err != nil {
  1547. return errors.Trace(err)
  1548. }
  1549. if speedTestSamples == nil {
  1550. speedTestSamples = make([]SpeedTestSample, 0)
  1551. }
  1552. if len(speedTestSamples)+1 > maxCount {
  1553. speedTestSamples = speedTestSamples[len(speedTestSamples)+1-maxCount:]
  1554. }
  1555. speedTestSamples = append(speedTestSamples, sample)
  1556. record, err := json.Marshal(speedTestSamples)
  1557. if err != nil {
  1558. return errors.Trace(err)
  1559. }
  1560. err = storer.SetSpeedTestSamplesRecord(networkID, record)
  1561. if err != nil {
  1562. return errors.Trace(err)
  1563. }
  1564. return nil
  1565. }
  1566. func getSpeedTestSamples(
  1567. storer Storer, networkID string) ([]SpeedTestSample, error) {
  1568. record, err := storer.GetSpeedTestSamplesRecord(networkID)
  1569. if err != nil {
  1570. return nil, errors.Trace(err)
  1571. }
  1572. if record == nil {
  1573. return nil, nil
  1574. }
  1575. var speedTestSamples []SpeedTestSample
  1576. err = json.Unmarshal(record, &speedTestSamples)
  1577. if err != nil {
  1578. return nil, errors.Trace(err)
  1579. }
  1580. return speedTestSamples, nil
  1581. }
  1582. func getStoredTacticsRecord(
  1583. storer Storer, networkID string) (*Record, error) {
  1584. marshaledRecord, err := storer.GetTacticsRecord(networkID)
  1585. if err != nil {
  1586. return nil, errors.Trace(err)
  1587. }
  1588. if marshaledRecord == nil {
  1589. return &Record{}, nil
  1590. }
  1591. var record *Record
  1592. err = json.Unmarshal(marshaledRecord, &record)
  1593. if err != nil {
  1594. return nil, errors.Trace(err)
  1595. }
  1596. if record == nil {
  1597. record = &Record{}
  1598. }
  1599. return record, nil
  1600. }
  1601. func applyTacticsPayload(
  1602. storer Storer,
  1603. networkID string,
  1604. record *Record,
  1605. payload *Payload) (bool, error) {
  1606. newTactics := false
  1607. if payload.Tag == "" {
  1608. return newTactics, errors.TraceNew("invalid tag")
  1609. }
  1610. // Replace the tactics data when the tags differ.
  1611. if payload.Tag != record.Tag {
  1612. // There is a potential race condition that may arise with multiple
  1613. // concurrent requests which may return tactics, such as in-proxy
  1614. // proxy announcements. In this scenario, an in-flight request
  1615. // matches the existing current tactics tag; then a concurrent
  1616. // request is sent while new tactics become available and its
  1617. // response returns new tactics and a new tag; the client applies the
  1618. // new tags and tactics; then, finally, the response for the first
  1619. // request arrives with a now apparently different tag -- the
  1620. // original tag -- but no tactics payload. In this case, simply fail
  1621. // the apply operation.
  1622. // A nil payload.Tactics, of type json.RawMessage, can be serialized
  1623. // as the JSON "null".
  1624. if payload.Tactics == nil ||
  1625. bytes.Equal(payload.Tactics, []byte("null")) {
  1626. return newTactics, errors.TraceNew("missing tactics")
  1627. }
  1628. payloadTactics, err := common.Decompress(
  1629. payload.TacticsCompression, payload.Tactics)
  1630. if err != nil {
  1631. return newTactics, errors.Trace(err)
  1632. }
  1633. record.Tag = payload.Tag
  1634. record.Tactics = Tactics{}
  1635. err = json.Unmarshal(payloadTactics, &record.Tactics)
  1636. if err != nil {
  1637. return newTactics, errors.Trace(err)
  1638. }
  1639. newTactics = true
  1640. }
  1641. // Note: record.Tactics.TTL is validated by server
  1642. ttl, err := time.ParseDuration(record.Tactics.TTL)
  1643. if err != nil {
  1644. return newTactics, errors.Trace(err)
  1645. }
  1646. if ttl <= 0 {
  1647. return newTactics, errors.TraceNew("invalid TTL")
  1648. }
  1649. // Set or extend the expiry.
  1650. record.Expiry = time.Now().UTC().Add(ttl)
  1651. return newTactics, nil
  1652. }
  1653. func setStoredTacticsRecord(
  1654. storer Storer,
  1655. networkID string,
  1656. record *Record) error {
  1657. marshaledRecord, err := json.Marshal(record)
  1658. if err != nil {
  1659. return errors.Trace(err)
  1660. }
  1661. err = storer.SetTacticsRecord(networkID, marshaledRecord)
  1662. if err != nil {
  1663. return errors.Trace(err)
  1664. }
  1665. return nil
  1666. }
  1667. func boxPayload(
  1668. nonce, peerPublicKey, privateKey, obfuscatedKey, bundlePublicKey []byte,
  1669. payload interface{},
  1670. marshaler func(any) ([]byte, error)) ([]byte, error) {
  1671. if len(nonce) > 24 ||
  1672. len(peerPublicKey) != 32 ||
  1673. len(privateKey) != 32 {
  1674. return nil, errors.TraceNew("unexpected box key length")
  1675. }
  1676. var marshaledPayload []byte
  1677. var err error
  1678. if marshaler == nil {
  1679. marshaler = json.Marshal
  1680. }
  1681. marshaledPayload, err = marshaler(payload)
  1682. if err != nil {
  1683. return nil, errors.Trace(err)
  1684. }
  1685. var nonceArray [24]byte
  1686. copy(nonceArray[:], nonce)
  1687. var peerPublicKeyArray, privateKeyArray [32]byte
  1688. copy(peerPublicKeyArray[:], peerPublicKey)
  1689. copy(privateKeyArray[:], privateKey)
  1690. box := box.Seal(nil, marshaledPayload, &nonceArray, &peerPublicKeyArray, &privateKeyArray)
  1691. if bundlePublicKey != nil {
  1692. bundledBox := make([]byte, 32+len(box))
  1693. copy(bundledBox[0:32], bundlePublicKey[0:32])
  1694. copy(bundledBox[32:], box)
  1695. box = bundledBox
  1696. }
  1697. // TODO: replay tactics request padding?
  1698. paddingPRNGSeed, err := prng.NewSeed()
  1699. if err != nil {
  1700. return nil, errors.Trace(err)
  1701. }
  1702. maxPadding := TACTICS_PADDING_MAX_SIZE
  1703. obfuscator, err := obfuscator.NewClientObfuscator(
  1704. &obfuscator.ObfuscatorConfig{
  1705. Keyword: string(obfuscatedKey),
  1706. PaddingPRNGSeed: paddingPRNGSeed,
  1707. MaxPadding: &maxPadding})
  1708. if err != nil {
  1709. return nil, errors.Trace(err)
  1710. }
  1711. obfuscatedBox, _ := obfuscator.SendPreamble()
  1712. seedLen := len(obfuscatedBox)
  1713. obfuscatedBox = append(obfuscatedBox, box...)
  1714. obfuscator.ObfuscateClientToServer(obfuscatedBox[seedLen:])
  1715. return obfuscatedBox, nil
  1716. }
  1717. // unboxPayload mutates obfuscatedBoxedPayload by deobfuscating in-place.
  1718. func unboxPayload(
  1719. nonce, peerPublicKey, privateKey, obfuscatedKey, obfuscatedBoxedPayload []byte,
  1720. payload interface{},
  1721. unmarshaler func([]byte, any) error) ([]byte, error) {
  1722. if len(nonce) > 24 ||
  1723. (peerPublicKey != nil && len(peerPublicKey) != 32) ||
  1724. len(privateKey) != 32 {
  1725. return nil, errors.TraceNew("unexpected box key length")
  1726. }
  1727. obfuscatedReader := bytes.NewReader(obfuscatedBoxedPayload[:])
  1728. obfuscator, err := obfuscator.NewServerObfuscator(
  1729. &obfuscator.ObfuscatorConfig{Keyword: string(obfuscatedKey)},
  1730. "",
  1731. obfuscatedReader)
  1732. if err != nil {
  1733. return nil, errors.Trace(err)
  1734. }
  1735. seedLen, err := obfuscatedReader.Seek(0, 1)
  1736. if err != nil {
  1737. return nil, errors.Trace(err)
  1738. }
  1739. boxedPayload := obfuscatedBoxedPayload[seedLen:]
  1740. obfuscator.ObfuscateClientToServer(boxedPayload)
  1741. var nonceArray [24]byte
  1742. copy(nonceArray[:], nonce)
  1743. var peerPublicKeyArray, privateKeyArray [32]byte
  1744. copy(privateKeyArray[:], privateKey)
  1745. var bundledPeerPublicKey []byte
  1746. if peerPublicKey != nil {
  1747. copy(peerPublicKeyArray[:], peerPublicKey)
  1748. } else {
  1749. if len(boxedPayload) < 32 {
  1750. return nil, errors.TraceNew("unexpected box size")
  1751. }
  1752. bundledPeerPublicKey = boxedPayload[0:32]
  1753. copy(peerPublicKeyArray[:], bundledPeerPublicKey)
  1754. boxedPayload = boxedPayload[32:]
  1755. }
  1756. marshaledPayload, ok := box.Open(nil, boxedPayload, &nonceArray, &peerPublicKeyArray, &privateKeyArray)
  1757. if !ok {
  1758. return nil, errors.TraceNew("invalid box")
  1759. }
  1760. if unmarshaler == nil {
  1761. unmarshaler = json.Unmarshal
  1762. }
  1763. err = unmarshaler(marshaledPayload, payload)
  1764. if err != nil {
  1765. return nil, errors.Trace(err)
  1766. }
  1767. return bundledPeerPublicKey, nil
  1768. }