diff --git a/defs/server.go b/defs/server.go index b7fc44b..697f725 100644 --- a/defs/server.go +++ b/defs/server.go @@ -40,6 +40,16 @@ type Server struct { NoICMP bool `json:"-"` TLog TelemetryLog `json:"-"` + + // What the connection to this server actually negotiated; nil over + // plain HTTP. Filled by IsUp, which runs before every measurement. + NegotiatedTLS *TLSInfo `json:"-"` +} + +// TLSInfo names the negotiated TLS parameters of a connection. +type TLSInfo struct { + Version string + Cipher string } // IsUp checks the speed test backend is up by accessing the ping URL @@ -73,9 +83,15 @@ func (s *Server) IsUp() bool { // Two runs can therefore differ several-fold for a reason the numbers alone // do not show, which is what this line is for. if resp.TLS != nil { + // Kept for the report as well as logged: the cipher explains the + // number next to it, and a debug line is gone when the history + // entry it would explain is read. + s.NegotiatedTLS = &TLSInfo{ + Version: tls.VersionName(resp.TLS.Version), + Cipher: tls.CipherSuiteName(resp.TLS.CipherSuite), + } output.WriteDebug("Negotiated %s with %s\n", - tls.VersionName(resp.TLS.Version), - tls.CipherSuiteName(resp.TLS.CipherSuite)) + s.NegotiatedTLS.Version, s.NegotiatedTLS.Cipher) } else { output.WriteDebug("Connection is not encrypted\n") } diff --git a/defs/tls_test.go b/defs/tls_test.go new file mode 100644 index 0000000..c7edcc1 --- /dev/null +++ b/defs/tls_test.go @@ -0,0 +1,49 @@ +package defs + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// The negotiated parameters must reach the Server for the report, and stay +// absent over plain HTTP. +func TestIsUpCapturesNegotiatedTLS(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + tlsSrv := httptest.NewTLSServer(handler) + defer tlsSrv.Close() + + // IsUp uses http.DefaultClient; trust the test server's certificate + // for the duration of the test. + orig := http.DefaultClient + http.DefaultClient = tlsSrv.Client() + defer func() { http.DefaultClient = orig }() + + s := Server{Name: "tls", Server: tlsSrv.URL, PingURL: "/empty"} + + if !s.IsUp() { + t.Fatal("TLS test server reported down") + } + if s.NegotiatedTLS == nil { + t.Fatal("NegotiatedTLS not captured over HTTPS") + } + if !strings.HasPrefix(s.NegotiatedTLS.Version, "TLS") || s.NegotiatedTLS.Cipher == "" { + t.Fatalf("implausible negotiation: %+v", s.NegotiatedTLS) + } + + plainSrv := httptest.NewServer(handler) + defer plainSrv.Close() + + p := Server{Name: "plain", Server: plainSrv.URL, PingURL: "/empty"} + + if !p.IsUp() { + t.Fatal("plain test server reported down") + } + if p.NegotiatedTLS != nil { + t.Fatalf("NegotiatedTLS set over plain HTTP: %+v", p.NegotiatedTLS) + } +} diff --git a/report/json.go b/report/json.go index 1cce819..c3b8cc2 100644 --- a/report/json.go +++ b/report/json.go @@ -19,6 +19,19 @@ type JSONReport struct { Upload float64 `json:"upload"` Download float64 `json:"download"` Share string `json:"share"` + + // TLS is what the connection to the server negotiated, absent over + // plain HTTP. On hardware without AES acceleration the cipher, not + // the link, can bound the result, and under TLS 1.3 the server picks + // it -- so two otherwise identical runs can differ several-fold for a + // reason the numbers alone do not show. + TLS *TLSReport `json:"tls,omitempty"` +} + +// TLSReport names the negotiated TLS parameters a measurement ran over. +type TLSReport struct { + Version string `json:"version"` + Cipher string `json:"cipher"` } // Server represents the speed test server's information diff --git a/speedtest/helper.go b/speedtest/helper.go index c5bbd26..48d3ef2 100644 --- a/speedtest/helper.go +++ b/speedtest/helper.go @@ -207,6 +207,13 @@ output.WriteDebug("IP info: %s\n", output.Sanitize(ispInfo.ProcessedString)) rep.Server.Name = currentServer.Name rep.Server.URL = u.String() + if currentServer.NegotiatedTLS != nil { + rep.TLS = &report.TLSReport{ + Version: currentServer.NegotiatedTLS.Version, + Cipher: currentServer.NegotiatedTLS.Cipher, + } + } + rep.Client = report.NewClient(ispInfo.RawISPInfo) rep.Client.Readme = "" // IP() falls back to processedString, so the report carries an