From 03c8b5e6b91f956636f9995dfe4261f00362547f Mon Sep 17 00:00:00 2001 From: HystericalDragon Date: Sun, 31 Mar 2024 10:58:34 +0800 Subject: [PATCH 01/11] feat: support Android protect path about: https://github.com/apernet/hysteria/commit/1ac9d4956bd16bbd59157da8e4532b2db6104ef9 Signed-off-by: HystericalDragon --- app/cmd/client.go | 6 ++- extras/go.mod | 2 +- extras/protect/protect.go | 9 +++++ extras/protect/protect_linux.go | 72 +++++++++++++++++++++++++++++++++ extras/protect/protect_stub.go | 13 ++++++ extras/transport/udphop/conn.go | 8 ++-- 6 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 extras/protect/protect.go create mode 100644 extras/protect/protect_linux.go create mode 100644 extras/protect/protect_stub.go diff --git a/app/cmd/client.go b/app/cmd/client.go index c1d04bd786..78071a51e9 100644 --- a/app/cmd/client.go +++ b/app/cmd/client.go @@ -30,6 +30,7 @@ import ( "github.com/apernet/hysteria/core/client" "github.com/apernet/hysteria/extras/correctnet" "github.com/apernet/hysteria/extras/obfs" + "github.com/apernet/hysteria/extras/protect" "github.com/apernet/hysteria/extras/transport/udphop" ) @@ -55,6 +56,7 @@ func initClientFlags() { type clientConfig struct { Server string `mapstructure:"server"` + ProtectPath string `mapstructure:"protectPath"` Auth string `mapstructure:"auth"` Transport clientConfigTransport `mapstructure:"transport"` Obfs clientConfigObfs `mapstructure:"obfs"` @@ -202,11 +204,11 @@ func (c *clientConfig) fillConnFactory(hyConfig *client.Config) error { if hyConfig.ServerAddr.Network() == "udphop" { hopAddr := hyConfig.ServerAddr.(*udphop.UDPHopAddr) newFunc = func(addr net.Addr) (net.PacketConn, error) { - return udphop.NewUDPHopPacketConn(hopAddr, c.Transport.UDP.HopInterval, nil) + return udphop.NewUDPHopPacketConn(hopAddr, c.Transport.UDP.HopInterval, protect.ListenUDP(c.ProtectPath)) } } else { newFunc = func(addr net.Addr) (net.PacketConn, error) { - return net.ListenUDP("udp", nil) + return protect.ListenUDP(c.ProtectPath)() } } default: diff --git a/extras/go.mod b/extras/go.mod index bc21044510..a470d1e801 100644 --- a/extras/go.mod +++ b/extras/go.mod @@ -11,6 +11,7 @@ require ( github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 golang.org/x/crypto v0.19.0 golang.org/x/net v0.21.0 + golang.org/x/sys v0.17.0 google.golang.org/protobuf v1.33.0 ) @@ -28,7 +29,6 @@ require ( go.uber.org/mock v0.4.0 // indirect golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect golang.org/x/mod v0.12.0 // indirect - golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.11.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/extras/protect/protect.go b/extras/protect/protect.go new file mode 100644 index 0000000000..1b5cd086b7 --- /dev/null +++ b/extras/protect/protect.go @@ -0,0 +1,9 @@ +// Package protect set VPN protect for every conns to bypass route. +package protect + +import ( + "net" +) + +// ListenUDPFunc listen UDP with VPN protect. +type ListenUDPFunc func() (net.PacketConn, error) diff --git a/extras/protect/protect_linux.go b/extras/protect/protect_linux.go new file mode 100644 index 0000000000..0fffe137a7 --- /dev/null +++ b/extras/protect/protect_linux.go @@ -0,0 +1,72 @@ +//go:build linux + +package protect + +import ( + "errors" + "net" + + "golang.org/x/sys/unix" +) + +const ( + timevalSec = 3 +) + +func protect(connFd int, path string) error { + if path == "" { + return nil + } + + socketFd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM, unix.PROT_NONE) + if err != nil { + return err + } + defer unix.Close(socketFd) + + _ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Sec: timevalSec}) + _ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_SNDTIMEO, &unix.Timeval{Sec: timevalSec}) + + err = unix.Connect(socketFd, &unix.SockaddrUnix{Name: path}) + if err != nil { + return err + } + + err = unix.Sendmsg(socketFd, nil, unix.UnixRights(connFd), nil, 0) + if err != nil { + return err + } + + dummy := []byte{1} + n, err := unix.Read(socketFd, dummy) + if err != nil { + return err + } + if n != 1 { + return errors.New("protect failed") + } + + return nil +} + +func ListenUDP(protectPath string) ListenUDPFunc { + return func() (net.PacketConn, error) { + udpConn, err := net.ListenUDP("udp", nil) + if err != nil { + return nil, err + } + + udpFile, err := udpConn.File() + if err != nil { + return nil, err + } + + err = protect(int(udpFile.Fd()), protectPath) + if err != nil { + _ = udpConn.Close() + return nil, err + } + + return udpConn, nil + } +} diff --git a/extras/protect/protect_stub.go b/extras/protect/protect_stub.go new file mode 100644 index 0000000000..a8da279948 --- /dev/null +++ b/extras/protect/protect_stub.go @@ -0,0 +1,13 @@ +//go:build !linux + +package protect + +import ( + "net" +) + +func ListenUDP(protectPath string) ListenUDPFunc { + return func() (net.PacketConn, error) { + return net.ListenUDP("udp", nil) + } +} diff --git a/extras/transport/udphop/conn.go b/extras/transport/udphop/conn.go index f20c583882..722aa1cd59 100644 --- a/extras/transport/udphop/conn.go +++ b/extras/transport/udphop/conn.go @@ -7,6 +7,8 @@ import ( "sync" "syscall" "time" + + "github.com/apernet/hysteria/extras/protect" ) const ( @@ -20,7 +22,7 @@ type udpHopPacketConn struct { Addr net.Addr Addrs []net.Addr HopInterval time.Duration - ListenUDPFunc ListenUDPFunc + ListenUDPFunc protect.ListenUDPFunc connMutex sync.RWMutex prevConn net.PacketConn @@ -44,9 +46,7 @@ type udpPacket struct { Err error } -type ListenUDPFunc func() (net.PacketConn, error) - -func NewUDPHopPacketConn(addr *UDPHopAddr, hopInterval time.Duration, listenUDPFunc ListenUDPFunc) (net.PacketConn, error) { +func NewUDPHopPacketConn(addr *UDPHopAddr, hopInterval time.Duration, listenUDPFunc protect.ListenUDPFunc) (net.PacketConn, error) { if hopInterval == 0 { hopInterval = defaultHopInterval } else if hopInterval < 5*time.Second { From a05383c2a186054ca160203cac6e24db9decf468 Mon Sep 17 00:00:00 2001 From: HystericalDragon Date: Wed, 3 Apr 2024 18:40:25 +0800 Subject: [PATCH 02/11] fix: use reflect to get fd conn.File() not returns real file. Signed-off-by: HystericalDragon --- extras/protect/protect_linux.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/extras/protect/protect_linux.go b/extras/protect/protect_linux.go index 0fffe137a7..003eb13c83 100644 --- a/extras/protect/protect_linux.go +++ b/extras/protect/protect_linux.go @@ -5,6 +5,7 @@ package protect import ( "errors" "net" + "reflect" "golang.org/x/sys/unix" ) @@ -13,6 +14,7 @@ const ( timevalSec = 3 ) +// protect try to connect with path by unix socket, then send the conn's fd to it. func protect(connFd int, path string) error { if path == "" { return nil @@ -56,12 +58,7 @@ func ListenUDP(protectPath string) ListenUDPFunc { return nil, err } - udpFile, err := udpConn.File() - if err != nil { - return nil, err - } - - err = protect(int(udpFile.Fd()), protectPath) + err = protect(fdFromConn(udpConn), protectPath) if err != nil { _ = udpConn.Close() return nil, err @@ -70,3 +67,12 @@ func ListenUDP(protectPath string) ListenUDPFunc { return udpConn, nil } } + +// fdFromConn get net.Conn's file descriptor. +func fdFromConn(conn net.Conn) int { + v := reflect.ValueOf(conn) + netFD := reflect.Indirect(reflect.Indirect(v).FieldByName("fd")) + pfd := reflect.Indirect(netFD.FieldByName("pfd")) + fd := int(pfd.FieldByName("Sysfd").Int()) + return fd +} From 3e34da1aa87f8409307e2ade59f85826f4236849 Mon Sep 17 00:00:00 2001 From: Haruue Date: Fri, 5 Apr 2024 02:20:45 +0800 Subject: [PATCH 03/11] refactor: protect => quic.sockopts Android's VpnService.protect() itself is confusing, so we rename the "protect" feature with the name `fdControlUnixSocket` and make it a sub-option under `quic.sockopts`. A unit test is added to make sure the protect feature works. I also added two other common options to `quic.sockopts` that I copied from my other projects but did not fully test here. --- app/cmd/client.go | 43 ++++++--- .../sockopts/fd_control_unix_socket_test.py | 65 +++++++++++++ app/internal/sockopts/sockopts.go | 76 ++++++++++++++++ app/internal/sockopts/sockopts_linux.go | 91 +++++++++++++++++++ app/internal/sockopts/sockopts_linux_test.go | 53 +++++++++++ extras/protect/protect.go | 9 -- extras/protect/protect_linux.go | 78 ---------------- extras/protect/protect_stub.go | 13 --- extras/transport/udphop/conn.go | 8 +- 9 files changed, 321 insertions(+), 115 deletions(-) create mode 100644 app/internal/sockopts/fd_control_unix_socket_test.py create mode 100644 app/internal/sockopts/sockopts.go create mode 100644 app/internal/sockopts/sockopts_linux.go create mode 100644 app/internal/sockopts/sockopts_linux_test.go delete mode 100644 extras/protect/protect.go delete mode 100644 extras/protect/protect_linux.go delete mode 100644 extras/protect/protect_stub.go diff --git a/app/cmd/client.go b/app/cmd/client.go index 78071a51e9..c230140866 100644 --- a/app/cmd/client.go +++ b/app/cmd/client.go @@ -22,6 +22,7 @@ import ( "github.com/apernet/hysteria/app/internal/forwarding" "github.com/apernet/hysteria/app/internal/http" "github.com/apernet/hysteria/app/internal/redirect" + "github.com/apernet/hysteria/app/internal/sockopts" "github.com/apernet/hysteria/app/internal/socks5" "github.com/apernet/hysteria/app/internal/tproxy" "github.com/apernet/hysteria/app/internal/tun" @@ -30,7 +31,6 @@ import ( "github.com/apernet/hysteria/core/client" "github.com/apernet/hysteria/extras/correctnet" "github.com/apernet/hysteria/extras/obfs" - "github.com/apernet/hysteria/extras/protect" "github.com/apernet/hysteria/extras/transport/udphop" ) @@ -56,7 +56,6 @@ func initClientFlags() { type clientConfig struct { Server string `mapstructure:"server"` - ProtectPath string `mapstructure:"protectPath"` Auth string `mapstructure:"auth"` Transport clientConfigTransport `mapstructure:"transport"` Obfs clientConfigObfs `mapstructure:"obfs"` @@ -101,13 +100,20 @@ type clientConfigTLS struct { } type clientConfigQUIC struct { - InitStreamReceiveWindow uint64 `mapstructure:"initStreamReceiveWindow"` - MaxStreamReceiveWindow uint64 `mapstructure:"maxStreamReceiveWindow"` - InitConnectionReceiveWindow uint64 `mapstructure:"initConnReceiveWindow"` - MaxConnectionReceiveWindow uint64 `mapstructure:"maxConnReceiveWindow"` - MaxIdleTimeout time.Duration `mapstructure:"maxIdleTimeout"` - KeepAlivePeriod time.Duration `mapstructure:"keepAlivePeriod"` - DisablePathMTUDiscovery bool `mapstructure:"disablePathMTUDiscovery"` + InitStreamReceiveWindow uint64 `mapstructure:"initStreamReceiveWindow"` + MaxStreamReceiveWindow uint64 `mapstructure:"maxStreamReceiveWindow"` + InitConnectionReceiveWindow uint64 `mapstructure:"initConnReceiveWindow"` + MaxConnectionReceiveWindow uint64 `mapstructure:"maxConnReceiveWindow"` + MaxIdleTimeout time.Duration `mapstructure:"maxIdleTimeout"` + KeepAlivePeriod time.Duration `mapstructure:"keepAlivePeriod"` + DisablePathMTUDiscovery bool `mapstructure:"disablePathMTUDiscovery"` + Sockopts clientConfigQUICSockopts `mapstructure:"sockopts"` +} + +type clientConfigQUICSockopts struct { + BindInterface *string `mapstructure:"bindInterface"` + FirewallMark *uint32 `mapstructure:"fwmark"` + FdControlUnixSocket *string `mapstructure:"fdControlUnixSocket"` } type clientConfigBandwidth struct { @@ -197,6 +203,21 @@ func (c *clientConfig) fillServerAddr(hyConfig *client.Config) error { // fillConnFactory must be called after fillServerAddr, as we have different logic // for ConnFactory depending on whether we have a port hopping address. func (c *clientConfig) fillConnFactory(hyConfig *client.Config) error { + so := &sockopts.SocketOptions{ + BindInterface: c.QUIC.Sockopts.BindInterface, + FirewallMark: c.QUIC.Sockopts.FirewallMark, + FdControlUnixSocket: c.QUIC.Sockopts.FdControlUnixSocket, + } + if err := so.CheckSupported(); err != nil { + var unsupportedErr *sockopts.UnsupportedError + if errors.As(err, &unsupportedErr) { + return configError{ + Field: "quic.sockopts." + unsupportedErr.Field, + Err: errors.New("unsupported on this platform"), + } + } + return configError{Field: "quic.sockopts", Err: err} + } // Inner PacketConn var newFunc func(addr net.Addr) (net.PacketConn, error) switch strings.ToLower(c.Transport.Type) { @@ -204,11 +225,11 @@ func (c *clientConfig) fillConnFactory(hyConfig *client.Config) error { if hyConfig.ServerAddr.Network() == "udphop" { hopAddr := hyConfig.ServerAddr.(*udphop.UDPHopAddr) newFunc = func(addr net.Addr) (net.PacketConn, error) { - return udphop.NewUDPHopPacketConn(hopAddr, c.Transport.UDP.HopInterval, protect.ListenUDP(c.ProtectPath)) + return udphop.NewUDPHopPacketConn(hopAddr, c.Transport.UDP.HopInterval, so.ListenUDP) } } else { newFunc = func(addr net.Addr) (net.PacketConn, error) { - return protect.ListenUDP(c.ProtectPath)() + return so.ListenUDP() } } default: diff --git a/app/internal/sockopts/fd_control_unix_socket_test.py b/app/internal/sockopts/fd_control_unix_socket_test.py new file mode 100644 index 0000000000..e47a6f64a1 --- /dev/null +++ b/app/internal/sockopts/fd_control_unix_socket_test.py @@ -0,0 +1,65 @@ +import socket +import array +import os +import struct +import sys + + +def serve(path): + try: + os.unlink(path) + except OSError: + if os.path.exists(path): + raise + + server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + server.bind(path) + server.listen() + print(f"Listening on {path}") + + try: + while True: + connection, client_address = server.accept() + print(f"Client connected") + + try: + # Receiving fd from client + fds = array.array("i") + msg, ancdata, flags, addr = connection.recvmsg(1, socket.CMSG_LEN(struct.calcsize('i'))) + for cmsg_level, cmsg_type, cmsg_data in ancdata: + if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS: + fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)]) + + fd = fds[0] + + # We make a call to setsockopt(2) here, so client can verify we have received the fd + # In the real scenario, the server would set things like SO_MARK, + # we use SO_RCVBUF as it doesn't require any special capabilities. + nbytes = struct.pack("i", 2500) + fdsocket = fd_to_socket(fd) + fdsocket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, nbytes) + fdsocket.close() + + # The only protocol-like thing specified in the client implementation. + connection.send(b'\x01') + finally: + connection.close() + print("Connection closed") + + except KeyboardInterrupt: + print("Exit") + + finally: + server.close() + os.unlink(path) + + +def fd_to_socket(fd): + return socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM) + + +if __name__ == "__main__": + if len(sys.argv) < 2: + raise ValueError("unix socket path is required") + + serve(sys.argv[1]) diff --git a/app/internal/sockopts/sockopts.go b/app/internal/sockopts/sockopts.go new file mode 100644 index 0000000000..9c08922048 --- /dev/null +++ b/app/internal/sockopts/sockopts.go @@ -0,0 +1,76 @@ +package sockopts + +import ( + "fmt" + "net" +) + +type SocketOptions struct { + BindInterface *string + FirewallMark *uint32 + FdControlUnixSocket *string +} + +// implemented in platform-specific files +var ( + bindInterfaceFunc func(c *net.UDPConn, device string) error + firewallMarkFunc func(c *net.UDPConn, fwmark uint32) error + fdControlUnixSocketFunc func(c *net.UDPConn, path string) error +) + +func (o *SocketOptions) CheckSupported() (err error) { + if o.BindInterface != nil && bindInterfaceFunc == nil { + return &UnsupportedError{"bindInterface"} + } + if o.FirewallMark != nil && firewallMarkFunc == nil { + return &UnsupportedError{"fwmark"} + } + if o.FdControlUnixSocket != nil && fdControlUnixSocketFunc == nil { + return &UnsupportedError{"fdControlUnixSocket"} + } + return nil +} + +type UnsupportedError struct { + Field string +} + +func (e *UnsupportedError) Error() string { + return fmt.Sprintf("%s is not supported on this platform", e.Field) +} + +func (o *SocketOptions) ListenUDP() (uconn net.PacketConn, err error) { + uconn, err = net.ListenUDP("udp", nil) + if err != nil { + return + } + err = o.applyToUDPConn(uconn.(*net.UDPConn)) + if err != nil { + uconn.Close() + uconn = nil + return + } + return +} + +func (o *SocketOptions) applyToUDPConn(c *net.UDPConn) (err error) { + if o.BindInterface != nil && bindInterfaceFunc != nil { + err = bindInterfaceFunc(c, *o.BindInterface) + if err != nil { + err = fmt.Errorf("failed to bind to interface: %w", err) + } + } + if o.FirewallMark != nil && firewallMarkFunc != nil { + err = firewallMarkFunc(c, *o.FirewallMark) + if err != nil { + err = fmt.Errorf("failed to set fwmark: %w", err) + } + } + if o.FdControlUnixSocket != nil && fdControlUnixSocketFunc != nil { + err = fdControlUnixSocketFunc(c, *o.FdControlUnixSocket) + if err != nil { + err = fmt.Errorf("failed to send fd to control unix socket: %w", err) + } + } + return +} diff --git a/app/internal/sockopts/sockopts_linux.go b/app/internal/sockopts/sockopts_linux.go new file mode 100644 index 0000000000..f4d8621d39 --- /dev/null +++ b/app/internal/sockopts/sockopts_linux.go @@ -0,0 +1,91 @@ +//go:build linux + +package sockopts + +import ( + "fmt" + "net" + "time" + + "golang.org/x/sys/unix" +) + +const ( + fdControlUnixTimeout = 3 * time.Second +) + +func init() { + bindInterfaceFunc = bindInterfaceImpl + firewallMarkFunc = firewallMarkImpl + fdControlUnixSocketFunc = fdControlUnixSocketImpl +} + +func controlUDPConn(c *net.UDPConn, cb func(fd int) error) (err error) { + rconn, err := c.SyscallConn() + if err != nil { + return + } + cerr := rconn.Control(func(fd uintptr) { + err = cb(int(fd)) + }) + if err != nil { + return + } + if cerr != nil { + err = fmt.Errorf("failed to control fd: %w", cerr) + return + } + return +} + +func bindInterfaceImpl(c *net.UDPConn, device string) error { + return controlUDPConn(c, func(fd int) error { + return unix.BindToDevice(fd, device) + }) +} + +func firewallMarkImpl(c *net.UDPConn, fwmark uint32) error { + return controlUDPConn(c, func(fd int) error { + return unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_MARK, int(fwmark)) + }) +} + +func fdControlUnixSocketImpl(c *net.UDPConn, path string) error { + return controlUDPConn(c, func(fd int) error { + socketFd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM, unix.PROT_NONE) + if err != nil { + return fmt.Errorf("failed to create unix socket: %w", err) + } + defer unix.Close(socketFd) + + timeoutUsec := fdControlUnixTimeout.Microseconds() + timeout := unix.Timeval{ + Sec: timeoutUsec / 1e6, + Usec: timeoutUsec % 1e6, + } + + _ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &timeout) + _ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_SNDTIMEO, &timeout) + + err = unix.Connect(socketFd, &unix.SockaddrUnix{Name: path}) + if err != nil { + return fmt.Errorf("failed to connect: %w", err) + } + + err = unix.Sendmsg(socketFd, nil, unix.UnixRights(fd), nil, 0) + if err != nil { + return fmt.Errorf("failed to send: %w", err) + } + + dummy := []byte{1} + n, err := unix.Read(socketFd, dummy) + if err != nil { + return fmt.Errorf("failed to receive: %w", err) + } + if n != 1 { + return fmt.Errorf("socket closed unexpectedly") + } + + return nil + }) +} diff --git a/app/internal/sockopts/sockopts_linux_test.go b/app/internal/sockopts/sockopts_linux_test.go new file mode 100644 index 0000000000..66614a4c7a --- /dev/null +++ b/app/internal/sockopts/sockopts_linux_test.go @@ -0,0 +1,53 @@ +//go:build linux + +package sockopts + +import ( + "net" + "os" + "os/exec" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "golang.org/x/sys/unix" +) + +func Test_fdControlUnixSocketImpl(t *testing.T) { + sockPath := "./fd_control_unix_socket_test.sock" + defer os.Remove(sockPath) + + // Run test server + cmd := exec.Command("python", "fd_control_unix_socket_test.py", sockPath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Start() + if !assert.NoError(t, err) { + return + } + defer cmd.Process.Kill() + + // Wait for the server to start + time.Sleep(1 * time.Second) + + so := SocketOptions{ + FdControlUnixSocket: &sockPath, + } + conn, err := so.ListenUDP() + if !assert.NoError(t, err) { + return + } + defer conn.Close() + + err = controlUDPConn(conn.(*net.UDPConn), func(fd int) (err error) { + rcvbuf, err := unix.GetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_RCVBUF) + if err != nil { + return + } + // The test server called setsockopt(fd, SOL_SOCKET, SO_RCVBUF, 2500), + // and kernel will double this value for getsockopt(). + assert.Equal(t, 5000, rcvbuf) + return + }) + assert.NoError(t, err) +} diff --git a/extras/protect/protect.go b/extras/protect/protect.go deleted file mode 100644 index 1b5cd086b7..0000000000 --- a/extras/protect/protect.go +++ /dev/null @@ -1,9 +0,0 @@ -// Package protect set VPN protect for every conns to bypass route. -package protect - -import ( - "net" -) - -// ListenUDPFunc listen UDP with VPN protect. -type ListenUDPFunc func() (net.PacketConn, error) diff --git a/extras/protect/protect_linux.go b/extras/protect/protect_linux.go deleted file mode 100644 index 003eb13c83..0000000000 --- a/extras/protect/protect_linux.go +++ /dev/null @@ -1,78 +0,0 @@ -//go:build linux - -package protect - -import ( - "errors" - "net" - "reflect" - - "golang.org/x/sys/unix" -) - -const ( - timevalSec = 3 -) - -// protect try to connect with path by unix socket, then send the conn's fd to it. -func protect(connFd int, path string) error { - if path == "" { - return nil - } - - socketFd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM, unix.PROT_NONE) - if err != nil { - return err - } - defer unix.Close(socketFd) - - _ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Sec: timevalSec}) - _ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_SNDTIMEO, &unix.Timeval{Sec: timevalSec}) - - err = unix.Connect(socketFd, &unix.SockaddrUnix{Name: path}) - if err != nil { - return err - } - - err = unix.Sendmsg(socketFd, nil, unix.UnixRights(connFd), nil, 0) - if err != nil { - return err - } - - dummy := []byte{1} - n, err := unix.Read(socketFd, dummy) - if err != nil { - return err - } - if n != 1 { - return errors.New("protect failed") - } - - return nil -} - -func ListenUDP(protectPath string) ListenUDPFunc { - return func() (net.PacketConn, error) { - udpConn, err := net.ListenUDP("udp", nil) - if err != nil { - return nil, err - } - - err = protect(fdFromConn(udpConn), protectPath) - if err != nil { - _ = udpConn.Close() - return nil, err - } - - return udpConn, nil - } -} - -// fdFromConn get net.Conn's file descriptor. -func fdFromConn(conn net.Conn) int { - v := reflect.ValueOf(conn) - netFD := reflect.Indirect(reflect.Indirect(v).FieldByName("fd")) - pfd := reflect.Indirect(netFD.FieldByName("pfd")) - fd := int(pfd.FieldByName("Sysfd").Int()) - return fd -} diff --git a/extras/protect/protect_stub.go b/extras/protect/protect_stub.go deleted file mode 100644 index a8da279948..0000000000 --- a/extras/protect/protect_stub.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build !linux - -package protect - -import ( - "net" -) - -func ListenUDP(protectPath string) ListenUDPFunc { - return func() (net.PacketConn, error) { - return net.ListenUDP("udp", nil) - } -} diff --git a/extras/transport/udphop/conn.go b/extras/transport/udphop/conn.go index 722aa1cd59..32cc31c068 100644 --- a/extras/transport/udphop/conn.go +++ b/extras/transport/udphop/conn.go @@ -7,8 +7,6 @@ import ( "sync" "syscall" "time" - - "github.com/apernet/hysteria/extras/protect" ) const ( @@ -22,7 +20,7 @@ type udpHopPacketConn struct { Addr net.Addr Addrs []net.Addr HopInterval time.Duration - ListenUDPFunc protect.ListenUDPFunc + ListenUDPFunc ListenUDPFunc connMutex sync.RWMutex prevConn net.PacketConn @@ -46,7 +44,9 @@ type udpPacket struct { Err error } -func NewUDPHopPacketConn(addr *UDPHopAddr, hopInterval time.Duration, listenUDPFunc protect.ListenUDPFunc) (net.PacketConn, error) { +type ListenUDPFunc = func() (net.PacketConn, error) + +func NewUDPHopPacketConn(addr *UDPHopAddr, hopInterval time.Duration, listenUDPFunc ListenUDPFunc) (net.PacketConn, error) { if hopInterval == 0 { hopInterval = defaultHopInterval } else if hopInterval < 5*time.Second { From 65f5e9caa535fc9022f8673d1ef13e542a2da622 Mon Sep 17 00:00:00 2001 From: Haruue Date: Fri, 5 Apr 2024 02:30:52 +0800 Subject: [PATCH 04/11] chore: go mod tidy --- app/go.mod | 2 +- extras/go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/go.mod b/app/go.mod index a5025c1de3..7aea8431cb 100644 --- a/app/go.mod +++ b/app/go.mod @@ -16,6 +16,7 @@ require ( github.com/stretchr/testify v1.8.4 github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 go.uber.org/zap v1.24.0 + golang.org/x/sys v0.17.0 ) require ( @@ -57,7 +58,6 @@ require ( golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/net v0.21.0 // indirect - golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.11.1 // indirect google.golang.org/protobuf v1.33.0 // indirect diff --git a/extras/go.mod b/extras/go.mod index a470d1e801..bc21044510 100644 --- a/extras/go.mod +++ b/extras/go.mod @@ -11,7 +11,6 @@ require ( github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 golang.org/x/crypto v0.19.0 golang.org/x/net v0.21.0 - golang.org/x/sys v0.17.0 google.golang.org/protobuf v1.33.0 ) @@ -29,6 +28,7 @@ require ( go.uber.org/mock v0.4.0 // indirect golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect golang.org/x/mod v0.12.0 // indirect + golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.11.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect From 13586df2bae9335652d73a3a476fc4281a7691ab Mon Sep 17 00:00:00 2001 From: HystericalDragon Date: Fri, 5 Apr 2024 08:36:04 +0800 Subject: [PATCH 05/11] fix: invalid const usage Signed-off-by: HystericalDragon --- app/internal/sockopts/sockopts_linux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/internal/sockopts/sockopts_linux.go b/app/internal/sockopts/sockopts_linux.go index f4d8621d39..19777b5aa0 100644 --- a/app/internal/sockopts/sockopts_linux.go +++ b/app/internal/sockopts/sockopts_linux.go @@ -52,7 +52,7 @@ func firewallMarkImpl(c *net.UDPConn, fwmark uint32) error { func fdControlUnixSocketImpl(c *net.UDPConn, path string) error { return controlUDPConn(c, func(fd int) error { - socketFd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM, unix.PROT_NONE) + socketFd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM, 0) if err != nil { return fmt.Errorf("failed to create unix socket: %w", err) } From 9520d840942d3950ed43af281935c262fac35615 Mon Sep 17 00:00:00 2001 From: HystericalDragon Date: Fri, 5 Apr 2024 09:11:37 +0800 Subject: [PATCH 06/11] fix: timeval in different arch Signed-off-by: HystericalDragon --- app/internal/sockopts/sockopts_linux.go | 6 +----- app/internal/sockopts/timeval_linux_32.go | 15 +++++++++++++++ app/internal/sockopts/timeval_linux_64.go | 15 +++++++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 app/internal/sockopts/timeval_linux_32.go create mode 100644 app/internal/sockopts/timeval_linux_64.go diff --git a/app/internal/sockopts/sockopts_linux.go b/app/internal/sockopts/sockopts_linux.go index 19777b5aa0..e51e75ae5f 100644 --- a/app/internal/sockopts/sockopts_linux.go +++ b/app/internal/sockopts/sockopts_linux.go @@ -58,11 +58,7 @@ func fdControlUnixSocketImpl(c *net.UDPConn, path string) error { } defer unix.Close(socketFd) - timeoutUsec := fdControlUnixTimeout.Microseconds() - timeout := unix.Timeval{ - Sec: timeoutUsec / 1e6, - Usec: timeoutUsec % 1e6, - } + timeout := unixTimeval() _ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &timeout) _ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_SNDTIMEO, &timeout) diff --git a/app/internal/sockopts/timeval_linux_32.go b/app/internal/sockopts/timeval_linux_32.go new file mode 100644 index 0000000000..af8c262975 --- /dev/null +++ b/app/internal/sockopts/timeval_linux_32.go @@ -0,0 +1,15 @@ +//go:build linux && (386 || arm || mips || mipsle || ppc) + +package sockopts + +import ( + "golang.org/x/sys/unix" +) + +func unixTimeval() unix.Timeval { + timeUsec := fdControlUnixTimeout.Microseconds() + return unix.Timeval{ + Sec: int32(timeUsec / 1e6), + Usec: int32(timeUsec % 1e6), + } +} diff --git a/app/internal/sockopts/timeval_linux_64.go b/app/internal/sockopts/timeval_linux_64.go new file mode 100644 index 0000000000..407438c8fd --- /dev/null +++ b/app/internal/sockopts/timeval_linux_64.go @@ -0,0 +1,15 @@ +//go:build linux && (amd64 || arm64 || loong64 || mips64 || mips64le || ppc64 || ppc64le || riscv64 || s390x || sparc64) + +package sockopts + +import ( + "golang.org/x/sys/unix" +) + +func unixTimeval() unix.Timeval { + timeUsec := fdControlUnixTimeout.Microseconds() + return unix.Timeval{ + Sec: timeUsec / 1e6, + Usec: timeUsec % 1e6, + } +} From e1d7ce4640c4c897c19398c4c1d7522db35db34d Mon Sep 17 00:00:00 2001 From: Haruue Date: Fri, 5 Apr 2024 10:49:03 +0800 Subject: [PATCH 07/11] chore: a better fix for 32-bit unix.Timeval Why there is no decltype() in Golang? At least we got generics now. ref: 9520d840942d3950ed43af281935c262fac35615 --- app/internal/sockopts/sockopts_linux.go | 12 +++++++++++- app/internal/sockopts/timeval_linux_32.go | 15 --------------- app/internal/sockopts/timeval_linux_64.go | 15 --------------- 3 files changed, 11 insertions(+), 31 deletions(-) delete mode 100644 app/internal/sockopts/timeval_linux_32.go delete mode 100644 app/internal/sockopts/timeval_linux_64.go diff --git a/app/internal/sockopts/sockopts_linux.go b/app/internal/sockopts/sockopts_linux.go index e51e75ae5f..ce188d96c5 100644 --- a/app/internal/sockopts/sockopts_linux.go +++ b/app/internal/sockopts/sockopts_linux.go @@ -7,6 +7,8 @@ import ( "net" "time" + "golang.org/x/exp/constraints" + "golang.org/x/sys/unix" ) @@ -58,7 +60,11 @@ func fdControlUnixSocketImpl(c *net.UDPConn, path string) error { } defer unix.Close(socketFd) - timeout := unixTimeval() + var timeout unix.Timeval + timeUsec := fdControlUnixTimeout.Microseconds() + castAssignInteger(timeUsec/1e6, &timeout.Sec) + // Specifying the type explicitly is not necessary here, but it makes GoLand happy. + castAssignInteger[int64](timeUsec%1e6, &timeout.Usec) _ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &timeout) _ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_SNDTIMEO, &timeout) @@ -85,3 +91,7 @@ func fdControlUnixSocketImpl(c *net.UDPConn, path string) error { return nil }) } + +func castAssignInteger[F, T constraints.Integer](from F, to *T) { + *to = T(from) +} diff --git a/app/internal/sockopts/timeval_linux_32.go b/app/internal/sockopts/timeval_linux_32.go deleted file mode 100644 index af8c262975..0000000000 --- a/app/internal/sockopts/timeval_linux_32.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build linux && (386 || arm || mips || mipsle || ppc) - -package sockopts - -import ( - "golang.org/x/sys/unix" -) - -func unixTimeval() unix.Timeval { - timeUsec := fdControlUnixTimeout.Microseconds() - return unix.Timeval{ - Sec: int32(timeUsec / 1e6), - Usec: int32(timeUsec % 1e6), - } -} diff --git a/app/internal/sockopts/timeval_linux_64.go b/app/internal/sockopts/timeval_linux_64.go deleted file mode 100644 index 407438c8fd..0000000000 --- a/app/internal/sockopts/timeval_linux_64.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build linux && (amd64 || arm64 || loong64 || mips64 || mips64le || ppc64 || ppc64le || riscv64 || s390x || sparc64) - -package sockopts - -import ( - "golang.org/x/sys/unix" -) - -func unixTimeval() unix.Timeval { - timeUsec := fdControlUnixTimeout.Microseconds() - return unix.Timeval{ - Sec: timeUsec / 1e6, - Usec: timeUsec % 1e6, - } -} From 297d64e48f99501d2e3f7dd4f4cec30213ae25cc Mon Sep 17 00:00:00 2001 From: HystericalDragon Date: Fri, 5 Apr 2024 11:26:22 +0800 Subject: [PATCH 08/11] chore: format code --- app/go.mod | 2 +- app/internal/sockopts/sockopts_linux.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/go.mod b/app/go.mod index 7aea8431cb..97c5bbb4c6 100644 --- a/app/go.mod +++ b/app/go.mod @@ -16,6 +16,7 @@ require ( github.com/stretchr/testify v1.8.4 github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 go.uber.org/zap v1.24.0 + golang.org/x/exp v0.0.0-20221205204356-47842c84f3db golang.org/x/sys v0.17.0 ) @@ -55,7 +56,6 @@ require ( go.uber.org/multierr v1.11.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect golang.org/x/crypto v0.19.0 // indirect - golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/app/internal/sockopts/sockopts_linux.go b/app/internal/sockopts/sockopts_linux.go index ce188d96c5..d1e5d23c88 100644 --- a/app/internal/sockopts/sockopts_linux.go +++ b/app/internal/sockopts/sockopts_linux.go @@ -8,7 +8,6 @@ import ( "time" "golang.org/x/exp/constraints" - "golang.org/x/sys/unix" ) From 5bebfd5732c914e24a4e36febdd3c4baaee8297f Mon Sep 17 00:00:00 2001 From: Haruue Date: Fri, 5 Apr 2024 13:17:21 +0800 Subject: [PATCH 09/11] fix(sockopts): error handling in applyToUDPConn --- app/internal/sockopts/sockopts.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/internal/sockopts/sockopts.go b/app/internal/sockopts/sockopts.go index 9c08922048..e068f98a38 100644 --- a/app/internal/sockopts/sockopts.go +++ b/app/internal/sockopts/sockopts.go @@ -58,18 +58,21 @@ func (o *SocketOptions) applyToUDPConn(c *net.UDPConn) (err error) { err = bindInterfaceFunc(c, *o.BindInterface) if err != nil { err = fmt.Errorf("failed to bind to interface: %w", err) + return } } if o.FirewallMark != nil && firewallMarkFunc != nil { err = firewallMarkFunc(c, *o.FirewallMark) if err != nil { err = fmt.Errorf("failed to set fwmark: %w", err) + return } } if o.FdControlUnixSocket != nil && fdControlUnixSocketFunc != nil { err = fdControlUnixSocketFunc(c, *o.FdControlUnixSocket) if err != nil { err = fmt.Errorf("failed to send fd to control unix socket: %w", err) + return } } return From e6da1f348c0cf0e9aad6936fcb44fa8fc4f071bf Mon Sep 17 00:00:00 2001 From: Haruue Date: Fri, 5 Apr 2024 13:20:17 +0800 Subject: [PATCH 10/11] fix(sockopts): error handling in applyToUDPConn it is just no reason to use named err retval here --- app/internal/sockopts/sockopts.go | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/app/internal/sockopts/sockopts.go b/app/internal/sockopts/sockopts.go index e068f98a38..14ee0c019f 100644 --- a/app/internal/sockopts/sockopts.go +++ b/app/internal/sockopts/sockopts.go @@ -53,27 +53,24 @@ func (o *SocketOptions) ListenUDP() (uconn net.PacketConn, err error) { return } -func (o *SocketOptions) applyToUDPConn(c *net.UDPConn) (err error) { +func (o *SocketOptions) applyToUDPConn(c *net.UDPConn) error { if o.BindInterface != nil && bindInterfaceFunc != nil { - err = bindInterfaceFunc(c, *o.BindInterface) + err := bindInterfaceFunc(c, *o.BindInterface) if err != nil { - err = fmt.Errorf("failed to bind to interface: %w", err) - return + return fmt.Errorf("failed to bind to interface: %w", err) } } if o.FirewallMark != nil && firewallMarkFunc != nil { - err = firewallMarkFunc(c, *o.FirewallMark) + err := firewallMarkFunc(c, *o.FirewallMark) if err != nil { - err = fmt.Errorf("failed to set fwmark: %w", err) - return + return fmt.Errorf("failed to set fwmark: %w", err) } } if o.FdControlUnixSocket != nil && fdControlUnixSocketFunc != nil { - err = fdControlUnixSocketFunc(c, *o.FdControlUnixSocket) + err := fdControlUnixSocketFunc(c, *o.FdControlUnixSocket) if err != nil { - err = fmt.Errorf("failed to send fd to control unix socket: %w", err) - return + return fmt.Errorf("failed to send fd to control unix socket: %w", err) } } - return + return nil } From 6b5486fc09d22c3fb4a1cc78c799c8cfe81e6dce Mon Sep 17 00:00:00 2001 From: Toby Date: Fri, 5 Apr 2024 16:15:29 -0700 Subject: [PATCH 11/11] feat: add test for sockopts config fields --- app/cmd/client_test.go | 13 +++++++++++++ app/cmd/client_test.yaml | 12 ++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/app/cmd/client_test.go b/app/cmd/client_test.go index c586949e9e..10b2d9911e 100644 --- a/app/cmd/client_test.go +++ b/app/cmd/client_test.go @@ -46,6 +46,11 @@ func TestClientConfig(t *testing.T) { MaxIdleTimeout: 10 * time.Second, KeepAlivePeriod: 4 * time.Second, DisablePathMTUDiscovery: true, + Sockopts: clientConfigQUICSockopts{ + BindInterface: stringRef("eth0"), + FirewallMark: uint32Ref(1234), + FdControlUnixSocket: stringRef("test.sock"), + }, }, Bandwidth: clientConfigBandwidth{ Up: "200 mbps", @@ -189,3 +194,11 @@ func TestClientConfigURI(t *testing.T) { }) } } + +func stringRef(s string) *string { + return &s +} + +func uint32Ref(i uint32) *uint32 { + return &i +} diff --git a/app/cmd/client_test.yaml b/app/cmd/client_test.yaml index 4f919df0b9..e8438f6c4c 100644 --- a/app/cmd/client_test.yaml +++ b/app/cmd/client_test.yaml @@ -26,6 +26,10 @@ quic: maxIdleTimeout: 10s keepAlivePeriod: 4s disablePathMTUDiscovery: true + sockopts: + bindInterface: eth0 + fwmark: 1234 + fdControlUnixSocket: test.sock bandwidth: up: 200 mbps @@ -75,7 +79,7 @@ tun: ipv6: 2001::ffff:ffff:ffff:fff1/126 route: strict: true - ipv4: [0.0.0.0/0] - ipv6: ["2000::/3"] - ipv4Exclude: [192.0.2.1/32] - ipv6Exclude: ["2001:db8::1/128"] + ipv4: [ 0.0.0.0/0 ] + ipv6: [ "2000::/3" ] + ipv4Exclude: [ 192.0.2.1/32 ] + ipv6Exclude: [ "2001:db8::1/128" ]