package measurexlite // // Dialer tracing // import ( "context" "log" "math" "net" "strconv" "time" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/netxlite" "github.com/ooni/probe-cli/v3/internal/tracex" ) // NewDialerWithoutResolver is equivalent to netxlite.NewDialerWithoutResolver // except that it returns a model.Dialer that uses this trace. // // Note: unlike code in netx or measurex, this factory DOES NOT return you a // dialer that also performs wrapping of a net.Conn in case of success. If you // want to wrap the conn, you need to wrap it explicitly using WrapNetConn. func (tx *Trace) NewDialerWithoutResolver(dl model.DebugLogger) model.Dialer { return &dialerTrace{ d: tx.newDialerWithoutResolver(dl), tx: tx, } } // dialerTrace is a trace-aware model.Dialer. type dialerTrace struct { d model.Dialer tx *Trace } var _ model.Dialer = &dialerTrace{} // DialContext implements model.Dialer.DialContext. func (d *dialerTrace) DialContext(ctx context.Context, network, address string) (net.Conn, error) { return d.d.DialContext(netxlite.ContextWithTrace(ctx, d.tx), network, address) } // CloseIdleConnections implements model.Dialer.CloseIdleConnections. func (d *dialerTrace) CloseIdleConnections() { d.d.CloseIdleConnections() } // OnTCPConnectDone implements model.Trace.OnTCPConnectDone. func (tx *Trace) OnConnectDone( started time.Time, network, domain, remoteAddr string, err error, finished time.Time) { switch network { case "tcp", "tcp4", "tcp6": // insert into the tcpConnect buffer select { case tx.tcpConnect <- NewArchivalTCPConnectResult( tx.Index, started.Sub(tx.ZeroTime), remoteAddr, err, finished.Sub(tx.ZeroTime), ): default: // buffer is full } // insert into the networkEvent buffer // see https://github.com/ooni/probe/issues/2254 select { case tx.networkEvent <- NewArchivalNetworkEvent( tx.Index, started.Sub(tx.ZeroTime), netxlite.ConnectOperation, "tcp", remoteAddr, 0, err, finished.Sub(tx.ZeroTime), ): default: // buffer is full } default: // ignore UDP connect attempts because they cannot fail // in interesting ways that make sense for censorship } } // NewArchivalTCPConnectResult generates a model.ArchivalTCPConnectResult // from the available information right after connect returns. func NewArchivalTCPConnectResult(index int64, started time.Duration, address string, err error, finished time.Duration) *model.ArchivalTCPConnectResult { ip, port := archivalSplitHostPort(address) return &model.ArchivalTCPConnectResult{ IP: ip, Port: archivalPortToString(port), Status: model.ArchivalTCPConnectStatus{ Blocked: nil, Failure: tracex.NewFailure(err), Success: err == nil, }, T0: started.Seconds(), T: finished.Seconds(), TransactionID: index, } } // archivalSplitHostPort is like net.SplitHostPort but does not return an error. This // function returns two empty strings in case of any failure. func archivalSplitHostPort(endpoint string) (string, string) { addr, port, err := net.SplitHostPort(endpoint) if err != nil { log.Printf("BUG: archivalSplitHostPort: invalid endpoint: %s", endpoint) return "", "" } return addr, port } // archivalPortToString is like strconv.Atoi but does not return an error. This // function returns a zero port number in case of any failure. func archivalPortToString(sport string) int { port, err := strconv.Atoi(sport) if err != nil || port < 0 || port > math.MaxUint16 { log.Printf("BUG: archivalStrconvAtoi: invalid port: %s", sport) return 0 } return port } // TCPConnects drains the network events buffered inside the TCPConnect channel. func (tx *Trace) TCPConnects() (out []*model.ArchivalTCPConnectResult) { for { select { case ev := <-tx.tcpConnect: out = append(out, ev) default: return // done } } } // FirstTCPConnectOrNil drains the network events buffered inside the TCPConnect channel // and returns the first TCPConnect, if any. Otherwise, it returns nil. func (tx *Trace) FirstTCPConnectOrNil() *model.ArchivalTCPConnectResult { ev := tx.TCPConnects() if len(ev) < 1 { return nil } return ev[0] }