-
Notifications
You must be signed in to change notification settings - Fork 62
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
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 603eaf9
add ReadFrom tests to buffer_test.go
jyyi1 779fe68
add TLS address list option to the dialer
jyyi1 1ff2fed
add wrapconn
jyyi1 725c3fe
1 round of refactoring
jyyi1 653b855
fix test cases for buffer
jyyi1 2ed8304
simplify stream_dialer by extracting out TLS dispatching
jyyi1 4bc8f2c
prevent writing empty buffers after split
jyyi1 5a687a0
add test cases for stream dialer
jyyi1 ddf46f0
Update transport/tlsfrag/buffer.go
jyyi1 4f00ec8
Update transport/tlsfrag/buffer.go
jyyi1 edb97bb
Update transport/tlsfrag/buffer.go
jyyi1 87c3592
refactor buffer to get rid of b.len and use cap instead
jyyi1 cb8e9f3
refactored with tlsRecordHeader type
jyyi1 8c0063d
try to reolve "Incorrect conversion between integer types"
jyyi1 2598369
resolving werid error: https://github.com/Jigsaw-Code/outline-sdk/pul…
jyyi1 5e6aa87
Update transport/tlsfrag/writer.go
jyyi1 fb75111
Update transport/tlsfrag/writer.go
jyyi1 c371139
Update transport/tlsfrag/writer.go
jyyi1 0314269
Update transport/tlsfrag/writer.go
jyyi1 19240a9
resolve comment (round 2)
jyyi1 16d2d9b
wrong comment
jyyi1 711162d
simplify getting tail from original record
jyyi1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}}, | ||
}, | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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)