From 5327e6af11c5745ac33650933c0d3e8cf23c735b Mon Sep 17 00:00:00 2001 From: Aleksei Kharlamov Date: Sat, 2 Nov 2024 12:50:58 +0100 Subject: [PATCH 1/8] WIP: disorder: a new linux-only transport --- x/configurl/disorder.go | 39 +++++++++++++++ x/configurl/module.go | 1 + x/disorder/stream_dialer.go | 97 +++++++++++++++++++++++++++++++++++++ x/disorder/writer.go | 66 +++++++++++++++++++++++++ x/examples/fetch/.gitignore | 1 + x/go.mod | 10 ++-- x/go.sum | 20 ++++---- 7 files changed, 219 insertions(+), 15 deletions(-) create mode 100644 x/configurl/disorder.go create mode 100644 x/disorder/stream_dialer.go create mode 100644 x/disorder/writer.go create mode 100644 x/examples/fetch/.gitignore diff --git a/x/configurl/disorder.go b/x/configurl/disorder.go new file mode 100644 index 00000000..b36cf34f --- /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 + } + prefixBytesStr := config.URL.Opaque + prefixBytes, err := strconv.Atoi(prefixBytesStr) + if err != nil { + return nil, fmt.Errorf("prefixBytes is not a number: %v. Split config should be in split: format", prefixBytesStr) + } + return disorder.NewStreamDialer(sd, int64(prefixBytes)) + }) +} 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..bde4b409 --- /dev/null +++ b/x/disorder/stream_dialer.go @@ -0,0 +1,97 @@ +// 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" + "net/netip" + + "github.com/Jigsaw-Code/outline-sdk/transport" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +var defaultTTL = 64 + +type disorderDialer struct { + dialer transport.StreamDialer + splitPoint int64 +} + +var _ transport.StreamDialer = (*disorderDialer)(nil) + +// NewStreamDialer creates a [transport.StreamDialer] +// It work almost the same as the other split dialer, however, it also manipulates socket TTL: +// * Before sending the first prefixBytes TTL is set to 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 +// Currently this only works with Linux kernel (for Windows/Mac a different implementation is required) +func NewStreamDialer(dialer transport.StreamDialer, prefixBytes int64) (transport.StreamDialer, error) { + if dialer == nil { + return nil, errors.New("argument dialer must not be nil") + } + return &disorderDialer{dialer: dialer, splitPoint: prefixBytes}, 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 + } + + oldTTL, err := setTtl(innerConn, 1) + if err != nil { + return nil, fmt.Errorf("disorder strategy: failed to change ttl: %w", err) + } + + dw := NewWriter(innerConn, d.splitPoint, oldTTL) + + return transport.WrapConn(innerConn, innerConn, dw), nil +} + +// setTtl changes the socket TTL and returns the old value +// socket must be `*net.TCPConn` +func setTtl(conn net.Conn, ttl int) (oldTTL int, err error) { + addr, err := netip.ParseAddrPort(conn.RemoteAddr().String()) + if err != nil { + return 0, err + } + + switch { + case addr.Addr().Is4(): + conn := ipv4.NewConn(conn) + oldTTL, _ = conn.TTL() + err = conn.SetTTL(ttl) + case addr.Addr().Is6(): + conn := ipv6.NewConn(conn) + oldTTL, _ = conn.HopLimit() + err = conn.SetHopLimit(ttl) + } + if err != nil { + return 0, fmt.Errorf("failed to change TTL: %w", err) + } + + if oldTTL == 0 { + oldTTL = defaultTTL + } + + return +} diff --git a/x/disorder/writer.go b/x/disorder/writer.go new file mode 100644 index 00000000..db729cdc --- /dev/null +++ b/x/disorder/writer.go @@ -0,0 +1,66 @@ +// 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" + "net" + "sync" +) + +type disorderWriter struct { + conn net.Conn + resetTTL sync.Once + prefixBytes int64 + oldTTL int +} + +var _ io.Writer = (*disorderWriter)(nil) + +// TODO +// var _ io.ReaderFrom = (*splitWriterReaderFrom)(nil) + +// TODO +func NewWriter(conn net.Conn, prefixBytes int64, oldTTL int) io.Writer { + // TODO support ReaderFrom + return &disorderWriter{ + conn: conn, + prefixBytes: prefixBytes, + oldTTL: oldTTL, + } +} + +func (w *disorderWriter) Write(data []byte) (written int, err error) { + if 0 < w.prefixBytes && w.prefixBytes < int64(len(data)) { + written, err = w.conn.Write(data[:w.prefixBytes]) + w.prefixBytes -= int64(written) + if err != nil { + return written, err + } + data = data[written:] + } + w.resetTTL.Do(func() { + _, err = setTtl(w.conn, w.oldTTL) + }) + if err != nil { + return written, fmt.Errorf("setsockopt IPPROTO_IP/IP_TTL error: %w", err) + } + + n, err := w.conn.Write(data) + written += n + w.prefixBytes -= int64(n) + return written, err +} diff --git a/x/examples/fetch/.gitignore b/x/examples/fetch/.gitignore new file mode 100644 index 00000000..bad6ace5 --- /dev/null +++ b/x/examples/fetch/.gitignore @@ -0,0 +1 @@ +fetch diff --git a/x/go.mod b/x/go.mod index b432aec4..71db16e8 100644 --- a/x/go.mod +++ b/x/go.mod @@ -13,9 +13,9 @@ require ( github.com/stretchr/testify v1.9.0 github.com/vishvananda/netlink v1.1.0 golang.org/x/mobile v0.0.0-20240520174638-fa72addaaa1b - golang.org/x/net v0.28.0 - golang.org/x/sys v0.23.0 - golang.org/x/term v0.23.0 + golang.org/x/net v0.30.0 + golang.org/x/sys v0.26.0 + golang.org/x/term v0.25.0 ) require ( @@ -72,11 +72,11 @@ require ( github.com/wader/filtertransport v0.0.0-20200316221534-bdd9e61eee78 // indirect gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/goptlib v1.5.0 // indirect go.uber.org/mock v0.4.0 // indirect - golang.org/x/crypto v0.26.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/text v0.19.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/x/go.sum b/x/go.sum index a7238e9a..87b4075b 100644 --- a/x/go.sum +++ b/x/go.sum @@ -205,8 +205,8 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/mobile v0.0.0-20240520174638-fa72addaaa1b h1:WX7nnnLfCEXg+FmdYZPai2XuP3VqCP1HZVMST0n9DF0= @@ -224,8 +224,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -247,8 +247,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -256,16 +256,16 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 42211fa7e802445ae1b93b956c964453e9a05033 Mon Sep 17 00:00:00 2001 From: Aleksei Kharlamov Date: Sun, 3 Nov 2024 18:16:04 +0100 Subject: [PATCH 2/8] WIP: disorder: review stuff --- x/configurl/disorder.go | 2 +- x/disorder/stream_dialer.go | 10 ++++++---- x/disorder/writer.go | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/x/configurl/disorder.go b/x/configurl/disorder.go index b36cf34f..da0695c5 100644 --- a/x/configurl/disorder.go +++ b/x/configurl/disorder.go @@ -32,7 +32,7 @@ func registerDisorderDialer(r TypeRegistry[transport.StreamDialer], typeID strin prefixBytesStr := config.URL.Opaque prefixBytes, err := strconv.Atoi(prefixBytesStr) if err != nil { - return nil, fmt.Errorf("prefixBytes is not a number: %v. Split config should be in split: format", prefixBytesStr) + return nil, fmt.Errorf("disoder: could not parse splice position: %v", err) } return disorder.NewStreamDialer(sd, int64(prefixBytes)) }) diff --git a/x/disorder/stream_dialer.go b/x/disorder/stream_dialer.go index bde4b409..30989f49 100644 --- a/x/disorder/stream_dialer.go +++ b/x/disorder/stream_dialer.go @@ -57,7 +57,7 @@ func (d *disorderDialer) DialStream(ctx context.Context, remoteAddr string) (tra return nil, err } - oldTTL, err := setTtl(innerConn, 1) + oldTTL, err := setHopLimit(innerConn, 1) if err != nil { return nil, fmt.Errorf("disorder strategy: failed to change ttl: %w", err) } @@ -67,12 +67,12 @@ func (d *disorderDialer) DialStream(ctx context.Context, remoteAddr string) (tra return transport.WrapConn(innerConn, innerConn, dw), nil } -// setTtl changes the socket TTL and returns the old value +// setHopLimit changes the socket TTL for IPv4 (or HopLimit for IPv6) and returns the old value // socket must be `*net.TCPConn` -func setTtl(conn net.Conn, ttl int) (oldTTL int, err error) { +func setHopLimit(conn net.Conn, ttl int) (oldTTL int, err error) { addr, err := netip.ParseAddrPort(conn.RemoteAddr().String()) if err != nil { - return 0, err + return 0, fmt.Errorf("could not parse remote addr: %w", err) } switch { @@ -84,6 +84,8 @@ func setTtl(conn net.Conn, ttl int) (oldTTL int, err error) { conn := ipv6.NewConn(conn) oldTTL, _ = conn.HopLimit() err = conn.SetHopLimit(ttl) + default: + return 0, fmt.Errorf("unknown remote addr type (%v)", addr.Addr().String()) } if err != nil { return 0, fmt.Errorf("failed to change TTL: %w", err) diff --git a/x/disorder/writer.go b/x/disorder/writer.go index db729cdc..d7a87cf8 100644 --- a/x/disorder/writer.go +++ b/x/disorder/writer.go @@ -53,7 +53,7 @@ func (w *disorderWriter) Write(data []byte) (written int, err error) { data = data[written:] } w.resetTTL.Do(func() { - _, err = setTtl(w.conn, w.oldTTL) + _, err = setHopLimit(w.conn, w.oldTTL) }) if err != nil { return written, fmt.Errorf("setsockopt IPPROTO_IP/IP_TTL error: %w", err) From 36def4b34a7cc7d1ca7db29a340a26f8009b47d4 Mon Sep 17 00:00:00 2001 From: Petr Zhizhin Date: Sat, 9 Nov 2024 23:41:40 +0100 Subject: [PATCH 3/8] Fix PR comments --- x/configurl/disorder.go | 6 ++-- x/disorder/stream_dialer.go | 54 ++++++++---------------------- x/disorder/writer.go | 66 +++++++++++++++++++++---------------- x/examples/fetch/.gitignore | 1 - x/go.mod | 10 +++--- x/go.sum | 20 +++++------ 6 files changed, 70 insertions(+), 87 deletions(-) delete mode 100644 x/examples/fetch/.gitignore diff --git a/x/configurl/disorder.go b/x/configurl/disorder.go index da0695c5..5ad5b40a 100644 --- a/x/configurl/disorder.go +++ b/x/configurl/disorder.go @@ -29,11 +29,11 @@ func registerDisorderDialer(r TypeRegistry[transport.StreamDialer], typeID strin if err != nil { return nil, err } - prefixBytesStr := config.URL.Opaque - prefixBytes, err := strconv.Atoi(prefixBytesStr) + 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, int64(prefixBytes)) + return disorder.NewStreamDialer(sd, disorderPacketN) }) } diff --git a/x/disorder/stream_dialer.go b/x/disorder/stream_dialer.go index 30989f49..eccdadf7 100644 --- a/x/disorder/stream_dialer.go +++ b/x/disorder/stream_dialer.go @@ -19,18 +19,14 @@ import ( "errors" "fmt" "net" - "net/netip" "github.com/Jigsaw-Code/outline-sdk/transport" - "golang.org/x/net/ipv4" - "golang.org/x/net/ipv6" + "github.com/Jigsaw-Code/outline-sdk/x/sockopt" ) -var defaultTTL = 64 - type disorderDialer struct { - dialer transport.StreamDialer - splitPoint int64 + dialer transport.StreamDialer + disorderPacketN int } var _ transport.StreamDialer = (*disorderDialer)(nil) @@ -43,11 +39,11 @@ var _ transport.StreamDialer = (*disorderDialer)(nil) // * The next part of data is sent normally // * Server notices the lost fragment and requests re-transmission // Currently this only works with Linux kernel (for Windows/Mac a different implementation is required) -func NewStreamDialer(dialer transport.StreamDialer, prefixBytes int64) (transport.StreamDialer, error) { +func NewStreamDialer(dialer transport.StreamDialer, disorderPacketN int) (transport.StreamDialer, error) { if dialer == nil { return nil, errors.New("argument dialer must not be nil") } - return &disorderDialer{dialer: dialer, splitPoint: prefixBytes}, nil + return &disorderDialer{dialer: dialer, disorderPacketN: disorderPacketN}, nil } // DialStream implements [transport.StreamDialer].DialStream. @@ -57,43 +53,21 @@ func (d *disorderDialer) DialStream(ctx context.Context, remoteAddr string) (tra return nil, err } - oldTTL, err := setHopLimit(innerConn, 1) - if err != nil { - return nil, fmt.Errorf("disorder strategy: failed to change ttl: %w", err) + tcpInnerConn, ok := innerConn.(*net.TCPConn) + if !ok { + return nil, fmt.Errorf("disorder strategy: expected base dialer to return TCPConn") } - - dw := NewWriter(innerConn, d.splitPoint, oldTTL) - - return transport.WrapConn(innerConn, innerConn, dw), nil -} - -// setHopLimit changes the socket TTL for IPv4 (or HopLimit for IPv6) and returns the old value -// socket must be `*net.TCPConn` -func setHopLimit(conn net.Conn, ttl int) (oldTTL int, err error) { - addr, err := netip.ParseAddrPort(conn.RemoteAddr().String()) + tcpOptions, err := sockopt.NewTCPOptions(tcpInnerConn) if err != nil { - return 0, fmt.Errorf("could not parse remote addr: %w", err) + return nil, err } - switch { - case addr.Addr().Is4(): - conn := ipv4.NewConn(conn) - oldTTL, _ = conn.TTL() - err = conn.SetTTL(ttl) - case addr.Addr().Is6(): - conn := ipv6.NewConn(conn) - oldTTL, _ = conn.HopLimit() - err = conn.SetHopLimit(ttl) - default: - return 0, fmt.Errorf("unknown remote addr type (%v)", addr.Addr().String()) - } + defaultHopLimit, err := tcpOptions.HopLimit() if err != nil { - return 0, fmt.Errorf("failed to change TTL: %w", err) + return nil, fmt.Errorf("disorder strategy: failed to get base connection HopLimit: %w", err) } - if oldTTL == 0 { - oldTTL = defaultTTL - } + dw := NewWriter(innerConn, tcpOptions, d.disorderPacketN, defaultHopLimit) - return + return transport.WrapConn(innerConn, innerConn, dw), nil } diff --git a/x/disorder/writer.go b/x/disorder/writer.go index d7a87cf8..8804ebc3 100644 --- a/x/disorder/writer.go +++ b/x/disorder/writer.go @@ -17,50 +17,60 @@ package disorder import ( "fmt" "io" - "net" - "sync" + + "github.com/Jigsaw-Code/outline-sdk/x/sockopt" ) type disorderWriter struct { - conn net.Conn - resetTTL sync.Once - prefixBytes int64 - oldTTL int + conn io.Writer + tcpOptions sockopt.TCPOptions + runAtPacketN int + defaultHopLimit int + writeCalls int } var _ io.Writer = (*disorderWriter)(nil) -// TODO -// var _ io.ReaderFrom = (*splitWriterReaderFrom)(nil) +// Setting number of hops to 1 will lead to data to get lost on host +var disorderHopN = 1 -// TODO -func NewWriter(conn net.Conn, prefixBytes int64, oldTTL int) io.Writer { - // TODO support ReaderFrom +func NewWriter(conn io.Writer, tcpOptions sockopt.TCPOptions, runAtPacketN int, defaultHopLimit int) io.Writer { return &disorderWriter{ - conn: conn, - prefixBytes: prefixBytes, - oldTTL: oldTTL, + conn: conn, + tcpOptions: tcpOptions, + runAtPacketN: runAtPacketN, + defaultHopLimit: defaultHopLimit, + writeCalls: 0, } } func (w *disorderWriter) Write(data []byte) (written int, err error) { - if 0 < w.prefixBytes && w.prefixBytes < int64(len(data)) { - written, err = w.conn.Write(data[:w.prefixBytes]) - w.prefixBytes -= int64(written) + shouldDoDisorder := w.writeCalls == w.runAtPacketN + if shouldDoDisorder { + err = w.tcpOptions.SetHopLimit(disorderHopN) if err != nil { - return written, err + return 0, fmt.Errorf("failed to set the hop limit to %d: %w", disorderHopN, err) } - data = data[written:] - } - w.resetTTL.Do(func() { - _, err = setHopLimit(w.conn, w.oldTTL) - }) - if err != nil { - return written, fmt.Errorf("setsockopt IPPROTO_IP/IP_TTL error: %w", err) + + // The packet will get lost at the first send, since the hop limit is too low } n, err := w.conn.Write(data) - written += n - w.prefixBytes -= int64(n) - return written, err + + // TODO: Wait for queued data to be sent by the kernel to the socket + + if shouldDoDisorder { + // 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(w.defaultHopLimit) + if err != nil { + return n, fmt.Errorf("failed to set the hop limit error %d: %w", w.defaultHopLimit, err) + } + } + + w.writeCalls += 1 + return n, err } diff --git a/x/examples/fetch/.gitignore b/x/examples/fetch/.gitignore deleted file mode 100644 index bad6ace5..00000000 --- a/x/examples/fetch/.gitignore +++ /dev/null @@ -1 +0,0 @@ -fetch diff --git a/x/go.mod b/x/go.mod index 71db16e8..b432aec4 100644 --- a/x/go.mod +++ b/x/go.mod @@ -13,9 +13,9 @@ require ( github.com/stretchr/testify v1.9.0 github.com/vishvananda/netlink v1.1.0 golang.org/x/mobile v0.0.0-20240520174638-fa72addaaa1b - golang.org/x/net v0.30.0 - golang.org/x/sys v0.26.0 - golang.org/x/term v0.25.0 + golang.org/x/net v0.28.0 + golang.org/x/sys v0.23.0 + golang.org/x/term v0.23.0 ) require ( @@ -72,11 +72,11 @@ require ( github.com/wader/filtertransport v0.0.0-20200316221534-bdd9e61eee78 // indirect gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/goptlib v1.5.0 // indirect go.uber.org/mock v0.4.0 // indirect - golang.org/x/crypto v0.28.0 // indirect + golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/text v0.17.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/x/go.sum b/x/go.sum index 87b4075b..a7238e9a 100644 --- a/x/go.sum +++ b/x/go.sum @@ -205,8 +205,8 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/mobile v0.0.0-20240520174638-fa72addaaa1b h1:WX7nnnLfCEXg+FmdYZPai2XuP3VqCP1HZVMST0n9DF0= @@ -224,8 +224,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -247,8 +247,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -256,16 +256,16 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 5d6424a286610bbca972a699e879f06ad289e46f Mon Sep 17 00:00:00 2001 From: Petr Zhizhin Date: Tue, 12 Nov 2024 21:31:34 +0100 Subject: [PATCH 4/8] Review fixes --- x/configurl/doc.go | 23 ++++++++++++++++ x/disorder/stream_dialer.go | 10 +++---- x/disorder/writer.go | 54 ++++++++++++++++++------------------- 3 files changed, 54 insertions(+), 33 deletions(-) diff --git a/x/configurl/doc.go b/x/configurl/doc.go index 9b2191d2..29c7aa70 100644 --- a/x/configurl/doc.go +++ b/x/configurl/doc.go @@ -103,12 +103,35 @@ For more details, refer to [github.com/Jigsaw-Code/outline-sdk/transport/tlsfrag tlsfrag:[LENGTH] +Disorder transport (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. + 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/disorder/stream_dialer.go b/x/disorder/stream_dialer.go index eccdadf7..a280ee60 100644 --- a/x/disorder/stream_dialer.go +++ b/x/disorder/stream_dialer.go @@ -43,6 +43,9 @@ func NewStreamDialer(dialer transport.StreamDialer, disorderPacketN int) (transp 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 } @@ -62,12 +65,7 @@ func (d *disorderDialer) DialStream(ctx context.Context, remoteAddr string) (tra return nil, err } - defaultHopLimit, err := tcpOptions.HopLimit() - if err != nil { - return nil, fmt.Errorf("disorder strategy: failed to get base connection HopLimit: %w", err) - } - - dw := NewWriter(innerConn, tcpOptions, d.disorderPacketN, defaultHopLimit) + 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 index 8804ebc3..b6dc1d4c 100644 --- a/x/disorder/writer.go +++ b/x/disorder/writer.go @@ -22,11 +22,9 @@ import ( ) type disorderWriter struct { - conn io.Writer - tcpOptions sockopt.TCPOptions - runAtPacketN int - defaultHopLimit int - writeCalls int + conn io.Writer + tcpOptions sockopt.TCPOptions + writesToDisorder int } var _ io.Writer = (*disorderWriter)(nil) @@ -34,43 +32,45 @@ var _ io.Writer = (*disorderWriter)(nil) // Setting number of hops to 1 will lead to data to get lost on host var disorderHopN = 1 -func NewWriter(conn io.Writer, tcpOptions sockopt.TCPOptions, runAtPacketN int, defaultHopLimit int) io.Writer { +func NewWriter(conn io.Writer, tcpOptions sockopt.TCPOptions, runAtPacketN int) io.Writer { + // TODO: Support ReadFrom. return &disorderWriter{ - conn: conn, - tcpOptions: tcpOptions, - runAtPacketN: runAtPacketN, - defaultHopLimit: defaultHopLimit, - writeCalls: 0, + conn: conn, + tcpOptions: tcpOptions, + writesToDisorder: runAtPacketN, } } func (w *disorderWriter) Write(data []byte) (written int, err error) { - shouldDoDisorder := w.writeCalls == w.runAtPacketN - if shouldDoDisorder { + if w.writesToDisorder == 0 { + defaultHopLimit, err := w.tcpOptions.HopLimit() + if err != nil { + return 0, fmt.Errorf("failed to get the hop limit: %w", err) + } + err = w.tcpOptions.SetHopLimit(disorderHopN) if err != nil { return 0, fmt.Errorf("failed to set the hop limit to %d: %w", disorderHopN, err) } - // The packet will get lost at the first send, since the hop limit is too low + 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 error %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 shouldDoDisorder { - // 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(w.defaultHopLimit) - if err != nil { - return n, fmt.Errorf("failed to set the hop limit error %d: %w", w.defaultHopLimit, err) - } - } - - w.writeCalls += 1 + w.writesToDisorder -= 1 return n, err } From b6aef3d293d6347fb0e2c23d79ab0ab3c03131f8 Mon Sep 17 00:00:00 2001 From: Petr Zhizhin Date: Wed, 13 Nov 2024 02:22:25 +0100 Subject: [PATCH 5/8] review fixes --- x/configurl/doc.go | 7 ++++--- x/disorder/stream_dialer.go | 14 +++++++------- x/disorder/writer.go | 18 ++++++++++-------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/x/configurl/doc.go b/x/configurl/doc.go index 29c7aa70..cea90498 100644 --- a/x/configurl/doc.go +++ b/x/configurl/doc.go @@ -103,7 +103,7 @@ For more details, refer to [github.com/Jigsaw-Code/outline-sdk/transport/tlsfrag tlsfrag:[LENGTH] -Disorder transport (streams only, package [github.com/Jigsaw-Code/outline-sdk/x/disorder]) +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 @@ -126,11 +126,12 @@ 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 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. +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 diff --git a/x/disorder/stream_dialer.go b/x/disorder/stream_dialer.go index a280ee60..8bd55a36 100644 --- a/x/disorder/stream_dialer.go +++ b/x/disorder/stream_dialer.go @@ -32,13 +32,13 @@ type disorderDialer struct { var _ transport.StreamDialer = (*disorderDialer)(nil) // NewStreamDialer creates a [transport.StreamDialer] -// It work almost the same as the other split dialer, however, it also manipulates socket TTL: -// * Before sending the first prefixBytes TTL is set to 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 -// Currently this only works with Linux kernel (for Windows/Mac a different implementation is required) +// It work 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") diff --git a/x/disorder/writer.go b/x/disorder/writer.go index b6dc1d4c..0abe4250 100644 --- a/x/disorder/writer.go +++ b/x/disorder/writer.go @@ -29,7 +29,7 @@ type disorderWriter struct { var _ io.Writer = (*disorderWriter)(nil) -// Setting number of hops to 1 will lead to data to get lost on host +// Setting number of hops to 1 will lead to data to get lost on host. var disorderHopN = 1 func NewWriter(conn io.Writer, tcpOptions sockopt.TCPOptions, runAtPacketN int) io.Writer { @@ -54,11 +54,11 @@ func (w *disorderWriter) Write(data []byte) (written int, err error) { } defer func() { - // The packet with low hop limit was sent - // Make next calls send data normally + // 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 + // 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 error %d: %w", defaultHopLimit, err) @@ -66,11 +66,13 @@ func (w *disorderWriter) Write(data []byte) (written int, err error) { }() } - // The packet will get lost at the first send, since the hop limit is too low + // 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 + // TODO: Wait for queued data to be sent by the kernel to the socket. - w.writesToDisorder -= 1 + if w.writesToDisorder > -1 { + w.writesToDisorder -= 1 + } return n, err } From c801550cdeda6ac1898d9688d3c53e26100901a1 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Wed, 13 Nov 2024 11:08:46 -0500 Subject: [PATCH 6/8] Update x/disorder/stream_dialer.go --- x/disorder/stream_dialer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x/disorder/stream_dialer.go b/x/disorder/stream_dialer.go index 8bd55a36..e8466eb7 100644 --- a/x/disorder/stream_dialer.go +++ b/x/disorder/stream_dialer.go @@ -31,8 +31,8 @@ type disorderDialer struct { var _ transport.StreamDialer = (*disorderDialer)(nil) -// NewStreamDialer creates a [transport.StreamDialer] -// It work like this: +// 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. From 32477fd484bf23a180ea0b0cb186d33baed0d497 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Wed, 13 Nov 2024 11:09:02 -0500 Subject: [PATCH 7/8] Apply suggestions from code review --- x/disorder/writer.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/x/disorder/writer.go b/x/disorder/writer.go index 0abe4250..b624d6d4 100644 --- a/x/disorder/writer.go +++ b/x/disorder/writer.go @@ -29,9 +29,6 @@ type disorderWriter struct { var _ io.Writer = (*disorderWriter)(nil) -// Setting number of hops to 1 will lead to data to get lost on host. -var disorderHopN = 1 - func NewWriter(conn io.Writer, tcpOptions sockopt.TCPOptions, runAtPacketN int) io.Writer { // TODO: Support ReadFrom. return &disorderWriter{ @@ -48,7 +45,8 @@ func (w *disorderWriter) Write(data []byte) (written int, err error) { return 0, fmt.Errorf("failed to get the hop limit: %w", err) } - err = w.tcpOptions.SetHopLimit(disorderHopN) + // 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 %d: %w", disorderHopN, err) } From 2a59cf49bc256a2a9c2a91c69bc5af9d16d46989 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Wed, 13 Nov 2024 11:12:48 -0500 Subject: [PATCH 8/8] Apply suggestions from code review --- x/disorder/writer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x/disorder/writer.go b/x/disorder/writer.go index b624d6d4..cd397e66 100644 --- a/x/disorder/writer.go +++ b/x/disorder/writer.go @@ -48,7 +48,7 @@ func (w *disorderWriter) Write(data []byte) (written int, err error) { // 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 %d: %w", disorderHopN, err) + return 0, fmt.Errorf("failed to set the hop limit to 1: %w", err) } defer func() { @@ -59,7 +59,7 @@ func (w *disorderWriter) Write(data []byte) (written int, err error) { // 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 error %d: %w", defaultHopLimit, err) + err = fmt.Errorf("failed to set the hop limit %d: %w", defaultHopLimit, err) } }() }