Skip to content
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

feat(transport): general TLS ClientHello fragmentation StreamDialer #133

Merged
merged 23 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dee4db7
feat(transport): general TLS ClientHello fragmentation StreamDialer
jyyi1 Nov 9, 2023
603eaf9
add ReadFrom tests to buffer_test.go
jyyi1 Nov 9, 2023
779fe68
add TLS address list option to the dialer
jyyi1 Nov 10, 2023
1ff2fed
add wrapconn
jyyi1 Nov 13, 2023
725c3fe
1 round of refactoring
jyyi1 Nov 15, 2023
653b855
fix test cases for buffer
jyyi1 Nov 16, 2023
2ed8304
simplify stream_dialer by extracting out TLS dispatching
jyyi1 Nov 16, 2023
4bc8f2c
prevent writing empty buffers after split
jyyi1 Nov 16, 2023
5a687a0
add test cases for stream dialer
jyyi1 Nov 16, 2023
ddf46f0
Update transport/tlsfrag/buffer.go
jyyi1 Nov 17, 2023
4f00ec8
Update transport/tlsfrag/buffer.go
jyyi1 Nov 17, 2023
edb97bb
Update transport/tlsfrag/buffer.go
jyyi1 Nov 17, 2023
87c3592
refactor buffer to get rid of b.len and use cap instead
jyyi1 Nov 21, 2023
cb8e9f3
refactored with tlsRecordHeader type
jyyi1 Nov 22, 2023
8c0063d
try to reolve "Incorrect conversion between integer types"
jyyi1 Nov 22, 2023
2598369
resolving werid error: https://github.com/Jigsaw-Code/outline-sdk/pul…
jyyi1 Nov 22, 2023
5e6aa87
Update transport/tlsfrag/writer.go
jyyi1 Nov 22, 2023
fb75111
Update transport/tlsfrag/writer.go
jyyi1 Nov 22, 2023
c371139
Update transport/tlsfrag/writer.go
jyyi1 Nov 22, 2023
0314269
Update transport/tlsfrag/writer.go
jyyi1 Nov 22, 2023
19240a9
resolve comment (round 2)
jyyi1 Nov 22, 2023
16d2d9b
wrong comment
jyyi1 Nov 22, 2023
711162d
simplify getting tail from original record
jyyi1 Nov 22, 2023
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
18 changes: 9 additions & 9 deletions network/dnstruncate/packet_proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,15 +164,15 @@ func constructDNSQuestionsFromDomainNames(questions []string) []layers.DNSQuesti

// constructDNSRequestOrResponse creates the following DNS request/response:
//
// [ `id` ]: 2 bytes
// [ Standard-Query/Response + Recursive ]: 0x01/0x81
// [ Reserved/Response-No-Err ]: 0x00
// [ Questions-Count ]: 2 bytes (= len(questions))
// [ Answers Count ]: 2 bytes (= 0x00 0x00 / len(questions))
// [ Authorities Count ]: 0x00 0x00
// [ Resources Count ]: 0x00 0x01
// [ `questions` ]: ? bytes
// [ Additional Resources ]: ? bytes (= OPT(payload_size=4096))
// [ `id` ]: 2 bytes
// [ Standard-Query/Response + Recursive ]: 0x01/0x81
// [ Reserved/Response-No-Err ]: 0x00
// [ Questions-Count ]: 2 bytes (= len(questions))
// [ Answers Count ]: 2 bytes (= 0x00 0x00 / len(questions))
// [ Authorities Count ]: 0x00 0x00
// [ Resources Count ]: 0x00 0x01
// [ `questions` ]: ? bytes
// [ Additional Resources ]: ? bytes (= OPT(payload_size=4096))
//
// https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.1
//
Expand Down
5 changes: 3 additions & 2 deletions transport/socks5/socks5.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package socks5

