Skip to content

Add WeirdSplitter #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions intra/dialers/weird_split.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package dialers

import (
"io"
"net"
"strings"
"syscall"
"github.com/celzero/firestack/intra/protect"
)

type WeirdSplitter struct {
*net.TCPConn
used bool
Size int
randomOffset bool
}

// similar to DialWithSplit(), but the second segment will be received first and it allow users to specify the size of 1st segment
func DialWithWeirdSplit(d *protect.RDial, addr *net.TCPAddr, size int) (DuplexConn, error) {
tcpConn, err := d.DialTCP(addr.Network(), nil, addr)
if err != nil {
return nil, err
}
if tcpConn == nil {
return nil, errNoConn
}
split1 := &WeirdSplitter{
TCPConn: tcpConn,
Size: size,
}
return split1, nil
}

// similar to DialWithSplit(), but the second segment will be received first.
func DialWithWeirdSplitRandomOffset(d *protect.RDial, addr *net.TCPAddr) (DuplexConn, error) {
tcpConn, err := d.DialTCP(addr.Network(), nil, addr)
if err != nil {
return nil, err
}
if tcpConn == nil {
return nil, errNoConn
}
split1 := &WeirdSplitter{
TCPConn: tcpConn,
randomOffset: true,
}
return split1, nil
}

func (s *WeirdSplitter) Write(b []byte) (int, error) {
conn := s.TCPConn
if s.used {
// After the first write, there is no special write behavior.
return conn.Write(b)
}

// Setting `used` to true ensures that this code only runs once per socket.
s.used = true
// One-byte segment is unable to be split.
if len(b) < 2 {
return conn.Write(b)
}
Comment on lines +60 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you think we should add a code comment on why len(b) < 2 won't work?

(to my untrained eye, it isn't obvious)

var b1, b2 []byte
if s.randomOffset {
b1, b2 = splitHello(b)
} else {
size := s.Size
if len(b) <= s.Size {
size = 1
}
b1 = b[:size]
b2 = b[size:]
}
rawConn, err := conn.SyscallConn()
if err != nil {
return 0, err
}
isIPv6 := strings.Contains(conn.RemoteAddr().String(), "[")

rawErr := rawConn.Control(func(fd uintptr) {
if isIPv6 {
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_UNICAST_HOPS, 1)
} else {
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_TTL, 1)
}
})
Comment on lines +80 to +86
Copy link
Contributor

@ignoramous ignoramous Apr 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a succinct code comment on what this approach does to bypass censorship or link to existing projects using this technique?

Is it similar to what's discussed here? #46

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GoodbyeDPI commit e28cb526
ValdikSS wrote:

Some websites (or more precisely, TLS terminators/balancers) can't
handle segmented TLS ClientHello packet properly, requiring the whole
ClientHello in a single segment, otherwise the connection gets dropped.

However they still operate with a proper TCP stack.
Cheat on them: send the latter segment first (with TCP SEQ "in the future"),
the former segment second (with "current" SEQ), allowing OS TCP
stack to combine it in a single TCP read().

But we don't have privileges, so we should implement this with setting TTL. The TCP stack transmits the segment with TTL=1 first, then we restore TTL option before the TCP stack retransmits, this idea is mentioned in #46 too.

if err != nil {
return 0, err
}
if rawErr != nil {
return 0, rawErr
}
n1, err := conn.Write(b1)
if err != nil {
return n1, err
}

rawErr = rawConn.Control(func(fd uintptr) {
if isIPv6 {
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_UNICAST_HOPS, 64)
} else {
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_TTL, 64)
}
})
if err != nil {
return n1, err
}
if rawErr != nil {
return n1, rawErr
}
n2, err := conn.Write(b2)
return n1 + n2, err
}

func (s *WeirdSplitter) ReadFrom(reader io.Reader) (bytes int64, err error) {
if !s.used {
// This is the first write on this socket.
// Use copyOnce(), which calls Write(), to get Write's splitting behavior for
// the first segment.
if bytes, err = copyOnce(s, reader); err != nil {
return
}
}

b, err := s.TCPConn.ReadFrom(reader)
bytes += b
return
}