Skip to content

Commit

Permalink
wip: input
Browse files Browse the repository at this point in the history
  • Loading branch information
ainghazal committed Mar 15, 2024
1 parent dfcf24c commit 7eb177d
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 16 deletions.
131 changes: 122 additions & 9 deletions internal/experiment/openvpn/endpoint.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
package openvpn

import (
"encoding/base64"
"errors"
"fmt"
"math/rand"
"net/url"
"strings"

vpnconfig "github.com/ooni/minivpn/pkg/config"

Check failure on line 11 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / test

no required module provides package github.com/ooni/minivpn/pkg/config; to add it:

Check failure on line 11 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / test

no required module provides package github.com/ooni/minivpn/pkg/config; to add it:

Check failure on line 11 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / test

no required module provides package github.com/ooni/minivpn/pkg/config; to add it:

Check failure on line 11 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / build_with_specific_go_version (1.16, ubuntu-latest)

no required module provides package github.com/ooni/minivpn/pkg/config; to add it:

Check failure on line 11 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / build_with_specific_go_version (1.15, ubuntu-latest)

no required module provides package github.com/ooni/minivpn/pkg/config; to add it:

Check failure on line 11 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / build_with_specific_go_version (1.21, ubuntu-latest)

no required module provides package github.com/ooni/minivpn/pkg/config; to add it:

Check failure on line 11 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / build_with_specific_go_version (1.20, ubuntu-latest)

no required module provides package github.com/ooni/minivpn/pkg/config; to add it:

Check failure on line 11 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / build_with_specific_go_version (1.17, ubuntu-latest)

no required module provides package github.com/ooni/minivpn/pkg/config; to add it:

Check failure on line 11 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / build_with_specific_go_version (1.19, ubuntu-latest)

no required module provides package github.com/ooni/minivpn/pkg/config; to add it:

Check failure on line 11 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / build_with_specific_go_version (1.18, ubuntu-latest)

no required module provides package github.com/ooni/minivpn/pkg/config; to add it:
vpntracex "github.com/ooni/minivpn/pkg/tracex"

Check failure on line 12 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / test

no required module provides package github.com/ooni/minivpn/pkg/tracex; to add it:

Check failure on line 12 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / test

no required module provides package github.com/ooni/minivpn/pkg/tracex; to add it:

Check failure on line 12 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / test

no required module provides package github.com/ooni/minivpn/pkg/tracex; to add it:

Check failure on line 12 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / build_with_specific_go_version (1.16, ubuntu-latest)

no required module provides package github.com/ooni/minivpn/pkg/tracex; to add it:

Check failure on line 12 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / build_with_specific_go_version (1.15, ubuntu-latest)

no required module provides package github.com/ooni/minivpn/pkg/tracex; to add it:

Check failure on line 12 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / build_with_specific_go_version (1.21, ubuntu-latest)

no required module provides package github.com/ooni/minivpn/pkg/tracex; to add it:

Check failure on line 12 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / build_with_specific_go_version (1.20, ubuntu-latest)

no required module provides package github.com/ooni/minivpn/pkg/tracex; to add it:

Check failure on line 12 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / build_with_specific_go_version (1.17, ubuntu-latest)

no required module provides package github.com/ooni/minivpn/pkg/tracex; to add it:

Check failure on line 12 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / build_with_specific_go_version (1.19, ubuntu-latest)

no required module provides package github.com/ooni/minivpn/pkg/tracex; to add it:

Check failure on line 12 in internal/experiment/openvpn/endpoint.go

View workflow job for this annotation

GitHub Actions / build_with_specific_go_version (1.18, ubuntu-latest)

no required module provides package github.com/ooni/minivpn/pkg/tracex; to add it:
)

var (
ErrBadBase64Blob = errors.New("wrong base64 encoding")
)

