Files
Archive/sing-box/common/tlsspoof/raw_windows.go
T
2026-04-17 21:15:10 +02:00

223 lines
6.2 KiB
Go

//go:build windows && (amd64 || 386)
package tlsspoof
import (
"errors"
"net"
"net/netip"
"sync"
"sync/atomic"
"time"
"github.com/sagernet/sing-box/common/windivert"
"github.com/sagernet/sing-tun/gtcpip/header"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/sys/windows"
)
const PlatformSupported = true
// closeGracePeriod caps how long Close() waits for the divert goroutine to
// observe the kernel-emitted real ClientHello and perform the reorder
// (fake → real). In practice this completes in microseconds; the cap
// bounds the pathological case where the kernel buffers the packet.
const closeGracePeriod = 2 * time.Second
// windowsSpoofer uses a single WinDivert handle for both capture and
// injection. Sequential Send() calls on one handle traverse one driver queue,
// so the fake provably precedes the released real on the wire — a guarantee
// two separate handles cannot make because cross-handle order depends on the
// scheduler.
type windowsSpoofer struct {
method Method
src, dst netip.AddrPort
divertH *windivert.Handle
fakeReady chan []byte // buffered(1): staged by Inject
done chan struct{} // closed by run() on exit
closeOnce sync.Once
runErr atomic.Pointer[error]
}
func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) {
_, src, dst, err := tcpEndpoints(conn)
if err != nil {
return nil, err
}
filter, err := windivert.OutboundTCP(src, dst)
if err != nil {
return nil, err
}
divertH, err := windivert.Open(filter, windivert.LayerNetwork, 0, 0)
if err != nil {
return nil, E.Cause(err, "tls_spoof: open WinDivert")
}
s := &windowsSpoofer{
method: method,
src: src,
dst: dst,
divertH: divertH,
fakeReady: make(chan []byte, 1),
done: make(chan struct{}),
}
go s.run()
return s, nil
}
func (s *windowsSpoofer) Inject(payload []byte) error {
select {
case s.fakeReady <- payload:
return nil
case <-s.done:
if p := s.runErr.Load(); p != nil {
return *p
}
return E.New("tls_spoof: spoofer closed before Inject")
}
}
func (s *windowsSpoofer) Close() error {
s.closeOnce.Do(func() {
// Give run() a grace window to finish handling the real packet.
select {
case <-s.done:
case <-time.After(closeGracePeriod):
// Force Recv() to return by closing the divert handle.
s.divertH.Close()
<-s.done
}
})
if p := s.runErr.Load(); p != nil {
return *p
}
return nil
}
func (s *windowsSpoofer) recordErr(err error) { s.runErr.Store(&err) }
func (s *windowsSpoofer) run() {
defer close(s.done)
defer s.divertH.Close()
buf := make([]byte, windivert.MTUMax)
for {
n, addr, err := s.divertH.Recv(buf)
if err != nil {
if errors.Is(err, windows.ERROR_OPERATION_ABORTED) ||
errors.Is(err, windows.ERROR_NO_DATA) {
return
}
s.recordErr(E.Cause(err, "windivert recv"))
return
}
pkt := buf[:n]
seq, ack, payloadLen, ok := parseTCPFields(pkt, addr.IPv6())
if !ok {
// Our filter is OutboundTCP(src, dst); a non-TCP or truncated
// match means driver state is suspect. Re-inject so the kernel
// still sees the byte stream, then abort — continuing would risk
// reordering against an unknown reference point.
_, sendErr := s.divertH.Send(pkt, &addr)
if sendErr != nil {
s.recordErr(E.Cause(sendErr, "windivert re-inject malformed"))
return
}
s.recordErr(E.New("windivert received malformed packet matching spoof filter"))
return
}
if payloadLen == 0 {
// Handshake ACK, keepalive, FIN — pass through unchanged.
_, err := s.divertH.Send(pkt, &addr)
if err != nil {
s.recordErr(E.Cause(err, "windivert re-inject empty"))
return
}
continue
}
// Non-empty outbound TCP payload = the real ClientHello.
var fake []byte
select {
case fake = <-s.fakeReady:
default:
// Inject() not yet called — pass through and keep observing.
_, err := s.divertH.Send(pkt, &addr)
if err != nil {
s.recordErr(E.Cause(err, "windivert re-inject early data"))
return
}
continue
}
frame, err := buildSpoofFrame(s.method, s.src, s.dst, seq, ack, fake)
if err != nil {
s.recordErr(err)
return
}
fakeAddr := addr // inherit Outbound, IfIdx
// buildSpoofFrame emits ready-to-wire bytes. The driver recomputes
// checksums on Send when TCPChecksum/IPChecksum are 0 — which would
// overwrite the intentionally corrupt checksum in WrongChecksum mode.
// Force both to 1 to keep our bytes intact.
fakeAddr.SetIPChecksum(true)
fakeAddr.SetTCPChecksum(true)
_, err = s.divertH.Send(frame, &fakeAddr)
if err != nil {
s.recordErr(E.Cause(err, "windivert inject fake"))
return
}
_, err = s.divertH.Send(pkt, &addr)
if err != nil {
s.recordErr(E.Cause(err, "windivert re-inject real"))
return
}
return // single-shot reorder complete
}
}
func parseTCPFields(pkt []byte, isV6 bool) (seq, ack uint32, payloadLen int, ok bool) {
if isV6 {
if len(pkt) < header.IPv6MinimumSize+header.TCPMinimumSize {
return 0, 0, 0, false
}
ip := header.IPv6(pkt)
if ip.TransportProtocol() != header.TCPProtocolNumber {
return 0, 0, 0, false
}
tcp := header.TCP(pkt[header.IPv6MinimumSize:])
tcpHdr := int(tcp.DataOffset())
if tcpHdr < header.TCPMinimumSize || header.IPv6MinimumSize+tcpHdr > len(pkt) {
return 0, 0, 0, false
}
return tcp.SequenceNumber(), tcp.AckNumber(),
len(pkt) - header.IPv6MinimumSize - tcpHdr, true
}
if len(pkt) < header.IPv4MinimumSize+header.TCPMinimumSize {
return 0, 0, 0, false
}
ip := header.IPv4(pkt)
if ip.Protocol() != uint8(header.TCPProtocolNumber) {
return 0, 0, 0, false
}
ihl := int(ip.HeaderLength())
// ihl+TCPMinimumSize guards the TCP-header field reads below; without
// this, an IPv4 packet with options (ihl>20) against a 40-byte buffer
// reads past the TCP slice when calling DataOffset.
if ihl < header.IPv4MinimumSize || ihl+header.TCPMinimumSize > len(pkt) {
return 0, 0, 0, false
}
tcp := header.TCP(pkt[ihl:])
tcpHdr := int(tcp.DataOffset())
if tcpHdr < header.TCPMinimumSize || ihl+tcpHdr > len(pkt) {
return 0, 0, 0, false
}
total := int(ip.TotalLength())
if total == 0 || total > len(pkt) {
total = len(pkt)
}
return tcp.SequenceNumber(), tcp.AckNumber(),
total - ihl - tcpHdr, true
}