hysteria/app/internal/forwarding/udp.go

180 lines
3.6 KiB
Go

package forwarding
import (
"net"
"sync"
"sync/atomic"
"time"
"github.com/apernet/hysteria/core/v2/client"
)
const (
udpBufferSize = 4096
defaultTimeout = 60 * time.Second
idleCleanupInterval = 1 * time.Second
)
type atomicTime struct {
v atomic.Value
}
func newAtomicTime(t time.Time) *atomicTime {
a := &atomicTime{}
a.Set(t)
return a
}
func (t *atomicTime) Set(new time.Time) {
t.v.Store(new)
}
func (t *atomicTime) Get() time.Time {
return t.v.Load().(time.Time)
}
type sessionEntry struct {
HyConn client.HyUDPConn
Last *atomicTime
Timeout bool // true if the session is closed due to timeout
}
func (e *sessionEntry) Feed(data []byte, addr string) error {
e.Last.Set(time.Now())
return e.HyConn.Send(data, addr)
}
func (e *sessionEntry) ReceiveLoop(pc net.PacketConn, addr net.Addr) error {
for {
data, _, err := e.HyConn.Receive()
if err != nil {
return err
}
_, err = pc.WriteTo(data, addr)
if err != nil {
return err
}
e.Last.Set(time.Now())
}
}
type UDPTunnel struct {
HyClient client.Client
Remote string
Timeout time.Duration
EventLogger UDPEventLogger
m map[string]*sessionEntry // addr -> HyConn
mutex sync.RWMutex
}
type UDPEventLogger interface {
Connect(addr net.Addr)
Error(addr net.Addr, err error)
}
func (t *UDPTunnel) Serve(pc net.PacketConn) error {
t.m = make(map[string]*sessionEntry)
stopCh := make(chan struct{})
go t.idleCleanupLoop(stopCh)
defer close(stopCh)
defer t.cleanup(false)
buf := make([]byte, udpBufferSize)
for {
n, addr, err := pc.ReadFrom(buf)
if err != nil {
return err
}
t.feed(pc, addr, buf[:n])
}
}
func (t *UDPTunnel) idleCleanupLoop(stopCh <-chan struct{}) {
ticker := time.NewTicker(idleCleanupInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
t.cleanup(true)
case <-stopCh:
return
}
}
}
func (t *UDPTunnel) cleanup(idleOnly bool) {
// We use RLock here as we are only scanning the map, not deleting from it.
t.mutex.RLock()
defer t.mutex.RUnlock()
timeout := t.Timeout
if timeout == 0 {
timeout = defaultTimeout
}
now := time.Now()
for _, entry := range t.m {
if !idleOnly || now.Sub(entry.Last.Get()) > timeout {
entry.Timeout = true
_ = entry.HyConn.Close()
// Closing the connection here will cause the ReceiveLoop to exit,
// and the session will be removed from the map there.
}
}
}
func (t *UDPTunnel) feed(pc net.PacketConn, addr net.Addr, data []byte) {
t.mutex.RLock()
entry := t.m[addr.String()]
t.mutex.RUnlock()
// Create a new session if not exists
if entry == nil {
if t.EventLogger != nil {
t.EventLogger.Connect(addr)
}
hyConn, err := t.HyClient.UDP()
if err != nil {
if t.EventLogger != nil {
t.EventLogger.Error(addr, err)
}
return
}
entry = &sessionEntry{
HyConn: hyConn,
Last: newAtomicTime(time.Now()),
}
// Start the receive loop for this session
// Local <- Remote
go func() {
err := entry.ReceiveLoop(pc, addr)
if !entry.Timeout {
_ = hyConn.Close()
if t.EventLogger != nil {
t.EventLogger.Error(addr, err)
}
} else {
// Connection already closed by timeout cleanup,
// no need to close again here.
// Use nil error to indicate timeout.
if t.EventLogger != nil {
t.EventLogger.Error(addr, nil)
}
}
// Remove the session from the map
t.mutex.Lock()
delete(t.m, addr.String())
t.mutex.Unlock()
}()
// Insert the session into the map
t.mutex.Lock()
t.m[addr.String()] = entry
t.mutex.Unlock()
}
// Feed the message to the session
_ = entry.Feed(data, t.Remote)
}