mirror of
https://github.com/apernet/hysteria.git
synced 2025-04-03 04:27:39 +03:00
180 lines
3.6 KiB
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)
|
|
}
|