mirror of
https://github.com/cedar2025/hysteria.git
synced 2025-07-08 04:09:55 +00:00
feat: DirectOutbound bind IP
This commit is contained in:
parent
e381c2eae8
commit
62fddff137
@ -9,6 +9,8 @@ import (
|
|||||||
|
|
||||||
type DirectOutboundMode int
|
type DirectOutboundMode int
|
||||||
|
|
||||||
|
type udpConnState int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DirectOutboundModeAuto DirectOutboundMode = iota // Dual-stack "happy eyeballs"-like mode
|
DirectOutboundModeAuto DirectOutboundMode = iota // Dual-stack "happy eyeballs"-like mode
|
||||||
DirectOutboundMode64 // Use IPv6 address when available, otherwise IPv4
|
DirectOutboundMode64 // Use IPv6 address when available, otherwise IPv4
|
||||||
@ -19,25 +21,99 @@ const (
|
|||||||
defaultDialerTimeout = 10 * time.Second
|
defaultDialerTimeout = 10 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
udpConnStateDualStack udpConnState = iota
|
||||||
|
udpConnStateIPv4
|
||||||
|
udpConnStateIPv6
|
||||||
|
)
|
||||||
|
|
||||||
// directOutbound is a PluggableOutbound that connects directly to the target
|
// directOutbound is a PluggableOutbound that connects directly to the target
|
||||||
// using the local network (as opposed to using a proxy, for example).
|
// using the local network (as opposed to using a proxy, for example).
|
||||||
// It prefers to use ResolveInfo in AddrEx if available. But if it's nil,
|
// It prefers to use ResolveInfo in AddrEx if available. But if it's nil,
|
||||||
// it will fall back to resolving Host using Go's built-in DNS resolver.
|
// it will fall back to resolving Host using Go's built-in DNS resolver.
|
||||||
type directOutbound struct {
|
type directOutbound struct {
|
||||||
Mode DirectOutboundMode
|
Mode DirectOutboundMode
|
||||||
Dialer *net.Dialer
|
|
||||||
DeviceName string // For UDP binding
|
// Dialer4 and Dialer6 are used for IPv4 and IPv6 TCP connections respectively.
|
||||||
|
Dialer4 *net.Dialer
|
||||||
|
Dialer6 *net.Dialer
|
||||||
|
|
||||||
|
// DeviceName & BindIPs are for UDP connections. They don't use dialers, so we
|
||||||
|
// need to bind them when creating the connection.
|
||||||
|
DeviceName string
|
||||||
|
BindIP4 net.IP
|
||||||
|
BindIP6 net.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
type noAddressError struct {
|
||||||
|
IPv4 bool
|
||||||
|
IPv6 bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e noAddressError) Error() string {
|
||||||
|
if e.IPv4 && e.IPv6 {
|
||||||
|
return "no IPv4 or IPv6 address available"
|
||||||
|
} else if e.IPv4 {
|
||||||
|
return "no IPv4 address available"
|
||||||
|
} else if e.IPv6 {
|
||||||
|
return "no IPv6 address available"
|
||||||
|
} else {
|
||||||
|
return "no address available"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type invalidOutboundModeError struct{}
|
||||||
|
|
||||||
|
func (e invalidOutboundModeError) Error() string {
|
||||||
|
return "invalid outbound mode"
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDirectOutboundSimple creates a new directOutbound with the given mode,
|
// NewDirectOutboundSimple creates a new directOutbound with the given mode,
|
||||||
// without binding to a specific device. Works on all platforms.
|
// without binding to a specific device. Works on all platforms.
|
||||||
func NewDirectOutboundSimple(mode DirectOutboundMode) PluggableOutbound {
|
func NewDirectOutboundSimple(mode DirectOutboundMode) PluggableOutbound {
|
||||||
|
d := &net.Dialer{
|
||||||
|
Timeout: defaultDialerTimeout,
|
||||||
|
}
|
||||||
return &directOutbound{
|
return &directOutbound{
|
||||||
Mode: mode,
|
Mode: mode,
|
||||||
Dialer: &net.Dialer{
|
Dialer4: d,
|
||||||
|
Dialer6: d,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDirectOutboundBindToIPs creates a new directOutbound with the given mode,
|
||||||
|
// and binds to the given IPv4 and IPv6 addresses. Either or both of the addresses
|
||||||
|
// can be nil, in which case the directOutbound will not bind to a specific address
|
||||||
|
// for that family.
|
||||||
|
func NewDirectOutboundBindToIPs(mode DirectOutboundMode, bindIP4, bindIP6 net.IP) (PluggableOutbound, error) {
|
||||||
|
if bindIP4 != nil && bindIP4.To4() == nil {
|
||||||
|
return nil, errors.New("bindIP4 must be an IPv4 address")
|
||||||
|
}
|
||||||
|
if bindIP6 != nil && bindIP6.To4() != nil {
|
||||||
|
return nil, errors.New("bindIP6 must be an IPv6 address")
|
||||||
|
}
|
||||||
|
ob := &directOutbound{
|
||||||
|
Mode: mode,
|
||||||
|
Dialer4: &net.Dialer{
|
||||||
Timeout: defaultDialerTimeout,
|
Timeout: defaultDialerTimeout,
|
||||||
},
|
},
|
||||||
|
Dialer6: &net.Dialer{
|
||||||
|
Timeout: defaultDialerTimeout,
|
||||||
|
},
|
||||||
|
BindIP4: bindIP4,
|
||||||
|
BindIP6: bindIP6,
|
||||||
}
|
}
|
||||||
|
if bindIP4 != nil {
|
||||||
|
ob.Dialer4.LocalAddr = &net.TCPAddr{
|
||||||
|
IP: bindIP4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bindIP6 != nil {
|
||||||
|
ob.Dialer6.LocalAddr = &net.TCPAddr{
|
||||||
|
IP: bindIP6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ob, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolve is our built-in DNS resolver for handling the case when
|
// resolve is our built-in DNS resolver for handling the case when
|
||||||
@ -51,7 +127,7 @@ func (d *directOutbound) resolve(reqAddr *AddrEx) {
|
|||||||
r := &ResolveInfo{}
|
r := &ResolveInfo{}
|
||||||
r.IPv4, r.IPv6 = splitIPv4IPv6(ips)
|
r.IPv4, r.IPv6 = splitIPv4IPv6(ips)
|
||||||
if r.IPv4 == nil && r.IPv6 == nil {
|
if r.IPv4 == nil && r.IPv6 == nil {
|
||||||
r.Err = errors.New("no IPv4 or IPv6 address available")
|
r.Err = noAddressError{IPv4: true, IPv6: true}
|
||||||
}
|
}
|
||||||
reqAddr.ResolveInfo = r
|
reqAddr.ResolveInfo = r
|
||||||
}
|
}
|
||||||
@ -94,21 +170,25 @@ func (d *directOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) {
|
|||||||
if r.IPv6 != nil {
|
if r.IPv6 != nil {
|
||||||
return d.dialTCP(r.IPv6, reqAddr.Port)
|
return d.dialTCP(r.IPv6, reqAddr.Port)
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New("no IPv6 address available")
|
return nil, noAddressError{IPv6: true}
|
||||||
}
|
}
|
||||||
case DirectOutboundMode4:
|
case DirectOutboundMode4:
|
||||||
if r.IPv4 != nil {
|
if r.IPv4 != nil {
|
||||||
return d.dialTCP(r.IPv4, reqAddr.Port)
|
return d.dialTCP(r.IPv4, reqAddr.Port)
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New("no IPv4 address available")
|
return nil, noAddressError{IPv4: true}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("invalid DirectOutboundMode")
|
return nil, invalidOutboundModeError{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *directOutbound) dialTCP(ip net.IP, port uint16) (net.Conn, error) {
|
func (d *directOutbound) dialTCP(ip net.IP, port uint16) (net.Conn, error) {
|
||||||
return d.Dialer.Dial("tcp", net.JoinHostPort(ip.String(), strconv.Itoa(int(port))))
|
if ip.To4() != nil {
|
||||||
|
return d.Dialer4.Dial("tcp4", net.JoinHostPort(ip.String(), strconv.Itoa(int(port))))
|
||||||
|
} else {
|
||||||
|
return d.Dialer6.Dial("tcp6", net.JoinHostPort(ip.String(), strconv.Itoa(int(port))))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type dialResult struct {
|
type dialResult struct {
|
||||||
@ -149,6 +229,7 @@ func (d *directOutbound) dualStackDialTCP(ipv4, ipv6 net.IP, port uint16) (net.C
|
|||||||
type directOutboundUDPConn struct {
|
type directOutboundUDPConn struct {
|
||||||
*directOutbound
|
*directOutbound
|
||||||
*net.UDPConn
|
*net.UDPConn
|
||||||
|
State udpConnState
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *directOutboundUDPConn) ReadFrom(b []byte) (int, *AddrEx, error) {
|
func (u *directOutboundUDPConn) ReadFrom(b []byte) (int, *AddrEx, error) {
|
||||||
@ -165,20 +246,36 @@ func (u *directOutboundUDPConn) ReadFrom(b []byte) (int, *AddrEx, error) {
|
|||||||
|
|
||||||
func (u *directOutboundUDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) {
|
func (u *directOutboundUDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) {
|
||||||
if addr.ResolveInfo == nil {
|
if addr.ResolveInfo == nil {
|
||||||
// Although practically rare, it is possible to send
|
|
||||||
// UDP packets to a hostname (instead of an IP address).
|
|
||||||
u.directOutbound.resolve(addr)
|
u.directOutbound.resolve(addr)
|
||||||
}
|
}
|
||||||
r := addr.ResolveInfo
|
r := addr.ResolveInfo
|
||||||
if r.IPv4 == nil && r.IPv6 == nil {
|
if r.IPv4 == nil && r.IPv6 == nil {
|
||||||
return 0, r.Err
|
return 0, r.Err
|
||||||
}
|
}
|
||||||
|
if u.State == udpConnStateIPv4 {
|
||||||
|
if r.IPv4 != nil {
|
||||||
|
return u.UDPConn.WriteToUDP(b, &net.UDPAddr{
|
||||||
|
IP: r.IPv4,
|
||||||
|
Port: int(addr.Port),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return 0, noAddressError{IPv4: true}
|
||||||
|
}
|
||||||
|
} else if u.State == udpConnStateIPv6 {
|
||||||
|
if r.IPv6 != nil {
|
||||||
|
return u.UDPConn.WriteToUDP(b, &net.UDPAddr{
|
||||||
|
IP: r.IPv6,
|
||||||
|
Port: int(addr.Port),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return 0, noAddressError{IPv6: true}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Dual stack
|
||||||
switch u.directOutbound.Mode {
|
switch u.directOutbound.Mode {
|
||||||
case DirectOutboundModeAuto:
|
case DirectOutboundModeAuto:
|
||||||
// This is a special case.
|
// This is a special case.
|
||||||
// It's not possible to do a "dual stack race dial" for UDP,
|
// We must make a decision here, so we prefer IPv4 for maximum compatibility.
|
||||||
// since UDP is connectionless.
|
|
||||||
// For maximum compatibility, we just behave like DirectOutboundMode46.
|
|
||||||
if r.IPv4 != nil {
|
if r.IPv4 != nil {
|
||||||
return u.UDPConn.WriteToUDP(b, &net.UDPAddr{
|
return u.UDPConn.WriteToUDP(b, &net.UDPAddr{
|
||||||
IP: r.IPv4,
|
IP: r.IPv4,
|
||||||
@ -221,7 +318,7 @@ func (u *directOutboundUDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) {
|
|||||||
Port: int(addr.Port),
|
Port: int(addr.Port),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return 0, errors.New("no IPv6 address available")
|
return 0, noAddressError{IPv6: true}
|
||||||
}
|
}
|
||||||
case DirectOutboundMode4:
|
case DirectOutboundMode4:
|
||||||
if r.IPv4 != nil {
|
if r.IPv4 != nil {
|
||||||
@ -230,10 +327,11 @@ func (u *directOutboundUDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) {
|
|||||||
Port: int(addr.Port),
|
Port: int(addr.Port),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return 0, errors.New("no IPv4 address available")
|
return 0, noAddressError{IPv4: true}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return 0, errors.New("invalid DirectOutboundMode")
|
return 0, invalidOutboundModeError{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,6 +340,8 @@ func (u *directOutboundUDPConn) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *directOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) {
|
func (d *directOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) {
|
||||||
|
if d.BindIP4 == nil && d.BindIP6 == nil {
|
||||||
|
// No bind address specified, use default dual stack implementation
|
||||||
c, err := net.ListenUDP("udp", nil)
|
c, err := net.ListenUDP("udp", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -256,5 +356,89 @@ func (d *directOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) {
|
|||||||
return &directOutboundUDPConn{
|
return &directOutboundUDPConn{
|
||||||
directOutbound: d,
|
directOutbound: d,
|
||||||
UDPConn: c,
|
UDPConn: c,
|
||||||
|
State: udpConnStateDualStack,
|
||||||
|
}, nil
|
||||||
|
} else {
|
||||||
|
// Bind address specified,
|
||||||
|
// need to check what kind of address is in reqAddr
|
||||||
|
// to determine which address family to bind to
|
||||||
|
if reqAddr.ResolveInfo == nil {
|
||||||
|
d.resolve(reqAddr)
|
||||||
|
}
|
||||||
|
r := reqAddr.ResolveInfo
|
||||||
|
if r.IPv4 == nil && r.IPv6 == nil {
|
||||||
|
return nil, r.Err
|
||||||
|
}
|
||||||
|
var bindIP net.IP // can be nil, in which case we still lock the address family but don't bind to any address
|
||||||
|
var state udpConnState // either IPv4 or IPv6
|
||||||
|
switch d.Mode {
|
||||||
|
case DirectOutboundModeAuto:
|
||||||
|
// This is a special case.
|
||||||
|
// We must make a decision here, so we prefer IPv4 for maximum compatibility.
|
||||||
|
if r.IPv4 != nil {
|
||||||
|
bindIP = d.BindIP4
|
||||||
|
state = udpConnStateIPv4
|
||||||
|
} else {
|
||||||
|
bindIP = d.BindIP6
|
||||||
|
state = udpConnStateIPv6
|
||||||
|
}
|
||||||
|
case DirectOutboundMode64:
|
||||||
|
if r.IPv6 != nil {
|
||||||
|
bindIP = d.BindIP6
|
||||||
|
state = udpConnStateIPv6
|
||||||
|
} else {
|
||||||
|
bindIP = d.BindIP4
|
||||||
|
state = udpConnStateIPv4
|
||||||
|
}
|
||||||
|
case DirectOutboundMode46:
|
||||||
|
if r.IPv4 != nil {
|
||||||
|
bindIP = d.BindIP4
|
||||||
|
state = udpConnStateIPv4
|
||||||
|
} else {
|
||||||
|
bindIP = d.BindIP6
|
||||||
|
state = udpConnStateIPv6
|
||||||
|
}
|
||||||
|
case DirectOutboundMode6:
|
||||||
|
if r.IPv6 != nil {
|
||||||
|
bindIP = d.BindIP6
|
||||||
|
state = udpConnStateIPv6
|
||||||
|
} else {
|
||||||
|
return nil, noAddressError{IPv6: true}
|
||||||
|
}
|
||||||
|
case DirectOutboundMode4:
|
||||||
|
if r.IPv4 != nil {
|
||||||
|
bindIP = d.BindIP4
|
||||||
|
state = udpConnStateIPv4
|
||||||
|
} else {
|
||||||
|
return nil, noAddressError{IPv4: true}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, invalidOutboundModeError{}
|
||||||
|
}
|
||||||
|
var network string
|
||||||
|
var c *net.UDPConn
|
||||||
|
var err error
|
||||||
|
if state == udpConnStateIPv4 {
|
||||||
|
network = "udp4"
|
||||||
|
} else {
|
||||||
|
network = "udp6"
|
||||||
|
}
|
||||||
|
if bindIP != nil {
|
||||||
|
c, err = net.ListenUDP(network, &net.UDPAddr{
|
||||||
|
IP: bindIP,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
c, err = net.ListenUDP(network, nil)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// We don't support binding to both device & address at the same time,
|
||||||
|
// so d.DeviceName is ignored in this case.
|
||||||
|
return &directOutboundUDPConn{
|
||||||
|
directOutbound: d,
|
||||||
|
UDPConn: c,
|
||||||
|
State: state,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -12,9 +12,7 @@ func NewDirectOutboundBindToDevice(mode DirectOutboundMode, deviceName string) (
|
|||||||
if err := verifyDeviceName(deviceName); err != nil {
|
if err := verifyDeviceName(deviceName); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &directOutbound{
|
d := &net.Dialer{
|
||||||
Mode: mode,
|
|
||||||
Dialer: &net.Dialer{
|
|
||||||
Timeout: defaultDialerTimeout,
|
Timeout: defaultDialerTimeout,
|
||||||
Control: func(network, address string, c syscall.RawConn) error {
|
Control: func(network, address string, c syscall.RawConn) error {
|
||||||
var errBind error
|
var errBind error
|
||||||
@ -26,7 +24,11 @@ func NewDirectOutboundBindToDevice(mode DirectOutboundMode, deviceName string) (
|
|||||||
}
|
}
|
||||||
return errBind
|
return errBind
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
|
return &directOutbound{
|
||||||
|
Mode: mode,
|
||||||
|
Dialer4: d,
|
||||||
|
Dialer6: d,
|
||||||
DeviceName: deviceName,
|
DeviceName: deviceName,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user