mirror of
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake.git
synced 2025-10-13 11:11:30 -04:00
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:
parent
d0529141ac
commit
8fb17de152
9 changed files with 472 additions and 4 deletions
|
@ -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
|
||||
|
|
135
client/lib/rendezvous_sqs.go
Normal file
135
client/lib/rendezvous_sqs.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue