From 520398dd8ed40f91a4527d2f4bc303b3bcaa0a3a Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Tue, 22 Jun 2021 00:12:03 +0200 Subject: [PATCH] feat: tutorial on how to write the torsf experiment (#390) Original tracking issue for Sprint 41: https://github.com/ooni/probe/issues/1507 Follow-up work in Sprint 42 tracked by: https://github.com/ooni/probe/issues/1689 --- internal/engine/experiment/dash/dash_test.go | 2 +- .../experiment/dnscheck/dnscheck_test.go | 2 +- .../engine/experiment/example/example_test.go | 2 +- .../fbmessenger/fbmessenger_test.go | 2 +- internal/engine/experiment/hhfm/hhfm_test.go | 2 +- internal/engine/experiment/hirl/hirl_test.go | 2 +- .../httphostheader/httphostheader_test.go | 2 +- internal/engine/experiment/ndt7/ndt7_test.go | 2 +- .../engine/experiment/psiphon/psiphon_test.go | 2 +- .../experiment/riseupvpn/riseupvpn_test.go | 2 +- internal/engine/experiment/run/run_test.go | 2 +- .../engine/experiment/signal/signal_test.go | 2 +- .../sniblocking/sniblocking_test.go | 2 +- .../stunreachability/stunreachability_test.go | 2 +- .../experiment/telegram/telegram_test.go | 2 +- .../engine/experiment/tlstool/tlstool_test.go | 2 +- internal/engine/experiment/tor/tor_test.go | 2 +- .../experiment/torsf/integration_test.go | 2 +- .../engine/experiment/torsf/torsf_test.go | 2 +- .../urlgetter/getter_integration_test.go | 2 +- .../experiment/urlgetter/getter_test.go | 2 +- .../engine/experiment/urlgetter/multi_test.go | 2 +- .../experiment/urlgetter/urlgetter_test.go | 2 +- .../webconnectivity/control_test.go | 2 +- .../experiment/whatsapp/whatsapp_test.go | 2 +- .../{internal => }/mockable/mockable.go | 0 .../probeservices/probeservices_test.go | 2 +- internal/tutorial/README.md | 18 ++ internal/tutorial/experiment/torsf/README.md | 20 ++ .../experiment/torsf/chapter01/README.md | 286 +++++++++++++++++ .../experiment/torsf/chapter01/main.go | 287 ++++++++++++++++++ .../experiment/torsf/chapter02/README.md | 204 +++++++++++++ .../experiment/torsf/chapter02/main.go | 65 ++++ .../experiment/torsf/chapter02/torsf.go | 180 +++++++++++ .../experiment/torsf/chapter03/README.md | 166 ++++++++++ .../experiment/torsf/chapter03/main.go | 39 +++ .../experiment/torsf/chapter03/torsf.go | 210 +++++++++++++ .../experiment/torsf/chapter04/README.md | 201 ++++++++++++ .../experiment/torsf/chapter04/main.go | 39 +++ .../experiment/torsf/chapter04/torsf.go | 277 +++++++++++++++++ internal/tutorial/generator/main.go | 95 ++++++ 41 files changed, 2113 insertions(+), 26 deletions(-) rename internal/engine/{internal => }/mockable/mockable.go (100%) create mode 100644 internal/tutorial/README.md create mode 100644 internal/tutorial/experiment/torsf/README.md create mode 100644 internal/tutorial/experiment/torsf/chapter01/README.md create mode 100644 internal/tutorial/experiment/torsf/chapter01/main.go create mode 100644 internal/tutorial/experiment/torsf/chapter02/README.md create mode 100644 internal/tutorial/experiment/torsf/chapter02/main.go create mode 100644 internal/tutorial/experiment/torsf/chapter02/torsf.go create mode 100644 internal/tutorial/experiment/torsf/chapter03/README.md create mode 100644 internal/tutorial/experiment/torsf/chapter03/main.go create mode 100644 internal/tutorial/experiment/torsf/chapter03/torsf.go create mode 100644 internal/tutorial/experiment/torsf/chapter04/README.md create mode 100644 internal/tutorial/experiment/torsf/chapter04/main.go create mode 100644 internal/tutorial/experiment/torsf/chapter04/torsf.go create mode 100644 internal/tutorial/generator/main.go diff --git a/internal/engine/experiment/dash/dash_test.go b/internal/engine/experiment/dash/dash_test.go index 2a83ac2..bad2c81 100644 --- a/internal/engine/experiment/dash/dash_test.go +++ b/internal/engine/experiment/dash/dash_test.go @@ -11,7 +11,7 @@ import ( "github.com/apex/log" "github.com/montanaflynn/stats" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" "github.com/ooni/probe-cli/v3/internal/engine/netx/errorx" "github.com/ooni/probe-cli/v3/internal/engine/netx/trace" diff --git a/internal/engine/experiment/dnscheck/dnscheck_test.go b/internal/engine/experiment/dnscheck/dnscheck_test.go index daec92d..2500882 100644 --- a/internal/engine/experiment/dnscheck/dnscheck_test.go +++ b/internal/engine/experiment/dnscheck/dnscheck_test.go @@ -8,7 +8,7 @@ import ( "time" "github.com/apex/log" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" ) diff --git a/internal/engine/experiment/example/example_test.go b/internal/engine/experiment/example/example_test.go index 5f2e39b..7b60960 100644 --- a/internal/engine/experiment/example/example_test.go +++ b/internal/engine/experiment/example/example_test.go @@ -8,7 +8,7 @@ import ( "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/engine/experiment/example" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" ) diff --git a/internal/engine/experiment/fbmessenger/fbmessenger_test.go b/internal/engine/experiment/fbmessenger/fbmessenger_test.go index 608b809..992d6e3 100644 --- a/internal/engine/experiment/fbmessenger/fbmessenger_test.go +++ b/internal/engine/experiment/fbmessenger/fbmessenger_test.go @@ -9,7 +9,7 @@ import ( engine "github.com/ooni/probe-cli/v3/internal/engine" "github.com/ooni/probe-cli/v3/internal/engine/experiment/fbmessenger" "github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" "github.com/ooni/probe-cli/v3/internal/engine/netx/archival" "github.com/ooni/probe-cli/v3/internal/engine/netx/errorx" diff --git a/internal/engine/experiment/hhfm/hhfm_test.go b/internal/engine/experiment/hhfm/hhfm_test.go index 71f76ef..3915670 100644 --- a/internal/engine/experiment/hhfm/hhfm_test.go +++ b/internal/engine/experiment/hhfm/hhfm_test.go @@ -15,7 +15,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/ooni/probe-cli/v3/internal/engine/experiment/hhfm" "github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" "github.com/ooni/probe-cli/v3/internal/engine/netx/archival" "github.com/ooni/probe-cli/v3/internal/engine/netx/errorx" diff --git a/internal/engine/experiment/hirl/hirl_test.go b/internal/engine/experiment/hirl/hirl_test.go index 47279b9..eda81b0 100644 --- a/internal/engine/experiment/hirl/hirl_test.go +++ b/internal/engine/experiment/hirl/hirl_test.go @@ -8,7 +8,7 @@ import ( "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/engine/experiment/hirl" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" "github.com/ooni/probe-cli/v3/internal/engine/netx" "github.com/ooni/probe-cli/v3/internal/engine/netx/archival" diff --git a/internal/engine/experiment/httphostheader/httphostheader_test.go b/internal/engine/experiment/httphostheader/httphostheader_test.go index 6de991c..6185de6 100644 --- a/internal/engine/experiment/httphostheader/httphostheader_test.go +++ b/internal/engine/experiment/httphostheader/httphostheader_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/apex/log" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" ) diff --git a/internal/engine/experiment/ndt7/ndt7_test.go b/internal/engine/experiment/ndt7/ndt7_test.go index 68e0bb3..232875a 100644 --- a/internal/engine/experiment/ndt7/ndt7_test.go +++ b/internal/engine/experiment/ndt7/ndt7_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/apex/log" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" ) diff --git a/internal/engine/experiment/psiphon/psiphon_test.go b/internal/engine/experiment/psiphon/psiphon_test.go index c33e8ef..de7c8aa 100644 --- a/internal/engine/experiment/psiphon/psiphon_test.go +++ b/internal/engine/experiment/psiphon/psiphon_test.go @@ -11,7 +11,7 @@ import ( "github.com/ooni/probe-cli/v3/internal/atomicx" "github.com/ooni/probe-cli/v3/internal/engine/experiment/psiphon" "github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" ) diff --git a/internal/engine/experiment/riseupvpn/riseupvpn_test.go b/internal/engine/experiment/riseupvpn/riseupvpn_test.go index aa27347..5e9a786 100644 --- a/internal/engine/experiment/riseupvpn/riseupvpn_test.go +++ b/internal/engine/experiment/riseupvpn/riseupvpn_test.go @@ -14,7 +14,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/ooni/probe-cli/v3/internal/engine/experiment/riseupvpn" "github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" "github.com/ooni/probe-cli/v3/internal/engine/netx/archival" "github.com/ooni/probe-cli/v3/internal/engine/netx/errorx" diff --git a/internal/engine/experiment/run/run_test.go b/internal/engine/experiment/run/run_test.go index 88c9f66..886aa79 100644 --- a/internal/engine/experiment/run/run_test.go +++ b/internal/engine/experiment/run/run_test.go @@ -8,7 +8,7 @@ import ( "github.com/ooni/probe-cli/v3/internal/engine/experiment/dnscheck" "github.com/ooni/probe-cli/v3/internal/engine/experiment/run" "github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" ) diff --git a/internal/engine/experiment/signal/signal_test.go b/internal/engine/experiment/signal/signal_test.go index 8fe0d60..211232e 100644 --- a/internal/engine/experiment/signal/signal_test.go +++ b/internal/engine/experiment/signal/signal_test.go @@ -7,7 +7,7 @@ import ( "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/engine/experiment/signal" "github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" "github.com/ooni/probe-cli/v3/internal/engine/netx/errorx" ) diff --git a/internal/engine/experiment/sniblocking/sniblocking_test.go b/internal/engine/experiment/sniblocking/sniblocking_test.go index c3342e2..eb8390c 100644 --- a/internal/engine/experiment/sniblocking/sniblocking_test.go +++ b/internal/engine/experiment/sniblocking/sniblocking_test.go @@ -7,7 +7,7 @@ import ( "time" "github.com/apex/log" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" "github.com/ooni/probe-cli/v3/internal/engine/netx/errorx" ) diff --git a/internal/engine/experiment/stunreachability/stunreachability_test.go b/internal/engine/experiment/stunreachability/stunreachability_test.go index 777bb4f..8ce0c84 100644 --- a/internal/engine/experiment/stunreachability/stunreachability_test.go +++ b/internal/engine/experiment/stunreachability/stunreachability_test.go @@ -10,7 +10,7 @@ import ( "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/engine/experiment/stunreachability" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" "github.com/ooni/probe-cli/v3/internal/engine/netx/errorx" "github.com/pion/stun" diff --git a/internal/engine/experiment/telegram/telegram_test.go b/internal/engine/experiment/telegram/telegram_test.go index 3a83813..3007534 100644 --- a/internal/engine/experiment/telegram/telegram_test.go +++ b/internal/engine/experiment/telegram/telegram_test.go @@ -10,7 +10,7 @@ import ( "github.com/ooni/probe-cli/v3/internal/atomicx" "github.com/ooni/probe-cli/v3/internal/engine/experiment/telegram" "github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" "github.com/ooni/probe-cli/v3/internal/engine/netx/errorx" ) diff --git a/internal/engine/experiment/tlstool/tlstool_test.go b/internal/engine/experiment/tlstool/tlstool_test.go index f71d18a..1256ee1 100644 --- a/internal/engine/experiment/tlstool/tlstool_test.go +++ b/internal/engine/experiment/tlstool/tlstool_test.go @@ -6,7 +6,7 @@ import ( "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/engine/experiment/tlstool" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" ) diff --git a/internal/engine/experiment/tor/tor_test.go b/internal/engine/experiment/tor/tor_test.go index 01eb2da..d62490d 100644 --- a/internal/engine/experiment/tor/tor_test.go +++ b/internal/engine/experiment/tor/tor_test.go @@ -13,9 +13,9 @@ import ( "github.com/apex/log" "github.com/google/go-cmp/cmp" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" "github.com/ooni/probe-cli/v3/internal/engine/legacy/oonidatamodel" "github.com/ooni/probe-cli/v3/internal/engine/legacy/oonitemplates" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" "github.com/ooni/probe-cli/v3/internal/engine/netx/errorx" ) diff --git a/internal/engine/experiment/torsf/integration_test.go b/internal/engine/experiment/torsf/integration_test.go index 8e61349..fe0ca51 100644 --- a/internal/engine/experiment/torsf/integration_test.go +++ b/internal/engine/experiment/torsf/integration_test.go @@ -7,7 +7,7 @@ import ( "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/engine/experiment/torsf" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" "golang.org/x/sys/execabs" ) diff --git a/internal/engine/experiment/torsf/torsf_test.go b/internal/engine/experiment/torsf/torsf_test.go index 4335d80..651062b 100644 --- a/internal/engine/experiment/torsf/torsf_test.go +++ b/internal/engine/experiment/torsf/torsf_test.go @@ -8,7 +8,7 @@ import ( "time" "github.com/apex/log" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" "github.com/ooni/probe-cli/v3/internal/tunnel" ) diff --git a/internal/engine/experiment/urlgetter/getter_integration_test.go b/internal/engine/experiment/urlgetter/getter_integration_test.go index 613a2d1..718c595 100644 --- a/internal/engine/experiment/urlgetter/getter_integration_test.go +++ b/internal/engine/experiment/urlgetter/getter_integration_test.go @@ -9,7 +9,7 @@ import ( "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/netx/errorx" ) diff --git a/internal/engine/experiment/urlgetter/getter_test.go b/internal/engine/experiment/urlgetter/getter_test.go index 5add909..d862db5 100644 --- a/internal/engine/experiment/urlgetter/getter_test.go +++ b/internal/engine/experiment/urlgetter/getter_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/apex/log" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" ) func TestGetterHTTPSWithTunnelCannotCreateTempDir(t *testing.T) { diff --git a/internal/engine/experiment/urlgetter/multi_test.go b/internal/engine/experiment/urlgetter/multi_test.go index 9a45b14..b5c161b 100644 --- a/internal/engine/experiment/urlgetter/multi_test.go +++ b/internal/engine/experiment/urlgetter/multi_test.go @@ -12,7 +12,7 @@ import ( "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" ) diff --git a/internal/engine/experiment/urlgetter/urlgetter_test.go b/internal/engine/experiment/urlgetter/urlgetter_test.go index 60bb2f0..c8897f8 100644 --- a/internal/engine/experiment/urlgetter/urlgetter_test.go +++ b/internal/engine/experiment/urlgetter/urlgetter_test.go @@ -7,7 +7,7 @@ import ( "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" ) diff --git a/internal/engine/experiment/webconnectivity/control_test.go b/internal/engine/experiment/webconnectivity/control_test.go index e33ff61..c166637 100644 --- a/internal/engine/experiment/webconnectivity/control_test.go +++ b/internal/engine/experiment/webconnectivity/control_test.go @@ -5,7 +5,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/ooni/probe-cli/v3/internal/engine/experiment/webconnectivity" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" ) func TestFillASNsEmpty(t *testing.T) { diff --git a/internal/engine/experiment/whatsapp/whatsapp_test.go b/internal/engine/experiment/whatsapp/whatsapp_test.go index abbc544..6831e27 100644 --- a/internal/engine/experiment/whatsapp/whatsapp_test.go +++ b/internal/engine/experiment/whatsapp/whatsapp_test.go @@ -13,7 +13,7 @@ import ( "github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter" "github.com/ooni/probe-cli/v3/internal/engine/experiment/whatsapp" "github.com/ooni/probe-cli/v3/internal/engine/internal/httpfailure" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" ) diff --git a/internal/engine/internal/mockable/mockable.go b/internal/engine/mockable/mockable.go similarity index 100% rename from internal/engine/internal/mockable/mockable.go rename to internal/engine/mockable/mockable.go diff --git a/internal/engine/probeservices/probeservices_test.go b/internal/engine/probeservices/probeservices_test.go index 3ea93d3..ecd7d92 100644 --- a/internal/engine/probeservices/probeservices_test.go +++ b/internal/engine/probeservices/probeservices_test.go @@ -12,7 +12,7 @@ import ( "github.com/apex/log" "github.com/google/go-cmp/cmp" - "github.com/ooni/probe-cli/v3/internal/engine/internal/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/model" "github.com/ooni/probe-cli/v3/internal/engine/probeservices" "github.com/ooni/probe-cli/v3/internal/engine/probeservices/testorchestra" diff --git a/internal/tutorial/README.md b/internal/tutorial/README.md new file mode 100644 index 0000000..22f1070 --- /dev/null +++ b/internal/tutorial/README.md @@ -0,0 +1,18 @@ +# Tutorials: writing OONI nettests + +This package contains a living tutorial explaining how to write OONI +nettests. The code in here is based on existing nettests. + +Because it's committed to the probe-cli repository and depends on +real OONI code, it should always be up to date. + +## Index + +- [Rewriting the torsf experiment](experiment/torsf/) + + +## Regenerating the tutorials + +``` +(cd ./internal/tutorial && go run ./generator) +``` \ No newline at end of file diff --git a/internal/tutorial/experiment/torsf/README.md b/internal/tutorial/experiment/torsf/README.md new file mode 100644 index 0000000..acd898c --- /dev/null +++ b/internal/tutorial/experiment/torsf/README.md @@ -0,0 +1,20 @@ +# Tutorial: rewriting the torsf experiment + +This tutorial teaches you how to write a minimal implementation of the +[torsf](https://github.com/ooni/spec/blob/master/nettests/ts-030-torsf.md) +experiment. We will do that in four steps. + +In the [first step](chapter01/) we will write a `main.go` +function that runs the existing `torsf` implementation. + +In the [second step](chapter02/) we will modify the existing +code to launch an empty experiment instead. + +In the [third step](chapter03/) we will start to fill in +the empty experiment to more closely simulate a real implementation +of the `torsf` experiment. + +In the [fourth step](chapter04/) we will replace the code +simulating a real `torsf` experiment with a minimal implementation +of such an experiment that uses other code in `ooni/probe-cli` to +attempt to bootstrap `tor` over Snowflake. diff --git a/internal/tutorial/experiment/torsf/chapter01/README.md b/internal/tutorial/experiment/torsf/chapter01/README.md new file mode 100644 index 0000000..4fd8a78 --- /dev/null +++ b/internal/tutorial/experiment/torsf/chapter01/README.md @@ -0,0 +1,286 @@ + +# Chapter I: main.go using the real torsf implementation + +In this chapter we will write together a `main.go` file that +uses the real `torsf` implementation to run the experiment. + +(This file is auto-generated from the corresponding source file, +so make sure you don't edit it manually.) + +## The torsf experiment + +This experiment attempts to bootstrap the `tor` binary using +Snowflake as the pluggable transport. + +You can read the [specification](https://github.com/ooni/spec/blob/master/nettests/ts-030-torsf.md) +of the `torsf` experiment in the [ooni/spec](https://github.com/ooni/spec) +repository. (The `ooni/spec` repository is the repository +containing the specification of all OONI nettests, as well +as of the data formats used by OONI.) + +## The main.go file + +We define `main.go` file using `package main`. + +```Go +package main + +``` + +### Imports + +Then we add the required imports. + +```Go +import ( +``` + +These are standard library imports. + +```Go + "context" + "encoding/json" + "fmt" + "io/ioutil" + +``` + +The apex/log library is the logging library used by OONI Probe. + +```Go + "github.com/apex/log" + +``` + +The torsf package contains the implementation of the torsf experiment. + +```Go + "github.com/ooni/probe-cli/v3/internal/engine/experiment/torsf" + +``` + +The mockable package contains widely used mocks. + +```Go + "github.com/ooni/probe-cli/v3/internal/engine/mockable" + +``` + +The model package contains the data model used by OONI experiments. + +```Go + "github.com/ooni/probe-cli/v3/internal/engine/model" + +``` + +We will need the execabs library to check whether there is +a binary called `tor` in the `PATH`. + +```Go + "golang.org/x/sys/execabs" +) + +``` + +### Main function + +Finally, here's the code of the `main function`. + +```Go +func main() { +``` + +We start by checking whether there is an executable named `"tor"` in +the `PATH`. If there is no such executable, we fail with an error. + +```Go + if _, err := execabs.LookPath("tor"); err != nil { + log.Fatal("cannot find the tor executable in path") + } +``` + +Then, we create a temporary directory to hold any state that may be +required either by the `tor` or by the Snowflake pluggable transport. + +```Go + tempdir, err := ioutil.TempDir("", "") + if err != nil { + log.WithError(err).Fatal("cannot create temporary directory") + } +``` + +### Creating the experiment measurer + +All OONI experiments implement a function called +`NewExprimentMeasurer` that allows you to make +an `ExperimentMeasurer` instance. The `ExperimentMeasurer` +is an `interface` defined by the `model` package we +imported above. Because we don't want to configure +any setting (and the experiment does not support any +setting anyway), here we're passing to the +`NewExperimentMeasurer` factory an empty `Config`. + +```Go + m := torsf.NewExperimentMeasurer(torsf.Config{}) +``` + +### Creating the measurement + +Next, we create an empty `Measurement`. OONI measurements +are JSON data structures that contain generic fields common +to all OONI experiments and experiment-specific data. The +experiment-specific data is contained by a the `test_keys` +field of the `Measurement`. + +In the *real* OONI implementation, there is common code +that fills the several fields of a `Measurement`. For +example, it will fill the country code and the autonomous +system number of the network in which the OONI Probe is +running. Because this is just an example to illustrate +how to write experiments, we will not bother with doing +that. Instead, we will pass to the experiment just an +emtpy measurement where no field has been set. + +```Go + measurement := &model.Measurement{} +``` + +### Creating the callbacks + +Then, we create an instance of the experiment callbacks. The +experiment callbacks historically groups a set of callbacks +called when the measurer is running. At the moment of writing +this note, the `model.ExperimentCallbacks` contains just a +single method called `OnDataUsage`, which is used to tell the +caller which is the amount of data used by the experiment. + +Because this is an example for illustrative purposes, here +we construct an implementation of `ExperimentCallbacks` that +just prints the data usage using the `log.Log` logger. + +```Go + callbacks := model.NewPrinterCallbacks(log.Log) +``` + +### Creating a session + +The `ExperimentMeasurer` also wants a `Session`. In normal +OONI code, the `Session` is a data structure containing +information regarding the current measurement session. Since +this is just an illustrative example, rather than creating +a real `Session` instance, we use much-simpler mock. + +The interface required by a `Session` is called +`ExperimentSession` and is part of the `model` package. + +Here we configure this mockable session to use `log.Log` +as a logger and the previously computed temp dir. + +```Go + sess := &mockable.Session{ + MockableLogger: log.Log, + MockableTempDir: tempdir, + } +``` + +# Running the experiment + +At last, it's time to run the experiment using all the +previously constructed data structures. The `Run` function +is the main function you need to implement when you are +defining a new OONI experiment. + +By convention, the `Run` function only returns an error +when some precondition required by the experiment is +not met. Say that, for example, the experiment needs a +port listening on the local host. If we cannot create +such a port, we will return an error to the caller. + +For network errors, instead, we return nil. Consider the +case where we connect to a remote host and the connection +fails. This is not really an error, rather it's a result +that we will include into the measurement. + +Apart from the other arguments that we discussed previously, +the `Run` function also wants a `context.Context` as its +first argument. The context is used to interrupt long running +functions early, and our code (mostly) honours contexts. + +Since here we are just writing a simple example, we don't +need any fancy context and we pass a `context.Background` to `Run`. + +```Go + ctx := context.Background() + if err = m.Run(ctx, sess, measurement, callbacks); err != nil { + log.WithError(err).Fatal("torsf experiment failed") + } +``` + +### Printing the measurement result + +The `Run` function modifies the `TestKeys` (`test_keys` in JSON) +field of the measurement. The real OONI implementation would +now submit this measurement. Because this is an illustrative example, +we will just pretty-print the measurement on the `stdout`. + +```Go + data, err := json.Marshal(measurement) + if err != nil { + log.WithError(err).Fatal("json.Marshal failed") + } + fmt.Printf("%s\n", data) +} + +``` + +## Running the code + +You can now run this code as follows: + +``` +$ go run ./experiment/torsf/chapter01 | jq +[snip] +{ + "data_format_version": "", + "input": null, + "measurement_start_time": "", + "probe_asn": "", + "probe_cc": "", + "probe_network_name": "", + "report_id": "", + "resolver_asn": "", + "resolver_ip": "", + "resolver_network_name": "", + "software_name": "", + "software_version": "", + "test_keys": { + "bootstrap_time": 68.909067459, + "failure": null + }, + "test_name": "", + "test_runtime": 0, + "test_start_time": "", + "test_version": "" +} +``` + +We have snipped through logs and we have used `jq` to +pretty print the measurement. You see that all the fields +except the `test_keys` are empty. + +Let us now analyze the content of the `test_keys`: + +- the `bootstrap_time` field contains the time (in seconds) to +bootstrap `tor` using the Snowflake transport; + +- the `failure` field contains the error that occurred, if +any, or `null` if no error occurred. + +## Concluding remarks + +This is all you need to know in terms of minimal code for +running an OONI experiment. In the remainder of this tutorial, +we will show how to reimplement the `torsf` experiment. + +Apart from minor changes, the `main.go` file would basically +not change for the remainder of this tutorial. diff --git a/internal/tutorial/experiment/torsf/chapter01/main.go b/internal/tutorial/experiment/torsf/chapter01/main.go new file mode 100644 index 0000000..3f34c57 --- /dev/null +++ b/internal/tutorial/experiment/torsf/chapter01/main.go @@ -0,0 +1,287 @@ +// -=-=- StartHere -=-=- +// +// # Chapter I: main.go using the real torsf implementation +// +// In this chapter we will write together a `main.go` file that +// uses the real `torsf` implementation to run the experiment. +// +// (This file is auto-generated from the corresponding source file, +// so make sure you don't edit it manually.) +// +// ## The torsf experiment +// +// This experiment attempts to bootstrap the `tor` binary using +// Snowflake as the pluggable transport. +// +// You can read the [specification](https://github.com/ooni/spec/blob/master/nettests/ts-030-torsf.md) +// of the `torsf` experiment in the [ooni/spec](https://github.com/ooni/spec) +// repository. (The `ooni/spec` repository is the repository +// containing the specification of all OONI nettests, as well +// as of the data formats used by OONI.) +// +// ## The main.go file +// +// We define `main.go` file using `package main`. +// +// ```Go +package main + +// ``` +// +// ### Imports +// +// Then we add the required imports. +// +// ```Go +import ( + // ``` + // + // These are standard library imports. + // + // ```Go + "context" + "encoding/json" + "fmt" + "io/ioutil" + + // ``` + // + // The apex/log library is the logging library used by OONI Probe. + // + // ```Go + "github.com/apex/log" + + // ``` + // + // The torsf package contains the implementation of the torsf experiment. + // + // ```Go + "github.com/ooni/probe-cli/v3/internal/engine/experiment/torsf" + + // ``` + // + // The mockable package contains widely used mocks. + // + // ```Go + "github.com/ooni/probe-cli/v3/internal/engine/mockable" + + // ``` + // + // The model package contains the data model used by OONI experiments. + // + // ```Go + "github.com/ooni/probe-cli/v3/internal/engine/model" + + // ``` + // + // We will need the execabs library to check whether there is + // a binary called `tor` in the `PATH`. + // + // ```Go + "golang.org/x/sys/execabs" +) + +// ``` +// +// ### Main function +// +// Finally, here's the code of the `main function`. +// +// ```Go +func main() { + // ``` + // + // We start by checking whether there is an executable named `"tor"` in + // the `PATH`. If there is no such executable, we fail with an error. + // + // ```Go + if _, err := execabs.LookPath("tor"); err != nil { + log.Fatal("cannot find the tor executable in path") + } + // ``` + // + // Then, we create a temporary directory to hold any state that may be + // required either by the `tor` or by the Snowflake pluggable transport. + // + // ```Go + tempdir, err := ioutil.TempDir("", "") + if err != nil { + log.WithError(err).Fatal("cannot create temporary directory") + } + // ``` + // + // ### Creating the experiment measurer + // + // All OONI experiments implement a function called + // `NewExprimentMeasurer` that allows you to make + // an `ExperimentMeasurer` instance. The `ExperimentMeasurer` + // is an `interface` defined by the `model` package we + // imported above. Because we don't want to configure + // any setting (and the experiment does not support any + // setting anyway), here we're passing to the + // `NewExperimentMeasurer` factory an empty `Config`. + // + // ```Go + m := torsf.NewExperimentMeasurer(torsf.Config{}) + // ``` + // + // ### Creating the measurement + // + // Next, we create an empty `Measurement`. OONI measurements + // are JSON data structures that contain generic fields common + // to all OONI experiments and experiment-specific data. The + // experiment-specific data is contained by a the `test_keys` + // field of the `Measurement`. + // + // In the *real* OONI implementation, there is common code + // that fills the several fields of a `Measurement`. For + // example, it will fill the country code and the autonomous + // system number of the network in which the OONI Probe is + // running. Because this is just an example to illustrate + // how to write experiments, we will not bother with doing + // that. Instead, we will pass to the experiment just an + // emtpy measurement where no field has been set. + // + // ```Go + measurement := &model.Measurement{} + // ``` + // + // ### Creating the callbacks + // + // Then, we create an instance of the experiment callbacks. The + // experiment callbacks historically groups a set of callbacks + // called when the measurer is running. At the moment of writing + // this note, the `model.ExperimentCallbacks` contains just a + // single method called `OnDataUsage`, which is used to tell the + // caller which is the amount of data used by the experiment. + // + // Because this is an example for illustrative purposes, here + // we construct an implementation of `ExperimentCallbacks` that + // just prints the data usage using the `log.Log` logger. + // + // ```Go + callbacks := model.NewPrinterCallbacks(log.Log) + // ``` + // + // ### Creating a session + // + // The `ExperimentMeasurer` also wants a `Session`. In normal + // OONI code, the `Session` is a data structure containing + // information regarding the current measurement session. Since + // this is just an illustrative example, rather than creating + // a real `Session` instance, we use much-simpler mock. + // + // The interface required by a `Session` is called + // `ExperimentSession` and is part of the `model` package. + // + // Here we configure this mockable session to use `log.Log` + // as a logger and the previously computed temp dir. + // + // ```Go + sess := &mockable.Session{ + MockableLogger: log.Log, + MockableTempDir: tempdir, + } + // ``` + // + // # Running the experiment + // + // At last, it's time to run the experiment using all the + // previously constructed data structures. The `Run` function + // is the main function you need to implement when you are + // defining a new OONI experiment. + // + // By convention, the `Run` function only returns an error + // when some precondition required by the experiment is + // not met. Say that, for example, the experiment needs a + // port listening on the local host. If we cannot create + // such a port, we will return an error to the caller. + // + // For network errors, instead, we return nil. Consider the + // case where we connect to a remote host and the connection + // fails. This is not really an error, rather it's a result + // that we will include into the measurement. + // + // Apart from the other arguments that we discussed previously, + // the `Run` function also wants a `context.Context` as its + // first argument. The context is used to interrupt long running + // functions early, and our code (mostly) honours contexts. + // + // Since here we are just writing a simple example, we don't + // need any fancy context and we pass a `context.Background` to `Run`. + // + // ```Go + ctx := context.Background() + if err = m.Run(ctx, sess, measurement, callbacks); err != nil { + log.WithError(err).Fatal("torsf experiment failed") + } + // ``` + // + // ### Printing the measurement result + // + // The `Run` function modifies the `TestKeys` (`test_keys` in JSON) + // field of the measurement. The real OONI implementation would + // now submit this measurement. Because this is an illustrative example, + // we will just pretty-print the measurement on the `stdout`. + // + // ```Go + data, err := json.Marshal(measurement) + if err != nil { + log.WithError(err).Fatal("json.Marshal failed") + } + fmt.Printf("%s\n", data) +} + +// ``` +// +// ## Running the code +// +// You can now run this code as follows: +// +// ``` +// $ go run ./experiment/torsf/chapter01 | jq +// [snip] +// { +// "data_format_version": "", +// "input": null, +// "measurement_start_time": "", +// "probe_asn": "", +// "probe_cc": "", +// "probe_network_name": "", +// "report_id": "", +// "resolver_asn": "", +// "resolver_ip": "", +// "resolver_network_name": "", +// "software_name": "", +// "software_version": "", +// "test_keys": { +// "bootstrap_time": 68.909067459, +// "failure": null +// }, +// "test_name": "", +// "test_runtime": 0, +// "test_start_time": "", +// "test_version": "" +//} +// ``` +// +// We have snipped through logs and we have used `jq` to +// pretty print the measurement. You see that all the fields +// except the `test_keys` are empty. +// +// Let us now analyze the content of the `test_keys`: +// +// - the `bootstrap_time` field contains the time (in seconds) to +// bootstrap `tor` using the Snowflake transport; +// +// - the `failure` field contains the error that occurred, if +// any, or `null` if no error occurred. +// +// ## Concluding remarks +// +// This is all you need to know in terms of minimal code for +// running an OONI experiment. In the remainder of this tutorial, +// we will show how to reimplement the `torsf` experiment. +// +// Apart from minor changes, the `main.go` file would basically +// not change for the remainder of this tutorial. diff --git a/internal/tutorial/experiment/torsf/chapter02/README.md b/internal/tutorial/experiment/torsf/chapter02/README.md new file mode 100644 index 0000000..6ca8ce6 --- /dev/null +++ b/internal/tutorial/experiment/torsf/chapter02/README.md @@ -0,0 +1,204 @@ + +# Chapter II: creating an empty experiment + +In this chapter we will create an empty experiment and replace +the code calling the real `torsf` experiment in `main.go` to +call our empty experiment instead. + +(This file is auto-generated from the corresponding source file, +so make sure you don't edit it manually.) + +## Changes in main.go + +In `main.go` we will simply replace the call to the +`torsf.NewExperimentMeasurer` function with a call to +a `NewExperimentMeasurer` function that we are going +to implement as part of this chapter. + +After you do this, you also need to remove the now-unneded +import of the `torsf` package. + +There are no additional changes to `main.go`. + +```Go + m := NewExperimentMeasurer(Config{}) +``` + +## The torsf.go file + +This file will contain the implementation of the +`NewExperimentMeasurer` function. + +As usual we start with the `package` declaration and +with the few imports we need to add. + +```Go +package main + +import ( + "context" + "time" + + "github.com/ooni/probe-cli/v3/internal/engine/model" +) + +``` + +### Data structures + +Next, we define data structures. + +Config contains config for the torsf experiment. As for the real +`torsf` experiment, we don't have any specific config, so we keep +the structure empty. We still need to define a `Config` struct +here, because, by convention, all OONI experiments have a `Config`. + +```Go +type Config struct{} + +``` + +Measurer is the torsf measurer. This structure implements the +`model.ExperimentMeasurer` interface, as we will see below. + +Most OONI experiments have a measurer that contains as its unique +field the specific configuration. Here we do the same. + +```Go +type Measurer struct { + config Config +} + +``` +NewExperimentMeasurer creates a new model.ExperimentMeasurer +instance for performing `torsf` measurements. This function +will just assemble a new instance of `Measurer` with the `config` +that was passed as an argument. + +```Go +func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { + return &Measurer{config: config} +} + +``` + +### Implementing the model.ExperimentMeasurer. + +Now it's time to implement the methods required by the `model`'s +`ExperimentMeasurer` interface. + +ExperimentName implements ExperimentMeasurer.ExperimentName. This function +returns the name of the experiment. This code is used by generic code +manipulating the experiment to print the experiment name. + +```Go +func (m *Measurer) ExperimentName() string { + return "torsf" +} + +``` + +ExperimentVersion implements ExperimentMeasurer.ExperimentVersion. This +function returns the version of the experiment. This code is also used by +generic code manipulating the experiment to print the experiment version. + +```Go +func (m *Measurer) ExperimentVersion() string { + return "0.1.0" +} + +``` + +Run implements ExperimentMeasurer.Run. This is the most interesting +function, where we run the experiment proper. In the previous chapter +we learned how to call this function from a `main.go` file. Here, +instead, we're going to create a minimal stub. In the subsequent +chapters, finally, we will modify this function until it is a +minimal implementation of the `torsf` experiment. + +```Go +func (m *Measurer) Run( + ctx context.Context, sess model.ExperimentSession, + measurement *model.Measurement, callbacks model.ExperimentCallbacks, +) error { +``` +As you can see, this is just a stub implementation that sleeps +for one second and prints a logging message. + +```Go + time.Sleep(time.Second) + sess.Logger().Info("hello from the torsf experiment!") + return nil +} + +``` +### Summary keys + +Before concluding this chapter, we also need to create the `SummaryKeys` +for this experiment. For historical reasons, the `TestKeys` of each +experiment is an `interface{}`. Every experiment also defines a `SummaryKeys` +data structure and a `GetSummaryKeys` method to convert the opaque +result of a measurement to the summary for such an experiment. + +The experiment summary is *only* used by the OONI Probe CLI. + +SummaryKeys contains summary keys for this experiment. Because this is +just an illustrative tutorial, we will just include a single key, named +`IsAnomaly`. This key is not exported as JSON and is used by the OONI +Probe CLI to inform the user of whether this measurement is ordinary or +anomalous. All OONI experiments' `SummaryKeys` contain such a field. + +```Go +type SummaryKeys struct { + IsAnomaly bool `json:"-"` +} + +``` + +GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys. This +method just converts the `TestKeys` inside `measurement` to an instance of +the `SummaryKeys` structure. For now, we'll just implement a stub returning +fake `SummaryKeys` declaring there was no anomaly. + +```Go +func (m *Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) { + return &SummaryKeys{IsAnomaly: false}, nil +} + +``` + +## Running the code + +We can run the code written in this chapter as follows: + +``` +$ go run ./experiment/torsf/chapter02 +2021/06/21 20:48:32 info hello from the torsf experiment! +{ + "data_format_version": "", + "input": null, + "measurement_start_time": "", + "probe_asn": "", + "probe_cc": "", + "probe_network_name": "", + "report_id": "", + "resolver_asn": "", + "resolver_ip": "", + "resolver_network_name": "", + "software_name": "", + "software_version": "", + "test_keys": null, + "test_name": "", + "test_runtime": 0, + "test_start_time": "", + "test_version": "" +} +``` + +Here you see that we're printing the log message and +that the `test_keys` are `null`. + +The OONI data processing popeline will not be so happy +if we pass it a `null` settings, because there is not +much interesting data in there. We will thus start filling +it in the next chapter. diff --git a/internal/tutorial/experiment/torsf/chapter02/main.go b/internal/tutorial/experiment/torsf/chapter02/main.go new file mode 100644 index 0000000..d4e612b --- /dev/null +++ b/internal/tutorial/experiment/torsf/chapter02/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + + "github.com/apex/log" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/model" + "golang.org/x/sys/execabs" +) + +func main() { + if _, err := execabs.LookPath("tor"); err != nil { + log.Fatal("cannot find the tor executable in path") + } + tempdir, err := ioutil.TempDir("", "") + if err != nil { + log.WithError(err).Fatal("cannot create temporary directory") + } + // -=-=- StartHere -=-=- + // + // # Chapter II: creating an empty experiment + // + // In this chapter we will create an empty experiment and replace + // the code calling the real `torsf` experiment in `main.go` to + // call our empty experiment instead. + // + // (This file is auto-generated from the corresponding source file, + // so make sure you don't edit it manually.) + // + // ## Changes in main.go + // + // In `main.go` we will simply replace the call to the + // `torsf.NewExperimentMeasurer` function with a call to + // a `NewExperimentMeasurer` function that we are going + // to implement as part of this chapter. + // + // After you do this, you also need to remove the now-unneded + // import of the `torsf` package. + // + // There are no additional changes to `main.go`. + // + // ```Go + m := NewExperimentMeasurer(Config{}) + // ``` + // -=-=- StopHere -=-=- + ctx := context.Background() + measurement := &model.Measurement{} + callbacks := model.NewPrinterCallbacks(log.Log) + sess := &mockable.Session{ + MockableLogger: log.Log, + MockableTempDir: tempdir, + } + if err = m.Run(ctx, sess, measurement, callbacks); err != nil { + log.WithError(err).Fatal("torsf experiment failed") + } + data, err := json.Marshal(measurement) + if err != nil { + log.WithError(err).Fatal("json.Marshal failed") + } + fmt.Printf("%s\n", data) +} diff --git a/internal/tutorial/experiment/torsf/chapter02/torsf.go b/internal/tutorial/experiment/torsf/chapter02/torsf.go new file mode 100644 index 0000000..298ec03 --- /dev/null +++ b/internal/tutorial/experiment/torsf/chapter02/torsf.go @@ -0,0 +1,180 @@ +// -=-=- StartHere -=-=- +// +// ## The torsf.go file +// +// This file will contain the implementation of the +// `NewExperimentMeasurer` function. +// +// As usual we start with the `package` declaration and +// with the few imports we need to add. +// +// ```Go +package main + +import ( + "context" + "time" + + "github.com/ooni/probe-cli/v3/internal/engine/model" +) + +// ``` +// +// ### Data structures +// +// Next, we define data structures. + +// Config contains config for the torsf experiment. As for the real +// `torsf` experiment, we don't have any specific config, so we keep +// the structure empty. We still need to define a `Config` struct +// here, because, by convention, all OONI experiments have a `Config`. +// +// ```Go +type Config struct{} + +// ``` +// +// Measurer is the torsf measurer. This structure implements the +// `model.ExperimentMeasurer` interface, as we will see below. +// +// Most OONI experiments have a measurer that contains as its unique +// field the specific configuration. Here we do the same. +// +// ```Go +type Measurer struct { + config Config +} + +// ``` +// NewExperimentMeasurer creates a new model.ExperimentMeasurer +// instance for performing `torsf` measurements. This function +// will just assemble a new instance of `Measurer` with the `config` +// that was passed as an argument. +// +// ```Go +func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { + return &Measurer{config: config} +} + +// ``` +// +// ### Implementing the model.ExperimentMeasurer. +// +// Now it's time to implement the methods required by the `model`'s +// `ExperimentMeasurer` interface. + +// ExperimentName implements ExperimentMeasurer.ExperimentName. This function +// returns the name of the experiment. This code is used by generic code +// manipulating the experiment to print the experiment name. +// +// ```Go +func (m *Measurer) ExperimentName() string { + return "torsf" +} + +// ``` +// +// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion. This +// function returns the version of the experiment. This code is also used by +// generic code manipulating the experiment to print the experiment version. +// +// ```Go +func (m *Measurer) ExperimentVersion() string { + return "0.1.0" +} + +// ``` +// +// Run implements ExperimentMeasurer.Run. This is the most interesting +// function, where we run the experiment proper. In the previous chapter +// we learned how to call this function from a `main.go` file. Here, +// instead, we're going to create a minimal stub. In the subsequent +// chapters, finally, we will modify this function until it is a +// minimal implementation of the `torsf` experiment. +// +// ```Go +func (m *Measurer) Run( + ctx context.Context, sess model.ExperimentSession, + measurement *model.Measurement, callbacks model.ExperimentCallbacks, +) error { + // ``` + // As you can see, this is just a stub implementation that sleeps + // for one second and prints a logging message. + // + // ```Go + time.Sleep(time.Second) + sess.Logger().Info("hello from the torsf experiment!") + return nil +} + +// ``` +// ### Summary keys +// +// Before concluding this chapter, we also need to create the `SummaryKeys` +// for this experiment. For historical reasons, the `TestKeys` of each +// experiment is an `interface{}`. Every experiment also defines a `SummaryKeys` +// data structure and a `GetSummaryKeys` method to convert the opaque +// result of a measurement to the summary for such an experiment. +// +// The experiment summary is *only* used by the OONI Probe CLI. + +// SummaryKeys contains summary keys for this experiment. Because this is +// just an illustrative tutorial, we will just include a single key, named +// `IsAnomaly`. This key is not exported as JSON and is used by the OONI +// Probe CLI to inform the user of whether this measurement is ordinary or +// anomalous. All OONI experiments' `SummaryKeys` contain such a field. +// +// ```Go +type SummaryKeys struct { + IsAnomaly bool `json:"-"` +} + +// ``` +// +// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys. This +// method just converts the `TestKeys` inside `measurement` to an instance of +// the `SummaryKeys` structure. For now, we'll just implement a stub returning +// fake `SummaryKeys` declaring there was no anomaly. +// +// ```Go +func (m *Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) { + return &SummaryKeys{IsAnomaly: false}, nil +} + +// ``` +// +// ## Running the code +// +// We can run the code written in this chapter as follows: +// +// ``` +// $ go run ./experiment/torsf/chapter02 +// 2021/06/21 20:48:32 info hello from the torsf experiment! +// { +// "data_format_version": "", +// "input": null, +// "measurement_start_time": "", +// "probe_asn": "", +// "probe_cc": "", +// "probe_network_name": "", +// "report_id": "", +// "resolver_asn": "", +// "resolver_ip": "", +// "resolver_network_name": "", +// "software_name": "", +// "software_version": "", +// "test_keys": null, +// "test_name": "", +// "test_runtime": 0, +// "test_start_time": "", +// "test_version": "" +// } +// ``` +// +// Here you see that we're printing the log message and +// that the `test_keys` are `null`. +// +// The OONI data processing popeline will not be so happy +// if we pass it a `null` settings, because there is not +// much interesting data in there. We will thus start filling +// it in the next chapter. diff --git a/internal/tutorial/experiment/torsf/chapter03/README.md b/internal/tutorial/experiment/torsf/chapter03/README.md new file mode 100644 index 0000000..b6b9f85 --- /dev/null +++ b/internal/tutorial/experiment/torsf/chapter03/README.md @@ -0,0 +1,166 @@ + +# Chapter III: starting to simulate a real torsf experiment + +In this chapter we will improve upon what we did in the previous +chapter by creating runner code for the `torsf` experiment. We will +not, yet, run the real experiment, but we will instead write +simple code that pretends to run a `tor` bootstrap using snowflake. + +(This file is auto-generated from the corresponding source file, +so make sure you don't edit it manually.) + +### The TestKeys structure + +Let us start by defining the `TestKeys` structure that contains +the experiment specific results. As we have already seen in +Chapter I, this structure must contain two fields. The bootstrap +time for the experiment and the failure. + +```Go +type TestKeys struct { + BootstrapTime float64 `json:"bootstrap_time"` + Failure *string `json:"failure"` +} + +``` + +### Rewriting the Run method + +Next we will rewrite the Run method. We will arrange for this +method to fill the `measurement`, to setup the timeout, and to +print periodic updates via the `callbacks`. We will defer the +real work to a private function called `run`. + +```Go +func (m *Measurer) Run( + ctx context.Context, sess model.ExperimentSession, + measurement *model.Measurement, callbacks model.ExperimentCallbacks, +) error { +``` + +Let's create an instance of `TestKeys` and let's modify +the `measurement` to refer to such an instance. + +```Go + testkeys := &TestKeys{} + measurement.TestKeys = testkeys +``` + +Next, we record the current time and we modify the +context to have a timeout after 300 seconds. Because +Snowflake *may* take a long time to bootstrap, we +need to specify a generous timeout here. + +```Go + start := time.Now() + const maxRuntime = 300 * time.Second + ctx, cancel := context.WithTimeout(ctx, maxRuntime) + defer cancel() +``` + +Okay, now we are ready to defer the real work to +the internal `run` function. We first create a +channel to receive the result of `run`. Then, we +create a ticker to emit periodic updates. We +emit an update every 250 milliseconds, which is +a reasonably smooth way of increasing a progress +bar (progress is indeed used to move progress bars +both in OONI Probe Desktop and mobile.) + +```Go + errch := make(chan error) + ticker := time.NewTicker(250 * time.Millisecond) + defer ticker.Stop() +``` + +Now we defer the real work to `run`, which will +run in a background goroutine. + +```Go + go m.run(ctx, sess, testkeys, errch) +``` + +While `run` is running, we loop and check which +channel has become ready. + +If the `errch` channel is ready, it means that `run` is +terminated, so we return to the caller. + +Instead, if `ticker.C` is ready, we emit a progress +update using the `callbacks`. + +```Go + for { + select { + case err := <-errch: + callbacks.OnProgress(1.0, "torsf experiment is finished") + return err + case <-ticker.C: + progress := time.Since(start).Seconds() / maxRuntime.Seconds() + callbacks.OnProgress(progress, "torsf experiment is running") + } + } +} + +``` + +### The run function + +We will now implement the `run` function. For now, this function +will not do any real work, but it will just pretend to do work. + +Note how we sleep for some time, set the `BootstrapTime` field +of the `TestKeys`, and then return using `errch`. + +```Go +func (m *Measurer) run(ctx context.Context, + sess model.ExperimentSession, testkeys *TestKeys, errch chan<- error) { + fakeBootstrapTime := 10 * time.Second + time.Sleep(fakeBootstrapTime) + testkeys.BootstrapTime = fakeBootstrapTime.Seconds() + errch <- nil +} + +``` + +## Running the code + +It's now time to run the new code we've written: + +``` +$ go run ./experiment/torsf/chapter03 | jq +2021/06/21 21:21:18 info [ 0.1%] torsf experiment is running +2021/06/21 21:21:19 info [ 0.2%] torsf experiment is running +[...] +2021/06/21 21:21:28 info [ 3.3%] torsf experiment is running +2021/06/21 21:21:28 info [100.0%] torsf experiment is finished +{ + "data_format_version": "", + "input": null, + "measurement_start_time": "", + "probe_asn": "", + "probe_cc": "", + "probe_network_name": "", + "report_id": "", + "resolver_asn": "", + "resolver_ip": "", + "resolver_network_name": "", + "software_name": "", + "software_version": "", + "test_keys": { + "bootstrap_time": 10, + "failure": null + }, + "test_name": "", + "test_runtime": 0, + "test_start_time": "", + "test_version": "" +} +``` + +You see that now we're filling the bootstrap time and we're +also printing progress using `callbacks`. + +In the next chapter, we'll replace the stub `run` implementation +with a real implementation using Snowflake. + diff --git a/internal/tutorial/experiment/torsf/chapter03/main.go b/internal/tutorial/experiment/torsf/chapter03/main.go new file mode 100644 index 0000000..ae53ccf --- /dev/null +++ b/internal/tutorial/experiment/torsf/chapter03/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + + "github.com/apex/log" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/model" + "golang.org/x/sys/execabs" +) + +func main() { + if _, err := execabs.LookPath("tor"); err != nil { + log.Fatal("cannot find the tor executable in path") + } + tempdir, err := ioutil.TempDir("", "") + if err != nil { + log.WithError(err).Fatal("cannot create temporary directory") + } + m := NewExperimentMeasurer(Config{}) + ctx := context.Background() + measurement := &model.Measurement{} + callbacks := model.NewPrinterCallbacks(log.Log) + sess := &mockable.Session{ + MockableLogger: log.Log, + MockableTempDir: tempdir, + } + if err = m.Run(ctx, sess, measurement, callbacks); err != nil { + log.WithError(err).Fatal("torsf experiment failed") + } + data, err := json.Marshal(measurement) + if err != nil { + log.WithError(err).Fatal("json.Marshal failed") + } + fmt.Printf("%s\n", data) +} diff --git a/internal/tutorial/experiment/torsf/chapter03/torsf.go b/internal/tutorial/experiment/torsf/chapter03/torsf.go new file mode 100644 index 0000000..1b9c611 --- /dev/null +++ b/internal/tutorial/experiment/torsf/chapter03/torsf.go @@ -0,0 +1,210 @@ +package main + +import ( + "context" + "time" + + "github.com/ooni/probe-cli/v3/internal/engine/model" +) + +// Config contains config for the torsf experiment. +type Config struct{} + +// Measurer is the torsf measurer. +type Measurer struct { + config Config +} + +// newExperimentMeasurer creates a new ExperimentMeasurer for torsf. +func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { + return &Measurer{config: config} +} + +// ExperimentName implements ExperimentMeasurer.ExperimentName. +func (m *Measurer) ExperimentName() string { + return "torsf" +} + +// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion. +func (m *Measurer) ExperimentVersion() string { + return "0.1.0" +} + +// -=-=- StartHere -=-=- +// +// # Chapter III: starting to simulate a real torsf experiment +// +// In this chapter we will improve upon what we did in the previous +// chapter by creating runner code for the `torsf` experiment. We will +// not, yet, run the real experiment, but we will instead write +// simple code that pretends to run a `tor` bootstrap using snowflake. +// +// (This file is auto-generated from the corresponding source file, +// so make sure you don't edit it manually.) +// +// ### The TestKeys structure +// +// Let us start by defining the `TestKeys` structure that contains +// the experiment specific results. As we have already seen in +// Chapter I, this structure must contain two fields. The bootstrap +// time for the experiment and the failure. +// +// ```Go +type TestKeys struct { + BootstrapTime float64 `json:"bootstrap_time"` + Failure *string `json:"failure"` +} + +// ``` +// +// ### Rewriting the Run method +// +// Next we will rewrite the Run method. We will arrange for this +// method to fill the `measurement`, to setup the timeout, and to +// print periodic updates via the `callbacks`. We will defer the +// real work to a private function called `run`. +// +// ```Go +func (m *Measurer) Run( + ctx context.Context, sess model.ExperimentSession, + measurement *model.Measurement, callbacks model.ExperimentCallbacks, +) error { + // ``` + // + // Let's create an instance of `TestKeys` and let's modify + // the `measurement` to refer to such an instance. + // + // ```Go + testkeys := &TestKeys{} + measurement.TestKeys = testkeys + // ``` + // + // Next, we record the current time and we modify the + // context to have a timeout after 300 seconds. Because + // Snowflake *may* take a long time to bootstrap, we + // need to specify a generous timeout here. + // + // ```Go + start := time.Now() + const maxRuntime = 300 * time.Second + ctx, cancel := context.WithTimeout(ctx, maxRuntime) + defer cancel() + // ``` + // + // Okay, now we are ready to defer the real work to + // the internal `run` function. We first create a + // channel to receive the result of `run`. Then, we + // create a ticker to emit periodic updates. We + // emit an update every 250 milliseconds, which is + // a reasonably smooth way of increasing a progress + // bar (progress is indeed used to move progress bars + // both in OONI Probe Desktop and mobile.) + // + // ```Go + errch := make(chan error) + ticker := time.NewTicker(250 * time.Millisecond) + defer ticker.Stop() + // ``` + // + // Now we defer the real work to `run`, which will + // run in a background goroutine. + // + // ```Go + go m.run(ctx, sess, testkeys, errch) + // ``` + // + // While `run` is running, we loop and check which + // channel has become ready. + // + // If the `errch` channel is ready, it means that `run` is + // terminated, so we return to the caller. + // + // Instead, if `ticker.C` is ready, we emit a progress + // update using the `callbacks`. + // + // ```Go + for { + select { + case err := <-errch: + callbacks.OnProgress(1.0, "torsf experiment is finished") + return err + case <-ticker.C: + progress := time.Since(start).Seconds() / maxRuntime.Seconds() + callbacks.OnProgress(progress, "torsf experiment is running") + } + } +} + +// ``` +// +// ### The run function +// +// We will now implement the `run` function. For now, this function +// will not do any real work, but it will just pretend to do work. +// +// Note how we sleep for some time, set the `BootstrapTime` field +// of the `TestKeys`, and then return using `errch`. +// +// ```Go +func (m *Measurer) run(ctx context.Context, + sess model.ExperimentSession, testkeys *TestKeys, errch chan<- error) { + fakeBootstrapTime := 10 * time.Second + time.Sleep(fakeBootstrapTime) + testkeys.BootstrapTime = fakeBootstrapTime.Seconds() + errch <- nil +} + +// ``` +// +// ## Running the code +// +// It's now time to run the new code we've written: +// +// ``` +// $ go run ./experiment/torsf/chapter03 | jq +// 2021/06/21 21:21:18 info [ 0.1%] torsf experiment is running +// 2021/06/21 21:21:19 info [ 0.2%] torsf experiment is running +// [...] +// 2021/06/21 21:21:28 info [ 3.3%] torsf experiment is running +// 2021/06/21 21:21:28 info [100.0%] torsf experiment is finished +// { +// "data_format_version": "", +// "input": null, +// "measurement_start_time": "", +// "probe_asn": "", +// "probe_cc": "", +// "probe_network_name": "", +// "report_id": "", +// "resolver_asn": "", +// "resolver_ip": "", +// "resolver_network_name": "", +// "software_name": "", +// "software_version": "", +// "test_keys": { +// "bootstrap_time": 10, +// "failure": null +// }, +// "test_name": "", +// "test_runtime": 0, +// "test_start_time": "", +// "test_version": "" +// } +// ``` +// +// You see that now we're filling the bootstrap time and we're +// also printing progress using `callbacks`. +// +// In the next chapter, we'll replace the stub `run` implementation +// with a real implementation using Snowflake. +// +// -=-=- StopHere -=-=- + +// SummaryKeys contains summary keys for this experiment. +type SummaryKeys struct { + IsAnomaly bool `json:"-"` +} + +// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys. +func (m *Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) { + return &SummaryKeys{IsAnomaly: false}, nil +} diff --git a/internal/tutorial/experiment/torsf/chapter04/README.md b/internal/tutorial/experiment/torsf/chapter04/README.md new file mode 100644 index 0000000..da937de --- /dev/null +++ b/internal/tutorial/experiment/torsf/chapter04/README.md @@ -0,0 +1,201 @@ + +# Chapter IV: writing minimal torsf experiment + +In this chapter we will replace the code written in the previous +chapter that simulates running the torsf experiment with code that +uses the `ooni/probe-cli` library to run the real experiment. + +(This file is auto-generated from the corresponding source file, +so make sure you don't edit it manually.) + +## Updating the imports + +We need to update the imports of `torsf.go` first to look like this: + +```Go + +import ( +``` + +These are standard library imports. + +```Go + "context" + "path" + "time" + +``` + +As we have already seen, the `model` package defines the +generic data model used by all experiments. + +```Go + "github.com/ooni/probe-cli/v3/internal/engine/model" + +``` + +The `archival` package contains code used to format internal +measurements representations to the OONI data format. + +```Go + "github.com/ooni/probe-cli/v3/internal/engine/netx/archival" + +``` + +The `ptx` package contains pluggable transport code. It includes +code to dial with obfs4 and snowflake and code to create a +pluggable transport listener. + +```Go + "github.com/ooni/probe-cli/v3/internal/ptx" + +``` + +The `tunnel` package contains code to create a tunnel. We will +use this package to start a `tor` tunnel, which executes the `tor` +binary using specified command line arguments. + +```Go + "github.com/ooni/probe-cli/v3/internal/tunnel" +) + +``` + + +## Rewriting the run method + +Let us now rewrite the `run` method to run a real `torsf` +test rather than just pretending to do it. + +```Go +func (m *Measurer) run(ctx context.Context, + sess model.ExperimentSession, testkeys *TestKeys, errch chan<- error) { +``` + +As a first step, we create a dialer for snowflake using the +`ptx` package. This dialer will allow us to create a `net.Conn`-like +network connection where traffic is sent using the Snowflake +pluggable transport. There are several optional fields in +`SnowflakeDialer`, but we don't need to override the default +values, so we can just use a default-initialized struct. + +```Go + sfdialer := &ptx.SnowflakeDialer{} +``` + +Let us now create a listener. The `ptx.Listener` is a listener +that listens on a local port and speaks the SOCKS5 protocol. When +tor connect to this port, the listener will forward the traffic +to the Snowflake dialer we previously created. We will also +use the session's logger to emit logging messages. + +```Go + ptl := &ptx.Listener{ + PTDialer: sfdialer, + Logger: sess.Logger(), + } +``` + +Now we start the listener. This entails opening a port on the +local host. If this operation fails, we return an error. In fact, +a failure here means a hard error that prevented us from even +starting the experiment. Therefore, it's consistent with the +`Run`'s expectations to return an error here. + +```Go + if err := ptl.Start(); err != nil { + testkeys.Failure = archival.NewFailure(err) + errch <- err + return + } + defer ptl.Stop() +``` + +Next, we start `tor` using the `tunnel` package. Note how we +pass specific `TorArgs` that cause `tor` to know about the +pluggable transport created by `ptl` and `sfdialer`. + +```Go + tun, err := tunnel.Start(ctx, &tunnel.Config{ + Name: "tor", + Session: sess, + TunnelDir: path.Join(sess.TempDir(), "torsf"), + Logger: sess.Logger(), + TorArgs: []string{ + "UseBridges", "1", + "ClientTransportPlugin", ptl.AsClientTransportPluginArgument(), + "Bridge", sfdialer.AsBridgeArgument(), + }, + }) +``` + +In case of error, we convert `err` to a OONI failure using +the `NewFailure` function of `archival`. This function reduces +Go error strings to the error strings used by OONI. You can +read the [errors spec](https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md) +at the [github.com/ooni/spec repo](https://github.com/ooni/spec). + +Note that, in this case, we return `nil` to the caller, because +a failure here is not a fundamental failure in running the +experiment, but rather a possibly interesting anomaly. + +```Go + if err != nil { + testkeys.Failure = archival.NewFailure(err) + errch <- nil + return + } +``` + +Otherwise, we successfully created a tor tunnel using Snowflake, +so we just close the tunnel and record the bootstrap time. + +```Go + defer tun.Stop() + testkeys.BootstrapTime = tun.BootstrapTime().Seconds() + errch <- nil +} + +``` + +## Running the code + +We can now run the code as follows to obtain: + +``` +$ go run ./experiment/torsf/chapter04 | jq +[...] +Jun 21 23:40:50.000 [notice] Bootstrapped 100% (done): Done +2021/06/21 23:40:50 info [100.0%] torsf experiment is finished +Jun 21 23:40:50.000 [notice] Catching signal TERM, exiting cleanly +{ + "data_format_version": "", + "input": null, + "measurement_start_time": "", + "probe_asn": "", + "probe_cc": "", + "probe_network_name": "", + "report_id": "", + "resolver_asn": "", + "resolver_ip": "", + "resolver_network_name": "", + "software_name": "", + "software_version": "", + "test_keys": { + "bootstrap_time": 48.122813, + "failure": null + }, + "test_name": "", + "test_runtime": 0, + "test_start_time": "", + "test_version": "" +} +``` + +## Concluding remarks + +Congratulations, we have now rewritten together (a simplified version of) +the `torsf` experiment! In this journey, we have learned how experiments +interact with the rest of OONI Probe, how they are typically organized, +and how to use lower-level libraries to implement them. + diff --git a/internal/tutorial/experiment/torsf/chapter04/main.go b/internal/tutorial/experiment/torsf/chapter04/main.go new file mode 100644 index 0000000..ae53ccf --- /dev/null +++ b/internal/tutorial/experiment/torsf/chapter04/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + + "github.com/apex/log" + "github.com/ooni/probe-cli/v3/internal/engine/mockable" + "github.com/ooni/probe-cli/v3/internal/engine/model" + "golang.org/x/sys/execabs" +) + +func main() { + if _, err := execabs.LookPath("tor"); err != nil { + log.Fatal("cannot find the tor executable in path") + } + tempdir, err := ioutil.TempDir("", "") + if err != nil { + log.WithError(err).Fatal("cannot create temporary directory") + } + m := NewExperimentMeasurer(Config{}) + ctx := context.Background() + measurement := &model.Measurement{} + callbacks := model.NewPrinterCallbacks(log.Log) + sess := &mockable.Session{ + MockableLogger: log.Log, + MockableTempDir: tempdir, + } + if err = m.Run(ctx, sess, measurement, callbacks); err != nil { + log.WithError(err).Fatal("torsf experiment failed") + } + data, err := json.Marshal(measurement) + if err != nil { + log.WithError(err).Fatal("json.Marshal failed") + } + fmt.Printf("%s\n", data) +} diff --git a/internal/tutorial/experiment/torsf/chapter04/torsf.go b/internal/tutorial/experiment/torsf/chapter04/torsf.go new file mode 100644 index 0000000..1ba96d0 --- /dev/null +++ b/internal/tutorial/experiment/torsf/chapter04/torsf.go @@ -0,0 +1,277 @@ +package main + +// -=-=- StartHere -=-=- +// +// # Chapter IV: writing minimal torsf experiment +// +// In this chapter we will replace the code written in the previous +// chapter that simulates running the torsf experiment with code that +// uses the `ooni/probe-cli` library to run the real experiment. +// +// (This file is auto-generated from the corresponding source file, +// so make sure you don't edit it manually.) +// +// ## Updating the imports +// +// We need to update the imports of `torsf.go` first to look like this: +// +// ```Go + +import ( + // ``` + // + // These are standard library imports. + // + // ```Go + "context" + "path" + "time" + + // ``` + // + // As we have already seen, the `model` package defines the + // generic data model used by all experiments. + // + // ```Go + "github.com/ooni/probe-cli/v3/internal/engine/model" + + // ``` + // + // The `archival` package contains code used to format internal + // measurements representations to the OONI data format. + // + // ```Go + "github.com/ooni/probe-cli/v3/internal/engine/netx/archival" + + // ``` + // + // The `ptx` package contains pluggable transport code. It includes + // code to dial with obfs4 and snowflake and code to create a + // pluggable transport listener. + // + // ```Go + "github.com/ooni/probe-cli/v3/internal/ptx" + + // ``` + // + // The `tunnel` package contains code to create a tunnel. We will + // use this package to start a `tor` tunnel, which executes the `tor` + // binary using specified command line arguments. + // + // ```Go + "github.com/ooni/probe-cli/v3/internal/tunnel" +) + +// ``` +// +// -=-=- StopHere -=-=- + +// Config contains config for the torsf experiment. +type Config struct{} + +// Measurer is the torsf measurer. +type Measurer struct { + config Config +} + +// newExperimentMeasurer creates a new ExperimentMeasurer for torsf. +func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { + return &Measurer{config: config} +} + +// ExperimentName implements ExperimentMeasurer.ExperimentName. +func (m *Measurer) ExperimentName() string { + return "torsf" +} + +// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion. +func (m *Measurer) ExperimentVersion() string { + return "0.1.0" +} + +// TestKeys contains the experiment results. +type TestKeys struct { + // BootstrapTime is the time required to bootstrap. + BootstrapTime float64 `json:"bootstrap_time"` + + // Failure is the failure that occurred, or nil. + Failure *string `json:"failure"` +} + +// Run implements ExperimentMeasurer.Run. +func (m *Measurer) Run( + ctx context.Context, sess model.ExperimentSession, + measurement *model.Measurement, callbacks model.ExperimentCallbacks, +) error { + testkeys := &TestKeys{} + measurement.TestKeys = testkeys + start := time.Now() + const maxRuntime = 300 * time.Second + ctx, cancel := context.WithTimeout(ctx, maxRuntime) + defer cancel() + errch := make(chan error) + ticker := time.NewTicker(250 * time.Millisecond) + defer ticker.Stop() + go m.run(ctx, sess, testkeys, errch) + for { + select { + case err := <-errch: + callbacks.OnProgress(1.0, "torsf experiment is finished") + return err + case <-ticker.C: + progress := time.Since(start).Seconds() / maxRuntime.Seconds() + callbacks.OnProgress(progress, "torsf experiment is running") + } + } +} + +// -=-=- StartHere -=-=- +// +// ## Rewriting the run method +// +// Let us now rewrite the `run` method to run a real `torsf` +// test rather than just pretending to do it. +// +// ```Go +func (m *Measurer) run(ctx context.Context, + sess model.ExperimentSession, testkeys *TestKeys, errch chan<- error) { + // ``` + // + // As a first step, we create a dialer for snowflake using the + // `ptx` package. This dialer will allow us to create a `net.Conn`-like + // network connection where traffic is sent using the Snowflake + // pluggable transport. There are several optional fields in + // `SnowflakeDialer`, but we don't need to override the default + // values, so we can just use a default-initialized struct. + // + // ```Go + sfdialer := &ptx.SnowflakeDialer{} + // ``` + // + // Let us now create a listener. The `ptx.Listener` is a listener + // that listens on a local port and speaks the SOCKS5 protocol. When + // tor connect to this port, the listener will forward the traffic + // to the Snowflake dialer we previously created. We will also + // use the session's logger to emit logging messages. + // + // ```Go + ptl := &ptx.Listener{ + PTDialer: sfdialer, + Logger: sess.Logger(), + } + // ``` + // + // Now we start the listener. This entails opening a port on the + // local host. If this operation fails, we return an error. In fact, + // a failure here means a hard error that prevented us from even + // starting the experiment. Therefore, it's consistent with the + // `Run`'s expectations to return an error here. + // + // ```Go + if err := ptl.Start(); err != nil { + testkeys.Failure = archival.NewFailure(err) + errch <- err + return + } + defer ptl.Stop() + // ``` + // + // Next, we start `tor` using the `tunnel` package. Note how we + // pass specific `TorArgs` that cause `tor` to know about the + // pluggable transport created by `ptl` and `sfdialer`. + // + // ```Go + tun, err := tunnel.Start(ctx, &tunnel.Config{ + Name: "tor", + Session: sess, + TunnelDir: path.Join(sess.TempDir(), "torsf"), + Logger: sess.Logger(), + TorArgs: []string{ + "UseBridges", "1", + "ClientTransportPlugin", ptl.AsClientTransportPluginArgument(), + "Bridge", sfdialer.AsBridgeArgument(), + }, + }) + // ``` + // + // In case of error, we convert `err` to a OONI failure using + // the `NewFailure` function of `archival`. This function reduces + // Go error strings to the error strings used by OONI. You can + // read the [errors spec](https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md) + // at the [github.com/ooni/spec repo](https://github.com/ooni/spec). + // + // Note that, in this case, we return `nil` to the caller, because + // a failure here is not a fundamental failure in running the + // experiment, but rather a possibly interesting anomaly. + // + // ```Go + if err != nil { + testkeys.Failure = archival.NewFailure(err) + errch <- nil + return + } + // ``` + // + // Otherwise, we successfully created a tor tunnel using Snowflake, + // so we just close the tunnel and record the bootstrap time. + // + // ```Go + defer tun.Stop() + testkeys.BootstrapTime = tun.BootstrapTime().Seconds() + errch <- nil +} + +// ``` +// +// ## Running the code +// +// We can now run the code as follows to obtain: +// +// ``` +// $ go run ./experiment/torsf/chapter04 | jq +// [...] +// Jun 21 23:40:50.000 [notice] Bootstrapped 100% (done): Done +// 2021/06/21 23:40:50 info [100.0%] torsf experiment is finished +// Jun 21 23:40:50.000 [notice] Catching signal TERM, exiting cleanly +// { +// "data_format_version": "", +// "input": null, +// "measurement_start_time": "", +// "probe_asn": "", +// "probe_cc": "", +// "probe_network_name": "", +// "report_id": "", +// "resolver_asn": "", +// "resolver_ip": "", +// "resolver_network_name": "", +// "software_name": "", +// "software_version": "", +// "test_keys": { +// "bootstrap_time": 48.122813, +// "failure": null +// }, +// "test_name": "", +// "test_runtime": 0, +// "test_start_time": "", +// "test_version": "" +// } +// ``` +// +// ## Concluding remarks +// +// Congratulations, we have now rewritten together (a simplified version of) +// the `torsf` experiment! In this journey, we have learned how experiments +// interact with the rest of OONI Probe, how they are typically organized, +// and how to use lower-level libraries to implement them. +// +// -=-=- StopHere -=-=- + +// SummaryKeys contains summary keys for this experiment. +type SummaryKeys struct { + IsAnomaly bool `json:"-"` +} + +// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys. +func (m *Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) { + return &SummaryKeys{IsAnomaly: false}, nil +} diff --git a/internal/tutorial/generator/main.go b/internal/tutorial/generator/main.go new file mode 100644 index 0000000..1b82b2c --- /dev/null +++ b/internal/tutorial/generator/main.go @@ -0,0 +1,95 @@ +// Command generator generates or re-generates the tutorial chapters. You +// should run this command like `go run ./generator`. +package main + +import ( + "bufio" + "io" + "log" + "os" + "path" + "strings" +) + +// writeString writes a string on the given writer. If there +// is a write error, this function will call log.Fatal. +func writeString(w io.Writer, s string) { + if _, err := io.WriteString(w, s); err != nil { + log.Fatal(err) + } +} + +// gen1 generates a single file within a chapter. +func gen1(destfile io.Writer, filepath string) { + srcfile, err := os.Open(filepath) + if err != nil { + log.Fatal(err) + } + defer srcfile.Close() + scanner := bufio.NewScanner(srcfile) + var started bool + for scanner.Scan() { + line := scanner.Text() + trimmed := strings.Trim(line, " \t\r\n") + if trimmed == "// -=-=- StopHere -=-=-" { + started = false + continue + } + if trimmed == "// -=-=- StartHere -=-=-" { + started = true + continue + } + if !started { + continue + } + if strings.HasPrefix(trimmed, "//") { + if strings.HasPrefix(trimmed, "// ") { + trimmed = trimmed[3:] + } else { + trimmed = trimmed[2:] + } + writeString(destfile, trimmed+"\n") + continue + } + writeString(destfile, line+"\n") + } + if err := scanner.Err(); err != nil { + log.Fatal(err) + } +} + +// gen generates or re-generates a chapter. The dirpath argument +// is the path to the directory that contains a chapter. The files +// arguments contains the source file names to process. We will process +// files using the specified order. Note that files names are not +// paths, just file names, e.g., +// +// gen("./experiment/torsf/chapter01", "main.go") +func gen(dirpath string, files ...string) { + readme := path.Join(dirpath, "README.md") + destfile, err := os.Create(path.Join(readme)) + if err != nil { + log.Fatal(err) + } + defer func() { + if err := destfile.Close(); err != nil { + log.Fatal(err) + } + }() + for _, file := range files { + gen1(destfile, path.Join(dirpath, file)) + } +} + +// gentorsf generates the torsf chapters. +func gentorsf() { + prefix := path.Join(".", "experiment", "torsf") + gen(path.Join(prefix, "chapter01"), "main.go") + gen(path.Join(prefix, "chapter02"), "main.go", "torsf.go") + gen(path.Join(prefix, "chapter03"), "torsf.go") + gen(path.Join(prefix, "chapter04"), "torsf.go") +} + +func main() { + gentorsf() +}