Skip to content

Commit 031a147

Browse files
committed
Fix macOS tlsspoof
1 parent d1cb61e commit 031a147

5 files changed

Lines changed: 119 additions & 27 deletions

File tree

common/tlsspoof/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# tls spoof
2+
3+
idea from https://github.com/therealaleph/sni-spoofing-rust

common/tlsspoof/integration_test.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,16 @@ func tcpdumpObserver(t *testing.T, iface string, port uint16, needle string, do
8484
}
8585

8686
func dialLocalEchoServer(t *testing.T) (client net.Conn, serverPort uint16) {
87+
return dialLocalEchoServerFamily(t, "tcp4", "127.0.0.1:0")
88+
}
89+
90+
func dialLocalEchoServerIPv6(t *testing.T) (client net.Conn, serverPort uint16) {
91+
return dialLocalEchoServerFamily(t, "tcp6", "[::1]:0")
92+
}
93+
94+
func dialLocalEchoServerFamily(t *testing.T, network, address string) (client net.Conn, serverPort uint16) {
8795
t.Helper()
88-
listener, err := net.Listen("tcp4", "127.0.0.1:0")
96+
listener, err := net.Listen(network, address)
8997
require.NoError(t, err)
9098

9199
accepted := make(chan net.Conn, 1)
@@ -97,7 +105,7 @@ func dialLocalEchoServer(t *testing.T) (client net.Conn, serverPort uint16) {
97105
close(accepted)
98106
}()
99107
addr := listener.Addr().(*net.TCPAddr)
100-
client, err = net.Dial("tcp4", addr.String())
108+
client, err = net.Dial(network, addr.String())
101109
require.NoError(t, err)
102110
server := <-accepted
103111
require.NotNil(t, server)

common/tlsspoof/integration_unix_test.go

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,56 @@ func TestIntegrationSpoofer_WrongSequence(t *testing.T) {
4848
require.True(t, captured, "injected fake ClientHello must be observable on loopback")
4949
}
5050

51+
func TestIntegrationSpoofer_IPv6_WrongChecksum(t *testing.T) {
52+
requireRoot(t)
53+
client, serverPort := dialLocalEchoServerIPv6(t)
54+
spoofer, err := NewSpoofer(client, MethodWrongChecksum)
55+
require.NoError(t, err)
56+
defer spoofer.Close()
57+
58+
payload, err := hex.DecodeString(realClientHello)
59+
require.NoError(t, err)
60+
fake, err := rewriteSNI(payload, "letsencrypt.org")
61+
require.NoError(t, err)
62+
63+
captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() {
64+
require.NoError(t, spoofer.Inject(fake))
65+
}, 3*time.Second)
66+
require.True(t, captured, "injected fake ClientHello must be observable on loopback")
67+
}
68+
69+
func TestIntegrationSpoofer_IPv6_WrongSequence(t *testing.T) {
70+
requireRoot(t)
71+
client, serverPort := dialLocalEchoServerIPv6(t)
72+
spoofer, err := NewSpoofer(client, MethodWrongSequence)
73+
require.NoError(t, err)
74+
defer spoofer.Close()
75+
76+
payload, err := hex.DecodeString(realClientHello)
77+
require.NoError(t, err)
78+
fake, err := rewriteSNI(payload, "letsencrypt.org")
79+
require.NoError(t, err)
80+
81+
captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() {
82+
require.NoError(t, spoofer.Inject(fake))
83+
}, 3*time.Second)
84+
require.True(t, captured, "injected fake ClientHello must be observable on loopback")
85+
}
86+
5187
// Loopback bypasses TCP checksum validation, so wrong-sequence is used instead.
5288
func TestIntegrationConn_InjectsThenForwardsRealCH(t *testing.T) {
5389
requireRoot(t)
90+
runInjectsThenForwardsRealCH(t, "tcp4", "127.0.0.1:0")
91+
}
92+
93+
func TestIntegrationConn_IPv6_InjectsThenForwardsRealCH(t *testing.T) {
94+
requireRoot(t)
95+
runInjectsThenForwardsRealCH(t, "tcp6", "[::1]:0")
96+
}
5497

