diff --git a/intra/dialers/split_and_desync.go b/intra/dialers/split_and_desync.go index 9b94716a..f78dd767 100644 --- a/intra/dialers/split_and_desync.go +++ b/intra/dialers/split_and_desync.go @@ -1,10 +1,5 @@ package dialers -/* -Combine direct_split with TCB (Transmission Control Block) Desynchronization Attack -Inspired by byedpi -*/ - import ( "io" "math/rand" @@ -37,12 +32,14 @@ const ( // TODO: invalidate cache on network changes. var ttlcache = core.NewDefaultSieve[netip.Addr, int]() +// Combines direct split with TCB Desynchronization Attack +// Inspired by byedpi: github.com/hufrea/byedpi/blob/82e5229df00/desync.c#L69-L123 type overwriteSplitter struct { - conn *net.TCPConn - used atomic.Bool - ttl int - ip6 bool - payload []byte // must be smaller than 1st written packet + conn *net.TCPConn // underlying connection + used atomic.Bool // ensures desync only runs once + ttl int // desync TTL + ip6 bool // IPv6 + payload []byte // must be smaller than 1st written packet // note: Normal ClientHello generated by browsers is 517 bytes. If kyber is enabled, the ClientHello can be larger. } @@ -214,22 +211,29 @@ func desyncWithTraceroute(d *protect.RDial, ipp netip.AddrPort) (*overwriteSplit core.Recycle(bptr) }() - split1 := &overwriteSplitter{ + oc := &overwriteSplitter{ conn: tcpConn, ttl: DESYNC_NOOP_TTL, payload: []byte(Http1_1String), ip6: isIPv6, } - // skip desync if no measurement is done - defer split1.used.Store(!measureTTL) - + processed := false // after TCP handshake, check received ICMP messages, if measureTTL is true. - for i := 0; i < DESYNC_MAX_TTL-1 && measureTTL; i += DESYNC_DELTA_TTL { + for i := 0; i < desync_max_ttl-1 && measureTTL; i += desync_delta_ttl { _, cmsgN, _, from, err := unix.Recvmsg(udpFD, msgBuf[:], cmsgBuf[:], unix.MSG_ERRQUEUE) if err != nil { - log.V("split-desync: recvmsg %v failed: %v", ipp, err) - break // udpConn must be nonblocking + log.V("split-desync: recvmsg %v, processed? %t, err: %v", ipp, processed, err) + if processed { // recvmsg processed at least once + break // uc must be nonblocking + } else { + processed = true + i -= desync_delta_ttl + time.Sleep(5 * time.Millisecond) + continue + } + } else { + processed = true } cmsgs, err := unix.ParseSocketControlMessage(cmsgBuf[:cmsgN]) @@ -242,26 +246,29 @@ func desyncWithTraceroute(d *protect.RDial, ipp netip.AddrPort) (*overwriteSplit if exceedsHopLimit(cmsgs) { fromPort := from.(*unix.SockaddrInet6).Port ttl := fromPort - basePort - if ttl > DESYNC_MAX_TTL { + if ttl > desync_max_ttl { break } - split1.ttl = max(split1.ttl, ttl) + oc.ttl = max(oc.ttl, ttl) } } else { if exceedsTTL(cmsgs) { fromPort := from.(*unix.SockaddrInet4).Port ttl := fromPort - basePort - if ttl > DESYNC_MAX_TTL { + if ttl > desync_max_ttl { break } - split1.ttl = max(split1.ttl, ttl) + oc.ttl = max(oc.ttl, ttl) } } } - log.D("split-desync: done: %v, ok? %t, ttl: %d", ipp, measureTTL, split1.ttl) + // skip desync if no measurement is done + oc.used.Store(!processed) + + log.D("split-desync: done: %v, ok? %t, ttl: %d", ipp, processed, oc.ttl) - return split1, nil + return oc, nil } func desyncWithFixedTtl(d *protect.RDial, ipp netip.AddrPort, initialTTL int) (*overwriteSplitter, error) { @@ -444,5 +451,7 @@ func (s *overwriteSplitter) ReadFrom(reader io.Reader) (bytes int64, err error) b, err := s.conn.ReadFrom(reader) bytes += b + log.V("split-desync: readfrom: done; sz: %d; err: %v", bytes, err) + return }