Files
lpra/internal/server/udp.go
T
redtea bcfe909aaa feat: tunnel host and port in the panel settings, ufw kept in sync
The direct tunnel port (8446 by default) and the host agents dial for
it can be set in Settings. The server moves its listeners at once and
agents pick the address up from the panel: on connect and with every
poll, unless TUNNEL_ADDR / TUNNEL_PORT pin it locally.

ufw: run with a C locale (the output is translated), do not adopt rules
that existed before the panel, skip loopback-only listeners, re-open
what saved forwards need on start and when the integration is turned
on, move the shared tunnel rule with the port and drop it when no
direct forward is left. The last ufw error is shown in Settings.

The forwards page shows the real tunnel and panel ports.
2026-10-13 14:50:50 +03:00

259 lines
5.4 KiB
Go

package server
import (
"encoding/binary"
"log"
"net"
"strings"
"sync"
"time"
"github.com/x-grk/longpolling-remote-access/internal/tunnel"
)
// udpRelay carries UDP forwards over the direct 8446 UDP channel. The agent
// registers its UDP return address; end-user datagrams arriving on a rule's
// host port are relayed to the agent and replies are sent back.
//
// Datagram framing on the 8446 control socket:
//
// registration (agent->server): "R <agentID> <sig>"
// data (both): 'D' | sid(4) | port(2) | payload
type udpRelay struct {
db *DB
mu sync.Mutex
ctrl *net.UDPConn
gcOnce sync.Once
agentAddr map[string]*net.UDPAddr // agentID -> last registered addr
sessions map[uint32]*udpSession
nextSID uint32
rulePorts map[int64]*net.UDPConn // ruleID -> per-rule listener
}
type udpSession struct {
id uint32
agentID string
ruleConn *net.UDPConn
userAddr *net.UDPAddr
clientPort int
last time.Time
}
func newUDPRelay(db *DB) *udpRelay {
return &udpRelay{
db: db,
agentAddr: map[string]*net.UDPAddr{},
sessions: map[uint32]*udpSession{},
rulePorts: map[int64]*net.UDPConn{},
}
}
// listen opens the shared UDP control socket. Listening again moves it: the
// previous socket is closed and agents register anew on the new address.
func (u *udpRelay) listen(addr string) error {
ua, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return err
}
c, err := net.ListenUDP("udp", ua)
if err != nil {
return err
}
u.mu.Lock()
old := u.ctrl
u.ctrl = c
if old != nil {
u.agentAddr = map[string]*net.UDPAddr{}
}
u.mu.Unlock()
if old != nil {
old.Close()
}
log.Printf("tunnel: listening on %s/udp (direct UDP forwarding)", addr)
go u.ctrlLoop(c)
u.gcOnce.Do(func() { go u.gc() })
return nil
}
func (u *udpRelay) ctrlLoop(ctrl *net.UDPConn) {
buf := make([]byte, 65535)
for {
n, src, err := ctrl.ReadFromUDP(buf)
if err != nil {
return
}
if n == 0 {
continue
}
switch buf[0] {
case 'R':
u.handleReg(string(buf[1:n]), src)
case 'D':
u.handleAgentData(buf[1:n])
}
}
}
// handleReg validates "agentID sig" and records the agent's UDP addr.
// sig = HMAC(key, "agentID|minute") accepting the current or previous minute.
func (u *udpRelay) handleReg(line string, src *net.UDPAddr) {
f := strings.Fields(strings.TrimSpace(line))
if len(f) != 2 {
return
}
agentID, sig := f[0], f[1]
agent, err := u.db.GetAgent(agentID)
if err != nil || agent == nil {
return
}
key, err := tunnel.KeyFromHash(agent.TokenHash)
if err != nil {
return
}
now := time.Now().Unix() / 60
ok := false
for _, m := range []int64{now, now - 1} {
msg := []byte(agentID + "|" + itoa64(m))
if tunnel.EqualSig(sig, tunnel.Sign(key, msg)) {
ok = true
break
}
}
if !ok {
return
}
u.mu.Lock()
u.agentAddr[agentID] = src
u.mu.Unlock()
}
// handleAgentData routes a reply datagram from an agent back to the end user.
func (u *udpRelay) handleAgentData(b []byte) {
if len(b) < 6 {
return
}
sid := binary.BigEndian.Uint32(b[0:4])
payload := b[6:]
u.mu.Lock()
s := u.sessions[sid]
if s != nil {
s.last = time.Now()
}
u.mu.Unlock()
if s != nil {
_, _ = s.ruleConn.WriteToUDP(payload, s.userAddr)
}
}
// openRulePort starts the per-rule UDP listener on the host port.
func (u *udpRelay) openRulePort(rule ForwardRule) error {
ua, err := net.ResolveUDPAddr("udp", rule.ListenAddr+":"+itoa(rule.HostPort))
if err != nil {
return err
}
c, err := net.ListenUDP("udp", ua)
if err != nil {
return err
}
u.mu.Lock()
u.rulePorts[rule.ID] = c
u.mu.Unlock()
go u.rulePortLoop(rule, c)
return nil
}
func (u *udpRelay) closeRulePort(ruleID int64) {
u.mu.Lock()
c := u.rulePorts[ruleID]
delete(u.rulePorts, ruleID)
u.mu.Unlock()
if c != nil {
c.Close()
}
}
func (u *udpRelay) rulePortLoop(rule ForwardRule, c *net.UDPConn) {
buf := make([]byte, 65535)
for {
n, userAddr, err := c.ReadFromUDP(buf)
if err != nil {
return
}
u.mu.Lock()
aAddr := u.agentAddr[rule.AgentID]
ctrl := u.ctrl
// Find existing session for this (rule, user).
var sess *udpSession
for _, s := range u.sessions {
if s.ruleConn == c && udpAddrEqual(s.userAddr, userAddr) {
sess = s
break
}
}
if sess == nil {
u.nextSID++
sess = &udpSession{
id: u.nextSID, agentID: rule.AgentID, ruleConn: c,
userAddr: userAddr, clientPort: rule.ClientPort,
}
u.sessions[sess.id] = sess
}
sess.last = time.Now()
u.mu.Unlock()
if aAddr == nil || ctrl == nil {
continue // agent has not registered its UDP endpoint yet
}
// 'D' | sid(4) | clientPort(2) | payload
out := make([]byte, 7+n)
out[0] = 'D'
binary.BigEndian.PutUint32(out[1:5], sess.id)
binary.BigEndian.PutUint16(out[5:7], uint16(rule.ClientPort))
copy(out[7:], buf[:n])
_, _ = ctrl.WriteToUDP(out, aAddr)
}
}
func (u *udpRelay) gc() {
t := time.NewTicker(30 * time.Second)
defer t.Stop()
for range t.C {
cut := time.Now().Add(-90 * time.Second)
u.mu.Lock()
for id, s := range u.sessions {
if s.last.Before(cut) {
delete(u.sessions, id)
}
}
u.mu.Unlock()
}
}
func udpAddrEqual(a, b *net.UDPAddr) bool {
return a != nil && b != nil && a.Port == b.Port && a.IP.Equal(b.IP)
}
func itoa(n int) string { return itoa64(int64(n)) }
func itoa64(n int64) string {
if n == 0 {
return "0"
}
neg := n < 0
if neg {
n = -n
}
var b [24]byte
i := len(b)
for n > 0 {
i--
b[i] = byte('0' + n%10)
n /= 10
}
if neg {
i--
b[i] = '-'
}
return string(b[i:])
}