inproxy.go 9.4 KB

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