ooni-probe-cli/internal/engine/experiment/fbmessenger/fbmessenger.go

228 lines
7.8 KiB
Go
Raw Permalink Normal View History

// Package fbmessenger contains the Facebook Messenger network experiment.
//
// See https://github.com/ooni/spec/blob/master/nettests/ts-019-facebook-messenger.md
package fbmessenger
import (
"context"
"errors"
"math/rand"
"time"
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netxlite"
)
const (
// FacebookASN is Facebook's ASN
FacebookASN = 32934
// ServiceSTUN is the STUN service
ServiceSTUN = "dnslookup://stun.fbsbx.com"
// ServiceBAPI is the b-api service
ServiceBAPI = "tcpconnect://b-api.facebook.com:443"
// ServiceBGraph is the b-graph service
ServiceBGraph = "tcpconnect://b-graph.facebook.com:443"
// ServiceEdge is the edge service
ServiceEdge = "tcpconnect://edge-mqtt.facebook.com:443"
// ServiceExternalCDN is the external CDN service
ServiceExternalCDN = "tcpconnect://external.xx.fbcdn.net:443"
// ServiceScontentCDN is the scontent CDN service
ServiceScontentCDN = "tcpconnect://scontent.xx.fbcdn.net:443"
// ServiceStar is the star service
ServiceStar = "tcpconnect://star.c10r.facebook.com:443"
testName = "facebook_messenger"
testVersion = "0.2.0"
)
// Config contains the experiment config.
type Config struct{}
// TestKeys contains the experiment results
type TestKeys struct {
urlgetter.TestKeys
FacebookBAPIDNSConsistent *bool `json:"facebook_b_api_dns_consistent"`
FacebookBAPIReachable *bool `json:"facebook_b_api_reachable"`
FacebookBGraphDNSConsistent *bool `json:"facebook_b_graph_dns_consistent"`
FacebookBGraphReachable *bool `json:"facebook_b_graph_reachable"`
FacebookEdgeDNSConsistent *bool `json:"facebook_edge_dns_consistent"`
FacebookEdgeReachable *bool `json:"facebook_edge_reachable"`
FacebookExternalCDNDNSConsistent *bool `json:"facebook_external_cdn_dns_consistent"`
FacebookExternalCDNReachable *bool `json:"facebook_external_cdn_reachable"`
FacebookScontentCDNDNSConsistent *bool `json:"facebook_scontent_cdn_dns_consistent"`
FacebookScontentCDNReachable *bool `json:"facebook_scontent_cdn_reachable"`
FacebookStarDNSConsistent *bool `json:"facebook_star_dns_consistent"`
FacebookStarReachable *bool `json:"facebook_star_reachable"`
FacebookSTUNDNSConsistent *bool `json:"facebook_stun_dns_consistent"`
FacebookSTUNReachable *bool `json:"facebook_stun_reachable"`
FacebookDNSBlocking *bool `json:"facebook_dns_blocking"`
FacebookTCPBlocking *bool `json:"facebook_tcp_blocking"`
}
// Update updates the TestKeys using the given MultiOutput result.
func (tk *TestKeys) Update(v urlgetter.MultiOutput) {
// Update the easy to update entries first
tk.NetworkEvents = append(tk.NetworkEvents, v.TestKeys.NetworkEvents...)
tk.Queries = append(tk.Queries, v.TestKeys.Queries...)
tk.Requests = append(tk.Requests, v.TestKeys.Requests...)
tk.TCPConnect = append(tk.TCPConnect, v.TestKeys.TCPConnect...)
tk.TLSHandshakes = append(tk.TLSHandshakes, v.TestKeys.TLSHandshakes...)
// Set the status of endpoints
switch v.Input.Target {
case ServiceSTUN:
var ignored *bool
tk.ComputeEndpointStatus(v, &tk.FacebookSTUNDNSConsistent, &ignored)
case ServiceBAPI:
tk.ComputeEndpointStatus(
v, &tk.FacebookBAPIDNSConsistent, &tk.FacebookBAPIReachable)
case ServiceBGraph:
tk.ComputeEndpointStatus(
v, &tk.FacebookBGraphDNSConsistent, &tk.FacebookBGraphReachable)
case ServiceEdge:
tk.ComputeEndpointStatus(
v, &tk.FacebookEdgeDNSConsistent, &tk.FacebookEdgeReachable)
case ServiceExternalCDN:
tk.ComputeEndpointStatus(
v, &tk.FacebookExternalCDNDNSConsistent, &tk.FacebookExternalCDNReachable)
case ServiceScontentCDN:
tk.ComputeEndpointStatus(
v, &tk.FacebookScontentCDNDNSConsistent, &tk.FacebookScontentCDNReachable)
case ServiceStar:
tk.ComputeEndpointStatus(
v, &tk.FacebookStarDNSConsistent, &tk.FacebookStarReachable)
}
}
var (
trueValue = true
falseValue = false
)
// ComputeEndpointStatus computes the DNS and TCP status of a specific endpoint.
func (tk *TestKeys) ComputeEndpointStatus(v urlgetter.MultiOutput, dns, tcp **bool) {
// start where all is unknown
*dns, *tcp = nil, nil
// process DNS first
if v.TestKeys.FailedOperation != nil && *v.TestKeys.FailedOperation == netxlite.ResolveOperation {
tk.FacebookDNSBlocking = &trueValue
*dns = &falseValue
return // we know that the DNS has failed
}
for _, query := range v.TestKeys.Queries {
for _, ans := range query.Answers {
if ans.ASN != FacebookASN {
tk.FacebookDNSBlocking = &trueValue
*dns = &falseValue
return // because DNS is lying
}
}
}
*dns = &trueValue
// now process connect
if v.TestKeys.FailedOperation != nil && *v.TestKeys.FailedOperation == netxlite.ConnectOperation {
tk.FacebookTCPBlocking = &trueValue
*tcp = &falseValue
return // because connect failed
}
// all good
*tcp = &trueValue
}
// Measurer performs the measurement
type Measurer struct {
// Config contains the experiment settings. If empty we
// will be using default settings.
Config Config
// Getter is an optional getter to be used for testing.
Getter urlgetter.MultiGetter
}
// ExperimentName implements ExperimentMeasurer.ExperimentName
func (m Measurer) ExperimentName() string {
return testName
}
// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion
func (m Measurer) ExperimentVersion() string {
return testVersion
}
// Run implements ExperimentMeasurer.Run
func (m Measurer) Run(
ctx context.Context, sess model.ExperimentSession,
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
) error {
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel()
urlgetter.RegisterExtensions(measurement)
// generate targets
services := []string{
ServiceSTUN, ServiceBAPI, ServiceBGraph, ServiceEdge, ServiceExternalCDN,
ServiceScontentCDN, ServiceStar,
}
var inputs []urlgetter.MultiInput
for _, service := range services {
inputs = append(inputs, urlgetter.MultiInput{Target: service})
}
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
rnd.Shuffle(len(inputs), func(i, j int) {
inputs[i], inputs[j] = inputs[j], inputs[i]
})
// measure in parallel
multi := urlgetter.Multi{Begin: time.Now(), Getter: m.Getter, Session: sess}
testkeys := new(TestKeys)
testkeys.Agent = "redirect"
measurement.TestKeys = testkeys
for entry := range multi.Collect(ctx, inputs, "facebook_messenger", callbacks) {
testkeys.Update(entry)
}
// if we haven't yet determined the status of DNS blocking and TCP blocking
// then no blocking has been detected and we can set them
if testkeys.FacebookDNSBlocking == nil {
testkeys.FacebookDNSBlocking = &falseValue
}
if testkeys.FacebookTCPBlocking == nil {
testkeys.FacebookTCPBlocking = &falseValue
}
return nil
}
// NewExperimentMeasurer creates a new ExperimentMeasurer.
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
return Measurer{Config: config}
}
// SummaryKeys contains summary keys for this experiment.
//
// Note that this structure is part of the ABI contract with ooniprobe
// therefore we should be careful when changing it.
type SummaryKeys struct {
DNSBlocking bool `json:"facebook_dns_blocking"`
TCPBlocking bool `json:"facebook_tcp_blocking"`
IsAnomaly bool `json:"-"`
}
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
func (m Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
sk := SummaryKeys{IsAnomaly: false}
tk, ok := measurement.TestKeys.(*TestKeys)
if !ok {
return sk, errors.New("invalid test keys type")
}
dnsBlocking := tk.FacebookDNSBlocking != nil && *tk.FacebookDNSBlocking
tcpBlocking := tk.FacebookTCPBlocking != nil && *tk.FacebookTCPBlocking
sk.DNSBlocking = dnsBlocking
sk.TCPBlocking = tcpBlocking
sk.IsAnomaly = dnsBlocking || tcpBlocking
return sk, nil
}