-
Notifications
You must be signed in to change notification settings - Fork 55
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
base: main
Are you sure you want to change the base?
Changes from all commits
639ff72
75fb663
a50b014
aaae1e5
a25de7a
250a731
ffb58dc
5cd7504
a0e3a8c
b65ad95
9e0d954
d16f104
84b696d
2e298a8
5a3532d
c8f3af7
175eda6
8b9fff0
afde648
82bceb5
5b7b486
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 ( | ||
|
@@ -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/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Revert. For development, you can use Go workspaces instead. |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this be called just Also, can we compose with the disorder from #323 instead of reproducing the logic here? /cc @PeterZhizhin There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
The function will do the following:
After we have #324 merged, we will also have it wait for the socket send all bytes before resetting the hop limit. The 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 | ||
} |
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)) | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
There was a problem hiding this comment.
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.