// endpoint is a single endpoint to be probed.
// The information contained in here is not generally not sufficient to complete a connection:
// we need more info, as cipher selection or obfuscating proxy credentials.
Expand Down Expand Up @@ -35,12 +43,64 @@ type endpoint struct {
// newEndpointFromInputString constructs an endpoint after parsing an input string.
//
// The input URI is in the form:
// "openvpn://1.2.3.4:443/udp/&provider=tunnelbear&obfs=none"
// "openvpn+obfs4://1.2.3.4:443/tcp/&provider=riseup&obfs=obfs4&cert=deadbeef"
// TODO(ainghazal): implement
func newEndpointFromInputString(input string) endpoint {
fmt.Println("should parse:", input)
return endpoint{}
// "openvpn://1.2.3.4:443/udp/&provider=tunnelbear"
// "openvpn+obfs4://1.2.3.4:443/tcp/&provider=riseup&cert=deadbeef"
func newEndpointFromInputString(uri string) (*endpoint, error) {
parsedURL, err := url.Parse(uri)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrInvalidInput, err)
}
var obfuscation string
switch parsedURL.Scheme {
case "openvpn":
obfuscation = "openvpn"
case "openvpn+obfs4":
obfuscation = "obfs4"
default:
return nil, fmt.Errorf("%w: unknown scheme: %s", ErrInvalidInput, parsedURL.Scheme)
}

host := parsedURL.Hostname()
if host == "" {
return nil, fmt.Errorf("%w: expected host: %s", ErrInvalidInput, parsedURL.Host)
}

port := parsedURL.Port()
if port == "" {
return nil, fmt.Errorf("%w: expected port: %s", ErrInvalidInput, parsedURL.Port())
}

pathParts := strings.Split(parsedURL.Path, "/")
if len(pathParts) != 3 {
return nil, fmt.Errorf("%w: invalid path: %s (%d)", ErrInvalidInput, pathParts, len(pathParts))
}
transport := pathParts[1]
if transport != "tcp" && transport != "udp" {
return nil, fmt.Errorf("%w: invalid transport: %s", ErrInvalidInput, transport)
}

params := parsedURL.Query()
provider := params.Get("provider")

if provider == "" {
return nil, fmt.Errorf("%w: please specify a provider as part of the input", ErrInvalidInput)
}

if provider != "riseup" {
// because we are hardcoding at the moment. figure out a way to pass info for
// arbitrary providers as options instead
return nil, fmt.Errorf("%w: unknown provider: %s", ErrInvalidInput, provider)
}

endpoint := &endpoint{
IPAddr: host,
Obfuscation: obfuscation,
Port: port,
Protocol: "openvpn",
Provider: provider,
Transport: transport,
}
return endpoint, nil
}

// String implements Stringer. This is a subset of the input URI scheme.
Expand All @@ -65,7 +125,7 @@ func (e *endpoint) AsInputURI() string {
}

// endpointList is a list of endpoints.
type endpointList []endpoint
type endpointList []*endpoint

// allEndpoints contains a subset of known endpoints to be used if no input is passed to the experiment.
// This is a hardcoded list for now, but the idea is that we can receive this from the check-in api in the future.
Expand Down Expand Up @@ -96,6 +156,15 @@ func (e endpointList) Shuffle() endpointList {
return e
}

func isValidProvider(provider string) bool {
switch provider {
case "riseup":
return true
default:
return false
}
}

