Skip to content
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

Implementation OOB and disOOB strategies from byeDPI #329

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions x/configurl/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ func RegisterDefaultProviders(c *ProviderContainer) *ProviderContainer {
registerWebsocketStreamDialer(&c.StreamDialers, "ws", c.StreamDialers.NewInstance)
registerWebsocketPacketDialer(&c.PacketDialers, "ws", c.StreamDialers.NewInstance)

registerOOBStreamDialer(&c.StreamDialers, "oob", c.StreamDialers.NewInstance)

return c
}

Expand Down
48 changes: 48 additions & 0 deletions x/configurl/oob.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package configurl

import (
"context"
"fmt"
"strconv"
"strings"
"time"

"github.com/Jigsaw-Code/outline-sdk/transport"
"github.com/Jigsaw-Code/outline-sdk/x/oob"
)

func registerOOBStreamDialer(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
}
params := config.URL.Opaque

splitStr := strings.Split(params, ":")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe named arguments? https://github.com/Jigsaw-Code/outline-sdk/pull/324/files#diff-974d5fac785c258e952052dd1f6aa5b012bf2e4e43eb708de99420175a67e99b

But it's a matter of preference. @fortuna should know better.

Also, maybe you can make most arguments optional? I believe most users can use default timeout, default URG byte value, etc.

if len(splitStr) != 4 {
return nil, fmt.Errorf("oob: config should be in oob:<number>:<char>:<boolean>:<interval> format")
}

position, err := strconv.Atoi(splitStr[0])
if err != nil {
return nil, fmt.Errorf("oob: oob position is not a number: %v", splitStr[0])
}

if len(splitStr[1]) != 1 {
return nil, fmt.Errorf("oob: oob byte should be a single character: %v", splitStr[1])
}
char := splitStr[1][0]

disOOB, err := strconv.ParseBool(splitStr[2])
if err != nil {
return nil, fmt.Errorf("oob: disOOB is not a boolean: %v", splitStr[2])
}

delay, err := time.ParseDuration(splitStr[3])
if err != nil {
return nil, fmt.Errorf("oob: delay is not a duration: %v", splitStr[3])
}
return oob.NewStreamDialer(sd, int64(position), char, disOOB, delay)
})
}
14 changes: 8 additions & 6 deletions x/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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.31.0
golang.org/x/sys v0.27.0
golang.org/x/term v0.26.0
)

require (
Expand Down Expand Up @@ -72,12 +72,14 @@ 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.29.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/sync v0.9.0 // indirect
golang.org/x/text v0.20.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
)

replace github.com/Jigsaw-Code/outline-sdk => /Users/sirius/repos/outline-sdk/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert. For development, you can use Go workspaces instead.

14 changes: 12 additions & 2 deletions x/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ github.com/AndreasBriese/bbloom v0.0.0-20170702084017-28f7e881ca57 h1:CVuXDbdzPW
github.com/AndreasBriese/bbloom v0.0.0-20170702084017-28f7e881ca57/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Jigsaw-Code/outline-sdk v0.0.18-0.20241106233708-faffebb12629 h1:sHi1X4vwtNNBUDCbxynGXe7cM/inwTbavowHziaxlbk=
github.com/Jigsaw-Code/outline-sdk v0.0.18-0.20241106233708-faffebb12629/go.mod h1:CFDKyGZA4zatKE4vMLe8TyQpZCyINOeRFbMAmYHxodw=
github.com/Psiphon-Inc/rotate-safe-writer v0.0.0-20210303140923-464a7a37606e h1:NPfqIbzmijrl0VclX2t8eO5EPBhqe47LLGKpRrcVjXk=
github.com/Psiphon-Inc/rotate-safe-writer v0.0.0-20210303140923-464a7a37606e/go.mod h1:ZdY5pBfat/WVzw3eXbIf7N1nZN0XD5H5+X8ZMDWbCs4=
github.com/Psiphon-Labs/bolt v0.0.0-20200624191537-23cedaef7ad7 h1:Hx/NCZTnvoKZuIBwSmxE58KKoNLXIGG6hBJYN7pj9Ag=
Expand Down Expand Up @@ -207,6 +205,8 @@ 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.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
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=
Expand All @@ -226,12 +226,16 @@ 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.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
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=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand All @@ -249,6 +253,8 @@ 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.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.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=
Expand All @@ -258,6 +264,8 @@ 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.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
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=
Expand All @@ -266,6 +274,8 @@ 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.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
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=
Expand Down
60 changes: 60 additions & 0 deletions x/oob/dialer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package oob

