193 lines
5.4 KiB
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()
|
|
}
|