api.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. /*
  2. * Copyright (c) 2025, 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 dsl implements the Dynamic Server List (DSL) mechanism.
  20. //
  21. // Unlike Remote Server Lists (RSLs) and Obfuscated Server Lists (OSLs), which
  22. // are based on static file downloads, with DSLs the client requests
  23. // discovery and download of server entries from a DSL backend that actively
  24. // selects from compartmentalized servers based on the client's inputs and
  25. // other properties.
  26. //
  27. // Clients use relays with obfuscation and blocking resistence properties to
  28. // transport requests to a DSL backend.
  29. //
  30. // The discovery concepts of OSLs are retained with the client reporting its
  31. // known OSL keys to the DSL backend, as a proof-of-knowledge used to access
  32. // certain compartments of servers.
  33. package dsl
  34. import (
  35. "encoding/base64"
  36. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
  37. )
  38. type OSLID []byte
  39. type OSLKey []byte
  40. type OSLFileSpec []byte
  41. // DiscoverServerEntriesRequest is a request from a client to potentially
  42. // discover new server entries. The DSL backend serving the request selects
  43. // discoverable server entries using a combination of inputs in
  44. // BaseAPIParameters; active OSL keys known to the client; and client GeoIP
  45. // data.
  46. //
  47. // The response contains a list of server entry tags and versions, and
  48. // the client will then proceed to request full server entries for unknown or
  49. // stale server entries, based on the tags and versions. DiscoverCount
  50. // specifies a maximum number of server entry tags/versions to return; the
  51. // DSL backend may return less, but not more.
  52. type DiscoverServerEntriesRequest struct {
  53. BaseAPIParameters protocol.PackedAPIParameters `cbor:"1,keyasint,omitempty"`
  54. OSLKeys []OSLKey `cbor:"2,keyasint,omitempty"`
  55. DiscoverCount int32 `cbor:"3,keyasint,omitempty"`
  56. }
  57. // ServerEntryTag is a binary representation of a protocol.ServerEntry.Tag
  58. // value. Hex- or base64-encoded tag strings should be converted to binary
  59. // for compactness.
  60. type ServerEntryTag []byte
  61. // MarshalText emits server entry tag as base64 with padding.
  62. // Uses the same string encoding as protocol.GenerateServerEntryTag.
  63. func (tag ServerEntryTag) MarshalText() ([]byte, error) {
  64. return []byte(tag.String()), nil
  65. }
  66. // String emits server entry tag as base64 with padding.
  67. // Uses the same string encoding as protocol.GenerateServerEntryTag.
  68. func (tag ServerEntryTag) String() string {
  69. return base64.StdEncoding.EncodeToString(tag)
  70. }
  71. // VersionedServerEntryTag is a server entry tag and version pair returned in
  72. // DiscoverServerEntriesResponse. When the client already has a server entry
  73. // for the specified tag, the client uses the version field to determine
  74. // whether the server entry needs to be updated. In addition, this return
  75. // value includes a PrioritizeDial hint, from the DSL backend, that the
  76. // server entry is expected to be effective for the client and the client
  77. // should prioritize the server in establishment scheduling.
  78. type VersionedServerEntryTag struct {
  79. Tag ServerEntryTag `cbor:"1,keyasint,omitempty"`
  80. Version int32 `cbor:"2,keyasint,omitempty"`
  81. PrioritizeDial bool `cbor:"3,keyasint,omitempty"`
  82. }
  83. // DiscoverServerEntriesResponse is the set of server entries revealed to the
  84. // client, specified as server entry tag and version pairs, which enable the
  85. // client to determine if it already has the server entry, and has the latest
  86. // version. For new or updated server entries, the client will proceed to
  87. // send a GetServerEntriesRequest to fetch the server entries.
  88. type DiscoverServerEntriesResponse struct {
  89. VersionedServerEntryTags []*VersionedServerEntryTag `cbor:"1,keyasint,omitempty"`
  90. }
  91. // GetServerEntriesRequest is a request from a client to download the
  92. // specified server entries.
  93. type GetServerEntriesRequest struct {
  94. BaseAPIParameters protocol.PackedAPIParameters `cbor:"1,keyasint,omitempty"`
  95. ServerEntryTags []ServerEntryTag `cbor:"2,keyasint,omitempty"`
  96. }
  97. // SourcedServerEntry is a server entry and server entry source pair. The
  98. // client stores the server entry source as protocol.ServerEntry.LocalSource,
  99. // which is used for server_entry_source stats reporting.
  100. type SourcedServerEntry struct {
  101. ServerEntryFields protocol.PackedServerEntryFields `cbor:"1,keyasint,omitempty"`
  102. Source string `cbor:"2,keyasint,omitempty"`
  103. }
  104. // GetServerEntriesResponse includes the list of server entries requested by
  105. // the client. Each requested tag has a corresponding entry in
  106. // SourcedServerEntries, in requested order. When a requested tag is no
  107. // longer available for distribution, there is a nil/empty entry.
  108. type GetServerEntriesResponse struct {
  109. SourcedServerEntries []*SourcedServerEntry `cbor:"1,keyasint,omitempty"`
  110. }
  111. // GetActiveOSLsRequest is a request from a client to get the list of
  112. // currently active OSL IDs.
  113. //
  114. // Clients maintain local copies of the OSL FileSpec for each active OSL,
  115. // using SLOKs to reassemble the keys for the OSLs using the key split
  116. // definitions in each OSL FileSpec. These current OSL keys, reassembled by
  117. // the client, are then included in DiscoverServerEntriesRequest requests,
  118. // demonstrating that the client can decrypt the OSL in the classic scheme;
  119. // the DSL backend uses the keys as proof-of-knowledge to grant access to
  120. // compartmentalized server entries.
  121. //
  122. // For new and unknown OSL IDs, clients will use GetOSLFileSpecsRequest to
  123. // download the corresponding OSL FileSpecs.
  124. //
  125. // It is assumed that the number of OSL schemes and scheme pave counts
  126. // (see common/osl.Config) produces an OSL ID list size that is appropriate
  127. // to return in full in a single response.
  128. type GetActiveOSLsRequest struct {
  129. BaseAPIParameters protocol.PackedAPIParameters `cbor:"1,keyasint,omitempty"`
  130. }
  131. // GetActiveOSLsResponse is a list of the currently active OSL IDs.
  132. type GetActiveOSLsResponse struct {
  133. ActiveOSLIDs []OSLID `cbor:"1,keyasint,omitempty"`
  134. }
  135. // GetOSLFileSpecsRequest is a request from a client to download the
  136. // OSL FileSpecs for the OSLs specified by ID.
  137. type GetOSLFileSpecsRequest struct {
  138. BaseAPIParameters protocol.PackedAPIParameters `cbor:"1,keyasint,omitempty"`
  139. OSLIDs []OSLID `cbor:"2,keyasint,omitempty"`
  140. }
  141. // GetOSLFileSpecsResponse includes the list of OSL FileSpecs requested by the
  142. // client. Each requested OSL ID has a corresponding entry in OSLFileSpecs.
  143. // When a requsted OSL is no longer active or available for distribution,
  144. // there is a nil/empty entry.
  145. //
  146. // Here, OSLFileSpec is a []byte, not an osl.FileSpec, as this value doesn't
  147. // need to be unmarshaled immediately in the fetcher processing.
  148. type GetOSLFileSpecsResponse struct {
  149. OSLFileSpecs []OSLFileSpec `cbor:"1,keyasint,omitempty"`
  150. }
  151. // Relay API layer
  152. //
  153. // DSL clients send requests to the DSL backend via a relay, which provides
  154. // circumvention and blocking resistance. Relays include in-proxy brokers,
  155. // with untunneled domain fronting over a secure Noise session; and Psiphon
  156. // servers, via SSH requests within an established tunnel. The relays remove
  157. // the RelayedRequest layer and forward requests to the DSL backend over
  158. // HTTPS with mutually authenticated TLS; and wrap responses with
  159. // RelayedResponse.
  160. //
  161. // The trusted relays will attach the original client IP and GeoIP data to
  162. // relayed requests; these inputs may be used by the DSL backend when
  163. // selecting server entries that the client may discover.
  164. //
  165. // 1. client -> broker/psiphond relay
  166. // CBOR[RelayedRequest(requestTypeDiscoverServerEntries, v1, CBOR[DiscoverServerEntriesRequest])]
  167. //
  168. // 2. broker/psiphond -> DSL
  169. // POST /DiscoverServerEntries/v1 HTTP/1.1
  170. // X-Psiphon-Client-IP: x.x.x.x
  171. // CBOR[DiscoverServerEntriesRequest]
  172. //
  173. // 3. DSL -> broker/psiphond
  174. // HTTP/1.1 200 OK
  175. // CBOR[DiscoverServerEntriesResponse]
  176. //
  177. // 4. broker/psiphond -> client
  178. // CBOR[RelayedResponse(ErrorCode, CBOR[DiscoverServerEntriesResponse])]
  179. //
  180. // MaxRelayPayloadSize is bounded by inproxy.BrokerMaxRequestBodySize,
  181. // 64K, and the common/crypto/ssh maxPacket, 256K.
  182. const MaxRelayPayloadSize = 65536
  183. const (
  184. PsiphonClientIPHeader = "X-Psiphon-Client-Ip"
  185. PsiphonClientGeoIPDataHeader = "X-Psiphon-Client-Geoipdata"
  186. PsiphonClientTunneledHeader = "X-Psiphon-Client-Tunneled"
  187. PsiphonHostIDHeader = "X-Psiphon-Host-Id"
  188. RequestPathDiscoverServerEntries = "/v1/DiscoverServerEntries"
  189. RequestPathGetServerEntries = "/v1/GetServerEntries"
  190. RequestPathGetActiveOSLs = "/v1/GetActiveOSLs"
  191. RequestPathGetOSLFileSpecs = "/v1/GetOSLFileSpecs"
  192. requestVersion = 0
  193. requestTypeDiscoverServerEntries = 1
  194. requestTypeGetServerEntries = 2
  195. requestTypeGetActiveOSLs = 3
  196. requestTypeGetOSLFileSpecs = 4
  197. )
  198. var requestTypeToHTTPPath = map[int32]string{
  199. requestTypeDiscoverServerEntries: RequestPathDiscoverServerEntries,
  200. requestTypeGetServerEntries: RequestPathGetServerEntries,
  201. requestTypeGetActiveOSLs: RequestPathGetActiveOSLs,
  202. requestTypeGetOSLFileSpecs: RequestPathGetOSLFileSpecs,
  203. }
  204. // RelayedRequest wraps a DSL request to be relayed. RequestType indicates the
  205. // type of the wrapped request. Version must be 0.
  206. type RelayedRequest struct {
  207. RequestType int32 `cbor:"1,keyasint,omitempty"`
  208. Version int32 `cbor:"2,keyasint,omitempty"`
  209. Request []byte `cbor:"3,keyasint,omitempty"`
  210. }
  211. // RelayedResponse wraps a DSL response value or error.
  212. type RelayedResponse struct {
  213. Error int32 `cbor:"1,keyasint,omitempty"`
  214. Compression int32 `cbor:"2,keyasint,omitempty"`
  215. Response []byte `cbor:"3,keyasint,omitempty"`
  216. }