diff --git a/x/configurl/module.go b/x/configurl/module.go index 83e14b89..9ba66d8d 100644 --- a/x/configurl/module.go +++ b/x/configurl/module.go @@ -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 } diff --git a/x/configurl/oob.go b/x/configurl/oob.go new file mode 100644 index 00000000..70cb3a23 --- /dev/null +++ b/x/configurl/oob.go @@ -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:::: 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) + }) +} diff --git a/x/go.mod b/x/go.mod index b432aec4..f639df52 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.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/ diff --git a/x/go.sum b/x/go.sum index a7238e9a..096d7ae7 100644 --- a/x/go.sum +++ b/x/go.sum @@ -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= @@ -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= @@ -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= @@ -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= @@ -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= @@ -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= diff --git a/x/oob/dialer.go b/x/oob/dialer.go new file mode 100644 index 00000000..5960b9f6 --- /dev/null +++ b/x/oob/dialer.go @@ -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 +} + +// 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 +} diff --git a/x/oob/oob_test.go b/x/oob/oob_test.go new file mode 100644 index 00000000..5f02e875 --- /dev/null +++ b/x/oob/oob_test.go @@ -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)) +} diff --git a/x/oob/unix_ops.go b/x/oob/unix_ops.go new file mode 100644 index 00000000..d3fcabff --- /dev/null +++ b/x/oob/unix_ops.go @@ -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 +} diff --git a/x/oob/windows_ops.go b/x/oob/windows_ops.go new file mode 100644 index 00000000..0a9eddfe --- /dev/null +++ b/x/oob/windows_ops.go @@ -0,0 +1,37 @@ +//go:build windows + +package oob + +import ( + "fmt" + "net" + "syscall" +) + +const MSG_OOB = windows.MSG_OOB + +type SocketDescriptor uintptr + +func sendTo(fd SocketDescriptor, data []byte, flags int) (err error) { + var wsaBuf [1]syscall.WSABuf + wsaBuf[0].Len = uint32(len(data)) + wsaBuf[0].Buf = &data[0] + + bytesSent := uint32(0) + + return syscall.WSASend(syscall.Handle(fd), &wsaBuf[0], 1, &bytesSent, uint32(flags), nil, nil) +} + +func getSocketDescriptor(conn *net.TCPConn) (SocketDescriptor, error) { + rawConn, err := conn.SyscallConn() + if err != nil { + return SocketDescriptor(0), fmt.Errorf("oob strategy was unable to get raw conn: %w", err) + } + + var sysFd syscall.Handle + err = rawConn.Control(func(fd uintptr) { + sysFd = syscall.Handle(fd) + }) + + return SocketDescriptor(sysFd), err +} diff --git a/x/oob/writer.go b/x/oob/writer.go new file mode 100644 index 00000000..9d30dd06 --- /dev/null +++ b/x/oob/writer.go @@ -0,0 +1,118 @@ +package oob + +import ( + "fmt" + "io" + "net" + "sync" + "time" + + "github.com/Jigsaw-Code/outline-sdk/x/sockopt" +) + +type oobWriter struct { + conn *net.TCPConn + opts sockopt.TCPOptions + resetTTL sync.Once + setTTL sync.Once + oobPosition int64 + sd SocketDescriptor + oobByte byte // Byte to send as OOB + disOOB bool // Flag to enable disOOB mode + delay time.Duration +} + +var _ io.Writer = (*oobWriter)(nil) + +type oobWriterReaderFrom struct { + *oobWriter + rf io.ReaderFrom +} + +// NewWriter creates an [io.Writer] that sends an OOB byte at the specified "oobPosition". +// If disOOB is enabled, it will apply the --disOOB strategy. +// "oobByte" specifies the value of the byte to send out-of-band. +func NewWriter( + conn *net.TCPConn, + opts sockopt.TCPOptions, + oobPosition int64, + oobByte byte, + disOOB bool, + delay time.Duration, +) io.Writer { + return &oobWriter{conn: conn, opts: opts, oobPosition: oobPosition, oobByte: oobByte, disOOB: disOOB, delay: delay} +} + +func (w *oobWriter) Write(data []byte) (int, error) { + var written int + var err error + + if w.oobPosition > 0 && w.oobPosition < int64(len(data))-1 { + firstPart := data[:w.oobPosition+1] + secondPart := data[w.oobPosition:] + + // Split the data into two parts + tmp := secondPart[0] + secondPart[0] = w.oobByte + + var oldTTL int + if w.disOOB { + w.setTTL.Do(func() { + oldTTL, err = w.opts.HopLimit() + if err != nil { + return + } + err = w.opts.SetHopLimit(0) + }) + if err != nil { + return written, fmt.Errorf("oob: new hop limit set error: %w", err) + } + } + + err = w.send(firstPart, MSG_OOB) + if err != nil { + return written, err + } + written = int(w.oobPosition) + secondPart[0] = tmp + + if w.disOOB { + w.resetTTL.Do(func() { + err = w.opts.SetHopLimit(oldTTL) + }) + if err != nil { + return written, fmt.Errorf("oob: old hop limit set error: %w", err) + } + } + + data = secondPart + + time.Sleep(w.delay) + } + + n, err := w.conn.Write(data) + written += n + + return written, err +} + +func (w *oobWriter) send(data []byte, flags int) error { + // Use SyscallConn to access the underlying file descriptor safely + rawConn, err := w.conn.SyscallConn() + if err != nil { + return fmt.Errorf("oob strategy was unable to get raw conn: %w", err) + } + + // Use Control to execute Sendto on the file descriptor + var sendErr error + err = rawConn.Control(func(fd uintptr) { + sendErr = sendTo(SocketDescriptor(fd), data, flags) + }) + if err != nil { + return fmt.Errorf("oob strategy was unable to control socket: %w", err) + } + if sendErr != nil { + return fmt.Errorf("oob strategy was unable to send data: %w", sendErr) + } + return nil +}