Skip to content

Commit add5793

Browse files
bassosimonekelmenhorst
authored andcommitted
refactor(telegram): rewrite tests using netemx (#1183)
This diff rewrites telegram tests using netemx. This is part of ooni/probe#2461. --------- Co-authored-by: kelmenhorst <[email protected]>
1 parent 1ee32dd commit add5793

File tree

1 file changed

+308
-61
lines changed

1 file changed

+308
-61
lines changed

internal/experiment/telegram/telegram_test.go

Lines changed: 308 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,21 @@ import (
44
"context"
55
"fmt"
66
"io"
7+
"net"
8+
"net/http"
9+
"sync"
710
"testing"
811

12+
"github.com/apex/log"
13+
"github.com/google/gopacket/layers"
14+
"github.com/ooni/netem"
915
"github.com/ooni/probe-cli/v3/internal/experiment/telegram"
1016
"github.com/ooni/probe-cli/v3/internal/experiment/urlgetter"
11-
"github.com/ooni/probe-cli/v3/internal/legacy/mockable"
17+
"github.com/ooni/probe-cli/v3/internal/mocks"
1218
"github.com/ooni/probe-cli/v3/internal/model"
19+
"github.com/ooni/probe-cli/v3/internal/netemx"
1320
"github.com/ooni/probe-cli/v3/internal/netxlite"
21+
"github.com/ooni/probe-cli/v3/internal/runtimex"
1422
)
1523

1624
func TestNewExperimentMeasurer(t *testing.T) {
@@ -23,66 +31,6 @@ func TestNewExperimentMeasurer(t *testing.T) {
2331
}
2432
}
2533

26-
func TestGood(t *testing.T) {
27-
measurer := telegram.NewExperimentMeasurer(telegram.Config{})
28-
measurement := new(model.Measurement)
29-
args := &model.ExperimentArgs{
30-
Callbacks: model.NewPrinterCallbacks(model.DiscardLogger),
31-
Measurement: measurement,
32-
Session: &mockable.Session{
33-
MockableLogger: model.DiscardLogger,
34-
},
35-
}
36-
err := measurer.Run(context.Background(), args)
37-
if err != nil {
38-
t.Fatal(err)
39-
}
40-
tk := measurement.TestKeys.(*telegram.TestKeys)
41-
if tk.Agent != "redirect" {
42-
t.Fatal("unexpected Agent")
43-
}
44-
if tk.FailedOperation != nil {
45-
t.Fatal("unexpected FailedOperation")
46-
}
47-
if tk.Failure != nil {
48-
t.Fatal("unexpected Failure")
49-
}
50-
if len(tk.NetworkEvents) <= 0 {
51-
t.Fatal("no NetworkEvents?!")
52-
}
53-
if len(tk.Queries) <= 0 {
54-
t.Fatal("no Queries?!")
55-
}
56-
if len(tk.Requests) <= 0 {
57-
t.Fatal("no Requests?!")
58-
}
59-
if len(tk.TCPConnect) <= 0 {
60-
t.Fatal("no TCPConnect?!")
61-
}
62-
if len(tk.TLSHandshakes) <= 0 {
63-
t.Fatal("no TLSHandshakes?!")
64-
}
65-
if tk.TelegramHTTPBlocking != false {
66-
t.Fatal("unexpected TelegramHTTPBlocking")
67-
}
68-
if tk.TelegramTCPBlocking != false {
69-
t.Fatal("unexpected TelegramTCPBlocking")
70-
}
71-
if tk.TelegramWebFailure != nil {
72-
t.Fatal("unexpected TelegramWebFailure")
73-
}
74-
if tk.TelegramWebStatus != "ok" {
75-
t.Fatal("unexpected TelegramWebStatus")
76-
}
77-
sk, err := measurer.GetSummaryKeys(measurement)
78-
if err != nil {
79-
t.Fatal(err)
80-
}
81-
if _, ok := sk.(telegram.SummaryKeys); !ok {
82-
t.Fatal("invalid type for summary keys")
83-
}
84-
}
85-
8634
func TestUpdateWithNoAccessPointsBlocking(t *testing.T) {
8735
tk := telegram.NewTestKeys()
8836
tk.Update(urlgetter.MultiOutput{
@@ -317,3 +265,302 @@ func TestSummaryKeysWorksAsIntended(t *testing.T) {
317265
})
318266
}
319267
}
268+
269+
// telegramWebAddr is the web.telegram.org IP address as of 2023-07-11
270+
const telegramWebAddr = "149.154.167.99"
271+
272+
// configureDNSWithAddr configures the given DNS config for web.telegram.org using the given addr.
273+
func configureDNSWithAddr(config *netem.DNSConfig, addr string) {
274+
config.AddRecord("web.telegram.org", "web.telegram.org", addr)
275+
}
276+
277+
// configureDNSWithDefaults configures the given DNS config for web.telegram.org using the default addr.
278+
func configureDNSWithDefaults(config *netem.DNSConfig) {
279+
configureDNSWithAddr(config, telegramWebAddr)
280+
}
281+
282+
// telegramHTTPServerNetStackHandler is a [netemx.QAEnvNetStackHandler] that serves HTTP requests
283+
// on the given addr and ports 443 and 80 as required by the telegram nettest
284+
type telegramHTTPServerNetStackHandler struct {
285+
closers []io.Closer
286+
mu sync.Mutex
287+
}
288+
289+
var _ netemx.QAEnvNetStackHandler = &telegramHTTPServerNetStackHandler{}
290+
291+
// Close implements netemx.QAEnvNetStackHandler.
292+
func (nsh *telegramHTTPServerNetStackHandler) Close() error {
293+
// make the method locked as requested by the documentation
294+
defer nsh.mu.Unlock()
295+
nsh.mu.Lock()
296+
297+
// close each of the closers
298+
for _, closer := range nsh.closers {
299+
_ = closer.Close()
300+
}
301+
302+
// be idempotent
303+
nsh.closers = []io.Closer{}
304+
return nil
305+
}
306+
307+
// Listen implements netemx.QAEnvNetStackHandler.
308+
func (nsh *telegramHTTPServerNetStackHandler) Listen(stack *netem.UNetStack) error {
309+
// make the method locked as requested by the documentation
310+
defer nsh.mu.Unlock()
311+
nsh.mu.Lock()
312+
313+
// we create an empty mux, which should cause a 404 for each webpage, which seems what
314+
// the servers used by telegram DC do as of 2023-07-11
315+
mux := http.NewServeMux()
316+
317+
// listen on port 80
318+
nsh.listenPort(stack, mux, 80)
319+
320+
// listen on port 443
321+
nsh.listenPort(stack, mux, 443)
322+
return nil
323+
}
324+
325+
func (nsh *telegramHTTPServerNetStackHandler) listenPort(stack *netem.UNetStack, mux *http.ServeMux, port uint16) {
326+
// create the listening address
327+
ipAddr := net.ParseIP(stack.IPAddress())
328+
runtimex.Assert(ipAddr != nil, "expected valid IP address")
329+
addr := &net.TCPAddr{IP: ipAddr, Port: int(port)}
330+
listener := runtimex.Try1(stack.ListenTCP("tcp", addr))
331+
srvr := &http.Server{Handler: mux}
332+
333+
// serve requests in a background goroutine
334+
go srvr.Serve(listener)
335+
336+
// make sure we track the server (the .Serve method will close the
337+
// listener once we close the server itself)
338+
nsh.closers = append(nsh.closers, srvr)
339+
}
340+
341+
// newQAEnvironment creates a QA environment for testing using the given addresses.
342+
func newQAEnvironment(ipaddrs ...string) *netemx.QAEnv {
343+
// create a single handler for handling all the requests
344+
handler := &telegramHTTPServerNetStackHandler{
345+
closers: []io.Closer{},
346+
mu: sync.Mutex{},
347+
}
348+
349+
// create the options for constructing the env
350+
var options []netemx.QAEnvOption
351+
for _, ipaddr := range ipaddrs {
352+
options = append(options, netemx.QAEnvOptionNetStack(ipaddr, handler))
353+
}
354+
355+
// add explicit logging which helps to inspect the tests results
356+
options = append(options, netemx.QAEnvOptionLogger(log.Log))
357+
358+
// add handler for telegram web (we're using a different-from-reality HTTP handler
359+
// but we're not testing for the returned webpage, so we should be fine)
360+
options = append(options, netemx.QAEnvOptionHTTPServer(telegramWebAddr, netemx.QAEnvDefaultHTTPHandler()))
361+
362+
// create the environment proper with all the options
363+
env := netemx.NewQAEnv(options...)
364+
365+
// register with all the possible resolvers the correct DNS records - registering again
366+
// inside individual tests will override the values we're setting here
367+
configureDNSWithDefaults(env.ISPResolverConfig())
368+
configureDNSWithDefaults(env.OtherResolversConfig())
369+
return env
370+
}
371+
372+
func TestMeasurerRun(t *testing.T) {
373+
t.Run("without DPI: expect success", func(t *testing.T) {
374+
// create a new test environment
375+
env := newQAEnvironment(telegram.DatacenterIPAddrs...)
376+
defer env.Close()
377+
378+
env.Do(func() {
379+
measurer := telegram.NewExperimentMeasurer(telegram.Config{})
380+
measurement := &model.Measurement{}
381+
args := &model.ExperimentArgs{
382+
Callbacks: model.NewPrinterCallbacks(log.Log),
383+
Measurement: measurement,
384+
Session: &mocks.Session{MockLogger: func() model.Logger { return log.Log }},
385+
}
386+
err := measurer.Run(context.Background(), args)
387+
if err != nil {
388+
t.Fatalf("Unexpected error: %s", err)
389+
}
390+
tk, _ := (measurement.TestKeys).(*telegram.TestKeys)
391+
if tk.TelegramWebFailure != nil {
392+
t.Fatalf("Unexpected Telegram Web failure %s", *tk.TelegramWebFailure)
393+
}
394+
if tk.TelegramHTTPBlocking {
395+
t.Fatalf("Unexpected HTTP blocking")
396+
}
397+
if tk.TelegramTCPBlocking {
398+
t.Fatal("Unexpected TCP blocking")
399+
}
400+
if tk.Agent != "redirect" {
401+
t.Fatal("unexpected Agent")
402+
}
403+
if tk.FailedOperation != nil {
404+
t.Fatal("unexpected FailedOperation")
405+
}
406+
if tk.Failure != nil {
407+
t.Fatal("unexpected Failure")
408+
}
409+
if len(tk.NetworkEvents) <= 0 {
410+
t.Fatal("no NetworkEvents?!")
411+
}
412+
if len(tk.Queries) <= 0 {
413+
t.Fatal("no Queries?!")
414+
}
415+
if len(tk.Requests) <= 0 {
416+
t.Fatal("no Requests?!")
417+
}
418+
if len(tk.TCPConnect) <= 0 {
419+
t.Fatal("no TCPConnect?!")
420+
}
421+
if len(tk.TLSHandshakes) <= 0 {
422+
t.Fatal("no TLSHandshakes?!")
423+
}
424+
if tk.TelegramWebStatus != "ok" {
425+
t.Fatal("unexpected TelegramWebStatus")
426+
}
427+
sk, err := measurer.GetSummaryKeys(measurement)
428+
if err != nil {
429+
t.Fatal(err)
430+
}
431+
if _, ok := sk.(telegram.SummaryKeys); !ok {
432+
t.Fatal("invalid type for summary keys")
433+
}
434+
})
435+
})
436+
437+
t.Run("with poisoned DNS: expect TelegramWebFailure", func(t *testing.T) {
438+
// create a new test environment
439+
env := newQAEnvironment(telegram.DatacenterIPAddrs...)
440+
defer env.Close()
441+
442+
// register bogon entries for web.telegram.org in the resolver's ISP
443+
env.ISPResolverConfig().AddRecord("web.telegram.org", "web.telegram.org", "10.10.34.35")
444+
445+
env.Do(func() {
446+
measurer := telegram.NewExperimentMeasurer(telegram.Config{})
447+
measurement := &model.Measurement{}
448+
args := &model.ExperimentArgs{
449+
Callbacks: model.NewPrinterCallbacks(log.Log),
450+
Measurement: measurement,
451+
Session: &mocks.Session{MockLogger: func() model.Logger { return log.Log }},
452+
}
453+
err := measurer.Run(context.Background(), args)
454+
if err != nil {
455+
t.Fatalf("Unexpected error: %s", err)
456+
}
457+
tk, _ := (measurement.TestKeys).(*telegram.TestKeys)
458+
if tk.TelegramWebFailure == nil {
459+
t.Fatalf("Expected Web Failure but got none")
460+
}
461+
if tk.TelegramHTTPBlocking {
462+
t.Fatal("Unexpected HTTP blocking")
463+
}
464+
if tk.TelegramTCPBlocking {
465+
t.Fatal("Unexpected TCP blocking")
466+
}
467+
})
468+
})
469+
470+
t.Run("with DPI that drops TCP traffic towards telegram endpoint: expect Telegram(HTTP|TCP)Blocking", func(t *testing.T) {
471+
if testing.Short() {
472+
t.Skip("skip test in short mode")
473+
}
474+
475+
// overwrite global Datacenters, otherwise the test times out because there are too many endpoints
476+
orig := telegram.DatacenterIPAddrs
477+
telegram.DatacenterIPAddrs = []string{
478+
"149.154.175.50",
479+
}
480+
defer func() {
481+
telegram.DatacenterIPAddrs = orig
482+
}()
483+
484+
// create a new test environment
485+
env := newQAEnvironment(telegram.DatacenterIPAddrs...)
486+
defer env.Close()
487+
488+
// add DPI engine to emulate the censorship condition
489+
dpi := env.DPIEngine()
490+
for _, dc := range telegram.DatacenterIPAddrs {
491+
dpi.AddRule(&netem.DPIDropTrafficForServerEndpoint{
492+
Logger: log.Log,
493+
ServerIPAddress: dc,
494+
ServerPort: 80,
495+
ServerProtocol: layers.IPProtocolTCP,
496+
})
497+
dpi.AddRule(&netem.DPIDropTrafficForServerEndpoint{
498+
Logger: log.Log,
499+
ServerIPAddress: dc,
500+
ServerPort: 443,
501+
ServerProtocol: layers.IPProtocolTCP,
502+
})
503+
}
504+
505+
env.Do(func() {
506+
measurer := telegram.NewExperimentMeasurer(telegram.Config{})
507+
measurement := &model.Measurement{}
508+
args := &model.ExperimentArgs{
509+
Callbacks: model.NewPrinterCallbacks(log.Log),
510+
Measurement: measurement,
511+
Session: &mocks.Session{MockLogger: func() model.Logger { return log.Log }},
512+
}
513+
err := measurer.Run(context.Background(), args)
514+
if err != nil {
515+
t.Fatalf("Unexpected error: %s", err)
516+
}
517+
tk, _ := (measurement.TestKeys).(*telegram.TestKeys)
518+
if tk.TelegramWebFailure != nil {
519+
t.Fatalf("Unexpected Telegram Web failure %s", *tk.TelegramWebFailure)
520+
}
521+
if !tk.TelegramHTTPBlocking {
522+
t.Fatal("Expected HTTP blocking but got none")
523+
}
524+
if !tk.TelegramTCPBlocking {
525+
t.Fatal("Expected TCP blocking but got none")
526+
}
527+
})
528+
})
529+
530+
t.Run("with DPI that drops TLS traffic with SNI = web.telegram.org: expect TelegramWebFailure", func(t *testing.T) {
531+
// create a new test environment
532+
env := newQAEnvironment(telegram.DatacenterIPAddrs...)
533+
defer env.Close()
534+
535+
// add DPI engine to emulate the censorship condition
536+
dpi := env.DPIEngine()
537+
dpi.AddRule(&netem.DPIResetTrafficForTLSSNI{
538+
Logger: log.Log,
539+
SNI: "web.telegram.org",
540+
})
541+
542+
env.Do(func() {
543+
measurer := telegram.NewExperimentMeasurer(telegram.Config{})
544+
measurement := &model.Measurement{}
545+
args := &model.ExperimentArgs{
546+
Callbacks: model.NewPrinterCallbacks(log.Log),
547+
Measurement: measurement,
548+
Session: &mocks.Session{MockLogger: func() model.Logger { return log.Log }},
549+
}
550+
err := measurer.Run(context.Background(), args)
551+
if err != nil {
552+
t.Fatalf("Unexpected error: %s", err)
553+
}
554+
tk, _ := (measurement.TestKeys).(*telegram.TestKeys)
555+
if tk.TelegramWebFailure == nil {
556+
t.Fatalf("Expected Web Failure but got none")
557+
}
558+
if tk.TelegramHTTPBlocking {
559+
t.Fatal("Unexpected HTTP blocking")
560+
}
561+
if tk.TelegramTCPBlocking {
562+
t.Fatal("Unexpected TCP blocking")
563+
}
564+
})
565+
})
566+
}

0 commit comments

Comments
 (0)