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

OOB and disOOB implementation #2

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, ":")
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/
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
delay time.Duration
Copy link

Choose a reason for hiding this comment

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

Is the delay really needed? I found the TCP writes to be quite consistent and never had issues without the delay.
For simplicity, I would only add it if proven needed in Go.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yes. Delay is essential.

static ssize_t send_oob(int sfd, char *buffer,
        ssize_t n, long pos, const char *c)
{
    char rchar = buffer[pos];
    buffer[pos] = c[1] ? c[0] : 'a';
    
    ssize_t len = send(sfd, buffer, pos + 1, MSG_OOB);
    buffer[pos] = rchar;
    
    if (len < 0) {
        uniperror("send");
        return -1;
    }
    wait_send_if_support(sfd);
    
    len--;
    if (len != pos) {
        return len;
    }
    return len;
}

This is a little bit complex implementation in byeDPI that works only on linux. So now it is only delay.

}

// 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
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)
}

func getSocketDescriptor(conn *net.TCPConn) (SocketDescriptor, error) {
file, err := conn.File()
if err != nil {
return 0, err
}
return SocketDescriptor(file.Fd()), nil
}
Loading