Skip to content

Commit 6b086ed

Browse files
committed
Add tcp-brutal support
1 parent 90a8306 commit 6b086ed

9 files changed

+280
-61
lines changed

brutal.go

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package mux
2+
3+
import (
4+
"encoding/binary"
5+
"io"
6+
7+
"github.com/sagernet/sing/common"
8+
"github.com/sagernet/sing/common/buf"
9+
E "github.com/sagernet/sing/common/exceptions"
10+
"github.com/sagernet/sing/common/rw"
11+
)
12+
13+
const (
14+
BrutalExchangeDomain = "_BrutalBwExchange"
15+
BrutalMinSpeedBPS = 65536
16+
)
17+
18+
func WriteBrutalRequest(writer io.Writer, receiveBPS uint64) error {
19+
return binary.Write(writer, binary.BigEndian, receiveBPS)
20+
}
21+
22+
func ReadBrutalRequest(reader io.Reader) (uint64, error) {
23+
var receiveBPS uint64
24+
err := binary.Read(reader, binary.BigEndian, &receiveBPS)
25+
return receiveBPS, err
26+
}
27+
28+
func WriteBrutalResponse(writer io.Writer, receiveBPS uint64, ok bool, message string) error {
29+
buffer := buf.New()
30+
defer buffer.Release()
31+
common.Must(binary.Write(buffer, binary.BigEndian, ok))
32+
if ok {
33+
common.Must(binary.Write(buffer, binary.BigEndian, receiveBPS))
34+
} else {
35+
err := rw.WriteVString(buffer, message)
36+
if err != nil {
37+
return err
38+
}
39+
}
40+
return common.Error(writer.Write(buffer.Bytes()))
41+
}
42+
43+
func ReadBrutalResponse(reader io.Reader) (uint64, error) {
44+
var ok bool
45+
err := binary.Read(reader, binary.BigEndian, &ok)
46+
if err != nil {
47+
return 0, err
48+
}
49+
if ok {
50+
var receiveBPS uint64
51+
err = binary.Read(reader, binary.BigEndian, &receiveBPS)
52+
return receiveBPS, err
53+
} else {
54+
var message string
55+
message, err = rw.ReadVString(reader)
56+
if err != nil {
57+
return 0, err
58+
}
59+
return 0, E.New("remote error: ", message)
60+
}
61+
}

brutal_linux.go

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package mux
2+
3+
import (
4+
"net"
5+
"os"
6+
"reflect"
7+
"syscall"
8+
"unsafe"
9+
_ "unsafe"
10+
11+
"github.com/sagernet/sing/common"
12+
"github.com/sagernet/sing/common/control"
13+
E "github.com/sagernet/sing/common/exceptions"
14+
15+
"golang.org/x/sys/unix"
16+
)
17+
18+
const (
19+
BrutalAvailable = true
20+
TCP_BRUTAL_PARAMS = 23301
21+
)
22+
23+
type TCPBrutalParams struct {
24+
Rate uint64
25+
CwndGain uint32
26+
}
27+
28+
//go:linkname setsockopt syscall.setsockopt
29+
func setsockopt(s int, level int, name int, val unsafe.Pointer, vallen uintptr) (err error)
30+
31+
func SetBrutalOptions(conn net.Conn, sendBPS uint64) error {
32+
syscallConn, loaded := common.Cast[syscall.Conn](conn)
33+
if !loaded {
34+
return E.New(
35+
"brutal: nested multiplexing is not supported: ",
36+
"cannot convert ", reflect.TypeOf(conn), " to syscall.Conn, final type: ", reflect.TypeOf(common.Top(conn)),
37+
)
38+
}
39+
return control.Conn(syscallConn, func(fd uintptr) error {
40+
err := unix.SetsockoptString(int(fd), unix.IPPROTO_TCP, unix.TCP_CONGESTION, "brutal")
41+
if err != nil {
42+
return E.Extend(
43+
os.NewSyscallError("setsockopt IPPROTO_TCP TCP_CONGESTION brutal", err),
44+
"please make sure you have installed the tcp-brutal kernel module",
45+
)
46+
}
47+
params := TCPBrutalParams{
48+
Rate: sendBPS,
49+
CwndGain: 20, // hysteria2 default
50+
}
51+
err = setsockopt(int(fd), unix.IPPROTO_TCP, TCP_BRUTAL_PARAMS, unsafe.Pointer(&params), unsafe.Sizeof(params))
52+
if err != nil {
53+
return os.NewSyscallError("setsockopt IPPROTO_TCP TCP_BRUTAL_PARAMS", err)
54+
}
55+
return nil
56+
})
57+
}

brutal_stub.go

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//go:build !linux
2+
3+
package mux
4+
5+
import (
6+
"net"
7+
8+
E "github.com/sagernet/sing/common/exceptions"
9+
)
10+
11+
const BrutalAvailable = false
12+
13+
func SetBrutalOptions(conn net.Conn, sendBPS uint64) error {
14+
return E.New("TCP Brutal is only supported on Linux")
15+
}

client.go

+53
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,51 @@ import (
88
"github.com/sagernet/sing/common"
99
"github.com/sagernet/sing/common/bufio"
1010
E "github.com/sagernet/sing/common/exceptions"
11+
"github.com/sagernet/sing/common/logger"
1112
M "github.com/sagernet/sing/common/metadata"
1213
N "github.com/sagernet/sing/common/network"
1314
"github.com/sagernet/sing/common/x/list"
1415
)
1516

