| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- /*
- * Copyright (c) 2023, Psiphon Inc.
- * All rights reserved.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
- package inproxy
- import (
- "context"
- std_errors "errors"
- "time"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
- )
- // Timeouts should be aligned with Broker timeouts.
- const (
- sessionHandshakeRoundTripTimeout = 10 * time.Second
- proxyAnnounceRequestTimeout = 2 * time.Minute
- proxyAnswerRequestTimeout = 10 * time.Second
- clientOfferRequestTimeout = 10 * time.Second
- clientRelayedPacketRequestTimeout = 10 * time.Second
- )
- // BrokerClient is used to make requests to a broker.
- //
- // Each BrokerClient maintains a secure broker session. A BrokerClient and its
- // session may be used for multiple concurrent requests. Session key material
- // is provided by BrokerDialCoordinator and must remain static for the
- // lifetime of the BrokerClient.
- //
- // Round trips between the BrokerClient and broker are provided by
- // BrokerClientRoundTripper from BrokerDialCoordinator. The RoundTripper must
- // maintain the association between a request payload and the corresponding
- // response payload. The canonical RoundTripper is an HTTP client, with
- // HTTP/2 or HTTP/3 used to multiplex concurrent requests.
- //
- // When the BrokerDialCoordinator BrokerClientRoundTripperSucceeded call back
- // is invoked, the RoundTripper provider may mark the RoundTripper dial
- // properties for replay.
- //
- // When the BrokerDialCoordinator BrokerClientRoundTripperFailed call back is
- // invoked, the RoundTripper provider should clear any replay state and also
- // create a new RoundTripper to be returned from BrokerClientRoundTripper.
- //
- // BrokerClient does not have a Close operation. The user should close the
- // provided RoundTripper as appropriate.
- //
- // The secure session layer includes obfuscation that provides random padding
- // and uniformly random payload content. The RoundTripper is expected to add
- // its own obfuscation layer; for example, domain fronting.
- type BrokerClient struct {
- coordinator BrokerDialCoordinator
- sessions *InitiatorSessions
- }
- // NewBrokerClient initializes a new BrokerClient with the provided
- // BrokerDialCoordinator.
- func NewBrokerClient(coordinator BrokerDialCoordinator) (*BrokerClient, error) {
- // A client is expected to use an ephemeral key, and can return a
- // zero-value private key. Each proxy should use a peristent key, as the
- // corresponding public key is the proxy ID, which is used to credit the
- // proxy for its service.
- privateKey := coordinator.BrokerClientPrivateKey()
- if privateKey.IsZero() {
- var err error
- privateKey, err = GenerateSessionPrivateKey()
- if err != nil {
- return nil, errors.Trace(err)
- }
- }
- return &BrokerClient{
- coordinator: coordinator,
- sessions: NewInitiatorSessions(privateKey),
- }, nil
- }
- // GetBrokerDialCoordinator returns the BrokerDialCoordinator associated with
- // the BrokerClient.
- func (b *BrokerClient) GetBrokerDialCoordinator() BrokerDialCoordinator {
- return b.coordinator
- }
- // ProxyAnnounce sends a ProxyAnnounce request and returns the response.
- func (b *BrokerClient) ProxyAnnounce(
- ctx context.Context,
- requestDelay time.Duration,
- request *ProxyAnnounceRequest) (*ProxyAnnounceResponse, error) {
- requestPayload, err := MarshalProxyAnnounceRequest(request)
- if err != nil {
- return nil, errors.Trace(err)
- }
- requestTimeout := common.ValueOrDefault(
- b.coordinator.AnnounceRequestTimeout(),
- proxyAnnounceRequestTimeout)
- responsePayload, err := b.roundTrip(
- ctx, requestDelay, requestTimeout, requestPayload)
- if err != nil {
- return nil, errors.Trace(err)
- }
- response, err := UnmarshalProxyAnnounceResponse(responsePayload)
- if err != nil {
- return nil, errors.Trace(err)
- }
- return response, nil
- }
- // ClientOffer sends a ClientOffer request and returns the response.
- func (b *BrokerClient) ClientOffer(
- ctx context.Context,
- request *ClientOfferRequest) (*ClientOfferResponse, error) {
- requestPayload, err := MarshalClientOfferRequest(request)
- if err != nil {
- return nil, errors.Trace(err)
- }
- requestTimeout := common.ValueOrDefault(
- b.coordinator.OfferRequestTimeout(),
- clientOfferRequestTimeout)
- responsePayload, err := b.roundTrip(
- ctx, 0, requestTimeout, requestPayload)
- if err != nil {
- return nil, errors.Trace(err)
- }
- response, err := UnmarshalClientOfferResponse(responsePayload)
- if err != nil {
- return nil, errors.Trace(err)
- }
- return response, nil
- }
- // ProxyAnswer sends a ProxyAnswer request and returns the response.
- func (b *BrokerClient) ProxyAnswer(
- ctx context.Context,
- request *ProxyAnswerRequest) (*ProxyAnswerResponse, error) {
- requestPayload, err := MarshalProxyAnswerRequest(request)
- if err != nil {
- return nil, errors.Trace(err)
- }
- requestTimeout := common.ValueOrDefault(
- b.coordinator.AnswerRequestTimeout(),
- proxyAnswerRequestTimeout)
- responsePayload, err := b.roundTrip(
- ctx, 0, requestTimeout, requestPayload)
- if err != nil {
- return nil, errors.Trace(err)
- }
- response, err := UnmarshalProxyAnswerResponse(responsePayload)
- if err != nil {
- return nil, errors.Trace(err)
- }
- return response, nil
- }
- // ClientRelayedPacket sends a ClientRelayedPacket request and returns the
- // response.
- func (b *BrokerClient) ClientRelayedPacket(
- ctx context.Context,
- request *ClientRelayedPacketRequest) (*ClientRelayedPacketResponse, error) {
- requestPayload, err := MarshalClientRelayedPacketRequest(request)
- if err != nil {
- return nil, errors.Trace(err)
- }
- requestTimeout := common.ValueOrDefault(
- b.coordinator.RelayedPacketRequestTimeout(),
- clientRelayedPacketRequestTimeout)
- responsePayload, err := b.roundTrip(
- ctx, 0, requestTimeout, requestPayload)
- if err != nil {
- return nil, errors.Trace(err)
- }
- response, err := UnmarshalClientRelayedPacketResponse(responsePayload)
- if err != nil {
- return nil, errors.Trace(err)
- }
- return response, nil
- }
- func (b *BrokerClient) roundTrip(
- ctx context.Context,
- requestDelay time.Duration,
- requestTimeout time.Duration,
- request []byte) ([]byte, error) {
- // The round tripper may need to establish a transport-level connection;
- // or this may already be established.
- roundTripper, err := b.coordinator.BrokerClientRoundTripper()
- if err != nil {
- return nil, errors.Trace(err)
- }
- // InitiatorSessions.RoundTrip may make serveral round trips with
- // roundTripper in order to complete a session establishment handshake.
- //
- // When there's an active session, only a single round trip is required,
- // to exchange the application-level request and response.
- //
- // When a concurrent BrokerClient request is currently performing a
- // session handshake, InitiatorSessions.RoundTrip will await completion
- // of that handshake before sending the application-layer request.
- //
- // Note the waitToShareSession limitation, documented in
- // InitiatorSessions.RoundTrip: a new session must complete a full,
- // application-level round trip (e.g., ProxyAnnounce/ClientOffer), not
- // just the session handshake, before a session becomes ready to share.
- //
- // Retries are built in to InitiatorSessions.RoundTrip: if there's an
- // existing session and it's expired, there will be additional round
- // trips to establish a fresh session.
- //
- // While the round tripper is responsible for maintaining the
- // request/response association, the application-level request and
- // response are tagged with a RoundTripID which is checked to ensure the
- // association is maintained.
- //
- // InitiatorSessions.RoundTrip will apply sessionHandshakeTimeout to any
- // round trips required for Noise session handshakes; apply requestDelay
- // before the application-level request round trip; and apply
- // requestTimeout to the network round trip following the delay, if any.
- // Any time spent blocking on waitToShareSession is not included in
- // requestDelay or requestTimeout.
- waitToShareSession := true
- sessionHandshakeTimeout := common.ValueOrDefault(
- b.coordinator.SessionHandshakeRoundTripTimeout(),
- sessionHandshakeRoundTripTimeout)
- response, err := b.sessions.RoundTrip(
- ctx,
- roundTripper,
- b.coordinator.BrokerPublicKey(),
- b.coordinator.BrokerRootObfuscationSecret(),
- waitToShareSession,
- sessionHandshakeTimeout,
- requestDelay,
- requestTimeout,
- request)
- if err != nil {
- var failedError *RoundTripperFailedError
- failed := std_errors.As(err, &failedError)
- if failed {
- // The BrokerDialCoordinator provider should close the existing
- // BrokerClientRoundTripper and create a new RoundTripper to return
- // in the next BrokerClientRoundTripper call.
- //
- // The session will be closed, if necessary, by InitiatorSessions.
- // It's possible that the session remains valid and only the
- // RoundTripper transport layer needs to be reset.
- b.coordinator.BrokerClientRoundTripperFailed(roundTripper)
- }
- return nil, errors.Trace(err)
- }
- b.coordinator.BrokerClientRoundTripperSucceeded(roundTripper)
- return response, nil
- }
|