/
stunreachability.go
184 lines (165 loc) · 5.06 KB
/
stunreachability.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
// Package stunreachability contains the STUN reachability experiment.
//
// See https://github.com/ooni/spec/blob/master/nettests/ts-025-stun-reachability.md.
package stunreachability
import (
"context"
"errors"
"net"
"net/url"
"time"
"github.com/ooni/probe-engine/pkg/legacy/netx"
"github.com/ooni/probe-engine/pkg/legacy/tracex"
"github.com/ooni/probe-engine/pkg/model"
"github.com/ooni/probe-engine/pkg/netxlite"
"github.com/pion/stun"
)
const (
testName = "stunreachability"
testVersion = "0.4.0"
)
// Config contains the experiment config.
type Config struct {
dialContext func(ctx context.Context, network, address string) (net.Conn, error)
newClient func(conn stun.Connection, options ...stun.ClientOption) (*stun.Client, error)
}
// TestKeys contains the experiment's result.
type TestKeys struct {
Endpoint string `json:"endpoint"`
Failure *string `json:"failure"`
NetworkEvents []tracex.NetworkEvent `json:"network_events"`
Queries []tracex.DNSQueryEntry `json:"queries"`
}
func registerExtensions(m *model.Measurement) {
tracex.ExtDNS.AddTo(m)
tracex.ExtNetevents.AddTo(m)
}
// Measurer performs the measurement.
type Measurer struct {
config Config
}
// ExperimentName implements ExperimentMeasurer.ExperiExperimentName.
func (m *Measurer) ExperimentName() string {
return testName
}
// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion.
func (m *Measurer) ExperimentVersion() string {
return testVersion
}
func wrap(err error) error {
if err != nil {
return netxlite.NewTopLevelGenericErrWrapper(err)
}
return nil
}
// errStunMissingInput means that the user did not provide any input
var errStunMissingInput = errors.New("stun: missing input")
// errStunMissingPortInURL means the URL is missing the port
var errStunMissingPortInURL = errors.New("stun: missing port in URL")
// errUnsupportedURLScheme means we don't support the URL scheme
var errUnsupportedURLScheme = errors.New("stun: unsupported URL scheme")
// Run implements ExperimentMeasurer.Run.
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error {
callbacks := args.Callbacks
measurement := args.Measurement
sess := args.Session
tk := new(TestKeys)
measurement.TestKeys = tk
registerExtensions(measurement)
input := string(measurement.Input)
if input == "" {
return errStunMissingInput
}
URL, err := url.Parse(input)
if err != nil {
return err
}
if URL.Port() == "" {
return errStunMissingPortInURL
}
if URL.Scheme != "stun" {
return errUnsupportedURLScheme
}
if err := wrap(tk.run(ctx, m.config, sess, measurement, callbacks, URL.Host)); err != nil {
s := err.Error()
tk.Failure = &s
return nil // we want to submit this measurement
}
return nil
}
func (tk *TestKeys) run(
ctx context.Context, config Config, sess model.ExperimentSession,
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
endpoint string,
) error {
tk.Endpoint = endpoint
saver := new(tracex.Saver)
begin := time.Now()
err := tk.do(ctx, config, netx.NewDialer(netx.Config{
ContextByteCounting: true,
Logger: sess.Logger(),
ReadWriteSaver: saver,
Saver: saver,
}), endpoint)
sess.Logger().Infof("stunreachability: measuring: %s... %s", endpoint, model.ErrorToStringOrOK(err))
events := saver.Read()
tk.NetworkEvents = append(
tk.NetworkEvents, tracex.NewNetworkEventsList(begin, events)...,
)
tk.Queries = append(
tk.Queries, tracex.NewDNSQueriesList(begin, events)...,
)
return err
}
func (tk *TestKeys) do(
ctx context.Context, config Config, dialer model.Dialer, endpoint string) error {
dialContext := dialer.DialContext
if config.dialContext != nil {
dialContext = config.dialContext
}
conn, err := dialContext(ctx, "udp", endpoint)
if err != nil {
return err
}
newClient := stun.NewClient
if config.newClient != nil {
newClient = config.newClient
}
client, err := newClient(conn)
if err != nil {
return err
}
defer client.Close()
message := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
ch := make(chan error)
err = client.Start(message, func(ev stun.Event) {
// As mentioned below this code will run after Start has returned.
if ev.Error != nil {
ch <- ev.Error
return
}
var xorAddr stun.XORMappedAddress
ch <- xorAddr.GetFrom(ev.Message)
})
// Implementation note: if we successfully started, then the callback
// will be called when we receive a response or fail.
if err != nil {
return err
}
return <-ch
}
// 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 {
IsAnomaly bool `json:"-"`
}
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
func (m Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
return SummaryKeys{IsAnomaly: false}, nil
}