V7
This commit is contained in:
@@ -0,0 +1,761 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"dragontcp/internal/protocol"
|
||||
)
|
||||
|
||||
type chunkClientOptions struct {
|
||||
startSize int
|
||||
minSize int
|
||||
maxSize int
|
||||
adaptive bool
|
||||
adaptSuccesses int
|
||||
adaptLog bool
|
||||
pollers int
|
||||
reconnectEvery int
|
||||
pollDelay time.Duration
|
||||
txnTimeout time.Duration
|
||||
tcpBuffer int
|
||||
}
|
||||
|
||||
func wireToken(token string) string {
|
||||
if token == "" {
|
||||
return "-"
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
type adaptiveSizer struct {
|
||||
mu sync.Mutex
|
||||
name string
|
||||
current int
|
||||
min int
|
||||
max int
|
||||
adaptive bool
|
||||
adaptSuccesses int
|
||||
successes int
|
||||
good int
|
||||
bad int
|
||||
logChanges bool
|
||||
}
|
||||
|
||||
func newAdaptiveSizer(name string, opts chunkClientOptions) *adaptiveSizer {
|
||||
start := opts.startSize
|
||||
if start < opts.minSize {
|
||||
start = opts.minSize
|
||||
}
|
||||
if start > opts.maxSize {
|
||||
start = opts.maxSize
|
||||
}
|
||||
return &adaptiveSizer{
|
||||
name: name,
|
||||
current: start,
|
||||
min: opts.minSize,
|
||||
max: opts.maxSize,
|
||||
adaptive: opts.adaptive,
|
||||
adaptSuccesses: opts.adaptSuccesses,
|
||||
logChanges: opts.adaptLog,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *adaptiveSizer) Current() int {
|
||||
s.mu.Lock()
|
||||
n := s.current
|
||||
s.mu.Unlock()
|
||||
return n
|
||||
}
|
||||
|
||||
func (s *adaptiveSizer) Success(attempted int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if !s.adaptive || s.current >= s.max {
|
||||
return
|
||||
}
|
||||
// Ignore stale successes from records that were already in flight when
|
||||
// another worker changed the shared size.
|
||||
if attempted != s.current {
|
||||
return
|
||||
}
|
||||
|
||||
if attempted > s.good {
|
||||
s.good = attempted
|
||||
}
|
||||
s.successes++
|
||||
|
||||
growAfter := s.adaptSuccesses
|
||||
// When we have converged close to a known failure boundary, stay stable
|
||||
// longer before probing again. This also lets us discover later network
|
||||
// improvements without constantly oscillating around the boundary.
|
||||
if s.bad > 0 && s.bad-s.good <= 32 {
|
||||
growAfter *= 8
|
||||
}
|
||||
if s.successes < growAfter {
|
||||
return
|
||||
}
|
||||
s.successes = 0
|
||||
|
||||
old := s.current
|
||||
var next int
|
||||
if s.bad > old+1 {
|
||||
// Binary-search the gap between known-good and known-bad sizes.
|
||||
next = old + (s.bad-old)/2
|
||||
} else {
|
||||
// Either there is no known ceiling, or we have stayed stable long enough
|
||||
// at it to probe the network again in case conditions improved.
|
||||
if s.bad > 0 {
|
||||
s.bad = 0
|
||||
}
|
||||
step := old / 4
|
||||
if step < 32 {
|
||||
step = 32
|
||||
}
|
||||
next = old + step
|
||||
}
|
||||
|
||||
if next > s.max {
|
||||
next = s.max
|
||||
}
|
||||
if next <= old {
|
||||
return
|
||||
}
|
||||
s.current = next
|
||||
|
||||
if s.logChanges {
|
||||
fmt.Printf("adaptive %s chunk: %d -> %d after stable success\n", s.name, old, next)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *adaptiveSizer) Failure(attempted int) (old, next int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
old = s.current
|
||||
|
||||
if !s.adaptive {
|
||||
return old, old
|
||||
}
|
||||
// Multiple pollers can fail on the same oversized value at once. Only the
|
||||
// first failure for the current value is allowed to reduce it.
|
||||
if attempted != s.current {
|
||||
return old, old
|
||||
}
|
||||
s.successes = 0
|
||||
|
||||
if s.bad == 0 || attempted < s.bad {
|
||||
s.bad = attempted
|
||||
}
|
||||
|
||||
if s.good > 0 && s.good < attempted {
|
||||
// Return directly to the last size that was proven to work.
|
||||
next = s.good
|
||||
} else {
|
||||
// A previously-good value just failed, so conditions worsened. Forget
|
||||
// the old lower bound and use multiplicative decrease.
|
||||
s.good = 0
|
||||
next = attempted / 2
|
||||
}
|
||||
if next < s.min {
|
||||
next = s.min
|
||||
}
|
||||
if next >= attempted && attempted > s.min {
|
||||
next = attempted - 1
|
||||
}
|
||||
if next < s.min {
|
||||
next = s.min
|
||||
}
|
||||
s.current = next
|
||||
|
||||
if s.logChanges && next != old {
|
||||
fmt.Printf("adaptive %s chunk: %d -> %d after transport failure\n", s.name, old, next)
|
||||
}
|
||||
return old, next
|
||||
}
|
||||
|
||||
type txnLane struct {
|
||||
mu sync.Mutex
|
||||
serverAddr string
|
||||
tcpBuffer int
|
||||
reconnectEvery int
|
||||
timeout time.Duration
|
||||
conn net.Conn
|
||||
count int
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newTxnLane(serverAddr string, tcpBuffer, reconnectEvery int, timeout time.Duration) *txnLane {
|
||||
return &txnLane{
|
||||
serverAddr: serverAddr,
|
||||
tcpBuffer: tcpBuffer,
|
||||
reconnectEvery: reconnectEvery,
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *txnLane) closeLocked() {
|
||||
if l.conn != nil {
|
||||
_ = l.conn.Close()
|
||||
l.conn = nil
|
||||
}
|
||||
l.count = 0
|
||||
}
|
||||
|
||||
func (l *txnLane) Close() {
|
||||
l.mu.Lock()
|
||||
l.closed = true
|
||||
l.closeLocked()
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *txnLane) ensureConn() error {
|
||||
if l.closed {
|
||||
return net.ErrClosed
|
||||
}
|
||||
if l.conn != nil && (l.reconnectEvery <= 0 || l.count < l.reconnectEvery) {
|
||||
return nil
|
||||
}
|
||||
|
||||
l.closeLocked()
|
||||
d := net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}
|
||||
conn, err := d.Dial("tcp", l.serverAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
protocol.TuneTCP(conn)
|
||||
protocol.TuneTCPBuffer(conn, l.tcpBuffer)
|
||||
l.conn = conn
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do performs exactly one framed transaction. Higher layers decide whether a
|
||||
// failed data record should be retried at a smaller adaptive size.
|
||||
func (l *txnLane) Do(payload []byte) ([]byte, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
if err := l.ensureConn(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timeout := l.timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
_ = l.conn.SetDeadline(time.Now().Add(timeout))
|
||||
requestID := requestCounter.Add(1)
|
||||
|
||||
if err := protocol.WriteRequestFrame(l.conn, requestID, payload); err != nil {
|
||||
l.closeLocked()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseID, response, err := protocol.ReadResponseFrame(l.conn)
|
||||
if err != nil {
|
||||
l.closeLocked()
|
||||
return nil, err
|
||||
}
|
||||
if responseID != requestID {
|
||||
l.closeLocked()
|
||||
return nil, fmt.Errorf("request ID mismatch")
|
||||
}
|
||||
|
||||
l.count++
|
||||
_ = l.conn.SetDeadline(time.Time{})
|
||||
if l.reconnectEvery > 0 && l.count >= l.reconnectEvery {
|
||||
// For restrictive TCP/53 networks, reconnectEvery=1 must really mean
|
||||
// one request/response per TCP connection. Close immediately after
|
||||
// receiving the response rather than waiting for the next request.
|
||||
l.closeLocked()
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func doControl(lane *txnLane, payload []byte) ([]byte, error) {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 5; attempt++ {
|
||||
resp, err := lane.Do(payload)
|
||||
if err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
lastErr = err
|
||||
time.Sleep(time.Duration(attempt+1) * 40 * time.Millisecond)
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
type chunkResult struct {
|
||||
seq uint64
|
||||
data []byte
|
||||
final uint64
|
||||
eof bool
|
||||
err error
|
||||
}
|
||||
|
||||
type chunkConn struct {
|
||||
serverAddr string
|
||||
token string
|
||||
sid string
|
||||
opts chunkClientOptions
|
||||
|
||||
pushLane *txnLane
|
||||
pullLanes []*txnLane
|
||||
|
||||
upSizer *adaptiveSizer
|
||||
downSizer *adaptiveSizer
|
||||
serverMax int
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
once sync.Once
|
||||
|
||||
writeMu sync.Mutex
|
||||
upSeq uint64
|
||||
|
||||
claim atomic.Uint64
|
||||
ack atomic.Int64
|
||||
|
||||
results chan chunkResult
|
||||
workers sync.WaitGroup
|
||||
|
||||
readMu sync.Mutex
|
||||
pending map[uint64][]byte
|
||||
nextRead uint64
|
||||
current []byte
|
||||
currentSeq uint64
|
||||
finalKnown bool
|
||||
finalSeq uint64
|
||||
terminalErr error
|
||||
}
|
||||
|
||||
func randomSessionID() (string, error) {
|
||||
var b [16]byte
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b[:]), nil
|
||||
}
|
||||
|
||||
func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts chunkClientOptions) (net.Conn, error) {
|
||||
if opts.minSize < 32 {
|
||||
opts.minSize = 32
|
||||
}
|
||||
if opts.maxSize < opts.minSize {
|
||||
opts.maxSize = opts.minSize
|
||||
}
|
||||
if opts.maxSize > protocol.MaxChunkPayload {
|
||||
opts.maxSize = protocol.MaxChunkPayload
|
||||
}
|
||||
if opts.startSize < opts.minSize {
|
||||
opts.startSize = opts.minSize
|
||||
}
|
||||
if opts.startSize > opts.maxSize {
|
||||
opts.startSize = opts.maxSize
|
||||
}
|
||||
if opts.adaptSuccesses < 1 {
|
||||
opts.adaptSuccesses = 64
|
||||
}
|
||||
if opts.pollers < 1 {
|
||||
opts.pollers = 1
|
||||
}
|
||||
if opts.pollers > 128 {
|
||||
opts.pollers = 128
|
||||
}
|
||||
if opts.txnTimeout <= 0 {
|
||||
opts.txnTimeout = 5 * time.Second
|
||||
}
|
||||
|
||||
sid, err := randomSessionID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c := &chunkConn{
|
||||
serverAddr: serverAddr,
|
||||
token: token,
|
||||
sid: sid,
|
||||
opts: opts,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
results: make(chan chunkResult, opts.pollers*4),
|
||||
pending: make(map[uint64][]byte, opts.pollers*2),
|
||||
}
|
||||
c.ack.Store(-1)
|
||||
c.upSizer = newAdaptiveSizer("upload", opts)
|
||||
c.downSizer = newAdaptiveSizer("download", opts)
|
||||
|
||||
c.pushLane = newTxnLane(serverAddr, opts.tcpBuffer, opts.reconnectEvery, opts.txnTimeout)
|
||||
|
||||
openPayload := []byte(fmt.Sprintf(
|
||||
"COPEN %s %s %s %d",
|
||||
wireToken(token), sid, targetHost, targetPort,
|
||||
))
|
||||
resp, err := doControl(c.pushLane, openPayload)
|
||||
if err != nil {
|
||||
c.pushLane.Close()
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
fields := strings.Fields(string(resp))
|
||||
if len(fields) != 2 || fields[0] != "OPENED" {
|
||||
c.pushLane.Close()
|
||||
cancel()
|
||||
return nil, fmt.Errorf("%s", resp)
|
||||
}
|
||||
serverMax, err := strconv.Atoi(fields[1])
|
||||
if err != nil || serverMax < 32 {
|
||||
c.pushLane.Close()
|
||||
cancel()
|
||||
return nil, fmt.Errorf("bad OPENED response: %q", resp)
|
||||
}
|
||||
c.serverMax = serverMax
|
||||
if serverMax < c.opts.maxSize {
|
||||
c.opts.maxSize = serverMax
|
||||
c.upSizer.max = serverMax
|
||||
c.downSizer.max = serverMax
|
||||
if c.upSizer.current > serverMax {
|
||||
c.upSizer.current = serverMax
|
||||
}
|
||||
if c.downSizer.current > serverMax {
|
||||
c.downSizer.current = serverMax
|
||||
}
|
||||
}
|
||||
|
||||
c.pullLanes = make([]*txnLane, opts.pollers)
|
||||
for i := 0; i < opts.pollers; i++ {
|
||||
lane := newTxnLane(serverAddr, opts.tcpBuffer, opts.reconnectEvery, opts.txnTimeout)
|
||||
c.pullLanes[i] = lane
|
||||
c.workers.Add(1)
|
||||
go c.pullWorker(lane)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func parseDataResponse(resp []byte) (seq uint64, offset int, total int, data []byte, err error) {
|
||||
if len(resp) < 6 || string(resp[:5]) != "DATA " {
|
||||
return 0, 0, 0, nil, fmt.Errorf("not DATA")
|
||||
}
|
||||
|
||||
rest := resp[5:]
|
||||
fields := make([][]byte, 0, 3)
|
||||
start := 0
|
||||
for i := 0; i < len(rest) && len(fields) < 3; i++ {
|
||||
if rest[i] == ' ' {
|
||||
fields = append(fields, rest[start:i])
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
if len(fields) != 3 {
|
||||
return 0, 0, 0, nil, fmt.Errorf("bad DATA response")
|
||||
}
|
||||
|
||||
seq, err = strconv.ParseUint(string(fields[0]), 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, 0, nil, err
|
||||
}
|
||||
offset, err = strconv.Atoi(string(fields[1]))
|
||||
if err != nil || offset < 0 {
|
||||
return 0, 0, 0, nil, fmt.Errorf("bad DATA offset")
|
||||
}
|
||||
total, err = strconv.Atoi(string(fields[2]))
|
||||
if err != nil || total < 0 {
|
||||
return 0, 0, 0, nil, fmt.Errorf("bad DATA total")
|
||||
}
|
||||
|
||||
// start now points immediately after the third separator.
|
||||
return seq, offset, total, rest[start:], nil
|
||||
}
|
||||
|
||||
func (c *chunkConn) pullWorker(lane *txnLane) {
|
||||
defer c.workers.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
seq := c.claim.Add(1) - 1
|
||||
offset := 0
|
||||
var assembled []byte
|
||||
consecutiveMinFailures := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
limit := c.downSizer.Current()
|
||||
ack := c.ack.Load()
|
||||
payload := []byte(fmt.Sprintf(
|
||||
"CPULL %s %s %d %d %d %d",
|
||||
wireToken(c.token), c.sid, ack, seq, offset, limit,
|
||||
))
|
||||
|
||||
resp, err := lane.Do(payload)
|
||||
if err != nil {
|
||||
old, next := c.downSizer.Failure(limit)
|
||||
if next == old && next == c.opts.minSize {
|
||||
consecutiveMinFailures++
|
||||
} else {
|
||||
consecutiveMinFailures = 0
|
||||
}
|
||||
if consecutiveMinFailures >= 8 {
|
||||
select {
|
||||
case c.results <- chunkResult{seq: seq, err: fmt.Errorf("download failed at minimum chunk %d: %w", next, err)}:
|
||||
case <-c.ctx.Done():
|
||||
}
|
||||
return
|
||||
}
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
if string(resp) == "WAIT" {
|
||||
if c.opts.pollDelay > 0 {
|
||||
select {
|
||||
case <-time.After(c.opts.pollDelay):
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(string(resp), "ERR ") {
|
||||
select {
|
||||
case c.results <- chunkResult{seq: seq, err: fmt.Errorf("%s", resp)}:
|
||||
case <-c.ctx.Done():
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(string(resp), "EOF ") {
|
||||
n, err := strconv.ParseUint(strings.TrimSpace(string(resp[4:])), 10, 64)
|
||||
if err != nil {
|
||||
select {
|
||||
case c.results <- chunkResult{seq: seq, err: err}:
|
||||
case <-c.ctx.Done():
|
||||
}
|
||||
return
|
||||
}
|
||||
select {
|
||||
case c.results <- chunkResult{seq: seq, eof: true, final: n}:
|
||||
case <-c.ctx.Done():
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
gotSeq, gotOffset, total, fragment, err := parseDataResponse(resp)
|
||||
if err != nil {
|
||||
select {
|
||||
case c.results <- chunkResult{seq: seq, err: err}:
|
||||
case <-c.ctx.Done():
|
||||
}
|
||||
return
|
||||
}
|
||||
if gotSeq != seq || gotOffset != offset {
|
||||
select {
|
||||
case c.results <- chunkResult{seq: seq, err: fmt.Errorf("DATA position mismatch")}:
|
||||
case <-c.ctx.Done():
|
||||
}
|
||||
return
|
||||
}
|
||||
if total > c.serverMax || total < offset+len(fragment) || len(fragment) == 0 {
|
||||
select {
|
||||
case c.results <- chunkResult{seq: seq, err: fmt.Errorf("invalid DATA fragment size")}:
|
||||
case <-c.ctx.Done():
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if assembled == nil {
|
||||
assembled = make([]byte, 0, total)
|
||||
}
|
||||
assembled = append(assembled, fragment...)
|
||||
offset += len(fragment)
|
||||
consecutiveMinFailures = 0
|
||||
c.downSizer.Success(limit)
|
||||
|
||||
if offset == total {
|
||||
select {
|
||||
case c.results <- chunkResult{seq: seq, data: assembled}:
|
||||
case <-c.ctx.Done():
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *chunkConn) Read(p []byte) (int, error) {
|
||||
c.readMu.Lock()
|
||||
defer c.readMu.Unlock()
|
||||
|
||||
for {
|
||||
if len(c.current) > 0 {
|
||||
n := copy(p, c.current)
|
||||
c.current = c.current[n:]
|
||||
if len(c.current) == 0 {
|
||||
c.nextRead++
|
||||
c.ack.Store(int64(c.currentSeq))
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
if c.terminalErr != nil {
|
||||
return 0, c.terminalErr
|
||||
}
|
||||
|
||||
if c.finalKnown && c.nextRead >= c.finalSeq {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
if data, ok := c.pending[c.nextRead]; ok {
|
||||
delete(c.pending, c.nextRead)
|
||||
c.current = data
|
||||
c.currentSeq = c.nextRead
|
||||
continue
|
||||
}
|
||||
|
||||
result, ok := <-c.results
|
||||
if !ok {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if result.err != nil {
|
||||
c.terminalErr = result.err
|
||||
return 0, result.err
|
||||
}
|
||||
if result.eof {
|
||||
if !c.finalKnown || result.final < c.finalSeq {
|
||||
c.finalKnown = true
|
||||
c.finalSeq = result.final
|
||||
}
|
||||
continue
|
||||
}
|
||||
if result.seq < c.nextRead {
|
||||
continue
|
||||
}
|
||||
c.pending[result.seq] = result.data
|
||||
}
|
||||
}
|
||||
|
||||
func parseAck(resp []byte, expectedSeq uint64) (int, error) {
|
||||
fields := strings.Fields(string(resp))
|
||||
if len(fields) != 3 || fields[0] != "ACK" {
|
||||
return 0, fmt.Errorf("bad CPUSH response: %q", resp)
|
||||
}
|
||||
seq, err := strconv.ParseUint(fields[1], 10, 64)
|
||||
if err != nil || seq != expectedSeq {
|
||||
return 0, fmt.Errorf("bad CPUSH sequence: %q", resp)
|
||||
}
|
||||
n, err := strconv.Atoi(fields[2])
|
||||
if err != nil || n <= 0 {
|
||||
return 0, fmt.Errorf("bad CPUSH length: %q", resp)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *chunkConn) Write(p []byte) (int, error) {
|
||||
c.writeMu.Lock()
|
||||
defer c.writeMu.Unlock()
|
||||
|
||||
total := 0
|
||||
consecutiveMinFailures := 0
|
||||
|
||||
for len(p) > 0 {
|
||||
size := c.upSizer.Current()
|
||||
n := size
|
||||
if len(p) < n {
|
||||
n = len(p)
|
||||
}
|
||||
|
||||
seq := c.upSeq
|
||||
prefix := []byte(fmt.Sprintf("CPUSH %s %s %d ", wireToken(c.token), c.sid, seq))
|
||||
payload := make([]byte, len(prefix)+n)
|
||||
copy(payload, prefix)
|
||||
copy(payload[len(prefix):], p[:n])
|
||||
|
||||
resp, err := c.pushLane.Do(payload)
|
||||
if err != nil {
|
||||
old, next := c.upSizer.Failure(size)
|
||||
if next == old && next == c.opts.minSize {
|
||||
consecutiveMinFailures++
|
||||
} else {
|
||||
consecutiveMinFailures = 0
|
||||
}
|
||||
if consecutiveMinFailures >= 8 {
|
||||
return total, fmt.Errorf("upload failed at minimum chunk %d: %w", next, err)
|
||||
}
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(string(resp), "ERR ") {
|
||||
return total, fmt.Errorf("%s", resp)
|
||||
}
|
||||
|
||||
accepted, err := parseAck(resp, seq)
|
||||
if err != nil {
|
||||
return total, err
|
||||
}
|
||||
if accepted > len(p) {
|
||||
return total, fmt.Errorf("server ACK length %d exceeds pending write %d", accepted, len(p))
|
||||
}
|
||||
|
||||
c.upSeq++
|
||||
total += accepted
|
||||
p = p[accepted:]
|
||||
consecutiveMinFailures = 0
|
||||
c.upSizer.Success(size)
|
||||
}
|
||||
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (c *chunkConn) Close() error {
|
||||
c.once.Do(func() {
|
||||
c.cancel()
|
||||
|
||||
lane := newTxnLane(c.serverAddr, c.opts.tcpBuffer, 1, c.opts.txnTimeout)
|
||||
_, _ = doControl(lane, []byte(fmt.Sprintf("CCLOSE %s %s", wireToken(c.token), c.sid)))
|
||||
lane.Close()
|
||||
|
||||
if c.pushLane != nil {
|
||||
c.pushLane.Close()
|
||||
}
|
||||
for _, lane := range c.pullLanes {
|
||||
lane.Close()
|
||||
}
|
||||
c.workers.Wait()
|
||||
close(c.results)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *chunkConn) LocalAddr() net.Addr { return dummyAddr("dragontcp-chunk-local") }
|
||||
func (c *chunkConn) RemoteAddr() net.Addr { return dummyAddr("dragontcp-chunk-remote") }
|
||||
func (c *chunkConn) SetDeadline(time.Time) error { return nil }
|
||||
func (c *chunkConn) SetReadDeadline(time.Time) error { return nil }
|
||||
func (c *chunkConn) SetWriteDeadline(time.Time) error { return nil }
|
||||
|
||||
type dummyAddr string
|
||||
|
||||
func (d dummyAddr) Network() string { return "dragontcp-chunk" }
|
||||
func (d dummyAddr) String() string { return string(d) }
|
||||
@@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAdaptiveSizerRecoversFromMinimum(t *testing.T) {
|
||||
opts := chunkClientOptions{
|
||||
startSize: 64,
|
||||
minSize: 32,
|
||||
maxSize: 1024,
|
||||
adaptive: true,
|
||||
adaptSuccesses: 2,
|
||||
}
|
||||
s := newAdaptiveSizer("test", opts)
|
||||
_, next := s.Failure(64)
|
||||
if next != 32 {
|
||||
t.Fatalf("failure should reduce 64 -> 32, got %d", next)
|
||||
}
|
||||
|
||||
// When good=32 and bad=64 are adjacent at the controller's probing
|
||||
// granularity, it deliberately waits 8x longer before testing upward.
|
||||
for i := 0; i < 16; i++ {
|
||||
s.Success(32)
|
||||
}
|
||||
if got := s.Current(); got <= 32 {
|
||||
t.Fatalf("adaptive controller remained stuck at minimum: %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWireTokenAllowsEmptyToken(t *testing.T) {
|
||||
if got := wireToken(""); got != "-" {
|
||||
t.Fatalf("empty token wire representation = %q, want '-'", got)
|
||||
}
|
||||
if got := wireToken("secret"); got != "secret" {
|
||||
t.Fatalf("non-empty token changed: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"dragontcp/internal/protocol"
|
||||
)
|
||||
|
||||
const maxHeader = 128 * 1024
|
||||
|
||||
var requestCounter atomic.Uint32
|
||||
|
||||
func readHTTPHeaders(conn net.Conn) ([]byte, []byte, error) {
|
||||
buf := make([]byte, 0, 8192)
|
||||
tmp := make([]byte, 8192)
|
||||
|
||||
for {
|
||||
n, err := conn.Read(tmp)
|
||||
if n > 0 {
|
||||
buf = append(buf, tmp[:n]...)
|
||||
|
||||
if len(buf) > maxHeader {
|
||||
return nil, nil, fmt.Errorf("HTTP headers too large")
|
||||
}
|
||||
|
||||
if i := bytes.Index(buf, []byte("\r\n\r\n")); i >= 0 {
|
||||
end := i + 4
|
||||
return buf[:end], buf[end:], nil
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseHostPort(authority string, defaultPort int) (string, int, error) {
|
||||
authority = strings.TrimSpace(authority)
|
||||
|
||||
if host, portText, err := net.SplitHostPort(authority); err == nil {
|
||||
port, err := strconv.Atoi(portText)
|
||||
return host, port, err
|
||||
}
|
||||
|
||||
// Host without port.
|
||||
if strings.HasPrefix(authority, "[") && strings.HasSuffix(authority, "]") {
|
||||
return strings.Trim(authority, "[]"), defaultPort, nil
|
||||
}
|
||||
|
||||
if strings.Count(authority, ":") == 0 {
|
||||
return authority, defaultPort, nil
|
||||
}
|
||||
|
||||
// Bare IPv6.
|
||||
if ip := net.ParseIP(authority); ip != nil {
|
||||
return authority, defaultPort, nil
|
||||
}
|
||||
|
||||
return "", 0, fmt.Errorf("invalid authority: %s", authority)
|
||||
}
|
||||
|
||||
func rewritePlainHTTPRequest(header []byte) (string, int, []byte, error) {
|
||||
text := string(header)
|
||||
lines := strings.Split(text, "\r\n")
|
||||
if len(lines) == 0 {
|
||||
return "", 0, nil, fmt.Errorf("empty request")
|
||||
}
|
||||
|
||||
parts := strings.SplitN(lines[0], " ", 3)
|
||||
if len(parts) != 3 {
|
||||
return "", 0, nil, fmt.Errorf("invalid request line")
|
||||
}
|
||||
|
||||
method, target, version := parts[0], parts[1], parts[2]
|
||||
|
||||
var (
|
||||
hostHeader string
|
||||
headers []string
|
||||
)
|
||||
|
||||
for _, line := range lines[1:] {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
k, v, ok := strings.Cut(line, ":")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
lk := strings.ToLower(strings.TrimSpace(k))
|
||||
|
||||
if lk == "host" {
|
||||
hostHeader = strings.TrimSpace(v)
|
||||
}
|
||||
|
||||
if lk == "connection" ||
|
||||
lk == "proxy-connection" ||
|
||||
lk == "proxy-authorization" {
|
||||
continue
|
||||
}
|
||||
|
||||
headers = append(headers, k+": "+strings.TrimSpace(v))
|
||||
}
|
||||
|
||||
u, err := url.Parse(target)
|
||||
if err != nil {
|
||||
return "", 0, nil, err
|
||||
}
|
||||
|
||||
var host string
|
||||
var port int
|
||||
path := target
|
||||
|
||||
if u.Hostname() != "" {
|
||||
if strings.ToLower(u.Scheme) != "http" {
|
||||
return "", 0, nil, fmt.Errorf("unsupported plain HTTP scheme: %s", u.Scheme)
|
||||
}
|
||||
|
||||
host = u.Hostname()
|
||||
port = 80
|
||||
|
||||
if u.Port() != "" {
|
||||
port, err = strconv.Atoi(u.Port())
|
||||
if err != nil {
|
||||
return "", 0, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
path = u.EscapedPath()
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
if u.RawQuery != "" {
|
||||
path += "?" + u.RawQuery
|
||||
}
|
||||
} else {
|
||||
if hostHeader == "" {
|
||||
return "", 0, nil, fmt.Errorf("missing Host header")
|
||||
}
|
||||
|
||||
host, port, err = parseHostPort(hostHeader, 80)
|
||||
if err != nil {
|
||||
return "", 0, nil, err
|
||||
}
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
}
|
||||
|
||||
var out strings.Builder
|
||||
fmt.Fprintf(&out, "%s %s %s\r\n", method, path, version)
|
||||
|
||||
sawHost := false
|
||||
for _, h := range headers {
|
||||
if strings.HasPrefix(strings.ToLower(h), "host:") {
|
||||
sawHost = true
|
||||
}
|
||||
out.WriteString(h)
|
||||
out.WriteString("\r\n")
|
||||
}
|
||||
|
||||
if !sawHost {
|
||||
if port == 80 {
|
||||
fmt.Fprintf(&out, "Host: %s\r\n", host)
|
||||
} else {
|
||||
fmt.Fprintf(&out, "Host: %s\r\n", net.JoinHostPort(host, strconv.Itoa(port)))
|
||||
}
|
||||
}
|
||||
|
||||
out.WriteString("Connection: close\r\n\r\n")
|
||||
|
||||
return host, port, []byte(out.String()), nil
|
||||
}
|
||||
|
||||
func openDragonTCPTunnel(serverAddr, token, targetHost string, targetPort int, transport string, tcpBuffer int) (net.Conn, error) {
|
||||
d := net.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}
|
||||
|
||||
conn, err := d.Dial("tcp", serverAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
protocol.TuneTCP(conn)
|
||||
protocol.TuneTCPBuffer(conn, tcpBuffer)
|
||||
_ = conn.SetDeadline(time.Now().Add(15 * time.Second))
|
||||
|
||||
// Correlation only; cryptographic randomness is unnecessary here.
|
||||
requestID := requestCounter.Add(1)
|
||||
|
||||
var command []byte
|
||||
if transport == "raw" {
|
||||
command = []byte(fmt.Sprintf("TUNNEL2 %s %s %d RAW", token, targetHost, targetPort))
|
||||
} else {
|
||||
// Legacy XOR command remains compatible with the older server.
|
||||
command = []byte(fmt.Sprintf("TUNNEL %s %s %d", token, targetHost, targetPort))
|
||||
}
|
||||
|
||||
if err := protocol.WriteRequestFrame(conn, requestID, command); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseID, response, err := protocol.ReadResponseFrame(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if responseID != requestID {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("request ID mismatch")
|
||||
}
|
||||
|
||||
if string(response) != "CONNECTED" {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("%s", response)
|
||||
}
|
||||
|
||||
_ = conn.SetDeadline(time.Time{})
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func writeHTTPError(conn net.Conn, code int, reason, detail string) {
|
||||
if detail == "" {
|
||||
detail = reason
|
||||
}
|
||||
|
||||
body := []byte(detail)
|
||||
|
||||
fmt.Fprintf(
|
||||
conn,
|
||||
"HTTP/1.1 %d %s\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: %d\r\nConnection: close\r\n\r\n",
|
||||
code,
|
||||
reason,
|
||||
len(body),
|
||||
)
|
||||
_, _ = conn.Write(body)
|
||||
}
|
||||
|
||||
func handleLocal(conn net.Conn, serverAddr, token, transport string, tcpBuffer int, chunkOpts chunkClientOptions, slots chan struct{}) {
|
||||
defer func() {
|
||||
<-slots
|
||||
_ = conn.Close()
|
||||
}()
|
||||
|
||||
protocol.TuneTCP(conn)
|
||||
protocol.TuneTCPBuffer(conn, tcpBuffer)
|
||||
_ = conn.SetDeadline(time.Now().Add(15 * time.Second))
|
||||
|
||||
header, extra, err := readHTTPHeaders(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
firstLine := strings.SplitN(string(header), "\r\n", 2)[0]
|
||||
parts := strings.SplitN(firstLine, " ", 3)
|
||||
|
||||
if len(parts) != 3 {
|
||||
writeHTTPError(conn, 400, "Bad Request", "invalid HTTP request line")
|
||||
return
|
||||
}
|
||||
|
||||
method, target := parts[0], parts[1]
|
||||
|
||||
if strings.EqualFold(method, "CONNECT") {
|
||||
host, port, err := parseHostPort(target, 443)
|
||||
if err != nil {
|
||||
writeHTTPError(conn, 400, "Bad Request", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var remote net.Conn
|
||||
if transport == "chunk" {
|
||||
remote, err = openChunkTunnel(serverAddr, token, host, port, chunkOpts)
|
||||
} else {
|
||||
remote, err = openDragonTCPTunnel(serverAddr, token, host, port, transport, tcpBuffer)
|
||||
}
|
||||
if err != nil {
|
||||
writeHTTPError(conn, 502, "Bad Gateway", err.Error())
|
||||
return
|
||||
}
|
||||
defer remote.Close()
|
||||
|
||||
_, _ = conn.Write([]byte(
|
||||
"HTTP/1.1 200 Connection Established\r\n" +
|
||||
"Proxy-Agent: dragontcp-proxy/2.0\r\n\r\n",
|
||||
))
|
||||
|
||||
if len(extra) > 0 {
|
||||
if transport == "xor" {
|
||||
protocol.XorInPlace(extra)
|
||||
}
|
||||
if _, err := remote.Write(extra); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_ = conn.SetDeadline(time.Time{})
|
||||
if transport == "xor" {
|
||||
protocol.RelayXOR(conn, remote)
|
||||
} else {
|
||||
// raw and chunk connections expose a normal plaintext net.Conn.
|
||||
protocol.RelayRaw(conn, remote)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
host, port, rewritten, err := rewritePlainHTTPRequest(header)
|
||||
if err != nil {
|
||||
writeHTTPError(conn, 400, "Bad Request", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var remote net.Conn
|
||||
if transport == "chunk" {
|
||||
remote, err = openChunkTunnel(serverAddr, token, host, port, chunkOpts)
|
||||
} else {
|
||||
remote, err = openDragonTCPTunnel(serverAddr, token, host, port, transport, tcpBuffer)
|
||||
}
|
||||
if err != nil {
|
||||
writeHTTPError(conn, 502, "Bad Gateway", err.Error())
|
||||
return
|
||||
}
|
||||
defer remote.Close()
|
||||
|
||||
initial := make([]byte, 0, len(rewritten)+len(extra))
|
||||
initial = append(initial, rewritten...)
|
||||
initial = append(initial, extra...)
|
||||
if transport == "xor" {
|
||||
protocol.XorInPlace(initial)
|
||||
}
|
||||
|
||||
if _, err := remote.Write(initial); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_ = conn.SetDeadline(time.Time{})
|
||||
if transport == "xor" {
|
||||
protocol.RelayXOR(conn, remote)
|
||||
} else {
|
||||
protocol.RelayRaw(conn, remote)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
var (
|
||||
listenHost = flag.String("listen-host", "127.0.0.1", "local proxy listen host")
|
||||
listenPort = flag.Int("listen-port", 8080, "local proxy listen port")
|
||||
serverHost = flag.String("server-host", "", "remote DragonTCP server host")
|
||||
serverPort = flag.Int("server-port", 53, "remote DragonTCP server port")
|
||||
token = flag.String("token", "", "optional shared token")
|
||||
maxConnections = flag.Int("max-connections", 20000, "max simultaneous proxy connections")
|
||||
transport = flag.String("transport", "chunk", "transport: chunk (mandatory in LiteVPN build)")
|
||||
tcpBuffer = flag.Int("tcp-buffer", 0, "optional TCP read/write buffer bytes; 0 keeps OS autotuning")
|
||||
chunkStart = flag.Int("chunk-start", 1048576, "initial adaptive chunk payload bytes")
|
||||
chunkMin = flag.Int("chunk-min", 32, "minimum adaptive chunk payload bytes")
|
||||
chunkMax = flag.Int("chunk-max", 1048576, "maximum adaptive chunk payload bytes (up to 1 MiB)")
|
||||
chunkAdaptive = flag.Bool("chunk-adaptive", true, "automatically shrink on failures and grow after stable success")
|
||||
chunkSuccesses = flag.Int("chunk-grow-after", 16, "successful data records required before increasing chunk size")
|
||||
chunkAdaptLog = flag.Bool("chunk-adapt-log", true, "print adaptive chunk size changes")
|
||||
chunkSizeLegacy = flag.Int("chunk-size", 0, "legacy fixed chunk size; nonzero disables adaptation")
|
||||
chunkPollers = flag.Int("chunk-pollers", 1, "parallel downstream chunk pollers (LiteVPN default 1)")
|
||||
chunkReconnect = flag.Int("chunk-reconnect-every", 1, "reconnect each transaction lane after N requests; 1 = one request per TCP/53 connection")
|
||||
chunkPollDelay = flag.Duration("chunk-poll-delay", 2*time.Millisecond, "delay after an empty chunk poll")
|
||||
chunkTimeout = flag.Duration("chunk-timeout", 2*time.Second, "per-record transaction timeout before adaptive shrink")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *serverHost == "" {
|
||||
fmt.Fprintln(os.Stderr, "--server-host is required")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
*transport = strings.ToLower(*transport)
|
||||
if *transport != "chunk" {
|
||||
fmt.Fprintln(os.Stderr, "DragonTCP LiteVPN requires --transport chunk (adaptive XOR-framed TCP/53)")
|
||||
os.Exit(2)
|
||||
}
|
||||
if *chunkSizeLegacy != 0 {
|
||||
if *chunkSizeLegacy < 32 || *chunkSizeLegacy > protocol.MaxChunkPayload {
|
||||
fmt.Fprintf(os.Stderr, "--chunk-size must be between 32 and %d\n", protocol.MaxChunkPayload)
|
||||
os.Exit(2)
|
||||
}
|
||||
*chunkStart = *chunkSizeLegacy
|
||||
*chunkMin = *chunkSizeLegacy
|
||||
*chunkMax = *chunkSizeLegacy
|
||||
*chunkAdaptive = false
|
||||
}
|
||||
if *chunkMin < 32 || *chunkMax > protocol.MaxChunkPayload || *chunkMin > *chunkStart || *chunkStart > *chunkMax {
|
||||
fmt.Fprintf(os.Stderr, "require 32 <= --chunk-min <= --chunk-start <= --chunk-max <= %d\n", protocol.MaxChunkPayload)
|
||||
os.Exit(2)
|
||||
}
|
||||
if *chunkSuccesses < 1 {
|
||||
fmt.Fprintln(os.Stderr, "--chunk-grow-after must be at least 1")
|
||||
os.Exit(2)
|
||||
}
|
||||
if *chunkPollers < 1 || *chunkPollers > 128 {
|
||||
fmt.Fprintln(os.Stderr, "--chunk-pollers must be between 1 and 128")
|
||||
os.Exit(2)
|
||||
}
|
||||
chunkOpts := chunkClientOptions{
|
||||
startSize: *chunkStart,
|
||||
minSize: *chunkMin,
|
||||
maxSize: *chunkMax,
|
||||
adaptive: *chunkAdaptive,
|
||||
adaptSuccesses: *chunkSuccesses,
|
||||
adaptLog: *chunkAdaptLog,
|
||||
pollers: *chunkPollers,
|
||||
reconnectEvery: *chunkReconnect,
|
||||
pollDelay: *chunkPollDelay,
|
||||
txnTimeout: *chunkTimeout,
|
||||
tcpBuffer: *tcpBuffer,
|
||||
}
|
||||
|
||||
listenAddr := net.JoinHostPort(*listenHost, strconv.Itoa(*listenPort))
|
||||
serverAddr := net.JoinHostPort(*serverHost, strconv.Itoa(*serverPort))
|
||||
|
||||
ln, err := net.Listen("tcp", listenAddr)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
fmt.Printf("local Go HTTP proxy listening on %s\n", listenAddr)
|
||||
fmt.Printf("remote DragonTCP endpoint=%s\n", serverAddr)
|
||||
fmt.Printf("max_connections=%d transport=%s tcp_buffer=%d\n", *maxConnections, *transport, *tcpBuffer)
|
||||
if *transport == "chunk" {
|
||||
fmt.Printf(
|
||||
"adaptive_chunk=%v start=%d min=%d max=%d grow_after=%d pollers=%d reconnect_every=%d timeout=%s\n",
|
||||
*chunkAdaptive,
|
||||
*chunkStart,
|
||||
*chunkMin,
|
||||
*chunkMax,
|
||||
*chunkSuccesses,
|
||||
*chunkPollers,
|
||||
*chunkReconnect,
|
||||
chunkTimeout.String(),
|
||||
)
|
||||
}
|
||||
|
||||
slots := make(chan struct{}, *maxConnections)
|
||||
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "accept:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case slots <- struct{}{}:
|
||||
go handleLocal(conn, serverAddr, *token, *transport, *tcpBuffer, chunkOpts, slots)
|
||||
default:
|
||||
writeHTTPError(
|
||||
conn,
|
||||
503,
|
||||
"Service Unavailable",
|
||||
"proxy connection limit reached",
|
||||
)
|
||||
_ = conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"dragontcp/internal/protocol"
|
||||
)
|
||||
|
||||
type chunkSession struct {
|
||||
id string
|
||||
target net.Conn
|
||||
maxChunk int
|
||||
maxChunks int
|
||||
|
||||
mu sync.Mutex
|
||||
notify chan struct{}
|
||||
chunks map[uint64][]byte
|
||||
nextDown uint64
|
||||
eof bool
|
||||
closed bool
|
||||
lastSeen time.Time
|
||||
debug *serverDebug
|
||||
|
||||
upMu sync.Mutex
|
||||
expectedUp uint64
|
||||
lastUpSeq uint64
|
||||
lastUpLen int
|
||||
haveLastUp bool
|
||||
}
|
||||
|
||||
func newChunkSession(id string, target net.Conn, maxChunk, maxChunks int, debug *serverDebug) *chunkSession {
|
||||
s := &chunkSession{
|
||||
id: id,
|
||||
target: target,
|
||||
maxChunk: maxChunk,
|
||||
maxChunks: maxChunks,
|
||||
notify: make(chan struct{}),
|
||||
chunks: make(map[uint64][]byte, maxChunks),
|
||||
lastSeen: time.Now(),
|
||||
debug: debug,
|
||||
}
|
||||
go s.readTarget()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *chunkSession) signalLocked() {
|
||||
close(s.notify)
|
||||
s.notify = make(chan struct{})
|
||||
}
|
||||
|
||||
func (s *chunkSession) touchLocked() {
|
||||
s.lastSeen = time.Now()
|
||||
}
|
||||
|
||||
func (s *chunkSession) touch() {
|
||||
s.mu.Lock()
|
||||
s.touchLocked()
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *chunkSession) readTarget() {
|
||||
buf := make([]byte, s.maxChunk)
|
||||
|
||||
for {
|
||||
n, err := s.target.Read(buf)
|
||||
if n > 0 {
|
||||
data := append([]byte(nil), buf[:n]...)
|
||||
if s.debug != nil && s.debug.enabled {
|
||||
s.debug.bytesDown.Add(uint64(n))
|
||||
}
|
||||
|
||||
for {
|
||||
s.mu.Lock()
|
||||
if s.closed {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if len(s.chunks) < s.maxChunks {
|
||||
seq := s.nextDown
|
||||
s.nextDown++
|
||||
s.chunks[seq] = data
|
||||
s.touchLocked()
|
||||
s.signalLocked()
|
||||
s.mu.Unlock()
|
||||
break
|
||||
}
|
||||
ch := s.notify
|
||||
s.mu.Unlock()
|
||||
<-ch
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if s.debug != nil && s.debug.enabled {
|
||||
s.debug.logf("TARGET EOF session=%s err=%v", s.id, err)
|
||||
}
|
||||
s.mu.Lock()
|
||||
if !s.closed {
|
||||
s.eof = true
|
||||
s.touchLocked()
|
||||
s.signalLocked()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// push is idempotent for the most recently accepted sequence. This matters
|
||||
// when the server receives a record but the tiny ACK is lost: the client can
|
||||
// retry the same sequence at a smaller adaptive size without duplicating bytes
|
||||
// in the target stream. The ACK reports the length that was actually accepted.
|
||||
func (s *chunkSession) push(seq uint64, data []byte) (int, error) {
|
||||
s.upMu.Lock()
|
||||
defer s.upMu.Unlock()
|
||||
|
||||
if len(data) == 0 || len(data) > s.maxChunk {
|
||||
return 0, fmt.Errorf("upload record size %d is invalid", len(data))
|
||||
}
|
||||
|
||||
if s.haveLastUp && seq == s.lastUpSeq {
|
||||
s.touch()
|
||||
return s.lastUpLen, nil
|
||||
}
|
||||
|
||||
if seq < s.expectedUp {
|
||||
return 0, fmt.Errorf("upload sequence %d is too old", seq)
|
||||
}
|
||||
if seq > s.expectedUp {
|
||||
return 0, fmt.Errorf("unexpected upload sequence %d, expected %d", seq, s.expectedUp)
|
||||
}
|
||||
|
||||
if _, err := s.target.Write(data); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if s.debug != nil && s.debug.enabled {
|
||||
s.debug.bytesUp.Add(uint64(len(data)))
|
||||
s.debug.pushRecords.Add(1)
|
||||
}
|
||||
|
||||
s.lastUpSeq = seq
|
||||
s.lastUpLen = len(data)
|
||||
s.haveLastUp = true
|
||||
s.expectedUp++
|
||||
s.touch()
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
// pull returns at most limit bytes from the requested stored chunk, beginning
|
||||
// at offset. The chunk sequence stays stable while the client retries smaller
|
||||
// fragments, so a large queued chunk can always be recovered after an MTU-like
|
||||
// failure without reopening the proxied destination connection.
|
||||
func (s *chunkSession) pull(want uint64, ack int64, offset, limit int, wait time.Duration) (data []byte, total int, eof bool, final uint64, waitExpired bool, err error) {
|
||||
if offset < 0 || limit <= 0 || limit > s.maxChunk {
|
||||
return nil, 0, false, 0, false, fmt.Errorf("invalid pull offset/limit")
|
||||
}
|
||||
|
||||
timer := time.NewTimer(wait)
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
s.mu.Lock()
|
||||
s.touchLocked()
|
||||
|
||||
if ack >= 0 {
|
||||
removed := false
|
||||
for seq := range s.chunks {
|
||||
if seq <= uint64(ack) {
|
||||
delete(s.chunks, seq)
|
||||
removed = true
|
||||
}
|
||||
}
|
||||
if removed {
|
||||
s.signalLocked()
|
||||
}
|
||||
}
|
||||
|
||||
if chunk, ok := s.chunks[want]; ok {
|
||||
if offset >= len(chunk) {
|
||||
s.mu.Unlock()
|
||||
return nil, len(chunk), false, 0, false, fmt.Errorf("pull offset %d beyond chunk size %d", offset, len(chunk))
|
||||
}
|
||||
end := offset + limit
|
||||
if end > len(chunk) {
|
||||
end = len(chunk)
|
||||
}
|
||||
out := append([]byte(nil), chunk[offset:end]...)
|
||||
total = len(chunk)
|
||||
s.mu.Unlock()
|
||||
return out, total, false, 0, false, nil
|
||||
}
|
||||
|
||||
if s.eof && want >= s.nextDown {
|
||||
final = s.nextDown
|
||||
s.mu.Unlock()
|
||||
return nil, 0, true, final, false, nil
|
||||
}
|
||||
|
||||
if s.closed {
|
||||
final = s.nextDown
|
||||
s.mu.Unlock()
|
||||
return nil, 0, true, final, false, nil
|
||||
}
|
||||
|
||||
ch := s.notify
|
||||
s.mu.Unlock()
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
continue
|
||||
case <-timer.C:
|
||||
return nil, 0, false, 0, true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *chunkSession) close() {
|
||||
s.mu.Lock()
|
||||
if s.closed {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.closed = true
|
||||
s.signalLocked()
|
||||
s.mu.Unlock()
|
||||
_ = s.target.Close()
|
||||
}
|
||||
|
||||
type chunkManager struct {
|
||||
mu sync.RWMutex
|
||||
sessions map[string]*chunkSession
|
||||
timeout time.Duration
|
||||
debug *serverDebug
|
||||
}
|
||||
|
||||
func newChunkManager(timeout time.Duration, debug *serverDebug) *chunkManager {
|
||||
m := &chunkManager{
|
||||
sessions: make(map[string]*chunkSession),
|
||||
timeout: timeout,
|
||||
debug: debug,
|
||||
}
|
||||
go m.cleanupLoop()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *chunkManager) get(id string) *chunkSession {
|
||||
m.mu.RLock()
|
||||
s := m.sessions[id]
|
||||
m.mu.RUnlock()
|
||||
return s
|
||||
}
|
||||
|
||||
func (m *chunkManager) count() int {
|
||||
m.mu.RLock()
|
||||
n := len(m.sessions)
|
||||
m.mu.RUnlock()
|
||||
return n
|
||||
}
|
||||
|
||||
func (m *chunkManager) add(id string, s *chunkSession) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, exists := m.sessions[id]; exists {
|
||||
return fmt.Errorf("session already exists")
|
||||
}
|
||||
m.sessions[id] = s
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *chunkManager) remove(id string) {
|
||||
m.mu.Lock()
|
||||
s := m.sessions[id]
|
||||
delete(m.sessions, id)
|
||||
m.mu.Unlock()
|
||||
if s != nil {
|
||||
s.close()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *chunkManager) cleanupLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
cutoff := time.Now().Add(-m.timeout)
|
||||
var stale []string
|
||||
|
||||
m.mu.RLock()
|
||||
for id, s := range m.sessions {
|
||||
s.mu.Lock()
|
||||
last := s.lastSeen
|
||||
closed := s.closed
|
||||
s.mu.Unlock()
|
||||
if closed || last.Before(cutoff) {
|
||||
stale = append(stale, id)
|
||||
}
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
for _, id := range stale {
|
||||
if m.debug != nil && m.debug.enabled {
|
||||
m.debug.logf("SESSION timeout-close id=%s active_sessions=%d", id, m.count())
|
||||
}
|
||||
m.remove(id)
|
||||
if m.debug != nil && m.debug.enabled {
|
||||
m.debug.sessionsClosed.Add(1)
|
||||
m.debug.activeSessions.Add(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeWireToken(token string) string {
|
||||
if token == "-" {
|
||||
return ""
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func isChunkCommand(payload []byte) bool {
|
||||
return bytes.HasPrefix(payload, []byte("COPEN ")) ||
|
||||
bytes.HasPrefix(payload, []byte("CPUSH ")) ||
|
||||
bytes.HasPrefix(payload, []byte("CPULL ")) ||
|
||||
bytes.HasPrefix(payload, []byte("CCLOSE "))
|
||||
}
|
||||
|
||||
func processChunkCommand(
|
||||
conn net.Conn,
|
||||
requestID uint32,
|
||||
payload []byte,
|
||||
token string,
|
||||
allowPrivate bool,
|
||||
cache *dnsCache,
|
||||
tcpBuffer int,
|
||||
manager *chunkManager,
|
||||
maxChunk int,
|
||||
maxBufferedChunks int,
|
||||
pollWait time.Duration,
|
||||
debug *serverDebug,
|
||||
) error {
|
||||
if bytes.HasPrefix(payload, []byte("COPEN ")) {
|
||||
parts := strings.Fields(string(payload))
|
||||
if len(parts) != 5 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR bad COPEN"))
|
||||
}
|
||||
if !tokenEqual(decodeWireToken(parts[1]), token) {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
|
||||
}
|
||||
sid := parts[2]
|
||||
if len(sid) < 16 || len(sid) > 64 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid session id"))
|
||||
}
|
||||
host := parts[3]
|
||||
port, err := strconv.Atoi(parts[4])
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid port"))
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
target, err := dialTarget(ctx, host, port, allowPrivate, cache, tcpBuffer)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
|
||||
}
|
||||
|
||||
session := newChunkSession(sid, target, maxChunk, maxBufferedChunks, debug)
|
||||
if err := manager.add(sid, session); err != nil {
|
||||
session.close()
|
||||
if debug != nil && debug.enabled {
|
||||
debug.errorf("COPEN session=%s target=%s:%d failed: %v", sid, host, port, err)
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
|
||||
}
|
||||
if debug != nil && debug.enabled {
|
||||
debug.sessionsOpened.Add(1)
|
||||
debug.activeSessions.Add(1)
|
||||
debug.logf("SESSION OPEN id=%s peer=%v target=%s:%d max_chunk=%d active_sessions=%d", sid, conn.RemoteAddr(), host, port, maxChunk, manager.count())
|
||||
debug.chunkf("COPEN id=%s target=%s:%d -> OPENED max=%d", sid, host, port, maxChunk)
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte(fmt.Sprintf("OPENED %d", maxChunk)))
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(payload, []byte("CPUSH ")) {
|
||||
parts := bytes.SplitN(payload, []byte(" "), 5)
|
||||
if len(parts) != 5 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR bad CPUSH"))
|
||||
}
|
||||
if !tokenEqual(decodeWireToken(string(parts[1])), token) {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
|
||||
}
|
||||
sid := string(parts[2])
|
||||
seq, err := strconv.ParseUint(string(parts[3]), 10, 64)
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid sequence"))
|
||||
}
|
||||
s := manager.get(sid)
|
||||
if s == nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR unknown session"))
|
||||
}
|
||||
accepted, err := s.push(seq, parts[4])
|
||||
if err != nil {
|
||||
if debug != nil && debug.enabled {
|
||||
debug.errorf("CPUSH id=%s seq=%d bytes=%d: %v", sid, seq, len(parts[4]), err)
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
|
||||
}
|
||||
if debug != nil {
|
||||
debug.chunkf("CPUSH id=%s seq=%d bytes=%d -> ACK accepted=%d", sid, seq, len(parts[4]), accepted)
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte(fmt.Sprintf("ACK %d %d", seq, accepted)))
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(payload, []byte("CPULL ")) {
|
||||
parts := strings.Fields(string(payload))
|
||||
if len(parts) != 7 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR bad CPULL"))
|
||||
}
|
||||
if !tokenEqual(decodeWireToken(parts[1]), token) {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
|
||||
}
|
||||
s := manager.get(parts[2])
|
||||
if s == nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR unknown session"))
|
||||
}
|
||||
ack, err := strconv.ParseInt(parts[3], 10, 64)
|
||||
if err != nil || ack < -1 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid ack"))
|
||||
}
|
||||
want, err := strconv.ParseUint(parts[4], 10, 64)
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid want"))
|
||||
}
|
||||
offset, err := strconv.Atoi(parts[5])
|
||||
if err != nil || offset < 0 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid offset"))
|
||||
}
|
||||
limit, err := strconv.Atoi(parts[6])
|
||||
if err != nil || limit < 1 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid limit"))
|
||||
}
|
||||
if limit > maxChunk {
|
||||
limit = maxChunk
|
||||
}
|
||||
if debug != nil && debug.enabled {
|
||||
debug.pullRequests.Add(1)
|
||||
debug.chunkf("CPULL id=%s ack=%d want=%d offset=%d limit=%d", parts[2], ack, want, offset, limit)
|
||||
}
|
||||
|
||||
data, total, eof, final, waitExpired, err := s.pull(want, ack, offset, limit, pollWait)
|
||||
if err != nil {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
|
||||
}
|
||||
if waitExpired {
|
||||
if debug != nil && debug.enabled {
|
||||
debug.waitRecords.Add(1)
|
||||
debug.chunkf("CPULL id=%s want=%d -> WAIT", parts[2], want)
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("WAIT"))
|
||||
}
|
||||
if eof {
|
||||
if debug != nil {
|
||||
debug.chunkf("CPULL id=%s want=%d -> EOF final=%d", parts[2], want, final)
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte(fmt.Sprintf("EOF %d", final)))
|
||||
}
|
||||
|
||||
if debug != nil && debug.enabled {
|
||||
debug.dataRecords.Add(1)
|
||||
debug.chunkf("DATA id=%s seq=%d offset=%d bytes=%d total=%d", parts[2], want, offset, len(data), total)
|
||||
}
|
||||
prefix := []byte(fmt.Sprintf("DATA %d %d %d ", want, offset, total))
|
||||
out := make([]byte, len(prefix)+len(data))
|
||||
copy(out, prefix)
|
||||
copy(out[len(prefix):], data)
|
||||
return protocol.WriteResponseFrame(conn, requestID, out)
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(payload, []byte("CCLOSE ")) {
|
||||
parts := strings.Fields(string(payload))
|
||||
if len(parts) != 3 {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR bad CCLOSE"))
|
||||
}
|
||||
if !tokenEqual(decodeWireToken(parts[1]), token) {
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
|
||||
}
|
||||
manager.remove(parts[2])
|
||||
if debug != nil && debug.enabled {
|
||||
debug.sessionsClosed.Add(1)
|
||||
debug.activeSessions.Add(-1)
|
||||
debug.logf("SESSION CLOSE id=%s peer=%v active_sessions=%d", parts[2], conn.RemoteAddr(), manager.count())
|
||||
debug.chunkf("CCLOSE id=%s -> CLOSED", parts[2])
|
||||
}
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("CLOSED"))
|
||||
}
|
||||
|
||||
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR unknown chunk command"))
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDecodeWireTokenAllowsEmptyToken(t *testing.T) {
|
||||
if got := decodeWireToken("-"); got != "" {
|
||||
t.Fatalf("empty wire token decoded as %q", got)
|
||||
}
|
||||
if got := decodeWireToken("secret"); got != "secret" {
|
||||
t.Fatalf("non-empty token changed: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type serverDebug struct {
|
||||
enabled bool
|
||||
chunks bool
|
||||
statsEvery time.Duration
|
||||
started time.Time
|
||||
|
||||
sessionsOpened atomic.Uint64
|
||||
sessionsClosed atomic.Uint64
|
||||
activeSessions atomic.Int64
|
||||
bytesUp atomic.Uint64
|
||||
bytesDown atomic.Uint64
|
||||
pushRecords atomic.Uint64
|
||||
pullRequests atomic.Uint64
|
||||
dataRecords atomic.Uint64
|
||||
waitRecords atomic.Uint64
|
||||
errors atomic.Uint64
|
||||
}
|
||||
|
||||
func newServerDebug(enabled, chunks bool, statsEvery time.Duration) *serverDebug {
|
||||
d := &serverDebug{
|
||||
enabled: enabled || chunks,
|
||||
chunks: chunks,
|
||||
statsEvery: statsEvery,
|
||||
started: time.Now(),
|
||||
}
|
||||
if d.enabled && d.statsEvery > 0 {
|
||||
go d.statsLoop()
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *serverDebug) logf(format string, args ...any) {
|
||||
if d == nil || !d.enabled {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "%s [DEBUG] "+format+"\n", append([]any{time.Now().Format("2006-01-02 15:04:05.000")}, args...)...)
|
||||
}
|
||||
|
||||
func (d *serverDebug) chunkf(format string, args ...any) {
|
||||
if d == nil || !d.chunks {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "%s [CHUNK] "+format+"\n", append([]any{time.Now().Format("2006-01-02 15:04:05.000")}, args...)...)
|
||||
}
|
||||
|
||||
func (d *serverDebug) errorf(format string, args ...any) {
|
||||
if d == nil || !d.enabled {
|
||||
return
|
||||
}
|
||||
d.errors.Add(1)
|
||||
fmt.Fprintf(os.Stderr, "%s [ERROR] "+format+"\n", append([]any{time.Now().Format("2006-01-02 15:04:05.000")}, args...)...)
|
||||
}
|
||||
|
||||
func (d *serverDebug) statsLoop() {
|
||||
ticker := time.NewTicker(d.statsEvery)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
d.logf(
|
||||
"STATS uptime=%s active_connections=%d active_sessions=%d sessions_opened=%d sessions_closed=%d bytes_up=%d bytes_down=%d push_records=%d pull_requests=%d data_records=%d waits=%d errors=%d",
|
||||
time.Since(d.started).Round(time.Second),
|
||||
atomic.LoadInt64(&active),
|
||||
d.activeSessions.Load(),
|
||||
d.sessionsOpened.Load(),
|
||||
d.sessionsClosed.Load(),
|
||||
d.bytesUp.Load(),
|
||||
d.bytesDown.Load(),
|
||||
d.pushRecords.Load(),
|
||||
d.pullRequests.Load(),
|
||||
d.dataRecords.Load(),
|
||||
d.waitRecords.Load(),
|
||||
d.errors.Load(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"dragontcp/internal/protocol"
|
||||
)
|
||||
|
||||
var active int64
|
||||
|
||||
type dnsEntry struct {
|
||||
ips []netip.Addr
|
||||
expires time.Time
|
||||
}
|
||||
|
||||
type dnsCache struct {
|
||||
mu sync.RWMutex
|
||||
entries map[string]dnsEntry
|
||||
ttl time.Duration
|
||||
max int
|
||||
}
|
||||
|
||||
func newDNSCache(ttl time.Duration, max int) *dnsCache {
|
||||
return &dnsCache{
|
||||
entries: make(map[string]dnsEntry),
|
||||
ttl: ttl,
|
||||
max: max,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *dnsCache) resolve(ctx context.Context, host string) ([]netip.Addr, error) {
|
||||
if ip, err := netip.ParseAddr(host); err == nil {
|
||||
return []netip.Addr{ip}, nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
c.mu.RLock()
|
||||
entry, ok := c.entries[host]
|
||||
c.mu.RUnlock()
|
||||
if ok && now.Before(entry.expires) {
|
||||
return entry.ips, nil
|
||||
}
|
||||
|
||||
ips, err := net.DefaultResolver.LookupNetIP(ctx, "ip", host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
if len(c.entries) >= c.max {
|
||||
// Simple bounded reset keeps the hot cache cheap and prevents growth.
|
||||
c.entries = make(map[string]dnsEntry, c.max)
|
||||
}
|
||||
c.entries[host] = dnsEntry{ips: ips, expires: now.Add(c.ttl)}
|
||||
c.mu.Unlock()
|
||||
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
func tokenEqual(a, b string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
||||
}
|
||||
|
||||
var blockedSpecial = []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/8"),
|
||||
netip.MustParsePrefix("100.64.0.0/10"),
|
||||
netip.MustParsePrefix("192.0.0.0/24"),
|
||||
netip.MustParsePrefix("192.0.2.0/24"),
|
||||
netip.MustParsePrefix("198.18.0.0/15"),
|
||||
netip.MustParsePrefix("198.51.100.0/24"),
|
||||
netip.MustParsePrefix("203.0.113.0/24"),
|
||||
netip.MustParsePrefix("240.0.0.0/4"),
|
||||
netip.MustParsePrefix("2001:db8::/32"),
|
||||
}
|
||||
|
||||
func addressAllowed(addr netip.Addr, allowPrivate bool) bool {
|
||||
if addr.IsUnspecified() || addr.IsMulticast() {
|
||||
return false
|
||||
}
|
||||
|
||||
if allowPrivate {
|
||||
return true
|
||||
}
|
||||
|
||||
if !addr.IsGlobalUnicast() ||
|
||||
addr.IsPrivate() ||
|
||||
addr.IsLoopback() ||
|
||||
addr.IsLinkLocalUnicast() {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, prefix := range blockedSpecial {
|
||||
if prefix.Contains(addr) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func dialTarget(ctx context.Context, host string, port int, allowPrivate bool, cache *dnsCache, tcpBuffer int) (net.Conn, error) {
|
||||
ips, err := cache.resolve(ctx, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
var blocked []string
|
||||
|
||||
d := net.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
if !addressAllowed(ip, allowPrivate) {
|
||||
blocked = append(blocked, ip.String())
|
||||
continue
|
||||
}
|
||||
|
||||
addr := net.JoinHostPort(ip.String(), strconv.Itoa(port))
|
||||
conn, err := d.DialContext(ctx, "tcp", addr)
|
||||
if err == nil {
|
||||
protocol.TuneTCP(conn)
|
||||
protocol.TuneTCPBuffer(conn, tcpBuffer)
|
||||
return conn, nil
|
||||
}
|
||||
lastErr = err
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
return nil, lastErr
|
||||
}
|
||||
if len(blocked) > 0 {
|
||||
return nil, fmt.Errorf("target resolves only to blocked addresses: %s", strings.Join(blocked, ","))
|
||||
}
|
||||
return nil, fmt.Errorf("no usable target address")
|
||||
}
|
||||
|
||||
func handle(
|
||||
conn net.Conn,
|
||||
token string,
|
||||
allowPrivate bool,
|
||||
cache *dnsCache,
|
||||
tcpBuffer int,
|
||||
slots chan struct{},
|
||||
manager *chunkManager,
|
||||
chunkMax int,
|
||||
chunkBuffered int,
|
||||
chunkPollWait time.Duration,
|
||||
debug *serverDebug,
|
||||
) {
|
||||
defer func() {
|
||||
<-slots
|
||||
atomic.AddInt64(&active, -1)
|
||||
_ = conn.Close()
|
||||
}()
|
||||
|
||||
protocol.TuneTCP(conn)
|
||||
protocol.TuneTCPBuffer(conn, tcpBuffer)
|
||||
|
||||
for {
|
||||
_ = conn.SetDeadline(time.Now().Add(20 * time.Second))
|
||||
|
||||
requestID, _, payload, err := protocol.ReadRequestFrame(conn)
|
||||
if err != nil {
|
||||
if debug != nil && debug.enabled && err != io.EOF {
|
||||
debug.errorf("peer=%v read request: %v", conn.RemoteAddr(), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if isChunkCommand(payload) {
|
||||
if err := processChunkCommand(
|
||||
conn,
|
||||
requestID,
|
||||
payload,
|
||||
token,
|
||||
allowPrivate,
|
||||
cache,
|
||||
tcpBuffer,
|
||||
manager,
|
||||
chunkMax,
|
||||
chunkBuffered,
|
||||
chunkPollWait,
|
||||
debug,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Fields(string(payload))
|
||||
transport := "xor"
|
||||
|
||||
if len(parts) == 4 && parts[0] == "TUNNEL" {
|
||||
transport = "xor"
|
||||
} else if len(parts) == 5 && parts[0] == "TUNNEL2" {
|
||||
transport = strings.ToLower(parts[4])
|
||||
if transport != "raw" && transport != "xor" {
|
||||
_ = protocol.WriteResponseFrame(conn, requestID, []byte("ERR transport must be RAW or XOR"))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_ = protocol.WriteResponseFrame(
|
||||
conn,
|
||||
requestID,
|
||||
[]byte("ERR expected TUNNEL, TUNNEL2, or chunk command"),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if !tokenEqual(parts[1], token) {
|
||||
_ = protocol.WriteResponseFrame(
|
||||
conn,
|
||||
requestID,
|
||||
[]byte("ERR authentication failed"),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(parts[3])
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
_ = protocol.WriteResponseFrame(
|
||||
conn,
|
||||
requestID,
|
||||
[]byte("ERR invalid port"),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if debug != nil && debug.enabled {
|
||||
debug.logf("TUNNEL peer=%v target=%s:%d transport=%s", conn.RemoteAddr(), parts[2], port, transport)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
target, err := dialTarget(ctx, parts[2], port, allowPrivate, cache, tcpBuffer)
|
||||
cancel()
|
||||
|
||||
if err != nil {
|
||||
if debug != nil && debug.enabled {
|
||||
debug.errorf("TUNNEL target=%s:%d connect failed: %v", parts[2], port, err)
|
||||
}
|
||||
_ = protocol.WriteResponseFrame(
|
||||
conn,
|
||||
requestID,
|
||||
[]byte("ERR "+err.Error()),
|
||||
)
|
||||
return
|
||||
}
|
||||
defer target.Close()
|
||||
|
||||
if err := protocol.WriteResponseFrame(conn, requestID, []byte("CONNECTED")); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_ = conn.SetDeadline(time.Time{})
|
||||
if transport == "raw" {
|
||||
protocol.RelayRaw(conn, target)
|
||||
} else {
|
||||
protocol.RelayXOR(conn, target)
|
||||
}
|
||||
if debug != nil && debug.enabled {
|
||||
debug.logf("TUNNEL closed peer=%v target=%s:%d transport=%s", conn.RemoteAddr(), parts[2], port, transport)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
var (
|
||||
host = flag.String("host", "0.0.0.0", "listen host")
|
||||
port = flag.Int("port", 53, "listen port")
|
||||
token = flag.String("token", "", "optional shared token")
|
||||
maxConnections = flag.Int("max-connections", 20000, "max simultaneous tunnels")
|
||||
allowPrivate = flag.Bool("allow-private", false, "allow private/loopback targets")
|
||||
dnsCacheTTL = flag.Duration("dns-cache-ttl", 30*time.Second, "server DNS cache TTL")
|
||||
dnsCacheSize = flag.Int("dns-cache-size", 4096, "maximum cached DNS hostnames")
|
||||
tcpBuffer = flag.Int("tcp-buffer", 0, "optional TCP read/write buffer bytes; 0 keeps OS autotuning")
|
||||
chunkMax = flag.Int("chunk-max", 1048576, "maximum adaptive chunk payload bytes (32 bytes to 1 MiB)")
|
||||
chunkBuffered = flag.Int("chunk-buffered", 256, "maximum buffered destination chunks per session")
|
||||
chunkPollWait = flag.Duration("chunk-poll-wait", 200*time.Millisecond, "server long-poll wait for chunk data")
|
||||
sessionTimeout = flag.Duration("chunk-session-timeout", 2*time.Minute, "idle chunk session timeout")
|
||||
debugEnabled = flag.Bool("debug", false, "log session/connect/errors and periodic statistics")
|
||||
debugChunks = flag.Bool("debug-chunks", false, "log every chunk protocol record; very verbose")
|
||||
debugStats = flag.Duration("debug-stats-interval", 5*time.Second, "periodic debug statistics interval; 0 disables")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *chunkMax < 32 || *chunkMax > protocol.MaxChunkPayload {
|
||||
fmt.Fprintf(os.Stderr, "--chunk-max must be between 32 and %d\n", protocol.MaxChunkPayload)
|
||||
os.Exit(2)
|
||||
}
|
||||
if *chunkBuffered < 8 {
|
||||
fmt.Fprintln(os.Stderr, "--chunk-buffered must be at least 8")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
listenAddr := net.JoinHostPort(*host, strconv.Itoa(*port))
|
||||
ln, err := net.Listen("tcp", listenAddr)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
fmt.Printf("DragonTCP Go server listening on %s\n", listenAddr)
|
||||
fmt.Printf("max_connections=%d tcp_buffer=%d\n", *maxConnections, *tcpBuffer)
|
||||
|
||||
slots := make(chan struct{}, *maxConnections)
|
||||
cache := newDNSCache(*dnsCacheTTL, *dnsCacheSize)
|
||||
debug := newServerDebug(*debugEnabled, *debugChunks, *debugStats)
|
||||
manager := newChunkManager(*sessionTimeout, debug)
|
||||
fmt.Printf("adaptive_chunk_max=%d buffered_chunks=%d poll_wait=%s\n", *chunkMax, *chunkBuffered, chunkPollWait.String())
|
||||
if debug.enabled {
|
||||
fmt.Printf("debug=true debug_chunks=%t stats_interval=%s\n", debug.chunks, debug.statsEvery)
|
||||
}
|
||||
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "accept:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case slots <- struct{}{}:
|
||||
atomic.AddInt64(&active, 1)
|
||||
if debug.enabled {
|
||||
debug.logf("ACCEPT peer=%v active_connections=%d", conn.RemoteAddr(), atomic.LoadInt64(&active))
|
||||
}
|
||||
go handle(
|
||||
conn,
|
||||
*token,
|
||||
*allowPrivate,
|
||||
cache,
|
||||
*tcpBuffer,
|
||||
slots,
|
||||
manager,
|
||||
*chunkMax,
|
||||
*chunkBuffered,
|
||||
*chunkPollWait,
|
||||
debug,
|
||||
)
|
||||
default:
|
||||
if debug.enabled {
|
||||
debug.errorf("REJECT peer=%v reason=max-connections", conn.RemoteAddr())
|
||||
}
|
||||
_ = conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,624 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"dragontcpvpn/internal/protocol"
|
||||
)
|
||||
|
||||
var requestCounter atomic.Uint32
|
||||
|
||||
type txnLane struct {
|
||||
mu sync.Mutex
|
||||
serverAddr string
|
||||
timeout time.Duration
|
||||
reconnectEvery int
|
||||
conn net.Conn
|
||||
count int
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newTxnLane(addr string, timeout time.Duration, reconnectEvery int) *txnLane {
|
||||
return &txnLane{serverAddr: addr, timeout: timeout, reconnectEvery: reconnectEvery}
|
||||
}
|
||||
func (l *txnLane) closeLocked() {
|
||||
if l.conn != nil {
|
||||
_ = l.conn.Close()
|
||||
l.conn = nil
|
||||
}
|
||||
l.count = 0
|
||||
}
|
||||
func (l *txnLane) Close() { l.mu.Lock(); l.closed = true; l.closeLocked(); l.mu.Unlock() }
|
||||
func (l *txnLane) ensureConn() error {
|
||||
if l.closed {
|
||||
return net.ErrClosed
|
||||
}
|
||||
if l.conn != nil && (l.reconnectEvery <= 0 || l.count < l.reconnectEvery) {
|
||||
return nil
|
||||
}
|
||||
l.closeLocked()
|
||||
d := net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}
|
||||
c, err := d.Dial("tcp", l.serverAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
protocol.TuneTCP(c)
|
||||
l.conn = c
|
||||
return nil
|
||||
}
|
||||
func (l *txnLane) Do(payload []byte) ([]byte, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if err := l.ensureConn(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
timeout := l.timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 3 * time.Second
|
||||
}
|
||||
_ = l.conn.SetDeadline(time.Now().Add(timeout))
|
||||
id := requestCounter.Add(1)
|
||||
if err := protocol.WriteRequestFrame(l.conn, id, payload); err != nil {
|
||||
l.closeLocked()
|
||||
return nil, err
|
||||
}
|
||||
rid, resp, err := protocol.ReadResponseFrame(l.conn)
|
||||
if err != nil {
|
||||
l.closeLocked()
|
||||
return nil, err
|
||||
}
|
||||
if rid != id {
|
||||
l.closeLocked()
|
||||
return nil, errors.New("request ID mismatch")
|
||||
}
|
||||
l.count++
|
||||
_ = l.conn.SetDeadline(time.Time{})
|
||||
return resp, nil
|
||||
}
|
||||
func doControl(l *txnLane, payload []byte) ([]byte, error) {
|
||||
var last error
|
||||
for i := 0; i < 6; i++ {
|
||||
r, e := l.Do(payload)
|
||||
if e == nil {
|
||||
return r, nil
|
||||
}
|
||||
last = e
|
||||
time.Sleep(time.Duration(i+1) * 50 * time.Millisecond)
|
||||
}
|
||||
return nil, last
|
||||
}
|
||||
|
||||
type adaptiveSizer struct {
|
||||
mu sync.Mutex
|
||||
name string
|
||||
current, min, max int
|
||||
successes int
|
||||
growAfter int
|
||||
log bool
|
||||
}
|
||||
|
||||
func newSizer(name string, start, min, max, growAfter int, log bool) *adaptiveSizer {
|
||||
if min < 32 {
|
||||
min = 32
|
||||
}
|
||||
if max > protocol.VPNMaxFragment {
|
||||
max = protocol.VPNMaxFragment
|
||||
}
|
||||
if max < min {
|
||||
max = min
|
||||
}
|
||||
if start < min {
|
||||
start = min
|
||||
}
|
||||
if start > max {
|
||||
start = max
|
||||
}
|
||||
if growAfter < 1 {
|
||||
growAfter = 32
|
||||
}
|
||||
return &adaptiveSizer{name: name, current: start, min: min, max: max, growAfter: growAfter, log: log}
|
||||
}
|
||||
func (s *adaptiveSizer) Current() int { s.mu.Lock(); v := s.current; s.mu.Unlock(); return v }
|
||||
func (s *adaptiveSizer) Failure(actual int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
old := s.current
|
||||
s.successes = 0
|
||||
basis := actual
|
||||
if basis <= 0 || basis > old {
|
||||
basis = old
|
||||
}
|
||||
next := basis / 2
|
||||
if next < s.min {
|
||||
next = s.min
|
||||
}
|
||||
if next >= old && old > s.min {
|
||||
next = old / 2
|
||||
if next < s.min {
|
||||
next = s.min
|
||||
}
|
||||
}
|
||||
if next < old {
|
||||
s.current = next
|
||||
if s.log {
|
||||
fmt.Printf("adaptive %s chunk: %d -> %d after transport failure (record=%d)\n", s.name, old, next, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
func (s *adaptiveSizer) Success(actual int, full bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.current >= s.max || !full {
|
||||
return
|
||||
}
|
||||
s.successes++
|
||||
if s.successes < s.growAfter {
|
||||
return
|
||||
}
|
||||
s.successes = 0
|
||||
old := s.current
|
||||
step := old / 4
|
||||
if step < 32 {
|
||||
step = 32
|
||||
}
|
||||
next := old + step
|
||||
if next > s.max {
|
||||
next = s.max
|
||||
}
|
||||
if next > old {
|
||||
s.current = next
|
||||
if s.log {
|
||||
fmt.Printf("adaptive %s chunk: %d -> %d after stable success\n", s.name, old, next)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func receiveTunFD(path string, timeout time.Duration) (*os.File, error) {
|
||||
_ = os.Remove(path)
|
||||
addr := &net.UnixAddr{Name: path, Net: "unix"}
|
||||
ln, err := net.ListenUnix("unix", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { ln.Close(); os.Remove(path) }()
|
||||
_ = os.Chmod(path, 0600)
|
||||
fmt.Printf("TUNFD READY %s\n", path)
|
||||
_ = ln.SetDeadline(time.Now().Add(timeout))
|
||||
c, err := ln.AcceptUnix()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer c.Close()
|
||||
buf := make([]byte, 1)
|
||||
oob := make([]byte, 128)
|
||||
n, oobn, _, _, err := c.ReadMsgUnix(buf, oob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n < 1 {
|
||||
return nil, errors.New("missing TUN fd marker")
|
||||
}
|
||||
msgs, err := syscall.ParseSocketControlMessage(oob[:oobn])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, m := range msgs {
|
||||
fds, e := syscall.ParseUnixRights(&m)
|
||||
if e == nil && len(fds) > 0 {
|
||||
return os.NewFile(uintptr(fds[0]), "android-tun"), nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("TUN file descriptor was not received")
|
||||
}
|
||||
|
||||
func randomSID() (protocol.VPNSessionID, error) {
|
||||
var sid protocol.VPNSessionID
|
||||
_, err := io.ReadFull(rand.Reader, sid[:])
|
||||
return sid, err
|
||||
}
|
||||
|
||||
type vpnClient struct {
|
||||
tun *os.File
|
||||
sid protocol.VPNSessionID
|
||||
serverAddr string
|
||||
token string
|
||||
ipv4, ipv6 netip.Addr
|
||||
mtu int
|
||||
timeout time.Duration
|
||||
batchDelay time.Duration
|
||||
reconnectEvery int
|
||||
upSizer, downSizer *adaptiveSizer
|
||||
control, upload, download *txnLane
|
||||
upPackets, downPackets, upBytes, downBytes atomic.Uint64
|
||||
upBatches, downBatches, localDropped atomic.Uint64
|
||||
stopped chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func newVPNClient(tun *os.File, addr, token string, v4, v6 netip.Addr, mtu, start, min, max, growAfter, reconnectEvery int, timeout, batchDelay time.Duration, adaptLog bool) (*vpnClient, error) {
|
||||
sid, err := randomSID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if batchDelay < 0 {
|
||||
batchDelay = 0
|
||||
}
|
||||
return &vpnClient{tun: tun, sid: sid, serverAddr: addr, token: token, ipv4: v4, ipv6: v6, mtu: mtu, timeout: timeout, batchDelay: batchDelay, reconnectEvery: reconnectEvery,
|
||||
upSizer: newSizer("upload", start, min, max, growAfter, adaptLog), downSizer: newSizer("download", start, min, max, growAfter, adaptLog),
|
||||
control: newTxnLane(addr, timeout, reconnectEvery), upload: newTxnLane(addr, timeout, reconnectEvery), download: newTxnLane(addr, timeout, reconnectEvery), stopped: make(chan struct{})}, nil
|
||||
}
|
||||
func (v *vpnClient) open() error {
|
||||
req, err := protocol.BuildVPNOpen(v.sid, v.token, v.ipv4, v.ipv6, v.mtu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := doControl(v.control, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
max, err := protocol.ParseVPNOpened(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if max < v.upSizer.max {
|
||||
v.upSizer.max = max
|
||||
if v.upSizer.current > max {
|
||||
v.upSizer.current = max
|
||||
}
|
||||
}
|
||||
if max < v.downSizer.max {
|
||||
v.downSizer.max = max
|
||||
if v.downSizer.current > max {
|
||||
v.downSizer.current = max
|
||||
}
|
||||
}
|
||||
fmt.Printf("VPN SESSION OPEN ipv4=%s ipv6=%s mtu=%d server_chunk_max=%d\n", v.ipv4, v.ipv6, v.mtu, max)
|
||||
return nil
|
||||
}
|
||||
func (v *vpnClient) close() {
|
||||
v.stopOnce.Do(func() {
|
||||
close(v.stopped)
|
||||
if p, err := protocol.BuildVPNClose(v.sid), error(nil); err == nil {
|
||||
_, _ = v.control.Do(p)
|
||||
}
|
||||
v.control.Close()
|
||||
v.upload.Close()
|
||||
v.download.Close()
|
||||
_ = v.tun.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func (v *vpnClient) logLocalDrop(reason string) {
|
||||
n := v.localDropped.Add(1)
|
||||
// Link-local/control traffic can be noisy. Keep it visible without filling
|
||||
// the Android live log or making a harmless packet fatal to the VPN.
|
||||
if n <= 8 || n%256 == 0 {
|
||||
fmt.Printf("VPN DROP local packet (%s) dropped=%d\n", reason, n)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *vpnClient) tunReadLoop(out chan<- []byte, errs chan<- error) {
|
||||
buf := make([]byte, protocol.VPNMaxPacket)
|
||||
for {
|
||||
n, err := v.tun.Read(buf)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
if n < 1 || n > protocol.VPNMaxPacket {
|
||||
continue
|
||||
}
|
||||
packet := append([]byte(nil), buf[:n]...)
|
||||
src, _, err := protocol.PacketAddresses(packet)
|
||||
if err != nil {
|
||||
v.logLocalDrop(err.Error())
|
||||
continue
|
||||
}
|
||||
if src != v.ipv4 && src != v.ipv6 {
|
||||
v.logLocalDrop(fmt.Sprintf("source %s is not assigned VPN address", src))
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case out <- packet:
|
||||
case <-v.stopped:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func batchWireSize(packets [][]byte) int {
|
||||
n := 1
|
||||
for _, p := range packets {
|
||||
n += 2 + len(p)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (v *vpnClient) uploadLoop(in <-chan []byte, errs chan<- error) {
|
||||
var seq uint32
|
||||
var carry []byte
|
||||
for {
|
||||
var first []byte
|
||||
if carry != nil {
|
||||
first, carry = carry, nil
|
||||
} else {
|
||||
select {
|
||||
case first = <-in:
|
||||
case <-v.stopped:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
packets := [][]byte{first}
|
||||
encodedSize := 1 + 2 + len(first)
|
||||
timer := time.NewTimer(v.batchDelay)
|
||||
collect:
|
||||
for encodedSize < protocol.VPNMaxBatch {
|
||||
select {
|
||||
case p := <-in:
|
||||
need := 2 + len(p)
|
||||
if encodedSize+need > protocol.VPNMaxBatch {
|
||||
carry = p
|
||||
break collect
|
||||
}
|
||||
packets = append(packets, p)
|
||||
encodedSize += need
|
||||
case <-timer.C:
|
||||
break collect
|
||||
case <-v.stopped:
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
batch, err := protocol.BuildVPNBatch(packets)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
offset := 0
|
||||
for offset < len(batch) {
|
||||
limit := v.upSizer.Current()
|
||||
size := len(batch) - offset
|
||||
if size > limit {
|
||||
size = limit
|
||||
}
|
||||
req, e := protocol.BuildVPNPush(v.sid, seq, offset, len(batch), batch[offset:offset+size])
|
||||
if e != nil {
|
||||
errs <- e
|
||||
return
|
||||
}
|
||||
resp, e := v.upload.Do(req)
|
||||
if e != nil {
|
||||
v.upSizer.Failure(size)
|
||||
continue
|
||||
}
|
||||
rseq, accepted, e := protocol.ParseVPNAck(resp)
|
||||
if e != nil {
|
||||
errs <- e
|
||||
return
|
||||
}
|
||||
if rseq != seq || accepted < offset || accepted > len(batch) {
|
||||
errs <- errors.New("bad server upload ACK")
|
||||
return
|
||||
}
|
||||
v.upSizer.Success(size, size == limit)
|
||||
offset = accepted
|
||||
}
|
||||
var rawBytes uint64
|
||||
for _, p := range packets {
|
||||
rawBytes += uint64(len(p))
|
||||
}
|
||||
v.upPackets.Add(uint64(len(packets)))
|
||||
v.upBytes.Add(rawBytes)
|
||||
v.upBatches.Add(1)
|
||||
seq++
|
||||
}
|
||||
}
|
||||
|
||||
func (v *vpnClient) downloadLoop(errs chan<- error) {
|
||||
var want uint32
|
||||
ack := protocol.VPNNoAck
|
||||
offset := 0
|
||||
var transfer []byte
|
||||
total := 0
|
||||
for {
|
||||
limit := v.downSizer.Current()
|
||||
req, e := protocol.BuildVPNPull(v.sid, ack, want, offset, limit)
|
||||
if e != nil {
|
||||
errs <- e
|
||||
return
|
||||
}
|
||||
resp, e := v.download.Do(req)
|
||||
if e != nil {
|
||||
v.downSizer.Failure(limit)
|
||||
continue
|
||||
}
|
||||
seq, roff, rtotal, data, wait, e := protocol.ParseVPNData(resp)
|
||||
if e != nil {
|
||||
errs <- e
|
||||
return
|
||||
}
|
||||
if wait {
|
||||
continue
|
||||
}
|
||||
if seq != want || roff != offset || rtotal < 1 || rtotal > protocol.VPNMaxBatch {
|
||||
errs <- errors.New("bad server download sequence")
|
||||
return
|
||||
}
|
||||
if offset == 0 {
|
||||
total = rtotal
|
||||
transfer = make([]byte, 0, total)
|
||||
} else if rtotal != total {
|
||||
errs <- errors.New("download transfer size changed")
|
||||
return
|
||||
}
|
||||
transfer = append(transfer, data...)
|
||||
offset += len(data)
|
||||
v.downSizer.Success(len(data), len(data) == limit)
|
||||
if offset < total {
|
||||
continue
|
||||
}
|
||||
if offset != total {
|
||||
errs <- errors.New("download transfer overflow")
|
||||
return
|
||||
}
|
||||
packets, e := protocol.ParseVPNBatch(transfer)
|
||||
if e != nil {
|
||||
// Compatibility with the first packet-VPN build, which used one raw
|
||||
// IP packet as each transfer object.
|
||||
if len(transfer) > 0 && (transfer[0]>>4 == 4 || transfer[0]>>4 == 6) {
|
||||
packets = [][]byte{transfer}
|
||||
} else {
|
||||
errs <- e
|
||||
return
|
||||
}
|
||||
}
|
||||
var rawBytes uint64
|
||||
for _, packet := range packets {
|
||||
n, e := v.tun.Write(packet)
|
||||
if e != nil {
|
||||
errs <- e
|
||||
return
|
||||
}
|
||||
if n != len(packet) {
|
||||
errs <- io.ErrShortWrite
|
||||
return
|
||||
}
|
||||
rawBytes += uint64(n)
|
||||
}
|
||||
v.downPackets.Add(uint64(len(packets)))
|
||||
v.downBytes.Add(rawBytes)
|
||||
v.downBatches.Add(1)
|
||||
ack = want
|
||||
want++
|
||||
offset = 0
|
||||
transfer = nil
|
||||
total = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (v *vpnClient) run() error {
|
||||
if err := v.open(); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("VPN READY")
|
||||
errs := make(chan error, 3)
|
||||
packets := make(chan []byte, 256)
|
||||
go v.tunReadLoop(packets, errs)
|
||||
go v.uploadLoop(packets, errs)
|
||||
go v.downloadLoop(errs)
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case err := <-errs:
|
||||
return err
|
||||
case <-ticker.C:
|
||||
fmt.Printf("STATS up_packets=%d down_packets=%d up_batches=%d down_batches=%d up_bytes=%d down_bytes=%d local_dropped=%d upload_chunk=%d download_chunk=%d pollers=1\n", v.upPackets.Load(), v.downPackets.Load(), v.upBatches.Load(), v.downBatches.Load(), v.upBytes.Load(), v.downBytes.Load(), v.localDropped.Load(), v.upSizer.Current(), v.downSizer.Current())
|
||||
case <-v.stopped:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
serverHost := flag.String("server-host", "", "DragonTCP VPN server host/IP")
|
||||
serverPort := flag.Int("server-port", 53, "DragonTCP VPN server TCP port")
|
||||
token := flag.String("token", "change-this-token", "shared token")
|
||||
tunFDSocket := flag.String("tun-fd-socket", "", "Unix socket path used by Android to pass the VpnService TUN fd")
|
||||
tunFD := flag.Int("tun-fd", -1, "existing TUN fd for testing/non-Android use")
|
||||
ipv4Text := flag.String("vpn-ipv4", "10.123.0.2", "client VPN IPv4 address")
|
||||
ipv6Text := flag.String("vpn-ipv6", "fd7a:4472:6167:6f6e::2", "client VPN IPv6 address")
|
||||
mtu := flag.Int("vpn-mtu", 1280, "VPN interface MTU")
|
||||
chunkMax := flag.Int("chunk-max", protocol.VPNMaxFragment, "maximum adaptive record bytes (up to 1 MiB)")
|
||||
chunkMin := flag.Int("chunk-min", 32, "minimum adaptive record bytes")
|
||||
chunkStart := flag.Int("chunk-start", protocol.VPNMaxFragment, "starting record bytes; app sets this equal to max")
|
||||
growAfter := flag.Int("chunk-grow-after", 64, "full successful records before increasing chunk size")
|
||||
timeout := flag.Duration("chunk-timeout", 2*time.Second, "framed transaction timeout")
|
||||
batchDelay := flag.Duration("batch-delay", time.Millisecond, "maximum delay used to combine adjacent TUN packets into one transfer object")
|
||||
reconnectEvery := flag.Int("chunk-reconnect-every", 32, "reconnect a TCP/53 lane after this many transactions; 0 keeps it open")
|
||||
adaptLog := flag.Bool("chunk-adapt-log", false, "log adaptive chunk changes")
|
||||
flag.Parse()
|
||||
if *serverHost == "" {
|
||||
fmt.Fprintln(os.Stderr, "--server-host is required")
|
||||
os.Exit(2)
|
||||
}
|
||||
if *serverPort < 1 || *serverPort > 65535 {
|
||||
fmt.Fprintln(os.Stderr, "invalid server port")
|
||||
os.Exit(2)
|
||||
}
|
||||
if *chunkMin < 32 || *chunkMax > protocol.VPNMaxFragment || *chunkMin > *chunkMax {
|
||||
fmt.Fprintf(os.Stderr, "chunks must satisfy 32 <= min <= max <= %d\n", protocol.VPNMaxFragment)
|
||||
os.Exit(2)
|
||||
}
|
||||
if *chunkStart < *chunkMin {
|
||||
*chunkStart = *chunkMin
|
||||
}
|
||||
if *chunkStart > *chunkMax {
|
||||
*chunkStart = *chunkMax
|
||||
}
|
||||
v4, err := netip.ParseAddr(*ipv4Text)
|
||||
if err != nil || !v4.Is4() {
|
||||
fmt.Fprintln(os.Stderr, "invalid --vpn-ipv4")
|
||||
os.Exit(2)
|
||||
}
|
||||
v6, err := netip.ParseAddr(*ipv6Text)
|
||||
if err != nil || !v6.Is6() {
|
||||
fmt.Fprintln(os.Stderr, "invalid --vpn-ipv6")
|
||||
os.Exit(2)
|
||||
}
|
||||
var tun *os.File
|
||||
if *tunFD >= 0 {
|
||||
tun = os.NewFile(uintptr(*tunFD), "tun")
|
||||
} else {
|
||||
if *tunFDSocket == "" {
|
||||
fmt.Fprintln(os.Stderr, "--tun-fd-socket is required on Android")
|
||||
os.Exit(2)
|
||||
}
|
||||
tun, err = receiveTunFD(*tunFDSocket, 10*time.Second)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "receive TUN fd:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
addr := net.JoinHostPort(*serverHost, strconv.Itoa(*serverPort))
|
||||
client, err := newVPNClient(tun, addr, *token, v4, v6, *mtu, *chunkStart, *chunkMin, *chunkMax, *growAfter, *reconnectEvery, *timeout, *batchDelay, *adaptLog)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() { <-sig; client.close() }()
|
||||
if err := client.run(); err != nil && !errors.Is(err, os.ErrClosed) && !errors.Is(err, net.ErrClosed) {
|
||||
fmt.Fprintln(os.Stderr, "VPN stopped:", err)
|
||||
client.close()
|
||||
os.Exit(1)
|
||||
}
|
||||
client.close()
|
||||
}
|
||||
@@ -1,841 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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
|
||||
upBatches atomic.Uint64
|
||||
downBatches 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
|
||||
maxQueueBytes int
|
||||
batchDelay time.Duration
|
||||
manager *vpnManager
|
||||
|
||||
mu sync.Mutex
|
||||
notify chan struct{}
|
||||
packets map[uint32]*downTransfer
|
||||
nextDown uint32
|
||||
closed bool
|
||||
lastSeen time.Time
|
||||
pendingPackets [][]byte
|
||||
pendingEncoded int
|
||||
pendingTimer *time.Timer
|
||||
queuedPacketCount int
|
||||
queuedBytes int
|
||||
|
||||
upMu sync.Mutex
|
||||
expectedUp uint32
|
||||
currentSeq uint32
|
||||
currentTotal int
|
||||
currentBuf []byte
|
||||
haveCurrent bool
|
||||
lastComplete uint32
|
||||
lastCompleteTotal int
|
||||
haveLastComplete bool
|
||||
}
|
||||
|
||||
type downTransfer struct {
|
||||
data []byte
|
||||
packetCount int
|
||||
rawBytes int
|
||||
}
|
||||
|
||||
func newVPNSession(m *vpnManager, sid protocol.VPNSessionID, v4, v6 netip.Addr, mtu, maxChunk, maxPackets, maxQueueBytes int, batchDelay time.Duration) *vpnSession {
|
||||
return &vpnSession{
|
||||
sid: sid, ipv4: v4, ipv6: v6, mtu: mtu, maxChunk: maxChunk, maxPackets: maxPackets, maxQueueBytes: maxQueueBytes, batchDelay: batchDelay,
|
||||
manager: m, notify: make(chan struct{}), packets: make(map[uint32]*downTransfer, 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) flushPendingLocked() {
|
||||
if len(s.pendingPackets) == 0 {
|
||||
return
|
||||
}
|
||||
if s.pendingTimer != nil {
|
||||
s.pendingTimer.Stop()
|
||||
s.pendingTimer = nil
|
||||
}
|
||||
batch, err := protocol.BuildVPNBatch(s.pendingPackets)
|
||||
if err != nil {
|
||||
if s.manager.debug != nil {
|
||||
s.manager.debug.dropped.Add(uint64(len(s.pendingPackets)))
|
||||
s.manager.debug.errorf("BATCH sid=%s: %v", shortSID(s.sid), err)
|
||||
}
|
||||
s.queuedPacketCount -= len(s.pendingPackets)
|
||||
for _, p := range s.pendingPackets {
|
||||
s.queuedBytes -= len(p)
|
||||
}
|
||||
s.pendingPackets = nil
|
||||
s.pendingEncoded = 0
|
||||
return
|
||||
}
|
||||
rawBytes := 0
|
||||
for _, p := range s.pendingPackets {
|
||||
rawBytes += len(p)
|
||||
}
|
||||
seq := s.nextDown
|
||||
s.nextDown++
|
||||
s.packets[seq] = &downTransfer{data: batch, packetCount: len(s.pendingPackets), rawBytes: rawBytes}
|
||||
if s.manager.debug != nil {
|
||||
s.manager.debug.downBatches.Add(1)
|
||||
s.manager.debug.packetf("BATCH QUEUE sid=%s seq=%d packets=%d raw_bytes=%d transfer_bytes=%d", shortSID(s.sid), seq, len(s.pendingPackets), rawBytes, len(batch))
|
||||
}
|
||||
s.pendingPackets = nil
|
||||
s.pendingEncoded = 0
|
||||
s.signalLocked()
|
||||
}
|
||||
|
||||
func (s *vpnSession) flushPending() {
|
||||
s.mu.Lock()
|
||||
if !s.closed {
|
||||
s.flushPendingLocked()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *vpnSession) enqueue(packet []byte) bool {
|
||||
if len(packet) == 0 || len(packet) > protocol.VPNMaxPacket {
|
||||
return false
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
return false
|
||||
}
|
||||
need := 2 + len(packet)
|
||||
if len(s.pendingPackets) > 0 && s.pendingEncoded+need > protocol.VPNMaxBatch {
|
||||
s.flushPendingLocked()
|
||||
}
|
||||
if s.queuedPacketCount >= s.maxPackets || s.queuedBytes+len(packet) > s.maxQueueBytes {
|
||||
if s.manager.debug != nil {
|
||||
s.manager.debug.dropped.Add(1)
|
||||
}
|
||||
return false
|
||||
}
|
||||
p := append([]byte(nil), packet...)
|
||||
if len(s.pendingPackets) == 0 {
|
||||
s.pendingEncoded = 1
|
||||
}
|
||||
s.pendingPackets = append(s.pendingPackets, p)
|
||||
s.pendingEncoded += 2 + len(p)
|
||||
s.queuedPacketCount++
|
||||
s.queuedBytes += len(p)
|
||||
s.touchLocked()
|
||||
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 bytes=%d pending_packets=%d pending_transfer=%d", shortSID(s.sid), len(packet), len(s.pendingPackets), s.pendingEncoded)
|
||||
}
|
||||
if s.pendingEncoded >= protocol.VPNMaxBatch {
|
||||
s.flushPendingLocked()
|
||||
} else if s.pendingTimer == nil {
|
||||
delay := s.batchDelay
|
||||
if delay <= 0 {
|
||||
s.flushPendingLocked()
|
||||
} else {
|
||||
s.pendingTimer = time.AfterFunc(delay, s.flushPending)
|
||||
}
|
||||
}
|
||||
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 > protocol.VPNMaxBatch || len(data) < 1 || len(data) > s.maxChunk || offset < 0 || offset+len(data) > total {
|
||||
return 0, errors.New("invalid transfer 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("transfer 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) && bytes.Equal(s.currentBuf[offset:end], 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
|
||||
}
|
||||
|
||||
transfer := append([]byte(nil), s.currentBuf...)
|
||||
s.haveCurrent = false
|
||||
s.currentBuf = nil
|
||||
|
||||
if err := s.manager.acceptClientTransfer(s, transfer); 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.upBatches.Add(1)
|
||||
s.manager.debug.packetf("UP BATCH sid=%s seq=%d transfer_bytes=%d", shortSID(s.sid), seq, len(transfer))
|
||||
}
|
||||
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 {
|
||||
rec := s.packets[seq]
|
||||
if rec != nil {
|
||||
s.queuedPacketCount -= rec.packetCount
|
||||
s.queuedBytes -= rec.rawBytes
|
||||
}
|
||||
delete(s.packets, seq)
|
||||
}
|
||||
}
|
||||
}
|
||||
if rec, ok := s.packets[want]; ok {
|
||||
if offset >= len(rec.data) {
|
||||
s.mu.Unlock()
|
||||
return nil, len(rec.data), false, errors.New("pull offset beyond transfer")
|
||||
}
|
||||
end := offset + limit
|
||||
if end > len(rec.data) {
|
||||
end = len(rec.data)
|
||||
}
|
||||
out := append([]byte(nil), rec.data[offset:end]...)
|
||||
total := len(rec.data)
|
||||
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
|
||||
if s.pendingTimer != nil {
|
||||
s.pendingTimer.Stop()
|
||||
s.pendingTimer = nil
|
||||
}
|
||||
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
|
||||
maxQueueBytes int
|
||||
batchDelay time.Duration
|
||||
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, maxQueueBytes int, pollWait, timeout, batchDelay 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, maxQueueBytes: maxQueueBytes, pollWait: pollWait, timeout: timeout, batchDelay: batchDelay, 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.maxQueueBytes, m.batchDelay)
|
||||
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 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) dropClientPacket(s *vpnSession, packet []byte, reason string) {
|
||||
if m.debug != nil {
|
||||
m.debug.dropped.Add(1)
|
||||
m.debug.packetf("DROP sid=%s bytes=%d reason=%s", shortSID(s.sid), len(packet), reason)
|
||||
// A source mismatch can be normal Android link-local/control traffic.
|
||||
// Never tear down the whole VPN session for one such packet.
|
||||
m.debug.logf("DROP sid=%s reason=%s", shortSID(s.sid), reason)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *vpnManager) acceptClientPacket(s *vpnSession, packet []byte) (bool, error) {
|
||||
src, dst, err := protocol.PacketAddresses(packet)
|
||||
if err != nil {
|
||||
m.dropClientPacket(s, packet, err.Error())
|
||||
return false, nil
|
||||
}
|
||||
if src != s.ipv4 && src != s.ipv6 {
|
||||
m.dropClientPacket(s, packet, fmt.Sprintf("source %s does not match session address", src))
|
||||
return false, nil
|
||||
}
|
||||
if !destinationAllowed(dst, m.allowPrivate) {
|
||||
m.dropClientPacket(s, packet, fmt.Sprintf("destination %s is blocked", dst))
|
||||
return false, nil
|
||||
}
|
||||
if m.mockEcho {
|
||||
s.enqueue(packet)
|
||||
return true, nil
|
||||
}
|
||||
if m.tun == nil {
|
||||
return false, errors.New("VPN TUN is unavailable")
|
||||
}
|
||||
m.tunWriteMu.Lock()
|
||||
n, err := m.tun.Write(packet)
|
||||
m.tunWriteMu.Unlock()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if n != len(packet) {
|
||||
return false, io.ErrShortWrite
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m *vpnManager) acceptClientTransfer(s *vpnSession, transfer []byte) error {
|
||||
packets, err := protocol.ParseVPNBatch(transfer)
|
||||
if err != nil {
|
||||
// Compatibility with the first packet-VPN build.
|
||||
if len(transfer) > 0 && (transfer[0]>>4 == 4 || transfer[0]>>4 == 6) {
|
||||
packets = [][]byte{transfer}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, packet := range packets {
|
||||
accepted, err := m.acceptClientPacket(s, packet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if accepted && m.debug != nil {
|
||||
m.debug.upPackets.Add(1)
|
||||
m.debug.upBytes.Add(uint64(len(packet)))
|
||||
}
|
||||
}
|
||||
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 := protocol.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", protocol.VPNMaxFragment, "maximum DragonTCP transport fragment bytes (32-1048576)")
|
||||
maxPackets := flag.Int("vpn-buffered-packets", 2048, "maximum queued return IP packets per client")
|
||||
maxQueueBytes := flag.Int("vpn-buffer-bytes", 8*1024*1024, "maximum queued raw return bytes per client")
|
||||
batchDelay := flag.Duration("batch-delay", time.Millisecond, "maximum delay to combine adjacent TUN packets into one transfer object")
|
||||
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 *maxPackets < 1 || *maxQueueBytes < protocol.VPNMaxPacket {
|
||||
fmt.Fprintln(os.Stderr, "invalid VPN buffer limits")
|
||||
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, *maxQueueBytes, *pollWait, *sessionTimeout, *batchDelay, *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 batch_max=%d batch_delay=%s poll_wait=%s buffered_packets=%d buffer_bytes=%d\n", *maxChunk, protocol.VPNMaxBatch, batchDelay.String(), pollWait.String(), *maxPackets, *maxQueueBytes)
|
||||
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_batches=%d down_batches=%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.upBatches.Load(), debug.downBatches.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
module dragontcpvpn
|
||||
module dragontcp
|
||||
|
||||
go 1.22
|
||||
|
||||
@@ -1,376 +0,0 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
const (
|
||||
VPNCmdOpen byte = 0x30
|
||||
VPNCmdPush byte = 0x31
|
||||
VPNCmdPull byte = 0x32
|
||||
VPNCmdClose byte = 0x33
|
||||
|
||||
VPNRespOpened byte = 0x40
|
||||
VPNRespAck byte = 0x41
|
||||
VPNRespData byte = 0x42
|
||||
VPNRespWait byte = 0x43
|
||||
VPNRespClosed byte = 0x44
|
||||
VPNRespError byte = 0x7f
|
||||
|
||||
VPNNoAck uint32 = 0xffffffff
|
||||
|
||||
// Raw IP packets remain bounded by the IPv4/IPv6 packet-length model.
|
||||
VPNMaxPacket = 65535
|
||||
|
||||
// DragonTCP transfer objects/records are independent of IP packet size.
|
||||
// Multiple IP packets may be batched into one transfer object.
|
||||
VPNMaxFragment = 1024 * 1024
|
||||
VPNMaxBatch = 1024 * 1024
|
||||
|
||||
VPNBatchVersion byte = 1
|
||||
)
|
||||
|
||||
type VPNSessionID [16]byte
|
||||
|
||||
func VPNError(message string) []byte {
|
||||
b := []byte(message)
|
||||
if len(b) > 4096 {
|
||||
b = b[:4096]
|
||||
}
|
||||
out := make([]byte, 1+len(b))
|
||||
out[0] = VPNRespError
|
||||
copy(out[1:], b)
|
||||
return out
|
||||
}
|
||||
|
||||
func ParseVPNError(payload []byte) error {
|
||||
if len(payload) == 0 {
|
||||
return errors.New("empty DragonTCP VPN response")
|
||||
}
|
||||
if payload[0] == VPNRespError {
|
||||
return errors.New(string(payload[1:]))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OPEN request:
|
||||
// cmd(1) sid(16) tokenLen(2) token(N) ipv4(4) ipv6(16) mtu(2)
|
||||
func BuildVPNOpen(sid VPNSessionID, token string, ipv4, ipv6 netip.Addr, mtu int) ([]byte, error) {
|
||||
if len(token) > 4096 {
|
||||
return nil, errors.New("token too long")
|
||||
}
|
||||
if !ipv4.Is4() || !ipv6.Is6() {
|
||||
return nil, errors.New("invalid VPN client addresses")
|
||||
}
|
||||
if mtu < 576 || mtu > VPNMaxPacket {
|
||||
return nil, errors.New("invalid VPN MTU")
|
||||
}
|
||||
out := make([]byte, 1+16+2+len(token)+4+16+2)
|
||||
out[0] = VPNCmdOpen
|
||||
copy(out[1:17], sid[:])
|
||||
binary.BigEndian.PutUint16(out[17:19], uint16(len(token)))
|
||||
pos := 19
|
||||
copy(out[pos:pos+len(token)], token)
|
||||
pos += len(token)
|
||||
v4 := ipv4.As4()
|
||||
copy(out[pos:pos+4], v4[:])
|
||||
pos += 4
|
||||
v6 := ipv6.As16()
|
||||
copy(out[pos:pos+16], v6[:])
|
||||
pos += 16
|
||||
binary.BigEndian.PutUint16(out[pos:pos+2], uint16(mtu))
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ParseVPNOpen(payload []byte) (sid VPNSessionID, token string, ipv4, ipv6 netip.Addr, mtu int, err error) {
|
||||
if len(payload) < 1+16+2+4+16+2 || payload[0] != VPNCmdOpen {
|
||||
err = errors.New("bad VPN OPEN")
|
||||
return
|
||||
}
|
||||
copy(sid[:], payload[1:17])
|
||||
tokenLen := int(binary.BigEndian.Uint16(payload[17:19]))
|
||||
need := 1 + 16 + 2 + tokenLen + 4 + 16 + 2
|
||||
if len(payload) != need {
|
||||
err = errors.New("bad VPN OPEN length")
|
||||
return
|
||||
}
|
||||
pos := 19
|
||||
token = string(payload[pos : pos+tokenLen])
|
||||
pos += tokenLen
|
||||
var a4 [4]byte
|
||||
copy(a4[:], payload[pos:pos+4])
|
||||
ipv4 = netip.AddrFrom4(a4)
|
||||
pos += 4
|
||||
var a6 [16]byte
|
||||
copy(a6[:], payload[pos:pos+16])
|
||||
ipv6 = netip.AddrFrom16(a6)
|
||||
pos += 16
|
||||
mtu = int(binary.BigEndian.Uint16(payload[pos : pos+2]))
|
||||
return
|
||||
}
|
||||
|
||||
// OPENED v2 response: cmd(1) maxChunk(4).
|
||||
// ParseVPNOpened also accepts the old 3-byte/uint16 response for compatibility.
|
||||
func BuildVPNOpened(maxChunk int) []byte {
|
||||
if maxChunk > VPNMaxFragment {
|
||||
maxChunk = VPNMaxFragment
|
||||
}
|
||||
if maxChunk < 1 {
|
||||
maxChunk = 1
|
||||
}
|
||||
out := make([]byte, 5)
|
||||
out[0] = VPNRespOpened
|
||||
binary.BigEndian.PutUint32(out[1:5], uint32(maxChunk))
|
||||
return out
|
||||
}
|
||||
|
||||
func ParseVPNOpened(payload []byte) (int, error) {
|
||||
if err := ParseVPNError(payload); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(payload) == 5 && payload[0] == VPNRespOpened {
|
||||
v := int(binary.BigEndian.Uint32(payload[1:5]))
|
||||
if v < 1 || v > VPNMaxFragment {
|
||||
return 0, errors.New("bad VPN OPENED max chunk")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
if len(payload) == 3 && payload[0] == VPNRespOpened {
|
||||
return int(binary.BigEndian.Uint16(payload[1:3])), nil
|
||||
}
|
||||
return 0, errors.New("bad VPN OPENED response")
|
||||
}
|
||||
|
||||
// PUSH v2 request: cmd(1) sid(16) seq(4) offset(4) total(4) data(N)
|
||||
func BuildVPNPush(sid VPNSessionID, seq uint32, offset, total int, data []byte) ([]byte, error) {
|
||||
if total < 1 || total > VPNMaxBatch || offset < 0 || offset > total || len(data) < 1 || offset+len(data) > total || len(data) > VPNMaxFragment {
|
||||
return nil, errors.New("invalid VPN PUSH fragment")
|
||||
}
|
||||
out := make([]byte, 29+len(data))
|
||||
out[0] = VPNCmdPush
|
||||
copy(out[1:17], sid[:])
|
||||
binary.BigEndian.PutUint32(out[17:21], seq)
|
||||
binary.BigEndian.PutUint32(out[21:25], uint32(offset))
|
||||
binary.BigEndian.PutUint32(out[25:29], uint32(total))
|
||||
copy(out[29:], data)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ParseVPNPush(payload []byte) (sid VPNSessionID, seq uint32, offset, total int, data []byte, err error) {
|
||||
if len(payload) < 30 || payload[0] != VPNCmdPush {
|
||||
err = errors.New("bad VPN PUSH")
|
||||
return
|
||||
}
|
||||
copy(sid[:], payload[1:17])
|
||||
seq = binary.BigEndian.Uint32(payload[17:21])
|
||||
offset = int(binary.BigEndian.Uint32(payload[21:25]))
|
||||
total = int(binary.BigEndian.Uint32(payload[25:29]))
|
||||
data = payload[29:]
|
||||
if total < 1 || total > VPNMaxBatch || offset < 0 || offset > total || len(data) < 1 || len(data) > VPNMaxFragment || offset+len(data) > total {
|
||||
err = errors.New("bad VPN PUSH fragment bounds")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func BuildVPNAck(seq uint32, accepted int) []byte {
|
||||
out := make([]byte, 9)
|
||||
out[0] = VPNRespAck
|
||||
binary.BigEndian.PutUint32(out[1:5], seq)
|
||||
binary.BigEndian.PutUint32(out[5:9], uint32(accepted))
|
||||
return out
|
||||
}
|
||||
|
||||
func ParseVPNAck(payload []byte) (seq uint32, accepted int, err error) {
|
||||
if e := ParseVPNError(payload); e != nil {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
if len(payload) != 9 || payload[0] != VPNRespAck {
|
||||
err = errors.New("bad VPN ACK")
|
||||
return
|
||||
}
|
||||
seq = binary.BigEndian.Uint32(payload[1:5])
|
||||
accepted = int(binary.BigEndian.Uint32(payload[5:9]))
|
||||
return
|
||||
}
|
||||
|
||||
// PULL v2 request: cmd(1) sid(16) ack(4) want(4) offset(4) limit(4)
|
||||
func BuildVPNPull(sid VPNSessionID, ack, want uint32, offset, limit int) ([]byte, error) {
|
||||
if offset < 0 || offset > VPNMaxBatch || limit < 1 || limit > VPNMaxFragment {
|
||||
return nil, errors.New("invalid VPN PULL")
|
||||
}
|
||||
out := make([]byte, 33)
|
||||
out[0] = VPNCmdPull
|
||||
copy(out[1:17], sid[:])
|
||||
binary.BigEndian.PutUint32(out[17:21], ack)
|
||||
binary.BigEndian.PutUint32(out[21:25], want)
|
||||
binary.BigEndian.PutUint32(out[25:29], uint32(offset))
|
||||
binary.BigEndian.PutUint32(out[29:33], uint32(limit))
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ParseVPNPull(payload []byte) (sid VPNSessionID, ack, want uint32, offset, limit int, err error) {
|
||||
if len(payload) != 33 || payload[0] != VPNCmdPull {
|
||||
err = errors.New("bad VPN PULL")
|
||||
return
|
||||
}
|
||||
copy(sid[:], payload[1:17])
|
||||
ack = binary.BigEndian.Uint32(payload[17:21])
|
||||
want = binary.BigEndian.Uint32(payload[21:25])
|
||||
offset = int(binary.BigEndian.Uint32(payload[25:29]))
|
||||
limit = int(binary.BigEndian.Uint32(payload[29:33]))
|
||||
if offset < 0 || offset > VPNMaxBatch || limit < 1 || limit > VPNMaxFragment {
|
||||
err = errors.New("bad VPN PULL bounds")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DATA v2 response: cmd(1) seq(4) offset(4) total(4) data(N)
|
||||
func BuildVPNData(seq uint32, offset, total int, data []byte) []byte {
|
||||
out := make([]byte, 13+len(data))
|
||||
out[0] = VPNRespData
|
||||
binary.BigEndian.PutUint32(out[1:5], seq)
|
||||
binary.BigEndian.PutUint32(out[5:9], uint32(offset))
|
||||
binary.BigEndian.PutUint32(out[9:13], uint32(total))
|
||||
copy(out[13:], data)
|
||||
return out
|
||||
}
|
||||
|
||||
func ParseVPNData(payload []byte) (seq uint32, offset, total int, data []byte, wait bool, err error) {
|
||||
if e := ParseVPNError(payload); e != nil {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
if len(payload) == 1 && payload[0] == VPNRespWait {
|
||||
wait = true
|
||||
return
|
||||
}
|
||||
if len(payload) < 14 || payload[0] != VPNRespData {
|
||||
err = fmt.Errorf("bad VPN DATA response type/length")
|
||||
return
|
||||
}
|
||||
seq = binary.BigEndian.Uint32(payload[1:5])
|
||||
offset = int(binary.BigEndian.Uint32(payload[5:9]))
|
||||
total = int(binary.BigEndian.Uint32(payload[9:13]))
|
||||
data = payload[13:]
|
||||
if total < 1 || total > VPNMaxBatch || offset < 0 || offset+len(data) > total || len(data) < 1 || len(data) > VPNMaxFragment {
|
||||
err = errors.New("bad VPN DATA bounds")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// A transfer object is a batch of raw IP packets:
|
||||
// version(1), then repeated packetLen(2) + packet bytes.
|
||||
func BuildVPNBatch(packets [][]byte) ([]byte, error) {
|
||||
if len(packets) == 0 {
|
||||
return nil, errors.New("empty VPN batch")
|
||||
}
|
||||
total := 1
|
||||
for _, packet := range packets {
|
||||
if len(packet) < 1 || len(packet) > VPNMaxPacket {
|
||||
return nil, errors.New("invalid IP packet length in VPN batch")
|
||||
}
|
||||
total += 2 + len(packet)
|
||||
if total > VPNMaxBatch {
|
||||
return nil, errors.New("VPN batch exceeds maximum")
|
||||
}
|
||||
}
|
||||
out := make([]byte, total)
|
||||
out[0] = VPNBatchVersion
|
||||
pos := 1
|
||||
for _, packet := range packets {
|
||||
binary.BigEndian.PutUint16(out[pos:pos+2], uint16(len(packet)))
|
||||
pos += 2
|
||||
copy(out[pos:pos+len(packet)], packet)
|
||||
pos += len(packet)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ParseVPNBatch(batch []byte) ([][]byte, error) {
|
||||
if len(batch) < 4 || len(batch) > VPNMaxBatch || batch[0] != VPNBatchVersion {
|
||||
return nil, errors.New("bad VPN batch")
|
||||
}
|
||||
packets := make([][]byte, 0, 8)
|
||||
pos := 1
|
||||
for pos < len(batch) {
|
||||
if pos+2 > len(batch) {
|
||||
return nil, errors.New("truncated VPN batch packet length")
|
||||
}
|
||||
n := int(binary.BigEndian.Uint16(batch[pos : pos+2]))
|
||||
pos += 2
|
||||
if n < 1 || n > VPNMaxPacket || pos+n > len(batch) {
|
||||
return nil, errors.New("invalid VPN batch packet")
|
||||
}
|
||||
packet := make([]byte, n)
|
||||
copy(packet, batch[pos:pos+n])
|
||||
packets = append(packets, packet)
|
||||
pos += n
|
||||
}
|
||||
if len(packets) == 0 {
|
||||
return nil, errors.New("VPN batch contains no packets")
|
||||
}
|
||||
return packets, nil
|
||||
}
|
||||
|
||||
// PacketAddresses returns the source and destination addresses from a raw
|
||||
// IPv4/IPv6 packet. The packet may contain trailing bytes; the IP header's own
|
||||
// length field is validated against the supplied buffer.
|
||||
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 BuildVPNClose(sid VPNSessionID) []byte {
|
||||
out := make([]byte, 17)
|
||||
out[0] = VPNCmdClose
|
||||
copy(out[1:17], sid[:])
|
||||
return out
|
||||
}
|
||||
|
||||
func ParseVPNClose(payload []byte) (sid VPNSessionID, err error) {
|
||||
if len(payload) != 17 || payload[0] != VPNCmdClose {
|
||||
return sid, errors.New("bad VPN CLOSE")
|
||||
}
|
||||
copy(sid[:], payload[1:17])
|
||||
return sid, nil
|
||||
}
|
||||
|
||||
func IsVPNCommand(payload []byte) bool {
|
||||
if len(payload) == 0 {
|
||||
return false
|
||||
}
|
||||
return payload[0] >= VPNCmdOpen && payload[0] <= VPNCmdClose
|
||||
}
|
||||
Reference in New Issue
Block a user