Basic SMTP/IMAP probe: look for bad TLS and StartTLS downgrade attacks
This commit is contained in:
parent
a0dc65641d
commit
eca06e83d0
7 changed files with 1272 additions and 0 deletions
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 a new issue