From db99e8a6a5ac8f96b6781ab050458c0ae1eff448 Mon Sep 17 00:00:00 2001 From: Aleksei Kharlamov Date: Sat, 2 Nov 2024 12:50:58 +0100 Subject: [PATCH] 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 ef66a9d5..04d0c08e 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 70bbc73a..1e23ff05 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=