Skip to content

Commit f14f5c7

Browse files
committed
feat: implement riseupvpn using the DSL
Part of ooni/probe#2502
1 parent c4e7ffc commit f14f5c7

File tree

9 files changed

+2799
-1
lines changed

9 files changed

+2799
-1
lines changed

pkg/experiment/fbmessenger/measurer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error {
6262
args.Session.Logger(), &dsl.NullMetrics{}, args.Measurement.MeasurementStartTimeSaved)
6363
defer rtx.Close()
6464

65-
// evaluate the function and handle exceptions
65+
// evaluate the pipeline and handle exceptions
6666
argument0 := dsl.NewValue(&dsl.Void{})
6767
if err := dsl.Try(pipeline.Run(ctx, rtx, argument0.AsGeneric())); err != nil {
6868
return err

pkg/experiment/riseupvpn/riseupvpn.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Package riseupvpn implements the riseupvpn experiment.
2+
package riseupvpn
3+
4+
import (
5+
"context"
6+
"encoding/json"
7+
8+
"github.com/ooni/2023-05-richer-input/pkg/dsl"
9+
"github.com/ooni/probe-engine/pkg/model"
10+
)
11+
12+
// NewMeasurer returns a new [Measurer] instance.
13+
func NewMeasurer(rawOptions json.RawMessage) *Measurer {
14+
return &Measurer{rawOptions}
15+
}
16+
17+
// Measurer is the riseupvpn measurer.
18+
type Measurer struct {
19+
// RawOptions contains the raw options for this experiment.
20+
RawOptions json.RawMessage
21+
}
22+
23+
var _ model.ExperimentMeasurer = &Measurer{}
24+
25+
// ExperimentName implements model.ExperimentMeasurer
26+
func (m *Measurer) ExperimentName() string {
27+
return "riseupvpn"
28+
}
29+
30+
// ExperimentVersion implements model.ExperimentMeasurer
31+
func (m *Measurer) ExperimentVersion() string {
32+
// TODO(bassosimone): the real experiment is at version 0.2.0 and
33+
// we will _probably_ be fine by saying we're at 0.4.0 since the
34+
// https://github.com/ooni/probe-cli/pull/1125 PR uses 0.3.0.
35+
return "0.4.0"
36+
}
37+
38+
// TestKeys contains the experiment test keys.
39+
type TestKeys struct {
40+
*dsl.Observations
41+
}
42+
43+
// Run implements model.ExperimentMeasurer
44+
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error {
45+
// parse the targets
46+
var astRoot dsl.LoadableASTNode
47+
if err := json.Unmarshal(m.RawOptions, &astRoot); err != nil {
48+
return err
49+
}
50+
51+
// create an AST loader
52+
loader := dsl.NewASTLoader()
53+
54+
// create the testkeys
55+
tk := &TestKeys{}
56+
57+
// load and make the AST runnable
58+
pipeline, err := loader.Load(&astRoot)
59+
if err != nil {
60+
return err
61+
}
62+
63+
// TODO(bassosimone): both fbmessenger and riseupvpn lack
64+
//
65+
// 1. an explicit mechanism to report the bytes sent and received, but the
66+
// implicit context-based mechanism probably works;
67+
//
68+
// 2. a DSL-based mechanism to increment the test progress percentage.
69+
70+
// create the DSL runtime
71+
rtx := dsl.NewMeasurexliteRuntime(
72+
args.Session.Logger(), &dsl.NullMetrics{}, args.Measurement.MeasurementStartTimeSaved)
73+
defer rtx.Close()
74+
75+
// evaluate the pipeline and handle exceptions
76+
argument0 := dsl.NewValue(&dsl.Void{})
77+
if err := dsl.Try(pipeline.Run(ctx, rtx, argument0.AsGeneric())); err != nil {
78+
return err
79+
}
80+
81+
// obtain the observations
82+
tk.Observations = dsl.ReduceObservations(rtx.ExtractObservations()...)
83+
84+
// save the testkeys
85+
args.Measurement.TestKeys = tk
86+
return nil
87+
}
88+
89+
// SummaryKeys contains summary keys for this experiment.
90+
//
91+
// Note that this structure is part of the ABI contract with ooniprobe
92+
// therefore we should be careful when changing it.
93+
type SummaryKeys struct {
94+
IsAnomaly bool `json:"-"`
95+
}
96+
97+
// GetSummaryKeys implements model.ExperimentMeasurer
98+
func (m *Measurer) GetSummaryKeys(*model.Measurement) (any, error) {
99+
sk := SummaryKeys{IsAnomaly: false}
100+
return sk, nil
101+
}

pkg/ooniprobe/runner/nettest.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type nettestFactory = func(args *modelx.InterpreterNettestRunArguments,
2626
// nettestRegistry maps nettests to their constructors.
2727
var nettestRegistry = map[string]nettestFactory{
2828
"facebook_messenger": fbmessengerNew,
29+
"riseupvpn": riseupvpnNew,
2930
"signal": signalNew,
3031
"telegram": telegramNew,
3132
"urlgetter": urlgetterNew,

pkg/ooniprobe/runner/riseupvpn.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package runner
2+
3+
//
4+
// riseupvpn.go implements the riseupvpn nettest
5+
//
6+
7+
import (
8+
"context"
9+
"time"
10+
11+
riseupvpnnew "github.com/ooni/2023-05-richer-input/pkg/experiment/riseupvpn"
12+
"github.com/ooni/2023-05-richer-input/pkg/modelx"
13+
"github.com/ooni/probe-engine/pkg/experiment/riseupvpn"
14+
"github.com/ooni/probe-engine/pkg/model"
15+
)
16+
17+
// riseupvpnNettest is the riseupvpn nettest.
18+
type riseupvpnNettest struct {
19+
args *modelx.InterpreterNettestRunArguments
20+
config *modelx.InterpreterConfig
21+
ix *Interpreter
22+
}
23+
24+
var _ nettest = &riseupvpnNettest{}
25+
26+
// riseupvpnNew constructs a new riseupvpn instance.
27+
func riseupvpnNew(args *modelx.InterpreterNettestRunArguments,
28+
config *modelx.InterpreterConfig, ix *Interpreter) (nettest, error) {
29+
// fill the nettest struct
30+
nettest := &riseupvpnNettest{
31+
args: args,
32+
config: config,
33+
ix: ix,
34+
}
35+
36+
// return to the caller
37+
return nettest, nil
38+
}
39+
40+
// Run implements nettest
41+
func (nt *riseupvpnNettest) Run(ctx context.Context) error {
42+
// make sure the location didn't change
43+
if err := nt.ix.location.Refresh(); err != nil {
44+
return err
45+
}
46+
47+
// save the start time
48+
t0 := time.Now()
49+
50+
// create a new experiment instance
51+
var exp model.ExperimentMeasurer
52+
if nt.args.ExperimentalFlags["dsl"] {
53+
exp = riseupvpnnew.NewMeasurer(nt.args.Targets)
54+
} else {
55+
exp = riseupvpn.NewExperimentMeasurer(riseupvpn.Config{})
56+
}
57+
58+
// run with the given experiment and input
59+
err := runExperiment(
60+
ctx,
61+
nt.args.Annotations,
62+
newProgressEmitterNettest(nt.ix.logger, nt.ix.view),
63+
exp,
64+
"", // input
65+
nt.ix,
66+
nt.args.ReportID,
67+
t0,
68+
nt.config.TestHelpers,
69+
)
70+
71+
// handle an immediate error such as a context error
72+
return err
73+
}

pkg/x/cmd/riseupvpn/apifetch.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"crypto/x509"
7+
"encoding/json"
8+
"net/http"
9+
10+
"github.com/apex/log"
11+
"github.com/ooni/probe-engine/pkg/netxlite"
12+
"github.com/ooni/probe-engine/pkg/runtimex"
13+
)
14+
15+
// apiMustFetchCA fetches, validates and returns the CA or PANICS.
16+
func apiMustFetchCA() string {
17+
log.Info("- fetching the CA")
18+
19+
resp := runtimex.Try1(http.Get("https://black.riseup.net/ca.crt"))
20+
runtimex.Assert(resp.StatusCode == 200, "unexpected HTTP response status")
21+
defer resp.Body.Close()
22+
23+
log.Infof("HTTP response: %+v", resp)
24+
25+
body := string(runtimex.Try1(netxlite.ReadAllContext(context.Background(), resp.Body)))
26+
log.Infof("fetched CA:\n%s\n", string(body))
27+
return body
28+
}
29+
30+
// apiMustFetchEIPService fetches and parses the [*apiEIPService] or PANICS.
31+
func apiMustFetchEIPService(caCert string) *apiEIPService {
32+
log.Info("- fetching eip-service.json")
33+
34+
// create and fill a certificate pool
35+
pool := x509.NewCertPool()
36+
runtimex.Assert(pool.AppendCertsFromPEM([]byte(caCert)), "AppendCertsFromPEM failed")
37+
38+
// create a client using a transport using the pool
39+
client := &http.Client{
40+
Transport: &http.Transport{
41+
TLSClientConfig: &tls.Config{
42+
RootCAs: pool,
43+
},
44+
},
45+
}
46+
47+
// perform the HTTP round trip
48+
resp := runtimex.Try1(client.Get("https://api.black.riseup.net/3/config/eip-service.json"))
49+
runtimex.Assert(resp.StatusCode == 200, "unexpected HTTP response status")
50+
defer resp.Body.Close()
51+
52+
log.Infof("HTTP response: %+v", resp)
53+
54+
// read the whole body
55+
body := runtimex.Try1(netxlite.ReadAllContext(context.Background(), resp.Body))
56+
log.Infof("fetched eip-service.json:\n%s\n", string(body))
57+
58+
// parse the response body
59+
var eipService apiEIPService
60+
runtimex.Try0(json.Unmarshal(body, &eipService))
61+
return &eipService
62+
}

pkg/x/cmd/riseupvpn/apimodel.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package main
2+
3+
//
4+
// Riseupvpn (and LEAP) API data model
5+
//
6+
// Code adapted from https://github.com/ooni/probe-cli/pull/1125, which was
7+
// originally authored by https://github.com/cyBerta.
8+
//
9+
10+
import "github.com/ooni/probe-engine/pkg/runtimex"
11+
12+
// apiEIPService is the main JSON object returned by https://api.black.riseup.net/3/config/eip-service.json.
13+
type apiEIPService struct {
14+
Gateways []apiGatewayV3
15+
}
16+
17+
// apiGatewayV3 describes a riseupvpn gateway.
18+
type apiGatewayV3 struct {
19+
Capabilities apiCapabilities
20+
Host string
21+
IPAddress string `json:"ip_address"`
22+
Location string `json:"location"`
23+
}
24+
25+
// apiCapabilities is a list of transports a gateway supports.
26+
type apiCapabilities struct {
27+
Transport []apiTransportV3
28+
}
29+
30+
// apiTransportV3 describes a transport.
31+
type apiTransportV3 struct {
32+
Type string
33+
Protocols []string
34+
Ports []string
35+
Options map[string]string
36+
}
37+
38+
// supportsTCP returns whether the transport supports TCP.
39+
func (txp *apiTransportV3) supportsTCP() bool {
40+
return txp.supportsTransportProtocol("tcp")
41+
}
42+
43+
// supportsTransportProtocol returns whether the transport uses the given
44+
// transport protocol, which is one of "tcp" and "udp".
45+
func (txp *apiTransportV3) supportsTransportProtocol(tp string) bool {
46+
runtimex.Assert(tp == "tcp" || tp == "udp", "invalid transport protocol")
47+
for _, protocol := range txp.Protocols {
48+
if tp == protocol {
49+
return true
50+
}
51+
}
52+
return false
53+
}
54+
55+
// typeIsOneOf returns whether the transport type is one of the given types.
56+
func (txp *apiTransportV3) typeIsOneOf(types ...string) bool {
57+
for _, t := range types {
58+
if txp.Type == t {
59+
return true
60+
}
61+
}
62+
return false
63+
}

0 commit comments

Comments
 (0)