Skip to content

Commit

Permalink
feat: a simple TLS fragmentation in addition to TCP split (#507)
Browse files Browse the repository at this point in the history
This PR adds the TLS Client Hello fragmentation in addition to the TCP split.

Note that this might cause issues with some TLS servers that don't support fragmented records. We plan to implement a more robust retry strategy in a future update.
  • Loading branch information
jyyi1 authored Mar 18, 2024
1 parent b538d23 commit ede31e6
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 23 deletions.
9 changes: 2 additions & 7 deletions Android/app/src/go/intra/split/direct_split.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,8 @@ func (s *splitter) Write(b []byte) (int, error) {

// Setting `used` to true ensures that this code only runs once per socket.
s.used = true
b1, b2 := splitHello(b)
n1, err := conn.Write(b1)
if err != nil {
return n1, err
}
n2, err := conn.Write(b2)
return n1 + n2, err
n, _, err := splitHello(b, conn)
return n, err
}

func (s *splitter) ReadFrom(reader io.Reader) (bytes int64, err error) {
Expand Down
85 changes: 70 additions & 15 deletions Android/app/src/go/intra/split/retrier.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package split

import (
"context"
"encoding/binary"
"errors"
"io"
"localhost/Intra/Android/app/src/go/logging"
Expand Down Expand Up @@ -165,8 +166,9 @@ func (r *retrier) Read(buf []byte) (n int, err error) {
}
// Read failed. Retry.
n, err = r.retry(buf)
} else {
logging.Debug("SplitRetry(retrier.Read) - direct conn succeeded, no need to split")
}
logging.Debug("SplitRetry(retrier.Read) - direct conn succeeded, no need to split")
close(r.retryCompleteFlag)
// Unset read deadline.
r.conn.SetReadDeadline(time.Time{})
Expand All @@ -186,14 +188,12 @@ func (r *retrier) retry(buf []byte) (n int, err error) {
return
}
r.conn = newConn.(*net.TCPConn)
first, second := splitHello(r.hello)
r.stats.Split = int16(len(first))
if _, err = r.conn.Write(first); err != nil {
return
}
if _, err = r.conn.Write(second); err != nil {
_, split, err := splitHello(r.hello, r.conn)
r.stats.Split = int16(split)
if err != nil {
return
}

// While we were creating the new socket, the caller might have called CloseRead
// or CloseWrite on the old socket. Copy that state to the new socket.
// CloseRead and CloseWrite are idempotent, so this is safe even if the user's
Expand All @@ -219,22 +219,77 @@ func (r *retrier) CloseRead() error {
return r.conn.CloseRead()
}

func splitHello(hello []byte) ([]byte, []byte) {
if len(hello) == 0 {
return hello, hello
func getTLSClientHelloRecordLen(h []byte) (uint16, bool) {
if len(h) < 5 {
return 0, false
}

const (
TYPE_HANDSHAKE byte = 22
VERSION_TLS10 uint16 = 0x0301
VERSION_TLS11 uint16 = 0x0302
VERSION_TLS12 uint16 = 0x0303
VERSION_TLS13 uint16 = 0x0304
)

if h[0] != TYPE_HANDSHAKE {
return 0, false
}

ver := binary.BigEndian.Uint16(h[1:3])
if ver != VERSION_TLS10 && ver != VERSION_TLS11 &&
ver != VERSION_TLS12 && ver != VERSION_TLS13 {
return 0, false
}

return binary.BigEndian.Uint16(h[3:5]), true
}

func splitHello(hello []byte, w io.Writer) (n int, splitLen int, err error) {
if len(hello) <= 1 {
n, err = w.Write(hello)
return
}

const (
MIN_SPLIT int = 32
MIN_SPLIT int = 6
MAX_SPLIT int = 64
)

// Random number in the range [MIN_SPLIT, MAX_SPLIT]
s := MIN_SPLIT + rand.Intn(MAX_SPLIT+1-MIN_SPLIT)
// splitLen includes 5 bytes of TLS header
splitLen = MIN_SPLIT + rand.Intn(MAX_SPLIT+1-MIN_SPLIT)
limit := len(hello) / 2
if s > limit {
s = limit
if splitLen > limit {
splitLen = limit
}
return hello[:s], hello[s:]

recordLen, ok := getTLSClientHelloRecordLen(hello)
recordSplitLen := splitLen - 5
if !ok || recordSplitLen <= 0 || recordSplitLen >= int(recordLen) {
// Do TCP split if hello is not a valid TLS Client Hello, or cannot be fragmented
n, err = w.Write(hello[:splitLen])
if err == nil {
var m int
m, err = w.Write(hello[splitLen:])
n += m
}
return
}

pkt := hello[:splitLen]
binary.BigEndian.PutUint16(pkt[3:5], uint16(recordSplitLen))
if n, err = w.Write(pkt); err != nil {
return
}

pkt = hello[splitLen-5:]
copy(pkt, hello[:5])
binary.BigEndian.PutUint16(pkt[3:5], recordLen-uint16(recordSplitLen))
var m int
m, err = w.Write(pkt)
n += m
return
}

// Write-related functions
Expand Down
2 changes: 1 addition & 1 deletion Android/app/src/go/intra/split/retrier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ func (s *setup) checkStats(bytes int32, chunks int16, timeout bool) {
if r.Timeout != timeout {
s.t.Errorf("Expected timeout to be %t", timeout)
}
if r.Split < 32 || r.Split > 64 {
if r.Split < 6 || r.Split > 64 {
s.t.Errorf("Unexpected split: %d", r.Split)
}
}
Expand Down

0 comments on commit ede31e6

Please sign in to comment.