1195 lines
40 KiB
Go
1195 lines
40 KiB
Go
package main
|
||
|
||
// This file integrates a minimal DNS‑tunnel server (dnstt) into the main
|
||
// application. It is adapted from the public‑domain dnstt-server project
|
||
// (see https://www.bamsoftware.com/software/dnstt/) but modified to
|
||
// terminate streams into the existing SSH handler (handleConn) rather than
|
||
// forwarding them to an upstream TCP service. Each stream accepted via
|
||
// dnstt is wrapped as a net.Conn and passed to handleConn. The DNS
|
||
// transport itself uses KCP over UDP, Noise encryption and smux
|
||
// multiplexing as in the original dnstt.
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/base32"
|
||
"encoding/binary"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"log"
|
||
"net"
|
||
"net/http"
|
||
"os"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"github.com/xtaci/kcp-go/v5"
|
||
"github.com/xtaci/smux"
|
||
"golang.org/x/crypto/ssh"
|
||
"www.bamsoftware.com/git/dnstt.git/dns"
|
||
"www.bamsoftware.com/git/dnstt.git/noise"
|
||
"www.bamsoftware.com/git/dnstt.git/turbotunnel"
|
||
)
|
||
|
||
// ---------- Hot-reload stop mechanism ----------
|
||
|
||
var (
|
||
dnsttConnMu sync.Mutex
|
||
dnsttConn net.PacketConn // active UDP socket; closing it stops runDNSTT
|
||
)
|
||
|
||
// stopDNSTT closes the active DNSTT UDP listener, causing runDNSTT to exit.
|
||
// It is a no-op if DNSTT is not running.
|
||
func stopDNSTT() {
|
||
dnsttConnMu.Lock()
|
||
defer dnsttConnMu.Unlock()
|
||
if dnsttConn != nil {
|
||
_ = dnsttConn.Close()
|
||
dnsttConn = nil
|
||
}
|
||
}
|
||
|
||
// Constants mirrored from dnstt-server. See dnstt-server/main.go for
|
||
// commentary.
|
||
const (
|
||
// smux streams will be closed after this much time without receiving data.
|
||
idleTimeout = 2 * time.Minute
|
||
// How to set the TTL field in Answer resource records.
|
||
responseTTL = 60
|
||
// How long we may wait for downstream data before sending an empty
|
||
// response. This number should be less than 2 seconds (Quad9 DNS
|
||
// timeout as of 2019).
|
||
maxResponseDelay = 1 * time.Second
|
||
)
|
||
|
||
// We don't send UDP payloads larger than this, in an attempt to avoid
|
||
// network-layer fragmentation. 1280 is the minimum IPv6 MTU, 40 bytes is
|
||
// the size of an IPv6 header (without extension headers), and 8 bytes is
|
||
// the size of a UDP header【561853413345496†L97-L109】.
|
||
// Control this value with the -mtu command-line option in the standalone
|
||
// dnstt-server. Here we use the default.
|
||
// maxUDPPayload defines the maximum UDP payload we ever send in a DNS
|
||
// response. It defaults to the IPv6 minimum MTU minus the IPv6 and UDP
|
||
// header sizes (1232 octets) but is clamped per‑query in responseFor and
|
||
// sendLoop based on the EDNS UDP payload advertised by the client. See
|
||
// startDNSTT for how this may be overridden via configuration.
|
||
var maxUDPPayload = 1280 - 40 - 8
|
||
|
||
// noEDNSFallbackPayload is the assumed UDP payload capability for resolver
|
||
// paths that do not include an EDNS(0) OPT RR (or that clamp it to 512) but
|
||
// still reliably carry larger UDP DNS messages. Many DNSTT deployments rely
|
||
// on this behaviour in the wild. This value is used as a *floor* for the
|
||
// inferred payload limit and is still clamped by maxUDPPayload.
|
||
//
|
||
// If you want strict RFC behaviour, set this to 512.
|
||
const noEDNSFallbackPayload = 932
|
||
|
||
// dnsttPrintStats controls whether periodic statistics are printed to stderr.
|
||
// It is set based on the DNSTT configuration provided by the main program.
|
||
// When false, the periodic stats will still be collected and made available
|
||
// via the admin API, but no log lines will be emitted. The default is
|
||
// true to preserve existing behaviour.
|
||
var dnsttPrintStats = true
|
||
|
||
// DnsttStatsSnapshot holds a recent snapshot of DNSTT counters over the
|
||
// previous 5‑second window. It is updated every 5 seconds by the
|
||
// runDNSTT goroutine. The Timestamp field records when the snapshot was
|
||
// taken. These values are surfaced via the admin API so that the web
|
||
// panel can display tunnel health without reading stderr logs.
|
||
type DnsttStatsSnapshot struct {
|
||
Timestamp time.Time `json:"timestamp"`
|
||
DNSRx uint64 `json:"dns_rx"`
|
||
ParseErr uint64 `json:"parse_err"`
|
||
NoEDNS uint64 `json:"no_edns"`
|
||
Limit512 uint64 `json:"limit512"`
|
||
RecQueued uint64 `json:"rec_queued"`
|
||
RecDropped uint64 `json:"rec_dropped"`
|
||
RespSent uint64 `json:"resp_sent"`
|
||
RespBytes uint64 `json:"resp_bytes"`
|
||
RespEmpty uint64 `json:"resp_empty"`
|
||
RespData uint64 `json:"resp_data"`
|
||
RespOversize uint64 `json:"resp_oversize"`
|
||
KCPNew uint64 `json:"kcp_new"`
|
||
KCPEnd uint64 `json:"kcp_end"`
|
||
SmuxNew uint64 `json:"smux_new"`
|
||
SmuxEnd uint64 `json:"smux_end"`
|
||
ChLen int `json:"ch_len"`
|
||
}
|
||
|
||
var (
|
||
dnsttStatsMu sync.Mutex
|
||
lastDnsttStats DnsttStatsSnapshot
|
||
)
|
||
|
||
// GetDNSTTStatsSnapshot returns the most recent DNSTT stats snapshot.
|
||
// It is safe for concurrent use by HTTP handlers and always returns a
|
||
// defensive copy of the snapshot.
|
||
func GetDNSTTStatsSnapshot() DnsttStatsSnapshot {
|
||
dnsttStatsMu.Lock()
|
||
defer dnsttStatsMu.Unlock()
|
||
return lastDnsttStats
|
||
}
|
||
|
||
// dnsttCounters holds aggregated counters used to debug tunnel instability.
|
||
// All fields are updated via sync/atomic.
|
||
type dnsttCounters struct {
|
||
// StreamSeq is a monotonically increasing identifier used to tag smux
|
||
// streams as they are handed off to the SSH handler.
|
||
StreamSeq uint64
|
||
DNSRx uint64
|
||
DNSParseErr uint64
|
||
// NoEDNS counts DNS queries without an EDNS(0) OPT RR. In many real-world
|
||
// deployments (especially mobile / carrier resolvers), EDNS may be stripped
|
||
// even though the path can carry UDP responses larger than 512 bytes.
|
||
NoEDNS uint64
|
||
// SmallEDNS counts queries where the inferred/advertised UDP payload limit
|
||
// is at the classic DNS size (512 bytes). Kept for backward-compatible
|
||
// stats output.
|
||
SmallEDNS uint64
|
||
RecQueued uint64
|
||
RecDropped uint64
|
||
RespSent uint64
|
||
RespSentBytes uint64
|
||
RespEmpty uint64
|
||
RespWithData uint64
|
||
RespOversize uint64
|
||
KCPSessionsNew uint64
|
||
KCPSessionsEnd uint64
|
||
SmuxStreamsNew uint64
|
||
SmuxStreamsEnd uint64
|
||
}
|
||
|
||
var dnsttStats dnsttCounters
|
||
|
||
var maxEncodedPayloadCache sync.Map // map[int]int
|
||
|
||
// dnsttClientPayloadCap tracks an inferred per-client UDP payload capability.
|
||
// Keyed by the turbotunnel "remote address" string (a hex ClientID). We use
|
||
// this to set a per-session KCP MTU so the tunnel can function on paths that
|
||
// do not send EDNS(0) but still support UDP payloads > 512.
|
||
// dnsttClientPayloadCap stores per-client capability with a last-seen timestamp.
|
||
// This needs periodic cleanup to avoid unbounded growth when many unique client
|
||
// IDs are observed over time.
|
||
type clientCapEntry struct {
|
||
Cap int
|
||
LastSeen int64 // unix nano
|
||
}
|
||
|
||
var dnsttClientPayloadCap sync.Map // map[string]clientCapEntry
|
||
|
||
var dnsttCapReaperOnce sync.Once
|
||
|
||
func startDNSTTCapReaper() {
|
||
// Reap old client capability entries so the map can't grow forever.
|
||
// Defaults chosen to be conservative: keep "active" client IDs for 6 hours.
|
||
const (
|
||
reapEvery = 10 * time.Minute
|
||
maxAge = 6 * time.Hour
|
||
)
|
||
dnsttCapReaperOnce.Do(func() {
|
||
go func() {
|
||
t := time.NewTicker(reapEvery)
|
||
defer t.Stop()
|
||
for range t.C {
|
||
cutoff := time.Now().Add(-maxAge).UnixNano()
|
||
dnsttClientPayloadCap.Range(func(k, v any) bool {
|
||
e, ok := v.(clientCapEntry)
|
||
if ok && e.LastSeen > 0 && e.LastSeen < cutoff {
|
||
dnsttClientPayloadCap.Delete(k)
|
||
}
|
||
return true
|
||
})
|
||
}
|
||
}()
|
||
})
|
||
}
|
||
|
||
func dnsttClientKey(clientID turbotunnel.ClientID) string {
|
||
return fmt.Sprintf("%x", clientID[:])
|
||
}
|
||
|
||
func updateClientPayloadCap(clientID turbotunnel.ClientID, cap int) {
|
||
if cap <= 0 {
|
||
return
|
||
}
|
||
key := dnsttClientKey(clientID)
|
||
now := time.Now().UnixNano()
|
||
if v, ok := dnsttClientPayloadCap.Load(key); ok {
|
||
e := v.(clientCapEntry)
|
||
if cap > e.Cap {
|
||
e.Cap = cap
|
||
}
|
||
e.LastSeen = now
|
||
dnsttClientPayloadCap.Store(key, e)
|
||
return
|
||
}
|
||
dnsttClientPayloadCap.Store(key, clientCapEntry{Cap: cap, LastSeen: now})
|
||
}
|
||
|
||
func cachedMaxEncodedPayload(limit int) int {
|
||
if limit <= 0 {
|
||
return 0
|
||
}
|
||
if v, ok := maxEncodedPayloadCache.Load(limit); ok {
|
||
return v.(int)
|
||
}
|
||
m := computeMaxEncodedPayload(limit)
|
||
maxEncodedPayloadCache.Store(limit, m)
|
||
return m
|
||
}
|
||
|
||
// base32Encoding is a base32 encoding without padding, as used by dnstt.
|
||
var base32Encoding = base32.StdEncoding.WithPadding(base32.NoPadding)
|
||
|
||
// dnsttSSHConfig holds the SSH server configuration used by the DNS tunnel.
|
||
// It is set by startDNSTT before any dnstt sessions are accepted.
|
||
var dnsttSSHConfig *ssh.ServerConfig
|
||
|
||
// dnsttLog is a dedicated logger for the integrated DNSTT server. It writes
|
||
// to stderr and uses a per-log prefix and microsecond precision. Unlike the
|
||
// global log.Logger used by the rest of the application, this logger is not
|
||
// affected by calls to log.SetOutput in main.go (e.g. when quiet mode is
|
||
// enabled). All log lines emitted by DNSTT should go through dnsttLog so
|
||
// that debugging output remains visible even when other logs are suppressed.
|
||
var dnsttLog = log.New(os.Stderr, "dnstt: ", log.LstdFlags|log.Lmicroseconds)
|
||
|
||
// dnsttLogBuf stores recent DNSTT log lines for the web panel. It acts as a
|
||
// circular buffer retaining the last N log lines. The capacity is set when
|
||
// the DNSTT server is started, typically around 100 lines to stay well
|
||
// within the configured memory budget. Lines are stored without the
|
||
// trailing newline. Access to the buffer is synchronized by an internal
|
||
// mutex.
|
||
type dnsttLogBuffer struct {
|
||
mu sync.Mutex
|
||
lines []string
|
||
maxLines int
|
||
}
|
||
|
||
// newDNSTTLogBuffer constructs a new ring buffer with the given capacity.
|
||
// The capacity must be positive.
|
||
func newDNSTTLogBuffer(maxLines int) *dnsttLogBuffer {
|
||
if maxLines <= 0 {
|
||
maxLines = 100
|
||
}
|
||
return &dnsttLogBuffer{
|
||
lines: make([]string, 0, maxLines),
|
||
maxLines: maxLines,
|
||
}
|
||
}
|
||
|
||
// Write implements io.Writer and appends complete lines from p to the
|
||
// buffer. It splits on '\n' and discards empty segments. When the
|
||
// buffer reaches its maximum length, the oldest lines are removed to make
|
||
// room for new ones.
|
||
func (b *dnsttLogBuffer) Write(p []byte) (n int, err error) {
|
||
b.mu.Lock()
|
||
defer b.mu.Unlock()
|
||
s := string(p)
|
||
// Split incoming data by newline. We intentionally discard empty
|
||
// strings to avoid blank lines from being stored.
|
||
parts := strings.Split(s, "\n")
|
||
for _, part := range parts {
|
||
if part == "" {
|
||
continue
|
||
}
|
||
if len(b.lines) < b.maxLines {
|
||
b.lines = append(b.lines, part)
|
||
} else {
|
||
// Shift left by one and append new line at end.
|
||
copy(b.lines, b.lines[1:])
|
||
b.lines[len(b.lines)-1] = part
|
||
}
|
||
}
|
||
return len(p), nil
|
||
}
|
||
|
||
// GetLines returns a copy of the current log lines. The lines are
|
||
// returned in chronological order from oldest to newest.
|
||
func (b *dnsttLogBuffer) GetLines() []string {
|
||
b.mu.Lock()
|
||
defer b.mu.Unlock()
|
||
out := make([]string, len(b.lines))
|
||
copy(out, b.lines)
|
||
return out
|
||
}
|
||
|
||
// global log buffer for DNSTT logs. It is initialised when the server
|
||
// starts. Access via getDNSTTLogLines for concurrency safety.
|
||
var dnsttLogBuf *dnsttLogBuffer
|
||
|
||
// getDNSTTLogLines returns the current DNSTT log lines in order. If the
|
||
// buffer has not been initialised, it returns an empty slice.
|
||
func getDNSTTLogLines() []string {
|
||
if dnsttLogBuf == nil {
|
||
return nil
|
||
}
|
||
return dnsttLogBuf.GetLines()
|
||
}
|
||
|
||
// startDNSTT starts the integrated dnstt server if cfg is non-nil. It reads
|
||
// the Noise private key from cfg.PrivKeyFile, parses cfg.Domain into a dns.Name,
|
||
// and then launches runDNSTT in a goroutine. Any errors during start are
|
||
// logged. The SSH server configuration is used when handling streams.
|
||
func startDNSTT(cfg *DNSTTConfig, sshConf *ssh.ServerConfig) error {
|
||
if cfg == nil {
|
||
return nil
|
||
}
|
||
startDNSTTCapReaper()
|
||
dnsttSSHConfig = sshConf
|
||
// Configure whether periodic DNSTT statistics should be emitted to stderr.
|
||
// When DisableStatsLog is true, stats will be collected but log lines are suppressed.
|
||
dnsttPrintStats = !cfg.DisableStatsLog
|
||
// Initialise the log buffer once. Use a capacity of 100 lines (~few KB).
|
||
if dnsttLogBuf == nil {
|
||
dnsttLogBuf = newDNSTTLogBuffer(100)
|
||
}
|
||
// Configure the DNSTT logger output. If DisableConsoleLog is set,
|
||
// write only to the buffer; otherwise tee to both the buffer and stderr.
|
||
if cfg.DisableConsoleLog {
|
||
dnsttLog.SetOutput(dnsttLogBuf)
|
||
} else {
|
||
dnsttLog.SetOutput(io.MultiWriter(dnsttLogBuf, os.Stderr))
|
||
}
|
||
|
||
// Read the private key from file.
|
||
f, err := os.Open(cfg.PrivKeyFile)
|
||
if err != nil {
|
||
msg := fmt.Errorf("cannot open privkey file %s: %w", cfg.PrivKeyFile, err)
|
||
dnsttLog.Print(msg.Error())
|
||
return msg
|
||
}
|
||
privkey, err := noise.ReadKey(f)
|
||
f.Close()
|
||
if err != nil {
|
||
msg := fmt.Errorf("cannot read privkey from file: %w", err)
|
||
dnsttLog.Print(msg.Error())
|
||
return msg
|
||
}
|
||
// Parse the domain name. dns.ParseName accepts a domain with a trailing
|
||
// dot or without. Any error here will abort the dnstt server.
|
||
domain, err := dns.ParseName(cfg.Domain)
|
||
if err != nil {
|
||
msg := fmt.Errorf("invalid domain %q: %w", cfg.Domain, err)
|
||
dnsttLog.Print(msg.Error())
|
||
return msg
|
||
}
|
||
udpListen := cfg.UDPListen
|
||
if udpListen == "" {
|
||
udpListen = defaultDNSTTListen
|
||
cfg.UDPListen = udpListen
|
||
}
|
||
|
||
// Bind synchronously so the admin panel can immediately know whether DNSTT
|
||
// really started or failed because of a bad address/locked port.
|
||
dnsConn, err := net.ListenPacket("udp", udpListen)
|
||
if err != nil {
|
||
msg := fmt.Errorf("dnstt: opening UDP listener on %s: %w", udpListen, err)
|
||
dnsttLog.Print(msg.Error())
|
||
return msg
|
||
}
|
||
|
||
// Log initialisation parameters so DNSTT startup is visible even when
|
||
// quiet logging is enabled. This helps with debugging.
|
||
dnsttLog.Printf("starting: domain=%q udp_listen=%q privkey=%q", cfg.Domain, udpListen, cfg.PrivKeyFile)
|
||
go func() {
|
||
if err := runDNSTTOnConn(privkey, domain, udpListen, dnsConn); err != nil && !errors.Is(err, net.ErrClosed) {
|
||
dnsttLog.Printf("server exited with error: %v", err)
|
||
}
|
||
}()
|
||
return nil
|
||
}
|
||
|
||
func dnsttRunning() bool {
|
||
dnsttConnMu.Lock()
|
||
defer dnsttConnMu.Unlock()
|
||
return dnsttConn != nil
|
||
}
|
||
|
||
// handleDNSTTStream accepts a smux.Stream from a client and hands it off to
|
||
// handleConn. The stream is wrapped as a net.Conn by streamConn so that
|
||
// handleConn sees a minimal net.Conn interface. This function blocks until
|
||
// handleConn returns, then returns nil. Any errors from handleConn are
|
||
// ignored; they are logged by handleConn itself.
|
||
func handleDNSTTStream(stream *smux.Stream, conv uint32) error {
|
||
// Assign a per-stream sequence number to help correlate open/close events.
|
||
sid := atomic.AddUint64(&dnsttStats.StreamSeq, 1)
|
||
start := time.Now()
|
||
dnsttLog.Printf("ssh stream begin: conv=%d sid=%d", conv, sid)
|
||
sc := &streamConn{Stream: stream}
|
||
// Delegate to the existing SSH connection handler. This call blocks
|
||
// until the SSH connection terminates. The smux stream will be closed
|
||
// by handleConn when it returns.
|
||
handleConn(sc, dnsttSSHConfig)
|
||
dnsttLog.Printf("ssh stream end: conv=%d sid=%d duration=%s", conv, sid, time.Since(start))
|
||
return nil
|
||
}
|
||
|
||
// streamConn adapts a smux.Stream to the net.Conn interface expected by
|
||
// handleConn. smux.Stream already implements Read and Write, but does not
|
||
// satisfy net.Conn because it lacks methods for deadlines and addresses. We
|
||
// implement those methods with no‑ops and placeholder addresses.
|
||
type streamConn struct {
|
||
*smux.Stream
|
||
}
|
||
|
||
func (s *streamConn) LocalAddr() net.Addr { return dummyAddr{} }
|
||
func (s *streamConn) RemoteAddr() net.Addr { return dummyAddr{} }
|
||
func (s *streamConn) SetDeadline(t time.Time) error { return nil }
|
||
func (s *streamConn) SetReadDeadline(t time.Time) error { return nil }
|
||
func (s *streamConn) SetWriteDeadline(t time.Time) error { return nil }
|
||
|
||
// dummyAddr is a stand‑in net.Addr implementation for dnstt streams. It
|
||
// reports a generic network and address; this satisfies the net.Conn
|
||
// interface. handleConn logs the remote address, but here we provide
|
||
// "dnstt" as both network and address to indicate a tunnelled connection.
|
||
type dummyAddr struct{}
|
||
|
||
func (d dummyAddr) Network() string { return "dnstt" }
|
||
func (d dummyAddr) String() string { return "dnstt" }
|
||
|
||
// acceptDNSTTStreams wraps a KCP session in a Noise channel and an smux
|
||
// Session, then waits for smux streams. Each stream is passed to
|
||
// handleDNSTTStream. Any errors from the Noise or smux layers are returned.
|
||
func acceptDNSTTStreams(conn *kcp.UDPSession, privkey []byte) error {
|
||
// Put a Noise channel on top of the KCP conn.
|
||
rw, err := noise.NewServer(conn, privkey)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// Put an smux session on top of the encrypted Noise channel.
|
||
smuxConfig := smux.DefaultConfig()
|
||
smuxConfig.Version = 2
|
||
smuxConfig.KeepAliveTimeout = idleTimeout
|
||
smuxConfig.MaxStreamBuffer = 1 * 1024 * 1024
|
||
sess, err := smux.Server(rw, smuxConfig)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer sess.Close()
|
||
|
||
for {
|
||
stream, err := sess.AcceptStream()
|
||
if err != nil {
|
||
if err, ok := err.(net.Error); ok && err.Temporary() {
|
||
continue
|
||
}
|
||
return err
|
||
}
|
||
// Log the creation of each new smux stream. Reporting the conv helps
|
||
// to correlate streams with their parent KCP session.
|
||
atomic.AddUint64(&dnsttStats.SmuxStreamsNew, 1)
|
||
dnsttLog.Printf("new smux stream: conv=%d", conn.GetConv())
|
||
// For each new smux stream, hand it off to our SSH handler.
|
||
go func(s *smux.Stream, conv uint32) {
|
||
defer s.Close()
|
||
_ = handleDNSTTStream(s, conv)
|
||
atomic.AddUint64(&dnsttStats.SmuxStreamsEnd, 1)
|
||
dnsttLog.Printf("smux stream closed: conv=%d", conv)
|
||
}(stream, conn.GetConv())
|
||
}
|
||
}
|
||
|
||
// acceptDNSTTSessions listens for incoming KCP connections and passes them to
|
||
// acceptDNSTTStreams. It configures window sizes and MTU on each accepted
|
||
// session as in the original dnstt-server.
|
||
func acceptDNSTTSessions(ln *kcp.Listener, privkey []byte, mtu int) error {
|
||
for {
|
||
conn, err := ln.AcceptKCP()
|
||
if err != nil {
|
||
if err, ok := err.(net.Error); ok && err.Temporary() {
|
||
continue
|
||
}
|
||
return err
|
||
}
|
||
from := conn.RemoteAddr().String()
|
||
// Choose a per-session MTU derived from the inferred/advertised UDP payload
|
||
// capability for this client. This is essential on paths that clamp UDP
|
||
// payloads below our global cap (e.g. ~930), because KCP packets larger than
|
||
// what can fit in a single DNS response will cause the tunnel to stall.
|
||
effectiveLimit := maxUDPPayload
|
||
if v, ok := dnsttClientPayloadCap.Load(from); ok {
|
||
effectiveLimit = v.(clientCapEntry).Cap
|
||
if effectiveLimit < 512 {
|
||
effectiveLimit = 512
|
||
}
|
||
if effectiveLimit > maxUDPPayload {
|
||
effectiveLimit = maxUDPPayload
|
||
}
|
||
}
|
||
maxEnc := cachedMaxEncodedPayload(effectiveLimit)
|
||
mtuSession := mtu
|
||
if maxEnc > 0 {
|
||
if m := maxEnc - 2; m >= 80 {
|
||
mtuSession = m
|
||
}
|
||
}
|
||
// Log each newly accepted KCP session. Include conversation ID, remote address
|
||
// (ClientID), and the chosen MTU.
|
||
atomic.AddUint64(&dnsttStats.KCPSessionsNew, 1)
|
||
dnsttLog.Printf("new KCP session: conv=%d from=%s mtu=%d limit=%d", conn.GetConv(), from, mtuSession, effectiveLimit)
|
||
// Permit coalescing the payloads of consecutive sends.
|
||
conn.SetStreamMode(true)
|
||
// Disable the dynamic congestion window (limit only by the maximum of
|
||
// local and remote static windows).
|
||
conn.SetNoDelay(0, 0, 0, 1)
|
||
conn.SetWindowSize(turbotunnel.QueueSize/2, turbotunnel.QueueSize/2)
|
||
if rc := conn.SetMtu(mtuSession); !rc {
|
||
panic(rc)
|
||
}
|
||
go func(c *kcp.UDPSession, conv uint32, from string) {
|
||
defer c.Close()
|
||
err := acceptDNSTTStreams(c, privkey)
|
||
atomic.AddUint64(&dnsttStats.KCPSessionsEnd, 1)
|
||
if err != nil && err != io.ErrClosedPipe {
|
||
dnsttLog.Printf("kcp session closed: conv=%d from=%s err=%v", conv, from, err)
|
||
} else {
|
||
dnsttLog.Printf("kcp session closed: conv=%d from=%s", conv, from)
|
||
}
|
||
}(conn, conn.GetConv(), conn.RemoteAddr().String())
|
||
}
|
||
}
|
||
|
||
// record represents a DNS message appropriate for a response to a previously
|
||
// received query, along with metadata necessary for sending the response.
|
||
// recvLoop sends instances of record to sendLoop via a channel. sendLoop
|
||
// receives instances of record and may fill in the message's Answer section
|
||
// before sending it.
|
||
type record struct {
|
||
Resp *dns.Message
|
||
Addr net.Addr
|
||
ClientID turbotunnel.ClientID
|
||
// PayloadLimit holds the maximum UDP payload size advertised by the
|
||
// client via EDNS(0). sendLoop uses this to clamp outgoing DNS
|
||
// responses so they never exceed what the client claims it will
|
||
// accept. A zero value means no per‑client limit and defaults to
|
||
// maxUDPPayload.
|
||
PayloadLimit int
|
||
}
|
||
|
||
// nextPacket reads the next length‑prefixed packet from r, ignoring padding.
|
||
// It returns a nil error only when a packet was read successfully. It
|
||
// returns io.EOF only when there were 0 bytes remaining to read from r. It
|
||
// returns io.ErrUnexpectedEOF when EOF occurs in the middle of an encoded
|
||
// packet. See dnstt-server/main.go for details.
|
||
func nextPacket(r *bytes.Reader) ([]byte, error) {
|
||
eof := func(err error) error {
|
||
if err == io.EOF {
|
||
err = io.ErrUnexpectedEOF
|
||
}
|
||
return err
|
||
}
|
||
for {
|
||
prefix, err := r.ReadByte()
|
||
if err != nil {
|
||
// We may return a real io.EOF only here.
|
||
return nil, err
|
||
}
|
||
if prefix >= 224 {
|
||
paddingLen := prefix - 224
|
||
_, err := io.CopyN(io.Discard, r, int64(paddingLen))
|
||
if err != nil {
|
||
return nil, eof(err)
|
||
}
|
||
} else {
|
||
p := make([]byte, int(prefix))
|
||
_, err = io.ReadFull(r, p)
|
||
return p, eof(err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// responseFor constructs a response dns.Message that is appropriate for query.
|
||
// Along with the dns.Message, it returns the query's decoded data payload. If
|
||
// the returned dns.Message is nil, it means that there should be no response
|
||
// to this query. If the returned dns.Message has an Rcode() of
|
||
// dns.RcodeNoError, the message is a candidate for carrying downstream data
|
||
// in a TXT record. This function is adapted from dnstt-server/main.go.
|
||
func responseFor(query *dns.Message, domain dns.Name) (*dns.Message, []byte) {
|
||
resp := &dns.Message{
|
||
ID: query.ID,
|
||
Flags: 0x8000, // QR = 1, RCODE = no error
|
||
Question: query.Question,
|
||
}
|
||
if query.Flags&0x8000 != 0 {
|
||
// QR != 0, this is not a query. Don't even send a response.
|
||
return nil, nil
|
||
}
|
||
// Check for EDNS(0) support. Include our own OPT RR only if we receive
|
||
// one from the requester.
|
||
payloadSize := 0
|
||
for _, rr := range query.Additional {
|
||
if rr.Type != dns.RRTypeOPT {
|
||
continue
|
||
}
|
||
version := (rr.TTL >> 16) & 0xff
|
||
if version != 0 {
|
||
resp.Flags |= dns.ExtendedRcodeBadVers & 0xf
|
||
additional := dns.RR{
|
||
Name: dns.Name{},
|
||
Type: dns.RRTypeOPT,
|
||
Class: 0,
|
||
TTL: (dns.ExtendedRcodeBadVers >> 4) << 24,
|
||
Data: []byte{},
|
||
}
|
||
resp.Additional = append(resp.Additional, additional)
|
||
return resp, nil
|
||
}
|
||
payloadSize = int(rr.Class)
|
||
}
|
||
if payloadSize < 512 {
|
||
payloadSize = 512
|
||
}
|
||
// There must be exactly one question.
|
||
if len(query.Question) != 1 {
|
||
resp.Flags |= dns.RcodeFormatError
|
||
dnsttLog.Printf("FORMERR: too few or too many questions (%d)", len(query.Question))
|
||
return resp, nil
|
||
}
|
||
question := query.Question[0]
|
||
// Check the name to see if it ends in our chosen domain, and extract
|
||
// all that comes before the domain if it does. If it does not, we
|
||
// return RcodeNameError.
|
||
prefix, ok := question.Name.TrimSuffix(domain)
|
||
if !ok {
|
||
resp.Flags |= dns.RcodeNameError
|
||
// NXDOMAIN: not authoritative for this name
|
||
return resp, nil
|
||
}
|
||
resp.Flags |= 0x0400 // AA = 1
|
||
if query.Opcode() != 0 {
|
||
resp.Flags |= dns.RcodeNotImplemented
|
||
return resp, nil
|
||
}
|
||
if question.Type != dns.RRTypeTXT {
|
||
// We only support QTYPE == TXT.
|
||
resp.Flags |= dns.RcodeNameError
|
||
return resp, nil
|
||
}
|
||
encoded := bytes.ToUpper(bytes.Join(prefix, nil))
|
||
payload := make([]byte, base32Encoding.DecodedLen(len(encoded)))
|
||
n, err := base32Encoding.Decode(payload, encoded)
|
||
if err != nil {
|
||
resp.Flags |= dns.RcodeNameError
|
||
return resp, nil
|
||
}
|
||
payload = payload[:n]
|
||
// Do not reject queries advertising a smaller EDNS UDP payload than
|
||
// maxUDPPayload. We clamp responses to the client‑advertised size
|
||
// later in sendLoop.
|
||
return resp, payload
|
||
}
|
||
|
||
// recvLoop repeatedly calls dnsConn.ReadFrom, extracts the packets contained in
|
||
// the incoming DNS queries, and puts them on ttConn's incoming queue.
|
||
// Whenever a query calls for a response, constructs a partial response and
|
||
// passes it to sendLoop over ch.
|
||
func recvLoop(domain dns.Name, dnsConn net.PacketConn, ttConn *turbotunnel.QueuePacketConn, ch chan<- *record) error {
|
||
for {
|
||
var buf [4096]byte
|
||
n, addr, err := dnsConn.ReadFrom(buf[:])
|
||
if err != nil {
|
||
if err, ok := err.(net.Error); ok && err.Temporary() {
|
||
dnsttLog.Printf("ReadFrom temporary error: %v", err)
|
||
continue
|
||
}
|
||
return err
|
||
}
|
||
atomic.AddUint64(&dnsttStats.DNSRx, 1)
|
||
// Parse DNS query.
|
||
query, err := dns.MessageFromWireFormat(buf[:n])
|
||
if err != nil {
|
||
atomic.AddUint64(&dnsttStats.DNSParseErr, 1)
|
||
dnsttLog.Printf("cannot parse DNS query: %v", err)
|
||
continue
|
||
}
|
||
// Determine an effective UDP payload limit for this query.
|
||
//
|
||
// Prefer EDNS(0) if present. However, many resolver paths strip EDNS while
|
||
// still allowing UDP responses larger than 512. For those paths, infer a
|
||
// practical limit from the observed query size (if we received a ~900-byte
|
||
// query, the path clearly supports >512 UDP). Clamp to our global cap.
|
||
payloadLimit := 0
|
||
hasEDNS := false
|
||
for _, rr := range query.Additional {
|
||
if rr.Type != dns.RRTypeOPT {
|
||
continue
|
||
}
|
||
hasEDNS = true
|
||
// The lower 16 bits of the Class field of the OPT RR specify the
|
||
// requestor's maximum UDP payload size (RFC 6891).
|
||
sz := int(rr.Class)
|
||
if sz < 512 {
|
||
sz = 512
|
||
}
|
||
payloadLimit = sz
|
||
break
|
||
}
|
||
if !hasEDNS {
|
||
atomic.AddUint64(&dnsttStats.NoEDNS, 1)
|
||
payloadLimit = n
|
||
if payloadLimit < 512 {
|
||
payloadLimit = 512
|
||
}
|
||
} else {
|
||
// If EDNS is present but appears to under-advertise the true path MTU,
|
||
// treat the observed query size as a lower bound. Some resolvers clamp or
|
||
// rewrite EDNS values even though they can carry larger UDP payloads.
|
||
if n > payloadLimit {
|
||
payloadLimit = n
|
||
}
|
||
}
|
||
// Many resolver paths do not include EDNS (or clamp it to 512) but still
|
||
// reliably carry larger UDP DNS messages (commonly ~900 bytes). For DNSTT
|
||
// this matters because downstream data rides in responses; treating such
|
||
// paths as strictly 512-byte causes the tunnel to stall.
|
||
//
|
||
// noEDNSFallbackPayload acts as a floor for these cases; if you want strict
|
||
// RFC 1035 behaviour, set it to 512.
|
||
if payloadLimit < noEDNSFallbackPayload {
|
||
payloadLimit = noEDNSFallbackPayload
|
||
}
|
||
if payloadLimit > maxUDPPayload {
|
||
payloadLimit = maxUDPPayload
|
||
}
|
||
if payloadLimit == 512 {
|
||
atomic.AddUint64(&dnsttStats.SmallEDNS, 1)
|
||
}
|
||
resp, payload := responseFor(&query, domain)
|
||
// Extract ClientID
|
||
var clientID turbotunnel.ClientID
|
||
n = copy(clientID[:], payload)
|
||
payload = payload[n:]
|
||
if n == len(clientID) {
|
||
// Update our per-client capability estimate so we can choose a suitable
|
||
// per-session KCP MTU even when EDNS is stripped.
|
||
updateClientPayloadCap(clientID, payloadLimit)
|
||
// Feed packets into KCP.
|
||
r := bytes.NewReader(payload)
|
||
for {
|
||
p, err := nextPacket(r)
|
||
if err != nil {
|
||
break
|
||
}
|
||
ttConn.QueueIncoming(p, clientID)
|
||
}
|
||
} else {
|
||
if resp != nil && resp.Rcode() == dns.RcodeNoError {
|
||
resp.Flags |= dns.RcodeNameError
|
||
}
|
||
}
|
||
if resp != nil {
|
||
// Push this record along with the per‑client payload limit to the sender.
|
||
rec := &record{
|
||
Resp: resp,
|
||
Addr: addr,
|
||
ClientID: clientID,
|
||
PayloadLimit: payloadLimit,
|
||
}
|
||
select {
|
||
case ch <- rec:
|
||
atomic.AddUint64(&dnsttStats.RecQueued, 1)
|
||
default:
|
||
d := atomic.AddUint64(&dnsttStats.RecDropped, 1)
|
||
// Log occasionally to avoid flooding logs under sustained overload.
|
||
if d == 1 || d%1000 == 0 {
|
||
dnsttLog.Printf("dropping response record: ch_len=%d ch_cap=%d dropped=%d", len(ch), cap(ch), d)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// sendLoop repeatedly receives records from ch. Those that represent an
|
||
// error response are sent immediately. Those that represent a response
|
||
// capable of carrying data are packed full of as many packets as will fit
|
||
// while keeping the total size under maxEncodedPayload, then sent.
|
||
func sendLoop(dnsConn net.PacketConn, ttConn *turbotunnel.QueuePacketConn, ch <-chan *record, maxEncodedPayload int) error {
|
||
var nextRec *record
|
||
for {
|
||
rec := nextRec
|
||
nextRec = nil
|
||
if rec == nil {
|
||
var ok bool
|
||
rec, ok = <-ch
|
||
if !ok {
|
||
break
|
||
}
|
||
}
|
||
// Determine the effective per-query UDP payload limit.
|
||
//
|
||
// rec.PayloadLimit comes from EDNS(0) and our heuristic floor/observations.
|
||
// Additionally, we maintain a per-client learned capability (dnsttClientPayloadCap)
|
||
// that can be promoted when we observe that larger downstream responses are
|
||
// needed and appear to be supported. Use the larger of the two, then clamp
|
||
// to our global cap.
|
||
effectivePayloadLimit := rec.PayloadLimit
|
||
if v, ok := dnsttClientPayloadCap.Load(dnsttClientKey(rec.ClientID)); ok {
|
||
if cap := v.(clientCapEntry).Cap; cap > effectivePayloadLimit {
|
||
effectivePayloadLimit = cap
|
||
}
|
||
}
|
||
if effectivePayloadLimit <= 0 {
|
||
effectivePayloadLimit = maxUDPPayload
|
||
}
|
||
if effectivePayloadLimit > maxUDPPayload {
|
||
effectivePayloadLimit = maxUDPPayload
|
||
}
|
||
// Compute a per-limit "max encoded payload" so we avoid generating a DNS
|
||
// response that would need truncation (TC). Truncation breaks DNSTT because
|
||
// resolvers retry over TCP, which we don't support here.
|
||
maxEnc := cachedMaxEncodedPayload(effectivePayloadLimit)
|
||
if maxEnc < 0 {
|
||
maxEnc = 0
|
||
}
|
||
// Note: KCP MTU is set per session in acceptDNSTTSessions based on inferred
|
||
// per-client capability, so we do not warn here about MTU mismatches.
|
||
|
||
if rec.Resp.Rcode() == dns.RcodeNoError && len(rec.Resp.Question) == 1 {
|
||
// It's a non-error response, so we can fill the Answer section.
|
||
rec.Resp.Answer = []dns.RR{{
|
||
Name: rec.Resp.Question[0].Name,
|
||
Type: rec.Resp.Question[0].Type,
|
||
Class: rec.Resp.Question[0].Class,
|
||
TTL: responseTTL,
|
||
Data: nil,
|
||
}}
|
||
var payload bytes.Buffer
|
||
limit := maxEnc
|
||
timer := time.NewTimer(maxResponseDelay)
|
||
packets := 0
|
||
for {
|
||
var p []byte
|
||
unstash := ttConn.Unstash(rec.ClientID)
|
||
outgoing := ttConn.OutgoingQueue(rec.ClientID)
|
||
select {
|
||
case p = <-unstash:
|
||
default:
|
||
select {
|
||
case p = <-unstash:
|
||
case p = <-outgoing:
|
||
default:
|
||
select {
|
||
case p = <-unstash:
|
||
case p = <-outgoing:
|
||
case <-timer.C:
|
||
case nextRec = <-ch:
|
||
}
|
||
}
|
||
}
|
||
timer.Reset(0)
|
||
if len(p) == 0 {
|
||
break
|
||
}
|
||
limit -= 2 + len(p)
|
||
// Never exceed the per-query maximum encoded payload. If the next packet
|
||
// would overflow, stash it for the next response (even if this would have
|
||
// been the first packet).
|
||
if limit < 0 {
|
||
ttConn.Stash(p, rec.ClientID)
|
||
break
|
||
}
|
||
binary.Write(&payload, binary.BigEndian, uint16(len(p)))
|
||
payload.Write(p)
|
||
packets++
|
||
}
|
||
timer.Stop()
|
||
rec.Resp.Answer[0].Data = dns.EncodeRDataTXT(payload.Bytes())
|
||
if packets == 0 {
|
||
atomic.AddUint64(&dnsttStats.RespEmpty, 1)
|
||
} else {
|
||
atomic.AddUint64(&dnsttStats.RespWithData, 1)
|
||
}
|
||
}
|
||
buf, err := rec.Resp.WireFormat()
|
||
if err != nil {
|
||
dnsttLog.Printf("resp WireFormat: %v", err)
|
||
continue
|
||
}
|
||
if len(buf) > effectivePayloadLimit {
|
||
// The resolver may under-advertise (or omit) its UDP payload limit while
|
||
// still accepting larger UDP responses. Rather than dropping downstream
|
||
// data (which can stall DNSTT), treat this as a hint and promote the
|
||
// per-client inferred payload capability.
|
||
atomic.AddUint64(&dnsttStats.RespOversize, 1)
|
||
promoteTo := len(buf)
|
||
if promoteTo > maxUDPPayload {
|
||
dnsttLog.Printf("oversize DNS response: size=%d limit=%d cap=%d (dropping)", len(buf), effectivePayloadLimit, maxUDPPayload)
|
||
continue
|
||
}
|
||
updateClientPayloadCap(rec.ClientID, promoteTo)
|
||
dnsttLog.Printf("oversize DNS response: size=%d limit=%d -> promote client to %d", len(buf), effectivePayloadLimit, promoteTo)
|
||
// After promotion, allow this response to be sent.
|
||
effectivePayloadLimit = promoteTo
|
||
}
|
||
_, err = dnsConn.WriteTo(buf, rec.Addr)
|
||
if err != nil {
|
||
if err, ok := err.(net.Error); ok && err.Temporary() {
|
||
dnsttLog.Printf("WriteTo temporary error: %v", err)
|
||
continue
|
||
}
|
||
return err
|
||
}
|
||
atomic.AddUint64(&dnsttStats.RespSent, 1)
|
||
atomic.AddUint64(&dnsttStats.RespSentBytes, uint64(len(buf)))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// computeMaxEncodedPayload computes the maximum amount of downstream TXT RR
|
||
// data that keep the overall response size less than maxUDPPayload, in the
|
||
// worst case when the response answers a query that has a maximum-length name
|
||
// in its Question section. Returns 0 in the case that no amount of data
|
||
// makes the overall response size small enough.
|
||
func computeMaxEncodedPayload(limit int) int {
|
||
maxLengthName, err := dns.NewName([][]byte{
|
||
[]byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
|
||
[]byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
|
||
[]byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
|
||
[]byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
|
||
})
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
{
|
||
n := 0
|
||
for _, label := range maxLengthName {
|
||
n += len(label) + 1
|
||
}
|
||
n += 1
|
||
if n != 255 {
|
||
panic(fmt.Sprintf("dnstt: max-length name is %d octets, should be %d", n, 255))
|
||
}
|
||
}
|
||
queryLimit := uint16(limit)
|
||
if int(queryLimit) != limit {
|
||
queryLimit = 0xffff
|
||
}
|
||
query := &dns.Message{
|
||
Question: []dns.Question{{
|
||
Name: maxLengthName,
|
||
Type: dns.RRTypeTXT,
|
||
Class: dns.RRTypeTXT,
|
||
}},
|
||
Additional: []dns.RR{{
|
||
Name: dns.Name{},
|
||
Type: dns.RRTypeOPT,
|
||
Class: uint16(queryLimit),
|
||
TTL: 0,
|
||
Data: []byte{},
|
||
}},
|
||
}
|
||
resp, _ := responseFor(query, dns.Name([][]byte{}))
|
||
resp.Answer = []dns.RR{{
|
||
Name: query.Question[0].Name,
|
||
Type: query.Question[0].Type,
|
||
Class: query.Question[0].Class,
|
||
TTL: responseTTL,
|
||
Data: nil,
|
||
}}
|
||
low := 0
|
||
high := 32768
|
||
for low+1 < high {
|
||
mid := (low + high) / 2
|
||
resp.Answer[0].Data = dns.EncodeRDataTXT(make([]byte, mid))
|
||
buf, err := resp.WireFormat()
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
if len(buf) <= limit {
|
||
low = mid
|
||
} else {
|
||
high = mid
|
||
}
|
||
}
|
||
return low
|
||
}
|
||
|
||
// runDNSTT starts a dnstt server on udpListen. It computes the effective
|
||
// MTU based on the configured maxUDPPayload, then accepts KCP sessions and
|
||
// handles DNS queries. Errors are returned only for fatal conditions.
|
||
func runDNSTT(privkey []byte, domain dns.Name, udpListen string) error {
|
||
dnsConn, err := net.ListenPacket("udp", udpListen)
|
||
if err != nil {
|
||
return fmt.Errorf("dnstt: opening UDP listener on %s: %v", udpListen, err)
|
||
}
|
||
return runDNSTTOnConn(privkey, domain, udpListen, dnsConn)
|
||
}
|
||
|
||
func runDNSTTOnConn(privkey []byte, domain dns.Name, udpListen string, dnsConn net.PacketConn) error {
|
||
if udp, ok := dnsConn.(*net.UDPConn); ok {
|
||
_ = udp.SetReadBuffer(4 * 1024 * 1024)
|
||
_ = udp.SetWriteBuffer(4 * 1024 * 1024)
|
||
}
|
||
|
||
// Register so stopDNSTT() can close this socket and unblock the read loop.
|
||
dnsttConnMu.Lock()
|
||
if dnsttConn != nil && dnsttConn != dnsConn {
|
||
_ = dnsttConn.Close()
|
||
}
|
||
dnsttConn = dnsConn
|
||
dnsttConnMu.Unlock()
|
||
defer func() {
|
||
dnsttConnMu.Lock()
|
||
if dnsttConn == dnsConn {
|
||
dnsttConn = nil
|
||
}
|
||
dnsttConnMu.Unlock()
|
||
}()
|
||
// Log readiness of the UDP listener.
|
||
dnsttLog.Printf("udp listener ready on %s", udpListen)
|
||
// compute maximum encoded payload and resulting MTU
|
||
maxEncodedPayload := computeMaxEncodedPayload(maxUDPPayload)
|
||
mtu := maxEncodedPayload - 2
|
||
if mtu < 80 {
|
||
if mtu < 0 {
|
||
mtu = 0
|
||
}
|
||
return fmt.Errorf("dnstt: computed MTU %d too small", mtu)
|
||
}
|
||
// set up turbotunnel and KCP listener
|
||
ttConn := turbotunnel.NewQueuePacketConn(turbotunnel.DummyAddr{}, idleTimeout*2)
|
||
ln, err := kcp.ServeConn(nil, 0, 0, ttConn)
|
||
if err != nil {
|
||
return fmt.Errorf("dnstt: opening KCP listener: %v", err)
|
||
}
|
||
go func() {
|
||
if err := acceptDNSTTSessions(ln, privkey, mtu); err != nil {
|
||
dnsttLog.Printf("acceptSessions error: %v", err)
|
||
}
|
||
}()
|
||
// NOTE: This channel buffers pending DNS response records. Keeping this
|
||
// extremely large can look like a "memory leak" under bursty load because
|
||
// records (and their associated allocations) are retained until drained.
|
||
// A moderate size provides smoothing while still applying backpressure.
|
||
ch := make(chan *record, 20000)
|
||
// Periodically aggregate DNSTT counters. This goroutine runs every 5 seconds,
|
||
// resetting the atomic counters, storing them in lastDnsttStats and optionally
|
||
// emitting a log line. Even when dnsttPrintStats is false, statistics will
|
||
// still be collected and made available via the admin API.
|
||
go func() {
|
||
t := time.NewTicker(5 * time.Second)
|
||
defer t.Stop()
|
||
for range t.C {
|
||
dnsRx := atomic.SwapUint64(&dnsttStats.DNSRx, 0)
|
||
parseErr := atomic.SwapUint64(&dnsttStats.DNSParseErr, 0)
|
||
noEDNS := atomic.SwapUint64(&dnsttStats.NoEDNS, 0)
|
||
limit512 := atomic.SwapUint64(&dnsttStats.SmallEDNS, 0)
|
||
queued := atomic.SwapUint64(&dnsttStats.RecQueued, 0)
|
||
dropped := atomic.SwapUint64(&dnsttStats.RecDropped, 0)
|
||
respSent := atomic.SwapUint64(&dnsttStats.RespSent, 0)
|
||
respBytes := atomic.SwapUint64(&dnsttStats.RespSentBytes, 0)
|
||
respEmpty := atomic.SwapUint64(&dnsttStats.RespEmpty, 0)
|
||
respData := atomic.SwapUint64(&dnsttStats.RespWithData, 0)
|
||
over := atomic.SwapUint64(&dnsttStats.RespOversize, 0)
|
||
kcpNew := atomic.SwapUint64(&dnsttStats.KCPSessionsNew, 0)
|
||
kcpEnd := atomic.SwapUint64(&dnsttStats.KCPSessionsEnd, 0)
|
||
smuxNew := atomic.SwapUint64(&dnsttStats.SmuxStreamsNew, 0)
|
||
smuxEnd := atomic.SwapUint64(&dnsttStats.SmuxStreamsEnd, 0)
|
||
// Update the snapshot
|
||
dnsttStatsMu.Lock()
|
||
lastDnsttStats = DnsttStatsSnapshot{
|
||
Timestamp: time.Now(),
|
||
DNSRx: dnsRx,
|
||
ParseErr: parseErr,
|
||
NoEDNS: noEDNS,
|
||
Limit512: limit512,
|
||
RecQueued: queued,
|
||
RecDropped: dropped,
|
||
RespSent: respSent,
|
||
RespBytes: respBytes,
|
||
RespEmpty: respEmpty,
|
||
RespData: respData,
|
||
RespOversize: over,
|
||
KCPNew: kcpNew,
|
||
KCPEnd: kcpEnd,
|
||
SmuxNew: smuxNew,
|
||
SmuxEnd: smuxEnd,
|
||
ChLen: len(ch),
|
||
}
|
||
dnsttStatsMu.Unlock()
|
||
// Optionally log the snapshot to stderr
|
||
if dnsttPrintStats {
|
||
dnsttLog.Printf(
|
||
"stats 5s: dns_rx=%d parse_err=%d no_edns=%d limit512=%d rec_queued=%d rec_dropped=%d resp_sent=%d resp_bytes=%d resp_empty=%d resp_data=%d resp_oversize=%d kcp_new=%d kcp_end=%d smux_new=%d smux_end=%d ch_len=%d",
|
||
dnsRx, parseErr, noEDNS, limit512, queued, dropped, respSent, respBytes, respEmpty, respData, over, kcpNew, kcpEnd, smuxNew, smuxEnd, len(ch),
|
||
)
|
||
}
|
||
}
|
||
}()
|
||
go func() {
|
||
if err := sendLoop(dnsConn, ttConn, ch, maxEncodedPayload); err != nil {
|
||
dnsttLog.Printf("sendLoop error: %v", err)
|
||
}
|
||
}()
|
||
return recvLoop(domain, dnsConn, ttConn, ch)
|
||
}
|
||
|
||
// ---- Key management API handlers ----
|
||
|
||
const dnsttKeyFile = "/opt/sshpanel/dnstt.key"
|
||
|
||
// handleDnsttGenKey generates a new Noise keypair, saves the private key to
|
||
// dnsttKeyFile, and returns the hex-encoded public key.
|
||
func handleDnsttGenKey(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodPost {
|
||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||
return
|
||
}
|
||
privkey, err := noise.GeneratePrivkey()
|
||
if err != nil {
|
||
http.Error(w, "keygen: "+err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
f, err := os.OpenFile(dnsttKeyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||
if err != nil {
|
||
http.Error(w, "write key: "+err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
if err := noise.WriteKey(f, privkey); err != nil {
|
||
f.Close()
|
||
http.Error(w, "write key: "+err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
f.Close()
|
||
pubkey := noise.PubkeyFromPrivkey(privkey)
|
||
w.Header().Set("Content-Type", "application/json")
|
||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||
"privkey_file": dnsttKeyFile,
|
||
"pubkey": noise.EncodeKey(pubkey),
|
||
})
|
||
}
|
||
|
||
// handleDnsttGetPubKey reads the configured private key and returns the
|
||
// corresponding public key so the admin can share it with dnstt clients.
|
||
func handleDnsttGetPubKey(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodGet {
|
||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||
return
|
||
}
|
||
cfg := getGlobalCfg()
|
||
keyPath := dnsttKeyFile
|
||
if cfg != nil && cfg.DNSTT != nil && cfg.DNSTT.PrivKeyFile != "" {
|
||
keyPath = cfg.DNSTT.PrivKeyFile
|
||
}
|
||
f, err := os.Open(keyPath)
|
||
if err != nil {
|
||
http.Error(w, "open key: "+err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
defer f.Close()
|
||
privkey, err := noise.ReadKey(f)
|
||
if err != nil {
|
||
http.Error(w, "read key: "+err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
pubkey := noise.PubkeyFromPrivkey(privkey)
|
||
w.Header().Set("Content-Type", "application/json")
|
||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||
"pubkey": noise.EncodeKey(pubkey),
|
||
})
|
||
}
|