import (
"context"
"errors"
"fmt"
"net"
"time"

"github.com/Jigsaw-Code/outline-sdk/transport"
"github.com/Jigsaw-Code/outline-sdk/x/sockopt"
)

// oobDialer is a dialer that applies the OOB and disOOB strategies.
type oobDialer struct {
dialer transport.StreamDialer
opts sockopt.TCPOptions
oobByte byte
oobPosition int64
disOOB bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be called just disorder?

Also, can we compose with the disorder from #323 instead of reproducing the logic here?

/cc @PeterZhizhin

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a clear way how OOB and disorder can be piped together to achieve desired behavior.

That said, I believe we can extract the following API from disorder package:

func withDisorder(tcpOpts *scokopt.TCPOptions, targetHopLimit int, functionToCall func() error) error

The function will do the following:

  1. Get the current hop limit.
  2. Set the hop limit to target targetHopLimit.
  3. Calls function functionToCall
  4. Restores the hop limit.

After we have #324 merged, we will also have it wait for the socket send all bytes before resetting the hop limit.

The withDisorder function will be called in both OOB strategy and disorder strategy.

What do you think?

delay time.Duration
}

// NewStreamDialer creates a [transport.StreamDialer] that applies OOB byte sending at "oobPosition" and supports disOOB.
// "oobByte" specifies the value of the byte to send out-of-band.
func NewStreamDialer(dialer transport.StreamDialer,
oobPosition int64, oobByte byte, disOOB bool, delay time.Duration) (transport.StreamDialer, error) {
if dialer == nil {
return nil, errors.New("argument dialer must not be nil")
}
return &oobDialer{
dialer: dialer,
oobPosition: oobPosition,
oobByte: oobByte,
disOOB: disOOB,
delay: delay,
}, nil
}

// DialStream implements [transport.StreamDialer].DialStream with OOB and disOOB support.
func (d *oobDialer) DialStream(ctx context.Context, remoteAddr string) (transport.StreamConn, error) {
innerConn, err := d.dialer.DialStream(ctx, remoteAddr)
if err != nil {
return nil, err
}
// this strategy only works when we set TCP as a strategy
tcpConn, ok := innerConn.(*net.TCPConn)
if !ok {
return nil, fmt.Errorf("oob: only works with direct TCP connections")
}

opts, err := sockopt.NewTCPOptions(tcpConn)
if err != nil {
return nil, fmt.Errorf("oob: unable to get TCP options: %w", err)
}

dw := NewWriter(tcpConn, opts, d.oobPosition, d.oobByte, d.disOOB, d.delay)

return transport.WrapConn(innerConn, innerConn, dw), nil
}
129 changes: 129 additions & 0 deletions x/oob/oob_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package oob

import (
"bufio"
"context"
"net"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"

"github.com/Jigsaw-Code/outline-sdk/transport"
)

const (
msg = "Hello OOB!\n"
msgLen = len(msg)
)

// OOBDialerTestSuite - test suite for testing oobDialer and oobWriter
type OOBDialerTestSuite struct {
suite.Suite
server net.Listener
dataChan chan []byte
serverAddr string
}

// SetupSuite - runs once before all tests
func (suite *OOBDialerTestSuite) SetupSuite() {
// Start TCP server
listener, dataChan := startTestServer(suite.T())
suite.server = listener
suite.dataChan = dataChan
suite.serverAddr = listener.Addr().String()
}

