From 031a1479585bd5dad03199f8ed2ed3457fc30784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 17 Apr 2026 12:13:41 +0800 Subject: [PATCH] Fix macOS tlsspoof --- common/tlsspoof/README.md | 3 ++ common/tlsspoof/integration_test.go | 12 +++++- common/tlsspoof/integration_unix_test.go | 49 +++++++++++++++++++++- common/tlsspoof/packet.go | 36 +++++++++++----- common/tlsspoof/raw_darwin.go | 52 ++++++++++++++++-------- 5 files changed, 122 insertions(+), 30 deletions(-) create mode 100644 common/tlsspoof/README.md diff --git a/common/tlsspoof/README.md b/common/tlsspoof/README.md new file mode 100644 index 000000000..de684e15c --- /dev/null +++ b/common/tlsspoof/README.md @@ -0,0 +1,3 @@ +# tls spoof + +idea from https://github.com/therealaleph/sni-spoofing-rust diff --git a/common/tlsspoof/integration_test.go b/common/tlsspoof/integration_test.go index e36592908..b7b07d54b 100644 --- a/common/tlsspoof/integration_test.go +++ b/common/tlsspoof/integration_test.go @@ -84,8 +84,16 @@ func tcpdumpObserver(t *testing.T, iface string, port uint16, needle string, do } func dialLocalEchoServer(t *testing.T) (client net.Conn, serverPort uint16) { + return dialLocalEchoServerFamily(t, "tcp4", "127.0.0.1:0") +} + +func dialLocalEchoServerIPv6(t *testing.T) (client net.Conn, serverPort uint16) { + return dialLocalEchoServerFamily(t, "tcp6", "[::1]:0") +} + +func dialLocalEchoServerFamily(t *testing.T, network, address string) (client net.Conn, serverPort uint16) { t.Helper() - listener, err := net.Listen("tcp4", "127.0.0.1:0") + listener, err := net.Listen(network, address) require.NoError(t, err) accepted := make(chan net.Conn, 1) @@ -97,7 +105,7 @@ func dialLocalEchoServer(t *testing.T) (client net.Conn, serverPort uint16) { close(accepted) }() addr := listener.Addr().(*net.TCPAddr) - client, err = net.Dial("tcp4", addr.String()) + client, err = net.Dial(network, addr.String()) require.NoError(t, err) server := <-accepted require.NotNil(t, server) diff --git a/common/tlsspoof/integration_unix_test.go b/common/tlsspoof/integration_unix_test.go index c734ed891..9ec5760c7 100644 --- a/common/tlsspoof/integration_unix_test.go +++ b/common/tlsspoof/integration_unix_test.go @@ -48,11 +48,56 @@ func TestIntegrationSpoofer_WrongSequence(t *testing.T) { require.True(t, captured, "injected fake ClientHello must be observable on loopback") } +func TestIntegrationSpoofer_IPv6_WrongChecksum(t *testing.T) { + requireRoot(t) + client, serverPort := dialLocalEchoServerIPv6(t) + spoofer, err := NewSpoofer(client, MethodWrongChecksum) + require.NoError(t, err) + defer spoofer.Close() + + payload, err := hex.DecodeString(realClientHello) + require.NoError(t, err) + fake, err := rewriteSNI(payload, "letsencrypt.org") + require.NoError(t, err) + + captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() { + require.NoError(t, spoofer.Inject(fake)) + }, 3*time.Second) + require.True(t, captured, "injected fake ClientHello must be observable on loopback") +} + +func TestIntegrationSpoofer_IPv6_WrongSequence(t *testing.T) { + requireRoot(t) + client, serverPort := dialLocalEchoServerIPv6(t) + spoofer, err := NewSpoofer(client, MethodWrongSequence) + require.NoError(t, err) + defer spoofer.Close() + + payload, err := hex.DecodeString(realClientHello) + require.NoError(t, err) + fake, err := rewriteSNI(payload, "letsencrypt.org") + require.NoError(t, err) + + captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() { + require.NoError(t, spoofer.Inject(fake)) + }, 3*time.Second) + require.True(t, captured, "injected fake ClientHello must be observable on loopback") +} + // Loopback bypasses TCP checksum validation, so wrong-sequence is used instead. func TestIntegrationConn_InjectsThenForwardsRealCH(t *testing.T) { requireRoot(t) + runInjectsThenForwardsRealCH(t, "tcp4", "127.0.0.1:0") +} - listener, err := net.Listen("tcp4", "127.0.0.1:0") +func TestIntegrationConn_IPv6_InjectsThenForwardsRealCH(t *testing.T) { + requireRoot(t) + runInjectsThenForwardsRealCH(t, "tcp6", "[::1]:0") +} + +func runInjectsThenForwardsRealCH(t *testing.T, network, address string) { + t.Helper() + listener, err := net.Listen(network, address) require.NoError(t, err) serverReceived := make(chan []byte, 1) @@ -69,7 +114,7 @@ func TestIntegrationConn_InjectsThenForwardsRealCH(t *testing.T) { addr := listener.Addr().(*net.TCPAddr) serverPort := uint16(addr.Port) - client, err := net.Dial("tcp4", addr.String()) + client, err := net.Dial(network, addr.String()) require.NoError(t, err) t.Cleanup(func() { client.Close() diff --git a/common/tlsspoof/packet.go b/common/tlsspoof/packet.go index d84fc4b12..9bdf7a59d 100644 --- a/common/tlsspoof/packet.go +++ b/common/tlsspoof/packet.go @@ -74,20 +74,36 @@ func encodeTCP(frame []byte, ipHeaderLen int, src, dst netip.AddrPort, seqNum, a } func buildSpoofFrame(method Method, src, dst netip.AddrPort, sendNext, receiveNext uint32, payload []byte) ([]byte, error) { - var sequence uint32 - corrupt := false - switch method { - case MethodWrongSequence: - sequence = sendNext - uint32(len(payload)) - case MethodWrongChecksum: - sequence = sendNext - corrupt = true - default: - return nil, E.New("tls_spoof: unknown method ", method) + sequence, corrupt, err := resolveSpoofSequence(method, sendNext, payload) + if err != nil { + return nil, err } return buildTCPSegment(src, dst, sequence, receiveNext, payload, corrupt), nil } +// buildSpoofTCPSegment returns a TCP segment without an IP header, for +// platforms where the kernel synthesises the IP header (darwin IPv6). +func buildSpoofTCPSegment(method Method, src, dst netip.AddrPort, sendNext, receiveNext uint32, payload []byte) ([]byte, error) { + sequence, corrupt, err := resolveSpoofSequence(method, sendNext, payload) + if err != nil { + return nil, err + } + segment := make([]byte, tcpHeaderLen+len(payload)) + encodeTCP(segment, 0, src, dst, sequence, receiveNext, payload, corrupt) + return segment, nil +} + +func resolveSpoofSequence(method Method, sendNext uint32, payload []byte) (uint32, bool, error) { + switch method { + case MethodWrongSequence: + return sendNext - uint32(len(payload)), false, nil + case MethodWrongChecksum: + return sendNext, true, nil + default: + return 0, false, E.New("tls_spoof: unknown method ", method) + } +} + func applyTCPChecksum(tcp header.TCP, srcAddr, dstAddr netip.Addr, payload []byte, corrupt bool) { tcpLen := tcpHeaderLen + len(payload) pseudo := header.PseudoHeaderChecksum(header.TCPProtocolNumber, srcAddr.AsSlice(), dstAddr.AsSlice(), uint16(tcpLen)) diff --git a/common/tlsspoof/raw_darwin.go b/common/tlsspoof/raw_darwin.go index 170561a87..99c9a5c66 100644 --- a/common/tlsspoof/raw_darwin.go +++ b/common/tlsspoof/raw_darwin.go @@ -59,7 +59,7 @@ func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) { if err != nil { return nil, err } - fd, sockaddr, err := openDarwinRawSocket(dst) + fd, sockaddr, err := openDarwinRawSocket(src, dst) if err != nil { return nil, err } @@ -119,31 +119,51 @@ func readDarwinTCPSequence(src, dst netip.AddrPort) (uint32, uint32, error) { return 0, 0, E.New("tls_spoof: connection ", src, "->", dst, " not found in pcblist_n") } -func openDarwinRawSocket(dst netip.AddrPort) (int, unix.Sockaddr, error) { - if !dst.Addr().Is4() { - // macOS does not expose IPV6_HDRINCL; raw AF_INET6 injection would - // require either BPF link-layer writes or kernel-side IPv6 header - // synthesis, neither of which is implemented here. - return -1, nil, E.New("tls_spoof: IPv6 not supported on darwin") +func openDarwinRawSocket(src, dst netip.AddrPort) (int, unix.Sockaddr, error) { + if dst.Addr().Is4() { + return openIPv4RawSocket(dst) } - return openIPv4RawSocket(dst) + // macOS does not accept IPV6_HDRINCL on AF_INET6 SOCK_RAW IPPROTO_TCP + // sockets, so the kernel builds the IPv6 header itself. Bind to the real + // connection's source address so in6_selectsrc returns it, and rely on + // in6p_cksum defaulting to -1 so the user-supplied TCP checksum is + // preserved (including deliberately corrupted ones). + fd, err := unix.Socket(unix.AF_INET6, unix.SOCK_RAW, unix.IPPROTO_TCP) + if err != nil { + return -1, nil, E.Cause(err, "open AF_INET6 SOCK_RAW") + } + err = unix.Bind(fd, &unix.SockaddrInet6{Addr: src.Addr().As16()}) + if err != nil { + unix.Close(fd) + return -1, nil, E.Cause(err, "bind AF_INET6 SOCK_RAW") + } + sockaddr := &unix.SockaddrInet6{Port: int(dst.Port()), Addr: dst.Addr().As16()} + return fd, sockaddr, nil } func (s *darwinSpoofer) Inject(payload []byte) error { + if !s.src.Addr().Is4() { + segment, err := buildSpoofTCPSegment(s.method, s.src, s.dst, s.sendNext, s.receiveNext, payload) + if err != nil { + return err + } + err = unix.Sendto(s.rawFD, segment, 0, s.rawSockAddr) + if err != nil { + return E.Cause(err, "sendto raw socket") + } + return nil + } frame, err := buildSpoofFrame(s.method, s.src, s.dst, s.sendNext, s.receiveNext, payload) if err != nil { return err } // Darwin inherits the historical BSD quirk: with IP_HDRINCL the kernel // expects ip_len and ip_off in host byte order, not network byte order. - // Apple's rip_output swaps them back before transmission. This does not - // apply to IPv6. - if s.src.Addr().Is4() { - totalLen := binary.BigEndian.Uint16(frame[2:4]) - binary.NativeEndian.PutUint16(frame[2:4], totalLen) - fragOff := binary.BigEndian.Uint16(frame[6:8]) - binary.NativeEndian.PutUint16(frame[6:8], fragOff) - } + // Apple's rip_output swaps them back before transmission. + totalLen := binary.BigEndian.Uint16(frame[2:4]) + binary.NativeEndian.PutUint16(frame[2:4], totalLen) + fragOff := binary.BigEndian.Uint16(frame[6:8]) + binary.NativeEndian.PutUint16(frame[6:8], fragOff) err = unix.Sendto(s.rawFD, frame, 0, s.rawSockAddr) if err != nil { return E.Cause(err, "sendto raw socket")