From 500adfe53902670b8bd723f0fa5c6bdedb93ab47 Mon Sep 17 00:00:00 2001 From: jyyi1 Date: Mon, 27 Nov 2023 23:14:20 -0500 Subject: [PATCH 1/7] feat(transport): TLS Client Hello fragmentation by fixed bytes --- transport/tlsfrag/doc.go | 37 ++++++++++++++++++++++++++++++ transport/tlsfrag/stream_dialer.go | 22 ++++++++++++++++++ x/config/config.go | 9 ++++++++ 3 files changed, 68 insertions(+) create mode 100644 transport/tlsfrag/doc.go diff --git a/transport/tlsfrag/doc.go b/transport/tlsfrag/doc.go new file mode 100644 index 00000000..4dd5aac0 --- /dev/null +++ b/transport/tlsfrag/doc.go @@ -0,0 +1,37 @@ +// 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 provides tools to split a single [TLS Client Hello message] +into multiple [TLS records]. This technique, known as TLS record fragmentation, +forces censors to maintain state and allocate memory for potential reassembly, +making censorship more difficult and resource-intensive. For detailed +explanation on how this technique works, refer to [Circumventing the GFW with +TLS Record Fragmentation]. + +This package offers convenient helper functions to create a TLS +[transport.StreamDialer] that fragments the [TLS Client Hello message]: + - [NewFixedBytesStreamDialer] creates a [transport.StreamDialer] that splits + the Client Hello message into two records. One record will have the + specified splitBytes length. + - [NewStreamDialerFunc] offers a more flexible way to fragment Client Hello + message. It accepts a callback function that determines the split point, + enabling advanced splitting logic such as splitting based on the SNI + extension. + +[Circumventing the GFW with TLS Record Fragmentation]: https://upb-syssec.github.io/blog/2023/record-fragmentation/#tls-record-fragmentation +[TLS Client Hello message]: https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2 +[TLS records]: https://datatracker.ietf.org/doc/html/rfc8446#section-5.1 +*/ +package tlsfrag diff --git a/transport/tlsfrag/stream_dialer.go b/transport/tlsfrag/stream_dialer.go index 28ae761f..fdc786a9 100644 --- a/transport/tlsfrag/stream_dialer.go +++ b/transport/tlsfrag/stream_dialer.go @@ -78,3 +78,25 @@ func WrapConnFunc(base transport.StreamConn, frag FragFunc) (transport.StreamCon } return transport.WrapConn(base, base, w), nil } + +// NewFixedBytesStreamDialer is a [transport.StreamDialer] that modifies the [TLS Client Hello] message. It splits the +// Client Hello message into two parts based on the given splitBytes. If splitBytes is positive, the first piece will +// contain the specified number of leading bytes from the original message. If it is negative, the second piece will +// contain the specified number of trailing bytes. +// +// [TLS Client Hello]: https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2 +func NewFixedBytesStreamDialer(base transport.StreamDialer, splitBytes int) (transport.StreamDialer, error) { + if base == nil { + return nil, errors.New("base dialer must not be nil") + } + if splitBytes == 0 { + return base, nil + } + return NewStreamDialerFunc(base, func(record []byte) int { + if splitBytes > 0 { + // TODO: optimize for the leading bytes split + return splitBytes + } + return len(record) + splitBytes + }) +} diff --git a/x/config/config.go b/x/config/config.go index 046b2893..7349f279 100644 --- a/x/config/config.go +++ b/x/config/config.go @@ -24,6 +24,7 @@ import ( "github.com/Jigsaw-Code/outline-sdk/transport" "github.com/Jigsaw-Code/outline-sdk/transport/socks5" "github.com/Jigsaw-Code/outline-sdk/transport/split" + "github.com/Jigsaw-Code/outline-sdk/transport/tlsfrag" ) func parseConfigPart(oneDialerConfig string) (*url.URL, error) { @@ -93,6 +94,14 @@ func newStreamDialerFromPart(innerDialer transport.StreamDialer, oneDialerConfig case "tls": return newTlsStreamDialerFromURL(innerDialer, url) + case "tlsfrag": + fixedBytesStr := url.Opaque + fixedBytes, err := strconv.Atoi(fixedBytesStr) + if err != nil { + return nil, fmt.Errorf("invalid tlsfrag option: %v. It should be in tlsfrag: format", fixedBytesStr) + } + return tlsfrag.NewFixedBytesStreamDialer(innerDialer, fixedBytes) + default: return nil, fmt.Errorf("config scheme '%v' is not supported", url.Scheme) } From 1d6526716e921d8db2339fa3efea47439cf771dc Mon Sep 17 00:00:00 2001 From: jyyi1 Date: Mon, 27 Nov 2023 23:21:10 -0500 Subject: [PATCH 2/7] revert the example change --- x/config/config.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/x/config/config.go b/x/config/config.go index 7349f279..046b2893 100644 --- a/x/config/config.go +++ b/x/config/config.go @@ -24,7 +24,6 @@ import ( "github.com/Jigsaw-Code/outline-sdk/transport" "github.com/Jigsaw-Code/outline-sdk/transport/socks5" "github.com/Jigsaw-Code/outline-sdk/transport/split" - "github.com/Jigsaw-Code/outline-sdk/transport/tlsfrag" ) func parseConfigPart(oneDialerConfig string) (*url.URL, error) { @@ -94,14 +93,6 @@ func newStreamDialerFromPart(innerDialer transport.StreamDialer, oneDialerConfig case "tls": return newTlsStreamDialerFromURL(innerDialer, url) - case "tlsfrag": - fixedBytesStr := url.Opaque - fixedBytes, err := strconv.Atoi(fixedBytesStr) - if err != nil { - return nil, fmt.Errorf("invalid tlsfrag option: %v. It should be in tlsfrag: format", fixedBytesStr) - } - return tlsfrag.NewFixedBytesStreamDialer(innerDialer, fixedBytes) - default: return nil, fmt.Errorf("config scheme '%v' is not supported", url.Scheme) } From 23a5ebda67ee0be257304f461f592f1091069967 Mon Sep 17 00:00:00 2001 From: jyyi1 Date: Mon, 27 Nov 2023 23:38:26 -0500 Subject: [PATCH 3/7] add tests for fixed bytes fragmentation --- transport/tlsfrag/stream_dialer_test.go | 71 +++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/transport/tlsfrag/stream_dialer_test.go b/transport/tlsfrag/stream_dialer_test.go index 887f8f5c..95b93a3b 100644 --- a/transport/tlsfrag/stream_dialer_test.go +++ b/transport/tlsfrag/stream_dialer_test.go @@ -35,13 +35,13 @@ func TestStreamDialerFuncSplitsClientHello(t *testing.T) { req1 := constructTLSRecord(t, layers.TLSApplicationData, 0x0303, []byte{0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88}) inner := &collectStreamDialer{} - conn := assertCanDialFragFunc(t, inner, "ipinfo.io:443", func(_ []byte) int { return 2 }) + conn := assertCanDialFragFunc(t, inner, "ipinfo.io:443", func(payload []byte) int { return len(payload) / 2 }) defer conn.Close() assertCanWriteAll(t, conn, net.Buffers{hello, cipher, req1, hello, cipher, req1}) - frag1 := constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0x01, 0x00}) - frag2 := constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0x00, 0x03, 0xaa, 0xbb, 0xcc}) + frag1 := constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0x01, 0x00, 0x00}) + frag2 := constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0x03, 0xaa, 0xbb, 0xcc}) expected := net.Buffers{ append(frag1, frag2...), // fragment 1 and fragment 2 will be merged in one single Write cipher, req1, hello, cipher, req1, // unchanged @@ -78,7 +78,7 @@ func TestStreamDialerFuncDontSplitNonClientHello(t *testing.T) { for _, tc := range cases { inner := &collectStreamDialer{} - conn := assertCanDialFragFunc(t, inner, "ipinfo.io:443", func(_ []byte) int { return 2 }) + conn := assertCanDialFragFunc(t, inner, "ipinfo.io:443", func(payload []byte) int { return len(payload) / 2 }) defer conn.Close() assertCanWriteAll(t, conn, net.Buffers{tc.pkt, cipher, req}) @@ -91,6 +91,59 @@ func TestStreamDialerFuncDontSplitNonClientHello(t *testing.T) { } } +// Make sure only the first Client Hello is splitted. +func TestFixedBytesStreamDialerSplitsClientHello(t *testing.T) { + hello := constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0x01, 0x00, 0x00, 0x03, 0xaa, 0xbb, 0xcc}) + cipher := constructTLSRecord(t, layers.TLSChangeCipherSpec, 0x0303, []byte{0x01}) + req1 := constructTLSRecord(t, layers.TLSApplicationData, 0x0303, []byte{0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88}) + + cases := []struct { + msg string + original, splitted net.Buffers + splitBytes int + }{ + { + msg: "split leading bytes", + original: net.Buffers{hello, cipher, req1, hello, cipher, req1}, + splitBytes: 2, + splitted: net.Buffers{ + // fragment 1 and fragment 2 will be merged in one single Write + append( + constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0x01, 0x00}), + constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0x00, 0x03, 0xaa, 0xbb, 0xcc})...), + cipher, req1, hello, cipher, req1, + }, + }, + { + msg: "split trailing bytes", + original: net.Buffers{hello, cipher, req1, hello, cipher, req1}, + splitBytes: -2, + splitted: net.Buffers{ + // fragment 1 and fragment 2 will be merged in one single Write + append( + constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0x01, 0x00, 0x00, 0x03, 0xaa}), + constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0xbb, 0xcc})...), + cipher, req1, hello, cipher, req1, + }, + }, + { + msg: "no split", + original: net.Buffers{hello, cipher, req1, hello, cipher, req1}, + splitBytes: 0, + splitted: net.Buffers{hello, cipher, req1, hello, cipher, req1}, + }, + } + + for _, tc := range cases { + inner := &collectStreamDialer{} + conn := assertCanDialFixedBytesFrag(t, inner, "ipinfo.io:443", tc.splitBytes) + defer conn.Close() + + assertCanWriteAll(t, conn, tc.original) + require.Equal(t, tc.splitted, inner.bufs, tc.msg) + } +} + // test assertions func assertCanDialFragFunc(t *testing.T, inner transport.StreamDialer, raddr string, frag FragFunc) transport.StreamConn { @@ -103,6 +156,16 @@ func assertCanDialFragFunc(t *testing.T, inner transport.StreamDialer, raddr str return conn } +func assertCanDialFixedBytesFrag(t *testing.T, inner transport.StreamDialer, raddr string, splitBytes int) transport.StreamConn { + d, err := NewFixedBytesStreamDialer(inner, splitBytes) + require.NoError(t, err) + require.NotNil(t, d) + conn, err := d.Dial(context.Background(), raddr) + require.NoError(t, err) + require.NotNil(t, conn) + return conn +} + func assertCanWriteAll(t *testing.T, w io.Writer, buf net.Buffers) { for _, p := range buf { n, err := w.Write(p) From ef8fd7484243a1cacf4532f35578ddd59cf6ca66 Mon Sep 17 00:00:00 2001 From: "J. Yi" <93548144+jyyi1@users.noreply.github.com> Date: Tue, 28 Nov 2023 14:31:50 -0500 Subject: [PATCH 4/7] Update transport/tlsfrag/stream_dialer.go Co-authored-by: Vinicius Fortuna --- transport/tlsfrag/stream_dialer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/tlsfrag/stream_dialer.go b/transport/tlsfrag/stream_dialer.go index fdc786a9..e21ed5be 100644 --- a/transport/tlsfrag/stream_dialer.go +++ b/transport/tlsfrag/stream_dialer.go @@ -79,7 +79,7 @@ func WrapConnFunc(base transport.StreamConn, frag FragFunc) (transport.StreamCon return transport.WrapConn(base, base, w), nil } -// NewFixedBytesStreamDialer is a [transport.StreamDialer] that modifies the [TLS Client Hello] message. It splits the +// NewFixedBytesStreamDialer is a [transport.StreamDialer] that fragments the handshake TLS record. It splits the // Client Hello message into two parts based on the given splitBytes. If splitBytes is positive, the first piece will // contain the specified number of leading bytes from the original message. If it is negative, the second piece will // contain the specified number of trailing bytes. From d54688c4cb0644073225014dc527e5c470582f0a Mon Sep 17 00:00:00 2001 From: jyyi1 Date: Tue, 28 Nov 2023 16:32:21 -0500 Subject: [PATCH 5/7] Add test case for nested fragmentations --- transport/tlsfrag/doc.go | 27 ++++++------ transport/tlsfrag/stream_dialer.go | 20 ++++----- transport/tlsfrag/stream_dialer_test.go | 58 +++++++++++++++++++------ 3 files changed, 69 insertions(+), 36 deletions(-) diff --git a/transport/tlsfrag/doc.go b/transport/tlsfrag/doc.go index 4dd5aac0..8b050c9d 100644 --- a/transport/tlsfrag/doc.go +++ b/transport/tlsfrag/doc.go @@ -13,25 +13,26 @@ // limitations under the License. /* -Package tlsfrag provides tools to split a single [TLS Client Hello message] -into multiple [TLS records]. This technique, known as TLS record fragmentation, -forces censors to maintain state and allocate memory for potential reassembly, -making censorship more difficult and resource-intensive. For detailed -explanation on how this technique works, refer to [Circumventing the GFW with -TLS Record Fragmentation]. +Package tlsfrag provides tools to split the [TLS handshake record] containing +the [Client Hello message] into multiple [TLS records]. This technique, +known as TLS record fragmentation, forces censors to maintain state and +allocate memory for potential reassembly, making censorship more difficult and +resource-intensive. For detailed explanation on how this technique works, refer +to [Circumventing the GFW with TLS Record Fragmentation]. This package offers convenient helper functions to create a TLS -[transport.StreamDialer] that fragments the [TLS Client Hello message]: - - [NewFixedBytesStreamDialer] creates a [transport.StreamDialer] that splits - the Client Hello message into two records. One record will have the - specified splitBytes length. - - [NewStreamDialerFunc] offers a more flexible way to fragment Client Hello - message. It accepts a callback function that determines the split point, +[transport.StreamDialer] that fragments the [TLS handshake record]: + - [NewFixedLenStreamDialer] creates a [transport.StreamDialer] that splits + the [Client Hello message] into two records. One of the records will have + the specified length of splitLen bytes. + - [NewStreamDialerFunc] offers a more flexible way to fragment [Client Hello + message]. It accepts a callback function that determines the split point, enabling advanced splitting logic such as splitting based on the SNI extension. [Circumventing the GFW with TLS Record Fragmentation]: https://upb-syssec.github.io/blog/2023/record-fragmentation/#tls-record-fragmentation -[TLS Client Hello message]: https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2 [TLS records]: https://datatracker.ietf.org/doc/html/rfc8446#section-5.1 +[TLS handshake record]: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.3 +[Client Hello message]: https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2 */ package tlsfrag diff --git a/transport/tlsfrag/stream_dialer.go b/transport/tlsfrag/stream_dialer.go index e21ed5be..550ae131 100644 --- a/transport/tlsfrag/stream_dialer.go +++ b/transport/tlsfrag/stream_dialer.go @@ -79,24 +79,24 @@ func WrapConnFunc(base transport.StreamConn, frag FragFunc) (transport.StreamCon return transport.WrapConn(base, base, w), nil } -// NewFixedBytesStreamDialer is a [transport.StreamDialer] that fragments the handshake TLS record. It splits the -// Client Hello message into two parts based on the given splitBytes. If splitBytes is positive, the first piece will -// contain the specified number of leading bytes from the original message. If it is negative, the second piece will -// contain the specified number of trailing bytes. +// NewFixedLenStreamDialer is a [transport.StreamDialer] that fragments the [TLS handshake record]. It splits the +// record into two records based on the given splitLen. If splitLen is positive, the first piece will contain the +// specified number of leading bytes from the original message. If it is negative, the second piece will contain +// the specified number of trailing bytes. // -// [TLS Client Hello]: https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2 -func NewFixedBytesStreamDialer(base transport.StreamDialer, splitBytes int) (transport.StreamDialer, error) { +// [TLS handshake record]: https://datatracker.ietf.org/doc/html/rfc8446#section-5.1 +func NewFixedLenStreamDialer(base transport.StreamDialer, splitLen int) (transport.StreamDialer, error) { if base == nil { return nil, errors.New("base dialer must not be nil") } - if splitBytes == 0 { + if splitLen == 0 { return base, nil } return NewStreamDialerFunc(base, func(record []byte) int { - if splitBytes > 0 { + if splitLen > 0 { // TODO: optimize for the leading bytes split - return splitBytes + return splitLen } - return len(record) + splitBytes + return len(record) + splitLen }) } diff --git a/transport/tlsfrag/stream_dialer_test.go b/transport/tlsfrag/stream_dialer_test.go index 95b93a3b..9efcf095 100644 --- a/transport/tlsfrag/stream_dialer_test.go +++ b/transport/tlsfrag/stream_dialer_test.go @@ -28,7 +28,7 @@ import ( "github.com/stretchr/testify/require" ) -// Make sure only the first Client Hello is splitted. +// Make sure only the first Client Hello is splitted in half. func TestStreamDialerFuncSplitsClientHello(t *testing.T) { hello := constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0x01, 0x00, 0x00, 0x03, 0xaa, 0xbb, 0xcc}) cipher := constructTLSRecord(t, layers.TLSChangeCipherSpec, 0x0303, []byte{0x01}) @@ -43,8 +43,8 @@ func TestStreamDialerFuncSplitsClientHello(t *testing.T) { frag1 := constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0x01, 0x00, 0x00}) frag2 := constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0x03, 0xaa, 0xbb, 0xcc}) expected := net.Buffers{ - append(frag1, frag2...), // fragment 1 and fragment 2 will be merged in one single Write - cipher, req1, hello, cipher, req1, // unchanged + append(frag1, frag2...), // First two fragments will be merged in one single Write + cipher, req1, hello, cipher, req1, // Unchanged } require.Equal(t, expected, inner.bufs) } @@ -84,15 +84,15 @@ func TestStreamDialerFuncDontSplitNonClientHello(t *testing.T) { assertCanWriteAll(t, conn, net.Buffers{tc.pkt, cipher, req}) expected := net.Buffers{tc.pkt, cipher, req} if len(tc.pkt) > 5 { - // header and content of the first pkt might be issued by two Writes, but they are not fragmented + // Header and content of the first pkt might be issued by two Writes, but they are not fragmented expected = net.Buffers{tc.pkt[:5], tc.pkt[5:], cipher, req} } require.Equal(t, expected, inner.bufs, tc.msg) } } -// Make sure only the first Client Hello is splitted. -func TestFixedBytesStreamDialerSplitsClientHello(t *testing.T) { +// Make sure only the first Client Hello is splitted by a fixed length. +func TestFixedLenStreamDialerSplitsClientHello(t *testing.T) { hello := constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0x01, 0x00, 0x00, 0x03, 0xaa, 0xbb, 0xcc}) cipher := constructTLSRecord(t, layers.TLSChangeCipherSpec, 0x0303, []byte{0x01}) req1 := constructTLSRecord(t, layers.TLSApplicationData, 0x0303, []byte{0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88}) @@ -107,7 +107,7 @@ func TestFixedBytesStreamDialerSplitsClientHello(t *testing.T) { original: net.Buffers{hello, cipher, req1, hello, cipher, req1}, splitBytes: 2, splitted: net.Buffers{ - // fragment 1 and fragment 2 will be merged in one single Write + // First two fragments will be merged in one single Write append( constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0x01, 0x00}), constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0x00, 0x03, 0xaa, 0xbb, 0xcc})...), @@ -119,7 +119,7 @@ func TestFixedBytesStreamDialerSplitsClientHello(t *testing.T) { original: net.Buffers{hello, cipher, req1, hello, cipher, req1}, splitBytes: -2, splitted: net.Buffers{ - // fragment 1 and fragment 2 will be merged in one single Write + // First two fragments will be merged in one single Write append( constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0x01, 0x00, 0x00, 0x03, 0xaa}), constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0xbb, 0xcc})...), @@ -136,7 +136,7 @@ func TestFixedBytesStreamDialerSplitsClientHello(t *testing.T) { for _, tc := range cases { inner := &collectStreamDialer{} - conn := assertCanDialFixedBytesFrag(t, inner, "ipinfo.io:443", tc.splitBytes) + conn := assertCanDialFixedLenFrag(t, inner, "ipinfo.io:443", tc.splitBytes) defer conn.Close() assertCanWriteAll(t, conn, tc.original) @@ -144,6 +144,38 @@ func TestFixedBytesStreamDialerSplitsClientHello(t *testing.T) { } } +// Make sure the first Client Hello can be splitted multiple times. +func TestNestedFixedLenStreamDialerSplitsClientHello(t *testing.T) { + hello := constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{ + 0x01, 0x00, 0x00, 0x03, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, + }) + frag1 := constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0x01, 0x00, 0x00}) + frag2 := constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0x03, 0xaa, 0xbb, 0xcc, 0xdd}) + frag3 := constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0xee, 0xff, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44}) + frag4 := constructTLSRecord(t, layers.TLSHandshake, 0x0301, []byte{0x33, 0x22, 0x11}) + + cipher := constructTLSRecord(t, layers.TLSChangeCipherSpec, 0x0303, []byte{0x01}) + req1 := constructTLSRecord(t, layers.TLSApplicationData, 0x0303, []byte{0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88}) + + inner := &collectStreamDialer{} + d, err := NewFixedLenStreamDialer(inner, 3) // Further split msg[:8] mentioned below into msg[:3] + msg[3:8] + require.NoError(t, err) + d, err = NewFixedLenStreamDialer(d, 8) // Further split msg[:16] mentioned below into msg[:8] + msg[8:16] + require.NoError(t, err) + conn := assertCanDialFixedLenFrag(t, d, "ipinfo.io:443", 16) // Split into msg[:16] + msg[16:] + defer conn.Close() + + assertCanWriteAll(t, conn, net.Buffers{hello, cipher, req1, hello, cipher, req1}) + + expected := net.Buffers{ + append(frag1, frag2...), // First two fragments will be merged in one single Write + frag3, + frag4, + cipher, req1, hello, cipher, req1, // Unchanged + } + require.Equal(t, expected, inner.bufs) +} + // test assertions func assertCanDialFragFunc(t *testing.T, inner transport.StreamDialer, raddr string, frag FragFunc) transport.StreamConn { @@ -156,8 +188,8 @@ func assertCanDialFragFunc(t *testing.T, inner transport.StreamDialer, raddr str return conn } -func assertCanDialFixedBytesFrag(t *testing.T, inner transport.StreamDialer, raddr string, splitBytes int) transport.StreamConn { - d, err := NewFixedBytesStreamDialer(inner, splitBytes) +func assertCanDialFixedLenFrag(t *testing.T, inner transport.StreamDialer, raddr string, splitBytes int) transport.StreamConn { + d, err := NewFixedLenStreamDialer(inner, splitBytes) require.NoError(t, err) require.NotNil(t, d) conn, err := d.Dial(context.Background(), raddr) @@ -174,7 +206,7 @@ func assertCanWriteAll(t *testing.T, w io.Writer, buf net.Buffers) { } } -// private test helpers +// Private test helpers func constructTLSRecord(t *testing.T, typ layers.TLSType, ver layers.TLSVersion, payload []byte) []byte { pkt := layers.TLS{ @@ -204,7 +236,7 @@ func (d *collectStreamDialer) Dial(ctx context.Context, raddr string) (transport } func (c *collectStreamDialer) Write(p []byte) (int, error) { - c.bufs = append(c.bufs, append([]byte{}, p...)) // copy p rather than retaining it according to the principle of Write + c.bufs = append(c.bufs, append([]byte{}, p...)) // Copy p rather than retaining it according to the principle of Write return len(p), nil } From 215713b6d2481445935adefc59d42d252731513b Mon Sep 17 00:00:00 2001 From: jyyi1 Date: Tue, 28 Nov 2023 16:35:43 -0500 Subject: [PATCH 6/7] test for negative splitLen in nested dialer as well. --- transport/tlsfrag/stream_dialer_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/tlsfrag/stream_dialer_test.go b/transport/tlsfrag/stream_dialer_test.go index 9efcf095..d2b841f7 100644 --- a/transport/tlsfrag/stream_dialer_test.go +++ b/transport/tlsfrag/stream_dialer_test.go @@ -162,7 +162,7 @@ func TestNestedFixedLenStreamDialerSplitsClientHello(t *testing.T) { require.NoError(t, err) d, err = NewFixedLenStreamDialer(d, 8) // Further split msg[:16] mentioned below into msg[:8] + msg[8:16] require.NoError(t, err) - conn := assertCanDialFixedLenFrag(t, d, "ipinfo.io:443", 16) // Split into msg[:16] + msg[16:] + conn := assertCanDialFixedLenFrag(t, d, "ipinfo.io:443", -3) // Split msg[:19] into msg[:16] + msg[16:19] defer conn.Close() assertCanWriteAll(t, conn, net.Buffers{hello, cipher, req1, hello, cipher, req1}) From 60be98af9e6cf62d7ab965bf194d106ac29a11b8 Mon Sep 17 00:00:00 2001 From: jyyi1 Date: Thu, 30 Nov 2023 16:51:00 -0500 Subject: [PATCH 7/7] rename splitBytes --- transport/tlsfrag/stream_dialer_test.go | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/transport/tlsfrag/stream_dialer_test.go b/transport/tlsfrag/stream_dialer_test.go index d2b841f7..c88754a8 100644 --- a/transport/tlsfrag/stream_dialer_test.go +++ b/transport/tlsfrag/stream_dialer_test.go @@ -100,12 +100,12 @@ func TestFixedLenStreamDialerSplitsClientHello(t *testing.T) { cases := []struct { msg string original, splitted net.Buffers - splitBytes int + splitLen int }{ { - msg: "split leading bytes", - original: net.Buffers{hello, cipher, req1, hello, cipher, req1}, - splitBytes: 2, + msg: "split leading bytes", + original: net.Buffers{hello, cipher, req1, hello, cipher, req1}, + splitLen: 2, splitted: net.Buffers{ // First two fragments will be merged in one single Write append( @@ -115,9 +115,9 @@ func TestFixedLenStreamDialerSplitsClientHello(t *testing.T) { }, }, { - msg: "split trailing bytes", - original: net.Buffers{hello, cipher, req1, hello, cipher, req1}, - splitBytes: -2, + msg: "split trailing bytes", + original: net.Buffers{hello, cipher, req1, hello, cipher, req1}, + splitLen: -2, splitted: net.Buffers{ // First two fragments will be merged in one single Write append( @@ -127,16 +127,16 @@ func TestFixedLenStreamDialerSplitsClientHello(t *testing.T) { }, }, { - msg: "no split", - original: net.Buffers{hello, cipher, req1, hello, cipher, req1}, - splitBytes: 0, - splitted: net.Buffers{hello, cipher, req1, hello, cipher, req1}, + msg: "no split", + original: net.Buffers{hello, cipher, req1, hello, cipher, req1}, + splitLen: 0, + splitted: net.Buffers{hello, cipher, req1, hello, cipher, req1}, }, } for _, tc := range cases { inner := &collectStreamDialer{} - conn := assertCanDialFixedLenFrag(t, inner, "ipinfo.io:443", tc.splitBytes) + conn := assertCanDialFixedLenFrag(t, inner, "ipinfo.io:443", tc.splitLen) defer conn.Close() assertCanWriteAll(t, conn, tc.original) @@ -188,8 +188,8 @@ func assertCanDialFragFunc(t *testing.T, inner transport.StreamDialer, raddr str return conn } -func assertCanDialFixedLenFrag(t *testing.T, inner transport.StreamDialer, raddr string, splitBytes int) transport.StreamConn { - d, err := NewFixedLenStreamDialer(inner, splitBytes) +func assertCanDialFixedLenFrag(t *testing.T, inner transport.StreamDialer, raddr string, splitLen int) transport.StreamConn { + d, err := NewFixedLenStreamDialer(inner, splitLen) require.NoError(t, err) require.NotNil(t, d) conn, err := d.Dial(context.Background(), raddr)