Basic SMTP/IMAP probe: look for bad TLS and StartTLS downgrade attacks
This commit is contained in:
parent
a0dc65641d
commit
eca06e83d0
325
internal/engine/experiment/imap/imap.go
Normal file
325
internal/engine/experiment/imap/imap.go
Normal file
|
@ -0,0 +1,325 @@
|
|||
package imap
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
||||
"github.com/ooni/probe-cli/v3/internal/measurexlite"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/tcprunner"
|
||||
"github.com/ooni/probe-cli/v3/internal/tracex"
|
||||
)
|
||||
|
||||
var (
|
||||
// errNoInputProvided indicates you didn't provide any input
|
||||
errNoInputProvided = errors.New("not input provided")
|
||||
|
||||
// errInputIsNotAnURL indicates that input is not an URL
|
||||
errInputIsNotAnURL = errors.New("input is not an URL")
|
||||
|
||||
// errInvalidScheme indicates that the scheme is invalid
|
||||
errInvalidScheme = errors.New("scheme must be imap(s)")
|
||||
)
|
||||
|
||||
const (
|
||||
testName = "imap"
|
||||
testVersion = "0.0.1"
|
||||
)
|
||||
|
||||
// Config contains the experiment config.
|
||||
type Config struct{}
|
||||
|
||||
type runtimeConfig struct {
|
||||
host string
|
||||
port string
|
||||
forcedTLS bool
|
||||
noopCount uint8
|
||||
}
|
||||
|
||||
func config(input model.MeasurementTarget) (*runtimeConfig, error) {
|
||||
if input == "" {
|
||||
// TODO: static input data (eg. gmail/riseup..)
|
||||
return nil, errNoInputProvided
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(string(input))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", errInputIsNotAnURL, err.Error())
|
||||
}
|
||||
if parsed.Scheme != "imap" && parsed.Scheme != "imaps" {
|
||||
return nil, errInvalidScheme
|
||||
}
|
||||
|
||||
port := ""
|
||||
|
||||
if parsed.Port() == "" {
|
||||
// Default ports for StartTLS and forced TLS respectively
|
||||
if parsed.Scheme == "imap" {
|
||||
port = "143"
|
||||
} else {
|
||||
port = "993"
|
||||
}
|
||||
} else {
|
||||
// Valid port is checked by URL parsing
|
||||
port = parsed.Port()
|
||||
}
|
||||
|
||||
validConfig := runtimeConfig{
|
||||
host: parsed.Hostname(),
|
||||
forcedTLS: parsed.Scheme == "imaps",
|
||||
port: port,
|
||||
noopCount: 10,
|
||||
}
|
||||
|
||||
return &validConfig, nil
|
||||
}
|
||||
|
||||
// TestKeys contains the experiment results for an entire domain host
|
||||
type TestKeys struct {
|
||||
Host string `json:"hostname"`
|
||||
Queries []*model.ArchivalDNSLookupResult `json:"queries"`
|
||||
// Individual IP/port results
|
||||
Runs []*IndividualTestKeys `json:"runs"`
|
||||
// Used for global failure (DNS resolution)
|
||||
Failure string `json:"failure"`
|
||||
}
|
||||
|
||||
func newTestKeys(host string) *TestKeys {
|
||||
tk := new(TestKeys)
|
||||
tk.Host = host
|
||||
return tk
|
||||
}
|
||||
|
||||
// Hostname TCPRunnerModel
|
||||
func (tk *TestKeys) Hostname(host string) {
|
||||
tk.Host = host
|
||||
}
|
||||
|
||||
// DNSResults TCPRunnerModel
|
||||
func (tk *TestKeys) DNSResults(res []*model.ArchivalDNSLookupResult) {
|
||||
// TODO: not sure if we are passed the overall trace results and should overwrite key, or just append
|
||||
tk.Queries = append(tk.Queries, res...)
|
||||
}
|
||||
|
||||
// Failed TCPRunnerModel
|
||||
func (tk *TestKeys) Failed(msg string) {
|
||||
tk.Failure = msg
|
||||
}
|
||||
|
||||
// NewRun TCPRunnerModel
|
||||
func (tk *TestKeys) NewRun(addr string, port string) tcprunner.TCPSessionModel {
|
||||
itk := newIndividualTestKeys(addr, port)
|
||||
tk.Runs = append(tk.Runs, itk)
|
||||
return itk
|
||||
}
|
||||
|
||||
// IndividualTestKeys contains the experiment results for a single IP/port combo
|
||||
type IndividualTestKeys struct {
|
||||
TCPConnect []*model.ArchivalTCPConnectResult `json:"tcp_connect"`
|
||||
TLSHandshake *model.ArchivalTLSOrQUICHandshakeResult `json:"tls_handshakes"`
|
||||
Failure string `json:"failure"`
|
||||
FailureStep string `json:"failed_step"`
|
||||
IP string `json:"ip"`
|
||||
Port string `json:"port"`
|
||||
noopCounter uint8
|
||||
}
|
||||
|
||||
func newIndividualTestKeys(addr string, port string) *IndividualTestKeys {
|
||||
itk := new(IndividualTestKeys)
|
||||
itk.IP = addr
|
||||
itk.Port = port
|
||||
return itk
|
||||
}
|
||||
|
||||
// IPPort TCPSessionModel
|
||||
func (itk *IndividualTestKeys) IPPort(ip string, port string) {
|
||||
itk.IP = ip
|
||||
itk.Port = port
|
||||
}
|
||||
|
||||
// ConnectResults TCPSessionModel
|
||||
func (itk *IndividualTestKeys) ConnectResults(res []*model.ArchivalTCPConnectResult) {
|
||||
itk.TCPConnect = append(itk.TCPConnect, res...)
|
||||
}
|
||||
|
||||
// HandshakeResult TCPSessionModel
|
||||
func (itk *IndividualTestKeys) HandshakeResult(res *model.ArchivalTLSOrQUICHandshakeResult) {
|
||||
itk.TLSHandshake = res
|
||||
}
|
||||
|
||||
// FailedStep TCPSessionModel
|
||||
func (itk *IndividualTestKeys) FailedStep(failure string, step string) {
|
||||
itk.Failure = failure
|
||||
itk.FailureStep = step
|
||||
}
|
||||
|
||||
// Measurer performs the measurement.
|
||||
type Measurer struct {
|
||||
// Config contains the experiment settings. If empty we
|
||||
// will be using default settings.
|
||||
Config Config
|
||||
|
||||
// Getter is an optional getter to be used for testing.
|
||||
Getter urlgetter.MultiGetter
|
||||
}
|
||||
|
||||
// ExperimentName implements ExperimentMeasurer.ExperimentName
|
||||
func (m Measurer) ExperimentName() string {
|
||||
return testName
|
||||
}
|
||||
|
||||
// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion
|
||||
func (m Measurer) ExperimentVersion() string {
|
||||
return testVersion
|
||||
}
|
||||
|
||||
// Run implements ExperimentMeasurer.Run
|
||||
func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error {
|
||||
sess := args.Session
|
||||
measurement := args.Measurement
|
||||
log := sess.Logger()
|
||||
trace := measurexlite.NewTrace(0, measurement.MeasurementStartTimeSaved)
|
||||
|
||||
config, err := config(measurement.Input)
|
||||
if err != nil {
|
||||
// Invalid input data, we don't even generate report
|
||||
return err
|
||||
}
|
||||
|
||||
tk := new(TestKeys)
|
||||
measurement.TestKeys = tk
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
tlsconfig := tls.Config{
|
||||
InsecureSkipVerify: false,
|
||||
ServerName: config.host,
|
||||
}
|
||||
|
||||
runner := &tcprunner.TCPRunner{
|
||||
Tk: tk,
|
||||
Trace: trace,
|
||||
Logger: log,
|
||||
Ctx: ctx,
|
||||
Tlsconfig: &tlsconfig,
|
||||
}
|
||||
|
||||
// First resolve DNS
|
||||
addrs, success := runner.Resolve(config.host)
|
||||
if !success {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
tcpSession, success := runner.Conn(addr, config.port)
|
||||
if !success {
|
||||
continue
|
||||
}
|
||||
defer tcpSession.Close()
|
||||
|
||||
if config.forcedTLS {
|
||||
log.Infof("Running direct TLS mode to %s:%s", addr, config.port)
|
||||
|
||||
if !tcpSession.Handshake() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Try NoOps
|
||||
if !testIMAP(tcpSession, config.noopCount) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
log.Infof("Running StartTLS mode to %s:%s", addr, config.port)
|
||||
|
||||
// Upgrade via StartTLS and try NoOps
|
||||
if !tcpSession.StartTLS("A1 STARTTLS\n", "TLS") {
|
||||
continue
|
||||
}
|
||||
|
||||
if !testIMAP(tcpSession, config.noopCount) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func testIMAP(s *tcprunner.TCPSession, noop uint8) bool {
|
||||
// Auto-choose plaintext/TCP session
|
||||
// TODO: move to Debugf
|
||||
s.Runner.Logger.Infof("Retrieving existing connection")
|
||||
conn := s.CurrentConn()
|
||||
s.Runner.Logger.Infof("Starting IMAP query")
|
||||
|
||||
command, err := bufio.NewReader(conn).ReadString('\n')
|
||||
if err != nil {
|
||||
s.FailedStep(*tracex.NewFailure(err), "imap_wait_capability")
|
||||
return false
|
||||
}
|
||||
|
||||
if !strings.Contains(command, "CAPABILITY") {
|
||||
s.FailedStep(fmt.Sprintf("Received unexpected IMAP response: %s", command), "imap_wrong_capability")
|
||||
return false
|
||||
}
|
||||
|
||||
s.Runner.Logger.Infof("Finished starting IMAP")
|
||||
|
||||
if noop > 0 {
|
||||
// Downcast TCPSession's itk into typed IndividualTestKeys to access noopCounter field
|
||||
concreteITK := s.Itk.(*IndividualTestKeys)
|
||||
s.Runner.Logger.Infof("Trying to generate more no-op traffic")
|
||||
concreteITK.noopCounter = 0
|
||||
for concreteITK.noopCounter < noop {
|
||||
concreteITK.noopCounter++
|
||||
s.Runner.Logger.Infof("NoOp Iteration %d", concreteITK.noopCounter)
|
||||
_, err = conn.Write([]byte("A1 NOOP\n"))
|
||||
if err != nil {
|
||||
s.FailedStep(*tracex.NewFailure(err), fmt.Sprintf("imap_noop_%d", concreteITK.noopCounter))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if concreteITK.noopCounter == noop {
|
||||
s.Runner.Logger.Infof("Successfully generated no-op traffic")
|
||||
return true
|
||||
}
|
||||
s.Runner.Logger.Warnf("Failed no-op traffic at iteration %d", concreteITK.noopCounter)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// NewExperimentMeasurer creates a new ExperimentMeasurer.
|
||||
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
|
||||
return Measurer{Config: config}
|
||||
}
|
||||
|
||||
// SummaryKeys contains summary keys for this experiment.
|
||||
//
|
||||
// Note that this structure is part of the ABI contract with ooniprobe
|
||||
// therefore we should be careful when changing it.
|
||||
type SummaryKeys struct {
|
||||
//DNSBlocking bool `json:"facebook_dns_blocking"`
|
||||
//TCPBlocking bool `json:"facebook_tcp_blocking"`
|
||||
IsAnomaly bool `json:"-"`
|
||||
}
|
||||
|
||||
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
|
||||
func (m Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
|
||||
sk := SummaryKeys{IsAnomaly: false}
|
||||
_, ok := measurement.TestKeys.(*TestKeys)
|
||||
if !ok {
|
||||
return sk, errors.New("invalid test keys type")
|
||||
}
|
||||
return sk, nil
|
||||
}
|
192
internal/engine/experiment/imap/imap_test.go
Normal file
192
internal/engine/experiment/imap/imap_test.go
Normal file
|
@ -0,0 +1,192 @@
|
|||
package imap
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
//"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func plaintextListener() net.Listener {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
if l, err = net.Listen("tcp6", "[::1]:0"); err != nil {
|
||||
panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err))
|
||||
}
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func tlsListener(l net.Listener) net.Listener {
|
||||
return tls.NewListener(l, &tls.Config{})
|
||||
}
|
||||
|
||||
func listenerAddr(l net.Listener) string {
|
||||
return l.Addr().String()
|
||||
}
|
||||
|
||||
func ValidIMAPServer(conn net.Conn) {
|
||||
starttls := false
|
||||
for {
|
||||
command, err := bufio.NewReader(conn).ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(command, "NOOP") {
|
||||
conn.Write([]byte("A1 OK NOOP completed.\n"))
|
||||
} else if command == "STARTTLS" {
|
||||
starttls = true
|
||||
conn.Write([]byte("A1 OK Begin TLS negotiation now.\n"))
|
||||
// TODO: conn.Close does not actually close connection? or does client not detect it?
|
||||
//conn.Close()
|
||||
return
|
||||
} else if starttls {
|
||||
conn.Write([]byte("GARBAGE TO BREAK STARTTLS"))
|
||||
}
|
||||
conn.Write([]byte("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
func TCPServer(l net.Listener) {
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer conn.Close()
|
||||
conn.Write([]byte("* OK [CAPABILITY IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE LITERAL+ STARTTLS LOGINDISABLED] howdy, ready.\n"))
|
||||
ValidIMAPServer(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeasurer_run(t *testing.T) {
|
||||
// runHelper is an helper function to run this set of tests.
|
||||
runHelper := func(input string) (*model.Measurement, model.ExperimentMeasurer, error) {
|
||||
m := NewExperimentMeasurer(Config{})
|
||||
if m.ExperimentName() != "imap" {
|
||||
t.Fatal("invalid experiment name")
|
||||
}
|
||||
if m.ExperimentVersion() != "0.0.1" {
|
||||
t.Fatal("invalid experiment version")
|
||||
}
|
||||
ctx := context.Background()
|
||||
meas := &model.Measurement{
|
||||
Input: model.MeasurementTarget(input),
|
||||
}
|
||||
sess := &mockable.Session{
|
||||
MockableLogger: model.DiscardLogger,
|
||||
}
|
||||
|
||||
args := &model.ExperimentArgs{
|
||||
Callbacks: model.NewPrinterCallbacks(model.DiscardLogger),
|
||||
Measurement: meas,
|
||||
Session: sess,
|
||||
}
|
||||
|
||||
err := m.Run(ctx, args)
|
||||
return meas, m, err
|
||||
}
|
||||
|
||||
t.Run("with empty input", func(t *testing.T) {
|
||||
_, _, err := runHelper("")
|
||||
if !errors.Is(err, errNoInputProvided) {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with invalid URL", func(t *testing.T) {
|
||||
_, _, err := runHelper("\t")
|
||||
if !errors.Is(err, errInputIsNotAnURL) {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with invalid scheme", func(t *testing.T) {
|
||||
_, _, err := runHelper("https://8.8.8.8:443/")
|
||||
if !errors.Is(err, errInvalidScheme) {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with broken TLS", func(t *testing.T) {
|
||||
p := plaintextListener()
|
||||
defer p.Close()
|
||||
|
||||
l := tlsListener(p)
|
||||
defer l.Close()
|
||||
addr := listenerAddr(l)
|
||||
go TCPServer(l)
|
||||
|
||||
meas, m, err := runHelper("imaps://" + addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tk := meas.TestKeys.(*TestKeys)
|
||||
|
||||
for _, run := range tk.Runs {
|
||||
if *run.TLSHandshake.Failure != "unknown_failure: remote error: tls: unrecognized name" {
|
||||
t.Fatal("expected unrecognized_name in TLS handshake")
|
||||
}
|
||||
|
||||
if run.noopCounter != 0 {
|
||||
t.Fatalf("expected to not have any noops, not %d noops", run.noopCounter)
|
||||
}
|
||||
}
|
||||
|
||||
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("with broken starttls", func(t *testing.T) {
|
||||
l := plaintextListener()
|
||||
defer l.Close()
|
||||
addr := listenerAddr(l)
|
||||
|
||||
go TCPServer(l)
|
||||
|
||||
meas, m, err := runHelper("imap://" + addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tk := meas.TestKeys.(*TestKeys)
|
||||
//bs, _ := json.Marshal(tk)
|
||||
//fmt.Println(string(bs))
|
||||
|
||||
for _, run := range tk.Runs {
|
||||
if *run.TLSHandshake.Failure != "unknown_failure: tls: first record does not look like a TLS handshake" {
|
||||
t.Fatalf("s%ss", *run.TLSHandshake.Failure)
|
||||
t.Fatal("expected broken handshake")
|
||||
}
|
||||
|
||||
if run.noopCounter != 0 {
|
||||
t.Fatalf("expected to not have any noops, not %d noops", run.noopCounter)
|
||||
}
|
||||
}
|
||||
|
||||
ask, err := m.GetSummaryKeys(meas)
|
||||
if err != nil {
|
||||
t.Fatal("cannot obtain summary")
|
||||
}
|
||||
summary := ask.(SummaryKeys)
|
||||
if summary.IsAnomaly {
|
||||
t.Fatal("expected no anomaly")
|
||||
}
|
||||
})
|
||||
}
|
333
internal/engine/experiment/smtp/smtp.go
Normal file
333
internal/engine/experiment/smtp/smtp.go
Normal file
|
@ -0,0 +1,333 @@
|
|||
package smtp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"net/smtp"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
||||
"github.com/ooni/probe-cli/v3/internal/measurexlite"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/tcprunner"
|
||||
"github.com/ooni/probe-cli/v3/internal/tracex"
|
||||
)
|
||||
|
||||
var (
|
||||
// errNoInputProvided indicates you didn't provide any input
|
||||
errNoInputProvided = errors.New("not input provided")
|
||||
|
||||
// errInputIsNotAnURL indicates that input is not an URL
|
||||
errInputIsNotAnURL = errors.New("input is not an URL")
|
||||
|
||||
// errInvalidScheme indicates that the scheme is invalid
|
||||
errInvalidScheme = errors.New("scheme must be smtp(s)")
|
||||
)
|
||||
|
||||
const (
|
||||
testName = "smtp"
|
||||
testVersion = "0.0.1"
|
||||
)
|
||||
|
||||
// Config contains the experiment config.
|
||||
type Config struct{}
|
||||
|
||||
type runtimeConfig struct {
|
||||
host string
|
||||
port string
|
||||
forcedTLS bool
|
||||
noopCount uint8
|
||||
}
|
||||
|
||||
func config(input model.MeasurementTarget) (*runtimeConfig, error) {
|
||||
if input == "" {
|
||||
// TODO: static input data (eg. gmail/riseup..)
|
||||
return nil, errNoInputProvided
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(string(input))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", errInputIsNotAnURL, err.Error())
|
||||
}
|
||||
if parsed.Scheme != "smtp" && parsed.Scheme != "smtps" {
|
||||
return nil, errInvalidScheme
|
||||
}
|
||||
|
||||
port := ""
|
||||
|
||||
if parsed.Port() == "" {
|
||||
// Default ports for StartTLS and forced TLS respectively
|
||||
if parsed.Scheme == "smtp" {
|
||||
port = "587"
|
||||
} else {
|
||||
port = "465"
|
||||
}
|
||||
} else {
|
||||
// Valid port is checked by URL parsing
|
||||
port = parsed.Port()
|
||||
}
|
||||
|
||||
validConfig := runtimeConfig{
|
||||
host: parsed.Hostname(),
|
||||
forcedTLS: parsed.Scheme == "smtps",
|
||||
port: port,
|
||||
noopCount: 10,
|
||||
}
|
||||
|
||||
return &validConfig, nil
|
||||
}
|
||||
|
||||
// TestKeys contains the experiment results for an entire domain host
|
||||
type TestKeys struct {
|
||||
Host string `json:"hostname"`
|
||||
Queries []*model.ArchivalDNSLookupResult `json:"queries"`
|
||||
// Individual IP/port results
|
||||
Runs []*IndividualTestKeys `json:"runs"`
|
||||
// Used for global failure (DNS resolution)
|
||||
Failure string `json:"failure"`
|
||||
}
|
||||
|
||||
func newTestKeys(host string) *TestKeys {
|
||||
tk := new(TestKeys)
|
||||
tk.Host = host
|
||||
return tk
|
||||
}
|
||||
|
||||
// Hostname TCPRunnerModel
|
||||
func (tk *TestKeys) Hostname(host string) {
|
||||
tk.Host = host
|
||||
}
|
||||
|
||||
// DNSResults TCPRunnerModel
|
||||
func (tk *TestKeys) DNSResults(res []*model.ArchivalDNSLookupResult) {
|
||||
// TODO: not sure if we are passed the overall trace results and should overwrite key, or just append
|
||||
tk.Queries = append(tk.Queries, res...)
|
||||
}
|
||||
|
||||
// Failed TCPRunnerModel
|
||||
func (tk *TestKeys) Failed(msg string) {
|
||||
tk.Failure = msg
|
||||
}
|
||||
|
||||
// NewRun TCPRunnerModel
|
||||
func (tk *TestKeys) NewRun(addr string, port string) tcprunner.TCPSessionModel {
|
||||
itk := newIndividualTestKeys(addr, port)
|
||||
tk.Runs = append(tk.Runs, itk)
|
||||
return itk
|
||||
}
|
||||
|
||||
// IndividualTestKeys contains the experiment results for a single IP/port combo
|
||||
type IndividualTestKeys struct {
|
||||
TCPConnect []*model.ArchivalTCPConnectResult `json:"tcp_connect"`
|
||||
TLSHandshake *model.ArchivalTLSOrQUICHandshakeResult `json:"tls_handshakes"`
|
||||
Failure string `json:"failure"`
|
||||
FailureStep string `json:"failed_step"`
|
||||
IP string `json:"ip"`
|
||||
Port string `json:"port"`
|
||||
noopCounter uint8
|
||||
}
|
||||
|
||||
func newIndividualTestKeys(addr string, port string) *IndividualTestKeys {
|
||||
itk := new(IndividualTestKeys)
|
||||
itk.IP = addr
|
||||
itk.Port = port
|
||||
return itk
|
||||
}
|
||||
|
||||
// IPPort TCPSessionModel
|
||||
func (itk *IndividualTestKeys) IPPort(ip string, port string) {
|
||||
itk.IP = ip
|
||||
itk.Port = port
|
||||
}
|
||||
|
||||
// ConnectResults TCPSessionModel
|
||||
func (itk *IndividualTestKeys) ConnectResults(res []*model.ArchivalTCPConnectResult) {
|
||||
itk.TCPConnect = append(itk.TCPConnect, res...)
|
||||
}
|
||||
|
||||
// HandshakeResult TCPSessionModel
|
||||
func (itk *IndividualTestKeys) HandshakeResult(res *model.ArchivalTLSOrQUICHandshakeResult) {
|
||||
itk.TLSHandshake = res
|
||||
}
|
||||
|
||||
// FailedStep TCPSessionModel
|
||||
func (itk *IndividualTestKeys) FailedStep(failure string, step string) {
|
||||
itk.Failure = failure
|
||||
itk.FailureStep = step
|
||||
}
|
||||
|
||||
// Measurer performs the measurement.
|
||||
type Measurer struct {
|
||||
// Config contains the experiment settings. If empty we
|
||||
// will be using default settings.
|
||||
Config Config
|
||||
|
||||
// Getter is an optional getter to be used for testing.
|
||||
Getter urlgetter.MultiGetter
|
||||
}
|
||||
|
||||
// ExperimentName implements ExperimentMeasurer.ExperimentName
|
||||
func (m Measurer) ExperimentName() string {
|
||||
return testName
|
||||
}
|
||||
|
||||
// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion
|
||||
func (m Measurer) ExperimentVersion() string {
|
||||
return testVersion
|
||||
}
|
||||
|
||||
// Run implements ExperimentMeasurer.Run
|
||||
func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error {
|
||||
sess := args.Session
|
||||
measurement := args.Measurement
|
||||
log := sess.Logger()
|
||||
trace := measurexlite.NewTrace(0, measurement.MeasurementStartTimeSaved)
|
||||
|
||||
config, err := config(measurement.Input)
|
||||
if err != nil {
|
||||
// Invalid input data, we don't even generate report
|
||||
return err
|
||||
}
|
||||
|
||||
tk := new(TestKeys)
|
||||
measurement.TestKeys = tk
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
tlsconfig := tls.Config{
|
||||
InsecureSkipVerify: false,
|
||||
ServerName: config.host,
|
||||
}
|
||||
|
||||
runner := &tcprunner.TCPRunner{
|
||||
Tk: tk,
|
||||
Trace: trace,
|
||||
Logger: log,
|
||||
Ctx: ctx,
|
||||
Tlsconfig: &tlsconfig,
|
||||
}
|
||||
|
||||
// First resolve DNS
|
||||
addrs, success := runner.Resolve(config.host)
|
||||
if !success {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
tcpSession, success := runner.Conn(addr, config.port)
|
||||
if !success {
|
||||
continue
|
||||
}
|
||||
defer tcpSession.Close()
|
||||
|
||||
if config.forcedTLS {
|
||||
log.Infof("Running direct TLS mode to %s:%s", addr, config.port)
|
||||
|
||||
if !tcpSession.Handshake() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Try EHLO + NoOps
|
||||
if !testSMTP(tcpSession, "localhost", config.noopCount) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
log.Infof("Running StartTLS mode to %s:%s", addr, config.port)
|
||||
|
||||
if !testSMTP(tcpSession, "localhost", 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Upgrade via StartTLS and try EHLO + NoOps
|
||||
if !tcpSession.StartTLS("STARTTLS\n", "TLS") {
|
||||
continue
|
||||
}
|
||||
|
||||
if !testSMTP(tcpSession, "localhost", config.noopCount) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func testSMTP(s *tcprunner.TCPSession, ehlo string, noop uint8) bool {
|
||||
// Auto-choose plaintext/TCP session
|
||||
// TODO: move to Debugf
|
||||
s.Runner.Logger.Infof("Retrieving existing connection")
|
||||
conn := s.CurrentConn()
|
||||
s.Runner.Logger.Infof("Initializing SMTP client")
|
||||
client, err := smtp.NewClient(conn, ehlo)
|
||||
if err != nil {
|
||||
s.FailedStep(*tracex.NewFailure(err), "smtp_init")
|
||||
return false
|
||||
}
|
||||
|
||||
s.Runner.Logger.Infof("Starting SMTP EHLO")
|
||||
err = client.Hello(ehlo)
|
||||
if err != nil {
|
||||
if s.TLS {
|
||||
s.FailedStep(*tracex.NewFailure(err), "smtp_tls_ehlo")
|
||||
} else {
|
||||
s.FailedStep(*tracex.NewFailure(err), "smtp_plaintext_ehlo")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
s.Runner.Logger.Infof("Finished SMTP EHLO")
|
||||
|
||||
if noop > 0 {
|
||||
// Downcast TCPSession's itk into typed IndividualTestKeys to access noopCounter field
|
||||
concreteITK := s.Itk.(*IndividualTestKeys)
|
||||
s.Runner.Logger.Infof("Trying to generate more no-op traffic")
|
||||
concreteITK.noopCounter = 0
|
||||
for concreteITK.noopCounter < noop {
|
||||
concreteITK.noopCounter++
|
||||
s.Runner.Logger.Infof("NoOp Iteration %d", concreteITK.noopCounter)
|
||||
err = client.Noop()
|
||||
if err != nil {
|
||||
s.FailedStep(*tracex.NewFailure(err), fmt.Sprintf("smtp_noop_%d", concreteITK.noopCounter))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if concreteITK.noopCounter == noop {
|
||||
s.Runner.Logger.Infof("Successfully generated no-op traffic")
|
||||
return true
|
||||
}
|
||||
s.Runner.Logger.Warnf("Failed no-op traffic at iteration %d", concreteITK.noopCounter)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// NewExperimentMeasurer creates a new ExperimentMeasurer.
|
||||
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
|
||||
return Measurer{Config: config}
|
||||
}
|
||||
|
||||
// SummaryKeys contains summary keys for this experiment.
|
||||
//
|
||||
// Note that this structure is part of the ABI contract with ooniprobe
|
||||
// therefore we should be careful when changing it.
|
||||
type SummaryKeys struct {
|
||||
//DNSBlocking bool `json:"facebook_dns_blocking"`
|
||||
//TCPBlocking bool `json:"facebook_tcp_blocking"`
|
||||
IsAnomaly bool `json:"-"`
|
||||
}
|
||||
|
||||
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
|
||||
func (m Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
|
||||
sk := SummaryKeys{IsAnomaly: false}
|
||||
_, ok := measurement.TestKeys.(*TestKeys)
|
||||
if !ok {
|
||||
return sk, errors.New("invalid test keys type")
|
||||
}
|
||||
return sk, nil
|
||||
}
|
186
internal/engine/experiment/smtp/smtp_test.go
Normal file
186
internal/engine/experiment/smtp/smtp_test.go
Normal file
|
@ -0,0 +1,186 @@
|
|||
package smtp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func plaintextListener() net.Listener {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
if l, err = net.Listen("tcp6", "[::1]:0"); err != nil {
|
||||
panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err))
|
||||
}
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func tlsListener(l net.Listener) net.Listener {
|
||||
return tls.NewListener(l, &tls.Config{})
|
||||
}
|
||||
|
||||
func listenerAddr(l net.Listener) string {
|
||||
return l.Addr().String()
|
||||
}
|
||||
|
||||
func ValidSMTPServer(conn net.Conn) {
|
||||
for {
|
||||
command, err := bufio.NewReader(conn).ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if command == "" {
|
||||
} else if command == "NOOP" {
|
||||
conn.Write([]byte("250 2.0.0 Ok\n"))
|
||||
} else if command == "STARTTLS" {
|
||||
conn.Write([]byte("220 2.0.0 Ready to start TLS\n"))
|
||||
// TODO: conn.Close does not actually close connection? or does client not detect it?
|
||||
conn.Close()
|
||||
return
|
||||
} else if strings.HasPrefix(command, "EHLO") {
|
||||
conn.Write([]byte("250 mock.example.com\n"))
|
||||
}
|
||||
conn.Write([]byte("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
func TCPServer(l net.Listener) {
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer conn.Close()
|
||||
conn.Write([]byte("220 mock.example.com ESMTP (spam is not appreciated)\n"))
|
||||
ValidSMTPServer(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeasurer_run(t *testing.T) {
|
||||
// runHelper is an helper function to run this set of tests.
|
||||
runHelper := func(input string) (*model.Measurement, model.ExperimentMeasurer, error) {
|
||||
m := NewExperimentMeasurer(Config{})
|
||||
if m.ExperimentName() != "smtp" {
|
||||
t.Fatal("invalid experiment name")
|
||||
}
|
||||
if m.ExperimentVersion() != "0.0.1" {
|
||||
t.Fatal("invalid experiment version")
|
||||
}
|
||||
ctx := context.Background()
|
||||
meas := &model.Measurement{
|
||||
Input: model.MeasurementTarget(input),
|
||||
}
|
||||
sess := &mockable.Session{
|
||||
MockableLogger: model.DiscardLogger,
|
||||
}
|
||||
|
||||
args := &model.ExperimentArgs{
|
||||
Callbacks: model.NewPrinterCallbacks(model.DiscardLogger),
|
||||
Measurement: meas,
|
||||
Session: sess,
|
||||
}
|
||||
err := m.Run(ctx, args)
|
||||
return meas, m, err
|
||||
}
|
||||
|
||||
t.Run("with empty input", func(t *testing.T) {
|
||||
_, _, err := runHelper("")
|
||||
if !errors.Is(err, errNoInputProvided) {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with invalid URL", func(t *testing.T) {
|
||||
_, _, err := runHelper("\t")
|
||||
if !errors.Is(err, errInputIsNotAnURL) {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with invalid scheme", func(t *testing.T) {
|
||||
_, _, err := runHelper("https://8.8.8.8:443/")
|
||||
if !errors.Is(err, errInvalidScheme) {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with broken TLS", func(t *testing.T) {
|
||||
p := plaintextListener()
|
||||
defer p.Close()
|
||||
|
||||
l := tlsListener(p)
|
||||
defer l.Close()
|
||||
addr := listenerAddr(l)
|
||||
go TCPServer(l)
|
||||
|
||||
meas, m, err := runHelper("smtps://" + addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tk := meas.TestKeys.(*TestKeys)
|
||||
|
||||
for _, run := range tk.Runs {
|
||||
if *run.TLSHandshake.Failure != "unknown_failure: remote error: tls: unrecognized name" {
|
||||
t.Fatal("expected unrecognized_name in TLS handshake")
|
||||
}
|
||||
|
||||
if run.noopCounter != 0 {
|
||||
t.Fatalf("expected to not have any noops, not %d noops", run.noopCounter)
|
||||
}
|
||||
}
|
||||
|
||||
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("with broken starttls", func(t *testing.T) {
|
||||
l := plaintextListener()
|
||||
defer l.Close()
|
||||
addr := listenerAddr(l)
|
||||
|
||||
go TCPServer(l)
|
||||
|
||||
meas, m, err := runHelper("smtp://" + addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tk := meas.TestKeys.(*TestKeys)
|
||||
|
||||
for _, run := range tk.Runs {
|
||||
if *run.TLSHandshake.Failure != "generic_timeout_error" {
|
||||
t.Fatal("expected timeout in TLS handshake")
|
||||
}
|
||||
|
||||
if run.noopCounter != 0 {
|
||||
t.Fatalf("expected to not have any noops, not %d noops", run.noopCounter)
|
||||
}
|
||||
}
|
||||
|
||||
ask, err := m.GetSummaryKeys(meas)
|
||||
if err != nil {
|
||||
t.Fatal("cannot obtain summary")
|
||||
}
|
||||
summary := ask.(SummaryKeys)
|
||||
if summary.IsAnomaly {
|
||||
t.Fatal("expected no anomaly")
|
||||
}
|
||||
})
|
||||
}
|
22
internal/registry/imap.go
Normal file
22
internal/registry/imap.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package registry
|
||||
|
||||
//
|
||||
// Registers the 'imap' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/imap"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AllExperiments["imap"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return imap.NewExperimentMeasurer(
|
||||
*config.(*imap.Config),
|
||||
)
|
||||
},
|
||||
config: &imap.Config{},
|
||||
inputPolicy: model.InputOrStaticDefault,
|
||||
}
|
||||
}
|
22
internal/registry/smtp.go
Normal file
22
internal/registry/smtp.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package registry
|
||||
|
||||
//
|
||||
// Registers the 'smtp' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/smtp"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AllExperiments["smtp"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return smtp.NewExperimentMeasurer(
|
||||
*config.(*smtp.Config),
|
||||
)
|
||||
},
|
||||
config: &smtp.Config{},
|
||||
inputPolicy: model.InputOrStaticDefault,
|
||||
}
|
||||
}
|
192
internal/tcprunner/tcprunner.go
Normal file
192
internal/tcprunner/tcprunner.go
Normal file
|
@ -0,0 +1,192 @@
|
|||
package tcprunner
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/measurexlite"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/tracex"
|
||||
)
|
||||
|
||||
// Model describes a type that does a DNS lookup(s), then attempts several TCP sessions
|
||||
type Model interface {
|
||||
// Stores the provided hostname
|
||||
Hostname(string)
|
||||
// Store DNS query result
|
||||
DNSResults([]*model.ArchivalDNSLookupResult)
|
||||
// Indicates one or more steps failed (can be overwritten)
|
||||
Failed(string)
|
||||
// Stores a new individual test key (for a TCP session) and returns a pointer to it
|
||||
NewRun(string, string) TCPSessionModel
|
||||
}
|
||||
|
||||
// TCPSessionModel describes a type that does a single TCP connection and TLS handshake with a given IP/Port combo
|
||||
type TCPSessionModel interface {
|
||||
// Store IP/port address used for this session
|
||||
IPPort(string, string)
|
||||
// Store TCP connect result
|
||||
ConnectResults([]*model.ArchivalTCPConnectResult)
|
||||
// Store TLS handshake result
|
||||
HandshakeResult(*model.ArchivalTLSOrQUICHandshakeResult)
|
||||
// Indicates a failure string, as well as an identifier for the failed step
|
||||
FailedStep(string, string)
|
||||
}
|
||||
|
||||
// TCPRunner manages sequential TCP sessions to the same hostname (over different IPs)
|
||||
type TCPRunner struct {
|
||||
Tk Model
|
||||
Trace *measurexlite.Trace
|
||||
Logger model.Logger
|
||||
Ctx context.Context
|
||||
Tlsconfig *tls.Config
|
||||
}
|
||||
|
||||
// TCPSession Manages a single TCP session and TLS handshake to a given ip:port
|
||||
type TCPSession struct {
|
||||
Itk TCPSessionModel
|
||||
Runner *TCPRunner
|
||||
Addr string
|
||||
Port string
|
||||
TLS bool
|
||||
RawConn *net.Conn
|
||||
TLSConn *net.Conn
|
||||
}
|
||||
|
||||
// FailedStep saves a failure (with an associated failed step identifier) into IndividualTestKeys
|
||||
func (s *TCPSession) FailedStep(failure string, step string) {
|
||||
// Save FailedStep inside ITK
|
||||
s.Itk.FailedStep(failure, step)
|
||||
// Copy FailedStep to global TK
|
||||
s.Runner.Tk.Failed(failure)
|
||||
// Print the warning message
|
||||
s.Runner.Logger.Warn(failure)
|
||||
}
|
||||
|
||||
// Close closes the open TCP connections
|
||||
func (s *TCPSession) Close() {
|
||||
if s.TLS {
|
||||
var conn = *s.TLSConn
|
||||
conn.Close()
|
||||
} else {
|
||||
// TODO: should raw connection be closed anyway?
|
||||
var conn = *s.RawConn
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// CurrentConn returns the currently active connection (TLS or plaintext)
|
||||
func (s *TCPSession) CurrentConn() net.Conn {
|
||||
if s.TLS {
|
||||
// TODO: move to Debugf
|
||||
s.Runner.Logger.Infof("Reusing TLS connection")
|
||||
return *s.TLSConn
|
||||
}
|
||||
s.Runner.Logger.Infof("Reusing plaintext connection")
|
||||
return *s.RawConn
|
||||
}
|
||||
|
||||
// Conn initializes a new Run and IndividualTestKeys
|
||||
func (r *TCPRunner) Conn(addr string, port string) (*TCPSession, bool) {
|
||||
// Get new individual test keys
|
||||
itk := r.Tk.NewRun(addr, port)
|
||||
|
||||
s := new(TCPSession)
|
||||
s.Runner = r
|
||||
s.Itk = itk
|
||||
s.Addr = addr
|
||||
s.Port = port
|
||||
s.TLS = false
|
||||
|
||||
if !s.Conn(addr, port) {
|
||||
return nil, false
|
||||
}
|
||||
return s, true
|
||||
}
|
||||
|
||||
// Conn starts a new TCP/IP connection to addr/port
|
||||
func (s *TCPSession) Conn(addr string, port string) bool {
|
||||
dialer := s.Runner.Trace.NewDialerWithoutResolver(s.Runner.Logger)
|
||||
s.Runner.Logger.Infof("Dialing to %s:%s", addr, port)
|
||||
conn, err := dialer.DialContext(s.Runner.Ctx, "tcp", net.JoinHostPort(addr, port))
|
||||
s.Itk.ConnectResults(s.Runner.Trace.TCPConnects())
|
||||
if err != nil {
|
||||
s.FailedStep(*tracex.NewFailure(err), "tcp_connect")
|
||||
return false
|
||||
}
|
||||
s.RawConn = &conn
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Resolve resolves a hostname to a list of addresses
|
||||
func (r *TCPRunner) Resolve(host string) ([]string, bool) {
|
||||
r.Logger.Infof("Resolving DNS for %s", host)
|
||||
resolver := r.Trace.NewStdlibResolver(r.Logger)
|
||||
addrs, err := resolver.LookupHost(r.Ctx, host)
|
||||
r.Tk.DNSResults(r.Trace.DNSLookupsFromRoundTrip())
|
||||
if err != nil {
|
||||
r.Tk.Failed(*tracex.NewFailure(err))
|
||||
return []string{}, false
|
||||
}
|
||||
r.Logger.Infof("Finished DNS for %s: %v", host, addrs)
|
||||
|
||||
return addrs, true
|
||||
}
|
||||
|
||||
// Handshake performs a TLS handshake over the currently active connection
|
||||
func (s *TCPSession) Handshake() bool {
|
||||
if s.TLS {
|
||||
// TLS already initialized...
|
||||
return true
|
||||
}
|
||||
s.Runner.Logger.Infof("Starting TLS handshake with %s:%s", s.Addr, s.Port)
|
||||
thx := s.Runner.Trace.NewTLSHandshakerStdlib(s.Runner.Logger)
|
||||
tconn, _, err := thx.Handshake(s.Runner.Ctx, *s.RawConn, s.Runner.Tlsconfig)
|
||||
s.Itk.HandshakeResult(s.Runner.Trace.FirstTLSHandshakeOrNil())
|
||||
if err != nil {
|
||||
s.FailedStep(*tracex.NewFailure(err), "tls_handshake")
|
||||
return false
|
||||
}
|
||||
|
||||
s.TLS = true
|
||||
s.TLSConn = &tconn
|
||||
s.Runner.Logger.Infof("Handshake succeeded")
|
||||
return true
|
||||
}
|
||||
|
||||
// StartTLS performs a StartTLS exchange by sending a message over the plaintext connection, waiting for a specific
|
||||
// response, then performing a TLS handshake
|
||||
func (s *TCPSession) StartTLS(message string, waitForResponse string) bool {
|
||||
if s.TLS {
|
||||
s.Runner.Logger.Warn("Requested TCPSession to do StartTLS when TLS is already enabled")
|
||||
return true
|
||||
}
|
||||
|
||||
if message != "" {
|
||||
s.Runner.Logger.Infof("Asking for StartTLS upgrade")
|
||||
s.CurrentConn().Write([]byte(message))
|
||||
}
|
||||
|
||||
if waitForResponse != "" {
|
||||
s.Runner.Logger.Infof("Waiting for server response containing: %s", waitForResponse)
|
||||
conn := s.CurrentConn()
|
||||
for {
|
||||
line, err := bufio.NewReader(conn).ReadString('\n')
|
||||
if err != nil {
|
||||
s.FailedStep(*tracex.NewFailure(err), "starttls_wait_ok")
|
||||
return false
|
||||
}
|
||||
s.Runner.Logger.Debugf("Received: %s", line)
|
||||
if strings.Contains(line, waitForResponse) {
|
||||
s.Runner.Logger.Infof("Server is ready for StartTLS")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s.Handshake()
|
||||
}
|
Loading…
Reference in New Issue
Block a user