From 94981e71eaaf49b2226ab570a3256eee4bff3b21 Mon Sep 17 00:00:00 2001 From: firshme Date: Mon, 11 Aug 2025 22:13:46 +0800 Subject: [PATCH] first commit --- .gitignore | 37 + .idea/.gitignore | 8 + .idea/camostream.iml | 9 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + README.md | 98 +++ client.sh | 5 + client_udp.sh | 7 + go.mod | 3 + kill.sh | 3 + main.go | 1562 ++++++++++++++++++++++++++++++++++++++++++ server.sh | 6 + server_udp.sh | 8 + sim/README.md | 17 + sim/udp_server.go | 44 ++ tcp_client.pcap | Bin 0 -> 5432 bytes tcp_server.pcap | Bin 0 -> 5432 bytes udp_client.pcap | Bin 0 -> 1925 bytes udp_server.pcap | Bin 0 -> 1925 bytes 19 files changed, 1821 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/camostream.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 README.md create mode 100755 client.sh create mode 100755 client_udp.sh create mode 100644 go.mod create mode 100755 kill.sh create mode 100644 main.go create mode 100755 server.sh create mode 100755 server_udp.sh create mode 100644 sim/README.md create mode 100644 sim/udp_server.go create mode 100644 tcp_client.pcap create mode 100644 tcp_server.pcap create mode 100644 udp_client.pcap create mode 100644 udp_server.pcap diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d1cdef --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +### Go template +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.dll +*.so +*.dylib +camostream +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +### Go.AllowList template +# Allowlisting gitignore template for GO projects prevents us +# from adding various unwanted local files, such as generated +# files, developer configurations or IDE-specific files etc. +# +# Recommended: Go.AllowList.gitignore + +# Ignore everything +# But not these files... +!/.gitignore + + diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/camostream.iml b/.idea/camostream.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/camostream.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..4106269 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..369a7e1 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +### CamoStreamPro + +>我也不知道有什么用 + +#### build + +```bash +go build -o camostream main.go +``` + +#### tcp + +```bash + +./camostream -role=client -mode=tcp -listen=:37001 -server=127.0.0.1:39001 \ + -bitrate-mbps=20 -decoy-rps=10 \ + -pcap=tcp_client.pcap -pcap-max-mb=50 -metrics=:9101 -log=info + + +./camostream -role=server -mode=tcp -listen=:39001 -forward=127.0.0.1:4141 \ + -bitrate-mbps=20 -decoy-rps=10 \ + -pcap=tcp_server.pcap -pcap-max-mb=50 -metrics=:9100 -log=info + +``` + + + + + + + + +#### udp + +```bash +./camostream -role=server -mode=udp -listen=:39001 -forward=127.0.0.1:18081 \ + -wire=rtpish -fps=60 -bitrate-mbps=20 \ + -decoy-rps=12 \ + -rtcp-sr-rps=2 -rtcp-rr-rps=3 -rtpkeep-rps=4 -stun-rps=1 \ + -pcap=udp_server.pcap -pcap-max-mb=100 -metrics=:9100 -log=info + + +./camostream -role=client -mode=udp -listen=:37001 -server=127.0.0.1:39001 \ + -wire=rtpish -fps=60 -bitrate-mbps=20 \ + -decoy-rps=12 \ + -rtcp-sr-rps=2 -rtcp-rr-rps=3 -rtpkeep-rps=4 -stun-rps=1 \ + -pcap=udp_client.pcap -pcap-max-mb=100 -metrics=:9101 -log=info + +``` + + + + +#### feat + +* ✅ UDP/TCP 双协议、Client/Server 双角色 +* ✅ UDP:RTP-ish(12B RTP 头)+ shim 载荷、60/120fps、GOP 峰谷、抖动 +* ✅ 码率整形(令牌桶) +* ✅ 诱饵插播(shim‑decoy)+ AES‑GCM 可选 +* ✅ 额外伪报文(UDP 无 shim):RTCP SR、RTCP RR、纯 RTP keepalive、STUN Binding +* ✅ 自测模式(UDP Echo + Client/Server + 负载) +* ✅ PCAP(UDP RAW)与指标(/debug/vars) + + +#### 增强 + +* UDP 方向新增无 shim 的额外伪报文(中间盒可见,但业务端不感知): +* RTCP SR(PT=200):包含 sender SSRC、NTP 时间戳、RTP 时间戳、包/字节计数。 +* RTCP RR(PT=201):简单接收者报告,无 report block。 +* 纯 RTP keepalive:PT=13(CN 习惯),小 payload/可零 payload。 +* STUN Binding Request:标准 20B 报文,含 Magic Cookie 和 Transaction ID。 +这些报文不带 shim,因此服务端在解析 RTP-ish+shim 失败时直接 continue 丢弃;同样客户端也会丢弃,从而只起到“流量伪装/背景噪声”作用。 +* 仍保持诱饵为“插播”(不替代真实帧): +* TCP:真实帧 → (可选)插播 shim‑decoy。 +* UDP:真实帧(RTP-ish+shim 或 shim)→ (可选)插播 shim‑decoy → (可选)插播 RTCP/RTP/STUN 等额外无壳伪报文。 +* 新增 CLI 控制这类伪报文注入概率(UDP only): +* -rtcp-sr-pct:插播 RTCP SR 的概率(默认 4) +* -rtcp-rr-pct:插播 RTCP RR 的概率(默认 6) +* -rtpkeep-pct:插播纯 RTP keepalive 的概率(默认 5) +* -stun-pct:插播 STUN Binding 的概率(默认 3) + + +>备注:-wire=rtpish 仅对 UDP 生效;TCP 会打印一个 WARN 并忽略 + + + + +#### 指标 + +* http://127.0.0.1:9100/debug/vars(server) +* http://127.0.0.1:9101/debug/vars(client) +* 关注:bytes_*、frames_*、decoy_dropped、shim_decoy_sent、rtcp_sr_sent、rtcp_rr_sent、rtp_keepalive_sent、stun_sent + + +#### Warn + +仅用于授权的内部安全测试。涉及伪装/混淆的功能,请严格遵循公司及法律合规要求。 +TCP 的 PCAP 是伪造的网络层帧,用于 debug 观察我们应用层写入/读到的 shim 帧,不代表内核真实的 TCP 会话(没有三次握手、窗口/ACK 真实演进),但校验和正确,可在 Wireshark 中查看和过滤。 \ No newline at end of file diff --git a/client.sh b/client.sh new file mode 100755 index 0000000..7a73754 --- /dev/null +++ b/client.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +./camostream -role=client -mode=tcp -listen=:37001 -server=127.0.0.1:39001 \ + -bitrate-mbps=20 -decoy-rps=10 \ + -pcap=tcp_client.pcap -pcap-max-mb=50 -metrics=:9101 -log=info \ No newline at end of file diff --git a/client_udp.sh b/client_udp.sh new file mode 100755 index 0000000..1d8cb70 --- /dev/null +++ b/client_udp.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +./camostream -role=client -mode=udp -listen=:37001 -server=127.0.0.1:39001 \ + -wire=rtpish -fps=60 -bitrate-mbps=20 \ + -decoy-rps=12 \ + -rtcp-sr-rps=2 -rtcp-rr-rps=3 -rtpkeep-rps=4 -stun-rps=1 \ + -pcap=udp_client.pcap -pcap-max-mb=100 -metrics=:9101 -log=info \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ccdb690 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module camostream + +go 1.24.3 diff --git a/kill.sh b/kill.sh new file mode 100755 index 0000000..9c6eeab --- /dev/null +++ b/kill.sh @@ -0,0 +1,3 @@ +#!/bin/bash + + ps -ef |grep camostream | awk '{print $2}' | xargs kill -9 diff --git a/main.go b/main.go new file mode 100644 index 0000000..78350a6 --- /dev/null +++ b/main.go @@ -0,0 +1,1562 @@ +// main.go +// camostream with shim-decoy RPS scheduling and TCP PCAP (size-capped) debug capture. +// UDP/TCP, RTP-ish masking (UDP only), fps profiles, bitrate shaping, decoy insertion, +// AES-GCM optional, selftest, and PCAP (LINKTYPE_RAW, IPv4/UDP|TCP). +// For internal security testing only. + +package main + +import ( + "context" + "crypto/aes" + "crypto/cipher" + cryptoRand "crypto/rand" + "encoding/binary" + "encoding/hex" + "errors" + "expvar" + "flag" + "fmt" + "io" + "math" + "math/rand" + "net" + "net/http" + "os" + "os/signal" + "strings" + "sync" + "sync/atomic" + "syscall" + "time" +) + +/* ============ Metrics ============ */ + +var ( + metricBytesUp = expvar.NewInt("bytes_up") + metricBytesDown = expvar.NewInt("bytes_down") + metricFramesUp = expvar.NewInt("frames_up") + metricFramesDown = expvar.NewInt("frames_down") + metricDecoyDropped = expvar.NewInt("decoy_dropped") + metricSessions = expvar.NewInt("sessions_active") + + metricRTCPsrSent = expvar.NewInt("rtcp_sr_sent") + metricRTCPrrSent = expvar.NewInt("rtcp_rr_sent") + metricRTPkeepSent = expvar.NewInt("rtp_keepalive_sent") + metricSTUNSent = expvar.NewInt("stun_sent") + metricShimDecoySent = expvar.NewInt("shim_decoy_sent") +) + +/* ============ CLI / Config ============ */ + +type Role string +type Mode string +type Wire string + +const ( + RoleServer Role = "server" + RoleClient Role = "client" + RoleSelf Role = "selftest" + + ModeUDP Mode = "udp" + ModeTCP Mode = "tcp" + + WireShim Wire = "shim" // ShimHeader + Payload + WireRTPish Wire = "rtpish" // RTP(12) + Shim + Payload (UDP only) +) + +type Config struct { + Role Role + Mode Mode + Listen string + ServerAddr string + ForwardAddr string + Wire Wire + FPS int + GOPMs int + BitrateMbps int + + // shim-decoy controls (both TCP/UDP): + DecoyPct int // legacy percentage + DecoyRps float64 // per-second rate (sliding window); overrides DecoyPct when >0 + + // Extra decoys (UDP only): + // Either use per-second rate (recommended), or fallback to percentage if rps == 0. + RTCPSrPct int // legacy percentage for RTCP SR + RTCPRrPct int // legacy percentage for RTCP RR + RTPKeepPct int // legacy percentage for pure RTP keepalive + STUNPct int // legacy percentage for STUN Binding + RTCPSrRps float64 // per-second rate (sliding window) for RTCP SR + RTCPRrRps float64 // per-second rate for RTCP RR + RTPKeepRps float64 // per-second rate for pure RTP keepalive + STUNRps float64 // per-second rate for STUN Binding + + AESKeyHex string + PaceMs int + JitterPct int + FrameMin int + FrameMax int + UDPRcvBuf int + UDPSndBuf int + SessID uint32 + MetricsAddr string + PcapPath string + PcapMaxMB int // Max PCAP size in MB (0 = unlimited) + SelfDur time.Duration + LogLevel string + LogDrop bool +} + +func defaultConfig() *Config { + return &Config{ + Role: RoleServer, + Mode: ModeUDP, + Listen: ":9001", + ServerAddr: "127.0.0.1:9001", + ForwardAddr: "127.0.0.1:18081", + Wire: WireRTPish, + FPS: 60, + GOPMs: 2000, + BitrateMbps: 20, + + // shim decoy + DecoyPct: 10, + DecoyRps: 0, + + // legacy percentages (only used if the corresponding RPS is 0) + RTCPSrPct: 0, + RTCPRrPct: 0, + RTPKeepPct: 0, + STUNPct: 0, + + // RPS default off (0). Set >0 to enable rate scheduling. + RTCPSrRps: 0, + RTCPRrRps: 0, + RTPKeepRps: 0, + STUNRps: 0, + + PaceMs: 16, // TCP: base rhythm + JitterPct: 30, // 0~100 + FrameMin: 800, // TCP split + FrameMax: 1400, // TCP split + UDPRcvBuf: 1 << 20, + UDPSndBuf: 1 << 20, + SessID: 0, + MetricsAddr: ":9100", + PcapPath: "", + PcapMaxMB: 50, + SelfDur: 15 * time.Second, + LogLevel: "info", + LogDrop: false, + } +} + +func parseFlags() *Config { + cfg := defaultConfig() + flag.StringVar((*string)(&cfg.Role), "role", string(cfg.Role), "server|client|selftest") + flag.StringVar((*string)(&cfg.Mode), "mode", string(cfg.Mode), "udp|tcp") + flag.StringVar(&cfg.Listen, "listen", cfg.Listen, "listen addr") + flag.StringVar(&cfg.ServerAddr, "server", cfg.ServerAddr, "server addr (client)") + flag.StringVar(&cfg.ForwardAddr, "forward", cfg.ForwardAddr, "forward addr (server)") + flag.StringVar((*string)(&cfg.Wire), "wire", string(cfg.Wire), "shim|rtpish (udp)") + flag.IntVar(&cfg.FPS, "fps", cfg.FPS, "60|120") + flag.IntVar(&cfg.GOPMs, "gop", cfg.GOPMs, "keyframe interval ms") + flag.IntVar(&cfg.BitrateMbps, "bitrate-mbps", cfg.BitrateMbps, "20|40 etc.") + + // shim decoy + flag.IntVar(&cfg.DecoyPct, "decoy", cfg.DecoyPct, "shim-decoy percent 0-100 (ignored if -decoy-rps>0)") + flag.Float64Var(&cfg.DecoyRps, "decoy-rps", cfg.DecoyRps, "shim-decoy frames per second (overrides -decoy)") + + // Extra decoys legacy percentages (UDP only; used only if corresponding RPS==0) + flag.IntVar(&cfg.RTCPSrPct, "rtcp-sr-pct", cfg.RTCPSrPct, "insert RTCP SR decoy percent 0-100 (UDP only; ignored if -rtcp-sr-rps>0)") + flag.IntVar(&cfg.RTCPRrPct, "rtcp-rr-pct", cfg.RTCPRrPct, "insert RTCP RR decoy percent 0-100 (UDP only; ignored if -rtcp-rr-rps>0)") + flag.IntVar(&cfg.RTPKeepPct, "rtpkeep-pct", cfg.RTPKeepPct, "insert pure RTP keepalive decoy percent 0-100 (UDP only; ignored if -rtpkeep-rps>0)") + flag.IntVar(&cfg.STUNPct, "stun-pct", cfg.STUNPct, "insert STUN Binding decoy percent 0-100 (UDP only; ignored if -stun-rps>0)") + + // Extra decoys per-second rates (UDP only; sliding window scheduling) + flag.Float64Var(&cfg.RTCPSrRps, "rtcp-sr-rps", cfg.RTCPSrRps, "RTCP SR decoys per second (UDP only; overrides -rtcp-sr-pct)") + flag.Float64Var(&cfg.RTCPRrRps, "rtcp-rr-rps", cfg.RTCPRrRps, "RTCP RR decoys per second (UDP only; overrides -rtcp-rr-pct)") + flag.Float64Var(&cfg.RTPKeepRps, "rtpkeep-rps", cfg.RTPKeepRps, "pure RTP keepalive decoys per second (UDP only; overrides -rtpkeep-pct)") + flag.Float64Var(&cfg.STUNRps, "stun-rps", cfg.STUNRps, "STUN Binding decoys per second (UDP only; overrides -stun-pct)") + + flag.StringVar(&cfg.AESKeyHex, "aes", cfg.AESKeyHex, "AES-GCM key hex (optional)") + flag.IntVar(&cfg.PaceMs, "pace", cfg.PaceMs, "base pace ms (TCP)") + flag.IntVar(&cfg.JitterPct, "jitter", cfg.JitterPct, "jitter percent") + flag.IntVar(&cfg.FrameMin, "fmin", cfg.FrameMin, "tcp frame min") + flag.IntVar(&cfg.FrameMax, "fmax", cfg.FrameMax, "tcp frame max") + flag.IntVar(&cfg.UDPRcvBuf, "udp-rbuf", cfg.UDPRcvBuf, "udp read buffer") + flag.IntVar(&cfg.UDPSndBuf, "udp-wbuf", cfg.UDPSndBuf, "udp write buffer") + + // sess flag (uint <-» uint32) + sessFlag := uint(cfg.SessID) + flag.UintVar(&sessFlag, "sess", sessFlag, "udp session id (0=random)") + + flag.StringVar(&cfg.MetricsAddr, "metrics", cfg.MetricsAddr, "metrics http addr") + flag.StringVar(&cfg.PcapPath, "pcap", cfg.PcapPath, "pcap output path (IPv4/UDP RAW + TCP)") + flag.IntVar(&cfg.PcapMaxMB, "pcap-max-mb", cfg.PcapMaxMB, "max PCAP size in MB (0=unlimited)") + flag.DurationVar(&cfg.SelfDur, "duration", cfg.SelfDur, "selftest duration") + flag.StringVar(&cfg.LogLevel, "log", cfg.LogLevel, "debug|info|warn|error") + flag.BoolVar(&cfg.LogDrop, "showdrop", cfg.LogDrop, "log when dropping decoy") + flag.Parse() + + cfg.SessID = uint32(sessFlag) + if cfg.Mode == ModeTCP && cfg.Wire == WireRTPish { + // 仅提示:RTP-ish 只在 UDP 生效 + fmt.Println("[WARN] wire=rtpish is ignored in TCP mode.") + } + return cfg +} + +/* ============ Logging ============ */ + +type LogLevel int + +const ( + LDebug LogLevel = iota + LInfo + LWarn + LError +) + +var gLogLevel = LInfo + +func setLog(s string) { + switch strings.ToLower(s) { + case "debug": + gLogLevel = LDebug + case "info": + gLogLevel = LInfo + case "warn": + gLogLevel = LWarn + case "error": + gLogLevel = LError + } +} +func logf(lv LogLevel, f string, a ...any) { + if lv < gLogLevel { + return + } + ts := time.Now().Format("15:04:05.000") + p := [...]string{"DEBU", "INFO", "WARN", "ERRO"}[lv] + fmt.Printf("%s [%s] %s\n", ts, p, fmt.Sprintf(f, a...)) +} +func must(err error) { + if err != nil { + logf(LError, "fatal: %v", err) + os.Exit(2) + } +} + +/* ============ Random/Timing ============ */ + +var grnd *rand.Rand + +func init() { + seed := time.Now().UnixNano() + var b [8]byte + if _, err := cryptoRand.Read(b[:]); err == nil { + seed = int64(binary.BigEndian.Uint64(b[:])) + } + grnd = rand.New(rand.NewSource(seed)) +} +func randRange(min, max int) int { + if max <= min { + return min + } + return min + grnd.Intn(max-min+1) +} +func jitterSleep(paceMs, jitterPct int) { + if paceMs <= 0 { + return + } + j := float64(paceMs) * float64(jitterPct) / 100 + delta := grnd.Float64()*2*j - j + d := time.Duration(math.Max(0, float64(paceMs)+delta)) * time.Millisecond + time.Sleep(d) +} + +/* ============ Token Bucket (bitrate shaping) ============ */ + +type tokenBucket struct { + rate int64 + burst int64 + tok int64 + last time.Time + mu sync.Mutex +} + +func newTB(mbps int, burst int) *tokenBucket { + if mbps <= 0 { + return nil + } + return &tokenBucket{ + rate: int64(mbps) * 1024 * 1024 / 8, // bytes/sec + burst: int64(burst), + last: time.Now(), + } +} +func (tb *tokenBucket) wait(n int) { + if tb == nil { + return + } + tb.mu.Lock() + defer tb.mu.Unlock() + now := time.Now() + el := now.Sub(tb.last).Seconds() + tb.tok += int64(float64(tb.rate) * el) + if tb.tok > tb.burst { tb.tok = tb.burst } + tb.last = now + need := int64(n) + for tb.tok < need { + def := float64(need-tb.tok) / float64(tb.rate) + sleep := time.Duration(def*1000.0)*time.Millisecond + time.Millisecond + tb.mu.Unlock() + time.Sleep(sleep) + tb.mu.Lock() + now = time.Now() + el = now.Sub(tb.last).Seconds() + tb.tok += int64(float64(tb.rate) * el) + if tb.tok > tb.burst { tb.tok = tb.burst } + tb.last = now + } + tb.tok -= need +} + +/* ============ Per-second sliding-window scheduler for decoys ============ */ + +type rateLimiter struct { + rps float64 + burst float64 + tokens float64 + last time.Time + mu sync.Mutex +} + +func newRL(rps float64) *rateLimiter { + if rps <= 0 { + return nil + } + burst := rps*1.5 + 1.0 // small burst to smooth boundary effects + return &rateLimiter{rps: rps, burst: burst, tokens: burst, last: time.Now()} +} + +// take up to max tokens (integer). returns how many allowed right now. +func (rl *rateLimiter) takeMax(max int) int { + if rl == nil { + return 0 + } + rl.mu.Lock() + defer rl.mu.Unlock() + now := time.Now() + el := now.Sub(rl.last).Seconds() + rl.tokens += el * rl.rps + if rl.tokens > rl.burst { + rl.tokens = rl.burst + } + rl.last = now + n := int(math.Floor(rl.tokens)) + if n <= 0 { + return 0 + } + if n > max { + n = max + } + rl.tokens -= float64(n) + return n +} + +type decoyRL struct { + sr *rateLimiter + rr *rateLimiter + keep *rateLimiter + stun *rateLimiter +} + +func newDecoyRL(cfg *Config, up bool) *decoyRL { + // Split given total RPS 50/50 into up and down directions. + f := 0.5 + return &decoyRL{ + sr: newRL(cfg.RTCPSrRps * f), + rr: newRL(cfg.RTCPRrRps * f), + keep: newRL(cfg.RTPKeepRps * f), + stun: newRL(cfg.STUNRps * f), + } +} + +/* ============ Shim & RTP-ish headers ============ */ + +const ( + magicConst uint32 = 0x5C10ADED + version uint8 = 1 + + modeTCP uint8 = 1 + modeUDP uint8 = 2 + + flagDecoy uint8 = 1 << 0 + flagEnc uint8 = 1 << 1 +) + +type shimHeader struct { + Magic uint32 + Version uint8 + Mode uint8 + Flags uint8 + Reserved uint8 + SessionID uint32 + TsMs uint32 + Len uint32 +} + +const shimLen = 20 + +func (h shimHeader) Marshal() []byte { + b := make([]byte, shimLen) + binary.BigEndian.PutUint32(b[0:4], h.Magic) + b[4] = h.Version + b[5] = h.Mode + b[6] = h.Flags + b[7] = h.Reserved + binary.BigEndian.PutUint32(b[8:12], h.SessionID) + binary.BigEndian.PutUint32(b[12:16], h.TsMs) + binary.BigEndian.PutUint32(b[16:20], h.Len) + return b +} + +func parseShimFull(b []byte) (shimHeader, []byte, error) { + if len(b) < shimLen { + return shimHeader{}, nil, io.ErrUnexpectedEOF + } + h := shimHeader{ + Magic: binary.BigEndian.Uint32(b[0:4]), + Version: b[4], + Mode: b[5], + Flags: b[6], + Reserved: b[7], + SessionID: binary.BigEndian.Uint32(b[8:12]), + TsMs: binary.BigEndian.Uint32(b[12:16]), + Len: binary.BigEndian.Uint32(b[16:20]), + } + if h.Magic != magicConst || h.Version != version { + return shimHeader{}, nil, errors.New("bad shim") + } + if int(h.Len) > len(b[shimLen:]) { + return shimHeader{}, nil, errors.New("bad len") + } + return h, b[shimLen : shimLen+int(h.Len)], nil +} + +// 仅解析头部(TCP 流模式) +func parseShimHeader(hb []byte) (shimHeader, error) { + if len(hb) < shimLen { + return shimHeader{}, io.ErrUnexpectedEOF + } + h := shimHeader{ + Magic: binary.BigEndian.Uint32(hb[0:4]), + Version: hb[4], + Mode: hb[5], + Flags: hb[6], + Reserved: hb[7], + SessionID: binary.BigEndian.Uint32(hb[8:12]), + TsMs: binary.BigEndian.Uint32(hb[12:16]), + Len: binary.BigEndian.Uint32(hb[16:20]), + } + if h.Magic != magicConst || h.Version != version { + return shimHeader{}, errors.New("bad shim header") + } + return h, nil +} + +// RTP-ish (UDP only) +type rtpState struct{ seq uint16; ssrc uint32 } + +func buildRTPHeader(seq uint16, ts uint32, pt uint8, marker bool, ssrc uint32) []byte { + b := make([]byte, 12) + b[0] = 0x80 // V=2 + b[1] = pt + if marker { + b[1] |= 0x80 + } + binary.BigEndian.PutUint16(b[2:4], seq) + binary.BigEndian.PutUint32(b[4:8], ts) + binary.BigEndian.PutUint32(b[8:12], ssrc) + return b +} +func stripRTP(b []byte) (pt uint8, marker bool, rest []byte, err error) { + if len(b) < 12 { + return 0, false, nil, io.ErrUnexpectedEOF + } + if (b[0]>>6)&0x3 != 2 { + return 0, false, nil, errors.New("rtp ver") + } + marker = (b[1] & 0x80) != 0 + pt = b[1] & 0x7F + return pt, marker, b[12:], nil +} + +/* ============ AES-GCM (optional) ============ */ + +type aeadBox struct{ aead cipher.AEAD } + +func newAEAD(hexKey string) (*aeadBox, error) { + if strings.TrimSpace(hexKey) == "" { + return nil, nil + } + kb, err := hex.DecodeString(hexKey) + if err != nil { + return nil, err + } + switch len(kb) { + case 16, 24, 32: + default: + return nil, fmt.Errorf("aes key must be 16/24/32 bytes") + } + block, err := aes.NewCipher(kb) + if err != nil { + return nil, err + } + a, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + return &aeadBox{aead: a}, nil +} +func (b *aeadBox) seal(p []byte) (out []byte, nonce []byte, err error) { + if b == nil { + return p, nil, nil + } + nonce = make([]byte, b.aead.NonceSize()) + if _, err = cryptoRand.Read(nonce); err != nil { + return nil, nil, err + } + c := b.aead.Seal(nil, nonce, p, nil) + return c, nonce, nil +} +func (b *aeadBox) open(nonce, c []byte) ([]byte, error) { + if b == nil { + return c, nil + } + return b.aead.Open(nil, nonce, c, nil) +} + +/* ============ PCAP Writer (IPv4 RAW, LINKTYPE_RAW=101) ============ */ + +type pcapWriter struct { + f *os.File + mu sync.Mutex + ipID uint16 + limitBytes int64 + written int64 + + tcpSeq map[string]uint32 +} + +func newPCAP(path string, maxBytes int64) (*pcapWriter, error) { + if strings.TrimSpace(path) == "" { + return nil, nil + } + f, err := os.Create(path) + if err != nil { + return nil, err + } + gh := make([]byte, 24) + binary.BigEndian.PutUint32(gh[0:4], 0xa1b2c3d4) + binary.BigEndian.PutUint16(gh[4:6], 2) + binary.BigEndian.PutUint16(gh[6:8], 4) + binary.BigEndian.PutUint32(gh[16:20], 0xffff) + binary.BigEndian.PutUint32(gh[20:24], 101) // LINKTYPE_RAW + if _, err := f.Write(gh); err != nil { + f.Close() + return nil, err + } + return &pcapWriter{ + f: f, ipID: uint16(rand.Intn(65535)), + limitBytes: maxBytes, written: 24, // include global header + tcpSeq: make(map[string]uint32), + }, nil +} +func (w *pcapWriter) Close() { if w != nil && w.f != nil { _ = w.f.Close() } } + +func (w *pcapWriter) canWrite(n int) bool { + if w == nil { + return false + } + if w.limitBytes <= 0 { + return true + } + return w.written+int64(n) <= w.limitBytes +} +func (w *pcapWriter) writeRecord(frame []byte) { + if w == nil { + return + } + w.mu.Lock() + defer w.mu.Unlock() + if !w.canWrite(16+len(frame)) { + return + } + now := time.Now() + pkth := make([]byte, 16) + binary.BigEndian.PutUint32(pkth[0:4], uint32(now.Unix())) + binary.BigEndian.PutUint32(pkth[4:8], uint32(now.Nanosecond()/1000)) + binary.BigEndian.PutUint32(pkth[8:12], uint32(len(frame))) + binary.BigEndian.PutUint32(pkth[12:16], uint32(len(frame))) + _, _ = w.f.Write(pkth) + _, _ = w.f.Write(frame) + w.written += int64(16 + len(frame)) +} + +func (w *pcapWriter) ipChecksum(hdr []byte) uint16 { + var sum uint32 + for i := 0; i < len(hdr); i += 2 { + sum += uint32(binary.BigEndian.Uint16(hdr[i : i+2])) + } + for (sum >> 16) != 0 { + sum = (sum & 0xffff) + (sum >> 16) + } + return ^uint16(sum) +} + +func (w *pcapWriter) udpFrame(srcIP net.IP, srcPort int, dstIP net.IP, dstPort int, payload []byte) []byte { + s4 := srcIP.To4() + d4 := dstIP.To4() + if s4 == nil || d4 == nil { return nil } + udp := make([]byte, 8) + binary.BigEndian.PutUint16(udp[0:2], uint16(srcPort)) + binary.BigEndian.PutUint16(udp[2:4], uint16(dstPort)) + binary.BigEndian.PutUint16(udp[4:6], uint16(8+len(payload))) + udp[6] = 0; udp[7] = 0 + + ip := make([]byte, 20) + ip[0] = (4 << 4) | 5 + ip[1] = 0 + total := 20 + 8 + len(payload) + binary.BigEndian.PutUint16(ip[2:4], uint16(total)) + w.ipID++ + binary.BigEndian.PutUint16(ip[4:6], w.ipID) + binary.BigEndian.PutUint16(ip[6:8], 0) + ip[8] = 64 + ip[9] = 17 // UDP + copy(ip[12:16], s4) + copy(ip[16:20], d4) + cs := w.ipChecksum(ip) + binary.BigEndian.PutUint16(ip[10:12], cs) + + frame := append(ip, udp...) + frame = append(frame, payload...) + return frame +} +func (w *pcapWriter) WriteUDP(srcIP net.IP, srcPort int, dstIP net.IP, dstPort int, payload []byte) { + if w == nil { + return + } + frame := w.udpFrame(srcIP, srcPort, dstIP, dstPort, payload) + if frame == nil { return } + w.writeRecord(frame) +} + +func key4(srcIP net.IP, srcPort int, dstIP net.IP, dstPort int) string { + return fmt.Sprintf("%s:%d>%s:%d", srcIP.String(), srcPort, dstIP.String(), dstPort) +} + +func checksumTCP(srcIP, dstIP net.IP, tcpHdr, payload []byte) uint16 { + // pseudo header + tcp header + payload + s4 := srcIP.To4(); d4 := dstIP.To4() + var sum uint32 + // pseudo + sum += uint32(binary.BigEndian.Uint16(s4[0:2])) + sum += uint32(binary.BigEndian.Uint16(s4[2:4])) + sum += uint32(binary.BigEndian.Uint16(d4[0:2])) + sum += uint32(binary.BigEndian.Uint16(d4[2:4])) + sum += uint32(6) // protocol + tcpLen := len(tcpHdr) + len(payload) + sum += uint32(tcpLen) + + // tcp header + for i := 0; i < len(tcpHdr); i += 2 { + sum += uint32(binary.BigEndian.Uint16(tcpHdr[i : i+2])) + } + // payload + for i := 0; i+1 < len(payload); i += 2 { + sum += uint32(binary.BigEndian.Uint16(payload[i : i+2])) + } + if len(payload)%2 == 1 { + sum += uint32(uint16(payload[len(payload)-1]) << 8) + } + for (sum >> 16) != 0 { + sum = (sum & 0xffff) + (sum >> 16) + } + return ^uint16(sum) +} + +func (w *pcapWriter) tcpFrame(srcIP net.IP, srcPort int, dstIP net.IP, dstPort int, payload []byte) []byte { + s4 := srcIP.To4(); d4 := dstIP.To4() + if s4 == nil || d4 == nil { return nil } + + // maintain a fake seq space per 4-tuple for debug + w.mu.Lock() + key := key4(s4, srcPort, d4, dstPort) + seq := w.tcpSeq[key] + if seq == 0 { + seq = uint32(rand.Int31()) + } + w.tcpSeq[key] = seq + uint32(len(payload)) + w.mu.Unlock() + + tcp := make([]byte, 20) + binary.BigEndian.PutUint16(tcp[0:2], uint16(srcPort)) + binary.BigEndian.PutUint16(tcp[2:4], uint16(dstPort)) + binary.BigEndian.PutUint32(tcp[4:8], seq) + binary.BigEndian.PutUint32(tcp[8:12], 0) // ack=0 (we're not modeling handshake) + tcp[12] = (5 << 4) // data offset=5 (20B) + tcp[13] = 0x08 // PSH (no ACK to avoid bogus ack=0+ACK) + binary.BigEndian.PutUint16(tcp[14:16], 65535) // window + binary.BigEndian.PutUint16(tcp[16:18], 0) // checksum placeholder + binary.BigEndian.PutUint16(tcp[18:20], 0) // urgent + + // IPv4 header + ip := make([]byte, 20) + ip[0] = (4 << 4) | 5 + ip[1] = 0 + total := 20 + 20 + len(payload) + binary.BigEndian.PutUint16(ip[2:4], uint16(total)) + w.ipID++ + binary.BigEndian.PutUint16(ip[4:6], w.ipID) + binary.BigEndian.PutUint16(ip[6:8], 0) + ip[8] = 64 + ip[9] = 6 // TCP + copy(ip[12:16], s4) + copy(ip[16:20], d4) + + // TCP checksum (needs pseudo header) + csTCP := checksumTCP(s4, d4, tcp, payload) + binary.BigEndian.PutUint16(tcp[16:18], csTCP) + + // IP checksum + csIP := w.ipChecksum(ip) + binary.BigEndian.PutUint16(ip[10:12], csIP) + + frame := append(ip, tcp...) + frame = append(frame, payload...) + return frame +} +func (w *pcapWriter) WriteTCP(srcIP net.IP, srcPort int, dstIP net.IP, dstPort int, payload []byte) { + if w == nil { + return + } + frame := w.tcpFrame(srcIP, srcPort, dstIP, dstPort, payload) + if frame == nil { return } + w.writeRecord(frame) +} + +/* ============ Common runtime & helpers ============ */ + +type runtimeCtx struct { + cfg *Config + tb *tokenBucket + ae *aeadBox + pc *pcapWriter + dropLog bool + + // Extra decoys + upRL *decoyRL + downRL *decoyRL + + // Shim decoy RPS + shimUpRL *rateLimiter + shimDownRL *rateLimiter +} + +func runMetrics(addr string) { + if addr == "" { return } + go func() { _ = http.ListenAndServe(addr, nil) }() +} + +func ip4OrLoopback(ip net.IP) net.IP { + v4 := ip.To4() + if v4 == nil || v4.IsUnspecified() { + return net.ParseIP("127.0.0.1") + } + return v4 +} +func max(a, b int) int { if a > b { return a } ; return b } + +/* ============ Extra Decoys (UDP only): RTCP SR/RR, RTP keepalive, STUN ============ */ + +const ntpEpochOffset = 2208988800 // seconds between 1900 and 1970 + +func buildRTCP_SR(ssrc uint32, rtpTs uint32, pktCount uint32, octCount uint32) []byte { + // RTCP SR (PT=200), RC=0, length=6 (7 words total: 28 bytes) + b := make([]byte, 4+24) + b[0] = 0x80 // V=2, P=0, RC=0 + b[1] = 200 // SR + binary.BigEndian.PutUint16(b[2:4], 6) + binary.BigEndian.PutUint32(b[4:8], ssrc) + now := time.Now() + sec := uint32(uint64(now.Unix()) + ntpEpochOffset) + frac := uint32(uint64(now.Nanosecond()) * (1<<32) / 1_000_000_000) + binary.BigEndian.PutUint32(b[8:12], sec) + binary.BigEndian.PutUint32(b[12:16], frac) + binary.BigEndian.PutUint32(b[16:20], rtpTs) + binary.BigEndian.PutUint32(b[20:24], pktCount) + binary.BigEndian.PutUint32(b[24:28], octCount) + return b +} +func buildRTCP_RR(ssrc uint32) []byte { + // RTCP RR (PT=201), RC=0, length=1 (2 words total: 8 bytes) + b := make([]byte, 4+4) + b[0] = 0x80 // V=2, RC=0 + b[1] = 201 // RR + binary.BigEndian.PutUint16(b[2:4], 1) + binary.BigEndian.PutUint32(b[4:8], ssrc) + return b +} +func buildRTPKeepalive(seq uint16, ts uint32, pt uint8, ssrc uint32, payloadLen int) []byte { + h := buildRTPHeader(seq, ts, pt, false, ssrc) + if payloadLen < 0 { payloadLen = 0 } + if payloadLen > 1400 { payloadLen = 1400 } + if payloadLen == 0 { + return h + } + p := make([]byte, payloadLen) + _, _ = cryptoRand.Read(p) + return append(h, p...) +} +func buildSTUNBindingRequest() []byte { + b := make([]byte, 20) + // Type: Binding Request 0x0001 + binary.BigEndian.PutUint16(b[0:2], 0x0001) + // Length: 0 (no attributes) + binary.BigEndian.PutUint16(b[2:4], 0) + // Magic cookie + binary.BigEndian.PutUint32(b[4:8], 0x2112A442) + // Transaction ID + _, _ = cryptoRand.Read(b[8:20]) + return b +} + +func sendExtraDecoysUDP(conn *net.UDPConn, src *net.UDPAddr, dst *net.UDPAddr, rt *runtimeCtx, + ssrc uint32, seq *uint16, ts *uint32, step int, upDirection bool) { + + srcIP := ip4OrLoopback(src.IP) + dstIP := ip4OrLoopback(dst.IP) + + // Choose per-direction rate limiters + var rl *decoyRL + if upDirection { rl = rt.upRL } else { rl = rt.downRL } + + const maxBurstPerTick = 3 // avoid huge bursts on long silent gaps + + // --- RTCP SR --- + if rl != nil && rl.sr != nil { + n := rl.sr.takeMax(maxBurstPerTick) + for i := 0; i < n; i++ { + pktCount := uint32(metricFramesUp.Value()) + octCount := uint32(metricBytesUp.Value()) + sr := buildRTCP_SR(ssrc, *ts, pktCount, octCount) + rt.tb.wait(len(sr) + 28) + _, _ = conn.WriteToUDP(sr, dst) + rt.pc.WriteUDP(srcIP, src.Port, dstIP, dst.Port, sr) + metricRTCPsrSent.Add(1) + if rt.dropLog { logf(LDebug, "[extra][rps] RTCP SR %dB (%s)", len(sr), dirStr(upDirection)) } + } + } else if rt.cfg.RTCPSrPct > 0 && grnd.Intn(100) < rt.cfg.RTCPSrPct { + // fallback: percentage + pktCount := uint32(metricFramesUp.Value()) + octCount := uint32(metricBytesUp.Value()) + sr := buildRTCP_SR(ssrc, *ts, pktCount, octCount) + rt.tb.wait(len(sr) + 28) + _, _ = conn.WriteToUDP(sr, dst) + rt.pc.WriteUDP(srcIP, src.Port, dstIP, dst.Port, sr) + metricRTCPsrSent.Add(1) + if rt.dropLog { logf(LDebug, "[extra][pct] RTCP SR %dB (%s)", len(sr), dirStr(upDirection)) } + } + + // --- RTCP RR --- + if rl != nil && rl.rr != nil { + n := rl.rr.takeMax(maxBurstPerTick) + for i := 0; i < n; i++ { + rr := buildRTCP_RR(ssrc) + rt.tb.wait(len(rr) + 28) + _, _ = conn.WriteToUDP(rr, dst) + rt.pc.WriteUDP(srcIP, src.Port, dstIP, dst.Port, rr) + metricRTCPrrSent.Add(1) + if rt.dropLog { logf(LDebug, "[extra][rps] RTCP RR %dB (%s)", len(rr), dirStr(upDirection)) } + } + } else if rt.cfg.RTCPRrPct > 0 && grnd.Intn(100) < rt.cfg.RTCPRrPct { + rr := buildRTCP_RR(ssrc) + rt.tb.wait(len(rr) + 28) + _, _ = conn.WriteToUDP(rr, dst) + rt.pc.WriteUDP(srcIP, src.Port, dstIP, dst.Port, rr) + metricRTCPrrSent.Add(1) + if rt.dropLog { logf(LDebug, "[extra][pct] RTCP RR %dB (%s)", len(rr), dirStr(upDirection)) } + } + + // --- pure RTP keepalive --- + if rl != nil && rl.keep != nil { + n := rl.keep.takeMax(maxBurstPerTick) + for i := 0; i < n; i++ { + *seq = *seq + 1 + *ts = *ts + uint32(step) + rtp := buildRTPKeepalive(*seq, *ts, 13, ssrc, randRange(0, 60)) + rt.tb.wait(len(rtp) + 28) + _, _ = conn.WriteToUDP(rtp, dst) + rt.pc.WriteUDP(srcIP, src.Port, dstIP, dst.Port, rtp) + metricRTPkeepSent.Add(1) + if rt.dropLog { logf(LDebug, "[extra][rps] RTP keepalive %dB (%s)", len(rtp), dirStr(upDirection)) } + } + } else if rt.cfg.RTPKeepPct > 0 && grnd.Intn(100) < rt.cfg.RTPKeepPct { + *seq = *seq + 1 + *ts = *ts + uint32(step) + rtp := buildRTPKeepalive(*seq, *ts, 13, ssrc, randRange(0, 60)) + rt.tb.wait(len(rtp) + 28) + _, _ = conn.WriteToUDP(rtp, dst) + rt.pc.WriteUDP(srcIP, src.Port, dstIP, dst.Port, rtp) + metricRTPkeepSent.Add(1) + if rt.dropLog { logf(LDebug, "[extra][pct] RTP keepalive %dB (%s)", len(rtp), dirStr(upDirection)) } + } + + // --- STUN Binding --- + if rl != nil && rl.stun != nil { + n := rl.stun.takeMax(maxBurstPerTick) + for i := 0; i < n; i++ { + stun := buildSTUNBindingRequest() + rt.tb.wait(len(stun) + 28) + _, _ = conn.WriteToUDP(stun, dst) + rt.pc.WriteUDP(srcIP, src.Port, dstIP, dst.Port, stun) + metricSTUNSent.Add(1) + if rt.dropLog { logf(LDebug, "[extra][rps] STUN binding %dB (%s)", len(stun), dirStr(upDirection)) } + } + } else if rt.cfg.STUNPct > 0 && grnd.Intn(100) < rt.cfg.STUNPct { + stun := buildSTUNBindingRequest() + rt.tb.wait(len(stun) + 28) + _, _ = conn.WriteToUDP(stun, dst) + rt.pc.WriteUDP(srcIP, src.Port, dstIP, dst.Port, stun) + metricSTUNSent.Add(1) + if rt.dropLog { logf(LDebug, "[extra][pct] STUN binding %dB (%s)", len(stun), dirStr(upDirection)) } + } +} +func dirStr(up bool) string { + if up { return "up" } + return "down" +} + +/* ============ UDP Client/Server ============ */ + +func runUDPClient(ctx context.Context, rt *runtimeCtx) error { + laddr, err := net.ResolveUDPAddr("udp", rt.cfg.Listen) + if err != nil { return err } + uc, err := net.ListenUDP("udp", laddr) + if err != nil { return err } + defer uc.Close() + if rt.cfg.UDPRcvBuf > 0 { _ = uc.SetReadBuffer(rt.cfg.UDPRcvBuf) } + if rt.cfg.UDPSndBuf > 0 { _ = uc.SetWriteBuffer(rt.cfg.UDPSndBuf) } + saddr, err := net.ResolveUDPAddr("udp", rt.cfg.ServerAddr) + if err != nil { return err } + + sess := rt.cfg.SessID + if sess == 0 { sess = grnd.Uint32() } + logf(LInfo, "[client][udp] %s -> server %s (sess=%d, wire=%s, %dfps, %dMbps)", + rt.cfg.Listen, rt.cfg.ServerAddr, sess, rt.cfg.Wire, rt.cfg.FPS, rt.cfg.BitrateMbps) + + var appPeerMu sync.RWMutex + var appPeer *net.UDPAddr + + // RTP 累加 + var rtp = rtpState{seq: uint16(grnd.Uint32()), ssrc: grnd.Uint32()} + var rtpTs uint32 + step := 90000 / max(1, rt.cfg.FPS) + + go func() { + buf := make([]byte, 64<<10) + for { + n, from, err := uc.ReadFromUDP(buf) + if err != nil { return } + + // 来自 server:解壳 -> 发给应用 + if from.IP.Equal(saddr.IP) && from.Port == saddr.Port { + raw := append([]byte(nil), buf[:n]...) + src := ip4OrLoopback(from.IP) + dst := ip4OrLoopback(uc.LocalAddr().(*net.UDPAddr).IP) + rt.pc.WriteUDP(src, from.Port, dst, uc.LocalAddr().(*net.UDPAddr).Port, raw) + + var payload []byte + if rt.cfg.Wire == WireRTPish { + _, _, rest, e := stripRTP(raw); if e != nil { continue } + h, p, e2 := parseShimFull(rest); if e2 != nil { continue } + if (h.Flags & flagDecoy) != 0 { metricDecoyDropped.Add(1); if rt.dropLog { logf(LDebug, "[client] drop decoy %dB", len(p)) }; continue } + payload = p + if (h.Flags & flagEnc) != 0 && rt.ae != nil { + ns := rt.ae.aead.NonceSize(); if len(payload) >= ns { + if plain, e := rt.ae.open(payload[:ns], payload[ns:]); e == nil { payload = plain } + } + } + } else { + h, p, e2 := parseShimFull(raw); if e2 != nil { continue } + if (h.Flags & flagDecoy) != 0 { metricDecoyDropped.Add(1); if rt.dropLog { logf(LDebug, "[client] drop decoy %dB", len(p)) }; continue } + payload = p + if (h.Flags & flagEnc) != 0 && rt.ae != nil { + ns := rt.ae.aead.NonceSize(); if len(payload) >= ns { + if plain, e := rt.ae.open(payload[:ns], payload[ns:]); e == nil { payload = plain } + } + } + } + appPeerMu.RLock(); dstPeer := appPeer; appPeerMu.RUnlock() + if dstPeer != nil { + _, _ = uc.WriteToUDP(payload, dstPeer) + metricFramesDown.Add(1); metricBytesDown.Add(int64(len(payload))) + } + continue + } + + // 来自应用:加壳 -> 先发真实帧,再插播 shim 诱饵(RPS 优先)→ 插播额外伪报文 + appPeerMu.Lock(); appPeer = from; appPeerMu.Unlock() + p := append([]byte(nil), buf[:n]...) + + // a) 真实帧(带 shim) + flags := uint8(0) + pp := p + if rt.ae != nil { + if c, nonce, e := rt.ae.seal(p); e == nil { pp = append(nonce, c...); flags |= flagEnc } + } + h := shimHeader{Magic: magicConst, Version: version, Mode: modeUDP, Flags: flags, + SessionID: sess, TsMs: uint32(time.Now().UnixMilli()), Len: uint32(len(pp))} + frame := append(h.Marshal(), pp...) + if rt.cfg.Wire == WireRTPish { + rtpTs += uint32(step) + rtpHdr := buildRTPHeader(rtp.seq, rtpTs, 96, false, rtp.ssrc) + rtp.seq++ + frame = append(rtpHdr, frame...) + } + rt.tb.wait(len(frame) + 28) + _, _ = uc.WriteToUDP(frame, saddr) + metricFramesUp.Add(1); metricBytesUp.Add(int64(n)) + srcIP := ip4OrLoopback(uc.LocalAddr().(*net.UDPAddr).IP) + dstIP := ip4OrLoopback(saddr.IP) + rt.pc.WriteUDP(srcIP, uc.LocalAddr().(*net.UDPAddr).Port, dstIP, saddr.Port, frame) + + // b) shim 诱饵(带 shim,RPS 优先,否则按百分比) + sendDecoy := false + if rt.shimUpRL != nil && rt.shimUpRL.takeMax(1) > 0 { sendDecoy = true + } else if rt.cfg.DecoyRps <= 0 && grnd.Intn(100) < rt.cfg.DecoyPct { sendDecoy = true } + if sendDecoy { + djunk := make([]byte, len(p)) + _, _ = cryptoRand.Read(djunk) + decFlags := uint8(flagDecoy) + decPayload := djunk + if rt.ae != nil { + if c, nonce, e := rt.ae.seal(decPayload); e == nil { + decPayload = append(nonce, c...) + decFlags |= flagEnc + } + } + hd := shimHeader{Magic: magicConst, Version: version, Mode: modeUDP, Flags: decFlags, + SessionID: sess, TsMs: uint32(time.Now().UnixMilli()), Len: uint32(len(decPayload))} + df := append(hd.Marshal(), decPayload...) + if rt.cfg.Wire == WireRTPish { + rtpTs += uint32(step) + rtpHdr := buildRTPHeader(rtp.seq, rtpTs, 96, false, rtp.ssrc) + rtp.seq++ + df = append(rtpHdr, df...) + } + rt.tb.wait(len(df) + 28) + _, _ = uc.WriteToUDP(df, saddr) + metricFramesUp.Add(1); metricShimDecoySent.Add(1) + rt.pc.WriteUDP(srcIP, uc.LocalAddr().(*net.UDPAddr).Port, dstIP, saddr.Port, df) + } + + // c) 额外伪报文(无 shim;RTCP / pure RTP / STUN) + sendExtraDecoysUDP(uc, uc.LocalAddr().(*net.UDPAddr), saddr, rt, rtp.ssrc, &rtp.seq, &rtpTs, step, true) + + // 节奏(fps) + base := 1000 / max(1, rt.cfg.FPS) + jitterSleep(base, rt.cfg.JitterPct) + } + }() + + <-ctx.Done() + return nil +} + +type udpSess struct { + client *net.UDPAddr + target *net.UDPConn + last int64 +} + +func runUDPServer(ctx context.Context, rt *runtimeCtx) error { + laddr, err := net.ResolveUDPAddr("udp", rt.cfg.Listen); if err != nil { return err } + ln, err := net.ListenUDP("udp", laddr); if err != nil { return err } + defer ln.Close() + if rt.cfg.UDPRcvBuf > 0 { _ = ln.SetReadBuffer(rt.cfg.UDPRcvBuf) } + if rt.cfg.UDPSndBuf > 0 { _ = ln.SetWriteBuffer(rt.cfg.UDPSndBuf) } + fwd, err := net.ResolveUDPAddr("udp", rt.cfg.ForwardAddr); if err != nil { return err } + logf(LInfo, "[server][udp] %s -> %s (wire=%s, %dfps, %dMbps)", + rt.cfg.Listen, rt.cfg.ForwardAddr, rt.cfg.Wire, rt.cfg.FPS, rt.cfg.BitrateMbps) + + type key struct{ ip string; port int; sess uint32 } + var mu sync.Mutex + smap := map[key]*udpSess{} + + // session GC + go func() { + t := time.NewTicker(60 * time.Second) + for { + select { + case <-t.C: + now := time.Now().Unix() + mu.Lock() + for k, s := range smap { + if now-atomic.LoadInt64(&s.last) > 300 { + if s.target != nil { s.target.Close() } + delete(smap, k) + metricSessions.Add(-1) + } + } + mu.Unlock() + case <-ctx.Done(): + return + } + } + }() + + buf := make([]byte, 64<<10) + for { + n, caddr, err := ln.ReadFromUDP(buf) + if err != nil { + select { case <-ctx.Done(): return nil ; default: return err } + } + raw := append([]byte(nil), buf[:n]...) + src := ip4OrLoopback(caddr.IP) + dst := ip4OrLoopback(ln.LocalAddr().(*net.UDPAddr).IP) + rt.pc.WriteUDP(src, caddr.Port, dst, ln.LocalAddr().(*net.UDPAddr).Port, raw) + + // 剥线(RTP+Shim 或 Shim) + var h shimHeader + var p []byte + if rt.cfg.Wire == WireRTPish { + _, _, rest, e := stripRTP(raw); if e != nil { continue } + hh, pp, e2 := parseShimFull(rest); if e2 != nil { continue } + h, p = hh, pp + } else { + hh, pp, e2 := parseShimFull(raw); if e2 != nil { continue } + h, p = hh, pp + } + k := key{ip: caddr.IP.String(), port: caddr.Port, sess: h.SessionID} + + // 丢诱饵 & 解密 + if (h.Flags & flagDecoy) != 0 { + metricDecoyDropped.Add(1) + if rt.dropLog { logf(LDebug, "[server] drop shim-decoy %dB", len(p)) } + continue + } + if (h.Flags & flagEnc) != 0 && rt.ae != nil { + ns := rt.ae.aead.NonceSize() + if len(p) >= ns { + if plain, e := rt.ae.open(p[:ns], p[ns:]); e == nil { p = plain } + } + } + + mu.Lock() + sess := smap[k] + if sess == nil { + tconn, err := net.DialUDP("udp", nil, fwd) + if err != nil { mu.Unlock(); continue } + sess = &udpSess{client: caddr, target: tconn, last: time.Now().Unix()} + smap[k] = sess; metricSessions.Add(1) + + // 回程:目标 -> client(真实帧 + shim诱饵 + 额外伪报文) + go func(k key, s *udpSess) { + b := make([]byte, 64<<10) + // RTP 构造(用于 RTP-ish + keepalive) + var rtp = rtpState{seq: uint16(grnd.Uint32()), ssrc: grnd.Uint32()} + var rtpTs uint32 + step := 90000 / max(1, rt.cfg.FPS) + + for { + n2, _, e2 := s.target.ReadFromUDP(b) + if e2 != nil { return } + pp := append([]byte(nil), b[:n2]...) + + // a) 真实帧(带 shim) + flags := uint8(0) + payload := pp + if rt.ae != nil { + if c, nonce, e := rt.ae.seal(payload); e == nil { + payload = append(nonce, c...) + flags |= flagEnc + } + } + h := shimHeader{Magic: magicConst, Version: version, Mode: modeUDP, Flags: flags, + SessionID: k.sess, TsMs: uint32(time.Now().UnixMilli()), Len: uint32(len(payload))} + pkt := append(h.Marshal(), payload...) + if rt.cfg.Wire == WireRTPish { + rtpTs += uint32(step) + rtpHdr := buildRTPHeader(rtp.seq, rtpTs, 96, false, rtp.ssrc) + rtp.seq++ + pkt = append(rtpHdr, pkt...) + } + rt.tb.wait(len(pkt) + 28) + _, _ = ln.WriteToUDP(pkt, s.client) + metricFramesDown.Add(1); metricBytesDown.Add(int64(n2)) + src2 := ip4OrLoopback(ln.LocalAddr().(*net.UDPAddr).IP) + dst2 := ip4OrLoopback(s.client.IP) + rt.pc.WriteUDP(src2, ln.LocalAddr().(*net.UDPAddr).Port, dst2, s.client.Port, pkt) + + // b) shim 诱饵(RPS 优先,否则百分比) + sendDecoy := false + if rt.shimDownRL != nil && rt.shimDownRL.takeMax(1) > 0 { sendDecoy = true + } else if rt.cfg.DecoyRps <= 0 && grnd.Intn(100) < rt.cfg.DecoyPct { sendDecoy = true } + if sendDecoy { + djunk := make([]byte, len(pp)) + _, _ = cryptoRand.Read(djunk) + decFlags := uint8(flagDecoy) + decPayload := djunk + if rt.ae != nil { + if c, nonce, e := rt.ae.seal(decPayload); e == nil { + decPayload = append(nonce, c...) + decFlags |= flagEnc + } + } + hd := shimHeader{Magic: magicConst, Version: version, Mode: modeUDP, Flags: decFlags, + SessionID: k.sess, TsMs: uint32(time.Now().UnixMilli()), Len: uint32(len(decPayload))} + df := append(hd.Marshal(), decPayload...) + if rt.cfg.Wire == WireRTPish { + rtpTs += uint32(step) + rtpHdr := buildRTPHeader(rtp.seq, rtpTs, 96, false, rtp.ssrc) + rtp.seq++ + df = append(rtpHdr, df...) + } + rt.tb.wait(len(df) + 28) + _, _ = ln.WriteToUDP(df, s.client) + metricFramesDown.Add(1); metricShimDecoySent.Add(1) + rt.pc.WriteUDP(src2, ln.LocalAddr().(*net.UDPAddr).Port, dst2, s.client.Port, df) + } + + // c) 额外伪报文(无 shim;RTCP / pure RTP / STUN),发往 client + sendExtraDecoysUDP(ln, ln.LocalAddr().(*net.UDPAddr), s.client, rt, rtp.ssrc, &rtp.seq, &rtpTs, step, false) + + // 节奏(fps) + base := 1000 / max(1, rt.cfg.FPS) + jitterSleep(base, rt.cfg.JitterPct) + } + }(k, sess) + } + atomic.StoreInt64(&sess.last, time.Now().Unix()) + mu.Unlock() + + // 发往真实目标(原始负载) + rt.tb.wait(len(p) + 28) + _, _ = sess.target.Write(p) + metricFramesUp.Add(1); metricBytesUp.Add(int64(len(p))) + } +} + +/* ============ TCP Client/Server ============ */ + +// Client:应用<->(加/解壳)<->Server +func runTCPClient(ctx context.Context, rt *runtimeCtx) error { + ln, err := net.Listen("tcp", rt.cfg.Listen); if err != nil { return err } + defer ln.Close() + logf(LInfo, "[client][tcp] %s -> server %s", rt.cfg.Listen, rt.cfg.ServerAddr) + for { + ac, err := ln.Accept() + if err != nil { select { case <-ctx.Done(): return nil ; default: return err } } + go func(app net.Conn) { + defer app.Close() + s, err := net.Dial("tcp", rt.cfg.ServerAddr); if err != nil { return } + defer s.Close() + // app->server: 加壳(写入 s,记录 TCP PCAP) + go tcpUp(app, s, rt) + // server->app: 解壳(从 s 读网络帧,记录 TCP PCAP) + tcpDown(s, app, rt) + }(ac) + } +} + +// Server:client<-(加/解壳)->target(注意方向) +func runTCPServer(ctx context.Context, rt *runtimeCtx) error { + ln, err := net.Listen("tcp", rt.cfg.Listen); if err != nil { return err } + defer ln.Close() + logf(LInfo, "[server][tcp] %s -> %s", rt.cfg.Listen, rt.cfg.ForwardAddr) + for { + c, err := ln.Accept() + if err != nil { select { case <-ctx.Done(): return nil ; default: return err } } + go func(cli net.Conn) { + defer cli.Close() + dst, err := net.Dial("tcp", rt.cfg.ForwardAddr) + if err != nil { logf(LError, "dial forward: %v", err); return } + defer dst.Close() + + // 从 client 读带壳 -> 解壳 -> 写给目标(记录来自 cli 的网络帧) + go tcpDown(cli, dst, rt) + // 从目标读原始 -> 加壳 -> 回 client(记录写给 cli 的网络帧) + tcpUp(dst, cli, rt) + }(c) + } +} + +// tcpUp:从 r 读“原始数据”,分片+抖动+(可选)加密,封成带壳帧写给 w(网络 conn) +// 诱饵为“插播”(RPS 优先),不影响真实流 +func tcpUp(r net.Conn, w net.Conn, rt *runtimeCtx) { + buf := make([]byte, 64<<10) + for { + n, err := r.Read(buf) + if n > 0 { + p := append([]byte(nil), buf[:n]...) + parts := splitTCP(p, rt.cfg.FrameMin, rt.cfg.FrameMax) + for _, ck := range parts { + // a) 真实帧 + realFlags := uint8(0) + realPayload := ck + if rt.ae != nil { + if c, nonce, e := rt.ae.seal(realPayload); e == nil { + realPayload = append(nonce, c...) + realFlags |= flagEnc + } + } + h := shimHeader{Magic: magicConst, Version: version, Mode: modeTCP, Flags: realFlags, + SessionID: grnd.Uint32(), TsMs: uint32(time.Now().UnixMilli()), Len: uint32(len(realPayload))} + frame := append(h.Marshal(), realPayload...) + rt.tb.wait(len(frame)) + if _, err := w.Write(frame); err != nil { return } + metricFramesUp.Add(1); metricBytesUp.Add(int64(len(ck))) + + // TCP PCAP(网络方向:w.Local->w.Remote) + if rt.pc != nil { + if la, ok1 := w.LocalAddr().(*net.TCPAddr); ok1 { + if ra, ok2 := w.RemoteAddr().(*net.TCPAddr); ok2 { + rt.pc.WriteTCP(ip4OrLoopback(la.IP), la.Port, ip4OrLoopback(ra.IP), ra.Port, frame) + } + } + } + + jitterSleep(rt.cfg.PaceMs, rt.cfg.JitterPct) + + // b) shim 诱饵插播(RPS 优先,否则百分比) + sendDecoy := false + if rt.shimUpRL != nil && rt.shimUpRL.takeMax(1) > 0 { sendDecoy = true + } else if rt.cfg.DecoyRps <= 0 && grnd.Intn(100) < rt.cfg.DecoyPct { sendDecoy = true } + if sendDecoy { + djunk := make([]byte, len(ck)) + _, _ = cryptoRand.Read(djunk) + decFlags := uint8(flagDecoy) + decPayload := djunk + if rt.ae != nil { + if c, nonce, e := rt.ae.seal(decPayload); e == nil { + decPayload = append(nonce, c...) + decFlags |= flagEnc + } + } + hd := shimHeader{Magic: magicConst, Version: version, Mode: modeTCP, Flags: decFlags, + SessionID: grnd.Uint32(), TsMs: uint32(time.Now().UnixMilli()), Len: uint32(len(decPayload))} + f2 := append(hd.Marshal(), decPayload...) + rt.tb.wait(len(f2)) + if _, err := w.Write(f2); err != nil { return } + metricFramesUp.Add(1); metricShimDecoySent.Add(1) + + // PCAP 记录诱饵 + if rt.pc != nil { + if la, ok1 := w.LocalAddr().(*net.TCPAddr); ok1 { + if ra, ok2 := w.RemoteAddr().(*net.TCPAddr); ok2 { + rt.pc.WriteTCP(ip4OrLoopback(la.IP), la.Port, ip4OrLoopback(ra.IP), ra.Port, f2) + } + } + } + } + } + } + if err != nil { return } + } +} + +// tcpDown:从 r(网络 conn)读“带壳帧”,解壳后写给 w(应用或另一侧网络) +// 同时用 r 的 5元组记录“入向”PCAP(debug) +func tcpDown(r net.Conn, w net.Conn, rt *runtimeCtx) { + for { + hb := make([]byte, shimLen) + if _, err := io.ReadFull(r, hb); err != nil { return } + h, err := parseShimHeader(hb) + if err != nil { return } + payload := make([]byte, h.Len) + if _, err := io.ReadFull(r, payload); err != nil { return } + + // PCAP 记录“网络入向”帧(按 r.Local->r.Remote) + if rt.pc != nil { + if la, ok1 := r.LocalAddr().(*net.TCPAddr); ok1 { + if ra, ok2 := r.RemoteAddr().(*net.TCPAddr); ok2 { + frame := append(append([]byte{}, hb...), payload...) + rt.pc.WriteTCP(ip4OrLoopback(la.IP), la.Port, ip4OrLoopback(ra.IP), ra.Port, frame) + } + } + } + + if (h.Flags & flagDecoy) != 0 { + metricDecoyDropped.Add(1) + if rt.dropLog { logf(LDebug, "drop shim-decoy %dB", len(payload)) } + continue + } + if (h.Flags & flagEnc) != 0 && rt.ae != nil { + ns := rt.ae.aead.NonceSize() + if len(payload) >= ns { + if plain, e := rt.ae.open(payload[:ns], payload[ns:]); e == nil { + payload = plain + } + } + } + if _, err := w.Write(payload); err != nil { return } + metricFramesDown.Add(1); metricBytesDown.Add(int64(len(payload))) + } +} + +func splitTCP(buf []byte, min, max int) [][]byte { + if min <= 0 { min = 800 } + if max < min { max = min } + var out [][]byte + for len(buf) > 0 { + n := randRange(min, max) + if n > len(buf) { n = len(buf) } + out = append(out, append([]byte(nil), buf[:n]...)) + buf = buf[n:] + } + return out +} + +/* ============ Self-test (UDP) ============ */ + +func runSelfTest(cfg *Config) { + setLog(cfg.LogLevel) + tb := newTB(cfg.BitrateMbps, 512*1024) + ae, _ := newAEAD(cfg.AESKeyHex) + pc, _ := newPCAP(cfg.PcapPath, int64(cfg.PcapMaxMB)*1024*1024) + + // build runtime with decoy RL + upRL := newDecoyRL(cfg, true) + downRL := newDecoyRL(cfg, false) + shimUp := newRL(cfg.DecoyRps * 0.5) + shimDown := newRL(cfg.DecoyRps * 0.5) + + rt := &runtimeCtx{ + cfg: cfg, tb: tb, ae: ae, pc: pc, dropLog: cfg.LogDrop, + upRL: upRL, downRL: downRL, + shimUpRL: shimUp, shimDownRL: shimDown, + } + defer func() { if pc != nil { pc.Close() } }() + + // 内置 UDP Echo 目标(在 forward 上) + go func() { + addr, _ := net.ResolveUDPAddr("udp", cfg.ForwardAddr) + ln, err := net.ListenUDP("udp", addr) + if err != nil { logf(LError, "echo bind: %v", err); return } + defer ln.Close() + buf := make([]byte, 64<<10) + for { + n, from, err := ln.ReadFromUDP(buf); if err != nil { return } + _, _ = ln.WriteToUDP(buf[:n], from) + } + }() + + // Server + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { _ = runUDPServer(ctx, rt) }() + + // Client + ccfg := *cfg + ccfg.Role = RoleClient + ccfg.Listen = ":7001" + rtC := &runtimeCtx{ + cfg: &ccfg, tb: tb, ae: ae, pc: pc, dropLog: cfg.LogDrop, + upRL: upRL, downRL: downRL, + shimUpRL: shimUp, shimDownRL: shimDown, + } + go func() { _ = runUDPClient(ctx, rtC) }() + + // 负载发生器(应用 -> client.listen) + go func() { + dst, _ := net.ResolveUDPAddr("udp", ccfg.Listen) + c, err := net.DialUDP("udp", nil, dst); if err != nil { return } + defer c.Close() + fps := max(1, cfg.FPS) + avgBytesPerSec := cfg.BitrateMbps * 1024 * 1024 / 8 + avgPerFrame := avgBytesPerSec / fps + ticker := time.NewTicker(time.Second / time.Duration(fps)) + defer ticker.Stop() + lastKey := time.Now() + for { + select { + case <-ticker.C: + var size int + isKey := time.Since(lastKey) >= time.Duration(cfg.GOPMs)*time.Millisecond + if isKey { + lastKey = time.Now() + size = int(float64(avgPerFrame)*2.5) + randRange(2000, 6000) + } else { + size = int(float64(avgPerFrame)*0.6) + randRange(300, 1500) + } + if size < 200 { size = 200 } + payload := make([]byte, size) + _, _ = cryptoRand.Read(payload) + _, _ = c.Write(payload) + case <-ctx.Done(): + return + } + } + }() + + logf(LInfo, "[selftest] running %v ...", cfg.SelfDur) + time.Sleep(cfg.SelfDur) + cancel() + logf(LInfo, "[selftest] done. pcap=%s metrics on %s (/debug/vars)", cfg.PcapPath, cfg.MetricsAddr) +} + +/* ============ main ============ */ + +func main() { + cfg := parseFlags() + setLog(cfg.LogLevel) + runMetrics(cfg.MetricsAddr) + + switch cfg.Role { + case RoleSelf: + runSelfTest(cfg) + return + } + + tb := newTB(cfg.BitrateMbps, 512*1024) + ae, err := newAEAD(cfg.AESKeyHex); must(err) + pc, err := newPCAP(cfg.PcapPath, int64(cfg.PcapMaxMB)*1024*1024); must(err) + + // build runtime with decoy RL + upRL := newDecoyRL(cfg, true) + downRL := newDecoyRL(cfg, false) + shimUp := newRL(cfg.DecoyRps * 0.5) + shimDown := newRL(cfg.DecoyRps * 0.5) + + rt := &runtimeCtx{ + cfg: cfg, tb: tb, ae: ae, pc: pc, dropLog: cfg.LogDrop, + upRL: upRL, downRL: downRL, + shimUpRL: shimUp, shimDownRL: shimDown, + } + defer func() { if pc != nil { pc.Close() } }() + + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + switch cfg.Role { + case RoleClient: + switch cfg.Mode { + case ModeUDP: must(runUDPClient(ctx, rt)) + case ModeTCP: must(runTCPClient(ctx, rt)) + default: must(fmt.Errorf("unknown mode")) + } + case RoleServer: + switch cfg.Mode { + case ModeUDP: must(runUDPServer(ctx, rt)) + case ModeTCP: must(runTCPServer(ctx, rt)) + default: must(fmt.Errorf("unknown mode")) + } + default: + must(fmt.Errorf("unknown role")) + } +} diff --git a/server.sh b/server.sh new file mode 100755 index 0000000..978953e --- /dev/null +++ b/server.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +./camostream -role=server -mode=tcp -listen=:39001 -forward=127.0.0.1:4141 \ + -bitrate-mbps=20 -decoy-rps=10 \ + -pcap=tcp_server.pcap -pcap-max-mb=50 -metrics=:9100 -log=info + diff --git a/server_udp.sh b/server_udp.sh new file mode 100755 index 0000000..671414e --- /dev/null +++ b/server_udp.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +./camostream -role=server -mode=udp -listen=:39001 -forward=127.0.0.1:18081 \ + -wire=rtpish -fps=60 -bitrate-mbps=20 \ + -decoy-rps=12 \ + -rtcp-sr-rps=2 -rtcp-rr-rps=3 -rtpkeep-rps=4 -stun-rps=1 \ + -pcap=udp_server.pcap -pcap-max-mb=100 -metrics=:9100 -log=info + diff --git a/sim/README.md b/sim/README.md new file mode 100644 index 0000000..8f3ea06 --- /dev/null +++ b/sim/README.md @@ -0,0 +1,17 @@ + +### udp_测试 + +* client 使用脚本 [client_udp.sh](../client_udp.sh) +* server 使用脚本 [server_udp.sh](../server_udp.sh) + + +```shell + +echo -n "test" | nc -u 127.0.0.1 37001 +HTTP/1.1 200 OK +Content-Type: text/html; charset=UTF-8 +Content-Length: 60 + +