55-
listener, err := net.Listen("tcp4", "127.0.0.1:0")
98+
func runInjectsThenForwardsRealCH(t *testing.T, network, address string) {
99+
t.Helper()
100+
listener, err := net.Listen(network, address)
56101
require.NoError(t, err)
57102

58103
serverReceived := make(chan []byte, 1)
@@ -69,7 +114,7 @@ func TestIntegrationConn_InjectsThenForwardsRealCH(t *testing.T) {
69114

70115
addr := listener.Addr().(*net.TCPAddr)
71116
serverPort := uint16(addr.Port)
72-
client, err := net.Dial("tcp4", addr.String())
117+
client, err := net.Dial(network, addr.String())
73118
require.NoError(t, err)
74119
t.Cleanup(func() {
75120
client.Close()

common/tlsspoof/packet.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,34 @@ func encodeTCP(frame []byte, ipHeaderLen int, src, dst netip.AddrPort, seqNum, a
7474
}
7575

7676
func buildSpoofFrame(method Method, src, dst netip.AddrPort, sendNext, receiveNext uint32, payload []byte) ([]byte, error) {
77-
var sequence uint32
78-
corrupt := false
77+
sequence, corrupt, err := resolveSpoofSequence(method, sendNext, payload)
78+
if err != nil {
79+
return nil, err
80+
}
81+
return buildTCPSegment(src, dst, sequence, receiveNext, payload, corrupt), nil
82+
}
83+
84+
// buildSpoofTCPSegment returns a TCP segment without an IP header, for
85+
// platforms where the kernel synthesises the IP header (darwin IPv6).
86+
func buildSpoofTCPSegment(method Method, src, dst netip.AddrPort, sendNext, receiveNext uint32, payload []byte) ([]byte, error) {
87+
sequence, corrupt, err := resolveSpoofSequence(method, sendNext, payload)
88+
if err != nil {
89+
return nil, err
90+
}
91+
segment := make([]byte, tcpHeaderLen+len(payload))
92+
encodeTCP(segment, 0, src, dst, sequence, receiveNext, payload, corrupt)
93+
return segment, nil
94+
}
95+
96+
func resolveSpoofSequence(method Method, sendNext uint32, payload []byte) (uint32, bool, error) {
7997
switch method {
8098
case MethodWrongSequence:
81-
sequence = sendNext - uint32(len(payload))
99+
return sendNext - uint32(len(payload)), false, nil
82100
case MethodWrongChecksum:
83-
sequence = sendNext
84-
corrupt = true
101+
return sendNext, true, nil
85102
default:
86-
return nil, E.New("tls_spoof: unknown method ", method)
103+
return 0, false, E.New("tls_spoof: unknown method ", method)
87104
}
88-
return buildTCPSegment(src, dst, sequence, receiveNext, payload, corrupt), nil
89105
}
90106

91107
func applyTCPChecksum(tcp header.TCP, srcAddr, dstAddr netip.Addr, payload []byte, corrupt bool) {

common/tlsspoof/raw_darwin.go

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) {
5959
if err != nil {
6060
return nil, err
6161
}
62-
fd, sockaddr, err := openDarwinRawSocket(dst)
62+
fd, sockaddr, err := openDarwinRawSocket(src, dst)
6363
if err != nil {
6464
return nil, err
6565
}
@@ -119,31 +119,51 @@ func readDarwinTCPSequence(src, dst netip.AddrPort) (uint32, uint32, error) {
119119
return 0, 0, E.New("tls_spoof: connection ", src, "->", dst, " not found in pcblist_n")
120120
}
121121

122-
func openDarwinRawSocket(dst netip.AddrPort) (int, unix.Sockaddr, error) {
123-
if !dst.Addr().Is4() {
124-
// macOS does not expose IPV6_HDRINCL; raw AF_INET6 injection would
125-
// require either BPF link-layer writes or kernel-side IPv6 header
126-
// synthesis, neither of which is implemented here.
127-
return -1, nil, E.New("tls_spoof: IPv6 not supported on darwin")
122+
func openDarwinRawSocket(src, dst netip.AddrPort) (int, unix.Sockaddr, error) {
123+
if dst.Addr().Is4() {
124+
return openIPv4RawSocket(dst)
128125
}
129-
return openIPv4RawSocket(dst)
126+
// macOS does not accept IPV6_HDRINCL on AF_INET6 SOCK_RAW IPPROTO_TCP
127+
// sockets, so the kernel builds the IPv6 header itself. Bind to the real
128+
// connection's source address so in6_selectsrc returns it, and rely on
129+
// in6p_cksum defaulting to -1 so the user-supplied TCP checksum is
130+
// preserved (including deliberately corrupted ones).
131+
fd, err := unix.Socket(unix.AF_INET6, unix.SOCK_RAW, unix.IPPROTO_TCP)
132+
if err != nil {
133+
return -1, nil, E.Cause(err, "open AF_INET6 SOCK_RAW")
134+
}
135+
err = unix.Bind(fd, &unix.SockaddrInet6{Addr: src.Addr().As16()})
136+
if err != nil {
137+
unix.Close(fd)
138+
return -1, nil, E.Cause(err, "bind AF_INET6 SOCK_RAW")
139+
}
140+
sockaddr := &unix.SockaddrInet6{Port: int(dst.Port()), Addr: dst.Addr().As16()}
141+
return fd, sockaddr, nil
130142
}
131143

132144
func (s *darwinSpoofer) Inject(payload []byte) error {
145+
if !s.src.Addr().Is4() {
146+
segment, err := buildSpoofTCPSegment(s.method, s.src, s.dst, s.sendNext, s.receiveNext, payload)
147+
if err != nil {
148+
return err
149+
}
150+
err = unix.Sendto(s.rawFD, segment, 0, s.rawSockAddr)
151+
if err != nil {
152+
return E.Cause(err, "sendto raw socket")
153+
}
154+
return nil
155+
}
133156
frame, err := buildSpoofFrame(s.method, s.src, s.dst, s.sendNext, s.receiveNext, payload)
134157
if err != nil {
135158
return err
136159
}
137160
// Darwin inherits the historical BSD quirk: with IP_HDRINCL the kernel
138161
// expects ip_len and ip_off in host byte order, not network byte order.
139-
// Apple's rip_output swaps them back before transmission. This does not
140-
// apply to IPv6.
141-
if s.src.Addr().Is4() {
142-
totalLen := binary.BigEndian.Uint16(frame[2:4])
143-
binary.NativeEndian.PutUint16(frame[2:4], totalLen)
144-
fragOff := binary.BigEndian.Uint16(frame[6:8])
145-
binary.NativeEndian.PutUint16(frame[6:8], fragOff)
146-
}
162+
// Apple's rip_output swaps them back before transmission.
163+
totalLen := binary.BigEndian.Uint16(frame[2:4])
164+
binary.NativeEndian.PutUint16(frame[2:4], totalLen)
165+
fragOff := binary.BigEndian.Uint16(frame[6:8])
166+
binary.NativeEndian.PutUint16(frame[6:8], fragOff)
147167
err = unix.Sendto(s.rawFD, frame, 0, s.rawSockAddr)
148168
if err != nil {
149169
return E.Cause(err, "sendto raw socket")

0 commit comments

Comments
 (0)