Skip to content

Commit

Permalink
feat(enginenetx): honor user-provided policy (#1298)
Browse files Browse the repository at this point in the history
While there, change the file name to ensure it's clear this policy is
static and we are not going to use stats to modify it.

Part of ooni/probe#2531
  • Loading branch information
bassosimone authored Sep 25, 2023
1 parent 039591e commit 6174783
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 21 deletions.
16 changes: 8 additions & 8 deletions internal/enginenetx/httpsdialerstatic.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ type HTTPSDialerStaticPolicy struct {
Root *HTTPSDialerStaticPolicyRoot
}

// httpsDialerStaticPolicyKey is the kvstore key used to retrieve the static policy.
const httpsDialerStaticPolicyKey = "httpsdialer.conf"
// HTTPSDialerStaticPolicyKey is the kvstore key used to retrieve the static policy.
const HTTPSDialerStaticPolicyKey = "httpsdialerstatic.conf"

// errDialerStaticPolicyWrongVersion means that the static policy document has the wrong version number.
var errDialerStaticPolicyWrongVersion = errors.New("wrong static policy version")
Expand All @@ -34,7 +34,7 @@ var errDialerStaticPolicyWrongVersion = errors.New("wrong static policy version"
func NewHTTPSDialerStaticPolicy(
kvStore model.KeyValueStore, fallback HTTPSDialerPolicy) (*HTTPSDialerStaticPolicy, error) {
// attempt to read the static policy bytes from the kvstore
data, err := kvStore.Get(httpsDialerStaticPolicyKey)
data, err := kvStore.Get(HTTPSDialerStaticPolicyKey)
if err != nil {
return nil, err
}
Expand All @@ -46,12 +46,12 @@ func NewHTTPSDialerStaticPolicy(
}

// make sure the version is OK
if root.Version != httpsDialerStaticPolicyVersion {
if root.Version != HTTPSDialerStaticPolicyVersion {
err := fmt.Errorf(
"%s: %w: expected=%d got=%d",
httpsDialerStaticPolicyKey,
HTTPSDialerStaticPolicyKey,
errDialerStaticPolicyWrongVersion,
httpsDialerStaticPolicyVersion,
HTTPSDialerStaticPolicyVersion,
root.Version,
)
return nil, err
Expand All @@ -64,8 +64,8 @@ func NewHTTPSDialerStaticPolicy(
return out, nil
}

// httpsDialerStaticPolicyVersion is the current version of the static policy file.
const httpsDialerStaticPolicyVersion = 1
// HTTPSDialerStaticPolicyVersion is the current version of the static policy file.
const HTTPSDialerStaticPolicyVersion = 1

// HTTPSDialerStaticPolicyRoot is the root of a statically loaded policy.
type HTTPSDialerStaticPolicyRoot struct {
Expand Down
18 changes: 9 additions & 9 deletions internal/enginenetx/httpsdialerstatic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,25 @@ func TestHTTPSDialerStaticPolicy(t *testing.T) {
expectedPolicy: nil,
}, {
name: "with nil input",
key: httpsDialerStaticPolicyKey,
key: HTTPSDialerStaticPolicyKey,
input: nil,
expectErr: "hujson: line 1, column 1: parsing value: unexpected EOF",
expectedPolicy: nil,
}, {
name: "with invalid serialized JSON",
key: httpsDialerStaticPolicyKey,
key: HTTPSDialerStaticPolicyKey,
input: []byte(`{`),
expectErr: "hujson: line 1, column 2: parsing value: unexpected EOF",
expectedPolicy: nil,
}, {
name: "with empty JSON",
key: httpsDialerStaticPolicyKey,
key: HTTPSDialerStaticPolicyKey,
input: []byte(`{}`),
expectErr: "httpsdialer.conf: wrong static policy version: expected=1 got=0",
expectErr: "httpsdialerstatic.conf: wrong static policy version: expected=1 got=0",
expectedPolicy: nil,
}, {
name: "with real serialized policy",
key: httpsDialerStaticPolicyKey,
key: HTTPSDialerStaticPolicyKey,
input: (func() []byte {
return runtimex.Try1(json.Marshal(&HTTPSDialerStaticPolicyRoot{
Domains: map[string][]*HTTPSDialerTactic{
Expand Down Expand Up @@ -92,7 +92,7 @@ func TestHTTPSDialerStaticPolicy(t *testing.T) {
VerifyHostname: "api.ooni.io",
}},
},
Version: httpsDialerStaticPolicyVersion,
Version: HTTPSDialerStaticPolicyVersion,
}))
})(),
expectErr: "",
Expand Down Expand Up @@ -127,7 +127,7 @@ func TestHTTPSDialerStaticPolicy(t *testing.T) {
VerifyHostname: "api.ooni.io",
}},
},
Version: httpsDialerStaticPolicyVersion,
Version: HTTPSDialerStaticPolicyVersion,
},
},
}}
Expand Down Expand Up @@ -185,7 +185,7 @@ func TestHTTPSDialerStaticPolicy(t *testing.T) {
Domains: map[string][]*HTTPSDialerTactic{
"api.ooni.io": expect,
},
Version: httpsDialerStaticPolicyVersion,
Version: HTTPSDialerStaticPolicyVersion,
},
}