import (
"encoding/binary"
"errors"
"fmt"
"net"
Expand Down Expand Up @@ -79,7 +80,7 @@ func appendSOCKS5Address(b []byte, address string) ([]byte, error) {
if err != nil {
return nil, err
}
portNum, err := strconv.Atoi(portStr)
portNum, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -109,6 +110,6 @@ func appendSOCKS5Address(b []byte, address string) ([]byte, error) {
b = append(b, byte(len(host)))
b = append(b, host...)
}
b = append(b, byte(portNum>>8), byte(portNum))
b = binary.BigEndian.AppendUint16(b, uint16(portNum))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is to resolve the CodeQL error: https://github.com/Jigsaw-Code/outline-sdk/pull/133/checks?check_run_id=18912663629

(Although the standard library AppendUint16 is using the same code to append the data)

return b, nil
}
147 changes: 147 additions & 0 deletions transport/tlsfrag/buffer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright 2023 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tlsfrag

import (
"bytes"
"errors"
"fmt"
"io"
)

var (
// errTLSClientHelloFullyReceived is returned when a full TLS Client Hello has been received and no
// more data can be pushed to the buffer.
errTLSClientHelloFullyReceived = errors.New("already received a complete TLS Client Hello packet")

// errInvalidTLSClientHello is the error used when the data received is not a valid TLS Client Hello.
errInvalidTLSClientHello = errors.New("not a valid TLS Client Hello packet")
)

// clientHelloBuffer is a byte buffer used to receive and buffer a TLS Client Hello packet.
type clientHelloBuffer struct {
// The buffer that hosts both header and content, cap: 5 -> 5+len(content)+padding
data []byte
// Indicates whether the content in data is a valid TLS Client Hello record
validationErr error
// A reader used to read from the slice passed to Write
bufrd *bytes.Reader
}

var _ io.Writer = (*clientHelloBuffer)(nil)
var _ io.ReaderFrom = (*clientHelloBuffer)(nil)

// newClientHelloBuffer creates and initializes a new buffer to receive a TLS Client Hello packet.
func newClientHelloBuffer() *clientHelloBuffer {
// Allocate the 5 bytes header first, and then reallocate it to contain the entire packet later
return &clientHelloBuffer{
fortuna marked this conversation as resolved.
Show resolved Hide resolved
data: make([]byte, 0, recordHeaderLen),
validationErr: nil,
bufrd: bytes.NewReader(nil), // It will be Reset in Write
}
}

// Bytes returns the full Client Hello packet including both the 5 bytes header and the content.
func (b *clientHelloBuffer) Bytes() []byte {
return b.data
}

// Write appends p to the buffer and returns the number of bytes actually used.
// If this data completes a valid TLS Client Hello, it returns errTLSClientHelloFullyReceived.
// If an invalid TLS Client Hello message is detected, it returns the error errInvalidTLSClientHello.
// If all bytes in p have been used and the buffer still requires more data to build a complete TLS Client Hello
// message, it returns (len(p), nil).
func (b *clientHelloBuffer) Write(p []byte) (int, error) {
b.bufrd.Reset(p)
n, err := b.ReadFrom(b.bufrd)
if err == nil && int(n) != len(p) {
err = io.ErrShortWrite
}
return int(n), err
}

// ReadFrom reads all the data from r and appends it to this buffer until a complete Client Hello packet has been
// received, or r returns EOF or error. It returns the number of bytes read. Any error except EOF encountered during
// the read is also returned.
//
// If this buffer completes a valid TLS Client Hello, it returns errTLSClientHelloFullyReceived.
// If an invalid TLS Client Hello message is detected, it returns the error errInvalidTLSClientHello.
// If this buffer still requires more data to build a complete TLS Client Hello message, it returns nil error.
//
// You can call ReadFrom multiple times if r doesn't provide enough data to build a complete Client Hello packet.
func (b *clientHelloBuffer) ReadFrom(r io.Reader) (n int64, err error) {
// Waiting to finish the header of 5 bytes
for len(b.data) < recordHeaderLen {
m, e := r.Read(b.data[len(b.data):recordHeaderLen])
b.data = b.data[:len(b.data)+m]
n += int64(m)

if len(b.data) == recordHeaderLen {
hdr := tlsRecordHeader(b.data)
if err = validateTLSClientHello(hdr); err != nil {
b.validationErr = err
return
}
buf := make([]byte, 0, recordHeaderLen*2+hdr.PayloadLen())
b.data = append(buf, b.data...)
}
if err = e; err != nil {
if err == io.EOF {
err = nil
}
return
}
}

// If the buffer is already invalid
if b.validationErr != nil {
err = b.validationErr
return
}

// Waiting to finish the payload of cap(b.data)-5 bytes
for len(b.data) < cap(b.data)-recordHeaderLen {
m, e := r.Read(b.data[len(b.data) : cap(b.data)-recordHeaderLen])
b.data = b.data[:len(b.data)+m]
n += int64(m)

if len(b.data) == cap(b.data)-recordHeaderLen {
err = errTLSClientHelloFullyReceived
return
}
if err = e; err != nil {
if err == io.EOF {
err = nil
}
return
}
}

err = errTLSClientHelloFullyReceived
return
}

func validateTLSClientHello(hdr tlsRecordHeader) error {
if typ := hdr.Type(); typ != recordTypeHandshake {
return fmt.Errorf("record type %d is not handshake: %w", typ, errInvalidTLSClientHello)
}
if ver := hdr.LegacyVersion(); !isValidTLSVersion(ver) {
return fmt.Errorf("%#04x is not a valid TLS version: %w", ver, errInvalidTLSClientHello)
}
if len := hdr.PayloadLen(); !isValidPayloadLenForHandshake(len) {
return fmt.Errorf("message length %v out of range: %w", len, errInvalidTLSClientHello)
}
return nil
}
140 changes: 140 additions & 0 deletions transport/tlsfrag/buffer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright 2023 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tlsfrag

import (
"bytes"
"net"
"testing"

"github.com/stretchr/testify/require"
)

// Test Write valid Client Hello to the buffer.
func TestWriteValidClientHello(t *testing.T) {
for _, tc := range validClientHelloTestCases() {
buf := newClientHelloBuffer()

totalExpectedBytes := []byte{}
for k, pkt := range tc.pkts {
n, err := buf.Write(pkt)
if k < tc.expectLastPkt {
require.NoError(t, err, tc.msg+": pkt-%d", k)
} else {
require.ErrorIs(t, err, errTLSClientHelloFullyReceived, tc.msg+": pkt-%d", k)
}
require.Equal(t, len(pkt)-len(tc.expectRemaining[k]), n, tc.msg+": pkt-%d", k)
require.Equal(t, tc.expectRemaining[k], pkt[n:], tc.msg+": pkt-%d", k)

totalExpectedBytes = append(totalExpectedBytes, pkt[:n]...)
require.Equal(t, totalExpectedBytes, buf.Bytes(), tc.msg+": pkt-%d", k)
}
require.Equal(t, tc.expectTotalPkt, buf.Bytes(), tc.msg)
require.Equal(t, len(tc.expectTotalPkt)+5, cap(buf.Bytes()), tc.msg)
}
}

// Test ReadFrom Reader(s) containing valid Client Hello.
func TestReadFromValidClientHello(t *testing.T) {
for _, tc := range validClientHelloTestCases() {
buf := newClientHelloBuffer()

totalExpectedBytes := []byte{}
for k, pkt := range tc.pkts {
r := bytes.NewBuffer(pkt)
require.Equal(t, len(pkt), r.Len(), tc.msg+": pkt-%d", k)

n, err := buf.ReadFrom(r)
if k < tc.expectLastPkt {
require.NoError(t, err, tc.msg+": pkt-%d", k)
} else {
require.ErrorIs(t, err, errTLSClientHelloFullyReceived, tc.msg+": pkt-%d", k)
}
require.Equal(t, len(pkt)-len(tc.expectRemaining[k]), int(n), tc.msg+": pkt-%d", k)
require.Equal(t, tc.expectRemaining[k], pkt[n:], tc.msg+": pkt-%d", k)

totalExpectedBytes = append(totalExpectedBytes, pkt[:n]...)
require.Equal(t, totalExpectedBytes, buf.Bytes(), tc.msg+": pkt-%d", k)
require.Equal(t, len(tc.expectRemaining[k]), r.Len(), tc.msg+": pkt-%d", k)
require.Equal(t, tc.expectRemaining[k], r.Bytes(), tc.msg+": pkt-%d", k)
}
require.Equal(t, tc.expectTotalPkt, buf.Bytes(), tc.msg)
require.Equal(t, len(tc.expectTotalPkt)+5, cap(buf.Bytes()), tc.msg)
}
}

// Example TLS Client Hello packet copied from https://tls13.xargs.org/#client-hello
// Total Len = 253, ContentLen = 253 - 5 = 248
var exampleTLS13ClientHello = []byte{
0x16, 0x03, 0x01, 0x00, 0xf8, 0x01, 0x00, 0x00, 0xf4, 0x03, 0x03, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
0x1d, 0x1e, 0x1f, 0x20, 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef,
0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, 0x00, 0x08, 0x13, 0x02,
0x13, 0x03, 0x13, 0x01, 0x00, 0xff, 0x01, 0x00, 0x00, 0xa3, 0x00, 0x00, 0x00, 0x18, 0x00, 0x16, 0x00, 0x00, 0x13, 0x65,
0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x75, 0x6c, 0x66, 0x68, 0x65, 0x69, 0x6d, 0x2e, 0x6e, 0x65, 0x74, 0x00, 0x0b,
0x00, 0x04, 0x03, 0x00, 0x01, 0x02, 0x00, 0x0a, 0x00, 0x16, 0x00, 0x14, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x1e, 0x00, 0x19,
0x00, 0x18, 0x01, 0x00, 0x01, 0x01, 0x01, 0x02, 0x01, 0x03, 0x01, 0x04, 0x00, 0x23, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00,
0x00, 0x17, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x1e, 0x00, 0x1c, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x08, 0x07, 0x08, 0x08,
0x08, 0x09, 0x08, 0x0a, 0x08, 0x0b, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06, 0x04, 0x01, 0x05, 0x01, 0x06, 0x01, 0x00, 0x2b,
0x00, 0x03, 0x02, 0x03, 0x04, 0x00, 0x2d, 0x00, 0x02, 0x01, 0x01, 0x00, 0x33, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00,
0x20, 0x35, 0x80, 0x72, 0xd6, 0x36, 0x58, 0x80, 0xd1, 0xae, 0xea, 0x32, 0x9a, 0xdf, 0x91, 0x21, 0x38, 0x38, 0x51, 0xed,
0x21, 0xa2, 0x8e, 0x3b, 0x75, 0xe9, 0x65, 0xd0, 0xd2, 0xcd, 0x16, 0x62, 0x54,
}

type validClientHelloCase struct {
msg string
pkts net.Buffers
expectTotalPkt []byte
expectLastPkt int // the index of the expected last packet
expectRemaining net.Buffers
}

// generating test cases that contain one valid Client Hello
func validClientHelloTestCases() []validClientHelloCase {
return []validClientHelloCase{
{
msg: "full client hello in single buffer",
pkts: [][]byte{exampleTLS13ClientHello},
expectTotalPkt: exampleTLS13ClientHello,
expectLastPkt: 0,
expectRemaining: [][]byte{{}},
},
{
msg: "full client hello with extra bytes",
pkts: [][]byte{append(exampleTLS13ClientHello, 0x88, 0x87, 0x86, 0x85, 0x84, 0x83, 0x82, 0x81)},
expectTotalPkt: exampleTLS13ClientHello,
expectLastPkt: 0,
expectRemaining: [][]byte{{0x88, 0x87, 0x86, 0x85, 0x84, 0x83, 0x82, 0x81}},
},
{
msg: "client hello in three buffers",
pkts: [][]byte{exampleTLS13ClientHello[:2], exampleTLS13ClientHello[2:123], exampleTLS13ClientHello[123:]},
expectTotalPkt: exampleTLS13ClientHello,
expectLastPkt: 2,
expectRemaining: [][]byte{{}, {}, {}},
},
{
msg: "client hello in three buffers with extra bytes",
pkts: [][]byte{
exampleTLS13ClientHello[:2],
exampleTLS13ClientHello[2:123],
append(exampleTLS13ClientHello[123:], 0x88, 0x87, 0x86, 0x85, 0x84, 0x83, 0x82, 0x81),
},
expectTotalPkt: exampleTLS13ClientHello,
expectLastPkt: 2,
expectRemaining: [][]byte{{}, {}, {0x88, 0x87, 0x86, 0x85, 0x84, 0x83, 0x82, 0x81}},
},
}
}
Loading