-
Notifications
You must be signed in to change notification settings - Fork 55
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): implement TLS record fragmentation #114
Conversation
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
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.
Thanks for the contribution!
transport/tls-record-frag/writer.go
Outdated
|
||
func (w *tlsRecordFragWriter) Write(data []byte) (written int, err error) { | ||
length := len(data) | ||
if length > 5+maxRecordLength { |
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 not the correct behavior, even if the connections is guaranteed TLS. A given write may have a single record, a partial record or multiple records. We need to fix this.
- remove the check
- read the record length from the data
- fix the split calculation
We should assume the first write has the record length.
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.
Let's call the package tlssplit
, so it can be more easily used. Or perhaps tlsfrag
is better.
@jyyi1 thoughts?
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.
I like tlsfrag
. And for the stream dialer's name, I prefer tlsClientHelloFragStreamDialer
to indicate that it will only be applied to client hello packets.
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.
Thank a lot for the contribution! Could you please sign the Google CLA to make the CI job pass:
📝 If you are not currently covered under a CLA, please visit https://cla.developers.google.com/. Once you've signed, follow the "New Contributors" link at the bottom of this page to update this check.
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package tlsrecordfrag |
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.
package tlsrecordfrag | |
package tlsfrag |
var _ transport.StreamDialer = (*tlsRecordFragDialer)(nil) | ||
|
||
// NewStreamDialer creates a [transport.StreamDialer] that splits the Client Hello Message | ||
func NewStreamDialer(dialer transport.StreamDialer, prefixBytes int32) (transport.StreamDialer, error) { |
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.
I prefer to put the "fixed split point" into the function name. Because we might need to introduce other kinds of fragmentation dialers in the future, like DomainNameSplitDialer
, or MultipleSegmentsSplitDialer
.
func NewStreamDialer(dialer transport.StreamDialer, prefixBytes int32) (transport.StreamDialer, error) { | |
func NewStreamDialerWithFixedSplitPoint(dialer transport.StreamDialer, prefixBytes int32) (transport.StreamDialer, error) { |
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.
That's too long. Perhaps NewFixedPrefixStreamDialer
But I doubt we will need anything more complex anytime soon, so NewStreamDialer may be ok.
// NewStreamDialer creates a [transport.StreamDialer] that splits the Client Hello Message | ||
func NewStreamDialer(dialer transport.StreamDialer, prefixBytes int32) (transport.StreamDialer, error) { | ||
if dialer == nil { | ||
return nil, errors.New("argument dialer must not be nil") |
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.
return nil, errors.New("argument dialer must not be nil") | |
return nil, errors.New("an inner dialer is required") |
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.
The original code is consistent with the split Dialer:
return nil, errors.New("argument dialer must not be nil") |
I think we can keep it as is for now, and change them all later.
if dialer == nil { | ||
return nil, errors.New("argument dialer must not be nil") | ||
} | ||
return &tlsRecordFragDialer{dialer: dialer, splitPoint: prefixBytes}, nil |
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.
Check prefixBytes
as well?
return &tlsRecordFragDialer{dialer: dialer, splitPoint: prefixBytes}, nil | |
if prefixBytes <= 0 { | |
return nil, errors.New("prefixBytes must be positive") | |
} | |
return &tlsRecordFragDialer{dialer: dialer, splitPoint: prefixBytes}, nil |
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.
The current code is consistent with split
, and that's helpful for the user.
Also, returning an error makes the use harder. We can just assume <=0 means disabled (perhaps document that).
|
||
const maxRecordLength = 16384 //For the fragments, not for the reassembled record | ||
|
||
func NewWriter(writer io.Writer, prefixBytes int32) *tlsRecordFragWriter { |
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.
I'm not sure whether we want to expose this, maybe make it private? newWriter
.
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.
We should provide the building blocks, so users can create their own dialers.
Tying to a dialer is a big drawback, that's why we expose the writers for split and Shadowsocks.
With the writer, you can easily write a Dialer, but not the other way around
|
||
// Dial implements [transport.StreamDialer].Dial. | ||
func (d *tlsRecordFragDialer) Dial(ctx context.Context, remoteAddr string) (transport.StreamConn, error) { | ||
innerConn, err := d.dialer.Dial(ctx, remoteAddr) |
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.
We might need to inspect remoteAddr
, and make sure that we are only splitting TLS traffic (not regular TCP traffic). Here are some common ports serving TLS traffic (feel free to add more):
- 443, HTTPS
- 990, FTPS
- 563, NNTPS
- 636, LDAPS
- 993, IMAPS
- 995, POP3S
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.
That's a good point. We should be able to restrict to specific ports.
But I would like the user to be able to specify that list.
@jyyi1 what are your thoughts on the questions below?
How to specify the ports?
Perhaps we pass a function that takes a host and port, and outputs a binary decision.
It could be passed in the New function with variadic options, like we do here:
https://github.com/Jigsaw-Code/outline-sdk/blob/375a66df04ef48659c73f0a7ed1156b04b2008a4/network/packet_listener_proxy.go#L58C68-L58C75
Or as a setter.
What's the default behavior?
I think the default should be either all or nothing. But perhaps 443, since that's the most common scenario.
Or perhaps we can force the user to make a decision by making the function mandatory in the constructor. That's probably my favorite. We can provide constant functions for "all" or "443 only". Also a EnableForPorts(portList) that returns a function that enables for that list. That way someone can do:
dialer, err := tlsfrag.NewStreamDialer(inner, 10, tlsfrag.EnablePorts(int[]{443}))
or
dialer, err := tlsfrag.NewStreamDialer(inner, 10, tlsfrag.Enable443)
Reference
Please link to https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=TLS
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package tlsrecordfrag |
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.
Please make the directory match the package name.
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.
tlssplit or tlsfrag?
|
||
const maxRecordLength = 16384 //For the fragments, not for the reassembled record | ||
|
||
func NewWriter(writer io.Writer, prefixBytes int32) *tlsRecordFragWriter { |
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.
We should provide the building blocks, so users can create their own dialers.
Tying to a dialer is a big drawback, that's why we expose the writers for split and Shadowsocks.
With the writer, you can easily write a Dialer, but not the other way around
// NewStreamDialer creates a [transport.StreamDialer] that splits the Client Hello Message | ||
func NewStreamDialer(dialer transport.StreamDialer, prefixBytes int32) (transport.StreamDialer, error) { | ||
if dialer == nil { | ||
return nil, errors.New("argument dialer must not be nil") |
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.
The original code is consistent with the split Dialer:
return nil, errors.New("argument dialer must not be nil") |
I think we can keep it as is for now, and change them all later.
|
||
var _ transport.StreamDialer = (*tlsRecordFragDialer)(nil) | ||
|
||
// NewStreamDialer creates a [transport.StreamDialer] that splits the Client Hello Message |
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.
We need to clarify the behavior.
// NewStreamDialer creates a [transport.StreamDialer] that splits the Client Hello Message | |
// NewStreamDialer creates a [transport.StreamDialer] that splits the first TLS record if it's a handshake message. If the record has length N, the new records will have length prefixBytes and N - prefixBytes, plus their headers. |
@jyyi1 let's make sure we agree on this behavior.
var _ transport.StreamDialer = (*tlsRecordFragDialer)(nil) | ||
|
||
// NewStreamDialer creates a [transport.StreamDialer] that splits the Client Hello Message | ||
func NewStreamDialer(dialer transport.StreamDialer, prefixBytes int32) (transport.StreamDialer, error) { |
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.
That's too long. Perhaps NewFixedPrefixStreamDialer
But I doubt we will need anything more complex anytime soon, so NewStreamDialer may be ok.
if dialer == nil { | ||
return nil, errors.New("argument dialer must not be nil") | ||
} | ||
return &tlsRecordFragDialer{dialer: dialer, splitPoint: prefixBytes}, nil |
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.
The current code is consistent with split
, and that's helpful for the user.
Also, returning an error makes the use harder. We can just assume <=0 means disabled (perhaps document that).
|
||
// Dial implements [transport.StreamDialer].Dial. | ||
func (d *tlsRecordFragDialer) Dial(ctx context.Context, remoteAddr string) (transport.StreamConn, error) { | ||
innerConn, err := d.dialer.Dial(ctx, remoteAddr) |
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.
That's a good point. We should be able to restrict to specific ports.
But I would like the user to be able to specify that list.
@jyyi1 what are your thoughts on the questions below?
How to specify the ports?
Perhaps we pass a function that takes a host and port, and outputs a binary decision.
It could be passed in the New function with variadic options, like we do here:
https://github.com/Jigsaw-Code/outline-sdk/blob/375a66df04ef48659c73f0a7ed1156b04b2008a4/network/packet_listener_proxy.go#L58C68-L58C75
Or as a setter.
What's the default behavior?
I think the default should be either all or nothing. But perhaps 443, since that's the most common scenario.
Or perhaps we can force the user to make a decision by making the function mandatory in the constructor. That's probably my favorite. We can provide constant functions for "all" or "443 only". Also a EnableForPorts(portList) that returns a function that enables for that list. That way someone can do:
dialer, err := tlsfrag.NewStreamDialer(inner, 10, tlsfrag.EnablePorts(int[]{443}))
or
dialer, err := tlsfrag.NewStreamDialer(inner, 10, tlsfrag.Enable443)
Reference
Please link to https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=TLS
@jyyi1 How can I get a non-Chinese phone number to register Google account? |
@Lanius-collaris Ugh, I see the issue. The CLA system requires Google account, which in turn seems to require a phone number. They require Google accounts because they are used to link individual and corporate CLAs to covered contributors and provide authentication for CLA management. Have you tried the "use existing email" option when creating the account? Google needs a backup contact information, that's why you need a phone number. But if you provide some email, then you may not need a phone. |
@fortuna Yes, I have tried. But I got unknown error. |
tlsRecordFragWriter: check if input is a handshake record return the number of input bytes consumed add an RFC link for reference tlsRecordFragDialer: rename field
@fortuna I tried again today, after I entered the verification code from mail, Google asked for a number. |
No description provided.