2021-02-02 12:05:47 +01:00
|
|
|
// 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"
|
2022-01-03 13:53:23 +01:00
|
|
|
"github.com/ooni/probe-cli/v3/internal/model"
|
2021-09-28 12:42:01 +02:00
|
|
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
2021-02-02 12:05:47 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
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
|
2021-09-28 12:42:01 +02:00
|
|
|
if v.TestKeys.FailedOperation != nil && *v.TestKeys.FailedOperation == netxlite.ResolveOperation {
|
2021-02-02 12:05:47 +01:00
|
|
|
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
|
2021-09-28 12:42:01 +02:00
|
|
|
if v.TestKeys.FailedOperation != nil && *v.TestKeys.FailedOperation == netxlite.ConnectOperation {
|
2021-02-02 12:05:47 +01:00
|
|
|
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.
|
|
|
|
//
|
2022-05-09 09:33:18 +02:00
|
|
|
// Note that this structure is part of the ABI contract with ooniprobe
|
2021-02-02 12:05:47 +01:00
|
|
|
// 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
|
|
|
|
}
|