ooni-probe-cli/internal/tcprunner/tcprunner.go

193 lines
5.4 KiB
Go

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()
}