Hello from UDP HTML Server!

+ +``` \ No newline at end of file diff --git a/sim/udp_server.go b/sim/udp_server.go new file mode 100644 index 0000000..e8ccbf9 --- /dev/null +++ b/sim/udp_server.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "net" +) + +func main() { + addr := net.UDPAddr{ + Port: 18081, + IP: net.ParseIP("0.0.0.0"), + } + + conn, err := net.ListenUDP("udp", &addr) + if err != nil { + panic(err) + } + defer conn.Close() + + fmt.Println("UDP server listening on", addr.String()) + + buf := make([]byte, 2048) + + for { + n, clientAddr, err := conn.ReadFromUDP(buf) + if err != nil { + fmt.Println("read error:", err) + continue + } + + fmt.Printf("Received from %s: %s\n", clientAddr.String(), string(buf[:n])) + + htmlResponse := `HTTP/1.1 200 OK +Content-Type: text/html; charset=UTF-8 +Content-Length: 60 + +

Hello from UDP HTML Server!

` + + _, err = conn.WriteToUDP([]byte(htmlResponse), clientAddr) + if err != nil { + fmt.Println("write error:", err) + } + } +} diff --git a/tcp_client.pcap b/tcp_client.pcap new file mode 100644 index 0000000000000000000000000000000000000000..9c06a342864999ac1f7617b61cc0371cafa11b4e GIT binary patch literal 5432 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 A>;M1& literal 0 HcmV?d00001 diff --git a/tcp_server.pcap b/tcp_server.pcap new file mode 100644 index 0000000000000000000000000000000000000000..aa027e6c9a8a80f5ba5922f103b2baf00acbd065 GIT binary patch literal 5432 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