1617
type Client struct {
1718
dialer N.Dialer
19+
logger logger.Logger
1820
protocol byte
1921
maxConnections int
2022
minStreams int
2123
maxStreams int
2224
padding bool
2325
access sync.Mutex
2426
connections list.List[abstractSession]
27+
brutal BrutalOptions
2528
}
2629

2730
type Options struct {
2831
Dialer N.Dialer
32+
Logger logger.Logger
2933
Protocol string
3034
MaxConnections int
3135
MinStreams int
3236
MaxStreams int
3337
Padding bool
38+
Brutal BrutalOptions
39+
}
40+
41+
type BrutalOptions struct {
42+
Enabled bool
43+
SendBPS uint64
44+
ReceiveBPS uint64
3445
}
3546

3647
func NewClient(options Options) (*Client, error) {
3748
client := &Client{
3849
dialer: options.Dialer,
50+
logger: options.Logger,
3951
maxConnections: options.MaxConnections,
4052
minStreams: options.MinStreams,
4153
maxStreams: options.MaxStreams,
4254
padding: options.Padding,
55+
brutal: options.Brutal,
4356
}
4457
if client.dialer == nil {
4558
client.dialer = N.SystemDialer
@@ -126,6 +139,12 @@ func (c *Client) offer(ctx context.Context) (abstractSession, error) {
126139
sessions = append(sessions, element.Value)
127140
element = element.Next()
128141
}
142+
if c.brutal.Enabled {
143+
if len(sessions) > 0 {
144+
return sessions[0], nil
145+
}
146+
return c.offerNew(ctx)
147+
}
129148
session := common.MinBy(common.Filter(sessions, abstractSession.CanTakeNewRequest), abstractSession.NumStreams)
130149
if session == nil {
131150
return c.offerNew(ctx)
@@ -170,10 +189,44 @@ func (c *Client) offerNew(ctx context.Context) (abstractSession, error) {
170189
conn.Close()
171190
return nil, err
172191
}
192+
if c.brutal.Enabled {
193+
err = c.brutalExchange(conn, session)
194+
if err != nil {
195+
conn.Close()
196+
session.Close()
197+
return nil, E.Cause(err, "brutal exchange")
198+
}
199+
}
173200
c.connections.PushBack(session)
174201
return session, nil
175202
}
176203

204+
func (c *Client) brutalExchange(sessionConn net.Conn, session abstractSession) error {
205+
stream, err := session.Open()
206+
if err != nil {
207+
return err
208+
}
209+
conn := &clientConn{Conn: &wrapStream{stream}, destination: M.Socksaddr{Fqdn: BrutalExchangeDomain}}
210+
err = WriteBrutalRequest(conn, c.brutal.ReceiveBPS)
211+
if err != nil {
212+
return err
213+
}
214+
serverReceiveBPS, err := ReadBrutalResponse(conn)
215+
if err != nil {
216+
return err
217+
}
218+
conn.Close()
219+
sendBPS := c.brutal.SendBPS
220+
if serverReceiveBPS < sendBPS {
221+
sendBPS = serverReceiveBPS
222+
}
223+
clientBrutalErr := SetBrutalOptions(sessionConn, sendBPS)
224+
if clientBrutalErr != nil {
225+
c.logger.Debug(E.Cause(clientBrutalErr, "failed to enable TCP Brutal at client"))
226+
}
227+
return nil
228+
}
229+
177230
func (c *Client) Reset() {
178231
c.access.Lock()
179232
defer c.access.Unlock()

go.mod

+3-5
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ go 1.18
44

55
require (
66
github.com/hashicorp/yamux v0.1.1
7-
github.com/sagernet/sing v0.2.17
7+
github.com/sagernet/sing v0.2.18-0.20231108041402-4fbbd193203c
88
github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37
99
golang.org/x/net v0.18.0
10+
golang.org/x/sys v0.14.0
1011
)
1112

12-
require (
13-
golang.org/x/sys v0.14.0 // indirect
14-
golang.org/x/text v0.14.0 // indirect
15-
)
13+
require golang.org/x/text v0.14.0 // indirect

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
22
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
33
github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
4-
github.com/sagernet/sing v0.2.17 h1:vMPKb3MV0Aa5ws4dCJkRI8XEjrsUcDn810czd0FwmzI=
5-
github.com/sagernet/sing v0.2.17/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
4+
github.com/sagernet/sing v0.2.18-0.20231108041402-4fbbd193203c h1:uask61Pxc3nGqsOSjqnBKrwfODWRoEa80lXm04LNk0E=
5+
github.com/sagernet/sing v0.2.18-0.20231108041402-4fbbd193203c/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
66
github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 h1:HuE6xSwco/Xed8ajZ+coeYLmioq0Qp1/Z2zczFaV8as=
77
github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37/go.mod h1:3skNSftZDJWTGVtVaM2jfbce8qHnmH/AGDRe62iNOg0=
88
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=

padding.go

+4
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@ func (c *paddingConn) FrontHeadroom() int {
201201
return 4 + 256 + 1024
202202
}
203203

204+
func (c *paddingConn) Upstream() any {
205+
return c.ExtendedConn
206+
}
207+
204208
type vectorisedPaddingConn struct {
205209
paddingConn
206210
writer N.VectorisedWriter

0 commit comments

Comments
 (0)