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.
259 lines
5.4 KiB
Go
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:])
|
|
}
|