diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8c03390..3d433c8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,3 +1,6 @@ +variables: + DOCKER_REGISTRY_URL: docker.io + stages: - test - deploy @@ -6,9 +9,12 @@ stages: variables: DEBIAN_FRONTEND: noninteractive - DEBIAN_OLD_STABLE: buster - DEBIAN_STABLE: bullseye + DEBIAN_OLD_STABLE: bullseye + DEBIAN_STABLE: bookworm REPRODUCIBLE_FLAGS: -trimpath -ldflags=-buildid= + # Don't fail pulling images if dependency_proxy.yml is not included + DOCKER_REGISTRY_URL: "docker.io" + # set up apt for automated use .apt-template: &apt-template @@ -92,7 +98,7 @@ variables: # -- jobs ------------------------------------------------------------ android: - image: containers.torproject.org/tpo/anti-censorship/duplicatedcontainerimages:golang-1.23-$DEBIAN_STABLE + image: ${DOCKER_REGISTRY_URL}/golang:1.24-$DEBIAN_OLD_STABLE variables: ANDROID_HOME: /usr/lib/android-sdk LANG: C.UTF-8 @@ -142,36 +148,39 @@ android: - go get golang.org/x/mobile/bind - gomobile bind -v -target=android $REPRODUCIBLE_FLAGS . -go-1.21: - image: containers.torproject.org/tpo/anti-censorship/duplicatedcontainerimages:golang-1.21-$DEBIAN_STABLE +go-1.23: + image: ${DOCKER_REGISTRY_URL}/golang:1.23-$DEBIAN_STABLE <<: *golang-docker-debian-template <<: *test-template script: - *go-test -go-1.23: - image: containers.torproject.org/tpo/anti-censorship/duplicatedcontainerimages:golang-1.23-$DEBIAN_STABLE +go-1.24: + image: ${DOCKER_REGISTRY_URL}/golang:1.24-$DEBIAN_STABLE <<: *golang-docker-debian-template <<: *test-template script: - *go-test debian-testing: - image: debian:testing + image: containers.torproject.org/tpo/tpa/base-images/debian:testing <<: *debian-native-template <<: *test-template script: - *go-test shadow-integration: - image: containers.torproject.org/tpo/anti-censorship/duplicatedcontainerimages:golang-1.21-$DEBIAN_STABLE + image: ${DOCKER_REGISTRY_URL}/golang:1.23-$DEBIAN_STABLE variables: - SHADOW_VERSION: "193924aae0dab30ffda0abe29467f552949849fa" + SHADOW_VERSION: "27d0bcf2cf1c7f0d403b6ad3efd575e45ae93126" TGEN_VERSION: "v1.1.2" cache: - key: sf-integration-$SHADOW_VERSION-$TGEN_VERSION - paths: - - /opt/ + - key: sf-integration-shadow-$SHADOW_VERSION + paths: + - opt/shadow + - key: sf-integration-tgen-$TGEN_VERSION + paths: + - opt/tgen artifacts: paths: - shadow.data.tar.gz @@ -181,15 +190,15 @@ shadow-integration: - tpa script: - apt-get update - - apt-get install -y git tor + - apt-get install -y git tor libglib2.0-0 libigraph3 - mkdir -p ~/.local/bin - mkdir -p ~/.local/src - - export PATH=$PATH:$CI_PROJECT_DIR/opt/bin/ + - export PATH=$PATH:$CI_PROJECT_DIR/opt/shadow/bin/:$CI_PROJECT_DIR/opt/tgen/bin/ # Install shadow and tgen - pushd ~/.local/src - | - if [ ! -f opt/shadow/bin/shadow ] + if [ ! -f $CI_PROJECT_DIR/opt/shadow/bin/shadow ] then echo "The required version of shadow was not cached, building from source" git clone --shallow-since=2021-08-01 https://github.com/shadow/shadow.git @@ -198,24 +207,24 @@ shadow-integration: CONTAINER=debian:stable-slim ci/container_scripts/install_deps.sh CC=gcc CONTAINER=debian:stable-slim ci/container_scripts/install_extra_deps.sh export PATH="$HOME/.cargo/bin:${PATH}" - ./setup build --jobs $(nproc) --prefix $CI_PROJECT_DIR/opt/ + ./setup build --jobs $(nproc) --prefix $CI_PROJECT_DIR/opt/shadow ./setup install popd fi - | - if [ ! -f opt/shadow/bin/tgen ] + if [ ! -f $CI_PROJECT_DIR/opt/tgen/bin/tgen ] then echo "The required version of tgen was not cached, building from source" git clone --branch $TGEN_VERSION --depth 1 https://github.com/shadow/tgen.git pushd tgen/ apt-get install -y cmake libglib2.0-dev libigraph-dev mkdir build && cd build - cmake .. -DCMAKE_INSTALL_PREFIX=$CI_PROJECT_DIR/opt/ + cmake .. -DCMAKE_INSTALL_PREFIX=$CI_PROJECT_DIR/opt/tgen make make install popd fi - install $CI_PROJECT_DIR/opt/bin/tgen ~/.local/bin/tgen + install $CI_PROJECT_DIR/opt/tgen/bin/tgen ~/.local/bin/tgen - popd # Apply snowflake patch(es) @@ -253,7 +262,7 @@ shadow-integration: generate_tarball: stage: deploy - image: golang:1.21-$DEBIAN_STABLE + image: ${DOCKER_REGISTRY_URL}/golang:1.22-$DEBIAN_STABLE rules: - if: $CI_COMMIT_TAG script: @@ -298,7 +307,6 @@ build-container: matrix: - ARCH: amd64 - ARCH: arm64 - - ARCH: s390x tags: - $ARCH image: @@ -323,7 +331,7 @@ merge-manifests: - job: build-container artifacts: false image: - name: containers.torproject.org/tpo/anti-censorship/duplicatedcontainerimages:mplatform-manifest-tool-alpine + name: ${DOCKER_REGISTRY_URL}/mplatform/manifest-tool:alpine entrypoint: [""] script: - if [ $CI_COMMIT_REF_NAME == "main" ]; then export TAG='nightly'; fi @@ -332,7 +340,7 @@ merge-manifests: --username="${CI_REGISTRY_USER}" --password="${CI_REGISTRY_PASSWORD}" push from-args - --platforms linux/amd64,linux/arm64,linux/s390x + --platforms linux/amd64,linux/arm64 --template "${CI_REGISTRY_IMAGE}:${TAG}_ARCH" --target "${CI_REGISTRY_IMAGE}:${TAG}" rules: diff --git a/Dockerfile b/Dockerfile index f0344f4..77673dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,5 @@ -FROM docker.io/library/golang:1.23-bookworm AS build +FROM docker.io/library/golang:latest AS build -# Set some labels -# io.containers.autoupdate label will instruct podman to reach out to the corres -# corresponding registry to check if the image has been updated. If an image -# must be updated, Podman pulls it down and restarts the systemd unit executing -# the container. See podman-auto-update(1) for more details, or -# https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html -LABEL io.containers.autoupdate=registry -LABEL org.opencontainers.image.authors="anti-censorship-team@lists.torproject.org" - -RUN apt-get update && apt-get install -y tor-geoipdb ADD . /app @@ -17,11 +7,44 @@ WORKDIR /app/proxy RUN go get RUN CGO_ENABLED=0 go build -o proxy -ldflags '-extldflags "-static" -w -s' . +FROM containers.torproject.org/tpo/tpa/base-images/debian:bookworm as debian-base + +# Install dependencies to add Tor's repository. +RUN apt-get update && apt-get install -y \ + curl \ + gpg \ + gpg-agent \ + ca-certificates \ + libcap2-bin \ + --no-install-recommends + +# See: +RUN curl https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --import +RUN gpg --export A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89 | apt-key add - + +RUN printf "deb https://deb.torproject.org/torproject.org bookworm main\n" >> /etc/apt/sources.list.d/tor.list + +# Install remaining dependencies. +RUN apt-get update && apt-get install -y \ + tor \ + tor-geoipdb \ + --no-install-recommends + + FROM scratch -COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt -COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo -COPY --from=build /usr/share/tor/geoip* /usr/share/tor/ +COPY --from=debian-base /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +COPY --from=debian-base /usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=debian-base /usr/share/tor/geoip* /usr/share/tor/ COPY --from=build /app/proxy/proxy /bin/proxy ENTRYPOINT [ "/bin/proxy" ] + +# Set some labels +# io.containers.autoupdate label will instruct podman to reach out to the +# corresponding registry to check if the image has been updated. If an image +# must be updated, Podman pulls it down and restarts the systemd unit executing +# the container. See podman-auto-update(1) for more details, or +# https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html +LABEL io.containers.autoupdate=registry +LABEL org.opencontainers.image.authors="anti-censorship-team@lists.torproject.org" diff --git a/broker/amp.go b/broker/amp.go index 99289de..4d19a6c 100644 --- a/broker/amp.go +++ b/broker/amp.go @@ -1,13 +1,14 @@ package main import ( + "context" "log" "net/http" "strings" + "time" "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/amp" "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/messages" - "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/util" ) // ampClientOffers is the AMP-speaking endpoint for client poll messages, @@ -16,9 +17,12 @@ import ( // HTTP request body (because an AMP cache does not support POST), and the // encoded client poll response is sent back as AMP-armored HTML. func ampClientOffers(i *IPC, w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), ClientTimeout*time.Second) + defer cancel() + // The encoded client poll message immediately follows the /amp/client/ // path prefix, so this function unfortunately needs to be aware of and - // remote its own routing prefix. + // remove its own routing prefix. path := strings.TrimPrefix(r.URL.Path, "/amp/client/") if path == r.URL.Path { // The path didn't start with the expected prefix. This probably @@ -36,8 +40,9 @@ func ampClientOffers(i *IPC, w http.ResponseWriter, r *http.Request) { if err == nil { arg := messages.Arg{ Body: encPollReq, - RemoteAddr: util.GetClientIp(r), + RemoteAddr: "", RendezvousMethod: messages.RendezvousAmpCache, + Context: ctx, } err = i.ClientOffers(arg, &response) } else { diff --git a/broker/broker.go b/broker/broker.go index a1252b7..8351d8f 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -44,9 +44,8 @@ type BrokerContext struct { proxyPolls chan *ProxyPoll metrics *Metrics - bridgeList BridgeListHolderFileBased - allowedRelayPattern string - presumedPatternForLegacyClient string + bridgeList BridgeListHolderFileBased + allowedRelayPattern string } func (ctx *BrokerContext) GetBridgeInfo(fingerprint bridgefingerprint.Fingerprint) (BridgeInfo, error) { @@ -55,8 +54,7 @@ func (ctx *BrokerContext) GetBridgeInfo(fingerprint bridgefingerprint.Fingerprin func NewBrokerContext( metricsLogger *log.Logger, - allowedRelayPattern, - presumedPatternForLegacyClient string, + allowedRelayPattern string, ) *BrokerContext { snowflakes := new(SnowflakeHeap) heap.Init(snowflakes) @@ -79,14 +77,13 @@ func NewBrokerContext( bridgeListHolder.LoadBridgeInfo(bytes.NewReader([]byte(DefaultBridges))) return &BrokerContext{ - snowflakes: snowflakes, - restrictedSnowflakes: rSnowflakes, - idToSnowflake: make(map[string]*Snowflake), - proxyPolls: make(chan *ProxyPoll), - metrics: metrics, - bridgeList: bridgeListHolder, - allowedRelayPattern: allowedRelayPattern, - presumedPatternForLegacyClient: presumedPatternForLegacyClient, + snowflakes: snowflakes, + restrictedSnowflakes: rSnowflakes, + idToSnowflake: make(map[string]*Snowflake), + proxyPolls: make(chan *ProxyPoll), + metrics: metrics, + bridgeList: bridgeListHolder, + allowedRelayPattern: allowedRelayPattern, } } @@ -176,7 +173,7 @@ func (ctx *BrokerContext) InstallBridgeListProfile(reader io.Reader) error { func (ctx *BrokerContext) CheckProxyRelayPattern(pattern string, nonSupported bool) bool { if nonSupported { - pattern = ctx.presumedPatternForLegacyClient + return false } proxyPattern := namematcher.NewNameMatcher(pattern) brokerPattern := namematcher.NewNameMatcher(ctx.allowedRelayPattern) @@ -197,7 +194,7 @@ func main() { var addr string var geoipDatabase string var geoip6Database string - var bridgeListFilePath, allowedRelayPattern, presumedPatternForLegacyClient string + var bridgeListFilePath, allowedRelayPattern string var brokerSQSQueueName, brokerSQSQueueRegion string var disableTLS bool var certFilename, keyFilename string @@ -215,7 +212,6 @@ func main() { flag.StringVar(&geoip6Database, "geoip6db", "/usr/share/tor/geoip6", "path to correctly formatted geoip database mapping IPv6 address ranges to country codes") flag.StringVar(&bridgeListFilePath, "bridge-list-path", "", "file path for bridgeListFile") flag.StringVar(&allowedRelayPattern, "allowed-relay-pattern", "", "allowed pattern for relay host name. The broker will reject proxies whose AcceptedRelayPattern is more restrictive than this") - flag.StringVar(&presumedPatternForLegacyClient, "default-relay-pattern", "", "presumed pattern for legacy client") flag.StringVar(&brokerSQSQueueName, "broker-sqs-name", "", "name of broker SQS queue to listen for incoming messages on") flag.StringVar(&brokerSQSQueueRegion, "broker-sqs-region", "", "name of AWS region of broker SQS queue") flag.BoolVar(&disableTLS, "disable-tls", false, "don't use HTTPS") @@ -248,7 +244,7 @@ func main() { metricsLogger := log.New(metricsFile, "", 0) - ctx := NewBrokerContext(metricsLogger, allowedRelayPattern, presumedPatternForLegacyClient) + ctx := NewBrokerContext(metricsLogger, allowedRelayPattern) if bridgeListFilePath != "" { bridgeListFile, err := os.Open(bridgeListFilePath) diff --git a/broker/http.go b/broker/http.go index b6f449d..ed8e24a 100644 --- a/broker/http.go +++ b/broker/http.go @@ -2,12 +2,14 @@ package main import ( "bytes" + "context" "errors" "fmt" "io" "log" "net/http" "os" + "time" "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/messages" "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/util" @@ -132,6 +134,9 @@ snowflake proxy, which responds with the SDP answer to be sent in the HTTP response back to the client. */ func clientOffers(i *IPC, w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), ClientTimeout*time.Second) + defer cancel() + body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, readLimit)) if err != nil { log.Printf("Error reading client request: %s", err.Error()) @@ -163,6 +168,7 @@ func clientOffers(i *IPC, w http.ResponseWriter, r *http.Request) { Body: body, RemoteAddr: util.GetClientIp(r), RendezvousMethod: messages.RendezvousHttp, + Context: ctx, } var response []byte @@ -214,7 +220,6 @@ func proxyAnswers(i *IPC, w http.ResponseWriter, r *http.Request) { err = validateSDP(body) if err != nil { - log.Println("Error proxy SDP: ", err.Error()) w.WriteHeader(http.StatusBadRequest) return } diff --git a/broker/ipc.go b/broker/ipc.go index 64cefcd..d58baec 100644 --- a/broker/ipc.go +++ b/broker/ipc.go @@ -5,10 +5,10 @@ import ( "encoding/hex" "fmt" "log" - "time" "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/bridgefingerprint" "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/constants" + "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/util" "github.com/prometheus/client_golang/prometheus" "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/messages" @@ -74,24 +74,17 @@ func (i *IPC) ProxyPolls(arg messages.Arg, response *[]byte) error { } if !relayPatternSupported { - i.ctx.metrics.lock.Lock() - i.ctx.metrics.proxyPollWithoutRelayURLExtension++ + i.ctx.metrics.IncrementCounter("proxy-poll-without-relay-url") i.ctx.metrics.promMetrics.ProxyPollWithoutRelayURLExtensionTotal.With(prometheus.Labels{"nat": natType, "type": proxyType}).Inc() - i.ctx.metrics.lock.Unlock() } else { - i.ctx.metrics.lock.Lock() - i.ctx.metrics.proxyPollWithRelayURLExtension++ + i.ctx.metrics.IncrementCounter("proxy-poll-with-relay-url") i.ctx.metrics.promMetrics.ProxyPollWithRelayURLExtensionTotal.With(prometheus.Labels{"nat": natType, "type": proxyType}).Inc() - i.ctx.metrics.lock.Unlock() } if !i.ctx.CheckProxyRelayPattern(relayPattern, !relayPatternSupported) { - i.ctx.metrics.lock.Lock() - i.ctx.metrics.proxyPollRejectedWithRelayURLExtension++ + i.ctx.metrics.IncrementCounter("proxy-poll-rejected-relay-url") i.ctx.metrics.promMetrics.ProxyPollRejectedForRelayURLExtensionTotal.With(prometheus.Labels{"nat": natType, "type": proxyType}).Inc() - i.ctx.metrics.lock.Unlock() - log.Printf("bad request: rejected relay pattern from proxy = %v", messages.ErrBadRequest) b, err := messages.EncodePollResponseWithRelayURL("", false, "", "", "incorrect relay pattern") *response = b if err != nil { @@ -105,9 +98,7 @@ func (i *IPC) ProxyPolls(arg messages.Arg, response *[]byte) error { if err != nil { log.Println("Warning: cannot process proxy IP: ", err.Error()) } else { - i.ctx.metrics.lock.Lock() - i.ctx.metrics.UpdateCountryStats(remoteIP, proxyType, natType) - i.ctx.metrics.lock.Unlock() + i.ctx.metrics.UpdateProxyStats(remoteIP, proxyType, natType) } var b []byte @@ -116,10 +107,8 @@ func (i *IPC) ProxyPolls(arg messages.Arg, response *[]byte) error { offer := i.ctx.RequestOffer(sid, proxyType, natType, clients) if offer == nil { - i.ctx.metrics.lock.Lock() - i.ctx.metrics.proxyIdleCount++ - i.ctx.metrics.promMetrics.ProxyPollTotal.With(prometheus.Labels{"nat": natType, "status": "idle"}).Inc() - i.ctx.metrics.lock.Unlock() + i.ctx.metrics.IncrementCounter("proxy-idle") + i.ctx.metrics.promMetrics.ProxyPollTotal.With(prometheus.Labels{"nat": natType, "type": proxyType, "status": "idle"}).Inc() b, err = messages.EncodePollResponse("", false, "") if err != nil { @@ -130,7 +119,7 @@ func (i *IPC) ProxyPolls(arg messages.Arg, response *[]byte) error { return nil } - i.ctx.metrics.promMetrics.ProxyPollTotal.With(prometheus.Labels{"nat": natType, "status": "matched"}).Inc() + i.ctx.metrics.promMetrics.ProxyPollTotal.With(prometheus.Labels{"nat": natType, "type": proxyType, "status": "matched"}).Inc() var relayURL string bridgeFingerprint, err := bridgefingerprint.FingerprintFromBytes(offer.fingerprint) if err != nil { @@ -163,13 +152,24 @@ func sendClientResponse(resp *messages.ClientPollResponse, response *[]byte) err func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error { - startTime := time.Now() - req, err := messages.DecodeClientPollRequest(arg.Body) if err != nil { return sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, response) } + // If we couldn't extract the remote IP from the rendezvous method + // pull it from the offer SDP + remoteAddr := arg.RemoteAddr + if remoteAddr == "" { + sdp, err := util.DeserializeSessionDescription(req.Offer) + if err == nil { + candidateAddrs := util.GetCandidateAddrs(sdp.SDP) + if len(candidateAddrs) > 0 { + remoteAddr = candidateAddrs[0].String() + } + } + } + offer := &ClientOffer{ natType: req.NAT, sdp: []byte(req.Offer), @@ -198,9 +198,7 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error { if snowflake != nil { snowflake.offerChannel <- offer } else { - i.ctx.metrics.lock.Lock() - i.ctx.metrics.UpdateRendezvousStats(arg.RemoteAddr, arg.RendezvousMethod, offer.natType, "denied") - i.ctx.metrics.lock.Unlock() + i.ctx.metrics.UpdateClientStats(remoteAddr, arg.RendezvousMethod, offer.natType, "denied") resp := &messages.ClientPollResponse{Error: messages.StrNoProxies} return sendClientResponse(resp, response) } @@ -208,19 +206,11 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error { // Wait for the answer to be returned on the channel or timeout. select { case answer := <-snowflake.answerChannel: - i.ctx.metrics.lock.Lock() - i.ctx.metrics.UpdateRendezvousStats(arg.RemoteAddr, arg.RendezvousMethod, offer.natType, "matched") - i.ctx.metrics.lock.Unlock() + i.ctx.metrics.UpdateClientStats(remoteAddr, arg.RendezvousMethod, offer.natType, "matched") resp := &messages.ClientPollResponse{Answer: answer} err = sendClientResponse(resp, response) - // Initial tracking of elapsed time. - i.ctx.metrics.lock.Lock() - i.ctx.metrics.clientRoundtripEstimate = time.Since(startTime) / time.Millisecond - i.ctx.metrics.lock.Unlock() - case <-time.After(time.Second * ClientTimeout): - i.ctx.metrics.lock.Lock() - i.ctx.metrics.UpdateRendezvousStats(arg.RemoteAddr, arg.RendezvousMethod, offer.natType, "timeout") - i.ctx.metrics.lock.Unlock() + case <-arg.Context.Done(): + i.ctx.metrics.UpdateClientStats(remoteAddr, arg.RendezvousMethod, offer.natType, "timeout") resp := &messages.ClientPollResponse{Error: messages.StrTimedOut} err = sendClientResponse(resp, response) } @@ -263,6 +253,7 @@ func (i *IPC) ProxyAnswers(arg messages.Arg, response *[]byte) error { // The snowflake took too long to respond with an answer, so its client // disappeared / the snowflake is no longer recognized by the Broker. success = false + i.ctx.metrics.promMetrics.ProxyAnswerTotal.With(prometheus.Labels{"type": "", "status": "timeout"}).Inc() } b, err := messages.EncodeAnswerResponse(success) @@ -273,6 +264,7 @@ func (i *IPC) ProxyAnswers(arg messages.Arg, response *[]byte) error { *response = b if success { + i.ctx.metrics.promMetrics.ProxyAnswerTotal.With(prometheus.Labels{"type": snowflake.proxyType, "status": "success"}).Inc() snowflake.answerChannel <- answer } diff --git a/broker/metrics.go b/broker/metrics.go index 53f315b..c1d4e4a 100644 --- a/broker/metrics.go +++ b/broker/metrics.go @@ -8,10 +8,11 @@ package main import ( "fmt" "log" - "math" "net" "sort" + "strings" "sync" + "sync/atomic" "time" "github.com/prometheus/client_golang/prometheus" @@ -25,130 +26,105 @@ const ( metricsResolution = 60 * 60 * 24 * time.Second //86400 seconds ) -var rendezvoudMethodList = [...]messages.RendezvousMethod{ - messages.RendezvousHttp, - messages.RendezvousAmpCache, - messages.RendezvousSqs, -} - -type CountryStats struct { - // map[proxyType][address]bool - proxies map[string]map[string]bool - unknown map[string]bool - - natRestricted map[string]bool - natUnrestricted map[string]bool - natUnknown map[string]bool - - counts map[string]int -} - -// Implements Observable type Metrics struct { logger *log.Logger geoipdb *geoip.Geoip - countryStats CountryStats - clientRoundtripEstimate time.Duration - proxyIdleCount uint - clientDeniedCount map[messages.RendezvousMethod]uint - clientRestrictedDeniedCount map[messages.RendezvousMethod]uint - clientUnrestrictedDeniedCount map[messages.RendezvousMethod]uint - clientProxyMatchCount map[messages.RendezvousMethod]uint - clientProxyTimeoutCount map[messages.RendezvousMethod]uint + ips *sync.Map // proxy IP addresses we've seen before + counters *sync.Map // counters for ip-based metrics - rendezvousCountryStats map[messages.RendezvousMethod]map[string]int - - proxyPollWithRelayURLExtension uint - proxyPollWithoutRelayURLExtension uint - proxyPollRejectedWithRelayURLExtension uint - - // synchronization for access to snowflake metrics - lock sync.Mutex + // counters for country-based metrics + proxies *sync.Map // ip-based counts of proxy country codes + clientHTTPPolls *sync.Map // poll-based counts of client HTTP rendezvous + clientAMPPolls *sync.Map // poll-based counts of client AMP cache rendezvous + clientSQSPolls *sync.Map // poll-based counts of client SQS rendezvous promMetrics *PromMetrics } -type record struct { - cc string - count int -} -type records []record +func NewMetrics(metricsLogger *log.Logger) (*Metrics, error) { + m := new(Metrics) -func (r records) Len() int { return len(r) } -func (r records) Swap(i, j int) { r[i], r[j] = r[j], r[i] } -func (r records) Less(i, j int) bool { - if r[i].count == r[j].count { - return r[i].cc > r[j].cc - } - return r[i].count < r[j].count + m.logger = metricsLogger + m.promMetrics = initPrometheus() + m.ips = new(sync.Map) + m.counters = new(sync.Map) + m.proxies = new(sync.Map) + m.clientHTTPPolls = new(sync.Map) + m.clientAMPPolls = new(sync.Map) + m.clientSQSPolls = new(sync.Map) + + // Write to log file every day with updated metrics + go m.logMetrics() + + return m, nil } -func (s CountryStats) Display() string { - output := "" - - // Use the records struct to sort our counts map by value. - rs := records{} - for cc, count := range s.counts { - rs = append(rs, record{cc: cc, count: count}) +func incrementMapCounter(counters *sync.Map, key string) { + start := uint64(1) + val, loaded := counters.LoadOrStore(key, &start) + if loaded { + ptr := val.(*uint64) + atomic.AddUint64(ptr, 1) } - sort.Sort(sort.Reverse(rs)) - for _, r := range rs { - output += fmt.Sprintf("%s=%d,", r.cc, r.count) - } - - // cut off trailing "," - if len(output) > 0 { - return output[:len(output)-1] - } - - return output } -func (m *Metrics) UpdateCountryStats(addr string, proxyType string, natType string) { - var country string - var ok bool +func (m *Metrics) IncrementCounter(key string) { + incrementMapCounter(m.counters, key) +} - addresses, ok := m.countryStats.proxies[proxyType] - if !ok { - if m.countryStats.unknown[addr] { - return - } - m.countryStats.unknown[addr] = true - } else { - if addresses[addr] { - return - } - addresses[addr] = true - } +func (m *Metrics) UpdateProxyStats(addr string, proxyType string, natType string) { + // perform geolocation of IP address ip := net.ParseIP(addr) if m.geoipdb == nil { return } - country, ok = m.geoipdb.GetCountryByAddr(ip) + country, ok := m.geoipdb.GetCountryByAddr(ip) if !ok { country = "??" } - m.countryStats.counts[country]++ - m.promMetrics.ProxyTotal.With(prometheus.Labels{ - "nat": natType, - "type": proxyType, - "cc": country, - }).Inc() + // check whether we've seen this proxy ip before + if _, loaded := m.ips.LoadOrStore(addr, true); !loaded { + m.IncrementCounter("proxy-total") + incrementMapCounter(m.proxies, country) + m.promMetrics.ProxyTotal.With(prometheus.Labels{ + "nat": natType, + "type": proxyType, + "cc": country, + }).Inc() + } - switch natType { - case NATRestricted: - m.countryStats.natRestricted[addr] = true - case NATUnrestricted: - m.countryStats.natUnrestricted[addr] = true - default: - m.countryStats.natUnknown[addr] = true + // update unique IP proxy NAT metrics + key := fmt.Sprintf("%s-%s", addr, natType) + if _, loaded := m.ips.LoadOrStore(key, true); !loaded { + switch natType { + case NATRestricted: + m.IncrementCounter("proxy-nat-restricted") + case NATUnrestricted: + m.IncrementCounter("proxy-nat-unrestricted") + default: + m.IncrementCounter("proxy-nat-unknown") + } + } + // update unique IP proxy type metrics + key = fmt.Sprintf("%s-%s", addr, proxyType) + if _, loaded := m.ips.LoadOrStore(key, true); !loaded { + switch proxyType { + case "standalone": + m.IncrementCounter("proxy-standalone") + case "badge": + m.IncrementCounter("proxy-badge") + case "iptproxy": + m.IncrementCounter("proxy-iptproxy") + case "webext": + m.IncrementCounter("proxy-webext") + } } } -func (m *Metrics) UpdateRendezvousStats(addr string, rendezvousMethod messages.RendezvousMethod, natType, status string) { +func (m *Metrics) UpdateClientStats(addr string, rendezvousMethod messages.RendezvousMethod, natType, status string) { ip := net.ParseIP(addr) country := "??" if m.geoipdb != nil { @@ -160,20 +136,31 @@ func (m *Metrics) UpdateRendezvousStats(addr string, rendezvousMethod messages.R switch status { case "denied": - m.clientDeniedCount[rendezvousMethod]++ + m.IncrementCounter("client-denied") if natType == NATUnrestricted { - m.clientUnrestrictedDeniedCount[rendezvousMethod]++ + m.IncrementCounter("client-unrestricted-denied") } else { - m.clientRestrictedDeniedCount[rendezvousMethod]++ + m.IncrementCounter("client-restricted-denied") } case "matched": - m.clientProxyMatchCount[rendezvousMethod]++ + m.IncrementCounter("client-match") case "timeout": - m.clientProxyTimeoutCount[rendezvousMethod]++ + m.IncrementCounter("client-timeout") default: log.Printf("Unknown rendezvous status: %s", status) } - m.rendezvousCountryStats[rendezvousMethod][country]++ + + switch rendezvousMethod { + case messages.RendezvousHttp: + m.IncrementCounter("client-http") + incrementMapCounter(m.clientHTTPPolls, country) + case messages.RendezvousAmpCache: + m.IncrementCounter("client-amp") + incrementMapCounter(m.clientAMPPolls, country) + case messages.RendezvousSqs: + m.IncrementCounter("client-sqs") + incrementMapCounter(m.clientSQSPolls, country) + } m.promMetrics.ClientPollTotal.With(prometheus.Labels{ "nat": natType, "status": status, @@ -182,25 +169,51 @@ func (m *Metrics) UpdateRendezvousStats(addr string, rendezvousMethod messages.R }).Inc() } -func (m *Metrics) DisplayRendezvousStatsByCountry(rendezvoudMethod messages.RendezvousMethod) string { - output := "" +// Types to facilitate sorting in formatAndClearCountryStats. +type record struct { + cc string + count uint64 +} +type records []record - // Use the records struct to sort our counts map by value. +// Implementation of sort.Interface for records. The ordering is lexicographic: +// first by count (descending), then by cc (ascending). +func (r records) Len() int { return len(r) } +func (r records) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r records) Less(i, j int) bool { + return r[i].count > r[j].count || (r[i].count == r[j].count && r[i].cc < r[j].cc) +} + +// formatAndClearCountryStats takes a map from country codes to counts, and +// returns a formatted string of comma-separated CC=COUNT. Entries are sorted by +// count from largest to smallest. When counts are equal, entries are sorted by +// country code in ascending order. +// +// formatAndClearCountryStats has the side effect of deleting all entries in m. +func formatAndClearCountryStats(m *sync.Map, binned bool) string { + // Extract entries from the map into a slice of records, binning counts + // if asked to. rs := records{} - for cc, count := range m.rendezvousCountryStats[rendezvoudMethod] { - rs = append(rs, record{cc: cc, count: count}) + m.Range(func(cc, countPtr any) bool { + count := *countPtr.(*uint64) + if binned { + count = binCount(count) + } + rs = append(rs, record{cc: cc.(string), count: count}) + m.Delete(cc) + return true + }) + // Sort the records. + sort.Sort(rs) + // Format and concatenate. + var output strings.Builder + for i, r := range rs { + if i != 0 { + output.WriteString(",") + } + fmt.Fprintf(&output, "%s=%d", r.cc, r.count) } - sort.Sort(sort.Reverse(rs)) - for _, r := range rs { - output += fmt.Sprintf("%s=%d,", r.cc, binCount(uint(r.count))) - } - - // cut off trailing "," - if len(output) > 0 { - return output[:len(output)-1] - } - - return output + return output.String() } func (m *Metrics) LoadGeoipDatabases(geoipDB string, geoip6DB string) error { @@ -212,126 +225,64 @@ func (m *Metrics) LoadGeoipDatabases(geoipDB string, geoip6DB string) error { return err } -func NewMetrics(metricsLogger *log.Logger) (*Metrics, error) { - m := new(Metrics) - - m.clientDeniedCount = make(map[messages.RendezvousMethod]uint) - m.clientRestrictedDeniedCount = make(map[messages.RendezvousMethod]uint) - m.clientUnrestrictedDeniedCount = make(map[messages.RendezvousMethod]uint) - m.clientProxyMatchCount = make(map[messages.RendezvousMethod]uint) - m.clientProxyTimeoutCount = make(map[messages.RendezvousMethod]uint) - - m.rendezvousCountryStats = make(map[messages.RendezvousMethod]map[string]int) - for _, rendezvousMethod := range rendezvoudMethodList { - m.rendezvousCountryStats[rendezvousMethod] = make(map[string]int) - } - - m.countryStats = CountryStats{ - counts: make(map[string]int), - proxies: make(map[string]map[string]bool), - unknown: make(map[string]bool), - natRestricted: make(map[string]bool), - natUnrestricted: make(map[string]bool), - natUnknown: make(map[string]bool), - } - for pType := range messages.KnownProxyTypes { - m.countryStats.proxies[pType] = make(map[string]bool) - } - - m.logger = metricsLogger - m.promMetrics = initPrometheus() - - // Write to log file every day with updated metrics - go m.logMetrics() - - return m, nil -} - // Logs metrics in intervals specified by metricsResolution func (m *Metrics) logMetrics() { heartbeat := time.Tick(metricsResolution) for range heartbeat { m.printMetrics() - m.zeroMetrics() } } +func (m *Metrics) loadAndZero(key string) uint64 { + count, loaded := m.counters.LoadAndDelete(key) + if !loaded { + count = new(uint64) + } + ptr := count.(*uint64) + return *ptr +} + func (m *Metrics) printMetrics() { - m.lock.Lock() m.logger.Println( "snowflake-stats-end", time.Now().UTC().Format("2006-01-02 15:04:05"), fmt.Sprintf("(%d s)", int(metricsResolution.Seconds())), ) - m.logger.Println("snowflake-ips", m.countryStats.Display()) - total := len(m.countryStats.unknown) - for pType, addresses := range m.countryStats.proxies { - m.logger.Printf("snowflake-ips-%s %d\n", pType, len(addresses)) - total += len(addresses) - } - m.logger.Println("snowflake-ips-total", total) - m.logger.Println("snowflake-idle-count", binCount(m.proxyIdleCount)) - m.logger.Println("snowflake-proxy-poll-with-relay-url-count", binCount(m.proxyPollWithRelayURLExtension)) - m.logger.Println("snowflake-proxy-poll-without-relay-url-count", binCount(m.proxyPollWithoutRelayURLExtension)) - m.logger.Println("snowflake-proxy-rejected-for-relay-url-count", binCount(m.proxyPollRejectedWithRelayURLExtension)) + m.logger.Println("snowflake-ips", formatAndClearCountryStats(m.proxies, false)) + m.logger.Printf("snowflake-ips-iptproxy %d\n", m.loadAndZero("proxy-iptproxy")) + m.logger.Printf("snowflake-ips-standalone %d\n", m.loadAndZero("proxy-standalone")) + m.logger.Printf("snowflake-ips-webext %d\n", m.loadAndZero("proxy-webext")) + m.logger.Printf("snowflake-ips-badge %d\n", m.loadAndZero("proxy-badge")) + m.logger.Println("snowflake-ips-total", m.loadAndZero("proxy-total")) + m.logger.Println("snowflake-idle-count", binCount(m.loadAndZero("proxy-idle"))) + m.logger.Println("snowflake-proxy-poll-with-relay-url-count", binCount(m.loadAndZero("proxy-poll-with-relay-url"))) + m.logger.Println("snowflake-proxy-poll-without-relay-url-count", binCount(m.loadAndZero("proxy-poll-without-relay-url"))) + m.logger.Println("snowflake-proxy-rejected-for-relay-url-count", binCount(m.loadAndZero("proxy-poll-rejected-relay-url"))) - m.logger.Println("client-denied-count", binCount(sumMapValues(&m.clientDeniedCount))) - m.logger.Println("client-restricted-denied-count", binCount(sumMapValues(&m.clientRestrictedDeniedCount))) - m.logger.Println("client-unrestricted-denied-count", binCount(sumMapValues(&m.clientUnrestrictedDeniedCount))) - m.logger.Println("client-snowflake-match-count", binCount(sumMapValues(&m.clientProxyMatchCount))) - m.logger.Println("client-snowflake-timeout-count", binCount(sumMapValues(&m.clientProxyTimeoutCount))) + m.logger.Println("client-denied-count", binCount(m.loadAndZero("client-denied"))) + m.logger.Println("client-restricted-denied-count", binCount(m.loadAndZero("client-restricted-denied"))) + m.logger.Println("client-unrestricted-denied-count", binCount(m.loadAndZero("client-unrestricted-denied"))) + m.logger.Println("client-snowflake-match-count", binCount(m.loadAndZero("client-match"))) + m.logger.Println("client-snowflake-timeout-count", binCount(m.loadAndZero("client-timeout"))) - for _, rendezvousMethod := range rendezvoudMethodList { - m.logger.Printf("client-%s-count %d\n", rendezvousMethod, binCount( - m.clientDeniedCount[rendezvousMethod]+m.clientProxyMatchCount[rendezvousMethod], - )) - m.logger.Printf("client-%s-ips %s\n", rendezvousMethod, m.DisplayRendezvousStatsByCountry(rendezvousMethod)) - } + m.logger.Printf("client-http-count %d\n", binCount(m.loadAndZero("client-http"))) + m.logger.Printf("client-http-ips %s\n", formatAndClearCountryStats(m.clientHTTPPolls, true)) + m.logger.Printf("client-ampcache-count %d\n", binCount(m.loadAndZero("client-amp"))) + m.logger.Printf("client-ampcache-ips %s\n", formatAndClearCountryStats(m.clientAMPPolls, true)) + m.logger.Printf("client-sqs-count %d\n", binCount(m.loadAndZero("client-sqs"))) + m.logger.Printf("client-sqs-ips %s\n", formatAndClearCountryStats(m.clientSQSPolls, true)) - m.logger.Println("snowflake-ips-nat-restricted", len(m.countryStats.natRestricted)) - m.logger.Println("snowflake-ips-nat-unrestricted", len(m.countryStats.natUnrestricted)) - m.logger.Println("snowflake-ips-nat-unknown", len(m.countryStats.natUnknown)) - m.lock.Unlock() + m.logger.Println("snowflake-ips-nat-restricted", m.loadAndZero("proxy-nat-restricted")) + m.logger.Println("snowflake-ips-nat-unrestricted", m.loadAndZero("proxy-nat-unrestricted")) + m.logger.Println("snowflake-ips-nat-unknown", m.loadAndZero("proxy-nat-unknown")) + + m.ips.Clear() } -// Restores all metrics to original values -func (m *Metrics) zeroMetrics() { - m.proxyIdleCount = 0 - m.clientDeniedCount = make(map[messages.RendezvousMethod]uint) - m.clientRestrictedDeniedCount = make(map[messages.RendezvousMethod]uint) - m.clientUnrestrictedDeniedCount = make(map[messages.RendezvousMethod]uint) - m.proxyPollRejectedWithRelayURLExtension = 0 - m.proxyPollWithRelayURLExtension = 0 - m.proxyPollWithoutRelayURLExtension = 0 - m.clientProxyMatchCount = make(map[messages.RendezvousMethod]uint) - m.clientProxyTimeoutCount = make(map[messages.RendezvousMethod]uint) - - m.rendezvousCountryStats = make(map[messages.RendezvousMethod]map[string]int) - for _, rendezvousMethod := range rendezvoudMethodList { - m.rendezvousCountryStats[rendezvousMethod] = make(map[string]int) - } - - m.countryStats.counts = make(map[string]int) - for pType := range m.countryStats.proxies { - m.countryStats.proxies[pType] = make(map[string]bool) - } - m.countryStats.unknown = make(map[string]bool) - m.countryStats.natRestricted = make(map[string]bool) - m.countryStats.natUnrestricted = make(map[string]bool) - m.countryStats.natUnknown = make(map[string]bool) -} - -// Rounds up a count to the nearest multiple of 8. -func binCount(count uint) uint { - return uint((math.Ceil(float64(count) / 8)) * 8) -} - -func sumMapValues(m *map[messages.RendezvousMethod]uint) uint { - var s uint = 0 - for _, v := range *m { - s += v - } - return s +// binCount rounds count up to the next multiple of 8. Returns 0 on integer +// overflow. +func binCount(count uint64) uint64 { + return (count + 7) / 8 * 8 } type PromMetrics struct { @@ -339,6 +290,7 @@ type PromMetrics struct { ProxyTotal *prometheus.CounterVec ProxyPollTotal *safeprom.CounterVec ClientPollTotal *safeprom.CounterVec + ProxyAnswerTotal *safeprom.CounterVec AvailableProxies *prometheus.GaugeVec ProxyPollWithRelayURLExtensionTotal *safeprom.CounterVec @@ -377,7 +329,16 @@ func initPrometheus() *PromMetrics { Name: "rounded_proxy_poll_total", Help: "The number of snowflake proxy polls, rounded up to a multiple of 8", }, - []string{"nat", "status"}, + []string{"nat", "type", "status"}, + ) + + promMetrics.ProxyAnswerTotal = safeprom.NewCounterVec( + prometheus.CounterOpts{ + Namespace: prometheusNamespace, + Name: "rounded_proxy_answer_total", + Help: "The number of snowflake proxy answers, rounded up to a multiple of 8", + }, + []string{"type", "status"}, ) promMetrics.ProxyPollWithRelayURLExtensionTotal = safeprom.NewCounterVec( @@ -419,7 +380,7 @@ func initPrometheus() *PromMetrics { // We need to register our metrics so they can be exported. promMetrics.registry.MustRegister( promMetrics.ClientPollTotal, promMetrics.ProxyPollTotal, - promMetrics.ProxyTotal, promMetrics.AvailableProxies, + promMetrics.ProxyTotal, promMetrics.ProxyAnswerTotal, promMetrics.AvailableProxies, promMetrics.ProxyPollWithRelayURLExtensionTotal, promMetrics.ProxyPollWithoutRelayURLExtensionTotal, promMetrics.ProxyPollRejectedForRelayURLExtensionTotal, diff --git a/broker/metrics_test.go b/broker/metrics_test.go new file mode 100644 index 0000000..fad1958 --- /dev/null +++ b/broker/metrics_test.go @@ -0,0 +1,47 @@ +package main + +import ( + "sync" + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestFormatAndClearCountryStats(t *testing.T) { + Convey("given a mapping of country stats", t, func() { + stats := new(sync.Map) + for _, record := range []struct { + cc string + count uint64 + }{ + {"IT", 50}, + {"FR", 200}, + {"TZ", 100}, + {"CN", 250}, + {"RU", 150}, + {"CA", 1}, + {"BE", 1}, + {"PH", 1}, + // The next 3 bin to the same value, 112. When not + // binned, they should go in the order MY,ZA,AT (ordered + // by count). When binned, they should go in the order + // AT,MY,ZA (ordered by country code). + {"AT", 105}, + {"MY", 112}, + {"ZA", 108}, + } { + stats.Store(record.cc, &record.count) + } + + Convey("the order should be correct with binned=false", func() { + So(formatAndClearCountryStats(stats, false), ShouldEqual, "CN=250,FR=200,RU=150,MY=112,ZA=108,AT=105,TZ=100,IT=50,BE=1,CA=1,PH=1") + }) + + Convey("the order should be correct with binned=true", func() { + So(formatAndClearCountryStats(stats, true), ShouldEqual, "CN=256,FR=200,RU=152,AT=112,MY=112,ZA=112,TZ=104,IT=56,BE=8,CA=8,PH=8") + }) + + // The map should be cleared on return. + stats.Range(func(_, _ any) bool { panic("map was not cleared") }) + }) +} diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go index 2f6f9e1..3eb4ba5 100644 --- a/broker/snowflake-broker_test.go +++ b/broker/snowflake-broker_test.go @@ -3,6 +3,8 @@ package main import ( "bytes" "container/heap" + "crypto/rand" + "encoding/base64" "encoding/hex" "fmt" "io" @@ -10,6 +12,7 @@ import ( "net/http" "net/http/httptest" "os" + "strings" "sync" "testing" "time" @@ -46,7 +49,8 @@ var ( "a=candidate:1000 1 udp 2000 8.8.8.8 3000 typ host\r\n" + "a=end-of-candidates\r\n" - sid = "ymbcCMto7KHNGYlp" + rawOffer = `{"type":"offer","sdp":"v=0\r\no=- 4358805017720277108 2 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=group:BUNDLE data\r\na=msid-semantic: WMS\r\nm=application 56688 DTLS/SCTP 5000\r\nc=IN IP4 0.0.0.0\r\na=candidate:3769337065 1 udp 2122260223 129.97.208.23 56688 typ host generation 0 network-id 1 network-cost 50\r\na=candidate:2921887769 1 tcp 1518280447 129.97.208.23 35441 typ host tcptype passive generation 0 network-id 1 network-cost 50\r\na=ice-ufrag:aMAZ\r\na=ice-pwd:jcHb08Jjgrazp2dzjdrvPPvV\r\na=ice-options:trickle\r\na=fingerprint:sha-256 C8:88:EE:B9:E7:02:2E:21:37:ED:7A:D1:EB:2B:A3:15:A2:3B:5B:1C:3D:D4:D5:1F:06:CF:52:40:03:F8:DD:66\r\na=setup:actpass\r\na=mid:data\r\na=sctpmap:5000 webrtc-datachannel 1024\r\n"}` + sid = "ymbcCMto7KHNGYlp" ) func createClientOffer(sdp, nat, fingerprint string) (*bytes.Reader, error) { @@ -89,7 +93,7 @@ func TestBroker(t *testing.T) { Convey("Context", t, func() { buf := new(bytes.Buffer) - ctx := NewBrokerContext(log.New(buf, "", 0), "", "") + ctx := NewBrokerContext(log.New(buf, "", 0), "snowflake.torproject.net") i := &IPC{ctx} Convey("Adds Snowflake", func() { @@ -402,12 +406,45 @@ client-sqs-ips So(body, ShouldEqual, `{"error":"timed out waiting for answer!"}`) }) + Convey("and correctly geolocates remote addr.", func() { + err := ctx.metrics.LoadGeoipDatabases("test_geoip", "test_geoip6") + So(err, ShouldBeNil) + clientRequest := &messages.ClientPollRequest{ + Offer: rawOffer, + NAT: NATUnknown, + Fingerprint: "", + } + encOffer, err := clientRequest.EncodeClientPollRequest() + So(err, ShouldBeNil) + r, err = http.NewRequest("GET", "/amp/client/"+amp.EncodePath(encOffer), nil) + So(err, ShouldBeNil) + ampClientOffers(i, w, r) + So(w.Code, ShouldEqual, http.StatusOK) + body, err := decodeAMPArmorToString(w.Body) + So(err, ShouldBeNil) + So(body, ShouldEqual, `{"error":"no snowflake proxies currently available"}`) + + ctx.metrics.printMetrics() + So(buf.String(), ShouldContainSubstring, `client-denied-count 8 +client-restricted-denied-count 8 +client-unrestricted-denied-count 0 +client-snowflake-match-count 0 +client-snowflake-timeout-count 0 +client-http-count 0 +client-http-ips +client-ampcache-count 8 +client-ampcache-ips CA=8 +client-sqs-count 0 +client-sqs-ips +`) + }) + }) Convey("Responds to proxy polls...", func() { done := make(chan bool) w := httptest.NewRecorder() - data := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`)) + data := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0", "AcceptedRelayPattern": "snowflake.torproject.net"}`)) r, err := http.NewRequest("POST", "snowflake.broker/proxy", data) So(err, ShouldBeNil) @@ -493,7 +530,7 @@ client-sqs-ips }) Convey("End-To-End", t, func() { - ctx := NewBrokerContext(NullLogger(), "", "") + ctx := NewBrokerContext(NullLogger(), "snowflake.torproject.net") i := &IPC{ctx} Convey("Check for client/proxy data race", func() { @@ -504,7 +541,7 @@ client-sqs-ips // Make proxy poll wp := httptest.NewRecorder() - datap := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`)) + datap := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0","AcceptedRelayPattern":"snowflake.torproject.net"}`)) rp, err := http.NewRequest("POST", "snowflake.broker/proxy", datap) So(err, ShouldBeNil) @@ -549,7 +586,7 @@ client-sqs-ips polled := make(chan bool) // Proxy polls with its ID first... - dataP := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`)) + dataP := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0","AcceptedRelayPattern":"snowflake.torproject.net"}`)) wP := httptest.NewRecorder() rP, err := http.NewRequest("POST", "snowflake.broker/proxy", dataP) So(err, ShouldBeNil) @@ -646,11 +683,11 @@ func TestSnowflakeHeap(t *testing.T) { func TestInvalidGeoipFile(t *testing.T) { Convey("Geoip", t, func() { // Make sure things behave properly if geoip file fails to load - ctx := NewBrokerContext(NullLogger(), "", "") + ctx := NewBrokerContext(NullLogger(), "") if err := ctx.metrics.LoadGeoipDatabases("invalid_filename", "invalid_filename6"); err != nil { log.Printf("loading geo ip databases returned error: %v", err) } - ctx.metrics.UpdateCountryStats("127.0.0.1", "", NATUnrestricted) + ctx.metrics.UpdateProxyStats("127.0.0.1", "", NATUnrestricted) So(ctx.metrics.geoipdb, ShouldBeNil) }) @@ -660,7 +697,7 @@ func TestMetrics(t *testing.T) { Convey("Test metrics...", t, func() { done := make(chan bool) buf := new(bytes.Buffer) - ctx := NewBrokerContext(log.New(buf, "", 0), "", "") + ctx := NewBrokerContext(log.New(buf, "", 0), "snowflake.torproject.net") i := &IPC{ctx} err := ctx.metrics.LoadGeoipDatabases("test_geoip", "test_geoip6") @@ -669,9 +706,9 @@ func TestMetrics(t *testing.T) { //Test addition of proxy polls Convey("for proxy polls", func() { w := httptest.NewRecorder() - data := bytes.NewReader([]byte("{\"Sid\":\"ymbcCMto7KHNGYlp\",\"Version\":\"1.0\"}")) + data := bytes.NewReader([]byte("{\"Sid\":\"ymbcCMto7KHNGYlp\",\"Version\":\"1.0\",\"AcceptedRelayPattern\":\"snowflake.torproject.net\"}")) r, err := http.NewRequest("POST", "snowflake.broker/proxy", data) - r.RemoteAddr = "129.97.208.23:8888" //CA geoip + r.RemoteAddr = "129.97.208.23" //CA geoip So(err, ShouldBeNil) go func(i *IPC) { proxyPolls(i, w, r) @@ -682,9 +719,9 @@ func TestMetrics(t *testing.T) { <-done w = httptest.NewRecorder() - data = bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0","Type":"standalone"}`)) + data = bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0","Type":"standalone","AcceptedRelayPattern":"snowflake.torproject.net"}`)) r, err = http.NewRequest("POST", "snowflake.broker/proxy", data) - r.RemoteAddr = "129.97.208.23:8888" //CA geoip + r.RemoteAddr = "129.97.208.24" //CA geoip So(err, ShouldBeNil) go func(i *IPC) { proxyPolls(i, w, r) @@ -695,9 +732,9 @@ func TestMetrics(t *testing.T) { <-done w = httptest.NewRecorder() - data = bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0","Type":"badge"}`)) + data = bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0","Type":"badge","AcceptedRelayPattern":"snowflake.torproject.net"}`)) r, err = http.NewRequest("POST", "snowflake.broker/proxy", data) - r.RemoteAddr = "129.97.208.23:8888" //CA geoip + r.RemoteAddr = "129.97.208.25" //CA geoip So(err, ShouldBeNil) go func(i *IPC) { proxyPolls(i, w, r) @@ -708,9 +745,9 @@ func TestMetrics(t *testing.T) { <-done w = httptest.NewRecorder() - data = bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0","Type":"webext"}`)) + data = bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0","Type":"webext","AcceptedRelayPattern":"snowflake.torproject.net"}`)) r, err = http.NewRequest("POST", "snowflake.broker/proxy", data) - r.RemoteAddr = "129.97.208.23:8888" //CA geoip + r.RemoteAddr = "129.97.208.26" //CA geoip So(err, ShouldBeNil) go func(i *IPC) { proxyPolls(i, w, r) @@ -728,8 +765,8 @@ func TestMetrics(t *testing.T) { So(metricsStr, ShouldContainSubstring, "\nsnowflake-ips-webext 1\n") So(metricsStr, ShouldEndWith, `snowflake-ips-total 4 snowflake-idle-count 8 -snowflake-proxy-poll-with-relay-url-count 0 -snowflake-proxy-poll-without-relay-url-count 8 +snowflake-proxy-poll-with-relay-url-count 8 +snowflake-proxy-poll-without-relay-url-count 0 snowflake-proxy-rejected-for-relay-url-count 0 client-denied-count 0 client-restricted-denied-count 0 @@ -744,7 +781,7 @@ client-sqs-count 0 client-sqs-ips snowflake-ips-nat-restricted 0 snowflake-ips-nat-unrestricted 0 -snowflake-ips-nat-unknown 1 +snowflake-ips-nat-unknown 4 `) }) @@ -774,7 +811,6 @@ client-sqs-ips `) // Test reset buf.Reset() - ctx.metrics.zeroMetrics() ctx.metrics.printMetrics() So(buf.String(), ShouldContainSubstring, "\nsnowflake-ips \n") So(buf.String(), ShouldContainSubstring, "\nsnowflake-ips-standalone 0\n") @@ -881,9 +917,6 @@ snowflake-ips-nat-unknown 0 So(err, ShouldBeNil) clientOffers(i, w, r) - ctx.metrics.printMetrics() - So(buf.String(), ShouldContainSubstring, "client-denied-count 8\nclient-restricted-denied-count 8\nclient-unrestricted-denied-count 0\n") - w = httptest.NewRecorder() data, err = createClientOffer(sdp, NATRestricted, "") So(err, ShouldBeNil) @@ -899,7 +932,7 @@ snowflake-ips-nat-unknown 0 //Test unique ip Convey("proxy counts by unique ip", func() { w := httptest.NewRecorder() - data := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`)) + data := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0","AcceptedRelayPattern":"snowflake.torproject.net"}`)) r, err := http.NewRequest("POST", "snowflake.broker/proxy", data) r.RemoteAddr = "129.97.208.23:8888" //CA geoip So(err, ShouldBeNil) @@ -911,7 +944,7 @@ snowflake-ips-nat-unknown 0 p.offerChannel <- nil <-done - data = bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`)) + data = bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0","AcceptedRelayPattern":"snowflake.torproject.net"}`)) r, err = http.NewRequest("POST", "snowflake.broker/proxy", data) if err != nil { log.Printf("unable to get NewRequest with error: %v", err) @@ -933,7 +966,7 @@ snowflake-ips-nat-unknown 0 //Test NAT types Convey("proxy counts by NAT type", func() { w := httptest.NewRecorder() - data := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.2","Type":"unknown","NAT":"restricted"}`)) + data := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.2","Type":"unknown","NAT":"restricted","AcceptedRelayPattern":"snowflake.torproject.net"}`)) r, err := http.NewRequest("POST", "snowflake.broker/proxy", data) r.RemoteAddr = "129.97.208.23:8888" //CA geoip So(err, ShouldBeNil) @@ -945,10 +978,7 @@ snowflake-ips-nat-unknown 0 p.offerChannel <- nil <-done - ctx.metrics.printMetrics() - So(buf.String(), ShouldContainSubstring, "snowflake-ips-nat-restricted 1\nsnowflake-ips-nat-unrestricted 0\nsnowflake-ips-nat-unknown 0") - - data = bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.2","Type":"unknown","NAT":"unrestricted"}`)) + data = bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.2","Type":"unknown","NAT":"unrestricted","AcceptedRelayPattern":"snowflake.torproject.net"}`)) r, err = http.NewRequest("POST", "snowflake.broker/proxy", data) if err != nil { log.Printf("unable to get NewRequest with error: %v", err) @@ -979,7 +1009,6 @@ snowflake-ips-nat-unknown 0 So(buf.String(), ShouldContainSubstring, "client-denied-count 8\nclient-restricted-denied-count 8\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0") buf.Reset() - ctx.metrics.zeroMetrics() data, err = createClientOffer(sdp, NATUnrestricted, "") So(err, ShouldBeNil) @@ -992,7 +1021,6 @@ snowflake-ips-nat-unknown 0 So(buf.String(), ShouldContainSubstring, "client-denied-count 8\nclient-restricted-denied-count 0\nclient-unrestricted-denied-count 8\nclient-snowflake-match-count 0") buf.Reset() - ctx.metrics.zeroMetrics() data, err = createClientOffer(sdp, NATUnknown, "") So(err, ShouldBeNil) @@ -1004,20 +1032,105 @@ snowflake-ips-nat-unknown 0 ctx.metrics.printMetrics() So(buf.String(), ShouldContainSubstring, "client-denied-count 8\nclient-restricted-denied-count 8\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0") }) - Convey("for country stats order", func() { + Convey("that seen IPs map is cleared after each print", func() { + w := httptest.NewRecorder() + data := bytes.NewReader([]byte("{\"Sid\":\"ymbcCMto7KHNGYlp\",\"Version\":\"1.0\",\"AcceptedRelayPattern\":\"snowflake.torproject.net\"}")) + r, err := http.NewRequest("POST", "snowflake.broker/proxy", data) + r.RemoteAddr = "129.97.208.23" //CA geoip + So(err, ShouldBeNil) + go func(i *IPC) { + proxyPolls(i, w, r) + done <- true + }(i) + p := <-ctx.proxyPolls //manually unblock poll + p.offerChannel <- nil + <-done + + ctx.metrics.printMetrics() + So(buf.String(), ShouldContainSubstring, "snowflake-ips CA=1") + So(buf.String(), ShouldContainSubstring, "snowflake-ips-total 1") + buf.Reset() + + w = httptest.NewRecorder() + data = bytes.NewReader([]byte("{\"Sid\":\"ymbcCMto7KHNGYlp\",\"Version\":\"1.0\",\"AcceptedRelayPattern\":\"snowflake.torproject.net\"}")) + r, err = http.NewRequest("POST", "snowflake.broker/proxy", data) + r.RemoteAddr = "129.97.208.23" //CA geoip + So(err, ShouldBeNil) + go func(i *IPC) { + proxyPolls(i, w, r) + done <- true + }(i) + p = <-ctx.proxyPolls //manually unblock poll + p.offerChannel <- nil + <-done + + ctx.metrics.printMetrics() + So(buf.String(), ShouldContainSubstring, "snowflake-ips CA=1") + So(buf.String(), ShouldContainSubstring, "snowflake-ips-total 1") + buf.Reset() - stats := map[string]int{ - "IT": 50, - "FR": 200, - "TZ": 100, - "CN": 250, - "RU": 150, - "CA": 1, - "BE": 1, - "PH": 1, - } - ctx.metrics.countryStats.counts = stats - So(ctx.metrics.countryStats.Display(), ShouldEqual, "CN=250,FR=200,RU=150,TZ=100,IT=50,BE=1,CA=1,PH=1") + }) + }) +} + +func TestConcurrency(t *testing.T) { + Convey("Test concurency with", t, func() { + ctx := NewBrokerContext(NullLogger(), "snowflake.torproject.net") + i := &IPC{ctx} + Convey("multiple simultaneous polls", func(c C) { + go ctx.Broker() + + var proxies sync.WaitGroup + var wg sync.WaitGroup + + proxies.Add(1000) + // Multiple proxy polls + for x := 0; x < 1000; x++ { + wp := httptest.NewRecorder() + buf := make([]byte, 16) + _, err := rand.Read(buf) + id := strings.TrimRight(base64.StdEncoding.EncodeToString(buf), "=") + + datap := bytes.NewReader([]byte(fmt.Sprintf("{\"Sid\": \"%s\",\"Version\":\"1.0\",\"AcceptedRelayPattern\":\"snowflake.torproject.net\"}", id))) + rp, err := http.NewRequest("POST", "snowflake.broker/proxy", datap) + So(err, ShouldBeNil) + + go func() { + proxies.Done() + proxyPolls(i, wp, rp) + c.So(wp.Code, ShouldEqual, http.StatusOK) + + // Proxy answers + wp = httptest.NewRecorder() + datap, err = createProxyAnswer(sdp, id) + c.So(err, ShouldBeNil) + rp, err = http.NewRequest("POST", "snowflake.broker/answer", datap) + c.So(err, ShouldBeNil) + go func() { + proxyAnswers(i, wp, rp) + }() + }() + } + // Wait for all proxies to poll before sending client offers + proxies.Wait() + + // Multiple client offers + for x := 0; x < 500; x++ { + wg.Add(1) + wc := httptest.NewRecorder() + datac, err := createClientOffer(sdp, NATUnrestricted, "") + So(err, ShouldBeNil) + rc, err := http.NewRequest("POST", "snowflake.broker/client", datac) + So(err, ShouldBeNil) + + go func() { + clientOffers(i, wc, rc) + c.So(wc.Code, ShouldEqual, http.StatusOK) + c.So(wc.Body.String(), ShouldContainSubstring, "8.8.8.8") + wg.Done() + }() + } + wg.Wait() }) }) } diff --git a/broker/sqs.go b/broker/sqs.go index fb1164e..16a97c9 100644 --- a/broker/sqs.go +++ b/broker/sqs.go @@ -12,7 +12,6 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sqs/types" "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/messages" "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/sqsclient" - "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/util" ) const ( @@ -119,23 +118,25 @@ func (r *sqsHandler) cleanupClientQueues(ctx context.Context) { } } - log.Printf("SQSHandler: finished running iteration of client queue cleanup. found and deleted %d client queues.\n", numDeleted) } } } -func (r *sqsHandler) handleMessage(context context.Context, message *types.Message) { +func (r *sqsHandler) handleMessage(mainCtx context.Context, message *types.Message) { var encPollReq []byte var response []byte var err error + ctx, cancel := context.WithTimeout(mainCtx, ClientTimeout*time.Second) + defer cancel() + clientID := message.MessageAttributes["ClientID"].StringValue if clientID == nil { log.Println("SQSHandler: got SDP offer in SQS message with no client ID. ignoring this message.") return } - res, err := r.SQSClient.CreateQueue(context, &sqs.CreateQueueInput{ + res, err := r.SQSClient.CreateQueue(ctx, &sqs.CreateQueueInput{ QueueName: aws.String("snowflake-client-" + *clientID), }) if err != nil { @@ -146,27 +147,11 @@ func (r *sqsHandler) handleMessage(context context.Context, message *types.Messa encPollReq = []byte(*message.Body) - // Get best guess Client IP for geolocating - remoteAddr := "" - req, err := messages.DecodeClientPollRequest(encPollReq) - if err != nil { - log.Printf("SQSHandler: error encounted when decoding client poll request %s: %v\n", *clientID, err) - } else { - sdp, err := util.DeserializeSessionDescription(req.Offer) - if err != nil { - log.Printf("SQSHandler: error encounted when deserializing session desc %s: %v\n", *clientID, err) - } else { - candidateAddrs := util.GetCandidateAddrs(sdp.SDP) - if len(candidateAddrs) > 0 { - remoteAddr = candidateAddrs[0].String() - } - } - } - arg := messages.Arg{ Body: encPollReq, - RemoteAddr: remoteAddr, + RemoteAddr: "", RendezvousMethod: messages.RendezvousSqs, + Context: ctx, } err = r.IPC.ClientOffers(arg, &response) @@ -175,7 +160,7 @@ func (r *sqsHandler) handleMessage(context context.Context, message *types.Messa return } - r.SQSClient.SendMessage(context, &sqs.SendMessageInput{ + r.SQSClient.SendMessage(ctx, &sqs.SendMessageInput{ QueueUrl: answerSQSURL, MessageBody: aws.String(string(response)), }) diff --git a/broker/sqs_test.go b/broker/sqs_test.go index 33e38f1..59fe701 100644 --- a/broker/sqs_test.go +++ b/broker/sqs_test.go @@ -23,7 +23,7 @@ func TestSQS(t *testing.T) { Convey("Context", t, func() { buf := new(bytes.Buffer) - ipcCtx := NewBrokerContext(log.New(buf, "", 0), "", "") + ipcCtx := NewBrokerContext(log.New(buf, "", 0), "") i := &IPC{ipcCtx} Convey("Responds to SQS client offers...", func() { @@ -138,7 +138,7 @@ func TestSQS(t *testing.T) { sqsHandlerContext, sqsCancelFunc := context.WithCancel(context.Background()) var numTimes atomic.Uint32 - mockSQSClient.EXPECT().ReceiveMessage(sqsHandlerContext, &sqsReceiveMessageInput).AnyTimes().DoAndReturn( + mockSQSClient.EXPECT().ReceiveMessage(gomock.Any(), &sqsReceiveMessageInput).AnyTimes().DoAndReturn( func(ctx context.Context, input *sqs.ReceiveMessageInput, optFns ...func(*sqs.Options)) (*sqs.ReceiveMessageOutput, error) { n := numTimes.Add(1) @@ -153,11 +153,11 @@ func TestSQS(t *testing.T) { return nil, errors.New("error") }) - mockSQSClient.EXPECT().CreateQueue(sqsHandlerContext, &sqsCreateQueueInput).Return(&sqs.CreateQueueOutput{ + mockSQSClient.EXPECT().CreateQueue(gomock.Any(), &sqsCreateQueueInput).Return(&sqs.CreateQueueOutput{ QueueUrl: responseQueueURL, }, nil).AnyTimes() mockSQSClient.EXPECT().DeleteMessage(gomock.Any(), gomock.Any()).AnyTimes() - mockSQSClient.EXPECT().SendMessage(sqsHandlerContext, gomock.Any()).Times(1).DoAndReturn( + mockSQSClient.EXPECT().SendMessage(gomock.Any(), gomock.Any()).Times(1).DoAndReturn( func(ctx context.Context, input *sqs.SendMessageInput, optFns ...func(*sqs.Options)) (*sqs.SendMessageOutput, error) { c.So(input.MessageBody, ShouldEqual, aws.String("{\"answer\":\"fake answer\"}")) // Ensure that match is correctly recorded in metrics diff --git a/client/README.md b/client/README.md index 1529e8d..2cbfb8f 100644 --- a/client/README.md +++ b/client/README.md @@ -35,7 +35,18 @@ UseBridges 1 ClientTransportPlugin snowflake exec ./client -log snowflake.log -Bridge snowflake 192.0.2.3:80 2B280B23E1107BB62ABFC40DDCC8824814F80A72 fingerprint=2B280B23E1107BB62ABFC40DDCC8824814F80A72 url=https://snowflake-broker.torproject.net.global.prod.fastly.net/ fronts=foursquare.com,github.githubassets.com ice=stun:stun.l.google.com:19302,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478 utls-imitate=hellorandomizedalpn +# CDN77 + +Bridge snowflake 192.0.2.4:80 8838024498816A039FCBBAB14E6F40A0843051FA fingerprint=8838024498816A039FCBBAB14E6F40A0843051FA url=https://1098762253.rsc.cdn77.org/ fronts=www.cdn77.com,www.phpmyadmin.net ice=stun:stun.antisip.com:3478,stun:stun.epygi.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.mixvoip.com:3478,stun:stun.nextcloud.com:3478,stun:stun.bethesda.net:3478,stun:stun.nextcloud.com:443 utls-imitate=hellorandomizedalpn +Bridge snowflake 192.0.2.3:80 2B280B23E1107BB62ABFC40DDCC8824814F80A72 fingerprint=2B280B23E1107BB62ABFC40DDCC8824814F80A72 url=https://1098762253.rsc.cdn77.org/ fronts=www.cdn77.com,www.phpmyadmin.net ice=stun:stun.antisip.com:3478,stun:stun.epygi.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.mixvoip.com:3478,stun:stun.nextcloud.com:3478,stun:stun.bethesda.net:3478,stun:stun.nextcloud.com:443 utls-imitate=hellorandomizedalpn + +# ampcache +#Bridge snowflake 192.0.2.5:80 2B280B23E1107BB62ABFC40DDCC8824814F80A72 fingerprint=2B280B23E1107BB62ABFC40DDCC8824814F80A72 url=https://snowflake-broker.torproject.net/ ampcache=https://cdn.ampproject.org/ front=www.google.com ice=stun:stun.antisip.com:3478,stun:stun.epygi.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.mixvoip.com:3478,stun:stun.nextcloud.com:3478,stun:stun.bethesda.net:3478,stun:stun.nextcloud.com:443 utls-imitate=hellorandomizedalpn +#Bridge snowflake 192.0.2.6:80 8838024498816A039FCBBAB14E6F40A0843051FA fingerprint=8838024498816A039FCBBAB14E6F40A0843051FA url=https://snowflake-broker.torproject.net/ ampcache=https://cdn.ampproject.org/ front=www.google.com ice=stun:stun.antisip.com:3478,stun:stun.epygi.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.mixvoip.com:3478,stun:stun.nextcloud.com:3478,stun:stun.bethesda.net:3478,stun:stun.nextcloud.com:443 utls-imitate=hellorandomizedalpn + +# sqs +#Bridge snowflake 192.0.2.5:80 2B280B23E1107BB62ABFC40DDCC8824814F80A72 fingerprint=2B280B23E1107BB62ABFC40DDCC8824814F80A72 sqsqueue=https://sqs.us-east-1.amazonaws.com/893902434899/snowflake-broker sqscreds=eyJhd3MtYWNjZXNzLWtleS1pZCI6IkFLSUE1QUlGNFdKSlhTN1lIRUczIiwiYXdzLXNlY3JldC1rZXkiOiI3U0RNc0pBNHM1RitXZWJ1L3pMOHZrMFFXV0lsa1c2Y1dOZlVsQ0tRIn0= ice=stun:stun.antisip.com:3478,stun:stun.epygi.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.nextcloud.com:3478,stun:stun.bethesda.net:3478,stun:stun.nextcloud.com:443 utls-imitate=hellorandomizedalpn +#Bridge snowflake 192.0.2.6:80 8838024498816A039FCBBAB14E6F40A0843051FA fingerprint=8838024498816A039FCBBAB14E6F40A0843051FA sqsqueue=https://sqs.us-east-1.amazonaws.com/893902434899/snowflake-broker sqscreds=eyJhd3MtYWNjZXNzLWtleS1pZCI6IkFLSUE1QUlGNFdKSlhTN1lIRUczIiwiYXdzLXNlY3JldC1rZXkiOiI3U0RNc0pBNHM1RitXZWJ1L3pMOHZrMFFXV0lsa1c2Y1dOZlVsQ0tRIn0= ice=stun:stun.antisip.com:3478,stun:stun.epygi.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.nextcloud.com:3478,stun:stun.bethesda.net:3478,stun:stun.nextcloud.com:443 utls-imitate=hellorandomizedalpn ``` `fingerprint=` is the fingerprint of bridge that the client will ultimately be connecting to. diff --git a/common/messages/ipc.go b/common/messages/ipc.go index 2a61b9d..91eccdb 100644 --- a/common/messages/ipc.go +++ b/common/messages/ipc.go @@ -1,6 +1,7 @@ package messages import ( + "context" "errors" ) @@ -16,6 +17,7 @@ type Arg struct { Body []byte RemoteAddr string RendezvousMethod RendezvousMethod + Context context.Context } var ( diff --git a/common/turbotunnel/queuepacketconn_test.go b/common/turbotunnel/queuepacketconn_test.go index 30e69e3..0ff19b2 100644 --- a/common/turbotunnel/queuepacketconn_test.go +++ b/common/turbotunnel/queuepacketconn_test.go @@ -150,6 +150,13 @@ func (c *TranscriptPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) { return c.PacketConn.WriteTo(p, addr) } +func (c *TranscriptPacketConn) Length() int { + c.lock.Lock() + defer c.lock.Unlock() + + return len(c.Transcript) +} + // Tests that QueuePacketConn.WriteTo is compatible with the way kcp-go uses // PacketConn, allocating source buffers in a sync.Pool. // @@ -208,15 +215,16 @@ func TestQueuePacketConnWriteToKCP(t *testing.T) { panic(err) } + // A sleep after the Write makes buffer reuse more likely, + // and to allow the connection to flush before close + time.Sleep(500 * time.Millisecond) + err = conn.Close() if err != nil { panic(err) } - // A sleep after the Write makes buffer reuse more likely. - time.Sleep(100 * time.Millisecond) - - if len(transcript.Transcript) == 0 { + if transcript.Length() == 0 { panic("empty transcript") } diff --git a/doc/broker-spec.txt b/doc/broker-spec.txt index 3f9ce7a..b502cb4 100644 --- a/doc/broker-spec.txt +++ b/doc/broker-spec.txt @@ -82,6 +82,13 @@ Metrics data from the Snowflake broker can be retrieved by sending an HTTP GET r A count of the number of times a client successfully received a proxy from the broker, rounded up to the nearest multiple of 8. + "client-snowflake-timeout-count" NUM NL + [At most once.] + + A count of the number of times a client was matched with a proxy + but timed out before receiving the proxy's WebRTC answer, + rounded up to the nearest multiple of 8. + "client-http-count" NUM NL [At most once.] @@ -97,6 +104,9 @@ Metrics data from the Snowflake broker can be retrieved by sending an HTTP GET r rounded up to the nearest multiple of 8. Each country code only appears once. + Note that this descriptor field name is misleading. We use IP addresses + to partition by country, but this metric counts polls, not unique IPs. + "client-ampcache-count" NUM NL [At most once.] @@ -112,6 +122,9 @@ Metrics data from the Snowflake broker can be retrieved by sending an HTTP GET r method, rounded up to the nearest multiple of 8. Each country code only appears once. + Note that this descriptor field name is misleading. We use IP addresses + to partition by country, but this metric counts polls, not unique IPs. + "client-sqs-count" NUM NL [At most once.] @@ -127,6 +140,9 @@ Metrics data from the Snowflake broker can be retrieved by sending an HTTP GET r rounded up to the nearest multiple of 8. Each country code only appears once. + Note that this descriptor field name is misleading. We use IP addresses + to partition by country, but this metric counts polls, not unique IPs. + "snowflake-ips-nat-restricted" NUM NL [At most once.] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8d8724 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + snowflake-proxy: + network_mode: host + image: containers.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake:latest + container_name: snowflake-proxy + restart: unless-stopped + # For a full list of Snowflake Proxy CLI parameters see + # https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/tree/main/proxy?ref_type=heads#running-a-standalone-snowflake-proxy + #command: [ "-ephemeral-ports-range", "30000:60000" ] + watchtower: + image: containrrr/watchtower + container_name: watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + command: snowflake-proxy diff --git a/go.mod b/go.mod index 622c4cc..6fbbf38 100644 --- a/go.mod +++ b/go.mod @@ -1,48 +1,48 @@ module gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2 -go 1.21 +go 1.23.0 require ( - github.com/aws/aws-sdk-go-v2 v1.36.1 - github.com/aws/aws-sdk-go-v2/config v1.29.6 - github.com/aws/aws-sdk-go-v2/credentials v1.17.59 - github.com/aws/aws-sdk-go-v2/service/sqs v1.37.14 + github.com/aws/aws-sdk-go-v2 v1.39.0 + github.com/aws/aws-sdk-go-v2/config v1.31.8 + github.com/aws/aws-sdk-go-v2/credentials v1.18.12 + github.com/aws/aws-sdk-go-v2/service/sqs v1.42.5 github.com/golang/mock v1.6.0 github.com/gorilla/websocket v1.5.3 - github.com/miekg/dns v1.1.63 - github.com/pion/ice/v4 v4.0.7 - github.com/pion/sdp/v3 v3.0.11 + github.com/miekg/dns v1.1.65 + github.com/pion/ice/v4 v4.0.10 + github.com/pion/sdp/v3 v3.0.16 github.com/pion/stun/v3 v3.0.0 github.com/pion/transport/v3 v3.0.7 - github.com/pion/webrtc/v4 v4.0.13 - github.com/prometheus/client_golang v1.21.0 + github.com/pion/webrtc/v4 v4.1.4 + github.com/prometheus/client_golang v1.22.0 github.com/realclientip/realclientip-go v1.0.0 github.com/refraction-networking/utls v1.6.7 github.com/smartystreets/goconvey v1.8.1 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 - github.com/xtaci/kcp-go/v5 v5.6.8 - github.com/xtaci/smux v1.5.34 + github.com/xtaci/kcp-go/v5 v5.6.24 + github.com/xtaci/smux v1.5.35 gitlab.torproject.org/tpo/anti-censorship/geoip v0.0.0-20210928150955-7ce4b3d98d01 gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/goptlib v1.6.0 - gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/ptutil v0.0.0-20250130151315-efaf4e0ec0d3 - golang.org/x/crypto v0.33.0 - golang.org/x/net v0.35.0 - golang.org/x/sys v0.30.0 + gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/ptutil v0.0.0-20250815012447-418f76dcf315 + golang.org/x/crypto v0.41.0 + golang.org/x/net v0.42.0 + golang.org/x/sys v0.35.0 ) require ( github.com/andybalholm/brotli v1.0.6 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.24.15 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.14 // indirect - github.com/aws/smithy-go v1.22.2 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 // indirect + github.com/aws/smithy-go v1.23.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect @@ -50,37 +50,37 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect - github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/klauspost/reedsolomon v1.12.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pion/datachannel v1.5.10 // indirect - github.com/pion/dtls/v3 v3.0.4 // indirect - github.com/pion/interceptor v0.1.37 // indirect - github.com/pion/logging v0.2.3 // indirect + github.com/pion/dtls/v3 v3.0.7 // indirect + github.com/pion/interceptor v0.1.40 // indirect + github.com/pion/logging v0.2.4 // indirect github.com/pion/mdns/v2 v2.0.7 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/rtcp v1.2.15 // indirect - github.com/pion/rtp v1.8.12 // indirect - github.com/pion/sctp v1.8.37 // indirect - github.com/pion/srtp/v3 v3.0.4 // indirect - github.com/pion/turn/v4 v4.0.0 // indirect + github.com/pion/rtp v1.8.21 // indirect + github.com/pion/sctp v1.8.39 // indirect + github.com/pion/srtp/v3 v3.0.7 // indirect + github.com/pion/turn/v4 v4.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/smarty/assertions v1.15.0 // indirect - github.com/templexxx/cpu v0.1.0 // indirect - github.com/templexxx/xorsimd v0.4.2 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect github.com/wlynxg/anet v0.0.5 // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/sync v0.11.0 // indirect - golang.org/x/text v0.22.0 // indirect - golang.org/x/tools v0.22.0 // indirect - google.golang.org/protobuf v1.36.1 // indirect + golang.org/x/mod v0.26.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/tools v0.35.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/refraction-networking/utls v1.6.7 => gitlab.torproject.org/shelikhoo/utls-temporary v0.0.0-20250428152032-7f32539913c8 diff --git a/go.sum b/go.sum index d0bc507..2e54433 100644 --- a/go.sum +++ b/go.sum @@ -2,34 +2,34 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/aws/aws-sdk-go-v2 v1.36.1 h1:iTDl5U6oAhkNPba0e1t1hrwAo02ZMqbrGq4k5JBWM5E= -github.com/aws/aws-sdk-go-v2 v1.36.1/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM= -github.com/aws/aws-sdk-go-v2/config v1.29.6 h1:fqgqEKK5HaZVWLQoLiC9Q+xDlSp+1LYidp6ybGE2OGg= -github.com/aws/aws-sdk-go-v2/config v1.29.6/go.mod h1:Ft+WLODzDQmCTHDvqAH1JfC2xxbZ0MxpZAcJqmE1LTQ= -github.com/aws/aws-sdk-go-v2/credentials v1.17.59 h1:9btwmrt//Q6JcSdgJOLI98sdr5p7tssS9yAsGe8aKP4= -github.com/aws/aws-sdk-go-v2/credentials v1.17.59/go.mod h1:NM8fM6ovI3zak23UISdWidyZuI1ghNe2xjzUZAyT+08= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28 h1:KwsodFKVQTlI5EyhRSugALzsV6mG/SGrdjlMXSZSdso= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28/go.mod h1:EY3APf9MzygVhKuPXAc5H+MkGb8k/DOSQjWS0LgkKqI= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 h1:BjUcr3X3K0wZPGFg2bxOWW3VPN8rkE3/61zhP+IHviA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32/go.mod h1:80+OGC/bgzzFFTUmcuwD0lb4YutwQeKLFpmt6hoWapU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32 h1:m1GeXHVMJsRsUAqG6HjZWx9dj7F5TR+cF1bjyfYyBd4= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32/go.mod h1:IitoQxGfaKdVLNg0hD8/DXmAqNy0H4K2H2Sf91ti8sI= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 h1:SYVGSFQHlchIcy6e7x12bsrxClCXSP5et8cqVhL8cuw= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13/go.mod h1:kizuDaLX37bG5WZaoxGPQR/LNFXpxp0vsUnqfkWXfNE= -github.com/aws/aws-sdk-go-v2/service/sqs v1.37.14 h1:KSVbQW2umLp7i4Lo6mvBUz5PqV+Ze/IL6LCTasxQWEk= -github.com/aws/aws-sdk-go-v2/service/sqs v1.37.14/go.mod h1:jiaEkIw2Bb6IsoY9PDAZqVXJjNaKSxQGGj10CiloDWU= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.15 h1:/eE3DogBjYlvlbhd2ssWyeuovWunHLxfgw3s/OJa4GQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.15/go.mod h1:2PCJYpi7EKeA5SkStAmZlF6fi0uUABuhtF8ILHjGc3Y= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14 h1:M/zwXiL2iXUrHputuXgmO94TVNmcenPHxgLXLutodKE= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14/go.mod h1:RVwIw3y/IqxC2YEXSIkAzRDdEU1iRabDPaYjpGCbCGQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.14 h1:TzeR06UCMUq+KA3bDkujxK1GVGy+G8qQN/QVYzGLkQE= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.14/go.mod h1:dspXf/oYWGWo6DEvj98wpaTeqt5+DMidZD0A9BYTizc= -github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= -github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/aws-sdk-go-v2 v1.39.0 h1:xm5WV/2L4emMRmMjHFykqiA4M/ra0DJVSWUkDyBjbg4= +github.com/aws/aws-sdk-go-v2 v1.39.0/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= +github.com/aws/aws-sdk-go-v2/config v1.31.8 h1:kQjtOLlTU4m4A64TsRcqwNChhGCwaPBt+zCQt/oWsHU= +github.com/aws/aws-sdk-go-v2/config v1.31.8/go.mod h1:QPpc7IgljrKwH0+E6/KolCgr4WPLerURiU592AYzfSY= +github.com/aws/aws-sdk-go-v2/credentials v1.18.12 h1:zmc9e1q90wMn8wQbjryy8IwA6Q4XlaL9Bx2zIqdNNbk= +github.com/aws/aws-sdk-go-v2/credentials v1.18.12/go.mod h1:3VzdRDR5u3sSJRI4kYcOSIBbeYsgtVk7dG5R/U6qLWY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 h1:Is2tPmieqGS2edBnmOJIbdvOA6Op+rRpaYR60iBAwXM= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7/go.mod h1:F1i5V5421EGci570yABvpIXgRIBPb5JM+lSkHF6Dq5w= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 h1:UCxq0X9O3xrlENdKf1r9eRJoKz/b0AfGkpp3a7FPlhg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7/go.mod h1:rHRoJUNUASj5Z/0eqI4w32vKvC7atoWR0jC+IkmVH8k= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 h1:Y6DTZUn7ZUC4th9FMBbo8LVE+1fyq3ofw+tRwkUd3PY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7/go.mod h1:x3XE6vMnU9QvHN/Wrx2s44kwzV2o2g5x/siw4ZUJ9g8= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 h1:mLgc5QIgOy26qyh5bvW+nDoAppxgn3J2WV3m9ewq7+8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7/go.mod h1:wXb/eQnqt8mDQIQTTmcw58B5mYGxzLGZGK8PWNFZ0BA= +github.com/aws/aws-sdk-go-v2/service/sqs v1.42.5 h1:HbaHWaTkGec2pMa/UQa3+WNWtUaFFF1ZLfwCeVFtBns= +github.com/aws/aws-sdk-go-v2/service/sqs v1.42.5/go.mod h1:wCAPjT7bNg5+4HSNefwNEC2hM3d+NSD5w5DU/8jrPrI= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 h1:7PKX3VYsZ8LUWceVRuv0+PU+E7OtQb1lgmi5vmUE9CM= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.3/go.mod h1:Ql6jE9kyyWI5JHn+61UT/Y5Z0oyVJGmgmJbZD5g4unY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 h1:e0XBRn3AptQotkyBFrHAxFB8mDhAIOfsG+7KyJ0dg98= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4/go.mod h1:XclEty74bsGBCr1s0VSaA11hQ4ZidK4viWK7rRfO88I= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 h1:PR00NXRYgY4FWHqOGx3fC3lhVKjsp1GdloDv2ynMSd8= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.4/go.mod h1:Z+Gd23v97pX9zK97+tX4ppAgqCt3Z2dIXB02CtBncK8= +github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= +github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -61,8 +61,8 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -72,8 +72,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/reedsolomon v1.12.0 h1:I5FEp3xSwVCcEh3F5A7dofEfhXdF/bWhQWPH+XwBFno= @@ -85,50 +85,50 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c= -github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= -github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= +github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc= +github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= -github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U= -github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg= -github.com/pion/ice/v4 v4.0.7 h1:mnwuT3n3RE/9va41/9QJqN5+Bhc0H/x/ZyiVlWMw35M= -github.com/pion/ice/v4 v4.0.7/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= -github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= -github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= -github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= -github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= +github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q= +github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= +github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= -github.com/pion/rtp v1.8.12 h1:nsKs8Wi0jQyBFHU3qmn/OvtZrhktVfJY0vRxwACsL5U= -github.com/pion/rtp v1.8.12/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4= -github.com/pion/sctp v1.8.37 h1:ZDmGPtRPX9mKCiVXtMbTWybFw3z/hVKAZgU81wcOrqs= -github.com/pion/sctp v1.8.37/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= -github.com/pion/sdp/v3 v3.0.11 h1:VhgVSopdsBKwhCFoyyPmT1fKMeV9nLMrEKxNOdy3IVI= -github.com/pion/sdp/v3 v3.0.11/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= -github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M= -github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ= +github.com/pion/rtp v1.8.21 h1:3yrOwmZFyUpcIosNcWRpQaU+UXIJ6yxLuJ8Bx0mw37Y= +github.com/pion/rtp v1.8.21/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= +github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo= +github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo= +github.com/pion/srtp/v3 v3.0.7 h1:QUElw0A/FUg3MP8/KNMZB3i0m8F9XeMnTum86F7S4bs= +github.com/pion/srtp/v3 v3.0.7/go.mod h1:qvnHeqbhT7kDdB+OGB05KA/P067G3mm7XBfLaLiaNF0= github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= -github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= -github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= -github.com/pion/webrtc/v4 v4.0.13 h1:XuUaWTjRufsiGJRC+G71OgiSMe7tl7mQ0kkd4bAqIaQ= -github.com/pion/webrtc/v4 v4.0.13/go.mod h1:Fadzxm0CbY99YdCEfxrgiVr0L4jN1l8bf8DBkPPpJbs= +github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc= +github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8= +github.com/pion/webrtc/v4 v4.1.4 h1:/gK1ACGHXQmtyVVbJFQDxNoODg4eSRiFLB7t9r9pg8M= +github.com/pion/webrtc/v4 v4.1.4/go.mod h1:Oab9npu1iZtQRMic3K3toYq5zFPvToe/QBw7dMI2ok4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= -github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= @@ -138,8 +138,6 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/realclientip/realclientip-go v1.0.0 h1:+yPxeC0mEaJzq1BfCt2h4BxlyrvIIBzR6suDc3BEF1U= github.com/realclientip/realclientip-go v1.0.0/go.mod h1:CXnUdVwFRcXFJIRb/dTYqbT7ud48+Pi2pFm80bxDmcI= -github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM= -github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= @@ -148,12 +146,8 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/templexxx/cpu v0.1.0 h1:wVM+WIJP2nYaxVxqgHPD4wGA2aJ9rvrQRV8CvFzNb40= -github.com/templexxx/cpu v0.1.0/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= -github.com/templexxx/xorsimd v0.4.2 h1:ocZZ+Nvu65LGHmCLZ7OoCtg8Fx8jnHKK37SjvngUoVI= -github.com/templexxx/xorsimd v0.4.2/go.mod h1:HgwaPoDREdi6OnULpSfxhzaiiSUY4Fi3JPn1wpt28NI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0= @@ -162,27 +156,29 @@ github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 h1:d/Wr/Vl/wiJHc github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= -github.com/xtaci/kcp-go/v5 v5.6.8 h1:jlI/0jAyjoOjT/SaGB58s4bQMJiNS41A2RKzR6TMWeI= -github.com/xtaci/kcp-go/v5 v5.6.8/go.mod h1:oE9j2NVqAkuKO5o8ByKGch3vgVX3BNf8zqP8JiGq0bM= +github.com/xtaci/kcp-go/v5 v5.6.24 h1:0tZL4NfpoESDrhaScrZfVDnYZ/3LhyVAbN/dQ2b4hbI= +github.com/xtaci/kcp-go/v5 v5.6.24/go.mod h1:7cAxNX/qFGeRUmUSnnDMoOg53FbXDK9IWBXAUfh+aBA= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= -github.com/xtaci/smux v1.5.34 h1:OUA9JaDFHJDT8ZT3ebwLWPAgEfE6sWo2LaTy3anXqwg= -github.com/xtaci/smux v1.5.34/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= +github.com/xtaci/smux v1.5.35 h1:RosihGJBeaS8gxOZ17HNxbhONwnqQwNwusHx4+SEGhk= +github.com/xtaci/smux v1.5.35/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +gitlab.torproject.org/shelikhoo/utls-temporary v0.0.0-20250428152032-7f32539913c8 h1:zZ1r9UjJ4qSPoLZG/vzITRsO0Qacpm20HlRAg7JVJ8Y= +gitlab.torproject.org/shelikhoo/utls-temporary v0.0.0-20250428152032-7f32539913c8/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= gitlab.torproject.org/tpo/anti-censorship/geoip v0.0.0-20210928150955-7ce4b3d98d01 h1:4949mHh9Vj2/okk48yG8nhP6TosFWOUfSfSr502sKGE= gitlab.torproject.org/tpo/anti-censorship/geoip v0.0.0-20210928150955-7ce4b3d98d01/go.mod h1:K3LOI4H8fa6j+7E10ViHeGEQV10304FG4j94ypmKLjY= gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/goptlib v1.6.0 h1:KD9m+mRBwtEdqe94Sv72uiedMWeRdIr4sXbrRyzRiIo= gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/goptlib v1.6.0/go.mod h1:70bhd4JKW/+1HLfm+TMrgHJsUHG4coelMWwiVEJ2gAg= -gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/ptutil v0.0.0-20250130151315-efaf4e0ec0d3 h1:pwWCiqrB6b3SynILsv3M+76utmcgMiTZ2aqfccjWmxo= -gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/ptutil v0.0.0-20250130151315-efaf4e0ec0d3/go.mod h1:PK7EvweKeypdelDyh1m7N922aldSeCAG8n0lJ7RAXWQ= +gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/ptutil v0.0.0-20250815012447-418f76dcf315 h1:9lmXguW9aH5sdZR5h5jOrdInCt0tQ9NRa7+wFD4MQBk= +gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/ptutil v0.0.0-20250815012447-418f76dcf315/go.mod h1:PK7EvweKeypdelDyh1m7N922aldSeCAG8n0lJ7RAXWQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -190,8 +186,8 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -203,8 +199,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -212,8 +208,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -226,8 +222,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -235,8 +231,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -247,8 +243,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -267,8 +263,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/probetest/Dockerfile b/probetest/Dockerfile index f73fbc1..63fdd44 100644 --- a/probetest/Dockerfile +++ b/probetest/Dockerfile @@ -1,3 +1,28 @@ -FROM golang:1.23 +FROM docker.io/library/golang:latest AS build -COPY probetest /go/bin + +ADD . /app + +WORKDIR /app/probetest +RUN go get +RUN CGO_ENABLED=0 go build -o probetest -ldflags '-extldflags "-static" -w -s' . + +FROM containers.torproject.org/tpo/tpa/base-images/debian:bookworm as debian-base + +RUN apt-get update && apt-get install -y \ + curl \ + gpg \ + gpg-agent \ + ca-certificates \ + libcap2-bin \ + --no-install-recommends + +FROM scratch + +COPY --from=debian-base /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +COPY --from=debian-base /usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=build /app/probetest/probetest /bin/probetest + +ENTRYPOINT [ "/bin/probetest" ] + +LABEL org.opencontainers.image.authors="anti-censorship-team@lists.torproject.org" diff --git a/probetest/README.md b/probetest/README.md index 44c7837..41451a9 100644 --- a/probetest/README.md +++ b/probetest/README.md @@ -24,6 +24,13 @@ but you should use TLS in production. To build the probe server, run ```go build``` +Or alternatively: + +``` +cd .. # switch to the repo root directory or $(git rev-parse --show-toplevel) +docker build -t snowflake-probetest -f probetest/Dockerfile . +``` + To deploy the probe server, first set the necessary env variables with ``` export HOSTNAMES=${YOUR HOSTNAMES}