Skip to content
2 changes: 1 addition & 1 deletion internal/experiment/tlsmiddlebox/measurer.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (

const (
testName = "tlsmiddlebox"
testVersion = "0.1.2"
testVersion = "0.1.3"
)

// Measurer performs the measurement.
Expand Down
15 changes: 15 additions & 0 deletions internal/experiment/tlsmiddlebox/tcp_traceroute_otherwise.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//go:build !(aix || darwin || dragonfly || freebsd || (js && wasm) || linux || nacl || netbsd || openbsd || solaris)

package tlsmiddlebox

import (
"sync"

"github.com/ooni/probe-cli/v3/internal/model"
)

func tracerouteTCP(_ string, _ int, _ int, wg *sync.WaitGroup, _ model.Logger, _ int64) (*ICMPIteration, error) {
defer wg.Done()

return nil, nil
}
96 changes: 96 additions & 0 deletions internal/experiment/tlsmiddlebox/tcp_traceroute_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//go:build aix || darwin || dragonfly || freebsd || (js && wasm) || linux || nacl || netbsd || openbsd || solaris

package tlsmiddlebox

import (
"sync"
"testing"

"github.com/ooni/probe-cli/v3/internal/model"
)

func TestParseQuotedPacket(t *testing.T) {
t.Run("buffer too short", func(t *testing.T) {
buf := []byte("Hello")
quotedPacket, err := parseQuotedPacket(buf)
if quotedPacket == nil && (err == nil || err.Error() != "tcp quote too short") {
t.Fatal("failed to error due to quote being too short")
}
})

t.Run("success case", func(t *testing.T) {
buf := []byte{0xed, 0x6e, 0x01, 0xbb, 0x16, 0xed, 0xf5, 0x4c}
quotedPacket, err := parseQuotedPacket(buf)

if err != nil {
t.Fatal("unexpected error:", err)
}

if quotedPacket == nil {
t.Fatal("expected *model.ArchivalICMPQuotation, got nil")
}

if quotedPacket.Protocol != 6 {
t.Fatalf("expected protocol 6, got %d", quotedPacket.Protocol)
}

if quotedPacket.SrcPort != 60782 {
t.Fatalf("expected source port 60782, got %d", quotedPacket.SrcPort)
}

if quotedPacket.DstPort != 443 {
t.Fatalf("expected destination port 443, got %d", quotedPacket.DstPort)
}

if quotedPacket.TCPSeqNum != 384693580 {
t.Fatalf("expected TCP seq num 384693580, got %d", quotedPacket.TCPSeqNum)
}
})
}

func TestProbeTCP(t *testing.T) {
wg := new(sync.WaitGroup)
ttl := 2
index := int64(1)

t.Run("invalid address and port format", func(t *testing.T) {
wg.Add(1)
address := "1.2.3.4"
ii, err := probeTCP(address, ttl, 3000, wg, model.DiscardLogger, index)

Check failure on line 59 in internal/experiment/tlsmiddlebox/tcp_traceroute_test.go

View workflow job for this annotation

GitHub Actions / measure_coverage

undefined: probeTCP

Check failure on line 59 in internal/experiment/tlsmiddlebox/tcp_traceroute_test.go

View workflow job for this annotation

GitHub Actions / test

undefined: probeTCP
if ii != nil {
t.Fatalf("expected nil, got %T", ii)
}

if err.Error() != "address 1.2.3.4: missing port in address" {
t.Fatalf("expected 'address 1.2.3.4: missing port in address', got %s", err.Error())
}
})

t.Run("invalid IPv4 address", func(t *testing.T) {
wg.Add(1)
address := "298.125.34.4:443"
ii, err := probeTCP(address, ttl, 3000, wg, model.DiscardLogger, index)

Check failure on line 72 in internal/experiment/tlsmiddlebox/tcp_traceroute_test.go

View workflow job for this annotation

GitHub Actions / measure_coverage

undefined: probeTCP

Check failure on line 72 in internal/experiment/tlsmiddlebox/tcp_traceroute_test.go

View workflow job for this annotation

GitHub Actions / test

undefined: probeTCP
if ii != nil {
t.Fatalf("expected nil, got %T", ii)
}

if err.Error() != "invalid IPv4 address" {
t.Fatalf("expected 'invalid IPv4 address', got %s", err.Error())
}

})

t.Run("invalid port number", func(t *testing.T) {
wg.Add(1)
address := "298.125.34.4:spot"
ii, err := probeTCP(address, ttl, 3000, wg, model.DiscardLogger, index)

Check failure on line 86 in internal/experiment/tlsmiddlebox/tcp_traceroute_test.go

View workflow job for this annotation

GitHub Actions / measure_coverage

undefined: probeTCP

Check failure on line 86 in internal/experiment/tlsmiddlebox/tcp_traceroute_test.go

View workflow job for this annotation

GitHub Actions / test

undefined: probeTCP
if ii != nil {
t.Fatalf("expected nil, got %T", ii)
}

if err.Error() != "strconv.Atoi: parsing \"spot\": invalid syntax" {
t.Fatalf("expected 'strconv.Atoi: parsing \"spot\": invalid syntax', got %s", err.Error())
}
})

}
262 changes: 262 additions & 0 deletions internal/experiment/tlsmiddlebox/tcp_traceroute_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
//go:build aix || darwin || dragonfly || freebsd || (js && wasm) || linux || nacl || netbsd || openbsd || solaris

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 parseQuotedPacket(buf []byte) (*model.ArchivalICMPQuotation, error) {
if len(buf) < 8 {
return nil, fmt.Errorf("tcp quote too short")
}

quotedPacket := &model.ArchivalICMPQuotation{
Protocol: 6,
SrcPort: int(binary.BigEndian.Uint16(buf[0:2])),
DstPort: int(binary.BigEndian.Uint16(buf[2:4])),
TCPSeqNum: binary.BigEndian.Uint32(buf[4:8]),
}

return quotedPacket, nil
}

func tracerouteTCP(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)

if err != nil {
return nil, err
}

port, err := strconv.Atoi(portString)

if err != nil {
return nil, err
}

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)
ii_timeout := &ICMPIteration{
TTL: ttl,
ICMPError: &model.ArchivalICMPErrorMessage{
Timeout: "yes",
},
}
return ii_timeout, nil
}

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
var quotedPacket *model.ArchivalICMPQuotation

