diff --git a/internal/cmd/ooporthelper/main.go b/internal/cmd/ooporthelper/main.go index ddb21f5425..e9dfcfaead 100644 --- a/internal/cmd/ooporthelper/main.go +++ b/internal/cmd/ooporthelper/main.go @@ -9,7 +9,7 @@ import ( "time" "github.com/apex/log" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/portfiltering" + "github.com/ooni/probe-cli/v3/internal/engine" "github.com/ooni/probe-cli/v3/internal/runtimex" ) @@ -64,7 +64,7 @@ func main() { flag.Parse() log.SetLevel(logmap[*debug]) defer srvCancel() - ports := portfiltering.Ports + ports := engine.PortFilteringDefaultInput if srvTest { ports = TestPorts } diff --git a/internal/engine/experiment/portfiltering/config.go b/internal/engine/experiment/portfiltering/config.go index d1f2e3f6c0..ca02cc3ccd 100644 --- a/internal/engine/experiment/portfiltering/config.go +++ b/internal/engine/experiment/portfiltering/config.go @@ -4,17 +4,15 @@ package portfiltering // Config for the port-filtering experiment // -import "time" - // Config contains the experiment configuration. type Config struct { - // Delay is the delay between each repetition (in milliseconds). - Delay int64 `ooni:"number of milliseconds to wait before testing each port"` + // TestHelper is the URL to use for port-scanning + TestHelper string `ooni:"testhelper URL for port scanning"` } -func (c *Config) delay() time.Duration { - if c.Delay > 0 { - return time.Duration(c.Delay) * time.Millisecond +func (c *Config) testhelper() string { + if c.TestHelper != "" { + return c.TestHelper } - return 100 * time.Millisecond + return "http://127.0.0.1" } diff --git a/internal/engine/experiment/portfiltering/config_test.go b/internal/engine/experiment/portfiltering/config_test.go index 9ea4b03e47..ccc4090d49 100644 --- a/internal/engine/experiment/portfiltering/config_test.go +++ b/internal/engine/experiment/portfiltering/config_test.go @@ -2,12 +2,11 @@ package portfiltering import ( "testing" - "time" ) func TestConfig_delay(t *testing.T) { c := Config{} - if c.delay() != 100*time.Millisecond { - t.Fatal("invalid default delay") + if c.testhelper() != "http://127.0.0.1" { + t.Fatal("invalid default testhelper") } } diff --git a/internal/engine/experiment/portfiltering/measurer.go b/internal/engine/experiment/portfiltering/measurer.go index 76c9a2dcf2..e51998fc3b 100644 --- a/internal/engine/experiment/portfiltering/measurer.go +++ b/internal/engine/experiment/portfiltering/measurer.go @@ -7,7 +7,9 @@ package portfiltering import ( "context" "errors" + "net" "net/url" + "strconv" "github.com/ooni/probe-cli/v3/internal/model" ) @@ -33,6 +35,12 @@ func (m *Measurer) ExperimentVersion() string { } var ( + // errInputRequired indicates that no input was provided + errInputRequired = errors.New("this experiment needs input") + + // errInvalidInput indicates an invalid port number + errInvalidInput = errors.New("port number is invalid") + // errInvalidTestHelper indicates that the given test helper is not an URL errInvalidTestHelper = errors.New("testhelper is not an URL") ) @@ -44,21 +52,26 @@ func (m *Measurer) Run( measurement *model.Measurement, callbacks model.ExperimentCallbacks, ) error { + input := string(measurement.Input) + if input == "" { + return errInputRequired + } + port, err := strconv.Atoi(input) + if err != nil || port >= 65536 || port < 0 { + return errInvalidInput + } // TODO(DecFox): Replace the localhost deployment with an OONI testhelper // Ensure that we only do this once we have a deployed testhelper - testhelper := "http://127.0.0.1" - parsed, err := url.Parse(testhelper) + th := m.config.testhelper() + parsed, err := url.Parse(th) if err != nil { return errInvalidTestHelper } tk := new(TestKeys) measurement.TestKeys = tk - out := make(chan *model.ArchivalTCPConnectResult) - go m.tcpConnectLoop(ctx, measurement.MeasurementStartTimeSaved, sess.Logger(), parsed.Host, out) - for len(tk.TCPConnect) < len(Ports) { - tk.TCPConnect = append(tk.TCPConnect, <-out) - } - return nil // return nil so we always submit the measurement + addr := net.JoinHostPort(parsed.Hostname(), input) + m.tcpConnect(ctx, int64(0), measurement.MeasurementStartTimeSaved, sess.Logger(), tk, addr) + return nil } // NewExperimentMeasurer creates a new ExperimentMeasurer. diff --git a/internal/engine/experiment/portfiltering/measurer_test.go b/internal/engine/experiment/portfiltering/measurer_test.go index e3a38766cf..49c7d2b837 100644 --- a/internal/engine/experiment/portfiltering/measurer_test.go +++ b/internal/engine/experiment/portfiltering/measurer_test.go @@ -2,10 +2,15 @@ package portfiltering import ( "context" + "net/http" + "net/http/httptest" + "net/url" + "strconv" "testing" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/model/mocks" + "github.com/ooni/probe-cli/v3/internal/netxlite" ) func TestMeasurerExperimentNameVersion(t *testing.T) { @@ -18,31 +23,136 @@ func TestMeasurerExperimentNameVersion(t *testing.T) { } } -// TODO(DecFox): Skip this test with -short in a future iteration. func TestMeasurer_run(t *testing.T) { - m := NewExperimentMeasurer(Config{}) - meas := &model.Measurement{} - sess := &mocks.Session{ - MockLogger: func() model.Logger { - return model.DiscardLogger - }, - } - callbacks := model.NewPrinterCallbacks(model.DiscardLogger) - ctx := context.Background() - err := m.Run(ctx, sess, meas, callbacks) - if err != nil { - t.Fatal(err) - } - tk := meas.TestKeys.(*TestKeys) - if len(tk.TCPConnect) != len(Ports) { - t.Fatal("unexpected number of ports") - } - ask, err := m.GetSummaryKeys(meas) - if err != nil { - t.Fatal("cannot obtain summary") - } - summary := ask.(SummaryKeys) - if summary.IsAnomaly { - t.Fatal("expected no anomaly") + runHelper := func(ctx context.Context, input string, url string) (*model.Measurement, model.ExperimentMeasurer, error) { + m := NewExperimentMeasurer(Config{ + TestHelper: url, + }) + meas := &model.Measurement{ + Input: model.MeasurementTarget(input), + } + sess := &mocks.Session{ + MockLogger: func() model.Logger { + return model.DiscardLogger + }, + } + callbacks := model.NewPrinterCallbacks(model.DiscardLogger) + err := m.Run(ctx, sess, meas, callbacks) + return meas, m, err } + + t.Run("with no input", func(t *testing.T) { + ctx := context.Background() + _, _, err := runHelper(ctx, "", "") + if err == nil || err != errInputRequired { + t.Fatal("unexpected error") + } + }) + + t.Run("with invalid input", func(t *testing.T) { + t.Run("with negative port number", func(t *testing.T) { + ctx := context.Background() + _, _, err := runHelper(ctx, "-1", "") + if err == nil || err != errInvalidInput { + t.Fatal(err) + } + }) + + t.Run("with large invalid port number", func(t *testing.T) { + ctx := context.Background() + _, _, err := runHelper(ctx, "70000", "") + if err == nil || err != errInvalidInput { + t.Fatal(err) + } + }) + + t.Run("with non-integer port number", func(t *testing.T) { + ctx := context.Background() + _, _, err := runHelper(ctx, "\t", "") + if err == nil || err != errInvalidInput { + t.Fatal(err) + } + }) + }) + + // TODO(DecFox): Add a test that checks ports on the OONI API + t.Run("with API testhelper", func(t *testing.T) { + }) + + t.Run("with local listener and successful outcome", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + })) + defer server.Close() + URL, err := url.Parse(server.URL) + if err != nil { + t.Fatal(err) + } + ctx := context.Background() + meas, m, err := runHelper(ctx, URL.Port(), URL.String()) + if err != nil { + t.Fatal(err) + } + ask, err := m.GetSummaryKeys(meas) + if err != nil { + t.Fatal("cannot obtain summary") + } + summary := ask.(SummaryKeys) + if summary.IsAnomaly { + t.Fatal("expected no anomaly") + } + + t.Run("testkeys", func(t *testing.T) { + tk := meas.TestKeys.(*TestKeys) + port, _ := strconv.Atoi(URL.Port()) + if tk.TCPConnect.IP != URL.Hostname() { + t.Fatal("unexpected target IP") + } + if tk.TCPConnect.Port != port { + t.Fatal("unexpected port") + } + if tk.TCPConnect.Status.Failure != nil { + t.Fatal("unexpected error") + } + }) + }) + + t.Run("with local listener and cancel", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + })) + defer server.Close() + URL, err := url.Parse(server.URL) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(context.Background()) + cancel() + meas, m, err := runHelper(ctx, URL.Port(), URL.String()) + if err != nil { + t.Fatal(err) + } + ask, err := m.GetSummaryKeys(meas) + if err != nil { + t.Fatal("cannot obtain summary") + } + summary := ask.(SummaryKeys) + if summary.IsAnomaly { + t.Fatal("expected no anomaly") + } + + t.Run("testkeys", func(t *testing.T) { + tk := meas.TestKeys.(*TestKeys) + port, _ := strconv.Atoi(URL.Port()) + if tk.TCPConnect.IP != URL.Hostname() { + t.Fatal("unexpected target IP") + } + if tk.TCPConnect.Port != port { + t.Fatal("unexpected port") + } + if *tk.TCPConnect.Status.Failure != netxlite.FailureInterrupted { + t.Fatal("unexpected error") + } + }) + }) } diff --git a/internal/engine/experiment/portfiltering/ports.go b/internal/engine/experiment/portfiltering/ports.go deleted file mode 100644 index 7f0234b177..0000000000 --- a/internal/engine/experiment/portfiltering/ports.go +++ /dev/null @@ -1,73 +0,0 @@ -package portfiltering - -// -// List of ports we want to measure -// - -// List generated from nmap-services: https://github.com/nmap/nmap/blob/master/nmap-services -// Note: Using privileged ports like :80 requires elevated permissions -var Ports = []string{ - "80", // tcp - World Wide Web HTTP - "631", // udp - Internet Printing Protocol - "161", // udp - Simple Net Mgmt Proto - "137", // udp - NETBIOS Name Service - "123", // udp - Network Time Protocol - "138", // udp - NETBIOS Datagram Service - "1434", // udp - Microsoft-SQL-Monitor - "135", // udp, tcp - epmap | Microsoft RPC services | DCE endpoint resolution - "67", // udp - DHCP/Bootstrap Protocol Server - "23", // tcp - "53", // udp, tcp - Domain Name Server - "443", // tcp - secure http (SSL) - "21", // tcp - File Transfer [Control] - "22", // tcp - Secure Shell Login - "500", // udp - "68", // udp - DHCP/Bootstrap Protocol Client - "520", // udp - router routed -- RIP - "1900", // udp - Universal PnP - "25", // tcp - Simple Mail Transfer - "4500", // udp - IKE Nat Traversal negotiation (RFC3947) - "514", // udp - BSD syslogd(8) - "49152", // udp - "162", // udp - snmp-trap - "69", // udp - Trivial File Transfer - "5353", // udp - Mac OS X Bonjour/Zeroconf port - "49154", // udp - "3389", // tcp - Microsoft Remote Display Protocol (aka ms-term-serv, microsoft-rdp) | MS WBT Server - "110", // tcp - PostOffice V.3 | Post Office Protocol - Version 3 - "1701", // udp - "998", // udp - "996", // udp - "997", // udp - "999", // udp - Applix ac - "3283", // udp - Apple Remote Desktop Net Assistant reporting feature - "49153", // udp - "445", // tcp - SMB directly over IP - "1812", // udp - RADIUS authentication protocol (RFC 2138) - "136", // udp - PROFILE Naming System - "139", // tcp, udp - NETBIOS Session Service - "143", // tcp - Interim Mail Access Protocol v2 | Internet Message Access Protocol - "2222", // udp - Microsoft Office OS X antipiracy network monitor - "3306", // tcp - "2049", // udp - networked file system - "32768", // udp - OpenMosix Autodiscovery Daemon - "5060", // udp - Session Initiation Protocol (SIP) - "8080", // tcp - http-alt | Common HTTP proxy/second web server port | HTTP Alternate (see port 80) - "1433", // udp - Microsoft-SQL-Server - "3456", // udp - also VAT default data - "1723", // tcp - Point-to-point tunnelling protocol - "111", // tcp, udp - sunrpc | portmapper, rpcbind | SUN Remote Procedure Call - "995", // tcp - POP3 protocol over TLS/SSL | pop3 protocol over TLS/SSL (was spop3) | POP3 over TLS protocol - "993", // tcp - imap4 protocol over TLS/SSL | IMAP over TLS protocol - "20031", // udp - BakBone NetVault primary communications port - "1026", // udp - Commonly used to send MS Messenger spam - "7", // udp - "5900", // tcp - rfb | Virtual Network Computer display 0 | Remote Framebuffer - "1646", // udp - radius accounting - "1645", // udp - radius authentication - "593", // udp # HTTP RPC Ep Map - "1025", // tcp, udp - blackjack | IIS, NFS, or listener RFS remote_file_sharing | network blackjack - "518", // udp - (talkd) - "2048", // udp - "626", // udp - Mac OS X Server serial number (licensing) daemon -} diff --git a/internal/engine/experiment/portfiltering/tcpconnect.go b/internal/engine/experiment/portfiltering/tcpconnect.go index fab2d93ff2..2c5f66e682 100644 --- a/internal/engine/experiment/portfiltering/tcpconnect.go +++ b/internal/engine/experiment/portfiltering/tcpconnect.go @@ -6,43 +6,20 @@ package portfiltering import ( "context" - "math/rand" - "net" "time" "github.com/ooni/probe-cli/v3/internal/measurexlite" "github.com/ooni/probe-cli/v3/internal/model" ) -// tcpPingLoop sends the TCP Connect requests to all ports and emits the results onto the out channel -func (m *Measurer) tcpConnectLoop(ctx context.Context, zeroTime time.Time, - logger model.Logger, address string, out chan<- *model.ArchivalTCPConnectResult) { - ticker := time.NewTicker(m.config.delay()) - defer ticker.Stop() - rand.Shuffle(len(Ports), func(i, j int) { - Ports[i], Ports[j] = Ports[j], Ports[i] - }) - for i, port := range Ports { - addr := net.JoinHostPort(address, port) - go m.tcpConnectAsync(ctx, int64(i), zeroTime, logger, addr, out) - <-ticker.C - } -} - -// tcpPingAsync performs a TCP Connect and emits the result onto the out channel. -func (m *Measurer) tcpConnectAsync(ctx context.Context, index int64, - zeroTime time.Time, logger model.Logger, address string, out chan<- *model.ArchivalTCPConnectResult) { - out <- m.tcpConnect(ctx, index, zeroTime, logger, address) -} - // tcpConnect performs a TCP connect and returns the result to the caller. func (m *Measurer) tcpConnect(ctx context.Context, index int64, - zeroTime time.Time, logger model.Logger, address string) *model.ArchivalTCPConnectResult { + zeroTime time.Time, logger model.Logger, tk *TestKeys, address string) { trace := measurexlite.NewTrace(index, zeroTime) ol := measurexlite.NewOperationLogger(logger, "TCPConnect #%d %s", index, address) dialer := trace.NewDialerWithoutResolver(logger) conn, err := dialer.DialContext(ctx, "tcp", address) ol.Stop(err) measurexlite.MaybeClose(conn) - return trace.FirstTCPConnectOrNil() + tk.TCPConnect = trace.FirstTCPConnectOrNil() } diff --git a/internal/engine/experiment/portfiltering/testkeys.go b/internal/engine/experiment/portfiltering/testkeys.go index 68dd640caa..e0d2c2fabf 100644 --- a/internal/engine/experiment/portfiltering/testkeys.go +++ b/internal/engine/experiment/portfiltering/testkeys.go @@ -4,5 +4,5 @@ import "github.com/ooni/probe-cli/v3/internal/model" // TestKeys contains the experiment results. type TestKeys struct { - TCPConnect []*model.ArchivalTCPConnectResult `json:"tcp_connect"` + TCPConnect *model.ArchivalTCPConnectResult `json:"tcp_connect"` } diff --git a/internal/engine/inputloader.go b/internal/engine/inputloader.go index b678db9230..62998c15bd 100644 --- a/internal/engine/inputloader.go +++ b/internal/engine/inputloader.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io/fs" + "math/rand" "net/url" "github.com/apex/log" @@ -212,6 +213,75 @@ var dnsCheckDefaultInput = []string{ var stunReachabilityDefaultInput = stuninput.AsnStunReachabilityInput() +var PortFilteringDefaultInput = []string{ + "80", // tcp - World Wide Web HTTP + "631", // udp - Internet Printing Protocol + "161", // udp - Simple Net Mgmt Proto + "137", // udp - NETBIOS Name Service + "123", // udp - Network Time Protocol + "138", // udp - NETBIOS Datagram Service + "1434", // udp - Microsoft-SQL-Monitor + "135", // udp, tcp - epmap | Microsoft RPC services | DCE endpoint resolution + "67", // udp - DHCP/Bootstrap Protocol Server + "23", // tcp + "53", // udp, tcp - Domain Name Server + "443", // tcp - secure http (SSL) + "21", // tcp - File Transfer [Control] + "22", // tcp - Secure Shell Login + "500", // udp + "68", // udp - DHCP/Bootstrap Protocol Client + "520", // udp - router routed -- RIP + "1900", // udp - Universal PnP + "25", // tcp - Simple Mail Transfer + "4500", // udp - IKE Nat Traversal negotiation (RFC3947) + "514", // udp - BSD syslogd(8) + "49152", // udp + "162", // udp - snmp-trap + "69", // udp - Trivial File Transfer + "5353", // udp - Mac OS X Bonjour/Zeroconf port + "49154", // udp + "3389", // tcp - Microsoft Remote Display Protocol (aka ms-term-serv, microsoft-rdp) | MS WBT Server + "110", // tcp - PostOffice V.3 | Post Office Protocol - Version 3 + "1701", // udp + "998", // udp + "996", // udp + "997", // udp + "999", // udp - Applix ac + "3283", // udp - Apple Remote Desktop Net Assistant reporting feature + "49153", // udp + "445", // tcp - SMB directly over IP + "1812", // udp - RADIUS authentication protocol (RFC 2138) + "136", // udp - PROFILE Naming System + "139", // tcp, udp - NETBIOS Session Service + "143", // tcp - Interim Mail Access Protocol v2 | Internet Message Access Protocol + "2222", // udp - Microsoft Office OS X antipiracy network monitor + "3306", // tcp + "2049", // udp - networked file system + "32768", // udp - OpenMosix Autodiscovery Daemon + "5060", // udp - Session Initiation Protocol (SIP) + "8080", // tcp - http-alt | Common HTTP proxy/second web server port | HTTP Alternate (see port 80) + "1433", // udp - Microsoft-SQL-Server + "3456", // udp - also VAT default data + "1723", // tcp - Point-to-point tunnelling protocol + "111", // tcp, udp - sunrpc | portmapper, rpcbind | SUN Remote Procedure Call + "995", // tcp - POP3 protocol over TLS/SSL | pop3 protocol over TLS/SSL (was spop3) | POP3 over TLS protocol + "993", // tcp - imap4 protocol over TLS/SSL | IMAP over TLS protocol + "20031", // udp - BakBone NetVault primary communications port + "1026", // udp - Commonly used to send MS Messenger spam + "7", // udp + "5900", // tcp - rfb | Virtual Network Computer display 0 | Remote Framebuffer + "1646", // udp - radius accounting + "1645", // udp - radius authentication + "593", // udp # HTTP RPC Ep Map + "1025", // tcp, udp - blackjack | IIS, NFS, or listener RFS remote_file_sharing | network blackjack + "518", // udp - (talkd) + "2048", // udp + "626", // udp - Mac OS X Server serial number (licensing) daemon + "8080", // tcp, udp - random ports to detect IP blocking + "5050", // tcp, udp - random ports to detect IP blocking + "5000", // tcp, udp - random ports to detect IP blocking +} + // StaticBareInputForExperiment returns the list of strings an // experiment should use as static input. In case there is no // static input for this experiment, we return an error. @@ -224,6 +294,11 @@ func StaticBareInputForExperiment(name string) ([]string, error) { return dnsCheckDefaultInput, nil case "stunreachability": return stunReachabilityDefaultInput, nil + case "portfiltering": + rand.Shuffle(len(PortFilteringDefaultInput), func(i, j int) { + PortFilteringDefaultInput[i], PortFilteringDefaultInput[j] = PortFilteringDefaultInput[j], PortFilteringDefaultInput[i] + }) + return PortFilteringDefaultInput, nil default: return nil, ErrNoStaticInput } diff --git a/internal/registry/portfiltering.go b/internal/registry/portfiltering.go index 093ccbabe6..7ea6c3c049 100644 --- a/internal/registry/portfiltering.go +++ b/internal/registry/portfiltering.go @@ -11,13 +11,12 @@ import ( func init() { AllExperiments["portfiltering"] = &Factory{ - build: func(config any) model.ExperimentMeasurer { + build: func(config interface{}) model.ExperimentMeasurer { return portfiltering.NewExperimentMeasurer( - config.(portfiltering.Config), + *config.(*portfiltering.Config), ) }, - config: portfiltering.Config{}, - interruptible: false, - inputPolicy: model.InputNone, + config: &portfiltering.Config{}, + inputPolicy: model.InputOrStaticDefault, } }