// TODO(ainghazal): this is extremely hacky, but it's a first step
// until we manage to have the check-in API handing credentials.
// Do note that these certificates will expire ca. Apr 6 2024
Expand Down Expand Up @@ -165,12 +234,41 @@ vly8wNG42zeRWAXz
// To obtain that, we merge the endpoint specific configuration with base options.
// These base options are for the moment hardcoded. In the future we will want to be smarter
// about getting information for different providers.
func getVPNConfig(tracer *vpntracex.Tracer, endpoint *endpoint) (*vpnconfig.Config, error) {
func getVPNConfig(tracer *vpntracex.Tracer, endpoint *endpoint, experimentConfig *Config) (*vpnconfig.Config, error) {
// TODO(ainghazal): use options merge (pending PR)

provider := endpoint.Provider
// TODO(ainghazal): return error if provider unknown. we're in the happy path for now.
if !isValidProvider(provider) {
return nil, fmt.Errorf("%w: unknown provider: %s", ErrInvalidInput, provider)
}

baseOptions := defaultOptionsByProvider[provider]

// We override any provider related options found in the config
if experimentConfig.SafeCA != "" {
ca, err := extractBase64Blob(experimentConfig.SafeCA)
if err != nil {
return nil, err
}
baseOptions.CA = []byte(ca)
}

if experimentConfig.SafeKey != "" {
key, err := extractBase64Blob(experimentConfig.SafeKey)
if err != nil {
return nil, err
}
baseOptions.Key = []byte(key)
}

if experimentConfig.SafeCert != "" {
cert, err := extractBase64Blob(experimentConfig.SafeCert)
if err != nil {
return nil, err
}
baseOptions.Key = []byte(cert)
}

cfg := vpnconfig.NewConfig(
vpnconfig.WithOpenVPNOptions(
&vpnconfig.OpenVPNOptions{
Expand All @@ -193,3 +291,18 @@ func getVPNConfig(tracer *vpntracex.Tracer, endpoint *endpoint) (*vpnconfig.Conf
// TODO: validate options here and return an error.
return cfg, nil
}

func extractBase64Blob(val string) (string, error) {
s := strings.TrimPrefix(val, "base64:")
if len(s) == len(val) {
return "", fmt.Errorf("%w: %s", ErrBadBase64Blob, "missing prefix")
}
dec, err := base64.URLEncoding.DecodeString(strings.TrimSpace(s))
if err != nil {
return "", fmt.Errorf("%w: %s", ErrBadBase64Blob, err)
}
if len(dec) == 0 {
return "", nil
}
return string(dec), nil
}
46 changes: 42 additions & 4 deletions internal/experiment/openvpn/openvpn.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"errors"
"strconv"
"strings"
"time"

"github.com/ooni/probe-cli/v3/internal/measurexlite"
Expand All @@ -25,7 +26,10 @@ const (
// of this experiment. By tagging these variables with `ooni:"..."`, we allow
// miniooni's -O flag to find them and set them.
type Config struct {
Provider string
Provider string `ooni:"VPN provider"`
SafeKey string `ooni:"key to connect to the OpenVPN endpoint"`
SafeCert string `ooni:"cert to connect to the OpenVPN endpoint"`
SafeCA string `ooni:"ca to connect to the OpenVPN endpoint"`
}

// TestKeys contains the experiment's result.
Expand Down Expand Up @@ -113,6 +117,24 @@ func (m Measurer) ExperimentVersion() string {
return testVersion
}

var (
ErrInvalidInput = errors.New("invalid input")
)

func parseListOfInputs(inputs string) (endpointList, error) {
endpoints := make(endpointList, 0)
inputList := strings.Split(inputs, ",")
for _, i := range inputList {
e, err := newEndpointFromInputString(i)
if err != nil {
return endpoints, err
}
endpoints = append(endpoints, e)
}
return endpoints, nil

}

// ErrFailure is the error returned when you set the
// config.ReturnError field to true.
var ErrFailure = errors.New("mocked error")
Expand All @@ -123,10 +145,25 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error {
measurement := args.Measurement
sess := args.Session

var endpoints endpointList
var err error

if measurement.Input == "" {
// if input is null, we get the hardcoded list of inputs.
endpoints = allEndpoints
} else {
// otherwise, we expect a comma-separated value of inputs in
// the URI scheme defined for openvpn experiments.
endpoints, err = parseListOfInputs(string(measurement.Input))
if err != nil {
return err
}
}

tk := NewTestKeys()

// TODO(ainghazal): we could parallelize multiple probing.
for idx, endpoint := range allEndpoints.Shuffle() {
for idx, endpoint := range endpoints.Shuffle() {
sess.Logger().Infof("Probing endpoint %s", endpoint.String())
tk.Inputs = append(tk.Inputs, endpoint.AsInputURI())

Expand All @@ -143,6 +180,7 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error {
measurement.TestKeys = tk

// TODO(ainghazal): validate we have valid config for each endpoint.
// TODO(ainghazal): validate hostname is a valid IP (ipv4 or 6)
// TODO(ainghazal): decide what to do if we have expired certs (abort one measurement or abort the whole experiment?)

// Note: if here we return an error, the parent code will assume
Expand All @@ -154,7 +192,7 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error {

// connectAndHandshake dials a connection and attempts an OpenVPN handshake using that dialer.
func (m *Measurer) connectAndHandshake(ctx context.Context, index int64,
zeroTime time.Time, logger model.Logger, endpoint endpoint) *SingleConnection {
zeroTime time.Time, logger model.Logger, endpoint *endpoint) *SingleConnection {

// create a trace for the network dialer
trace := measurexlite.NewTrace(index, zeroTime)
Expand All @@ -165,7 +203,7 @@ func (m *Measurer) connectAndHandshake(ctx context.Context, index int64,
// create a vpn tun Device that attempts to dial and performs the handshake
handshakeTracer := vpntracex.NewTracerWithTransactionID(zeroTime, index)

openvpnConfig, err := getVPNConfig(handshakeTracer, &endpoint)
openvpnConfig, err := getVPNConfig(handshakeTracer, endpoint, &m.config)
if err != nil {
// TODO: find a better way to return the error - this is not a test failure,
// it's a failure to start the measurement. we should abort
Expand Down
5 changes: 2 additions & 3 deletions internal/registry/openvpn.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ func init() {
*config.(*openvpn.Config), "openvpn",
)
},
// TODO(ainghazal): we can pass an array of providers here.
config: &openvpn.Config{},
enabledByDefault: true,
enabledByDefault: false,
interruptible: true,
inputPolicy: model.InputNone,
inputPolicy: model.InputOptional,
}
}

0 comments on commit 7eb177d

Please sign in to comment.