for {
n, oobn, _, _, err := unix.Recvmsg(fd, buf, oob, unix.MSG_ERRQUEUE)

if err == unix.EAGAIN {
break
}

if err != nil {
return nil, err
}

quoted := buf[:n]

quotedPacket, err = parseQuotedPacket(quoted)

if err != nil {
fmt.Println("quote parse failed:", err)
return nil, err
}

cms, err := unix.ParseSocketControlMessage(oob[:oobn])
if err != nil {
return nil, err
}

for _, cm := range cms {

// fmt.Printf("n=%d\n", n)
// fmt.Printf("buf=%x\n", buf[:n])

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,
},
}
if quotedPacket != nil && err == nil {
ii.ICMPError.Quote = *quotedPacket
}

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)
ii_connected := &ICMPIteration{
TTL: ttl,
ICMPError: &model.ArchivalICMPErrorMessage{
Connected: "yes",
},
}
return ii_connected, nil
} else {
ol := logx.NewOperationLogger(logger, "Traceroute #%d TTL %d %s SO_ERROR %v", index, ttl, address, unix.Errno(soerr))
ol.Stop(nil)
ii_error := &ICMPIteration{
TTL: ttl,
ICMPError: &model.ArchivalICMPErrorMessage{
Error: unix.Errno(soerr).Error(),
},
}
return ii_error, nil
}
}

return nil, nil
}
Loading
Loading