inproxy.go 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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 server
  20. import (
  21. "bytes"
  22. "context"
  23. "crypto/tls"
  24. "crypto/x509"
  25. "io"
  26. "net"
  27. "net/http"
  28. "net/url"
  29. "strings"
  30. "time"
  31. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
  32. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
  33. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/inproxy"
  34. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
  35. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
  36. )
  37. // MakeInproxyProxyQualityBrokerRoundTripper creates a new
  38. // InproxyProxyQualityBrokerRoundTripper for an in-proxy broker specified by
  39. // public key.
  40. func MakeInproxyProxyQualityBrokerRoundTripper(
  41. support *SupportServices,
  42. brokerPublicKey inproxy.SessionPublicKey) (
  43. *InproxyProxyQualityBrokerRoundTripper, common.APIParameters, error) {
  44. // Lookup the broker dial information in InproxyAllBrokerSpecs.
  45. //
  46. // Assumes no GeoIP targeting for InproxyAllBrokerSpecs.
  47. p, err := support.ServerTacticsParametersCache.Get(NewGeoIPData())
  48. if err != nil {
  49. return nil, nil, errors.Trace(err)
  50. }
  51. if p.IsNil() {
  52. return nil, nil, errors.TraceNew("missing tactics")
  53. }
  54. // Fall back to InproxyBrokerSpecs if InproxyAllBrokerSpecs is not
  55. // configured.
  56. brokerSpecs := p.InproxyBrokerSpecs(
  57. parameters.InproxyAllBrokerSpecs, parameters.InproxyBrokerSpecs)
  58. // InproxyProxyQualityReporterTrustedCACertificates and
  59. // InproxyProxyQualityReporterAdditionalHeaders are intended to support
  60. // testing.
  61. trustedCACertificates := p.String(
  62. parameters.InproxyProxyQualityReporterTrustedCACertificates)
  63. if trustedCACertificates != "" {
  64. // Convert JSON-escaped "/n" back to PEM newlines.
  65. trustedCACertificates = strings.ReplaceAll(trustedCACertificates, "\\n", "\n")
  66. }
  67. additionalHeaders := p.HTTPHeaders(
  68. parameters.InproxyProxyQualityReporterAdditionalHeaders)
  69. // This linear search over all broker specs is suitable for a handful of
  70. // brokers, and assumes broker round trippers are reused and not
  71. // recreated continuously.
  72. brokerPublicKeyStr := brokerPublicKey.String()
  73. for _, brokerSpec := range brokerSpecs {
  74. if brokerSpec.BrokerPublicKey == brokerPublicKeyStr {
  75. roundTripper, err := NewInproxyProxyQualityBrokerRoundTripper(
  76. brokerSpec, trustedCACertificates, additionalHeaders)
  77. if err != nil {
  78. return nil, nil, errors.Trace(err)
  79. }
  80. return roundTripper, roundTripper.dialParams, nil
  81. }
  82. }
  83. return nil, nil, errors.Tracef("broker public key not found: %s", brokerPublicKeyStr)
  84. }
  85. // InproxyProxyQualityBrokerRoundTripper is a broker request round trip
  86. // network transport which implements the inproxy.RoundTripper interface.
  87. type InproxyProxyQualityBrokerRoundTripper struct {
  88. transport *http.Transport
  89. conns *common.Conns[net.Conn]
  90. requestURL string
  91. additionalHeaders http.Header
  92. dialParams common.APIParameters
  93. }
  94. // NewInproxyProxyQualityBrokerRoundTripper initializes a new
  95. // InproxyProxyQualityBrokerRoundTripper, using the dial parameters in the
  96. // input InproxyBrokerSpec.
  97. func NewInproxyProxyQualityBrokerRoundTripper(
  98. brokerSpec *parameters.InproxyBrokerSpec,
  99. trustedCACertificates string,
  100. additionalHeaders http.Header) (*InproxyProxyQualityBrokerRoundTripper, error) {
  101. // While server to broker connections are not expected to be subject to
  102. // blocking, this transport currently uses CDN fronts, as already
  103. // specified for client and proxy broker connections. Direct server to
  104. // broker connections are not supported, but could be added in the future.
  105. //
  106. // The CDN path may, in fact, assist with performance and scaling, given
  107. // that requests routed through CDNs will be multiplexed over existing
  108. // CDN-to-origin connections, and not use additional ephemeral ports on
  109. // the broker origin host.
  110. frontingProviderID,
  111. frontingTransport,
  112. frontingDialAddress,
  113. _, // SNIServerName is ignored
  114. verifyServerName,
  115. verifyPins,
  116. hostHeader,
  117. err := brokerSpec.BrokerFrontingSpecs.SelectParameters()
  118. if err != nil {
  119. return nil, errors.Trace(err)
  120. }
  121. if frontingTransport != protocol.FRONTING_TRANSPORT_HTTPS {
  122. return nil, errors.TraceNew("unsupported fronting transport")
  123. }
  124. // The following wires up simplified domain fronted requests, including
  125. // the basic, distinct dial/SNI and host header values. Certificate
  126. // validation based on FrontingSpec parameters, including pins, includes
  127. // a subset of the functionality from psiphon.CustomTLSDial.
  128. //
  129. // psiphon.DialMeek/CustomTLSDial features omitted here include dial
  130. // parameter replay, the QUIC transport, and obfuscation techniques
  131. // including custom DNS resolution, SNI transforms, utls TLS
  132. // fingerprints, and TCP and TLS fragmentation, TLS padding, and other
  133. // mechanisms.
  134. // FrontingSpec.Addresses may include a port; default to 443 if none.
  135. dialAddress := frontingDialAddress
  136. if _, _, err := net.SplitHostPort(dialAddress); err != nil {
  137. dialAddress = net.JoinHostPort(frontingDialAddress, "443")
  138. }
  139. var tlsConfigRootCAs *x509.CertPool
  140. if trustedCACertificates != "" {
  141. tlsConfigRootCAs = x509.NewCertPool()
  142. if !tlsConfigRootCAs.AppendCertsFromPEM([]byte(trustedCACertificates)) {
  143. return nil, errors.TraceNew("AppendCertsFromPEM failed")
  144. }
  145. }
  146. conns := common.NewConns[net.Conn]()
  147. transport := &http.Transport{
  148. DialTLSContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
  149. conn, err := (&net.Dialer{}).DialContext(ctx, network, dialAddress)
  150. if err != nil {
  151. return nil, errors.Trace(err)
  152. }
  153. // Track conn to facilitate InproxyProxyQualityBrokerRoundTripper.Close
  154. // interrupting and closing all connections.
  155. conn = &inproxyProxyQualityBrokerRoundTripperConn{Conn: conn, conns: conns}
  156. if !conns.Add(conn) {
  157. conn.Close()
  158. return nil, errors.Trace(err)
  159. }
  160. tlsConn := tls.Client(
  161. conn,
  162. &tls.Config{
  163. InsecureSkipVerify: true,
  164. VerifyPeerCertificate: func(
  165. rawCerts [][]byte, _ [][]*x509.Certificate) error {
  166. var verifiedChains [][]*x509.Certificate
  167. var err error
  168. if verifyServerName != "" {
  169. verifiedChains, err = common.VerifyServerCertificate(
  170. tlsConfigRootCAs, rawCerts, verifyServerName)
  171. if err != nil {
  172. return errors.Trace(err)
  173. }
  174. }
  175. if len(verifyPins) > 0 {
  176. err := common.VerifyCertificatePins(
  177. verifyPins, verifiedChains)
  178. if err != nil {
  179. return errors.Trace(err)
  180. }
  181. }
  182. return nil
  183. },
  184. })
  185. err = tlsConn.HandshakeContext(ctx)
  186. if err != nil {
  187. conn.Close()
  188. return nil, errors.Trace(err)
  189. }
  190. return tlsConn, nil
  191. },
  192. }
  193. url := &url.URL{
  194. Scheme: "https",
  195. Host: hostHeader,
  196. Path: "/",
  197. }
  198. // Note that there's currently no custom log formatter (or validator) in
  199. // inproxy.ServerProxyQualityRequest.ValidateAndGetLogFields, so field
  200. // transforms, such as "0"/"1" to bool, are not yet supported here.
  201. dialParams := common.APIParameters{
  202. "fronting_provider_id": frontingProviderID,
  203. }
  204. return &InproxyProxyQualityBrokerRoundTripper{
  205. transport: transport,
  206. conns: conns,
  207. requestURL: url.String(),
  208. additionalHeaders: additionalHeaders,
  209. dialParams: dialParams,
  210. }, nil
  211. }
  212. type inproxyProxyQualityBrokerRoundTripperConn struct {
  213. net.Conn
  214. conns *common.Conns[net.Conn]
  215. }
  216. func (conn *inproxyProxyQualityBrokerRoundTripperConn) Close() error {
  217. conn.conns.Remove(conn)
  218. return errors.Trace(conn.Conn.Close())
  219. }
  220. // RoundTrip performs a broker request round trip.
  221. func (r *InproxyProxyQualityBrokerRoundTripper) RoundTrip(
  222. ctx context.Context,
  223. roundTripDelay time.Duration,
  224. roundTripTimeout time.Duration,
  225. requestPayload []byte) (retResponsePayload []byte, retErr error) {
  226. defer func() {
  227. // Wrap every return with RoundTripperFailedError to conform with the
  228. // inproxy.RoundTripper interface. This is a simplification of the
  229. // logic in InproxyBrokerRoundTripper.RoundTrip, which conditionally
  230. // wraps errors based on various heuristics and conditions that are
  231. // more relevant to clients and proxies with long polling and
  232. // multiple concurrent requests.
  233. if retErr != nil {
  234. retErr = inproxy.NewRoundTripperFailedError(retErr)
  235. }
  236. }()
  237. // Proxy quality broker round trips are not expected to apply a delay here.
  238. if roundTripDelay > 0 {
  239. return nil, errors.TraceNew("roundTripDelay unsupported")
  240. }
  241. request, err := http.NewRequestWithContext(
  242. ctx, "POST", r.requestURL, bytes.NewReader(requestPayload))
  243. if err != nil {
  244. return nil, errors.Trace(err)
  245. }
  246. for name, value := range r.additionalHeaders {
  247. request.Header[name] = value
  248. }
  249. response, err := r.transport.RoundTrip(request)
  250. if err != nil {
  251. return nil, errors.Trace(err)
  252. }
  253. defer response.Body.Close()
  254. if response.StatusCode != http.StatusOK {
  255. return nil, errors.Tracef(
  256. "unexpected response status code %d", response.StatusCode)
  257. }
  258. responsePayload, err := io.ReadAll(response.Body)
  259. if err != nil {
  260. return nil, errors.Trace(err)
  261. }
  262. return responsePayload, nil
  263. }
  264. // Close interrupts any in-flight request and closes all underlying network
  265. // connections.
  266. func (r *InproxyProxyQualityBrokerRoundTripper) Close() error {
  267. r.conns.CloseAll()
  268. return nil
  269. }