// TearDownSuite - runs once after all tests
func (suite *OOBDialerTestSuite) TearDownSuite() {
suite.server.Close()
close(suite.dataChan)
}

// startTestServer - starts a test server and returns listener and data channel

func startTestServer(t *testing.T) (net.Listener, chan []byte) {
listener, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err, "Failed to create server")

dataChan := make(chan []byte, 10)
go func() {
for {
conn, err := listener.Accept()
if err != nil {
return
}

go func(conn net.Conn) {
defer conn.Close()

scanner := bufio.NewScanner(conn)
for scanner.Scan() {
line := scanner.Bytes()
dataChan <- append([]byte{}, line...)
break
}

if err := scanner.Err(); err != nil {
t.Logf("Error reading data: %v", err)
}
}(conn)
}
}()

return listener, dataChan
}

// TestDialStreamWithDifferentParameters - test data transmission with different parameters
func (suite *OOBDialerTestSuite) TestDialStreamWithDifferentParameters() {
tests := []struct {
oobPosition int64
oobByte byte
disOOB bool
delay time.Duration
}{
{oobPosition: 0, oobByte: 0x01, disOOB: false, delay: 100 * time.Millisecond},
{oobPosition: 0, oobByte: 0x01, disOOB: true, delay: 100 * time.Millisecond},

{oobPosition: 2, oobByte: 0x02, disOOB: true, delay: 200 * time.Millisecond},
{oobPosition: 2, oobByte: 0x02, disOOB: false, delay: 200 * time.Millisecond},

{oobPosition: int64(msgLen) - 2, oobByte: 0x02, disOOB: true, delay: 200 * time.Millisecond},
{oobPosition: int64(msgLen) - 2, oobByte: 0x02, disOOB: false, delay: 200 * time.Millisecond},

{oobPosition: int64(msgLen) - 1, oobByte: 0x02, disOOB: true, delay: 200 * time.Millisecond},
{oobPosition: int64(msgLen) - 1, oobByte: 0x02, disOOB: false, delay: 200 * time.Millisecond},
}

for _, tt := range tests {
suite.Run("Testing with different parameters", func() {
ctx := context.Background()

dialer := &transport.TCPDialer{
Dialer: net.Dialer{},
}
oobDialer, err := NewStreamDialer(dialer, tt.oobPosition, tt.oobByte, tt.disOOB, tt.delay)

conn, err := oobDialer.DialStream(ctx, suite.serverAddr)

require.NoError(suite.T(), err)

// Send test message
message := []byte("Hello OOB!\n")
n, err := conn.Write(message)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), len(message), n)

// Check that the server received the message
receivedData := <-suite.dataChan
assert.Equal(suite.T(), string(message[0:len(message)-1]), string(receivedData))
})
}
}

// TestOOBDialerSuite - main test suite
func TestOOBDialerSuite(t *testing.T) {
suite.Run(t, new(OOBDialerTestSuite))
}
25 changes: 25 additions & 0 deletions x/oob/unix_ops.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The convention in go is to have the platform as a name suffix. Use ops_unix.go instead. Same for windows.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//go:build linux || darwin

package oob

import (
"golang.org/x/sys/unix"
"net"
"syscall"
)

const MSG_OOB = unix.MSG_OOB

type SocketDescriptor int

func sendTo(fd SocketDescriptor, data []byte, flags int) (err error) {
return syscall.Sendmsg(int(fd), data, nil, nil, flags)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use https://pkg.go.dev/golang.org/x/sys/unix#SendmsgN, so we can return the number of bytes written in case of error.

}

func getSocketDescriptor(conn *net.TCPConn) (SocketDescriptor, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused, delete. Also, the docs say this file descriptor may not be reliable: https://pkg.go.dev/net#TCPConn.File

file, err := conn.File()
if err != nil {
return 0, err
}
return SocketDescriptor(file.Fd()), nil
}
Loading
Loading