From 2725bceb45d3af74af03dc1a552c848da4435d5d Mon Sep 17 00:00:00 2001 From: Petr Date: Wed, 13 Nov 2024 17:50:33 +0100 Subject: [PATCH] Disorder strategy from byeDPI (#323) --- x/configurl/disorder.go | 39 +++++++++++++++++++ x/configurl/doc.go | 24 ++++++++++++ x/configurl/module.go | 1 + x/disorder/stream_dialer.go | 71 ++++++++++++++++++++++++++++++++++ x/disorder/writer.go | 76 +++++++++++++++++++++++++++++++++++++ 5 files changed, 211 insertions(+) create mode 100644 x/configurl/disorder.go create mode 100644 x/disorder/stream_dialer.go create mode 100644 x/disorder/writer.go diff --git a/x/configurl/disorder.go b/x/configurl/disorder.go new file mode 100644 index 00000000..5ad5b40a --- /dev/null +++ b/x/configurl/disorder.go @@ -0,0 +1,39 @@ +// Copyright 2024 The Outline Authors +// +// 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 configurl + +import ( + "context" + "fmt" + "strconv" + + "github.com/Jigsaw-Code/outline-sdk/transport" + "github.com/Jigsaw-Code/outline-sdk/x/disorder" +) + +func registerDisorderDialer(r TypeRegistry[transport.StreamDialer], typeID string, newSD BuildFunc[transport.StreamDialer]) { + r.RegisterType(typeID, func(ctx context.Context, config *Config) (transport.StreamDialer, error) { + sd, err := newSD(ctx, config.BaseConfig) + if err != nil { + return nil, err + } + disorderPacketNStr := config.URL.Opaque + disorderPacketN, err := strconv.Atoi(disorderPacketNStr) + if err != nil { + return nil, fmt.Errorf("disoder: could not parse splice position: %v", err) + } + return disorder.NewStreamDialer(sd, disorderPacketN) + }) +} diff --git a/x/configurl/doc.go b/x/configurl/doc.go index 9b2191d2..cea90498 100644 --- a/x/configurl/doc.go +++ b/x/configurl/doc.go @@ -103,12 +103,36 @@ For more details, refer to [github.com/Jigsaw-Code/outline-sdk/transport/tlsfrag tlsfrag:[LENGTH] +Packet reordering (streams only, package [github.com/Jigsaw-Code/outline-sdk/x/disorder]) + +The disorder strategy sends TCP packets out of order by manipulating the +socket's Time To Live (TTL) or Hop Limit. It temporarily sets the TTL to a low +value, causing specific packets to be dropped early in the network (like at the +first router). These dropped packets are then re-transmitted later by the TCP +stack, resulting in the receiver getting packets out of order. This can help +bypass network filters that rely on inspecting the initial packets of a TCP +connection. + + disorder:[PACKET_NUMBER] + +PACKET_NUMBER: The number of writes before the disorder action occurs. The +disorder action triggers when the number of writes equals PACKET_NUMBER. If set +to 0 (default), the disorder happens on the first write. If set to 1, it happens +on the second write, and so on. + # Examples Packet splitting - To split outgoing streams on bytes 2 and 123, you can use: split:2|split:123 +Disorder transport - Send some of the packets out of order: + + disorder:0|split:123 + +Split at position 123, then send packet 0 of 123 bytes (from splitting) out of order. The network filter will first receive packet 1, only then packet 0. This +is done by setting the hop limit for the write to 1, and then restoring it. It will be sent with its original hop limit on retransmission. + Evading DNS and SNI blocking - You can use Cloudflare's DNS-over-HTTPS to protect against DNS disruption. The DoH resolver cloudflare-dns.com is accessible from any cloudflare.net IP, so you can specify the address to avoid blocking of the resolver itself. This can be combines with a TCP split or TLS Record Fragmentation to bypass SNI-based blocking: diff --git a/x/configurl/module.go b/x/configurl/module.go index 83e14b89..dd86e221 100644 --- a/x/configurl/module.go +++ b/x/configurl/module.go @@ -42,6 +42,7 @@ func NewProviderContainer() *ProviderContainer { // RegisterDefaultProviders registers a set of default providers with the providers in [ProviderContainer]. func RegisterDefaultProviders(c *ProviderContainer) *ProviderContainer { // Please keep the list in alphabetical order. + registerDisorderDialer(&c.StreamDialers, "disorder", c.StreamDialers.NewInstance) registerDO53StreamDialer(&c.StreamDialers, "do53", c.StreamDialers.NewInstance, c.PacketDialers.NewInstance) registerDOHStreamDialer(&c.StreamDialers, "doh", c.StreamDialers.NewInstance) diff --git a/x/disorder/stream_dialer.go b/x/disorder/stream_dialer.go new file mode 100644 index 00000000..e8466eb7 --- /dev/null +++ b/x/disorder/stream_dialer.go @@ -0,0 +1,71 @@ +// Copyright 2024 The Outline Authors +// +// 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 disorder + +import ( + "context" + "errors" + "fmt" + "net" + + "github.com/Jigsaw-Code/outline-sdk/transport" + "github.com/Jigsaw-Code/outline-sdk/x/sockopt" +) + +type disorderDialer struct { + dialer transport.StreamDialer + disorderPacketN int +} + +var _ transport.StreamDialer = (*disorderDialer)(nil) + +// NewStreamDialer creates a [transport.StreamDialer]. +// It works like this: +// * Wait for disorderPacketN'th call to Write. All Write requests before and after the target packet are written normally. +// * Send the disorderPacketN'th packet with TTL == 1. +// * This packet is dropped somewhere in the network and never reaches the server. +// * TTL is restored. +// * The next part of data is sent normally. +// * Server notices the lost fragment and requests re-transmission of lost packet. +func NewStreamDialer(dialer transport.StreamDialer, disorderPacketN int) (transport.StreamDialer, error) { + if dialer == nil { + return nil, errors.New("argument dialer must not be nil") + } + if disorderPacketN < 0 { + return nil, fmt.Errorf("disorder argument must be >= 0, got %d", disorderPacketN) + } + return &disorderDialer{dialer: dialer, disorderPacketN: disorderPacketN}, nil +} + +// DialStream implements [transport.StreamDialer].DialStream. +func (d *disorderDialer) DialStream(ctx context.Context, remoteAddr string) (transport.StreamConn, error) { + innerConn, err := d.dialer.DialStream(ctx, remoteAddr) + if err != nil { + return nil, err + } + + tcpInnerConn, ok := innerConn.(*net.TCPConn) + if !ok { + return nil, fmt.Errorf("disorder strategy: expected base dialer to return TCPConn") + } + tcpOptions, err := sockopt.NewTCPOptions(tcpInnerConn) + if err != nil { + return nil, err + } + + dw := NewWriter(innerConn, tcpOptions, d.disorderPacketN) + + return transport.WrapConn(innerConn, innerConn, dw), nil +} diff --git a/x/disorder/writer.go b/x/disorder/writer.go new file mode 100644 index 00000000..cd397e66 --- /dev/null +++ b/x/disorder/writer.go @@ -0,0 +1,76 @@ +// Copyright 2024 The Outline Authors +// +// 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 disorder + +import ( + "fmt" + "io" + + "github.com/Jigsaw-Code/outline-sdk/x/sockopt" +) + +type disorderWriter struct { + conn io.Writer + tcpOptions sockopt.TCPOptions + writesToDisorder int +} + +var _ io.Writer = (*disorderWriter)(nil) + +func NewWriter(conn io.Writer, tcpOptions sockopt.TCPOptions, runAtPacketN int) io.Writer { + // TODO: Support ReadFrom. + return &disorderWriter{ + conn: conn, + tcpOptions: tcpOptions, + writesToDisorder: runAtPacketN, + } +} + +func (w *disorderWriter) Write(data []byte) (written int, err error) { + if w.writesToDisorder == 0 { + defaultHopLimit, err := w.tcpOptions.HopLimit() + if err != nil { + return 0, fmt.Errorf("failed to get the hop limit: %w", err) + } + + // Setting number of hops to 1 will lead to data to get lost on host. + err = w.tcpOptions.SetHopLimit(1) + if err != nil { + return 0, fmt.Errorf("failed to set the hop limit to 1: %w", err) + } + + defer func() { + // The packet with low hop limit was sent. + // Make next calls send data normally. + // + // The packet with the low hop limit will get resent by the kernel later. + // The network filters will receive data out of order. + err = w.tcpOptions.SetHopLimit(defaultHopLimit) + if err != nil { + err = fmt.Errorf("failed to set the hop limit %d: %w", defaultHopLimit, err) + } + }() + } + + // The packet will get lost at the first send, since the hop limit is too low. + n, err := w.conn.Write(data) + + // TODO: Wait for queued data to be sent by the kernel to the socket. + + if w.writesToDisorder > -1 { + w.writesToDisorder -= 1 + } + return n, err +}