744 lines
22 KiB
Go
744 lines
22 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"dragontcpvpn/internal/protocol"
|
|
)
|
|
|
|
const (
|
|
defaultVPNv4Prefix = "10.123.0.0/16"
|
|
defaultVPNv6Prefix = "fd7a:4472:6167:6f6e::/64"
|
|
)
|
|
|
|
type debugStats struct {
|
|
enabled bool
|
|
packets bool
|
|
started time.Time
|
|
activeConns atomic.Int64
|
|
activeSessions atomic.Int64
|
|
upPackets atomic.Uint64
|
|
downPackets atomic.Uint64
|
|
upBytes atomic.Uint64
|
|
downBytes atomic.Uint64
|
|
dropped atomic.Uint64
|
|
errors atomic.Uint64
|
|
}
|
|
|
|
func (d *debugStats) logf(format string, args ...any) {
|
|
if d != nil && d.enabled {
|
|
fmt.Printf("[DEBUG] "+format+"\n", args...)
|
|
}
|
|
}
|
|
func (d *debugStats) packetf(format string, args ...any) {
|
|
if d != nil && d.packets {
|
|
fmt.Printf("[PACKET] "+format+"\n", args...)
|
|
}
|
|
}
|
|
func (d *debugStats) errorf(format string, args ...any) {
|
|
if d != nil {
|
|
d.errors.Add(1)
|
|
if d.enabled {
|
|
fmt.Printf("[ERROR] "+format+"\n", args...)
|
|
}
|
|
}
|
|
}
|
|
|
|
func tokenEqual(a, b string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
|
}
|
|
|
|
type vpnSession struct {
|
|
sid protocol.VPNSessionID
|
|
ipv4 netip.Addr
|
|
ipv6 netip.Addr
|
|
mtu int
|
|
maxChunk int
|
|
maxPackets int
|
|
manager *vpnManager
|
|
|
|
mu sync.Mutex
|
|
notify chan struct{}
|
|
packets map[uint32][]byte
|
|
nextDown uint32
|
|
closed bool
|
|
lastSeen time.Time
|
|
|
|
upMu sync.Mutex
|
|
expectedUp uint32
|
|
currentSeq uint32
|
|
currentTotal int
|
|
currentBuf []byte
|
|
haveCurrent bool
|
|
lastComplete uint32
|
|
lastCompleteTotal int
|
|
haveLastComplete bool
|
|
}
|
|
|
|
func newVPNSession(m *vpnManager, sid protocol.VPNSessionID, v4, v6 netip.Addr, mtu, maxChunk, maxPackets int) *vpnSession {
|
|
return &vpnSession{
|
|
sid: sid, ipv4: v4, ipv6: v6, mtu: mtu, maxChunk: maxChunk, maxPackets: maxPackets,
|
|
manager: m, notify: make(chan struct{}), packets: make(map[uint32][]byte, maxPackets), lastSeen: time.Now(),
|
|
}
|
|
}
|
|
|
|
func (s *vpnSession) signalLocked() {
|
|
close(s.notify)
|
|
s.notify = make(chan struct{})
|
|
}
|
|
func (s *vpnSession) touchLocked() { s.lastSeen = time.Now() }
|
|
func (s *vpnSession) touch() { s.mu.Lock(); s.touchLocked(); s.mu.Unlock() }
|
|
|
|
func (s *vpnSession) enqueue(packet []byte) bool {
|
|
if len(packet) == 0 || len(packet) > 65535 {
|
|
return false
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.closed {
|
|
return false
|
|
}
|
|
if len(s.packets) >= s.maxPackets {
|
|
if s.manager.debug != nil {
|
|
s.manager.debug.dropped.Add(1)
|
|
}
|
|
return false
|
|
}
|
|
seq := s.nextDown
|
|
s.nextDown++
|
|
s.packets[seq] = append([]byte(nil), packet...)
|
|
s.touchLocked()
|
|
s.signalLocked()
|
|
if s.manager.debug != nil {
|
|
s.manager.debug.downPackets.Add(1)
|
|
s.manager.debug.downBytes.Add(uint64(len(packet)))
|
|
s.manager.debug.packetf("QUEUE sid=%s seq=%d bytes=%d", shortSID(s.sid), seq, len(packet))
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *vpnSession) push(seq uint32, offset, total int, data []byte) (int, error) {
|
|
s.upMu.Lock()
|
|
defer s.upMu.Unlock()
|
|
if total < 1 || total > 65535 || len(data) < 1 || len(data) > s.maxChunk || offset < 0 || offset+len(data) > total {
|
|
return 0, errors.New("invalid packet fragment")
|
|
}
|
|
|
|
if s.haveLastComplete && seq == s.lastComplete {
|
|
s.touch()
|
|
return s.lastCompleteTotal, nil
|
|
}
|
|
if seq < s.expectedUp {
|
|
return 0, fmt.Errorf("old upload sequence %d", seq)
|
|
}
|
|
if seq > s.expectedUp {
|
|
return 0, fmt.Errorf("upload sequence %d expected %d", seq, s.expectedUp)
|
|
}
|
|
|
|
if !s.haveCurrent {
|
|
if offset != 0 {
|
|
return 0, errors.New("first fragment offset must be zero")
|
|
}
|
|
s.haveCurrent = true
|
|
s.currentSeq = seq
|
|
s.currentTotal = total
|
|
s.currentBuf = make([]byte, 0, total)
|
|
}
|
|
if s.currentSeq != seq || s.currentTotal != total {
|
|
return 0, errors.New("packet fragment metadata changed")
|
|
}
|
|
|
|
// Idempotent retry: if this exact offset was already accepted, acknowledge
|
|
// the existing bytes instead of appending duplicate data.
|
|
if offset < len(s.currentBuf) {
|
|
end := offset + len(data)
|
|
if end <= len(s.currentBuf) && string(s.currentBuf[offset:end]) == string(data) {
|
|
return len(s.currentBuf), nil
|
|
}
|
|
return 0, errors.New("retry fragment does not match accepted data")
|
|
}
|
|
if offset != len(s.currentBuf) {
|
|
return 0, fmt.Errorf("fragment offset %d expected %d", offset, len(s.currentBuf))
|
|
}
|
|
|
|
s.currentBuf = append(s.currentBuf, data...)
|
|
accepted := len(s.currentBuf)
|
|
if accepted < total {
|
|
s.touch()
|
|
return accepted, nil
|
|
}
|
|
|
|
packet := append([]byte(nil), s.currentBuf...)
|
|
s.haveCurrent = false
|
|
s.currentBuf = nil
|
|
|
|
if err := s.manager.acceptClientPacket(s, packet); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
s.lastComplete = seq
|
|
s.lastCompleteTotal = total
|
|
s.haveLastComplete = true
|
|
s.expectedUp++
|
|
s.touch()
|
|
if s.manager.debug != nil {
|
|
s.manager.debug.upPackets.Add(1)
|
|
s.manager.debug.upBytes.Add(uint64(len(packet)))
|
|
s.manager.debug.packetf("UP sid=%s seq=%d bytes=%d", shortSID(s.sid), seq, len(packet))
|
|
}
|
|
return accepted, nil
|
|
}
|
|
|
|
func (s *vpnSession) pull(ack, want uint32, offset, limit int, wait time.Duration) ([]byte, int, bool, error) {
|
|
if offset < 0 || limit < 1 || limit > s.maxChunk {
|
|
return nil, 0, false, errors.New("invalid pull")
|
|
}
|
|
timer := time.NewTimer(wait)
|
|
defer timer.Stop()
|
|
for {
|
|
s.mu.Lock()
|
|
s.touchLocked()
|
|
if ack != protocol.VPNNoAck {
|
|
for seq := range s.packets {
|
|
if seq <= ack {
|
|
delete(s.packets, seq)
|
|
}
|
|
}
|
|
}
|
|
if packet, ok := s.packets[want]; ok {
|
|
if offset >= len(packet) {
|
|
s.mu.Unlock()
|
|
return nil, len(packet), false, errors.New("pull offset beyond packet")
|
|
}
|
|
end := offset + limit
|
|
if end > len(packet) {
|
|
end = len(packet)
|
|
}
|
|
out := append([]byte(nil), packet[offset:end]...)
|
|
total := len(packet)
|
|
s.mu.Unlock()
|
|
return out, total, false, nil
|
|
}
|
|
if s.closed {
|
|
s.mu.Unlock()
|
|
return nil, 0, false, net.ErrClosed
|
|
}
|
|
ch := s.notify
|
|
s.mu.Unlock()
|
|
select {
|
|
case <-ch:
|
|
case <-timer.C:
|
|
return nil, 0, true, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *vpnSession) close() {
|
|
s.mu.Lock()
|
|
if !s.closed {
|
|
s.closed = true
|
|
s.signalLocked()
|
|
}
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
type vpnManager struct {
|
|
mu sync.RWMutex
|
|
sessions map[protocol.VPNSessionID]*vpnSession
|
|
byIPv4 map[netip.Addr]*vpnSession
|
|
byIPv6 map[netip.Addr]*vpnSession
|
|
maxChunk int
|
|
maxPackets int
|
|
pollWait time.Duration
|
|
timeout time.Duration
|
|
tun *os.File
|
|
tunWriteMu sync.Mutex
|
|
mockEcho bool
|
|
allowPrivate bool
|
|
debug *debugStats
|
|
v4Prefix netip.Prefix
|
|
v6Prefix netip.Prefix
|
|
}
|
|
|
|
func newVPNManager(tun *os.File, mockEcho bool, maxChunk, maxPackets int, pollWait, timeout time.Duration, allowPrivate bool, debug *debugStats) *vpnManager {
|
|
v4p := netip.MustParsePrefix(defaultVPNv4Prefix)
|
|
v6p := netip.MustParsePrefix(defaultVPNv6Prefix)
|
|
m := &vpnManager{
|
|
sessions: make(map[protocol.VPNSessionID]*vpnSession), byIPv4: make(map[netip.Addr]*vpnSession), byIPv6: make(map[netip.Addr]*vpnSession),
|
|
maxChunk: maxChunk, maxPackets: maxPackets, pollWait: pollWait, timeout: timeout, tun: tun, mockEcho: mockEcho, allowPrivate: allowPrivate, debug: debug,
|
|
v4Prefix: v4p, v6Prefix: v6p,
|
|
}
|
|
if tun != nil {
|
|
go m.tunReadLoop()
|
|
}
|
|
go m.cleanupLoop()
|
|
return m
|
|
}
|
|
|
|
func (m *vpnManager) addOrGet(sid protocol.VPNSessionID, v4, v6 netip.Addr, mtu int) (*vpnSession, error) {
|
|
if !m.v4Prefix.Contains(v4) || v4 == netip.MustParseAddr("10.123.0.1") {
|
|
return nil, errors.New("client IPv4 outside DragonTCP subnet")
|
|
}
|
|
if !m.v6Prefix.Contains(v6) || v6 == netip.MustParseAddr("fd7a:4472:6167:6f6e::1") {
|
|
return nil, errors.New("client IPv6 outside DragonTCP subnet")
|
|
}
|
|
if mtu < 576 || mtu > 9000 {
|
|
return nil, errors.New("invalid client MTU")
|
|
}
|
|
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if old := m.sessions[sid]; old != nil {
|
|
if old.ipv4 != v4 || old.ipv6 != v6 {
|
|
return nil, errors.New("session address mismatch")
|
|
}
|
|
old.touch()
|
|
return old, nil
|
|
}
|
|
if m.byIPv4[v4] != nil || m.byIPv6[v6] != nil {
|
|
return nil, errors.New("client VPN address already in use")
|
|
}
|
|
s := newVPNSession(m, sid, v4, v6, mtu, m.maxChunk, m.maxPackets)
|
|
m.sessions[sid] = s
|
|
m.byIPv4[v4] = s
|
|
m.byIPv6[v6] = s
|
|
if m.debug != nil {
|
|
m.debug.activeSessions.Add(1)
|
|
m.debug.logf("SESSION OPEN sid=%s ipv4=%s ipv6=%s mtu=%d", shortSID(sid), v4, v6, mtu)
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func (m *vpnManager) get(sid protocol.VPNSessionID) *vpnSession {
|
|
m.mu.RLock()
|
|
s := m.sessions[sid]
|
|
m.mu.RUnlock()
|
|
return s
|
|
}
|
|
func (m *vpnManager) remove(sid protocol.VPNSessionID) {
|
|
m.mu.Lock()
|
|
s := m.sessions[sid]
|
|
if s != nil {
|
|
delete(m.sessions, sid)
|
|
delete(m.byIPv4, s.ipv4)
|
|
delete(m.byIPv6, s.ipv6)
|
|
}
|
|
m.mu.Unlock()
|
|
if s != nil {
|
|
s.close()
|
|
if m.debug != nil {
|
|
m.debug.activeSessions.Add(-1)
|
|
m.debug.logf("SESSION CLOSE sid=%s", shortSID(sid))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *vpnManager) cleanupLoop() {
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
cutoff := time.Now().Add(-m.timeout)
|
|
var stale []protocol.VPNSessionID
|
|
m.mu.RLock()
|
|
for sid, s := range m.sessions {
|
|
s.mu.Lock()
|
|
last := s.lastSeen
|
|
closed := s.closed
|
|
s.mu.Unlock()
|
|
if closed || last.Before(cutoff) {
|
|
stale = append(stale, sid)
|
|
}
|
|
}
|
|
m.mu.RUnlock()
|
|
for _, sid := range stale {
|
|
m.remove(sid)
|
|
}
|
|
}
|
|
}
|
|
|
|
func packetAddresses(packet []byte) (src, dst netip.Addr, err error) {
|
|
if len(packet) < 1 {
|
|
return src, dst, errors.New("empty IP packet")
|
|
}
|
|
switch packet[0] >> 4 {
|
|
case 4:
|
|
if len(packet) < 20 {
|
|
return src, dst, errors.New("short IPv4 packet")
|
|
}
|
|
total := int(packet[2])<<8 | int(packet[3])
|
|
if total < 20 || total > len(packet) {
|
|
return src, dst, errors.New("invalid IPv4 total length")
|
|
}
|
|
var a, b [4]byte
|
|
copy(a[:], packet[12:16])
|
|
copy(b[:], packet[16:20])
|
|
return netip.AddrFrom4(a), netip.AddrFrom4(b), nil
|
|
case 6:
|
|
if len(packet) < 40 {
|
|
return src, dst, errors.New("short IPv6 packet")
|
|
}
|
|
total := 40 + (int(packet[4])<<8 | int(packet[5]))
|
|
if total > len(packet) {
|
|
return src, dst, errors.New("invalid IPv6 payload length")
|
|
}
|
|
var a, b [16]byte
|
|
copy(a[:], packet[8:24])
|
|
copy(b[:], packet[24:40])
|
|
return netip.AddrFrom16(a), netip.AddrFrom16(b), nil
|
|
default:
|
|
return src, dst, errors.New("unsupported IP version")
|
|
}
|
|
}
|
|
|
|
func destinationAllowed(dst netip.Addr, allowPrivate bool) bool {
|
|
if dst.IsUnspecified() || dst.IsMulticast() {
|
|
return false
|
|
}
|
|
if allowPrivate {
|
|
return true
|
|
}
|
|
if dst.IsLoopback() || dst.IsLinkLocalUnicast() || dst.IsPrivate() {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (m *vpnManager) acceptClientPacket(s *vpnSession, packet []byte) error {
|
|
src, dst, err := packetAddresses(packet)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if src != s.ipv4 && src != s.ipv6 {
|
|
return fmt.Errorf("source %s does not match session address", src)
|
|
}
|
|
if !destinationAllowed(dst, m.allowPrivate) {
|
|
return fmt.Errorf("destination %s is blocked; use --allow-private to permit it", dst)
|
|
}
|
|
if m.mockEcho {
|
|
s.enqueue(packet)
|
|
return nil
|
|
}
|
|
if m.tun == nil {
|
|
return errors.New("VPN TUN is unavailable")
|
|
}
|
|
m.tunWriteMu.Lock()
|
|
n, err := m.tun.Write(packet)
|
|
m.tunWriteMu.Unlock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n != len(packet) {
|
|
return io.ErrShortWrite
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *vpnManager) tunReadLoop() {
|
|
buf := make([]byte, 65535)
|
|
for {
|
|
n, err := m.tun.Read(buf)
|
|
if err != nil {
|
|
if m.debug != nil {
|
|
m.debug.errorf("TUN read: %v", err)
|
|
}
|
|
return
|
|
}
|
|
if n < 1 {
|
|
continue
|
|
}
|
|
packet := append([]byte(nil), buf[:n]...)
|
|
_, dst, e := packetAddresses(packet)
|
|
if e != nil {
|
|
continue
|
|
}
|
|
m.mu.RLock()
|
|
var s *vpnSession
|
|
if dst.Is4() {
|
|
s = m.byIPv4[dst]
|
|
} else {
|
|
s = m.byIPv6[dst]
|
|
}
|
|
m.mu.RUnlock()
|
|
if s != nil {
|
|
s.enqueue(packet)
|
|
}
|
|
}
|
|
}
|
|
|
|
func shortSID(sid protocol.VPNSessionID) string { return hex.EncodeToString(sid[:4]) }
|
|
|
|
func processVPN(conn net.Conn, requestID uint32, payload []byte, token string, m *vpnManager) error {
|
|
switch payload[0] {
|
|
case protocol.VPNCmdOpen:
|
|
sid, tok, v4, v6, mtu, err := protocol.ParseVPNOpen(payload)
|
|
if err != nil {
|
|
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError(err.Error()))
|
|
}
|
|
if !tokenEqual(tok, token) {
|
|
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError("authentication failed"))
|
|
}
|
|
_, err = m.addOrGet(sid, v4, v6, mtu)
|
|
if err != nil {
|
|
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError(err.Error()))
|
|
}
|
|
return protocol.WriteResponseFrame(conn, requestID, protocol.BuildVPNOpened(m.maxChunk))
|
|
case protocol.VPNCmdPush:
|
|
sid, seq, offset, total, data, err := protocol.ParseVPNPush(payload)
|
|
if err != nil {
|
|
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError(err.Error()))
|
|
}
|
|
s := m.get(sid)
|
|
if s == nil {
|
|
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError("unknown VPN session"))
|
|
}
|
|
accepted, err := s.push(seq, offset, total, data)
|
|
if err != nil {
|
|
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError(err.Error()))
|
|
}
|
|
return protocol.WriteResponseFrame(conn, requestID, protocol.BuildVPNAck(seq, accepted))
|
|
case protocol.VPNCmdPull:
|
|
sid, ack, want, offset, limit, err := protocol.ParseVPNPull(payload)
|
|
if err != nil {
|
|
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError(err.Error()))
|
|
}
|
|
s := m.get(sid)
|
|
if s == nil {
|
|
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError("unknown VPN session"))
|
|
}
|
|
if limit > s.maxChunk {
|
|
limit = s.maxChunk
|
|
}
|
|
data, total, wait, err := s.pull(ack, want, offset, limit, m.pollWait)
|
|
if err != nil {
|
|
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError(err.Error()))
|
|
}
|
|
if wait {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte{protocol.VPNRespWait})
|
|
}
|
|
m.debug.packetf("DOWN sid=%s seq=%d offset=%d bytes=%d total=%d", shortSID(sid), want, offset, len(data), total)
|
|
return protocol.WriteResponseFrame(conn, requestID, protocol.BuildVPNData(want, offset, total, data))
|
|
case protocol.VPNCmdClose:
|
|
sid, err := protocol.ParseVPNClose(payload)
|
|
if err != nil {
|
|
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError(err.Error()))
|
|
}
|
|
m.remove(sid)
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte{protocol.VPNRespClosed})
|
|
default:
|
|
return protocol.WriteResponseFrame(conn, requestID, protocol.VPNError("unknown VPN command"))
|
|
}
|
|
}
|
|
|
|
func handleConn(conn net.Conn, token string, m *vpnManager, slots chan struct{}, debug *debugStats) {
|
|
defer func() { <-slots; debug.activeConns.Add(-1); _ = conn.Close() }()
|
|
protocol.TuneTCP(conn)
|
|
for {
|
|
_ = conn.SetDeadline(time.Now().Add(30 * time.Second))
|
|
requestID, _, payload, err := protocol.ReadRequestFrame(conn)
|
|
if err != nil {
|
|
if !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) {
|
|
debug.errorf("peer=%v read: %v", conn.RemoteAddr(), err)
|
|
}
|
|
return
|
|
}
|
|
if !protocol.IsVPNCommand(payload) {
|
|
_ = protocol.WriteResponseFrame(conn, requestID, protocol.VPNError("this binary accepts DragonTCP VPN packet commands only"))
|
|
continue
|
|
}
|
|
if err := processVPN(conn, requestID, payload, token, m); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Linux TUN setup.
|
|
type ifreq struct {
|
|
Name [16]byte
|
|
Flags uint16
|
|
_ [22]byte
|
|
}
|
|
|
|
const tunSetIFF = 0x400454ca
|
|
const iffTun = 0x0001
|
|
const iffNoPI = 0x1000
|
|
|
|
func openTun(name string) (*os.File, error) {
|
|
fd, err := syscall.Open("/dev/net/tun", syscall.O_RDWR|syscall.O_CLOEXEC, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var req ifreq
|
|
copy(req.Name[:], []byte(name))
|
|
req.Flags = iffTun | iffNoPI
|
|
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(tunSetIFF), uintptr(unsafe.Pointer(&req)))
|
|
if errno != 0 {
|
|
syscall.Close(fd)
|
|
return nil, errno
|
|
}
|
|
return os.NewFile(uintptr(fd), name), nil
|
|
}
|
|
|
|
func run(cmd string, args ...string) error {
|
|
c := exec.Command(cmd, args...)
|
|
out, err := c.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("%s %s: %v: %s", cmd, strings.Join(args, " "), err, strings.TrimSpace(string(out)))
|
|
}
|
|
return nil
|
|
}
|
|
func runOptional(debug *debugStats, cmd string, args ...string) {
|
|
if err := run(cmd, args...); err != nil {
|
|
debug.logf("optional command failed: %v", err)
|
|
}
|
|
}
|
|
func ensureRule(debug *debugStats, binary string, argsCheck, argsAdd []string) {
|
|
if err := exec.Command(binary, argsCheck...).Run(); err == nil {
|
|
return
|
|
}
|
|
if err := run(binary, argsAdd...); err != nil {
|
|
debug.logf("NAT rule warning: %v", err)
|
|
}
|
|
}
|
|
func setupLinuxVPN(tunName string, mtu int, autoNAT bool, debug *debugStats) (*os.File, error) {
|
|
tun, err := openTun(tunName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open /dev/net/tun: %w", err)
|
|
}
|
|
fail := func(e error) (*os.File, error) { tun.Close(); return nil, e }
|
|
if err := run("ip", "link", "set", "dev", tunName, "mtu", strconv.Itoa(mtu)); err != nil {
|
|
return fail(err)
|
|
}
|
|
if err := run("ip", "addr", "replace", "10.123.0.1/16", "dev", tunName); err != nil {
|
|
return fail(err)
|
|
}
|
|
// IPv6 may be disabled on some hosts; report clearly instead of silently bypassing it.
|
|
if err := run("ip", "-6", "addr", "replace", "fd7a:4472:6167:6f6e::1/64", "dev", tunName); err != nil {
|
|
return fail(err)
|
|
}
|
|
if err := run("ip", "link", "set", "dev", tunName, "up"); err != nil {
|
|
return fail(err)
|
|
}
|
|
if err := os.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte("1\n"), 0644); err != nil {
|
|
return fail(fmt.Errorf("enable IPv4 forwarding: %w", err))
|
|
}
|
|
if err := os.WriteFile("/proc/sys/net/ipv6/conf/all/forwarding", []byte("1\n"), 0644); err != nil {
|
|
return fail(fmt.Errorf("enable IPv6 forwarding: %w", err))
|
|
}
|
|
if autoNAT {
|
|
if _, err := exec.LookPath("iptables"); err != nil {
|
|
return fail(errors.New("iptables not found; install iptables or start with --auto-nat=false and configure NAT yourself"))
|
|
}
|
|
ensureRule(debug, "iptables", []string{"-t", "nat", "-C", "POSTROUTING", "-s", "10.123.0.0/16", "-j", "MASQUERADE"}, []string{"-t", "nat", "-A", "POSTROUTING", "-s", "10.123.0.0/16", "-j", "MASQUERADE"})
|
|
ensureRule(debug, "iptables", []string{"-C", "FORWARD", "-i", tunName, "-j", "ACCEPT"}, []string{"-A", "FORWARD", "-i", tunName, "-j", "ACCEPT"})
|
|
ensureRule(debug, "iptables", []string{"-C", "FORWARD", "-o", tunName, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"}, []string{"-A", "FORWARD", "-o", tunName, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"})
|
|
if _, err := exec.LookPath("ip6tables"); err == nil {
|
|
ensureRule(debug, "ip6tables", []string{"-t", "nat", "-C", "POSTROUTING", "-s", "fd7a:4472:6167:6f6e::/64", "-j", "MASQUERADE"}, []string{"-t", "nat", "-A", "POSTROUTING", "-s", "fd7a:4472:6167:6f6e::/64", "-j", "MASQUERADE"})
|
|
ensureRule(debug, "ip6tables", []string{"-C", "FORWARD", "-i", tunName, "-j", "ACCEPT"}, []string{"-A", "FORWARD", "-i", tunName, "-j", "ACCEPT"})
|
|
ensureRule(debug, "ip6tables", []string{"-C", "FORWARD", "-o", tunName, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"}, []string{"-A", "FORWARD", "-o", tunName, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"})
|
|
} else {
|
|
debug.logf("WARNING: ip6tables not found; IPv6 Internet access needs manual routing/NAT")
|
|
}
|
|
}
|
|
return tun, nil
|
|
}
|
|
|
|
func main() {
|
|
host := flag.String("host", "0.0.0.0", "listen host")
|
|
port := flag.Int("port", 53, "listen TCP port")
|
|
token := flag.String("token", "change-this-token", "shared token")
|
|
maxConnections := flag.Int("max-connections", 20000, "maximum simultaneous TCP/53 connections")
|
|
maxChunk := flag.Int("chunk-max", 65535, "maximum VPN fragment payload bytes (32-65535)")
|
|
maxPackets := flag.Int("vpn-buffered-packets", 2048, "maximum queued return IP packets per client")
|
|
pollWait := flag.Duration("poll-wait", 100*time.Millisecond, "long-poll wait for a return packet")
|
|
sessionTimeout := flag.Duration("session-timeout", 5*time.Minute, "idle VPN session timeout")
|
|
tunName := flag.String("tun", "dragontcp0", "Linux TUN interface name")
|
|
mtu := flag.Int("mtu", 1280, "server TUN MTU")
|
|
autoNAT := flag.Bool("auto-nat", true, "configure IPv4/IPv6 forwarding and iptables MASQUERADE")
|
|
allowPrivate := flag.Bool("allow-private", false, "allow VPN clients to access private/link-local destinations")
|
|
mockEcho := flag.Bool("mock-echo", false, "test mode: echo client IP packets back instead of using Linux TUN/NAT")
|
|
debugOn := flag.Bool("debug", false, "debug sessions and statistics")
|
|
debugPackets := flag.Bool("debug-packets", false, "very verbose per-IP-packet logging")
|
|
statsEvery := flag.Duration("debug-stats-interval", 10*time.Second, "debug statistics interval; 0 disables")
|
|
flag.Parse()
|
|
if *maxChunk < 32 || *maxChunk > protocol.VPNMaxFragment {
|
|
fmt.Fprintf(os.Stderr, "--chunk-max must be 32-%d\n", protocol.VPNMaxFragment)
|
|
os.Exit(2)
|
|
}
|
|
if *mtu < 576 || *mtu > 9000 {
|
|
fmt.Fprintln(os.Stderr, "--mtu must be 576-9000")
|
|
os.Exit(2)
|
|
}
|
|
debug := &debugStats{enabled: *debugOn, packets: *debugPackets, started: time.Now()}
|
|
var tun *os.File
|
|
var err error
|
|
if !*mockEcho {
|
|
tun, err = setupLinuxVPN(*tunName, *mtu, *autoNAT, debug)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "VPN setup failed:", err)
|
|
os.Exit(1)
|
|
}
|
|
defer tun.Close()
|
|
}
|
|
manager := newVPNManager(tun, *mockEcho, *maxChunk, *maxPackets, *pollWait, *sessionTimeout, *allowPrivate, debug)
|
|
addr := net.JoinHostPort(*host, strconv.Itoa(*port))
|
|
ln, err := net.Listen("tcp", addr)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
defer ln.Close()
|
|
fmt.Printf("DragonTCP VPN server listening on %s\n", addr)
|
|
if *mockEcho {
|
|
fmt.Println("mode=mock-echo (no Internet forwarding)")
|
|
} else {
|
|
fmt.Printf("tun=%s mtu=%d IPv4=10.123.0.1/16 IPv6=fd7a:4472:6167:6f6e::1/64 auto_nat=%t\n", *tunName, *mtu, *autoNAT)
|
|
}
|
|
fmt.Printf("chunk_max=%d poll_wait=%s buffered_packets=%d\n", *maxChunk, pollWait.String(), *maxPackets)
|
|
if debug.enabled && *statsEvery > 0 {
|
|
go func() {
|
|
t := time.NewTicker(*statsEvery)
|
|
defer t.Stop()
|
|
for range t.C {
|
|
fmt.Printf("[DEBUG] STATS uptime=%s conns=%d sessions=%d up_packets=%d down_packets=%d up_bytes=%d down_bytes=%d dropped=%d errors=%d\n", time.Since(debug.started).Round(time.Second), debug.activeConns.Load(), debug.activeSessions.Load(), debug.upPackets.Load(), debug.downPackets.Load(), debug.upBytes.Load(), debug.downBytes.Load(), debug.dropped.Load(), debug.errors.Load())
|
|
}
|
|
}()
|
|
}
|
|
sig := make(chan os.Signal, 1)
|
|
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
|
go func() { <-sig; fmt.Println("Stopping DragonTCP VPN server..."); ln.Close() }()
|
|
slots := make(chan struct{}, *maxConnections)
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
break
|
|
}
|
|
select {
|
|
case slots <- struct{}{}:
|
|
debug.activeConns.Add(1)
|
|
go handleConn(conn, *token, manager, slots, debug)
|
|
default:
|
|
_ = conn.Close()
|
|
}
|
|
}
|
|
}
|