feat: port hopping random interval
This commit is contained in:
parent
82d9935c85
commit
6ef838d2c8
4 changed files with 183 additions and 15 deletions
|
|
@ -80,7 +80,9 @@ type clientConfig struct {
|
|||
}
|
||||
|
||||
type clientConfigTransportUDP struct {
|
||||
HopInterval time.Duration `mapstructure:"hopInterval"`
|
||||
HopInterval time.Duration `mapstructure:"hopInterval"`
|
||||
MinHopInterval time.Duration `mapstructure:"minHopInterval"`
|
||||
MaxHopInterval time.Duration `mapstructure:"maxHopInterval"`
|
||||
}
|
||||
|
||||
type clientConfigTransport struct {
|
||||
|
|
@ -232,12 +234,16 @@ func (c *clientConfig) fillConnFactory(hyConfig *client.Config) error {
|
|||
}
|
||||
// Inner PacketConn
|
||||
var newFunc func(addr net.Addr) (net.PacketConn, error)
|
||||
hopInterval, err := c.Transport.UDP.hopIntervalConfig()
|
||||
if err != nil {
|
||||
return configError{Field: "transport.udp", Err: err}
|
||||
}
|
||||
switch strings.ToLower(c.Transport.Type) {
|
||||
case "", "udp":
|
||||
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, so.ListenUDP)
|
||||
return udphop.NewUDPHopPacketConn(hopAddr, hopInterval, so.ListenUDP)
|
||||
}
|
||||
} else {
|
||||
newFunc = func(addr net.Addr) (net.PacketConn, error) {
|
||||
|
|
@ -249,7 +255,6 @@ func (c *clientConfig) fillConnFactory(hyConfig *client.Config) error {
|
|||
}
|
||||
// Obfuscation
|
||||
var ob obfs.Obfuscator
|
||||
var err error
|
||||
switch strings.ToLower(c.Obfs.Type) {
|
||||
case "", "plain":
|
||||
// Keep it nil
|
||||
|
|
@ -268,6 +273,25 @@ func (c *clientConfig) fillConnFactory(hyConfig *client.Config) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c clientConfigTransportUDP) hopIntervalConfig() (udphop.HopIntervalConfig, error) {
|
||||
if c.HopInterval != 0 && (c.MinHopInterval != 0 || c.MaxHopInterval != 0) {
|
||||
return udphop.HopIntervalConfig{}, errors.New("hopInterval cannot be used together with minHopInterval or maxHopInterval")
|
||||
}
|
||||
if c.MinHopInterval == 0 && c.MaxHopInterval == 0 {
|
||||
if c.HopInterval == 0 {
|
||||
return udphop.HopIntervalConfig{}, nil
|
||||
}
|
||||
return udphop.HopIntervalConfig{Min: c.HopInterval, Max: c.HopInterval}, nil
|
||||
}
|
||||
if c.MinHopInterval == 0 || c.MaxHopInterval == 0 {
|
||||
return udphop.HopIntervalConfig{}, errors.New("minHopInterval and maxHopInterval must both be set")
|
||||
}
|
||||
return udphop.HopIntervalConfig{
|
||||
Min: c.MinHopInterval,
|
||||
Max: c.MaxHopInterval,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *clientConfig) fillAuth(hyConfig *client.Config) error {
|
||||
hyConfig.Auth = c.Auth
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -242,6 +242,48 @@ func TestClientFillCongestionConfig(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestClientTransportUDPHopIntervalConfig(t *testing.T) {
|
||||
t.Run("fixed interval", func(t *testing.T) {
|
||||
cfg, err := (clientConfigTransportUDP{HopInterval: 30 * time.Second}).hopIntervalConfig()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30*time.Second, cfg.Min)
|
||||
assert.Equal(t, 30*time.Second, cfg.Max)
|
||||
})
|
||||
|
||||
t.Run("range interval", func(t *testing.T) {
|
||||
cfg, err := (clientConfigTransportUDP{
|
||||
MinHopInterval: 10 * time.Second,
|
||||
MaxHopInterval: 30 * time.Second,
|
||||
}).hopIntervalConfig()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10*time.Second, cfg.Min)
|
||||
assert.Equal(t, 30*time.Second, cfg.Max)
|
||||
})
|
||||
|
||||
t.Run("default interval", func(t *testing.T) {
|
||||
cfg, err := (clientConfigTransportUDP{}).hopIntervalConfig()
|
||||
assert.NoError(t, err)
|
||||
assert.Zero(t, cfg.Min)
|
||||
assert.Zero(t, cfg.Max)
|
||||
})
|
||||
|
||||
t.Run("rejects mixed fields", func(t *testing.T) {
|
||||
_, err := (clientConfigTransportUDP{
|
||||
HopInterval: 30 * time.Second,
|
||||
MinHopInterval: 10 * time.Second,
|
||||
MaxHopInterval: 30 * time.Second,
|
||||
}).hopIntervalConfig()
|
||||
assert.EqualError(t, err, "hopInterval cannot be used together with minHopInterval or maxHopInterval")
|
||||
})
|
||||
|
||||
t.Run("rejects partial range", func(t *testing.T) {
|
||||
_, err := (clientConfigTransportUDP{
|
||||
MinHopInterval: 10 * time.Second,
|
||||
}).hopIntervalConfig()
|
||||
assert.EqualError(t, err, "minHopInterval and maxHopInterval must both be set")
|
||||
})
|
||||
}
|
||||
|
||||
func stringRef(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@ package udphop
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
|
@ -14,12 +17,19 @@ const (
|
|||
udpBufferSize = 2048 // QUIC packets are at most 1500 bytes long, so 2k should be more than enough
|
||||
|
||||
defaultHopInterval = 30 * time.Second
|
||||
|
||||
debugEnv = "HYSTERIA_UDPHOP_DEBUG"
|
||||
)
|
||||
|
||||
type HopIntervalConfig struct {
|
||||
Min time.Duration
|
||||
Max time.Duration
|
||||
}
|
||||
|
||||
type udpHopPacketConn struct {
|
||||
Addr net.Addr
|
||||
Addrs []net.Addr
|
||||
HopInterval time.Duration
|
||||
HopInterval HopIntervalConfig
|
||||
ListenUDPFunc ListenUDPFunc
|
||||
|
||||
connMutex sync.RWMutex
|
||||
|
|
@ -38,6 +48,7 @@ type udpHopPacketConn struct {
|
|||
closed bool
|
||||
|
||||
bufPool sync.Pool
|
||||
debug bool
|
||||
}
|
||||
|
||||
type udpPacket struct {
|
||||
|
|
@ -49,11 +60,10 @@ type udpPacket struct {
|
|||
|
||||
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 {
|
||||
return nil, errors.New("hop interval must be at least 5 seconds")
|
||||
func NewUDPHopPacketConn(addr *UDPHopAddr, hopInterval HopIntervalConfig, listenUDPFunc ListenUDPFunc) (net.PacketConn, error) {
|
||||
hopInterval, err := hopInterval.normalized()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if listenUDPFunc == nil {
|
||||
listenUDPFunc = func() (net.PacketConn, error) {
|
||||
|
|
@ -68,6 +78,7 @@ func NewUDPHopPacketConn(addr *UDPHopAddr, hopInterval time.Duration, listenUDPF
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
debug, _ := strconv.ParseBool(os.Getenv(debugEnv))
|
||||
hConn := &udpHopPacketConn{
|
||||
Addr: addr,
|
||||
Addrs: addrs,
|
||||
|
|
@ -83,12 +94,32 @@ func NewUDPHopPacketConn(addr *UDPHopAddr, hopInterval time.Duration, listenUDPF
|
|||
return make([]byte, udpBufferSize)
|
||||
},
|
||||
},
|
||||
debug: debug,
|
||||
}
|
||||
if hConn.debug {
|
||||
hConn.debugPrint("Initialized: local=%s target=%s interval=%s", curConn.LocalAddr(), addr, hopInterval)
|
||||
}
|
||||
go hConn.recvLoop(curConn)
|
||||
go hConn.hopLoop()
|
||||
return hConn, nil
|
||||
}
|
||||
|
||||
func (c HopIntervalConfig) normalized() (HopIntervalConfig, error) {
|
||||
if c.Min == 0 && c.Max == 0 {
|
||||
return HopIntervalConfig{Min: defaultHopInterval, Max: defaultHopInterval}, nil
|
||||
}
|
||||
if c.Min == 0 || c.Max == 0 {
|
||||
return HopIntervalConfig{}, errors.New("min and max hop interval must both be set")
|
||||
}
|
||||
if c.Min > c.Max {
|
||||
return HopIntervalConfig{}, errors.New("min hop interval must not be greater than max hop interval")
|
||||
}
|
||||
if c.Min < 5*time.Second {
|
||||
return HopIntervalConfig{}, errors.New("hop interval must be at least 5 seconds")
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (u *udpHopPacketConn) recvLoop(conn net.PacketConn) {
|
||||
for {
|
||||
buf := u.bufPool.Get().([]byte)
|
||||
|
|
@ -115,19 +146,30 @@ func (u *udpHopPacketConn) recvLoop(conn net.PacketConn) {
|
|||
}
|
||||
|
||||
func (u *udpHopPacketConn) hopLoop() {
|
||||
ticker := time.NewTicker(u.HopInterval)
|
||||
defer ticker.Stop()
|
||||
next := u.nextHopInterval()
|
||||
timer := time.NewTimer(next)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
u.hop()
|
||||
case <-timer.C:
|
||||
hopInterval := next
|
||||
u.hop(hopInterval)
|
||||
next = u.nextHopInterval()
|
||||
timer.Reset(next)
|
||||
case <-u.closeChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *udpHopPacketConn) hop() {
|
||||
func (u *udpHopPacketConn) nextHopInterval() time.Duration {
|
||||
if u.HopInterval.Min == u.HopInterval.Max {
|
||||
return u.HopInterval.Min
|
||||
}
|
||||
return u.HopInterval.Min + time.Duration(rand.Int63n(int64(u.HopInterval.Max-u.HopInterval.Min)+1))
|
||||
}
|
||||
|
||||
func (u *udpHopPacketConn) hop(hopInterval time.Duration) {
|
||||
u.connMutex.Lock()
|
||||
defer u.connMutex.Unlock()
|
||||
if u.closed {
|
||||
|
|
@ -136,6 +178,9 @@ func (u *udpHopPacketConn) hop() {
|
|||
newConn, err := u.ListenUDPFunc()
|
||||
if err != nil {
|
||||
// Could be temporary, just skip this hop
|
||||
if u.debug {
|
||||
u.debugPrint("Hop skipped: listen failed: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
// We need to keep receiving packets from the previous connection,
|
||||
|
|
@ -169,7 +214,14 @@ func (u *udpHopPacketConn) hop() {
|
|||
}
|
||||
go u.recvLoop(newConn)
|
||||
// Update addrIndex to a new random value
|
||||
prevRemote := u.Addrs[u.addrIndex]
|
||||
u.addrIndex = rand.Intn(len(u.Addrs))
|
||||
if u.debug {
|
||||
u.debugPrint("Hop after %s: local=%s -> %s remote=%s -> %s",
|
||||
formatHopInterval(hopInterval),
|
||||
u.prevConn.LocalAddr(), u.currentConn.LocalAddr(),
|
||||
prevRemote, u.Addrs[u.addrIndex])
|
||||
}
|
||||
}
|
||||
|
||||
func (u *udpHopPacketConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
|
||||
|
|
@ -311,3 +363,14 @@ func trySetWriteBuffer(pc net.PacketConn, bytes int) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *udpHopPacketConn) debugPrint(format string, a ...any) {
|
||||
fmt.Printf("[UDPHop] [%s] %s\n",
|
||||
time.Now().Format("15:04:05"),
|
||||
fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
func formatHopInterval(d time.Duration) string {
|
||||
seconds := d.Seconds()
|
||||
return fmt.Sprintf("%.2fs", seconds)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,9 +134,48 @@ func TestHopReappliesStoredDeadlines(t *testing.T) {
|
|||
require.NoError(t, u.SetReadDeadline(readDeadline))
|
||||
require.NoError(t, u.SetWriteDeadline(writeDeadline))
|
||||
|
||||
u.hop()
|
||||
u.hop(time.Second)
|
||||
|
||||
require.Empty(t, secondConn.setDeadlineCalls)
|
||||
require.Equal(t, []time.Time{readDeadline}, secondConn.setReadDeadlineCalls)
|
||||
require.Equal(t, []time.Time{writeDeadline}, secondConn.setWriteDeadlineCalls)
|
||||
}
|
||||
|
||||
func TestHopIntervalConfigNormalized(t *testing.T) {
|
||||
t.Run("defaults", func(t *testing.T) {
|
||||
cfg, err := (HopIntervalConfig{}).normalized()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, defaultHopInterval, cfg.Min)
|
||||
require.Equal(t, defaultHopInterval, cfg.Max)
|
||||
})
|
||||
|
||||
t.Run("rejects partial range", func(t *testing.T) {
|
||||
_, err := (HopIntervalConfig{Min: 10 * time.Second}).normalized()
|
||||
require.EqualError(t, err, "min and max hop interval must both be set")
|
||||
})
|
||||
|
||||
t.Run("rejects reversed range", func(t *testing.T) {
|
||||
_, err := (HopIntervalConfig{Min: 30 * time.Second, Max: 10 * time.Second}).normalized()
|
||||
require.EqualError(t, err, "min hop interval must not be greater than max hop interval")
|
||||
})
|
||||
|
||||
t.Run("rejects too short interval", func(t *testing.T) {
|
||||
_, err := (HopIntervalConfig{Min: 4 * time.Second, Max: 6 * time.Second}).normalized()
|
||||
require.EqualError(t, err, "hop interval must be at least 5 seconds")
|
||||
})
|
||||
}
|
||||
|
||||
func TestNextHopIntervalWithinRange(t *testing.T) {
|
||||
u := &udpHopPacketConn{
|
||||
HopInterval: HopIntervalConfig{
|
||||
Min: 10 * time.Second,
|
||||
Max: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
d := u.nextHopInterval()
|
||||
require.GreaterOrEqual(t, d, 10*time.Second)
|
||||
require.LessOrEqual(t, d, 30*time.Second)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue