snowflake/client/lib/rendezvous.go
WofWca 94b6647d33
feat(client): try restricted proxy if NAT unknown
Just once, to offload unrestricted proxies.
This is useful when our STUN servers are blocked or don't support
the NAT discovery feature, or if they're just slow.

Closes https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/issues/40178.
Partially addresses https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/issues/40376

Signed-off-by: Cecylia Bocovich <cohosh@torproject.org>
2024-12-05 10:34:08 -05:00

320 lines
10 KiB
Go

// WebRTC rendezvous requires the exchange of SessionDescriptions between
// peers in order to establish a PeerConnection.
package snowflake_client
import (
"crypto/tls"
"errors"
"fmt"
"log"
"net/http"
"net/url"
"sync"
"time"
"github.com/pion/webrtc/v4"
utls "github.com/refraction-networking/utls"
"gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/certs"
"gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/event"
"gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/messages"
"gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/nat"
"gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/util"
utlsutil "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/utls"
)
const (
brokerErrorUnexpected string = "Unexpected error, no answer."
rendezvousErrorMsg string = "One of SQS, AmpCache, or Domain Fronting rendezvous methods must be used."
readLimit = 100000 //Maximum number of bytes to be read from an HTTP response
)
// RendezvousMethod represents a way of communicating with the broker: sending
// an encoded client poll request (SDP offer) and receiving an encoded client
// poll response (SDP answer) in return. RendezvousMethod is used by
// BrokerChannel, which is in charge of encoding and decoding, and all other
// tasks that are independent of the rendezvous method.
type RendezvousMethod interface {
Exchange([]byte) ([]byte, error)
}
// BrokerChannel uses a RendezvousMethod to communicate with the Snowflake broker.
// The BrokerChannel is responsible for encoding and decoding SDP offers and answers;
// RendezvousMethod is responsible for the exchange of encoded information.
type BrokerChannel struct {
Rendezvous RendezvousMethod
keepLocalAddresses bool
natType string
lock sync.Mutex
BridgeFingerprint string
}
// We make a copy of DefaultTransport because we want the default Dial
// and TLSHandshakeTimeout settings. But we want to disable the default
// ProxyFromEnvironment setting.
func createBrokerTransport(proxy *url.URL) http.RoundTripper {
tlsConfig := &tls.Config{
RootCAs: certs.GetRootCAs(),
}
transport := &http.Transport{TLSClientConfig: tlsConfig}
transport.Proxy = nil
if proxy != nil {
transport.Proxy = http.ProxyURL(proxy)
}
transport.ResponseHeaderTimeout = 15 * time.Second
return transport
}
func newBrokerChannelFromConfig(config ClientConfig) (*BrokerChannel, error) {
log.Println("Rendezvous using Broker at:", config.BrokerURL)
if len(config.FrontDomains) != 0 {
log.Printf("Domain fronting using a randomly selected domain from: %v", config.FrontDomains)
}
brokerTransport := createBrokerTransport(config.CommunicationProxy)
if config.UTLSClientID != "" {
utlsClientHelloID, err := utlsutil.NameToUTLSID(config.UTLSClientID)
if err != nil {
return nil, fmt.Errorf("unable to create broker channel: %w", err)
}
utlsConfig := &utls.Config{
RootCAs: certs.GetRootCAs(),
}
brokerTransport = utlsutil.NewUTLSHTTPRoundTripperWithProxy(utlsClientHelloID, utlsConfig, brokerTransport,
config.UTLSRemoveSNI, config.CommunicationProxy)
}
var rendezvous RendezvousMethod
var err error
if config.SQSQueueURL != "" {
if config.AmpCacheURL != "" || config.BrokerURL != "" {
log.Fatalln("Multiple rendezvous methods specified. " + rendezvousErrorMsg)
}
if config.SQSCredsStr == "" {
log.Fatalln("sqscreds must be specified to use SQS rendezvous method.")
}
log.Println("Through SQS queue at:", config.SQSQueueURL)
rendezvous, err = newSQSRendezvous(config.SQSQueueURL, config.SQSCredsStr, brokerTransport)
} else if config.AmpCacheURL != "" && config.BrokerURL != "" {
log.Println("Through AMP cache at:", config.AmpCacheURL)
rendezvous, err = newAMPCacheRendezvous(
config.BrokerURL, config.AmpCacheURL, config.FrontDomains,
brokerTransport)
} else if config.BrokerURL != "" {
rendezvous, err = newHTTPRendezvous(
config.BrokerURL, config.FrontDomains, brokerTransport)
} else {
log.Fatalln("No rendezvous method was specified. " + rendezvousErrorMsg)
}
if err != nil {
return nil, err
}
return &BrokerChannel{
Rendezvous: rendezvous,
keepLocalAddresses: config.KeepLocalAddresses,
natType: nat.NATUnknown,
BridgeFingerprint: config.BridgeFingerprint,
}, nil
}
// Negotiate uses a RendezvousMethod to send the client's WebRTC SDP offer
// and receive a snowflake proxy WebRTC SDP answer in return.
func (bc *BrokerChannel) Negotiate(
offer *webrtc.SessionDescription,
natTypeToSend string,
) (
*webrtc.SessionDescription, error,
) {
encReq, err := preparePollRequest(offer, natTypeToSend, bc.BridgeFingerprint)
if err != nil {
return nil, err
}
// Do the exchange using our RendezvousMethod.
encResp, err := bc.Rendezvous.Exchange(encReq)
if err != nil {
return nil, err
}
log.Printf("Received answer: %s", string(encResp))
// Decode the client poll response.
resp, err := messages.DecodeClientPollResponse(encResp)
if err != nil {
return nil, err
}
if resp.Error != "" {
return nil, errors.New(resp.Error)
}
return util.DeserializeSessionDescription(resp.Answer)
}
// Pure function
func preparePollRequest(
offer *webrtc.SessionDescription,
natType string,
bridgeFingerprint string,
) (encReq []byte, err error) {
offerSDP, err := util.SerializeSessionDescription(offer)
if err != nil {
return nil, err
}
req := &messages.ClientPollRequest{
Offer: offerSDP,
NAT: natType,
Fingerprint: bridgeFingerprint,
}
encReq, err = req.EncodeClientPollRequest()
return
}
// SetNATType sets the NAT type of the client so we can send it to the WebRTC broker.
func (bc *BrokerChannel) SetNATType(NATType string) {
bc.lock.Lock()
bc.natType = NATType
bc.lock.Unlock()
log.Printf("NAT Type: %s", NATType)
}
func (bc *BrokerChannel) GetNATType() string {
bc.lock.Lock()
defer bc.lock.Unlock()
return bc.natType
}
type NATPolicy struct {
assumedUnrestrictedNATAndFailedToConnect bool
}
// When our NAT type is unknown, we want to try to connect to a
// restricted / unknown proxy initially
// to offload the unrestricted ones.
// So, instead of always sending the actual NAT type,
// we should use this function to determine the NAT type to send.
//
// This is useful when our STUN servers are blocked or don't support
// the NAT discovery feature, or if they're just slow.
func (p *NATPolicy) NATTypeToSend(actualNatType string) string {
if !p.assumedUnrestrictedNATAndFailedToConnect && actualNatType == nat.NATUnknown {
// If our NAT type is unknown, and we haven't failed to connect
// with a spoofed NAT type yet, then spoof a NATUnrestricted
// type.
return nat.NATUnrestricted
} else {
// In all other cases, do not spoof, and just return our actual
// NAT type (even if it is NATUnknown).
return actualNatType
}
}
// This function must be called whenever a connection with a proxy succeeds,
// because the connection outcome tells us about NAT compatibility
// between the proxy and us.
func (p *NATPolicy) Success(actualNATType, sentNATType string) {
// Yes, right now this does nothing but log.
if actualNATType != sentNATType {
log.Printf(
"Connected to a proxy by using a spoofed NAT type \"%v\"! "+
"Our actual NAT type was \"%v\"",
sentNATType,
actualNATType,
)
}
}
// This function must be called whenever a connection with a proxy fails,
// because the connection outcome tells us about NAT compatibility
// between the proxy and us.
func (p *NATPolicy) Failure(actualNATType, sentNATType string) {
if actualNATType == nat.NATUnknown && sentNATType == nat.NATUnrestricted {
log.Printf(
"Tried to connect to a restricted proxy while our NAT type "+
"is \"%v\", and failed. Let's not do that again.",
actualNATType,
)
p.assumedUnrestrictedNATAndFailedToConnect = true
}
}
// WebRTCDialer implements the |Tongue| interface to catch snowflakes, using BrokerChannel.
type WebRTCDialer struct {
*BrokerChannel
// Can be `nil`, in which case we won't apply special logic,
// and simply always send the current NAT type instead.
natPolicy *NATPolicy
webrtcConfig *webrtc.Configuration
max int
eventLogger event.SnowflakeEventReceiver
proxy *url.URL
}
// Deprecated: Use NewWebRTCDialerWithNatPolicyAndEventsAndProxy instead
func NewWebRTCDialer(broker *BrokerChannel, iceServers []webrtc.ICEServer, max int) *WebRTCDialer {
return NewWebRTCDialerWithNatPolicyAndEventsAndProxy(
broker, nil, iceServers, max, nil, nil,
)
}
// Deprecated: Use NewWebRTCDialerWithNatPolicyAndEventsAndProxy instead
func NewWebRTCDialerWithEvents(broker *BrokerChannel, iceServers []webrtc.ICEServer, max int, eventLogger event.SnowflakeEventReceiver) *WebRTCDialer {
return NewWebRTCDialerWithNatPolicyAndEventsAndProxy(
broker, nil, iceServers, max, eventLogger, nil,
)
}
// Deprecated: Use NewWebRTCDialerWithNatPolicyAndEventsAndProxy instead
func NewWebRTCDialerWithEventsAndProxy(broker *BrokerChannel, iceServers []webrtc.ICEServer, max int,
eventLogger event.SnowflakeEventReceiver, proxy *url.URL,
) *WebRTCDialer {
return NewWebRTCDialerWithNatPolicyAndEventsAndProxy(
broker,
nil,
iceServers,
max,
eventLogger,
proxy,
)
}
// NewWebRTCDialerWithNatPolicyAndEventsAndProxy constructs a new WebRTCDialer.
func NewWebRTCDialerWithNatPolicyAndEventsAndProxy(
broker *BrokerChannel,
natPolicy *NATPolicy,
iceServers []webrtc.ICEServer,
max int,
eventLogger event.SnowflakeEventReceiver,
proxy *url.URL,
) *WebRTCDialer {
config := webrtc.Configuration{
ICEServers: iceServers,
}
return &WebRTCDialer{
BrokerChannel: broker,
natPolicy: natPolicy,
webrtcConfig: &config,
max: max,
eventLogger: eventLogger,
proxy: proxy,
}
}
// Catch initializes a WebRTC Connection by signaling through the BrokerChannel.
func (w WebRTCDialer) Catch() (*WebRTCPeer, error) {
// TODO: [#25591] Fetch ICE server information from Broker.
// TODO: [#25596] Consider TURN servers here too.
return NewWebRTCPeerWithNatPolicyAndEventsAndProxy(
w.webrtcConfig, w.BrokerChannel, w.natPolicy, w.eventLogger, w.proxy,
)
}
// GetMax returns the maximum number of snowflakes to collect.
func (w WebRTCDialer) GetMax() int {
return w.max
}