diff --git a/internal/experiment/tlsmiddlebox/tcp_traceroute_probe.go b/internal/experiment/tlsmiddlebox/tcp_traceroute_probe.go new file mode 100644 index 0000000000..ab7cb5f784 --- /dev/null +++ b/internal/experiment/tlsmiddlebox/tcp_traceroute_probe.go @@ -0,0 +1,199 @@ +package tlsmiddlebox + +import ( + "encoding/binary" + "fmt" + "net" + "strconv" + "sync" + "time" + "unsafe" + + "github.com/ooni/probe-cli/v3/internal/logx" + "github.com/ooni/probe-cli/v3/internal/model" + "golang.org/x/sys/unix" +) + +func probeTCP(address string, ttl int, timeoutMS int, wg *sync.WaitGroup, logger model.Logger, index int64) (*ICMPIteration, error) { + defer wg.Done() + host, portString, err := net.SplitHostPort(address) + + port, err := strconv.Atoi(portString) + + fd, err := unix.Socket(unix.AF_INET, unix.SOCK_STREAM, unix.IPPROTO_TCP) + + if err != nil { + return nil, err + } + + defer unix.Close(fd) + + if err := unix.SetsockoptInt(fd, unix.IPPROTO_IP, unix.IP_RECVERR, 1); err != nil { + return nil, err + } + + if err := unix.SetNonblock(fd, true); err != nil { + return nil, err + } + + if err := unix.SetsockoptInt(fd, unix.IPPROTO_IP, unix.IP_TTL, ttl); err != nil { + return nil, err + } + + timestampFlags := unix.SOF_TIMESTAMPING_TX_SOFTWARE | + unix.SOF_TIMESTAMPING_RX_SOFTWARE | + unix.SOF_TIMESTAMPING_SOFTWARE + + if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_TIMESTAMPING, timestampFlags); err != nil { + return nil, err + } + + ip := net.ParseIP(host).To4() + if ip == nil { + return nil, fmt.Errorf("invalid IPv4 address") + } + + var addr [4]byte + copy(addr[:], ip) + + sa := &unix.SockaddrInet4{ + Port: port, + Addr: addr, + } + + var txTime *time.Time + + txTimeVal := time.Now() + txTime = &txTimeVal + + err = unix.Connect(fd, sa) + if err != nil && err != unix.EINPROGRESS { + return nil, err + } + + pfds := []unix.PollFd{ + { + Fd: int32(fd), + Events: unix.POLLOUT | unix.POLLERR, + }, + } + + n, err := unix.Poll(pfds, timeoutMS) + if err != nil { + return nil, err + } + + if n == 0 { + ol := logx.NewOperationLogger(logger, "Traceroute #%d TTL %d %s TIMEOUT", index, ttl, address) + ol.Stop(nil) + return nil, nil //change this + } + + if pfds[0].Revents&unix.POLLERR != 0 { + + buf := make([]byte, 64) + oob := make([]byte, 512) + + var data []byte + var rxTime *time.Time + var eeType byte + var eeCode byte + var ip net.IP + + for { + _, oobn, _, _, err := unix.Recvmsg(fd, buf, oob, unix.MSG_ERRQUEUE) + + if err == unix.EAGAIN { + break + } + + if err != nil { + return nil, err + } + + cms, err := unix.ParseSocketControlMessage(oob[:oobn]) + if err != nil { + return nil, err + } + + for _, cm := range cms { + + switch { + case cm.Header.Level == unix.SOL_SOCKET && + cm.Header.Type == unix.SO_TIMESTAMPING: + var ts [3]unix.Timespec + if len(cm.Data) >= int(unsafe.Sizeof([3]unix.Timespec{})) { + ts = *(*[3]unix.Timespec)(unsafe.Pointer(&cm.Data[0])) + } + + if ts[0].Sec != 0 { + t := time.Unix(ts[0].Sec, ts[0].Nsec) + rxTime = &t + } + + case cm.Header.Level == unix.IPPROTO_IP && + cm.Header.Type == unix.IP_RECVERR: + data = cm.Data + + if len(data) < 16 { + continue + } + + eeType = data[5] + eeCode = data[6] + + if len(data) >= 24 { + family := binary.LittleEndian.Uint16(data[16:18]) + + if family == unix.AF_INET { + ip = net.IP(data[20:24]) + } + } + + } + } + } + + var t0, t float64 + + if txTime != nil { + t0 = float64(txTime.UnixMilli()) + } + + if rxTime != nil { + t = float64(rxTime.UnixMilli()) + } + + ii := &ICMPIteration{ + TTL: ttl, + ICMPError: &model.ArchivalICMPErrorMessage{ + Timeout: "no", + SrcIP: ip.String(), + Type: int(eeType), + Code: int(eeCode), + T0: t0, + T: t, + }, + } + ol := logx.NewOperationLogger(logger, "Traceroute #%d TTL %d %s Router %s", index, ttl, address, ip.String()) + ol.Stop(err) + return ii, nil + } + + if pfds[0].Revents&unix.POLLOUT != 0 { + soerr, err := unix.GetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_ERROR) + if err != nil { + return nil, err + } + + if soerr == 0 { + ol := logx.NewOperationLogger(logger, "Traceroute #%d TTL %d %s CONNECTED", index, ttl, address) + ol.Stop(nil) + } else { + ol := logx.NewOperationLogger(logger, "Traceroute #%d TTL %d %s SO_ERROR %v", index, ttl, address, unix.Errno(soerr)) + ol.Stop(nil) + } + } + + return nil, nil +} diff --git a/internal/experiment/tlsmiddlebox/trace.go b/internal/experiment/tlsmiddlebox/trace.go index a3fbae9b26..49aac7114c 100644 --- a/internal/experiment/tlsmiddlebox/trace.go +++ b/internal/experiment/tlsmiddlebox/trace.go @@ -10,9 +10,10 @@ import ( // CompleteTrace records the result of the network trace // using a control SNI and a target SNI type CompleteTrace struct { - Address string `json:"address"` - ControlTrace *IterativeTrace `json:"control_trace"` - TargetTrace *IterativeTrace `json:"target_trace"` + Address string `json:"address"` + TCPTraceroute *IterativeTraceroute `json:"tcp_traceroute"` + ControlTrace *IterativeTrace `json:"control_trace"` + TargetTrace *IterativeTrace `json:"target_trace"` } // Trace is an iterative trace for the corresponding servername and address @@ -29,6 +30,21 @@ type Iteration struct { Handshake *model.ArchivalTLSOrQUICHandshakeResult `json:"handshake"` } +// IterativeTraceroute is an iterative traceroute towards the address +type IterativeTraceroute struct { + SNI string `json:"server_name"` + Iterations []*ICMPIteration `json:"iterations"` + + mu sync.Mutex +} + +// ICMPIteration is a single ICMP error message associated with a TTL-limited +// probe when used for traceroute +type ICMPIteration struct { + TTL int `json:"ttl"` + ICMPError *model.ArchivalICMPErrorMessage `json:"icmp_error"` +} + // NewIterationFromHandshake returns a new iteration from a model.ArchivalTLSOrQUICHandshakeResult func newIterationFromHandshake(ttl int, err error, soErr error, handshake *model.ArchivalTLSOrQUICHandshakeResult) *Iteration { if err != nil { @@ -46,9 +62,34 @@ func newIterationFromHandshake(ttl int, err error, soErr error, handshake *model } } +// NewICMPIterationFromHandshake returns a new iteration from a model.ArchivalICMPErrorMessage +func newICMPIterationFromHandshake(ttl int, err error, soErr error, icmpError *model.ArchivalICMPErrorMessage) *ICMPIteration { + if err != nil { + return &ICMPIteration{ + TTL: ttl, + ICMPError: &model.ArchivalICMPErrorMessage{ + Timeout: "yes", + }, + } + } + + return &ICMPIteration{ + TTL: ttl, + ICMPError: icmpError, + } + +} + // addIterations adds iterations to the trace func (t *IterativeTrace) addIterations(ev ...*Iteration) { t.mu.Lock() t.Iterations = append(t.Iterations, ev...) t.mu.Unlock() } + +// addIterationsTraceroute adds iterations to the traceroute +func (t *IterativeTraceroute) addIterationsTraceroute(ev ...*ICMPIteration) { + t.mu.Lock() + t.Iterations = append(t.Iterations, ev...) + t.mu.Unlock() +} diff --git a/internal/experiment/tlsmiddlebox/tracing.go b/internal/experiment/tlsmiddlebox/tracing.go index dd55b40457..ffc3a78112 100644 --- a/internal/experiment/tlsmiddlebox/tracing.go +++ b/internal/experiment/tlsmiddlebox/tracing.go @@ -33,12 +33,37 @@ var ClientIDs = map[int]*utls.ClientHelloID{ // TLSTrace performs tracing using control and target SNI func (m *Measurer) TLSTrace(ctx context.Context, index int64, zeroTime time.Time, logger model.Logger, address string, targetSNI string, trace *CompleteTrace) { + // perform a TCP traceroute + trace.TCPTraceroute = m.runTraceroute(ctx, index, zeroTime, logger, address, targetSNI) // perform an iterative trace with the control SNI trace.ControlTrace = m.startIterativeTrace(ctx, index, zeroTime, logger, address, m.config.snicontrol()) // perform an iterative trace with the target SNI trace.TargetTrace = m.startIterativeTrace(ctx, index, zeroTime, logger, address, targetSNI) } +func (m *Measurer) runTraceroute(ctx context.Context, index int64, zeroTime time.Time, logger model.Logger, + address string, sni string) (tr *IterativeTraceroute) { + tr = &IterativeTraceroute{ + SNI: sni, + Iterations: []*ICMPIteration{}, + } + maxTTL := m.config.maxttl() + ticker := time.NewTicker(m.config.delay()) + wg := new(sync.WaitGroup) + for i := int64(1); i <= maxTTL; i++ { + wg.Add(1) + icmpIteration, err := probeTCP(address, int(i), 3000, wg, logger, index) + if err != nil { + return + } + tr.addIterationsTraceroute(icmpIteration) + + <-ticker.C + } + wg.Wait() + return +} + // startIterativeTrace creates a Trace and calls iterativeTrace func (m *Measurer) startIterativeTrace(ctx context.Context, index int64, zeroTime time.Time, logger model.Logger, address string, sni string) (tr *IterativeTrace) { diff --git a/internal/model/archival.go b/internal/model/archival.go index 1930dc53c5..cf5e77489c 100644 --- a/internal/model/archival.go +++ b/internal/model/archival.go @@ -171,6 +171,36 @@ func (value *ArchivalScrubbedMaybeBinaryString) UnmarshalJSON(rawData []byte) er return nil } +// +// ICMP +// + +// ArchivalICMPErrorMessage is the result of any ICMP error message. +// +// See https://github.com/ooni/spec/blob/master/data-formats/df-002-dnst.md. //update this, will also add timestamp info +type ArchivalICMPErrorMessage struct { + Timeout string `json:"timeout"` + SrcIP string `json:"source_ip"` + DstIP string `json:"destination_ip"` + Type int `json:"type"` + Code int `json:"code"` + Quote []ArchivalICMPQuotation `json:"quote"` + T0 float64 `json:"t0,omitempty"` + T float64 `json:"t"` +} + +// ArchivalICMPQuotation is the quotation of an ICMP error message. +type ArchivalICMPQuotation struct { + SrcIP string `json:"source_ip"` + DstIP string `json:"destination_ip"` + Protocol string `json:"protocol"` + SrcPort int `json:"source_port"` + DstPort int `json:"destination_port"` + TCPSequenceNumber int `json:"tcp_sequence_number"` + UDPLength int `json:"udp_length"` + UDPChecksum int `json:"udp_checksum"` +} + // // DNS lookup //