-
Notifications
You must be signed in to change notification settings - Fork 54
/
Copy pathqaenv.go
298 lines (251 loc) · 9.79 KB
/
qaenv.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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
package netemx
//
// QA environment
//
import (
"fmt"
"io"
"net"
"sync"
"sync/atomic"
"time"
"github.com/ooni/netem"
"github.com/ooni/probe-cli/v3/internal/logx"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/runtimex"
)
// qaEnvConfig is the private configuration for [MustNewQAEnv].
type qaEnvConfig struct {
// clientAddress is the client IP address to use.
clientAddress string
// clientNICWrapper is the OPTIONAL wrapper for the client NIC.
clientNICWrapper netem.LinkNICWrapper
// ispResolver is the ISP resolver to use.
ispResolver string
// logger is the logger to use.
logger model.Logger
// netStacks contains information about the net stacks to create.
netStacks map[string][]NetStackServerFactory
// rootResolver is the root resolver address to use.
rootResolver string
}
// QAEnvOption is an option to modify [NewQAEnv] default behavior.
type QAEnvOption func(config *qaEnvConfig)
// QAEnvOptionClientAddress sets the client IP address. If you do not set this option
// we will use [DefaultClientAddress].
func QAEnvOptionClientAddress(ipAddr string) QAEnvOption {
runtimex.Assert(net.ParseIP(ipAddr) != nil, "not an IP addr")
return func(config *qaEnvConfig) {
config.clientAddress = ipAddr
}
}
// QAEnvOptionClientNICWrapper sets the NIC wrapper for the client. The most common use case
// for this functionality is capturing packets using [netem.NewPCAPDumper].
func QAEnvOptionClientNICWrapper(wrapper netem.LinkNICWrapper) QAEnvOption {
return func(config *qaEnvConfig) {
config.clientNICWrapper = wrapper
}
}
// QAEnvOptionLogger sets the logger to use. If you do not set this option we
// will use [model.DiscardLogger] as the logger.
func QAEnvOptionLogger(logger model.Logger) QAEnvOption {
return func(config *qaEnvConfig) {
config.logger = logger
}
}
// QAEnvOptionNetStack creates an userspace network stack with the given IP address and binds it
// to the given factory, which will be responsible to create listening sockets and closing them
// when we're done running. Examples of factories you can use with this method are:
//
// - [NewTCPEchoServerFactory];
//
// - [HTTPCleartextServerFactory];
//
// - [HTTPSecureServerFactory];
//
// - [HTTP3ServerFactory];
//
// - [UDPResolverFactory].
//
// Calling this method multiple times is equivalent to calling this method once with several
// factories. This would work as long as you do not specify the same port multiple times, otherwise
// the second bind attempt for an already bound port would fail.
//
// This function PANICS if you try to configure [ISPResolverAddress] or [RootResolverAddress]
// because these two addresses are already configured by [MustNewQAEnv].
func QAEnvOptionNetStack(ipAddr string, factories ...NetStackServerFactory) QAEnvOption {
runtimex.Assert(
ipAddr != ISPResolverAddress && ipAddr != RootResolverAddress,
"QAEnvOptionNetStack: cannot configure RootResolverAddress or ISPResolverAddress",
)
return qaEnvOptionNetStack(ipAddr, factories...)
}
func qaEnvOptionNetStack(ipAddr string, factories ...NetStackServerFactory) QAEnvOption {
return func(config *qaEnvConfig) {
config.netStacks[ipAddr] = append(config.netStacks[ipAddr], factories...)
}
}
// QAEnv is the environment for running QA tests using [github.com/ooni/netem]. The zero
// value of this struct is invalid; please, use [NewQAEnv].
type QAEnv struct {
// baseLogger is the base [model.Logger] to use.
baseLogger model.Logger
// clientNICWrapper is the OPTIONAL wrapper for the client NIC.
clientNICWrapper netem.LinkNICWrapper
// ClientStack is the client stack to use.
ClientStack *netem.UNetStack
// closables contains all entities where we have to take care of closing.
closables []io.Closer
// emulateAndroidGetaddrinfo controls whether to emulate the behavior of our wrapper for
// the android implementation of getaddrinfo returning android_dns_cache_no_data
emulateAndroidGetaddrinfo *atomic.Bool
// ispResolverConfig is the DNS config used by the ISP resolver.
ispResolverConfig *netem.DNSConfig
// dpi refers to the [netem.DPIEngine] we're using.
dpi *netem.DPIEngine
// once ensures Close has "once" semantics.
once sync.Once
// otherResolversConfig is the DNS config used by non-ISP resolvers.
otherResolversConfig *netem.DNSConfig
// topology is the topology we're using.
topology *netem.StarTopology
}
// MustNewQAEnv creates a new [QAEnv]. This function PANICs on failure.
func MustNewQAEnv(options ...QAEnvOption) *QAEnv {
// initialize the configuration
config := &qaEnvConfig{
clientAddress: DefaultClientAddress,
clientNICWrapper: nil,
ispResolver: ISPResolverAddress,
logger: model.DiscardLogger,
rootResolver: RootResolverAddress,
netStacks: map[string][]NetStackServerFactory{},
}
for _, option := range options {
option(config)
}
// make sure we're going to create the ISP's DNS resolver.
qaEnvOptionNetStack(config.ispResolver, &dnsOverUDPServerFactoryForGetaddrinfo{})(config)
// make sure we're going to create the root DNS resolver.
qaEnvOptionNetStack(config.rootResolver, &DNSOverUDPServerFactory{})(config)
// use a prefix logger for the QA env
prefixLogger := &logx.PrefixLogger{
Prefix: fmt.Sprintf("%-16s", "NETEM"),
Logger: config.logger,
}
// create an empty QAEnv
env := &QAEnv{
baseLogger: config.logger,
clientNICWrapper: config.clientNICWrapper,
ClientStack: nil,
closables: []io.Closer{},
emulateAndroidGetaddrinfo: &atomic.Bool{},
ispResolverConfig: netem.NewDNSConfig(),
dpi: netem.NewDPIEngine(prefixLogger),
once: sync.Once{},
otherResolversConfig: netem.NewDNSConfig(),
topology: netem.MustNewStarTopology(prefixLogger),
}
// create all the required internals
env.ClientStack = env.mustNewClientStack(config)
env.closables = append(env.closables, env.mustNewNetStacks(config)...)
return env
}
func (env *QAEnv) mustNewClientStack(config *qaEnvConfig) *netem.UNetStack {
// Note: because the stack is created using topology.AddHost, we don't
// need to call Close when done using it, since the topology will do that
// for us when we call the topology's Close method.
//
// TODO(bassosimone,kelmenhorst): consider allowing to configure the
// delays and losses should the need for this arise in the future.
return runtimex.Try1(env.topology.AddHost(
DefaultClientAddress,
config.ispResolver,
&netem.LinkConfig{
DPIEngine: env.dpi,
LeftNICWrapper: env.clientNICWrapper,
LeftToRightDelay: time.Millisecond,
RightToLeftDelay: time.Millisecond,
},
))
}
func (env *QAEnv) mustNewNetStacks(config *qaEnvConfig) (closables []io.Closer) {
resolver := config.rootResolver
for ipAddr, factories := range config.netStacks {
// Create the server's TCP/IP stack
//
// Note: because the stack is created using topology.AddHost, we don't
// need to call Close when done using it, since the topology will do that
// for us when we call the topology's Close method.
stack := runtimex.Try1(env.topology.AddHost(
ipAddr, // IP address
resolver, // default resolver address
&netem.LinkConfig{
LeftToRightDelay: time.Millisecond,
RightToLeftDelay: time.Millisecond,
},
))
for _, factory := range factories {
// instantiate a server with the given underlying network
server := factory.MustNewServer(env, stack)
// listen and start serving in the background
server.MustStart()
// track the server as the something that needs to be closed
closables = append(closables, server)
}
}
return
}
// AddRecordToAllResolvers adds the given DNS record to all DNS resolvers. You can safely
// add new DNS records from concurrent goroutines at any time.
func (env *QAEnv) AddRecordToAllResolvers(domain string, cname string, addrs ...string) {
runtimex.Try0(env.ISPResolverConfig().AddRecord(domain, cname, addrs...))
runtimex.Try0(env.OtherResolversConfig().AddRecord(domain, cname, addrs...))
}
// ISPResolverConfig returns the [*netem.DNSConfig] of the ISP resolver. Note that can safely
// add new DNS records from concurrent goroutines at any time.
func (env *QAEnv) ISPResolverConfig() *netem.DNSConfig {
return env.ispResolverConfig
}
// Logger is the [model.Logger] configured for this [*QAEnv],
func (env *QAEnv) Logger() model.Logger {
return env.baseLogger
}
// OtherResolversConfig returns the [*netem.DNSConfig] of the non-ISP resolvers. Note that can safely
// add new DNS records from concurrent goroutines at any time.
func (env *QAEnv) OtherResolversConfig() *netem.DNSConfig {
return env.otherResolversConfig
}
// DPIEngine returns the [*netem.DPIEngine] we're using on the
// link between the client stack and the router. You can safely
// add new DPI rules from concurrent goroutines at any time.
func (env *QAEnv) DPIEngine() *netem.DPIEngine {
return env.dpi
}
// EmulateAndroidGetaddrinfo configures [QAEnv] such that the Do method wraps
// the underlying client stack to return android_dns_cache_no_data on any error
// that occurs. This method can be safely called by multiple goroutines.
func (env *QAEnv) EmulateAndroidGetaddrinfo(value bool) {
env.emulateAndroidGetaddrinfo.Store(value)
}
// Do executes the given function such that [netxlite] code uses the
// underlying clientStack rather than ordinary networking code.
func (env *QAEnv) Do(function func()) {
var stack netem.UnderlyingNetwork = env.ClientStack
if env.emulateAndroidGetaddrinfo.Load() {
stack = &androidStack{stack}
}
WithCustomTProxy(stack, function)
}
// Close closes all the resources used by [QAEnv].
func (env *QAEnv) Close() error {
env.once.Do(func() {
// first close all the possible closables we track
for _, c := range env.closables {
_ = c.Close()
}
// finally close the whole network topology
_ = env.topology.Close()
})
return nil
}