Implement SQS rendezvous in client and broker

This features adds an additional rendezvous method to send client offers
and receive proxy answers through the use of Amazon SQS queues.

https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/issues/26151
This commit is contained in:
Michael Pu 2023-11-18 20:43:28 -05:00 committed by Cecylia Bocovich
parent d0529141ac
commit 8fb17de152
No known key found for this signature in database
GPG key ID: 009DE379FD9B7B90
9 changed files with 472 additions and 4 deletions

View file

@ -26,7 +26,9 @@ import (
const (
brokerErrorUnexpected string = "Unexpected error, no answer."
readLimit = 100000 //Maximum number of bytes to be read from an HTTP response
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
@ -88,14 +90,25 @@ func newBrokerChannelFromConfig(config ClientConfig) (*BrokerChannel, error) {
var rendezvous RendezvousMethod
var err error
if config.AmpCacheURL != "" {
if config.SQSQueueURL != "" {
if config.AmpCacheURL != "" || config.BrokerURL != "" {
log.Fatalln("Multiple rendezvous methods specified. " + rendezvousErrorMsg)
}
if config.SQSAccessKeyID == "" || config.SQSSecretKey == "" {
log.Fatalln("sqsakid and sqsskey must be specified to use SQS rendezvous method.")
}
log.Println("Through SQS queue at:", config.SQSQueueURL)
rendezvous, err = newSQSRendezvous(config.SQSQueueURL, config.SQSAccessKeyID, config.SQSSecretKey, 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 {
} 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

View file

@ -0,0 +1,135 @@
package snowflake_client
import (
"context"
"crypto/rand"
"encoding/hex"
"log"
"net/http"
"net/url"
"regexp"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/aws/aws-sdk-go-v2/service/sqs/types"
)
type sqsRendezvous struct {
transport http.RoundTripper
sqsClientID string
sqsClient *sqs.Client
sqsURL *url.URL
}
func newSQSRendezvous(sqsQueue string, sqsAccessKeyId string, sqsSecretKey string, transport http.RoundTripper) (*sqsRendezvous, error) {
sqsURL, err := url.Parse(sqsQueue)
if err != nil {
return nil, err
}
var id [8]byte
_, err = rand.Read(id[:])
if err != nil {
log.Fatal(err)
}
clientID := hex.EncodeToString(id[:])
queueURL := sqsURL.String()
hostName := sqsURL.Hostname()
regionRegex, _ := regexp.Compile(`^sqs\.([\w-]+)\.amazonaws\.com$`)
res := regionRegex.FindStringSubmatch(hostName)
if len(res) < 2 {
log.Fatal("Could not extract AWS region from SQS URL. Ensure that the SQS Queue URL provided is valid.")
}
region := res[1]
cfg, err := config.LoadDefaultConfig(context.TODO(),
config.WithCredentialsProvider(
credentials.NewStaticCredentialsProvider(sqsAccessKeyId, sqsSecretKey, ""),
),
config.WithRegion(region),
)
if err != nil {
log.Fatal(err)
}
client := sqs.NewFromConfig(cfg)
log.Println("Queue URL: ", queueURL)
log.Println("SQS Client ID: ", clientID)
return &sqsRendezvous{
transport: transport,
sqsClientID: clientID,
sqsClient: client,
sqsURL: sqsURL,
}, nil
}
func (r *sqsRendezvous) Exchange(encPollReq []byte) ([]byte, error) {
log.Println("Negotiating via SQS Queue rendezvous...")
_, err := r.sqsClient.SendMessage(context.TODO(), &sqs.SendMessageInput{
MessageAttributes: map[string]types.MessageAttributeValue{
"ClientID": {
DataType: aws.String("String"),
StringValue: aws.String(r.sqsClientID),
},
},
MessageBody: aws.String(string(encPollReq)),
QueueUrl: aws.String(r.sqsURL.String()),
})
if err != nil {
return nil, err
}
time.Sleep(time.Second) // wait for client queue to be created by the broker
numRetries := 5
var responseQueueURL *string
for i := 0; i < numRetries; i++ {
// The SQS queue corresponding to the client where the SDP Answer will be placed
// may not be created yet. We will retry up to 5 times before we error out.
var res *sqs.GetQueueUrlOutput
res, err = r.sqsClient.GetQueueUrl(context.TODO(), &sqs.GetQueueUrlInput{
QueueName: aws.String("snowflake-client-" + r.sqsClientID),
})
if err != nil {
log.Println(err)
log.Printf("Attempt %d of %d to retrieve URL of response SQS queue failed.\n", i+1, numRetries)
time.Sleep(time.Second)
} else {
responseQueueURL = res.QueueUrl
break
}
}
if err != nil {
return nil, err
}
var answer string
for i := 0; i < numRetries; i++ {
// Waiting for SDP Answer from proxy to be placed in SQS queue.
// We will retry upt to 5 times before we error out.
res, err := r.sqsClient.ReceiveMessage(context.TODO(), &sqs.ReceiveMessageInput{
QueueUrl: responseQueueURL,
MaxNumberOfMessages: 1,
WaitTimeSeconds: 20,
})
if err != nil {
return nil, err
}
if len(res.Messages) == 0 {
log.Printf("Attempt %d of %d to receive message from response SQS queue failed. No message found in queue.\n", i+1, numRetries)
delay := float64(i)/2.0 + 1
time.Sleep(time.Duration(delay*1000) * time.Millisecond)
} else {
answer = *res.Messages[0].Body
break
}
}
return []byte(answer), nil
}

View file

@ -86,6 +86,12 @@ 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
// SQSQueueURL is the full URL of an AWS SQS Queue. A nonzero value indicates
// that SQS queue will be used as the rendezvous method with the broker.
SQSQueueURL string
// Access Key ID and Secret Key of the credentials used to access the AWS SQS Qeueue
SQSAccessKeyID string
SQSSecretKey string
// 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

View file

@ -16,6 +16,8 @@ import (
"sync"
"syscall"
"gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/proxy"
pt "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/goptlib"
sf "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/client/lib"
@ -82,6 +84,15 @@ func socksAcceptLoop(ln *pt.SocksListener, config sf.ClientConfig, shutdown chan
if arg, ok := conn.Req.Args.Get("ampcache"); ok {
config.AmpCacheURL = arg
}
if arg, ok := conn.Req.Args.Get("sqsqueue"); ok {
config.SQSQueueURL = arg
}
if arg, ok := conn.Req.Args.Get("sqsakid"); ok {
config.SQSAccessKeyID = arg
}
if arg, ok := conn.Req.Args.Get("sqsskey"); ok {
config.SQSSecretKey = arg
}
if arg, ok := conn.Req.Args.Get("fronts"); ok {
if arg != "" {
config.FrontDomains = strings.Split(strings.TrimSpace(arg), ",")
@ -160,6 +171,9 @@ func main() {
frontDomain := flag.String("front", "", "front domain")
frontDomainsCommas := flag.String("fronts", "", "comma-separated list of front domains")
ampCacheURL := flag.String("ampcache", "", "URL of AMP cache to use as a proxy for signaling")
sqsQueueURL := flag.String("sqsqueue", "", "URL of SQS Queue to use as a proxy for signaling")
sqsAccessKeyId := flag.String("sqsakid", "", "Access Key ID for credentials to access SQS Queue ")
sqsSecretKey := flag.String("sqsskey", "", "Secret Key for credentials to access SQS Queue")
logFilename := flag.String("log", "", "name of log file")
logToStateDir := flag.Bool("log-to-state-dir", false, "resolve the log file relative to tor's pt state dir")
keepLocalAddresses := flag.Bool("keep-local-addresses", false, "keep local LAN address ICE candidates")
@ -227,6 +241,9 @@ func main() {
config := sf.ClientConfig{
BrokerURL: *brokerURL,
AmpCacheURL: *ampCacheURL,
SQSQueueURL: *sqsQueueURL,
SQSAccessKeyID: *sqsAccessKeyId,
SQSSecretKey: *sqsSecretKey,
FrontDomains: frontDomains,
ICEAddresses: iceAddresses,
KeepLocalAddresses: *keepLocalAddresses || *oldKeepLocalAddresses,