mirror of
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake.git
synced 2025-10-13 20:11:19 -04:00
Formerly, BrokerChannel represented the broker URL and possible domain fronting as bc.url *url.URL bc.Host string That is, bc.url is the URL of the server which we contact directly, and bc.Host is the Host header to use in the request. With no domain fronting, bc.url points directly at the broker itself, and bc.Host is blank. With domain fronting, we do the following reshuffling: if front != "" { bc.Host = bc.url.Host bc.url.Host = front } That is, we alter bc.url to reflect that the server to which we send requests directly is the CDN, not the broker, and store the broker's own URL in the HTTP Host header. The above representation was always confusing to me, because in my mental model, we are always conceptually communicating with the broker; but we may optionally be using a CDN proxy in the middle. The new representation is bc.url *url.URL bc.front string bc.url is the URL of the broker itself, and never changes. bc.front is the optional CDN front domain, and likewise never changes after initialization. When domain fronting is in use, we do the swap in the http.Request struct, not in BrokerChannel itself: if bc.front != "" { request.Host = request.URL.Host request.URL.Host = bc.front } Compare to the representation in meek-client: https://gitweb.torproject.org/pluggable-transports/meek.git/tree/meek-client/meek-client.go?h=v0.35.0#n94 var options struct { URL string Front string } https://gitweb.torproject.org/pluggable-transports/meek.git/tree/meek-client/meek-client.go?h=v0.35.0#n308 if ok { // if front is set info.Host = info.URL.Host info.URL.Host = front }
195 lines
5.6 KiB
Go
195 lines
5.6 KiB
Go
// WebRTC rendezvous requires the exchange of SessionDescriptions between
|
|
// peers in order to establish a PeerConnection.
|
|
//
|
|
// This file contains the one method currently available to Snowflake:
|
|
//
|
|
// - Domain-fronted HTTP signaling. The Broker automatically exchange offers
|
|
// and answers between this client and some remote WebRTC proxy.
|
|
|
|
package lib
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.torproject.org/pluggable-transports/snowflake.git/common/messages"
|
|
"git.torproject.org/pluggable-transports/snowflake.git/common/nat"
|
|
"git.torproject.org/pluggable-transports/snowflake.git/common/util"
|
|
"github.com/pion/webrtc/v3"
|
|
)
|
|
|
|
const (
|
|
BrokerErrorUnexpected string = "Unexpected error, no answer."
|
|
readLimit = 100000 //Maximum number of bytes to be read from an HTTP response
|
|
)
|
|
|
|
// Signalling Channel to the Broker.
|
|
type BrokerChannel struct {
|
|
url *url.URL
|
|
front string // Optional front domain to replace url.Host in requests.
|
|
transport http.RoundTripper // Used to make all requests.
|
|
keepLocalAddresses bool
|
|
NATType string
|
|
lock sync.Mutex
|
|
}
|
|
|
|
// 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() http.RoundTripper {
|
|
transport := http.DefaultTransport.(*http.Transport)
|
|
transport.Proxy = nil
|
|
transport.ResponseHeaderTimeout = 15 * time.Second
|
|
return transport
|
|
}
|
|
|
|
// Construct a new BrokerChannel, where:
|
|
// |broker| is the full URL of the facilitating program which assigns proxies
|
|
// to clients, and |front| is the option fronting domain.
|
|
func NewBrokerChannel(broker string, front string, transport http.RoundTripper, keepLocalAddresses bool) (*BrokerChannel, error) {
|
|
targetURL, err := url.Parse(broker)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
log.Println("Rendezvous using Broker at:", broker)
|
|
if front != "" {
|
|
log.Println("Domain fronting using:", front)
|
|
}
|
|
bc := new(BrokerChannel)
|
|
bc.url = targetURL
|
|
bc.front = front
|
|
bc.transport = transport
|
|
bc.keepLocalAddresses = keepLocalAddresses
|
|
bc.NATType = nat.NATUnknown
|
|
return bc, nil
|
|
}
|
|
|
|
func limitedRead(r io.Reader, limit int64) ([]byte, error) {
|
|
p, err := ioutil.ReadAll(&io.LimitedReader{R: r, N: limit + 1})
|
|
if err != nil {
|
|
return p, err
|
|
} else if int64(len(p)) == limit+1 {
|
|
return p[0:limit], io.ErrUnexpectedEOF
|
|
}
|
|
return p, err
|
|
}
|
|
|
|
// Roundtrip HTTP POST using WebRTC SessionDescriptions.
|
|
//
|
|
// Send an SDP offer to the broker, which assigns a proxy and responds
|
|
// with an SDP answer from a designated remote WebRTC peer.
|
|
func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) (
|
|
*webrtc.SessionDescription, error) {
|
|
log.Println("Negotiating via BrokerChannel...\nTarget URL: ",
|
|
bc.url.Host, "\nFront URL: ", bc.front)
|
|
// Ideally, we could specify an `RTCIceTransportPolicy` that would handle
|
|
// this for us. However, "public" was removed from the draft spec.
|
|
// See https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration#RTCIceTransportPolicy_enum
|
|
if !bc.keepLocalAddresses {
|
|
offer = &webrtc.SessionDescription{
|
|
Type: offer.Type,
|
|
SDP: util.StripLocalAddresses(offer.SDP),
|
|
}
|
|
}
|
|
offerSDP, err := util.SerializeSessionDescription(offer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Encode client poll request
|
|
bc.lock.Lock()
|
|
req := &messages.ClientPollRequest{
|
|
Offer: offerSDP,
|
|
NAT: bc.NATType,
|
|
}
|
|
body, err := req.EncodePollRequest()
|
|
bc.lock.Unlock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data := bytes.NewReader([]byte(body))
|
|
// Suffix with broker's client registration handler.
|
|
clientURL := bc.url.ResolveReference(&url.URL{Path: "client"})
|
|
request, err := http.NewRequest("POST", clientURL.String(), data)
|
|
if nil != err {
|
|
return nil, err
|
|
}
|
|
if bc.front != "" {
|
|
// Do domain fronting. Replace the domain in the URL's with the
|
|
// front, and store the original domain the HTTP Host header.
|
|
request.Host = request.URL.Host
|
|
request.URL.Host = bc.front
|
|
}
|
|
resp, err := bc.transport.RoundTrip(request)
|
|
if nil != err {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
log.Printf("BrokerChannel Response:\n%s\n\n", resp.Status)
|
|
|
|
switch resp.StatusCode {
|
|
case http.StatusOK:
|
|
body, err := limitedRead(resp.Body, readLimit)
|
|
if nil != err {
|
|
return nil, err
|
|
}
|
|
log.Printf("Received answer: %s", string(body))
|
|
|
|
resp, err := messages.DecodeClientPollResponse(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Error != "" {
|
|
return nil, errors.New(resp.Error)
|
|
}
|
|
return util.DeserializeSessionDescription(resp.Answer)
|
|
default:
|
|
return nil, errors.New(BrokerErrorUnexpected)
|
|
}
|
|
}
|
|
|
|
func (bc *BrokerChannel) SetNATType(NATType string) {
|
|
bc.lock.Lock()
|
|
bc.NATType = NATType
|
|
bc.lock.Unlock()
|
|
log.Printf("NAT Type: %s", NATType)
|
|
}
|
|
|
|
// Implements the |Tongue| interface to catch snowflakes, using BrokerChannel.
|
|
type WebRTCDialer struct {
|
|
*BrokerChannel
|
|
webrtcConfig *webrtc.Configuration
|
|
max int
|
|
}
|
|
|
|
func NewWebRTCDialer(broker *BrokerChannel, iceServers []webrtc.ICEServer, max int) *WebRTCDialer {
|
|
config := webrtc.Configuration{
|
|
ICEServers: iceServers,
|
|
}
|
|
|
|
return &WebRTCDialer{
|
|
BrokerChannel: broker,
|
|
webrtcConfig: &config,
|
|
max: max,
|
|
}
|
|
}
|
|
|
|
// Initialize a WebRTC Connection by signaling through the broker.
|
|
func (w WebRTCDialer) Catch() (*WebRTCPeer, error) {
|
|
// TODO: [#25591] Fetch ICE server information from Broker.
|
|
// TODO: [#25596] Consider TURN servers here too.
|
|
return NewWebRTCPeer(w.webrtcConfig, w.BrokerChannel)
|
|
}
|
|
|
|
// Returns the maximum number of snowflakes to collect
|
|
func (w WebRTCDialer) GetMax() int {
|
|
return w.max
|
|
}
|