Skip to content

Commit

Permalink
feat(enginenetx): support getting stats on a domain endpoint (#1311)
Browse files Browse the repository at this point in the history
We need this functionality to ask the stats questions like "what are all
the available tactics for www.example.com:443 along with their stats?".
In turn, this information is needed to implement a new policy that uses
existing stats to choose what to do.

While there, rename files to make it clear which files contain policies
and which contain state trackers. Useful because upcoming changes will
add files containing state-based policy.

While there, stop being paranoid about cloning a nil map to a nil map,
acknowledge this is not important and making it round trip correctly
would require more code we don't need, and simply adjust tests to accept
nil and empty map as equal.

Part of ooni/probe#2531
  • Loading branch information
bassosimone authored Sep 26, 2023
1 parent 51bf872 commit 619d536
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 3 deletions.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"encoding/json"
"errors"
"fmt"
"net"
"sync"
"time"

Expand Down Expand Up @@ -87,10 +88,8 @@ type statsTactic struct {
}

func statsCloneMapStringInt64(input map[string]int64) (output map[string]int64) {
output = make(map[string]int64)
for key, value := range input {
if output == nil {
output = make(map[string]int64) // the idea here is to clone a nil map to a nil map
}
output[key] = value
}
return
Expand Down Expand Up @@ -401,3 +400,20 @@ func (mt *statsManager) Close() error {
// write updated stats into the underlying key-value store
return mt.kvStore.Set(statsKey, runtimex.Try1(json.Marshal(mt.container)))
}

// LookupTacticsStats returns stats about tactics for a given domain and port. The returned
// list is a clone of the one stored by [*statsManager] so, it can easily be modified.
func (mt *statsManager) LookupTactics(domain string, port string) []*statsTactic {
out := []*statsTactic{}

// get exclusive access
defer mt.mu.Unlock()
mt.mu.Lock()

// return a copy of each entry
domainEpnts := mt.container.DomainEndpoints[net.JoinHostPort(domain, port)]
for _, entry := range domainEpnts.Tactics {
out = append(out, entry.Clone())
}
return out
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"sort"
"testing"
"time"

Expand Down Expand Up @@ -746,6 +747,7 @@ func TestStatsManagerCallbacks(t *testing.T) {
// make sure the stats are the ones we expect
diffOptions := []cmp.Option{
cmpopts.IgnoreFields(statsTactic{}, "LastUpdated"),
cmpopts.EquateEmpty(),
}
if diff := cmp.Diff(tc.expectRoot, root, diffOptions...); diff != "" {
t.Fatal(diff)
Expand All @@ -758,3 +760,112 @@ func TestStatsManagerCallbacks(t *testing.T) {
})
}
}

// Make sure that we can safely obtain statistics for a domain and a port.
func TestStatsManagerLookupTacticsStats(t *testing.T) {

// prepare the content of the stats
twentyMinutesAgo := time.Now().Add(-20 * time.Minute)

expectTactics := []*statsTactic{{
CountStarted: 5,
CountTCPConnectError: 0,
CountTCPConnectInterrupt: 0,
CountTLSHandshakeError: 0,
CountTLSHandshakeInterrupt: 0,
CountTLSVerificationError: 0,
CountSuccess: 5,
HistoTCPConnectError: map[string]int64{},
HistoTLSHandshakeError: map[string]int64{},
HistoTLSVerificationError: map[string]int64{},
LastUpdated: twentyMinutesAgo,
Tactic: &httpsDialerTactic{
Address: "162.55.247.208",
InitialDelay: 0,
Port: "443",
SNI: "www.repubblica.it",
VerifyHostname: "api.ooni.io",
},
}, {
CountStarted: 1,
CountTCPConnectError: 0,
CountTCPConnectInterrupt: 0,
CountTLSHandshakeError: 0,
CountTLSHandshakeInterrupt: 0,
CountTLSVerificationError: 0,
CountSuccess: 1,
HistoTCPConnectError: map[string]int64{},
HistoTLSHandshakeError: map[string]int64{},
HistoTLSVerificationError: map[string]int64{},
LastUpdated: twentyMinutesAgo,
Tactic: &httpsDialerTactic{
Address: "162.55.247.208",
InitialDelay: 0,
Port: "443",
SNI: "www.kernel.org",
VerifyHostname: "api.ooni.io",
},
}, {
CountStarted: 3,
CountTCPConnectError: 0,
CountTCPConnectInterrupt: 0,
CountTLSHandshakeError: 0,
CountTLSHandshakeInterrupt: 0,
CountTLSVerificationError: 0,
CountSuccess: 3,
HistoTCPConnectError: map[string]int64{},
HistoTLSHandshakeError: map[string]int64{},
HistoTLSVerificationError: map[string]int64{},
LastUpdated: twentyMinutesAgo,
Tactic: &httpsDialerTactic{
Address: "162.55.247.208",
InitialDelay: 0,
Port: "443",
SNI: "theconversation.com",
VerifyHostname: "api.ooni.io",
},
}}

expectContainer := &statsContainer{
DomainEndpoints: map[string]*statsDomainEndpoint{
"api.ooni.io:443": {
Tactics: map[string]*statsTactic{},
},
},
Version: statsContainerVersion,
}

for _, tactic := range expectTactics {
expectContainer.DomainEndpoints["api.ooni.io:443"].Tactics[tactic.Tactic.tacticSummaryKey()] = tactic
}

// configure the initial value of the stats
kvStore := &kvstore.Memory{}
if err := kvStore.Set(statsKey, runtimex.Try1(json.Marshal(expectContainer))); err != nil {
t.Fatal(err)
}

// create the stats manager
stats := newStatsManager(kvStore, log.Log)

// obtain tactics
tactics := stats.LookupTactics("api.ooni.io", "443")
if len(tactics) != 3 {
t.Fatal("unexpected tactics length")
}

// sort obtained tactics lexicographically
sort.SliceStable(tactics, func(i, j int) bool {
return tactics[i].Tactic.tacticSummaryKey() < tactics[j].Tactic.tacticSummaryKey()
})

// sort the initial tactics as well
sort.SliceStable(expectTactics, func(i, j int) bool {
return expectTactics[i].Tactic.tacticSummaryKey() < expectTactics[j].Tactic.tacticSummaryKey()
})

// compare once we have sorted
if diff := cmp.Diff(expectTactics, tactics); diff != "" {
t.Fatal(diff)
}
}

0 comments on commit 619d536

Please sign in to comment.