Expand Down Expand Up @@ -214,7 +214,7 @@ func TestHTTPSDialerStaticPolicy(t *testing.T) {
Fallback: &HTTPSDialerNullPolicy{},
Root: &HTTPSDialerStaticPolicyRoot{
Domains: nil, // empty so we fallback for all domains
Version: httpsDialerStaticPolicyVersion,
Version: HTTPSDialerStaticPolicyVersion,
},
}

Expand Down
19 changes: 15 additions & 4 deletions internal/enginenetx/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,11 @@ func NewNetwork(
// Additionally, please note the following limitations (to be overcome through
// future refactoring of this func):
//
// - for now, we're using a "null" policy that does happy eyeballs but otherwise
// does not use beacons or other TLS handshake tricks;
//
// - for now, we're using a "null" stats tracker, meaning we don't track stats.
httpsDialer := NewHTTPSDialer(
logger,
&netxlite.Netx{Underlying: nil}, // nil means using netxlite's singleton
&HTTPSDialerNullPolicy{},
newHTTPSDialerPolicy(kvStore),
resolver,
&HTTPSDialerNullStatsTracker{},
)
Expand Down Expand Up @@ -127,3 +124,17 @@ func NewNetwork(

return &Network{txp}
}

// newHTTPSDialerPolicy contains the logic to select the [HTTPSDialerPolicy] to use.
func newHTTPSDialerPolicy(kvStore model.KeyValueStore) HTTPSDialerPolicy {
// the fallback policy we're using is the "null" policy
fallback := &HTTPSDialerNullPolicy{}

// make sure we honor a user-provided policy
policy, err := NewHTTPSDialerStaticPolicy(kvStore, fallback)
if err != nil {
return fallback
}

return policy
}
87 changes: 87 additions & 0 deletions internal/enginenetx/network_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ package enginenetx_test

import (
"context"
"encoding/json"
"net"
"net/http"
"net/url"
"testing"
"time"

"github.com/apex/log"
"github.com/google/go-cmp/cmp"
"github.com/ooni/probe-cli/v3/internal/bytecounter"
"github.com/ooni/probe-cli/v3/internal/enginenetx"
"github.com/ooni/probe-cli/v3/internal/kvstore"
"github.com/ooni/probe-cli/v3/internal/measurexlite"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netemx"
"github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/runtimex"
"github.com/ooni/probe-cli/v3/internal/testingsocks5"
"github.com/ooni/probe-cli/v3/internal/testingx"
)
Expand Down Expand Up @@ -269,4 +273,87 @@ func TestNetworkQA(t *testing.T) {
t.Fatal("expected non-nil cookie jar")
}
})

t.Run("NewNetwork uses the correct HTTPSDialerPolicy", func(t *testing.T) {
// testcase is a test case run by this func
type testcase struct {
name string
kvStore func() model.KeyValueStore
expectStatus int
expectBody []byte
}

cases := []testcase{
// Without a policy accessing www.example.com should lead to 200 as status
// code and the expected web page when we're using netem
{
name: "when there is no user-provided policy",
kvStore: func() model.KeyValueStore {
return &kvstore.Memory{}
},
expectStatus: 200,
expectBody: []byte(netemx.ExampleWebPage),
},

// But we can create a policy that can land us on a different website (not the
// typical use case of the policy, but definitely demonstrating it works)
{
name: "when there's a user-provided policy",
kvStore: func() model.KeyValueStore {
policy := &enginenetx.HTTPSDialerStaticPolicyRoot{
Domains: map[string][]*enginenetx.HTTPSDialerTactic{
"www.example.com": {{
Endpoint: net.JoinHostPort(netemx.AddressApiOONIIo, "443"),
InitialDelay: 0,
SNI: "www.example.com",
VerifyHostname: "api.ooni.io",
}},
},
Version: enginenetx.HTTPSDialerStaticPolicyVersion,
}
rawPolicy := runtimex.Try1(json.Marshal(policy))
kvStore := &kvstore.Memory{}
runtimex.Try0(kvStore.Set(enginenetx.HTTPSDialerStaticPolicyKey, rawPolicy))
return kvStore
},
expectStatus: 404,
expectBody: []byte{},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
env := netemx.MustNewScenario(netemx.InternetScenario)
defer env.Close()

env.Do(func() {
netx := enginenetx.NewNetwork(
bytecounter.New(),
tc.kvStore(),
log.Log,
nil, // proxy URL
netxlite.NewStdlibResolver(log.Log),
)
defer netx.Close()

client := netx.NewHTTPClient()
resp, err := client.Get("https://www.example.com/")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != tc.expectStatus {
t.Fatal("StatusCode: expected", tc.expectStatus, "got", resp.StatusCode)
}
data, err := netxlite.ReadAllContext(context.Background(), resp.Body)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(tc.expectBody, data); diff != "" {
t.Fatal(diff)
}
})
})
}
})
}

0 comments on commit 6174783

Please sign in to comment.