├── .gitignore ├── results ├── .gitignore └── .DS_Store ├── research ├── MDM-Research-Final.pdf ├── MDM-regulations-in-the-US-Database.xlsx └── Censorship-without-borders-Deconstructing-the-myth-of-West-vs.-East-Navid-Report.pdf ├── docker-compose.yaml ├── files ├── registry_nym.go └── experiment_nym │ └── nym.go ├── Dockerfile └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /results/.gitignore: -------------------------------------------------------------------------------- 1 | report.jsonl 2 | -------------------------------------------------------------------------------- /results/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nymtech/CensorshipMeasurements/HEAD/results/.DS_Store -------------------------------------------------------------------------------- /research/MDM-Research-Final.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nymtech/CensorshipMeasurements/HEAD/research/MDM-Research-Final.pdf -------------------------------------------------------------------------------- /research/MDM-regulations-in-the-US-Database.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nymtech/CensorshipMeasurements/HEAD/research/MDM-regulations-in-the-US-Database.xlsx -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | ooni-probe: 3 | build: . 4 | environment: 5 | - OONI_NYMVALIDATORURL=${OONI_NYMVALIDATORURL:-} 6 | volumes: 7 | - ./results:/results 8 | -------------------------------------------------------------------------------- /research/Censorship-without-borders-Deconstructing-the-myth-of-West-vs.-East-Navid-Report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nymtech/CensorshipMeasurements/HEAD/research/Censorship-without-borders-Deconstructing-the-myth-of-West-vs.-East-Navid-Report.pdf -------------------------------------------------------------------------------- /files/registry_nym.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | // 4 | // Registers the `nym' experiment. 5 | // 6 | 7 | import ( 8 | "github.com/ooni/probe-cli/v3/internal/experiment/nym" 9 | "github.com/ooni/probe-cli/v3/internal/model" 10 | ) 11 | 12 | func init() { 13 | AllExperiments["nym"] = &Factory{ 14 | build: func(config interface{}) model.ExperimentMeasurer { 15 | return nym.NewExperimentMeasurer( 16 | *config.(*nym.Config), 17 | ) 18 | }, 19 | config: &nym.Config{}, 20 | inputPolicy: model.InputNone, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # OONI Release 3.17 is working with go version 1.19 (see GOVERSION file) 2 | FROM golang:1.19.6-alpine 3 | RUN apk update && apk upgrade 4 | RUN apk add git gcc libc-dev 5 | 6 | COPY ./files /nym_files 7 | RUN git clone -b release/3.17 --single-branch https://github.com/ooni/probe-cli.git /ooni-probe 8 | RUN cp /nym_files/registry_nym.go /ooni-probe/internal/registry/nym.go 9 | RUN cp -r /nym_files/experiment_nym /ooni-probe/internal/experiment/nym 10 | WORKDIR /ooni-probe 11 | RUN go build -v -ldflags '-s -w' ./internal/cmd/miniooni 12 | 13 | CMD ./miniooni --no-collector --yes --verbose -O NymValidatorURL=${OONI_NYMVALIDATORURL:-https://validator.nymtech.net} -o /results/report.jsonl nym && chmod 666 /results/report.jsonl 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Censorship Measurement of the Nym network 2 | 3 | ## OONI-Probe tests for the Nym network 4 | 5 | This repository introduces a new type of Go test for Nym network censorship. Our tests are performed using the [OONI probe](https://ooni.org/). 6 | * First, the test evaluates the connectivity to the to the validator API, which is a necessary step to allow users access to the Nym network, as it allows the Nym client to retrieve vital information such as the list of active relay nodes, 7 | gateways, their topology, and the necessary credentials for network access. 8 | * After successfully fetching the list of available gateways in the initial test, 9 | a second test is conducted to determine the reachability of these gateways. 10 | Since the Nym network topology mandates that packets are routed through 11 | a gateway before entering the mixnet, being able to connect to at least one 12 | gateway is crucial for network access. To assess this, the test attempts to 13 | establish connections with each gateway individually. 14 | 15 | ## Requirements 16 | To be able able to run the tests, you should have `docker` and `docker-compose` installed. You can follow [this procedure](https://docs.docker.com/desktop) to install docker desktop which contains `docker` and `docker-compose` and a GUI to manage docker. Once installed and running, you can run the docker tests. 17 | 18 | ## How to run the tests ? 19 | 20 | From the command line, enter the `CensorshipMeasurements` folder by running the command 21 | ``` 22 | cd CensorshipMeasurements 23 | ``` 24 | 25 | and run in the command line (note, your `Docker` desktop app should be open at this point) 26 | 27 | ```bash 28 | docker-compose up 29 | ``` 30 | By using `docker-compose`, it will build the image, install the requirements inside the image, compile the binaries for the test and run the test. The result will be in the directory `./results` in file `report.jsonl`. 31 | 32 | To force to build again each time, you can do 33 | 34 | ```bash 35 | docker-compose up --build 36 | ``` 37 | 38 | Please share your `report.jsonl` with us. 39 | 40 | ## Licensing and copyright information 41 | This program is available as open source under the terms of the Apache 2.0 license. 42 | 43 | ## Troubleshooting 44 | 45 | If you run `docker-compose up` and get an error: 46 | 47 | `Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?` 48 | 49 | this means that you forgot to open your `Docker` desktop app. 50 | 51 | ## Notes 52 | The environment variable `OONI_NYMVALIDATORURL` can be used to modify the validator server to reach for the tests. By default, it is `"https://validators.nymtech.net"`. 53 | 54 | ```bash 55 | OONI_NYMVALIDATORURL="https://validators.nymtech.net" docker-compose up 56 | ``` 57 | -------------------------------------------------------------------------------- /files/experiment_nym/nym.go: -------------------------------------------------------------------------------- 1 | // Package example contains a nym experiment. 2 | // 3 | package nym 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | "net/url" 13 | "time" 14 | 15 | "github.com/gorilla/websocket" 16 | "github.com/ooni/probe-cli/v3/internal/model" 17 | ) 18 | 19 | const ( 20 | testName = "nym" 21 | testVersion = "0.1.0" 22 | ) 23 | 24 | // Gateway description is the struct that is sent by the validator api 25 | type GatewayDescription struct { 26 | BlockHeight int `json:"block_height"` 27 | Gateway struct { 28 | ClientsPort int `json:"clients_port"` 29 | Host string `json:"host"` 30 | IdentityKey string `json:"identity_key"` 31 | Location string `json:"location"` 32 | MixPort int `json:"mix_port"` 33 | SphinxKey string `json:"sphinx_key"` 34 | Version string `json:"version"` 35 | } `json:"gateway"` 36 | Owner string `json:"owner"` 37 | PledgeAmount struct { 38 | Amount string `json:"amount"` 39 | Denom string `json:"denom"` 40 | } `json:"pledge_amount"` 41 | Proxy struct{} `json:"proxy"` 42 | } 43 | 44 | // Config contains the experiment config. 45 | // 46 | // This contains all the settings that user can set to modify the behaviour 47 | // of this experiment. By tagging these variables with `ooni:"..."`, we allow 48 | // miniooni's -O flag to find them and set them. 49 | type Config struct { 50 | NymValidatorURL string `json:"nym_validator_url"` 51 | } 52 | 53 | // TestKeys contains the experiment's result. 54 | // 55 | // This is what will end up into the Measurement.TestKeys field 56 | // when you run this experiment. 57 | // 58 | // In other words, the variables in this struct will be 59 | // the specific results of this experiment. 60 | type TestKeys struct { 61 | ValidatorAPIReachable bool `json:"validator_api_reachable"` 62 | ValidatorAPIGettingGateways bool `json:"validator_api_gettingGateways"` 63 | GatewaysTotal int64 `json:"gateways_total"` 64 | GatewaysAccessible int64 `json:"gateways_accessible"` 65 | } 66 | 67 | // Measurer performs the measurement. 68 | type Measurer struct { 69 | config Config 70 | } 71 | 72 | // ExperimentName implements model.ExperimentMeasurer.ExperimentName. 73 | func (m Measurer) ExperimentName() string { 74 | return testName 75 | } 76 | 77 | // ExperimentVersion implements model.ExperimentMeasurer.ExperimentVersion. 78 | func (m Measurer) ExperimentVersion() string { 79 | return testVersion 80 | } 81 | 82 | // Run implements model.ExperimentMeasurer.Run. 83 | func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { 84 | //callbacks := args.Callbacks 85 | measurement := args.Measurement 86 | sess := args.Session 87 | testkeys := &TestKeys{ValidatorAPIReachable: false, ValidatorAPIGettingGateways: false, GatewaysTotal: 0, GatewaysAccessible: 0} 88 | measurement.TestKeys = testkeys 89 | ctx, cancel := context.WithTimeout(ctx, 60*time.Second) 90 | defer cancel() 91 | 92 | // We start by parsing the input URL. If we cannot parse it, of 93 | // course this is a hard error and we cannot continue. 94 | apiURL := m.config.NymValidatorURL + "/api/v1/gateways" 95 | sess.Logger().Infof("Using API %s", apiURL) 96 | parsedURL, err := url.Parse(apiURL) 97 | _ = parsedURL 98 | if err != nil { 99 | return err 100 | } 101 | 102 | clnt := &http.Client{} 103 | _ = clnt 104 | 105 | //resp, err := m.HTTPClientGET(ctx, clnt, parsedURL) 106 | resp, err := http.Get(apiURL) 107 | if err != nil { 108 | testkeys.ValidatorAPIReachable = false 109 | return nil 110 | } 111 | testkeys.ValidatorAPIReachable = true 112 | 113 | defer resp.Body.Close() 114 | body, err := io.ReadAll(resp.Body) 115 | if err != nil { 116 | testkeys.ValidatorAPIGettingGateways = false 117 | return nil 118 | } 119 | 120 | var gateways []GatewayDescription 121 | err = json.Unmarshal(body, &gateways) 122 | if err != nil { 123 | testkeys.ValidatorAPIGettingGateways = false 124 | return nil 125 | } 126 | testkeys.ValidatorAPIGettingGateways = true 127 | 128 | for _, g := range gateways { 129 | testkeys.GatewaysTotal++ 130 | u := url.URL{Scheme: "ws", Host: fmt.Sprintf("%s:%d", g.Gateway.Host, g.Gateway.ClientsPort), Path: "/"} 131 | sess.Logger().Infof("Websocket connecting to %s", u.String()) 132 | 133 | c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) 134 | if err != nil { 135 | sess.Logger().Warnf("Websocket handshake error: %s", err) 136 | continue 137 | } 138 | defer c.Close() 139 | 140 | // Cleanly close the connection by sending a close message 141 | err = c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) 142 | if err != nil { 143 | sess.Logger().Warnf("Websocket closing errior: %s", err) 144 | continue 145 | } 146 | testkeys.GatewaysAccessible++ 147 | } 148 | 149 | return nil 150 | } 151 | 152 | // NewExperimentMeasurer creates a new ExperimentMeasurer. 153 | func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { 154 | return Measurer{config: config} 155 | } 156 | 157 | // SummaryKeys contains summary keys for this experiment. 158 | // 159 | // Note that this structure is part of the ABI contract with ooniprobe 160 | // therefore we should be careful when changing it. 161 | type SummaryKeys struct { 162 | IsAnomaly bool `json:"-"` 163 | } 164 | 165 | // GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys. 166 | func (m Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) { 167 | sk := SummaryKeys{IsAnomaly: false} 168 | tk, ok := measurement.TestKeys.(*TestKeys) 169 | if !ok { 170 | return sk, errors.New("invalid test keys type") 171 | } 172 | sk.IsAnomaly = !tk.ValidatorAPIReachable || !tk.ValidatorAPIGettingGateways || tk.GatewaysAccessible == 0 173 | return sk, nil 174 | } 175 | --------------------------------------------------------------------------------