Randomly select front domain from comma-separated list

This commmit changes the command-line and Bridge line arguments to take
a comma-separated list of front domains. The change is backwards
compatible with old Bridge and ClientTransportPlugin lines. At
rendezvous time, a front domain will be randomly chosen from the list.
This commit is contained in:
Cecylia Bocovich 2023-09-23 10:46:46 -04:00
parent 5cdf52c813
commit 9fdfb3d1b5
No known key found for this signature in database
GPG key ID: 009DE379FD9B7B90
7 changed files with 65 additions and 48 deletions

View file

@ -64,8 +64,8 @@ func createBrokerTransport() http.RoundTripper {
func newBrokerChannelFromConfig(config ClientConfig) (*BrokerChannel, error) {
log.Println("Rendezvous using Broker at:", config.BrokerURL)
if config.FrontDomain != "" {
log.Println("Domain fronting using:", config.FrontDomain)
if len(config.FrontDomains) != 0 {
log.Printf("Domain fronting using a randomly selected domain from: %v", config.FrontDomains)
}
brokerTransport := createBrokerTransport()
@ -86,11 +86,11 @@ func newBrokerChannelFromConfig(config ClientConfig) (*BrokerChannel, error) {
if config.AmpCacheURL != "" {
log.Println("Through AMP cache at:", config.AmpCacheURL)
rendezvous, err = newAMPCacheRendezvous(
config.BrokerURL, config.AmpCacheURL, config.FrontDomain,
config.BrokerURL, config.AmpCacheURL, config.FrontDomains,
brokerTransport)
} else {
rendezvous, err = newHTTPRendezvous(
config.BrokerURL, config.FrontDomain, brokerTransport)
config.BrokerURL, config.FrontDomains, brokerTransport)
}
if err != nil {
return nil, err

View file

@ -5,8 +5,10 @@ import (
"io"
"io/ioutil"
"log"
"math/rand"
"net/http"
"net/url"
"time"
"gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/amp"
)
@ -17,7 +19,7 @@ import (
type ampCacheRendezvous struct {
brokerURL *url.URL
cacheURL *url.URL // Optional AMP cache URL.
front string // Optional front domain to replace url.Host in requests.
fronts []string // Optional front domains to replace url.Host in requests.
transport http.RoundTripper // Used to make all requests.
}
@ -25,7 +27,7 @@ type ampCacheRendezvous struct {
// broker at the given URL, optionally proxying through an AMP cache, and with
// an optional front domain. transport is the http.RoundTripper used to make all
// requests.
func newAMPCacheRendezvous(broker, cache, front string, transport http.RoundTripper) (*ampCacheRendezvous, error) {
func newAMPCacheRendezvous(broker, cache string, fronts []string, transport http.RoundTripper) (*ampCacheRendezvous, error) {
brokerURL, err := url.Parse(broker)
if err != nil {
return nil, err
@ -41,7 +43,7 @@ func newAMPCacheRendezvous(broker, cache, front string, transport http.RoundTrip
return &ampCacheRendezvous{
brokerURL: brokerURL,
cacheURL: cacheURL,
front: front,
fronts: fronts,
transport: transport,
}, nil
}
@ -50,7 +52,6 @@ func (r *ampCacheRendezvous) Exchange(encPollReq []byte) ([]byte, error) {
log.Println("Negotiating via AMP cache rendezvous...")
log.Println("Broker URL:", r.brokerURL)
log.Println("AMP cache URL:", r.cacheURL)
log.Println("Front domain:", r.front)
// We cannot POST a body through an AMP cache, so instead we GET and
// encode the client poll request message into the URL.
@ -72,11 +73,14 @@ func (r *ampCacheRendezvous) Exchange(encPollReq []byte) ([]byte, error) {
return nil, err
}
if r.front != "" {
// Do domain fronting. Replace the domain in the URL's with the
// front, and store the original domain the HTTP Host header.
if len(r.fronts) != 0 {
// Do domain fronting. Replace the domain in the URL's with a randomly
// selected front, and store the original domain the HTTP Host header.
rand.Seed(time.Now().UnixNano())
front := r.fronts[rand.Intn(len(r.fronts))]
log.Println("Front domain:", front)
req.Host = req.URL.Host
req.URL.Host = r.front
req.URL.Host = front
}
resp, err := r.transport.RoundTrip(req)

View file

@ -6,29 +6,31 @@ import (
"io"
"io/ioutil"
"log"
"math/rand"
"net/http"
"net/url"
"time"
)
// httpRendezvous is a RendezvousMethod that communicates with the .../client
// route of the broker over HTTP or HTTPS, with optional domain fronting.
type httpRendezvous struct {
brokerURL *url.URL
front string // Optional front domain to replace url.Host in requests.
fronts []string // Optional front domain to replace url.Host in requests.
transport http.RoundTripper // Used to make all requests.
}
// newHTTPRendezvous creates a new httpRendezvous that contacts the broker at
// the given URL, with an optional front domain. transport is the
// http.RoundTripper used to make all requests.
func newHTTPRendezvous(broker, front string, transport http.RoundTripper) (*httpRendezvous, error) {
func newHTTPRendezvous(broker string, fronts []string, transport http.RoundTripper) (*httpRendezvous, error) {
brokerURL, err := url.Parse(broker)
if err != nil {
return nil, err
}
return &httpRendezvous{
brokerURL: brokerURL,
front: front,
fronts: fronts,
transport: transport,
}, nil
}
@ -36,7 +38,6 @@ func newHTTPRendezvous(broker, front string, transport http.RoundTripper) (*http
func (r *httpRendezvous) Exchange(encPollReq []byte) ([]byte, error) {
log.Println("Negotiating via HTTP rendezvous...")
log.Println("Target URL: ", r.brokerURL.Host)
log.Println("Front URL: ", r.front)
// Suffix the path with the broker's client registration handler.
reqURL := r.brokerURL.ResolveReference(&url.URL{Path: "client"})
@ -45,11 +46,14 @@ func (r *httpRendezvous) Exchange(encPollReq []byte) ([]byte, error) {
return nil, err
}
if r.front != "" {
// Do domain fronting. Replace the domain in the URL's with the
// front, and store the original domain the HTTP Host header.
if len(r.fronts) != 0 {
// Do domain fronting. Replace the domain in the URL's with a randomly
// selected front, and store the original domain the HTTP Host header.
rand.Seed(time.Now().UnixNano())
front := r.fronts[rand.Intn(len(r.fronts))]
log.Println("Front URL: ", front)
req.Host = req.URL.Host
req.URL.Host = r.front
req.URL.Host = front
}
resp, err := r.transport.RoundTrip(req)

View file

@ -71,21 +71,21 @@ func TestHTTPRendezvous(t *testing.T) {
Convey("HTTP rendezvous", t, func() {
Convey("Construct httpRendezvous with no front domain", func() {
transport := &mockTransport{http.StatusOK, []byte{}}
rend, err := newHTTPRendezvous("http://test.broker", "", transport)
rend, err := newHTTPRendezvous("http://test.broker", []string{}, transport)
So(err, ShouldBeNil)
So(rend.brokerURL, ShouldNotBeNil)
So(rend.brokerURL.Host, ShouldResemble, "test.broker")
So(rend.front, ShouldResemble, "")
So(rend.fronts, ShouldEqual, []string{})
So(rend.transport, ShouldEqual, transport)
})
Convey("Construct httpRendezvous *with* front domain", func() {
transport := &mockTransport{http.StatusOK, []byte{}}
rend, err := newHTTPRendezvous("http://test.broker", "front", transport)
rend, err := newHTTPRendezvous("http://test.broker", []string{"front"}, transport)
So(err, ShouldBeNil)
So(rend.brokerURL, ShouldNotBeNil)
So(rend.brokerURL.Host, ShouldResemble, "test.broker")
So(rend.front, ShouldResemble, "front")
So(rend.fronts, ShouldContain, "front")
So(rend.transport, ShouldEqual, transport)
})
@ -94,7 +94,7 @@ func TestHTTPRendezvous(t *testing.T) {
`{"answer": "{\"type\":\"answer\",\"sdp\":\"fake\"}" }`,
"",
)
rend, err := newHTTPRendezvous("http://test.broker", "",
rend, err := newHTTPRendezvous("http://test.broker", []string{},
&mockTransport{http.StatusOK, fakeEncPollResp})
So(err, ShouldBeNil)
answer, err := rend.Exchange(fakeEncPollReq)
@ -107,7 +107,7 @@ func TestHTTPRendezvous(t *testing.T) {
"",
`{"error": "no snowflake proxies currently available"}`,
)
rend, err := newHTTPRendezvous("http://test.broker", "",
rend, err := newHTTPRendezvous("http://test.broker", []string{},
&mockTransport{http.StatusOK, fakeEncPollResp})
So(err, ShouldBeNil)
answer, err := rend.Exchange(fakeEncPollReq)
@ -116,7 +116,7 @@ func TestHTTPRendezvous(t *testing.T) {
})
Convey("httpRendezvous.Exchange fails with unexpected HTTP status code", func() {
rend, err := newHTTPRendezvous("http://test.broker", "",
rend, err := newHTTPRendezvous("http://test.broker", []string{},
&mockTransport{http.StatusInternalServerError, []byte{}})
So(err, ShouldBeNil)
answer, err := rend.Exchange(fakeEncPollReq)
@ -127,7 +127,7 @@ func TestHTTPRendezvous(t *testing.T) {
Convey("httpRendezvous.Exchange fails with error", func() {
transportErr := errors.New("error")
rend, err := newHTTPRendezvous("http://test.broker", "",
rend, err := newHTTPRendezvous("http://test.broker", []string{},
&errorTransport{err: transportErr})
So(err, ShouldBeNil)
answer, err := rend.Exchange(fakeEncPollReq)
@ -136,7 +136,7 @@ func TestHTTPRendezvous(t *testing.T) {
})
Convey("httpRendezvous.Exchange fails with large read", func() {
rend, err := newHTTPRendezvous("http://test.broker", "",
rend, err := newHTTPRendezvous("http://test.broker", []string{},
&mockTransport{http.StatusOK, make([]byte, readLimit+1)})
So(err, ShouldBeNil)
_, err = rend.Exchange(fakeEncPollReq)
@ -166,47 +166,47 @@ func TestAMPCacheRendezvous(t *testing.T) {
Convey("AMP cache rendezvous", t, func() {
Convey("Construct ampCacheRendezvous with no cache and no front domain", func() {
transport := &mockTransport{http.StatusOK, []byte{}}
rend, err := newAMPCacheRendezvous("http://test.broker", "", "", transport)
rend, err := newAMPCacheRendezvous("http://test.broker", "", []string{}, transport)
So(err, ShouldBeNil)
So(rend.brokerURL, ShouldNotBeNil)
So(rend.brokerURL.String(), ShouldResemble, "http://test.broker")
So(rend.cacheURL, ShouldBeNil)
So(rend.front, ShouldResemble, "")
So(rend.fronts, ShouldResemble, []string{})
So(rend.transport, ShouldEqual, transport)
})
Convey("Construct ampCacheRendezvous with cache and no front domain", func() {
transport := &mockTransport{http.StatusOK, []byte{}}
rend, err := newAMPCacheRendezvous("http://test.broker", "https://amp.cache/", "", transport)
rend, err := newAMPCacheRendezvous("http://test.broker", "https://amp.cache/", []string{}, transport)
So(err, ShouldBeNil)
So(rend.brokerURL, ShouldNotBeNil)
So(rend.brokerURL.String(), ShouldResemble, "http://test.broker")
So(rend.cacheURL, ShouldNotBeNil)
So(rend.cacheURL.String(), ShouldResemble, "https://amp.cache/")
So(rend.front, ShouldResemble, "")
So(rend.fronts, ShouldResemble, []string{})
So(rend.transport, ShouldEqual, transport)
})
Convey("Construct ampCacheRendezvous with no cache and front domain", func() {
transport := &mockTransport{http.StatusOK, []byte{}}
rend, err := newAMPCacheRendezvous("http://test.broker", "", "front", transport)
rend, err := newAMPCacheRendezvous("http://test.broker", "", []string{"front"}, transport)
So(err, ShouldBeNil)
So(rend.brokerURL, ShouldNotBeNil)
So(rend.brokerURL.String(), ShouldResemble, "http://test.broker")
So(rend.cacheURL, ShouldBeNil)
So(rend.front, ShouldResemble, "front")
So(rend.fronts, ShouldContain, "front")
So(rend.transport, ShouldEqual, transport)
})
Convey("Construct ampCacheRendezvous with cache and front domain", func() {
transport := &mockTransport{http.StatusOK, []byte{}}
rend, err := newAMPCacheRendezvous("http://test.broker", "https://amp.cache/", "front", transport)
rend, err := newAMPCacheRendezvous("http://test.broker", "https://amp.cache/", []string{"front"}, transport)
So(err, ShouldBeNil)
So(rend.brokerURL, ShouldNotBeNil)
So(rend.brokerURL.String(), ShouldResemble, "http://test.broker")
So(rend.cacheURL, ShouldNotBeNil)
So(rend.cacheURL.String(), ShouldResemble, "https://amp.cache/")
So(rend.front, ShouldResemble, "front")
So(rend.fronts, ShouldContain, "front")
So(rend.transport, ShouldEqual, transport)
})
@ -215,7 +215,7 @@ func TestAMPCacheRendezvous(t *testing.T) {
`{"answer": "{\"type\":\"answer\",\"sdp\":\"fake\"}" }`,
"",
)
rend, err := newAMPCacheRendezvous("http://test.broker", "", "",
rend, err := newAMPCacheRendezvous("http://test.broker", "", []string{},
&mockTransport{http.StatusOK, ampArmorEncode(fakeEncPollResp)})
So(err, ShouldBeNil)
answer, err := rend.Exchange(fakeEncPollReq)
@ -228,7 +228,7 @@ func TestAMPCacheRendezvous(t *testing.T) {
"",
`{"error": "no snowflake proxies currently available"}`,
)
rend, err := newAMPCacheRendezvous("http://test.broker", "", "",
rend, err := newAMPCacheRendezvous("http://test.broker", "", []string{},
&mockTransport{http.StatusOK, ampArmorEncode(fakeEncPollResp)})
So(err, ShouldBeNil)
answer, err := rend.Exchange(fakeEncPollReq)
@ -237,7 +237,7 @@ func TestAMPCacheRendezvous(t *testing.T) {
})
Convey("ampCacheRendezvous.Exchange fails with unexpected HTTP status code", func() {
rend, err := newAMPCacheRendezvous("http://test.broker", "", "",
rend, err := newAMPCacheRendezvous("http://test.broker", "", []string{},
&mockTransport{http.StatusInternalServerError, []byte{}})
So(err, ShouldBeNil)
answer, err := rend.Exchange(fakeEncPollReq)
@ -248,7 +248,7 @@ func TestAMPCacheRendezvous(t *testing.T) {
Convey("ampCacheRendezvous.Exchange fails with error", func() {
transportErr := errors.New("error")
rend, err := newAMPCacheRendezvous("http://test.broker", "", "",
rend, err := newAMPCacheRendezvous("http://test.broker", "", []string{},
&errorTransport{err: transportErr})
So(err, ShouldBeNil)
answer, err := rend.Exchange(fakeEncPollReq)
@ -261,7 +261,7 @@ func TestAMPCacheRendezvous(t *testing.T) {
// encoded bytes. Encode readLimit bytes—the encoded
// size will be larger—and try to read the body. It
// should fail.
rend, err := newAMPCacheRendezvous("http://test.broker", "", "",
rend, err := newAMPCacheRendezvous("http://test.broker", "", []string{},
&mockTransport{http.StatusOK, ampArmorEncode(make([]byte, readLimit))})
So(err, ShouldBeNil)
_, err = rend.Exchange(fakeEncPollReq)

View file

@ -85,11 +85,14 @@ type ClientConfig struct {
// AmpCacheURL is the full URL of a valid AMP cache. A nonzero value indicates
// that AMP cache will be used as the rendezvous method with the broker.
AmpCacheURL string
// FrontDomain is a the full URL of an optional front domain that can be used with either
// FrontDomain is the full URL of an optional front domain that can be used with either
// the AMP cache or HTTP domain fronting rendezvous method.
FrontDomain string
// ICEAddresses are a slice of ICE server URLs that will be used for NAT traversal and
// the creation of the client's WebRTC SDP offer.
FrontDomains []string
// ICEAddresses are a slice of ICE server URLs that will be used for NAT traversal and
// the creation of the client's WebRTC SDP offer.
ICEAddresses []string
// KeepLocalAddresses is an optional setting that will prevent the removal of local or
// invalid addresses from the client's SDP offer. This is useful for local deployments
@ -134,6 +137,11 @@ func NewSnowflakeClient(config ClientConfig) (*Transport, error) {
log.Printf("url: %v", strings.Join(server.URLs, " "))
}
// Maintain backwards compatability with old FrontDomain field of ClientConfig
if (len(config.FrontDomains) == 0) && (config.FrontDomain != "") {
config.FrontDomains = []string{config.FrontDomain}
}
// Rendezvous with broker using the given parameters.
broker, err := newBrokerChannelFromConfig(config)
if err != nil {