From 5e2e7e183480df3d61be2f20fafdbca64a47a52d Mon Sep 17 00:00:00 2001 From: moralpriest Date: Sun, 12 Jul 2026 21:27:12 -0600 Subject: [PATCH] fix(metrics): remove deprecated FECShortShards and add new KCP metrics - Remove deprecated KCP_FECShortShards metric (field removed in kcp-go v5.6.72) - Add new KCP SNMP metrics: FECFullShardSet, FECShardSet, FECShardMin, RingBufferSndQueue, RingBufferRcvQueue, RingBufferSndBuffer, OOBPackets - Vendor kcp-go v5.6.72 (replaces old ~2020 vendored copy) without touching go.mod - Drop dead vendored deps github.com/templexxx/cpu and templexxx/xorsimd (only imported by the old kcp-go; new version uses klauspost/cpuid/v2) All 30 Snmp struct fields are now exported. Preparatory work for the eventual go.mod modernization (PR #19). API compatibility verified: kcp-go v5.6.72 symbol signatures used by metrics.go and p2p/controller.go are identical to the prior version. --- metrics/metrics.go | 35 +- vendor/github.com/templexxx/cpu/.gitignore | 13 - vendor/github.com/templexxx/cpu/LICENSE | 32 - vendor/github.com/templexxx/cpu/README.md | 23 - vendor/github.com/templexxx/cpu/cpu.go | 235 --- vendor/github.com/templexxx/cpu/cpu_386.go | 7 - vendor/github.com/templexxx/cpu/cpu_amd64.go | 7 - .../github.com/templexxx/cpu/cpu_amd64p32.go | 7 - vendor/github.com/templexxx/cpu/cpu_arm.go | 7 - vendor/github.com/templexxx/cpu/cpu_arm64.go | 102 -- vendor/github.com/templexxx/cpu/cpu_mips.go | 7 - vendor/github.com/templexxx/cpu/cpu_mips64.go | 7 - .../github.com/templexxx/cpu/cpu_mips64le.go | 7 - vendor/github.com/templexxx/cpu/cpu_mipsle.go | 7 - .../github.com/templexxx/cpu/cpu_no_init.go | 16 - vendor/github.com/templexxx/cpu/cpu_ppc64x.go | 68 - .../github.com/templexxx/cpu/cpu_riscv64.go | 10 - vendor/github.com/templexxx/cpu/cpu_s390x.go | 153 -- vendor/github.com/templexxx/cpu/cpu_s390x.s | 55 - vendor/github.com/templexxx/cpu/cpu_wasm.go | 7 - vendor/github.com/templexxx/cpu/cpu_x86.go | 433 ------ vendor/github.com/templexxx/cpu/cpu_x86.s | 32 - .../templexxx/xorsimd/.gitattributes | 1 - .../xorsimd/.github/workflows/unit-test.yml | 36 - .../github.com/templexxx/xorsimd/.gitignore | 13 - vendor/github.com/templexxx/xorsimd/LICENSE | 21 - vendor/github.com/templexxx/xorsimd/README.md | 46 - vendor/github.com/templexxx/xorsimd/go.mod | 5 - vendor/github.com/templexxx/xorsimd/go.sum | 2 - vendor/github.com/templexxx/xorsimd/xor.go | 89 -- .../github.com/templexxx/xorsimd/xor_amd64.go | 95 -- .../templexxx/xorsimd/xor_generic.go | 205 --- .../github.com/templexxx/xorsimd/xor_test.go | 480 ------ .../templexxx/xorsimd/xoravx2_amd64.s | 124 -- .../templexxx/xorsimd/xoravx512_amd64.s | 124 -- .../templexxx/xorsimd/xorbytes_amd64.s | 72 - .../templexxx/xorsimd/xorsse2_amd64.s | 123 -- vendor/github.com/xtaci/kcp-go/v5/.gitignore | 25 - vendor/github.com/xtaci/kcp-go/v5/.travis.yml | 24 - vendor/github.com/xtaci/kcp-go/v5/LICENSE | 2 +- vendor/github.com/xtaci/kcp-go/v5/README.md | 318 ++-- vendor/github.com/xtaci/kcp-go/v5/autotune.go | 153 +- .../xtaci/kcp-go/v5/autotune_test.go | 568 ++++++- .../github.com/xtaci/kcp-go/v5/batchconn.go | 12 - .../github.com/xtaci/kcp-go/v5/bufferpool.go | 65 + .../xtaci/kcp-go/v5/bufferpool_test.go | 79 + vendor/github.com/xtaci/kcp-go/v5/crypt.go | 522 ++++--- .../github.com/xtaci/kcp-go/v5/crypt_test.go | 240 ++- vendor/github.com/xtaci/kcp-go/v5/entropy.go | 179 ++- .../xtaci/kcp-go/v5/entropy_test.go | 142 ++ .../xtaci/kcp-go/v5/examples/echo.go | 77 - vendor/github.com/xtaci/kcp-go/v5/fec.go | 453 ++++-- vendor/github.com/xtaci/kcp-go/v5/fec_test.go | 472 +++++- vendor/github.com/xtaci/kcp-go/v5/flame.png | Bin 57142 -> 0 bytes vendor/github.com/xtaci/kcp-go/v5/frame.png | Bin 36006 -> 0 bytes vendor/github.com/xtaci/kcp-go/v5/go.mod | 28 +- vendor/github.com/xtaci/kcp-go/v5/go.sum | 127 +- vendor/github.com/xtaci/kcp-go/v5/kcp-go.png | Bin 9153 -> 0 bytes vendor/github.com/xtaci/kcp-go/v5/kcp.go | 887 +++++------ vendor/github.com/xtaci/kcp-go/v5/kcp_test.go | 137 +- .../xtaci/kcp-go/v5/kcp_trace_off.go | 6 + .../xtaci/kcp-go/v5/kcp_trace_on.go | 39 + vendor/github.com/xtaci/kcp-go/v5/pkg.go | 8 + .../xtaci/kcp-go/v5/platform_generic.go | 29 + .../xtaci/kcp-go/v5/platform_linux.go | 76 + vendor/github.com/xtaci/kcp-go/v5/readloop.go | 87 +- .../xtaci/kcp-go/v5/readloop_generic.go | 24 +- .../xtaci/kcp-go/v5/readloop_linux.go | 138 +- .../github.com/xtaci/kcp-go/v5/ringbuffer.go | 244 ++++ .../xtaci/kcp-go/v5/ringbuffer_test.go | 301 ++++ vendor/github.com/xtaci/kcp-go/v5/sess.go | 1163 ++++++++++----- .../github.com/xtaci/kcp-go/v5/sess_test.go | 1299 ++++++++++++++--- vendor/github.com/xtaci/kcp-go/v5/snmp.go | 156 +- .../github.com/xtaci/kcp-go/v5/timedsched.go | 88 +- vendor/github.com/xtaci/kcp-go/v5/tx.go | 32 +- .../github.com/xtaci/kcp-go/v5/tx_generic.go | 24 +- vendor/github.com/xtaci/kcp-go/v5/tx_linux.go | 57 +- 77 files changed, 6383 insertions(+), 4604 deletions(-) delete mode 100644 vendor/github.com/templexxx/cpu/.gitignore delete mode 100644 vendor/github.com/templexxx/cpu/LICENSE delete mode 100644 vendor/github.com/templexxx/cpu/README.md delete mode 100644 vendor/github.com/templexxx/cpu/cpu.go delete mode 100644 vendor/github.com/templexxx/cpu/cpu_386.go delete mode 100644 vendor/github.com/templexxx/cpu/cpu_amd64.go delete mode 100644 vendor/github.com/templexxx/cpu/cpu_amd64p32.go delete mode 100644 vendor/github.com/templexxx/cpu/cpu_arm.go delete mode 100644 vendor/github.com/templexxx/cpu/cpu_arm64.go delete mode 100644 vendor/github.com/templexxx/cpu/cpu_mips.go delete mode 100644 vendor/github.com/templexxx/cpu/cpu_mips64.go delete mode 100644 vendor/github.com/templexxx/cpu/cpu_mips64le.go delete mode 100644 vendor/github.com/templexxx/cpu/cpu_mipsle.go delete mode 100644 vendor/github.com/templexxx/cpu/cpu_no_init.go delete mode 100644 vendor/github.com/templexxx/cpu/cpu_ppc64x.go delete mode 100644 vendor/github.com/templexxx/cpu/cpu_riscv64.go delete mode 100644 vendor/github.com/templexxx/cpu/cpu_s390x.go delete mode 100644 vendor/github.com/templexxx/cpu/cpu_s390x.s delete mode 100644 vendor/github.com/templexxx/cpu/cpu_wasm.go delete mode 100644 vendor/github.com/templexxx/cpu/cpu_x86.go delete mode 100644 vendor/github.com/templexxx/cpu/cpu_x86.s delete mode 100644 vendor/github.com/templexxx/xorsimd/.gitattributes delete mode 100644 vendor/github.com/templexxx/xorsimd/.github/workflows/unit-test.yml delete mode 100644 vendor/github.com/templexxx/xorsimd/.gitignore delete mode 100644 vendor/github.com/templexxx/xorsimd/LICENSE delete mode 100644 vendor/github.com/templexxx/xorsimd/README.md delete mode 100644 vendor/github.com/templexxx/xorsimd/go.mod delete mode 100644 vendor/github.com/templexxx/xorsimd/go.sum delete mode 100644 vendor/github.com/templexxx/xorsimd/xor.go delete mode 100644 vendor/github.com/templexxx/xorsimd/xor_amd64.go delete mode 100644 vendor/github.com/templexxx/xorsimd/xor_generic.go delete mode 100644 vendor/github.com/templexxx/xorsimd/xor_test.go delete mode 100644 vendor/github.com/templexxx/xorsimd/xoravx2_amd64.s delete mode 100644 vendor/github.com/templexxx/xorsimd/xoravx512_amd64.s delete mode 100644 vendor/github.com/templexxx/xorsimd/xorbytes_amd64.s delete mode 100644 vendor/github.com/templexxx/xorsimd/xorsse2_amd64.s delete mode 100644 vendor/github.com/xtaci/kcp-go/v5/.gitignore delete mode 100644 vendor/github.com/xtaci/kcp-go/v5/.travis.yml delete mode 100644 vendor/github.com/xtaci/kcp-go/v5/batchconn.go create mode 100644 vendor/github.com/xtaci/kcp-go/v5/bufferpool.go create mode 100644 vendor/github.com/xtaci/kcp-go/v5/bufferpool_test.go create mode 100644 vendor/github.com/xtaci/kcp-go/v5/entropy_test.go delete mode 100644 vendor/github.com/xtaci/kcp-go/v5/examples/echo.go delete mode 100644 vendor/github.com/xtaci/kcp-go/v5/flame.png delete mode 100644 vendor/github.com/xtaci/kcp-go/v5/frame.png delete mode 100644 vendor/github.com/xtaci/kcp-go/v5/kcp-go.png create mode 100644 vendor/github.com/xtaci/kcp-go/v5/kcp_trace_off.go create mode 100644 vendor/github.com/xtaci/kcp-go/v5/kcp_trace_on.go create mode 100644 vendor/github.com/xtaci/kcp-go/v5/pkg.go create mode 100644 vendor/github.com/xtaci/kcp-go/v5/platform_generic.go create mode 100644 vendor/github.com/xtaci/kcp-go/v5/platform_linux.go create mode 100644 vendor/github.com/xtaci/kcp-go/v5/ringbuffer.go create mode 100644 vendor/github.com/xtaci/kcp-go/v5/ringbuffer_test.go diff --git a/metrics/metrics.go b/metrics/metrics.go index 3f4dbdae..76c56ecf 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -18,18 +18,21 @@ package metrics -import "fmt" -import "io" -import "os" -import "time" -import "bytes" -import "net" -import "net/url" -import "net/http" -import "path/filepath" -import "github.com/go-logr/logr" -import "github.com/VictoriaMetrics/metrics" -import "github.com/xtaci/kcp-go/v5" +import ( + "bytes" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "time" + + "github.com/VictoriaMetrics/metrics" + "github.com/go-logr/logr" + "github.com/xtaci/kcp-go/v5" +) // these are exported by the daemon for various analysis var Version string //this is later converted to metrics format @@ -83,10 +86,16 @@ func writePrometheusMetrics(w io.Writer) { fmt.Fprintf(w, "KCP_EarlyRetransSegs %d\n", kcp.DefaultSnmp.EarlyRetransSegs) fmt.Fprintf(w, "KCP_LostSegs %d\n", kcp.DefaultSnmp.LostSegs) fmt.Fprintf(w, "KCP_RepeatSegs %d\n", kcp.DefaultSnmp.RepeatSegs) + fmt.Fprintf(w, "KCP_FECFullShardSet %d\n", kcp.DefaultSnmp.FECFullShardSet) fmt.Fprintf(w, "KCP_FECRecovered %d\n", kcp.DefaultSnmp.FECRecovered) fmt.Fprintf(w, "KCP_FECErrs %d\n", kcp.DefaultSnmp.FECErrs) fmt.Fprintf(w, "KCP_FECParityShards %d\n", kcp.DefaultSnmp.FECParityShards) - fmt.Fprintf(w, "KCP_FECShortShards %d\n", kcp.DefaultSnmp.FECShortShards) + fmt.Fprintf(w, "KCP_FECShardSet %d\n", kcp.DefaultSnmp.FECShardSet) + fmt.Fprintf(w, "KCP_FECShardMin %d\n", kcp.DefaultSnmp.FECShardMin) + fmt.Fprintf(w, "KCP_RingBufferSndQueue %d\n", kcp.DefaultSnmp.RingBufferSndQueue) + fmt.Fprintf(w, "KCP_RingBufferRcvQueue %d\n", kcp.DefaultSnmp.RingBufferRcvQueue) + fmt.Fprintf(w, "KCP_RingBufferSndBuffer %d\n", kcp.DefaultSnmp.RingBufferSndBuffer) + fmt.Fprintf(w, "KCP_OOBPackets %d\n", kcp.DefaultSnmp.OOBPackets) } diff --git a/vendor/github.com/templexxx/cpu/.gitignore b/vendor/github.com/templexxx/cpu/.gitignore deleted file mode 100644 index 63c61f36..00000000 --- a/vendor/github.com/templexxx/cpu/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, build with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out -.idea/ diff --git a/vendor/github.com/templexxx/cpu/LICENSE b/vendor/github.com/templexxx/cpu/LICENSE deleted file mode 100644 index dfa8f7b8..00000000 --- a/vendor/github.com/templexxx/cpu/LICENSE +++ /dev/null @@ -1,32 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2018 Temple3x (temple3x@gmail.com) -Copyright 2017 The Go Authors -Copyright (c) 2015 Klaus Post - -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/templexxx/cpu/README.md b/vendor/github.com/templexxx/cpu/README.md deleted file mode 100644 index 50ccb9fd..00000000 --- a/vendor/github.com/templexxx/cpu/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# cpu -internal/cpu(in Go standard lib) with these detections: - ->- AVX512 -> ->- Cache Size -> ->- Invariant TSC -> - -It also provides: - ->- False sharing range, see `X86FalseSharingRange` for X86 platform. -> ->- TSC frequency -> ->- Name -> ->- Family & Model - -# Acknowledgement - -[klauspost/cpuid](https://github.com/klauspost/cpuid) \ No newline at end of file diff --git a/vendor/github.com/templexxx/cpu/cpu.go b/vendor/github.com/templexxx/cpu/cpu.go deleted file mode 100644 index 767f8b3f..00000000 --- a/vendor/github.com/templexxx/cpu/cpu.go +++ /dev/null @@ -1,235 +0,0 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package cpu implements processor feature detection -// used by the Go standard library. -package cpu - -// debugOptions is set to true by the runtime if go was compiled with GOEXPERIMENT=debugcpu -// and GOOS is Linux or Darwin. This variable is linknamed in runtime/proc.go. -var debugOptions bool - -var X86 x86 - -// "Loads data or instructions from memory to the second-level cache. -// To use the streamer, organize the data or instructions in blocks of 128 bytes, -// aligned on 128 bytes." -// From , -// in section 3.7.3 "Hardware Prefetching for Second-Level Cache" -// -// In practice, I have found use 128bytes can gain better performance than 64bytes (one cache line). -const X86FalseSharingRange = 128 - -// The booleans in x86 contain the correspondingly named cpuid feature bit. -// HasAVX and HasAVX2 are only set if the OS does support XMM and YMM registers -// in addition to the cpuid feature bit being set. -// The struct is padded to avoid false sharing. -type x86 struct { - _ [X86FalseSharingRange]byte - HasCMPXCHG16B bool - HasAES bool - HasADX bool - HasAVX bool - HasAVX2 bool - HasAVX512F bool - HasAVX512DQ bool - HasAVX512BW bool - HasAVX512VL bool - HasBMI1 bool - HasBMI2 bool - HasERMS bool - HasFMA bool - HasOSXSAVE bool - HasPCLMULQDQ bool - HasPOPCNT bool - HasSSE2 bool - HasSSE3 bool - HasSSSE3 bool - HasSSE41 bool - HasSSE42 bool - // The invariant TSC will run at a constant rate in all ACPI P-, C-, and T-states. - // This is the architectural behavior moving forward. On processors with - // invariant TSC support, the OS may use the TSC for wall clock timer services (instead of ACPI or HPET timers). - HasInvariantTSC bool - - Cache Cache - - // TSCFrequency only meaningful when HasInvariantTSC == true. - // Unit: Hz. - // - // Warn: - // 1. If it's 0, means failed to get it from frequency table provided by Intel manual. - TSCFrequency uint64 - - Name string - Signature string // DisplayFamily_DisplayModel. - Family uint32 // CPU family number. - Model uint32 // CPU model number. - SteppingID uint32 - - _ [X86FalseSharingRange]byte -} - -// CPU Cache Size. -// -1 if undetected. -type Cache struct { - L1I int - L1D int - L2 int - L3 int -} - -var PPC64 ppc64 - -// For ppc64x, it is safe to check only for ISA level starting on ISA v3.00, -// since there are no optional categories. There are some exceptions that also -// require kernel support to work (darn, scv), so there are feature bits for -// those as well. The minimum processor requirement is POWER8 (ISA 2.07), so we -// maintain some of the old feature checks for optional categories for -// safety. -// The struct is padded to avoid false sharing. -type ppc64 struct { - _ [CacheLineSize]byte - HasVMX bool // Vector unit (Altivec) - HasDFP bool // Decimal Floating Point unit - HasVSX bool // Vector-scalar unit - HasHTM bool // Hardware Transactional Memory - HasISEL bool // Integer select - HasVCRYPTO bool // Vector cryptography - HasHTMNOSC bool // HTM: kernel-aborted transaction in syscalls - HasDARN bool // Hardware random number generator (requires kernel enablement) - HasSCV bool // Syscall vectored (requires kernel enablement) - IsPOWER8 bool // ISA v2.07 (POWER8) - IsPOWER9 bool // ISA v3.00 (POWER9) - _ [CacheLineSize]byte -} - -var ARM64 arm64 - -// The booleans in arm64 contain the correspondingly named cpu feature bit. -// The struct is padded to avoid false sharing. -type arm64 struct { - _ [CacheLineSize]byte - HasFP bool - HasASIMD bool - HasEVTSTRM bool - HasAES bool - HasPMULL bool - HasSHA1 bool - HasSHA2 bool - HasCRC32 bool - HasATOMICS bool - HasFPHP bool - HasASIMDHP bool - HasCPUID bool - HasASIMDRDM bool - HasJSCVT bool - HasFCMA bool - HasLRCPC bool - HasDCPOP bool - HasSHA3 bool - HasSM3 bool - HasSM4 bool - HasASIMDDP bool - HasSHA512 bool - HasSVE bool - HasASIMDFHM bool - _ [CacheLineSize]byte -} - -var S390X s390x - -type s390x struct { - _ [CacheLineSize]byte - HasZArch bool // z architecture mode is active [mandatory] - HasSTFLE bool // store facility list extended [mandatory] - HasLDisp bool // long (20-bit) displacements [mandatory] - HasEImm bool // 32-bit immediates [mandatory] - HasDFP bool // decimal floating point - HasETF3Enhanced bool // ETF-3 enhanced - HasMSA bool // message security assist (CPACF) - HasAES bool // KM-AES{128,192,256} functions - HasAESCBC bool // KMC-AES{128,192,256} functions - HasAESCTR bool // KMCTR-AES{128,192,256} functions - HasAESGCM bool // KMA-GCM-AES{128,192,256} functions - HasGHASH bool // KIMD-GHASH function - HasSHA1 bool // K{I,L}MD-SHA-1 functions - HasSHA256 bool // K{I,L}MD-SHA-256 functions - HasSHA512 bool // K{I,L}MD-SHA-512 functions - HasVX bool // vector facility. Note: the runtime sets this when it processes auxv records. - _ [CacheLineSize]byte -} - -// initialize examines the processor and sets the relevant variables above. -// This is called by the runtime package early in program initialization, -// before normal init functions are run. env is set by runtime on Linux and Darwin -// if go was compiled with GOEXPERIMENT=debugcpu. -func init() { - doinit() - processOptions("") -} - -// options contains the cpu debug options that can be used in GODEBUGCPU. -// Options are arch dependent and are added by the arch specific doinit functions. -// Features that are mandatory for the specific GOARCH should not be added to options -// (e.g. SSE2 on amd64). -var options []option - -// Option names should be lower case. e.g. avx instead of AVX. -type option struct { - Name string - Feature *bool -} - -// processOptions disables CPU feature values based on the parsed env string. -// The env string is expected to be of the form feature1=0,feature2=0... -// where feature names is one of the architecture specifc list stored in the -// cpu packages options variable. If env contains all=0 then all capabilities -// referenced through the options variable are disabled. Other feature -// names and values other than 0 are silently ignored. -func processOptions(env string) { -field: - for env != "" { - field := "" - i := indexByte(env, ',') - if i < 0 { - field, env = env, "" - } else { - field, env = env[:i], env[i+1:] - } - i = indexByte(field, '=') - if i < 0 { - continue - } - key, value := field[:i], field[i+1:] - - // Only allow turning off CPU features by specifying '0'. - if value == "0" { - if key == "all" { - for _, v := range options { - *v.Feature = false - } - return - } else { - for _, v := range options { - if v.Name == key { - *v.Feature = false - continue field - } - } - } - } - } -} - -// indexByte returns the index of the first instance of c in s, -// or -1 if c is not present in s. -func indexByte(s string, c byte) int { - for i := 0; i < len(s); i++ { - if s[i] == c { - return i - } - } - return -1 -} diff --git a/vendor/github.com/templexxx/cpu/cpu_386.go b/vendor/github.com/templexxx/cpu/cpu_386.go deleted file mode 100644 index 561c81f8..00000000 --- a/vendor/github.com/templexxx/cpu/cpu_386.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cpu - -const GOARCH = "386" diff --git a/vendor/github.com/templexxx/cpu/cpu_amd64.go b/vendor/github.com/templexxx/cpu/cpu_amd64.go deleted file mode 100644 index 9b001536..00000000 --- a/vendor/github.com/templexxx/cpu/cpu_amd64.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cpu - -const GOARCH = "amd64" diff --git a/vendor/github.com/templexxx/cpu/cpu_amd64p32.go b/vendor/github.com/templexxx/cpu/cpu_amd64p32.go deleted file mode 100644 index 177b14e3..00000000 --- a/vendor/github.com/templexxx/cpu/cpu_amd64p32.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cpu - -const GOARCH = "amd64p32" diff --git a/vendor/github.com/templexxx/cpu/cpu_arm.go b/vendor/github.com/templexxx/cpu/cpu_arm.go deleted file mode 100644 index 078a6c3b..00000000 --- a/vendor/github.com/templexxx/cpu/cpu_arm.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cpu - -const CacheLineSize = 32 diff --git a/vendor/github.com/templexxx/cpu/cpu_arm64.go b/vendor/github.com/templexxx/cpu/cpu_arm64.go deleted file mode 100644 index 487ccf8e..00000000 --- a/vendor/github.com/templexxx/cpu/cpu_arm64.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cpu - -const CacheLineSize = 64 - -// arm64 doesn't have a 'cpuid' equivalent, so we rely on HWCAP/HWCAP2. -// These are linknamed in runtime/os_linux_arm64.go and are initialized by -// archauxv(). -var hwcap uint -var hwcap2 uint - -// HWCAP/HWCAP2 bits. These are exposed by Linux. -const ( - hwcap_FP = (1 << 0) - hwcap_ASIMD = (1 << 1) - hwcap_EVTSTRM = (1 << 2) - hwcap_AES = (1 << 3) - hwcap_PMULL = (1 << 4) - hwcap_SHA1 = (1 << 5) - hwcap_SHA2 = (1 << 6) - hwcap_CRC32 = (1 << 7) - hwcap_ATOMICS = (1 << 8) - hwcap_FPHP = (1 << 9) - hwcap_ASIMDHP = (1 << 10) - hwcap_CPUID = (1 << 11) - hwcap_ASIMDRDM = (1 << 12) - hwcap_JSCVT = (1 << 13) - hwcap_FCMA = (1 << 14) - hwcap_LRCPC = (1 << 15) - hwcap_DCPOP = (1 << 16) - hwcap_SHA3 = (1 << 17) - hwcap_SM3 = (1 << 18) - hwcap_SM4 = (1 << 19) - hwcap_ASIMDDP = (1 << 20) - hwcap_SHA512 = (1 << 21) - hwcap_SVE = (1 << 22) - hwcap_ASIMDFHM = (1 << 23) -) - -func doinit() { - options = []option{ - {"evtstrm", &ARM64.HasEVTSTRM}, - {"aes", &ARM64.HasAES}, - {"pmull", &ARM64.HasPMULL}, - {"sha1", &ARM64.HasSHA1}, - {"sha2", &ARM64.HasSHA2}, - {"crc32", &ARM64.HasCRC32}, - {"atomics", &ARM64.HasATOMICS}, - {"fphp", &ARM64.HasFPHP}, - {"asimdhp", &ARM64.HasASIMDHP}, - {"cpuid", &ARM64.HasCPUID}, - {"asimdrdm", &ARM64.HasASIMDRDM}, - {"jscvt", &ARM64.HasJSCVT}, - {"fcma", &ARM64.HasFCMA}, - {"lrcpc", &ARM64.HasLRCPC}, - {"dcpop", &ARM64.HasDCPOP}, - {"sha3", &ARM64.HasSHA3}, - {"sm3", &ARM64.HasSM3}, - {"sm4", &ARM64.HasSM4}, - {"asimddp", &ARM64.HasASIMDDP}, - {"sha512", &ARM64.HasSHA512}, - {"sve", &ARM64.HasSVE}, - {"asimdfhm", &ARM64.HasASIMDFHM}, - - // These capabilities should always be enabled on arm64: - // {"fp", &ARM64.HasFP}, - // {"asimd", &ARM64.HasASIMD}, - } - - // HWCAP feature bits - ARM64.HasFP = isSet(hwcap, hwcap_FP) - ARM64.HasASIMD = isSet(hwcap, hwcap_ASIMD) - ARM64.HasEVTSTRM = isSet(hwcap, hwcap_EVTSTRM) - ARM64.HasAES = isSet(hwcap, hwcap_AES) - ARM64.HasPMULL = isSet(hwcap, hwcap_PMULL) - ARM64.HasSHA1 = isSet(hwcap, hwcap_SHA1) - ARM64.HasSHA2 = isSet(hwcap, hwcap_SHA2) - ARM64.HasCRC32 = isSet(hwcap, hwcap_CRC32) - ARM64.HasATOMICS = isSet(hwcap, hwcap_ATOMICS) - ARM64.HasFPHP = isSet(hwcap, hwcap_FPHP) - ARM64.HasASIMDHP = isSet(hwcap, hwcap_ASIMDHP) - ARM64.HasCPUID = isSet(hwcap, hwcap_CPUID) - ARM64.HasASIMDRDM = isSet(hwcap, hwcap_ASIMDRDM) - ARM64.HasJSCVT = isSet(hwcap, hwcap_JSCVT) - ARM64.HasFCMA = isSet(hwcap, hwcap_FCMA) - ARM64.HasLRCPC = isSet(hwcap, hwcap_LRCPC) - ARM64.HasDCPOP = isSet(hwcap, hwcap_DCPOP) - ARM64.HasSHA3 = isSet(hwcap, hwcap_SHA3) - ARM64.HasSM3 = isSet(hwcap, hwcap_SM3) - ARM64.HasSM4 = isSet(hwcap, hwcap_SM4) - ARM64.HasASIMDDP = isSet(hwcap, hwcap_ASIMDDP) - ARM64.HasSHA512 = isSet(hwcap, hwcap_SHA512) - ARM64.HasSVE = isSet(hwcap, hwcap_SVE) - ARM64.HasASIMDFHM = isSet(hwcap, hwcap_ASIMDFHM) -} - -func isSet(hwc uint, value uint) bool { - return hwc&value != 0 -} diff --git a/vendor/github.com/templexxx/cpu/cpu_mips.go b/vendor/github.com/templexxx/cpu/cpu_mips.go deleted file mode 100644 index 078a6c3b..00000000 --- a/vendor/github.com/templexxx/cpu/cpu_mips.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cpu - -const CacheLineSize = 32 diff --git a/vendor/github.com/templexxx/cpu/cpu_mips64.go b/vendor/github.com/templexxx/cpu/cpu_mips64.go deleted file mode 100644 index 078a6c3b..00000000 --- a/vendor/github.com/templexxx/cpu/cpu_mips64.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cpu - -const CacheLineSize = 32 diff --git a/vendor/github.com/templexxx/cpu/cpu_mips64le.go b/vendor/github.com/templexxx/cpu/cpu_mips64le.go deleted file mode 100644 index 078a6c3b..00000000 --- a/vendor/github.com/templexxx/cpu/cpu_mips64le.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cpu - -const CacheLineSize = 32 diff --git a/vendor/github.com/templexxx/cpu/cpu_mipsle.go b/vendor/github.com/templexxx/cpu/cpu_mipsle.go deleted file mode 100644 index 078a6c3b..00000000 --- a/vendor/github.com/templexxx/cpu/cpu_mipsle.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cpu - -const CacheLineSize = 32 diff --git a/vendor/github.com/templexxx/cpu/cpu_no_init.go b/vendor/github.com/templexxx/cpu/cpu_no_init.go deleted file mode 100644 index 1be4f29d..00000000 --- a/vendor/github.com/templexxx/cpu/cpu_no_init.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !386 -// +build !amd64 -// +build !amd64p32 -// +build !arm64 -// +build !ppc64 -// +build !ppc64le -// +build !s390x - -package cpu - -func doinit() { -} diff --git a/vendor/github.com/templexxx/cpu/cpu_ppc64x.go b/vendor/github.com/templexxx/cpu/cpu_ppc64x.go deleted file mode 100644 index 995cf020..00000000 --- a/vendor/github.com/templexxx/cpu/cpu_ppc64x.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build ppc64 ppc64le - -package cpu - -const CacheLineSize = 128 - -// ppc64x doesn't have a 'cpuid' equivalent, so we rely on HWCAP/HWCAP2. -// These are linknamed in runtime/os_linux_ppc64x.go and are initialized by -// archauxv(). -var hwcap uint -var hwcap2 uint - -// HWCAP/HWCAP2 bits. These are exposed by the kernel. -const ( - // ISA Level - _PPC_FEATURE2_ARCH_2_07 = 0x80000000 - _PPC_FEATURE2_ARCH_3_00 = 0x00800000 - - // CPU features - _PPC_FEATURE_HAS_ALTIVEC = 0x10000000 - _PPC_FEATURE_HAS_DFP = 0x00000400 - _PPC_FEATURE_HAS_VSX = 0x00000080 - _PPC_FEATURE2_HAS_HTM = 0x40000000 - _PPC_FEATURE2_HAS_ISEL = 0x08000000 - _PPC_FEATURE2_HAS_VEC_CRYPTO = 0x02000000 - _PPC_FEATURE2_HTM_NOSC = 0x01000000 - _PPC_FEATURE2_DARN = 0x00200000 - _PPC_FEATURE2_SCV = 0x00100000 -) - -func doinit() { - options = []option{ - {"htm", &PPC64.HasHTM}, - {"htmnosc", &PPC64.HasHTMNOSC}, - {"darn", &PPC64.HasDARN}, - {"scv", &PPC64.HasSCV}, - - // These capabilities should always be enabled on ppc64 and ppc64le: - // {"vmx", &PPC64.HasVMX}, - // {"dfp", &PPC64.HasDFP}, - // {"vsx", &PPC64.HasVSX}, - // {"isel", &PPC64.HasISEL}, - // {"vcrypto", &PPC64.HasVCRYPTO}, - } - - // HWCAP feature bits - PPC64.HasVMX = isSet(hwcap, _PPC_FEATURE_HAS_ALTIVEC) - PPC64.HasDFP = isSet(hwcap, _PPC_FEATURE_HAS_DFP) - PPC64.HasVSX = isSet(hwcap, _PPC_FEATURE_HAS_VSX) - - // HWCAP2 feature bits - PPC64.IsPOWER8 = isSet(hwcap2, _PPC_FEATURE2_ARCH_2_07) - PPC64.HasHTM = isSet(hwcap2, _PPC_FEATURE2_HAS_HTM) - PPC64.HasISEL = isSet(hwcap2, _PPC_FEATURE2_HAS_ISEL) - PPC64.HasVCRYPTO = isSet(hwcap2, _PPC_FEATURE2_HAS_VEC_CRYPTO) - PPC64.HasHTMNOSC = isSet(hwcap2, _PPC_FEATURE2_HTM_NOSC) - PPC64.IsPOWER9 = isSet(hwcap2, _PPC_FEATURE2_ARCH_3_00) - PPC64.HasDARN = isSet(hwcap2, _PPC_FEATURE2_DARN) - PPC64.HasSCV = isSet(hwcap2, _PPC_FEATURE2_SCV) -} - -func isSet(hwc uint, value uint) bool { - return hwc&value != 0 -} diff --git a/vendor/github.com/templexxx/cpu/cpu_riscv64.go b/vendor/github.com/templexxx/cpu/cpu_riscv64.go deleted file mode 100644 index 0b86a4fd..00000000 --- a/vendor/github.com/templexxx/cpu/cpu_riscv64.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cpu - -const CacheLineSize = 32 - -func doinit() { -} diff --git a/vendor/github.com/templexxx/cpu/cpu_s390x.go b/vendor/github.com/templexxx/cpu/cpu_s390x.go deleted file mode 100644 index 389a058c..00000000 --- a/vendor/github.com/templexxx/cpu/cpu_s390x.go +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cpu - -const CacheLineSize = 256 - -// bitIsSet reports whether the bit at index is set. The bit index -// is in big endian order, so bit index 0 is the leftmost bit. -func bitIsSet(bits []uint64, index uint) bool { - return bits[index/64]&((1<<63)>>(index%64)) != 0 -} - -// function is the function code for the named function. -type function uint8 - -const ( - // KM{,A,C,CTR} function codes - aes128 function = 18 // AES-128 - aes192 = 19 // AES-192 - aes256 = 20 // AES-256 - - // K{I,L}MD function codes - sha1 = 1 // SHA-1 - sha256 = 2 // SHA-256 - sha512 = 3 // SHA-512 - - // KLMD function codes - ghash = 65 // GHASH -) - -// queryResult contains the result of a Query function -// call. Bits are numbered in big endian order so the -// leftmost bit (the MSB) is at index 0. -type queryResult struct { - bits [2]uint64 -} - -// Has reports whether the given functions are present. -func (q *queryResult) Has(fns ...function) bool { - if len(fns) == 0 { - panic("no function codes provided") - } - for _, f := range fns { - if !bitIsSet(q.bits[:], uint(f)) { - return false - } - } - return true -} - -// facility is a bit index for the named facility. -type facility uint8 - -const ( - // mandatory facilities - zarch facility = 1 // z architecture mode is active - stflef = 7 // store-facility-list-extended - ldisp = 18 // long-displacement - eimm = 21 // extended-immediate - - // miscellaneous facilities - dfp = 42 // decimal-floating-point - etf3eh = 30 // extended-translation 3 enhancement - - // cryptography facilities - msa = 17 // message-security-assist - msa3 = 76 // message-security-assist extension 3 - msa4 = 77 // message-security-assist extension 4 - msa5 = 57 // message-security-assist extension 5 - msa8 = 146 // message-security-assist extension 8 - - // Note: vx and highgprs are excluded because they require - // kernel support and so must be fetched from HWCAP. -) - -// facilityList contains the result of an STFLE call. -// Bits are numbered in big endian order so the -// leftmost bit (the MSB) is at index 0. -type facilityList struct { - bits [4]uint64 -} - -// Has reports whether the given facilities are present. -func (s *facilityList) Has(fs ...facility) bool { - if len(fs) == 0 { - panic("no facility bits provided") - } - for _, f := range fs { - if !bitIsSet(s.bits[:], uint(f)) { - return false - } - } - return true -} - -// The following feature detection functions are defined in cpu_s390x.s. -// They are likely to be expensive to call so the results should be cached. -func stfle() facilityList -func kmQuery() queryResult -func kmcQuery() queryResult -func kmctrQuery() queryResult -func kmaQuery() queryResult -func kimdQuery() queryResult -func klmdQuery() queryResult - -func doinit() { - options = []option{ - {"zarch", &S390X.HasZArch}, - {"stfle", &S390X.HasSTFLE}, - {"ldisp", &S390X.HasLDisp}, - {"msa", &S390X.HasMSA}, - {"eimm", &S390X.HasEImm}, - {"dfp", &S390X.HasDFP}, - {"etf3eh", &S390X.HasETF3Enhanced}, - {"vx", &S390X.HasVX}, - } - - aes := []function{aes128, aes192, aes256} - facilities := stfle() - - S390X.HasZArch = facilities.Has(zarch) - S390X.HasSTFLE = facilities.Has(stflef) - S390X.HasLDisp = facilities.Has(ldisp) - S390X.HasEImm = facilities.Has(eimm) - S390X.HasDFP = facilities.Has(dfp) - S390X.HasETF3Enhanced = facilities.Has(etf3eh) - S390X.HasMSA = facilities.Has(msa) - - if S390X.HasMSA { - // cipher message - km, kmc := kmQuery(), kmcQuery() - S390X.HasAES = km.Has(aes...) - S390X.HasAESCBC = kmc.Has(aes...) - if facilities.Has(msa4) { - kmctr := kmctrQuery() - S390X.HasAESCTR = kmctr.Has(aes...) - } - if facilities.Has(msa8) { - kma := kmaQuery() - S390X.HasAESGCM = kma.Has(aes...) - } - - // compute message digest - kimd := kimdQuery() // intermediate (no padding) - klmd := klmdQuery() // last (padding) - S390X.HasSHA1 = kimd.Has(sha1) && klmd.Has(sha1) - S390X.HasSHA256 = kimd.Has(sha256) && klmd.Has(sha256) - S390X.HasSHA512 = kimd.Has(sha512) && klmd.Has(sha512) - S390X.HasGHASH = kimd.Has(ghash) // KLMD-GHASH does not exist - } -} diff --git a/vendor/github.com/templexxx/cpu/cpu_s390x.s b/vendor/github.com/templexxx/cpu/cpu_s390x.s deleted file mode 100644 index 9678035f..00000000 --- a/vendor/github.com/templexxx/cpu/cpu_s390x.s +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -#include "textflag.h" - -// func stfle() facilityList -TEXT ·stfle(SB), NOSPLIT|NOFRAME, $0-32 - MOVD $ret+0(FP), R1 - MOVD $3, R0 // last doubleword index to store - XC $32, (R1), (R1) // clear 4 doublewords (32 bytes) - WORD $0xb2b01000 // store facility list extended (STFLE) - RET - -// func kmQuery() queryResult -TEXT ·kmQuery(SB), NOSPLIT|NOFRAME, $0-16 - MOVD $0, R0 // set function code to 0 (KM-Query) - MOVD $ret+0(FP), R1 // address of 16-byte return value - WORD $0xB92E0024 // cipher message (KM) - RET - -// func kmcQuery() queryResult -TEXT ·kmcQuery(SB), NOSPLIT|NOFRAME, $0-16 - MOVD $0, R0 // set function code to 0 (KMC-Query) - MOVD $ret+0(FP), R1 // address of 16-byte return value - WORD $0xB92F0024 // cipher message with chaining (KMC) - RET - -// func kmctrQuery() queryResult -TEXT ·kmctrQuery(SB), NOSPLIT|NOFRAME, $0-16 - MOVD $0, R0 // set function code to 0 (KMCTR-Query) - MOVD $ret+0(FP), R1 // address of 16-byte return value - WORD $0xB92D4024 // cipher message with counter (KMCTR) - RET - -// func kmaQuery() queryResult -TEXT ·kmaQuery(SB), NOSPLIT|NOFRAME, $0-16 - MOVD $0, R0 // set function code to 0 (KMA-Query) - MOVD $ret+0(FP), R1 // address of 16-byte return value - WORD $0xb9296024 // cipher message with authentication (KMA) - RET - -// func kimdQuery() queryResult -TEXT ·kimdQuery(SB), NOSPLIT|NOFRAME, $0-16 - MOVD $0, R0 // set function code to 0 (KIMD-Query) - MOVD $ret+0(FP), R1 // address of 16-byte return value - WORD $0xB93E0024 // compute intermediate message digest (KIMD) - RET - -// func klmdQuery() queryResult -TEXT ·klmdQuery(SB), NOSPLIT|NOFRAME, $0-16 - MOVD $0, R0 // set function code to 0 (KLMD-Query) - MOVD $ret+0(FP), R1 // address of 16-byte return value - WORD $0xB93F0024 // compute last message digest (KLMD) - RET diff --git a/vendor/github.com/templexxx/cpu/cpu_wasm.go b/vendor/github.com/templexxx/cpu/cpu_wasm.go deleted file mode 100644 index 1107a7ad..00000000 --- a/vendor/github.com/templexxx/cpu/cpu_wasm.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cpu - -const CacheLineSize = 64 diff --git a/vendor/github.com/templexxx/cpu/cpu_x86.go b/vendor/github.com/templexxx/cpu/cpu_x86.go deleted file mode 100644 index 7297fe2d..00000000 --- a/vendor/github.com/templexxx/cpu/cpu_x86.go +++ /dev/null @@ -1,433 +0,0 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build 386 amd64 amd64p32 - -package cpu - -import ( - "fmt" - "strings" -) - -const CacheLineSize = 64 - -// cpuid is implemented in cpu_x86.s. -func cpuid(eaxArg, ecxArg uint32) (eax, ebx, ecx, edx uint32) - -// xgetbv with ecx = 0 is implemented in cpu_x86.s. -func xgetbv() (eax, edx uint32) - -const ( - // edx bits - cpuid_SSE2 = 1 << 26 - - // ecx bits - cpuid_SSE3 = 1 << 0 - cpuid_PCLMULQDQ = 1 << 1 - cpuid_SSSE3 = 1 << 9 - cpuid_FMA = 1 << 12 - cpuid_SSE41 = 1 << 19 - cpuid_SSE42 = 1 << 20 - cpuid_POPCNT = 1 << 23 - cpuid_AES = 1 << 25 - cpuid_OSXSAVE = 1 << 27 - cpuid_AVX = 1 << 28 - cpuid_CMPXCHG16B = 1 << 13 - - // ebx bits - cpuid_BMI1 = 1 << 3 - cpuid_AVX2 = 1 << 5 - cpuid_BMI2 = 1 << 8 - cpuid_ERMS = 1 << 9 - cpuid_ADX = 1 << 19 - cpuid_AVX512F = 1 << 16 - cpuid_AVX512DQ = 1 << 17 - cpuid_AVX512BW = 1 << 30 - cpuid_AVX512VL = 1 << 31 - - // edx bits - cpuid_Invariant_TSC = 1 << 8 -) - -func doinit() { - options = []option{ - {"adx", &X86.HasADX}, - {"aes", &X86.HasAES}, - {"avx", &X86.HasAVX}, - {"avx2", &X86.HasAVX2}, - {"bmi1", &X86.HasBMI1}, - {"bmi2", &X86.HasBMI2}, - {"erms", &X86.HasERMS}, - {"fma", &X86.HasFMA}, - {"pclmulqdq", &X86.HasPCLMULQDQ}, - {"popcnt", &X86.HasPOPCNT}, - {"sse3", &X86.HasSSE3}, - {"sse41", &X86.HasSSE41}, - {"sse42", &X86.HasSSE42}, - {"ssse3", &X86.HasSSSE3}, - {"avx512f", &X86.HasAVX512F}, - {"avx512dq", &X86.HasAVX512DQ}, - {"avx512bw", &X86.HasAVX512BW}, - {"avx512vl", &X86.HasAVX512VL}, - {"invariant_tsc", &X86.HasInvariantTSC}, - - // sse2 set as last element so it can easily be removed again. See code below. - {"sse2", &X86.HasSSE2}, - } - - // Remove sse2 from options on amd64(p32) because SSE2 is a mandatory feature for these GOARCHs. - if GOARCH == "amd64" || GOARCH == "amd64p32" { - options = options[:len(options)-1] - } - - maxID, _, _, _ := cpuid(0, 0) - - if maxID < 1 { - return - } - - _, _, ecx1, edx1 := cpuid(1, 0) - X86.HasSSE2 = isSet(edx1, cpuid_SSE2) - - X86.HasSSE3 = isSet(ecx1, cpuid_SSE3) - X86.HasPCLMULQDQ = isSet(ecx1, cpuid_PCLMULQDQ) - X86.HasSSSE3 = isSet(ecx1, cpuid_SSSE3) - X86.HasFMA = isSet(ecx1, cpuid_FMA) - X86.HasSSE41 = isSet(ecx1, cpuid_SSE41) - X86.HasSSE42 = isSet(ecx1, cpuid_SSE42) - X86.HasPOPCNT = isSet(ecx1, cpuid_POPCNT) - X86.HasAES = isSet(ecx1, cpuid_AES) - X86.HasCMPXCHG16B = isSet(ecx1, cpuid_CMPXCHG16B) - X86.HasOSXSAVE = isSet(ecx1, cpuid_OSXSAVE) - - osSupportsAVX := false - osSupportsAVX512 := false - // For XGETBV, OSXSAVE bit is required and sufficient. - if X86.HasOSXSAVE { - eax, _ := xgetbv() - // Check if XMM and YMM registers have OS support. - osSupportsAVX = isSet(eax, 1<<1) && isSet(eax, 1<<2) - // Check is ZMM registers have OS support. - osSupportsAVX512 = isSet(eax>>5, 7) && isSet(eax>>1, 3) - } - - X86.HasAVX = isSet(ecx1, cpuid_AVX) && osSupportsAVX - - if maxID < 7 { - return - } - - _, ebx7, _, _ := cpuid(7, 0) - X86.HasBMI1 = isSet(ebx7, cpuid_BMI1) - X86.HasAVX2 = isSet(ebx7, cpuid_AVX2) && osSupportsAVX - X86.HasAVX512F = isSet(ebx7, cpuid_AVX512F) && osSupportsAVX512 - X86.HasAVX512DQ = isSet(ebx7, cpuid_AVX512DQ) && osSupportsAVX512 - X86.HasAVX512BW = isSet(ebx7, cpuid_AVX512BW) && osSupportsAVX512 - X86.HasAVX512VL = isSet(ebx7, cpuid_AVX512VL) && osSupportsAVX512 - X86.HasBMI2 = isSet(ebx7, cpuid_BMI2) - X86.HasERMS = isSet(ebx7, cpuid_ERMS) - X86.HasADX = isSet(ebx7, cpuid_ADX) - - X86.Cache = getCacheSize() - - X86.HasInvariantTSC = hasInvariantTSC() - - X86.Family, X86.Model, X86.SteppingID = getVersionInfo() - - X86.Signature = makeSignature(X86.Family, X86.Model) - - X86.Name = getName() - - X86.TSCFrequency = getNativeTSCFrequency(X86.Name, X86.Signature, X86.SteppingID) -} - -func isSet(hwc uint32, value uint32) bool { - return hwc&value != 0 -} - -func hasInvariantTSC() bool { - if maxExtendedFunction() < 0x80000007 { - return false - } - _, _, _, edx := cpuid(0x80000007, 0) - return isSet(edx, cpuid_Invariant_TSC) -} - -func getName() string { - if maxExtendedFunction() >= 0x80000004 { - v := make([]uint32, 0, 48) - for i := uint32(0); i < 3; i++ { - a, b, c, d := cpuid(0x80000002+i, 0) - v = append(v, a, b, c, d) - } - return strings.Trim(string(valAsString(v...)), " ") - } - return "unknown" -} - -// getNativeTSCFrequency gets TSC frequency from CPUID, -// only supports Intel (Skylake or later microarchitecture) & key information is from Intel manual & kernel codes -// (especially this commit: https://github.com/torvalds/linux/commit/604dc9170f2435d27da5039a3efd757dceadc684). -func getNativeTSCFrequency(name, sign string, steppingID uint32) uint64 { - - if vendorID() != Intel { - return 0 - } - - if maxFunctionID() < 0x15 { - return 0 - } - - // ApolloLake, GeminiLake, CannonLake (and presumably all new chipsets - // from this point) report the crystal frequency directly via CPUID.0x15. - // That's definitive data that we can rely upon. - eax, ebx, ecx, _ := cpuid(0x15, 0) - - // If ebx is 0, the TSC/”core crystal clock” ratio is not enumerated. - // We won't provide TSC frequency detection in this situation. - if eax == 0 || ebx == 0 { - return 0 - } - - // Skylake, Kabylake and all variants of those two chipsets report a - // crystal frequency of zero. - if ecx == 0 { // Crystal clock frequency is not enumerated. - ecx = getCrystalClockFrequency(sign, steppingID) - } - - // TSC frequency = “core crystal clock frequency” * EBX/EAX. - return uint64(ecx) * (uint64(ebx) / uint64(eax)) -} - -// Copied from: CPUID Signature values of DisplayFamily and DisplayModel, -// in Intel® 64 and IA-32 Architectures Software Developer’s Manual -// Volume 4: Model-Specific Registers -// & https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/intel-family.h -const ( - IntelFam6SkylakeL = "06_4EH" - IntelFam6Skylake = "06_5EH" - IntelFam6XeonScalable = "06_55H" - IntelFam6KabylakeL = "06_8EH" - IntelFam6Kabylake = "06_9EH" -) - -// getCrystalClockFrequency gets crystal clock frequency -// for Intel processors in which CPUID.15H.EBX[31:0] ÷ CPUID.0x15.EAX[31:0] is enumerated -// but CPUID.15H.ECX is not enumerated using this function to get nominal core crystal clock frequency. -// -// Actually these crystal clock frequencies provided by Intel hardcoded tables are not so accurate in some cases, -// e.g. SkyLake server CPU may have issue (All SKX subject the crystal to an EMI reduction circuit that -//reduces its actual frequency by (approximately) -0.25%): -// see https://lore.kernel.org/lkml/ff6dcea166e8ff8f2f6a03c17beab2cb436aa779.1513920414.git.len.brown@intel.com/ -// for more details. -// With this report, I set a coefficient (0.9975) for IntelFam6SkyLakeX. -// -// Unlike the kernel way (mentioned in https://github.com/torvalds/linux/commit/604dc9170f2435d27da5039a3efd757dceadc684), -// I prefer the Intel hardcoded tables, (in -// 18.7.3 Determining the Processor Base Frequency, Table 18-85. Nominal Core Crystal Clock Frequency) -// because after some testing (comparing with wall clock, see https://github.com/templexxx/tsc/tsc_test.go for more details), -// I found hardcoded tables are more accurate. -func getCrystalClockFrequency(sign string, steppingID uint32) uint32 { - - if maxFunctionID() < 0x16 { - return 0 - } - - switch sign { - case IntelFam6SkylakeL: - return 24 * 1000 * 1000 - case IntelFam6Skylake: - return 24 * 1000 * 1000 - case IntelFam6XeonScalable: - // SKL-SP. - // see: https://community.intel.com/t5/Software-Tuning-Performance/How-to-detect-microarchitecture-on-Xeon-Scalable/m-p/1205162#M7633. - if steppingID == 0x2 || steppingID == 0x3 || steppingID == 0x4 { - return 25 * 1000 * 1000 * 0.9975 - } - return 25 * 1000 * 1000 // TODO check other Xeon Scalable has no slow down issue. - case IntelFam6KabylakeL: - return 24 * 1000 * 1000 - case IntelFam6Kabylake: - return 24 * 1000 * 1000 - } - - return 0 -} - -func getVersionInfo() (uint32, uint32, uint32) { - if maxFunctionID() < 0x1 { - return 0, 0, 0 - } - eax, _, _, _ := cpuid(1, 0) - family := (eax >> 8) & 0xf - displayFamily := family - if family == 0xf { - displayFamily = ((eax >> 20) & 0xff) + family - } - model := (eax >> 4) & 0xf - displayModel := model - if family == 0x6 || family == 0xf { - displayModel = ((eax >> 12) & 0xf0) + model - } - return displayFamily, displayModel, eax & 0x7 -} - -// signature format: XX_XXH -func makeSignature(family, model uint32) string { - signature := strings.ToUpper(fmt.Sprintf("0%x_0%xH", family, model)) - ss := strings.Split(signature, "_") - for i, s := range ss { - // Maybe insert too more `0`, drop it. - if len(s) > 2 { - s = s[1:] - ss[i] = s - } - } - return strings.Join(ss, "_") -} - -// getCacheSize is from -// https://github.com/klauspost/cpuid/blob/5a626f7029c910cc8329dae5405ee4f65034bce5/cpuid.go#L723 -func getCacheSize() Cache { - c := Cache{ - L1I: -1, - L1D: -1, - L2: -1, - L3: -1, - } - - vendor := vendorID() - switch vendor { - case Intel: - if maxFunctionID() < 4 { - return c - } - for i := uint32(0); ; i++ { - eax, ebx, ecx, _ := cpuid(4, i) - cacheType := eax & 15 - if cacheType == 0 { - break - } - cacheLevel := (eax >> 5) & 7 - coherency := int(ebx&0xfff) + 1 - partitions := int((ebx>>12)&0x3ff) + 1 - associativity := int((ebx>>22)&0x3ff) + 1 - sets := int(ecx) + 1 - size := associativity * partitions * coherency * sets - switch cacheLevel { - case 1: - if cacheType == 1 { - // 1 = Data Cache - c.L1D = size - } else if cacheType == 2 { - // 2 = Instruction Cache - c.L1I = size - } else { - if c.L1D < 0 { - c.L1I = size - } - if c.L1I < 0 { - c.L1I = size - } - } - case 2: - c.L2 = size - case 3: - c.L3 = size - } - } - case AMD, Hygon: - // Untested. - if maxExtendedFunction() < 0x80000005 { - return c - } - _, _, ecx, edx := cpuid(0x80000005, 0) - c.L1D = int(((ecx >> 24) & 0xFF) * 1024) - c.L1I = int(((edx >> 24) & 0xFF) * 1024) - - if maxExtendedFunction() < 0x80000006 { - return c - } - _, _, ecx, _ = cpuid(0x80000006, 0) - c.L2 = int(((ecx >> 16) & 0xFFFF) * 1024) - } - - return c -} - -func maxFunctionID() uint32 { - a, _, _, _ := cpuid(0, 0) - return a -} - -func maxExtendedFunction() uint32 { - eax, _, _, _ := cpuid(0x80000000, 0) - return eax -} - -const ( - Other = iota - Intel - AMD - VIA - Transmeta - NSC - KVM // Kernel-based Virtual Machine - MSVM // Microsoft Hyper-V or Windows Virtual PC - VMware - XenHVM - Bhyve - Hygon -) - -// Except from http://en.wikipedia.org/wiki/CPUID#EAX.3D0:_Get_vendor_ID -var vendorMapping = map[string]int{ - "AMDisbetter!": AMD, - "AuthenticAMD": AMD, - "CentaurHauls": VIA, - "GenuineIntel": Intel, - "TransmetaCPU": Transmeta, - "GenuineTMx86": Transmeta, - "Geode by NSC": NSC, - "VIA VIA VIA ": VIA, - "KVMKVMKVMKVM": KVM, - "Microsoft Hv": MSVM, - "VMwareVMware": VMware, - "XenVMMXenVMM": XenHVM, - "bhyve bhyve ": Bhyve, - "HygonGenuine": Hygon, -} - -func vendorID() int { - _, b, c, d := cpuid(0, 0) - v := valAsString(b, d, c) - vend, ok := vendorMapping[string(v)] - if !ok { - return Other - } - return vend -} - -func valAsString(values ...uint32) []byte { - r := make([]byte, 4*len(values)) - for i, v := range values { - dst := r[i*4:] - dst[0] = byte(v & 0xff) - dst[1] = byte((v >> 8) & 0xff) - dst[2] = byte((v >> 16) & 0xff) - dst[3] = byte((v >> 24) & 0xff) - switch { - case dst[0] == 0: - return r[:i*4] - case dst[1] == 0: - return r[:i*4+1] - case dst[2] == 0: - return r[:i*4+2] - case dst[3] == 0: - return r[:i*4+3] - } - } - return r -} diff --git a/vendor/github.com/templexxx/cpu/cpu_x86.s b/vendor/github.com/templexxx/cpu/cpu_x86.s deleted file mode 100644 index 228fbcf6..00000000 --- a/vendor/github.com/templexxx/cpu/cpu_x86.s +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build 386 amd64 amd64p32 - -#include "textflag.h" - -// func cpuid(eaxArg, ecxArg uint32) (eax, ebx, ecx, edx uint32) -TEXT ·cpuid(SB), NOSPLIT, $0-24 - MOVL eaxArg+0(FP), AX - MOVL ecxArg+4(FP), CX - CPUID - MOVL AX, eax+8(FP) - MOVL BX, ebx+12(FP) - MOVL CX, ecx+16(FP) - MOVL DX, edx+20(FP) - RET - -// func xgetbv() (eax, edx uint32) -TEXT ·xgetbv(SB),NOSPLIT,$0-8 -#ifdef GOOS_nacl - // nacl does not support XGETBV. - MOVL $0, eax+0(FP) - MOVL $0, edx+4(FP) -#else - MOVL $0, CX - XGETBV - MOVL AX, eax+0(FP) - MOVL DX, edx+4(FP) -#endif - RET diff --git a/vendor/github.com/templexxx/xorsimd/.gitattributes b/vendor/github.com/templexxx/xorsimd/.gitattributes deleted file mode 100644 index 68f7d043..00000000 --- a/vendor/github.com/templexxx/xorsimd/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.s linguist-language=go:x diff --git a/vendor/github.com/templexxx/xorsimd/.github/workflows/unit-test.yml b/vendor/github.com/templexxx/xorsimd/.github/workflows/unit-test.yml deleted file mode 100644 index 1f8d8850..00000000 --- a/vendor/github.com/templexxx/xorsimd/.github/workflows/unit-test.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: unit-test - -on: - push: - branches: - - master - - release/* - pull_request: - branches: - - master - -jobs: - - test: - name: Test - runs-on: ubuntu-latest - steps: - - - name: Set up Go 1.13 - uses: actions/setup-go@v1 - with: - go-version: 1.13 - id: go - - - name: Check out code into the Go module directory - uses: actions/checkout@v1 - - - name: Get dependencies - run: | - go get -v -t -d ./... - if [ -f Gopkg.toml ]; then - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - dep ensure - fi - - name: Run test - run: CGO_ENABLED=1 GO111MODULE=on go test -v -race diff --git a/vendor/github.com/templexxx/xorsimd/.gitignore b/vendor/github.com/templexxx/xorsimd/.gitignore deleted file mode 100644 index 43309f8b..00000000 --- a/vendor/github.com/templexxx/xorsimd/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, build with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out -.idea diff --git a/vendor/github.com/templexxx/xorsimd/LICENSE b/vendor/github.com/templexxx/xorsimd/LICENSE deleted file mode 100644 index 08ee7141..00000000 --- a/vendor/github.com/templexxx/xorsimd/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 Temple3x (temple3x@gmail.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/templexxx/xorsimd/README.md b/vendor/github.com/templexxx/xorsimd/README.md deleted file mode 100644 index 9dce5c9c..00000000 --- a/vendor/github.com/templexxx/xorsimd/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# XOR SIMD - -[![GoDoc][1]][2] [![MIT licensed][3]][4] [![Build Status][5]][6] [![Go Report Card][7]][8] [![Sourcegraph][9]][10] - -[1]: https://godoc.org/github.com/templexxx/xorsimd?status.svg -[2]: https://godoc.org/github.com/templexxx/xorsimd -[3]: https://img.shields.io/badge/license-MIT-blue.svg -[4]: LICENSE -[5]: https://github.com/templexxx/xorsimd/workflows/unit-test/badge.svg -[6]: https://github.com/templexxx/xorsimd -[7]: https://goreportcard.com/badge/github.com/templexxx/xorsimd -[8]: https://goreportcard.com/report/github.com/templexxx/xorsimd -[9]: https://sourcegraph.com/github.com/templexxx/xorsimd/-/badge.svg -[10]: https://sourcegraph.com/github.com/templexxx/xorsimd?badge - -## Introduction: - ->- XOR code engine in pure Go. -> ->- [High Performance](https://github.com/templexxx/xorsimd#performance): -More than 270GB/s per physics core. - -## Performance - -Performance depends mainly on: - ->- CPU instruction extension. -> ->- Number of source row vectors. - -**Platform:** - -*AWS c5d.xlarge (Intel(R) Xeon(R) Platinum 8124M CPU @ 3.00GHz)* - -**All test run on a single Core.** - -`I/O = (src_num + 1) * vector_size / cost` - -| Src Num | Vector size | AVX512 I/O (MB/S) | AVX2 I/O (MB/S) |SSE2 I/O (MB/S) | -|-------|-------------|-------------|---------------|---------------| -|5|4KB| 270403.73 | 142825.25 | 74443.91 | -|5|1MB| 26948.34 | 26887.37 | 26950.65 | -|5|8MB| 17881.32 | 17212.56 | 16402.97 | -|10|4KB| 190445.30 | 102953.59 | 53244.04 | -|10|1MB| 26424.44 | 26618.65 | 26094.39 | -|10|8MB| 15471.31 | 14866.72 | 13565.80 | diff --git a/vendor/github.com/templexxx/xorsimd/go.mod b/vendor/github.com/templexxx/xorsimd/go.mod deleted file mode 100644 index ac5f57fc..00000000 --- a/vendor/github.com/templexxx/xorsimd/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/templexxx/xorsimd - -require github.com/templexxx/cpu v0.0.1 - -go 1.13 diff --git a/vendor/github.com/templexxx/xorsimd/go.sum b/vendor/github.com/templexxx/xorsimd/go.sum deleted file mode 100644 index 04d04de8..00000000 --- a/vendor/github.com/templexxx/xorsimd/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/templexxx/cpu v0.0.1 h1:hY4WdLOgKdc8y13EYklu9OUTXik80BkxHoWvTO6MQQY= -github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= diff --git a/vendor/github.com/templexxx/xorsimd/xor.go b/vendor/github.com/templexxx/xorsimd/xor.go deleted file mode 100644 index ae88911d..00000000 --- a/vendor/github.com/templexxx/xorsimd/xor.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) 2019. Temple3x (temple3x@gmail.com) -// -// Use of this source code is governed by the MIT License -// that can be found in the LICENSE file. - -package xorsimd - -import "github.com/templexxx/cpu" - -// EnableAVX512 may slow down CPU Clock (maybe not). -// TODO need more research: -// https://lemire.me/blog/2018/04/19/by-how-much-does-avx-512-slow-down-your-cpu-a-first-experiment/ -var EnableAVX512 = true - -// cpuFeature indicates which instruction set will be used. -var cpuFeature = getCPUFeature() - -const ( - avx512 = iota - avx2 - sse2 - generic -) - -// TODO: Add ARM feature... -func getCPUFeature() int { - if hasAVX512() && EnableAVX512 { - return avx512 - } else if cpu.X86.HasAVX2 { - return avx2 - } else { - return sse2 // amd64 must has sse2 - } -} - -func hasAVX512() (ok bool) { - - return cpu.X86.HasAVX512VL && - cpu.X86.HasAVX512BW && - cpu.X86.HasAVX512F && - cpu.X86.HasAVX512DQ -} - -// Encode encodes elements from source slice into a -// destination slice. The source and destination may overlap. -// Encode returns the number of bytes encoded, which will be the minimum of -// len(src[i]) and len(dst). -func Encode(dst []byte, src [][]byte) (n int) { - n = checkLen(dst, src) - if n == 0 { - return - } - - dst = dst[:n] - for i := range src { - src[i] = src[i][:n] - } - - if len(src) == 1 { - copy(dst, src[0]) - return - } - - encode(dst, src) - return -} - -func checkLen(dst []byte, src [][]byte) int { - n := len(dst) - for i := range src { - if len(src[i]) < n { - n = len(src[i]) - } - } - - if n <= 0 { - return 0 - } - return n -} - -// Bytes XORs the bytes in a and b into a -// destination slice. The source and destination may overlap. -// -// Bytes returns the number of bytes encoded, which will be the minimum of -// len(dst), len(a), len(b). -func Bytes(dst, a, b []byte) int { - return Encode(dst, [][]byte{a, b}) -} diff --git a/vendor/github.com/templexxx/xorsimd/xor_amd64.go b/vendor/github.com/templexxx/xorsimd/xor_amd64.go deleted file mode 100644 index 5d46df35..00000000 --- a/vendor/github.com/templexxx/xorsimd/xor_amd64.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) 2019. Temple3x (temple3x@gmail.com) -// -// Use of this source code is governed by the MIT License -// that can be found in the LICENSE file. - -package xorsimd - -func encode(dst []byte, src [][]byte) { - - switch cpuFeature { - case avx512: - encodeAVX512(dst, src) - case avx2: - encodeAVX2(dst, src) - default: - encodeSSE2(dst, src) - } - return -} - -// Bytes8 XORs of 8 Bytes. -// The slice arguments a, b, dst's lengths are assumed to be at least 8, -// if not, Bytes8 will panic. -func Bytes8(dst, a, b []byte) { - - bytes8(&dst[0], &a[0], &b[0]) -} - -// Bytes16 XORs of packed 16 Bytes. -// The slice arguments a, b, dst's lengths are assumed to be at least 16, -// if not, Bytes16 will panic. -func Bytes16(dst, a, b []byte) { - - bytes16(&dst[0], &a[0], &b[0]) -} - -// Bytes8Align XORs of 8 Bytes. -// The slice arguments a, b, dst's lengths are assumed to be at least 8, -// if not, Bytes8 will panic. -func Bytes8Align(dst, a, b []byte) { - - bytes8(&dst[0], &a[0], &b[0]) -} - -// Bytes16Align XORs of packed 16 Bytes. -// The slice arguments a, b, dst's lengths are assumed to be at least 16, -// if not, Bytes16 will panic. -func Bytes16Align(dst, a, b []byte) { - - bytes16(&dst[0], &a[0], &b[0]) -} - -// BytesA XORs the len(a) bytes in a and b into a -// destination slice. -// The destination should have enough space. -// -// It's used for encoding small bytes slices (< dozens bytes), -// and the slices may not be aligned to 8 bytes or 16 bytes. -// If the length is big, it's better to use 'func Bytes(dst, a, b []byte)' instead -// for gain better performance. -func BytesA(dst, a, b []byte) { - - bytesN(&dst[0], &a[0], &b[0], len(a)) -} - -// BytesB XORs the len(b) bytes in a and b into a -// destination slice. -// The destination should have enough space. -// -// It's used for encoding small bytes slices (< dozens bytes), -// and the slices may not be aligned to 8 bytes or 16 bytes. -// If the length is big, it's better to use 'func Bytes(dst, a, b []byte)' instead -// for gain better performance. -func BytesB(dst, a, b []byte) { - - bytesN(&dst[0], &a[0], &b[0], len(b)) -} - -//go:noescape -func encodeAVX512(dst []byte, src [][]byte) - -//go:noescape -func encodeAVX2(dst []byte, src [][]byte) - -//go:noescape -func encodeSSE2(dst []byte, src [][]byte) - -//go:noescape -func bytesN(dst, a, b *byte, n int) - -//go:noescape -func bytes8(dst, a, b *byte) - -//go:noescape -func bytes16(dst, a, b *byte) diff --git a/vendor/github.com/templexxx/xorsimd/xor_generic.go b/vendor/github.com/templexxx/xorsimd/xor_generic.go deleted file mode 100644 index b12908f8..00000000 --- a/vendor/github.com/templexxx/xorsimd/xor_generic.go +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (c) 2019. Temple3x (temple3x@gmail.com) -// -// Use of this source code is governed by the MIT License -// that can be found in the LICENSE file. -// -// Copyright 2013 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !amd64 - -package xorsimd - -import ( - "runtime" - "unsafe" -) - -const wordSize = int(unsafe.Sizeof(uintptr(0))) -const supportsUnaligned = runtime.GOARCH == "386" || runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "s390x" - -func encode(dst []byte, src [][]byte) { - if supportsUnaligned { - fastEncode(dst, src, len(dst)) - } else { - // TODO(hanwen): if (dst, a, b) have common alignment - // we could still try fastEncode. It is not clear - // how often this happens, and it's only worth it if - // the block encryption itself is hardware - // accelerated. - safeEncode(dst, src, len(dst)) - } - -} - -// fastEncode xor in bulk. It only works on architectures that -// support unaligned read/writes. -func fastEncode(dst []byte, src [][]byte, n int) { - w := n / wordSize - if w > 0 { - wordBytes := w * wordSize - - wordAlignSrc := make([][]byte, len(src)) - for i := range src { - wordAlignSrc[i] = src[i][:wordBytes] - } - fastEnc(dst[:wordBytes], wordAlignSrc) - } - - for i := n - n%wordSize; i < n; i++ { - s := src[0][i] - for j := 1; j < len(src); j++ { - s ^= src[j][i] - } - dst[i] = s - } -} - -func fastEnc(dst []byte, src [][]byte) { - dw := *(*[]uintptr)(unsafe.Pointer(&dst)) - sw := make([][]uintptr, len(src)) - for i := range src { - sw[i] = *(*[]uintptr)(unsafe.Pointer(&src[i])) - } - - n := len(dst) / wordSize - for i := 0; i < n; i++ { - s := sw[0][i] - for j := 1; j < len(sw); j++ { - s ^= sw[j][i] - } - dw[i] = s - } -} - -func safeEncode(dst []byte, src [][]byte, n int) { - for i := 0; i < n; i++ { - s := src[0][i] - for j := 1; j < len(src); j++ { - s ^= src[j][i] - } - dst[i] = s - } -} - -// Bytes8 XORs of word 8 Bytes. -// The slice arguments a, b, dst's lengths are assumed to be at least 8, -// if not, Bytes8 will panic. -func Bytes8(dst, a, b []byte) { - - bytesWords(dst[:8], a[:8], b[:8]) -} - -// Bytes16 XORs of packed doubleword 16 Bytes. -// The slice arguments a, b, dst's lengths are assumed to be at least 16, -// if not, Bytes16 will panic. -func Bytes16(dst, a, b []byte) { - - bytesWords(dst[:16], a[:16], b[:16]) -} - -// bytesWords XORs multiples of 4 or 8 bytes (depending on architecture.) -// The slice arguments a and b are assumed to be of equal length. -func bytesWords(dst, a, b []byte) { - if supportsUnaligned { - dw := *(*[]uintptr)(unsafe.Pointer(&dst)) - aw := *(*[]uintptr)(unsafe.Pointer(&a)) - bw := *(*[]uintptr)(unsafe.Pointer(&b)) - n := len(b) / wordSize - for i := 0; i < n; i++ { - dw[i] = aw[i] ^ bw[i] - } - } else { - n := len(b) - for i := 0; i < n; i++ { - dst[i] = a[i] ^ b[i] - } - } -} - -// Bytes8Align XORs of 8 Bytes. -// The slice arguments a, b, dst's lengths are assumed to be at least 8, -// if not, Bytes8 will panic. -// -// All the byte slices must be aligned to wordsize. -func Bytes8Align(dst, a, b []byte) { - - bytesWordsAlign(dst[:8], a[:8], b[:8]) -} - -// Bytes16Align XORs of packed 16 Bytes. -// The slice arguments a, b, dst's lengths are assumed to be at least 16, -// if not, Bytes16 will panic. -// -// All the byte slices must be aligned to wordsize. -func Bytes16Align(dst, a, b []byte) { - - bytesWordsAlign(dst[:16], a[:16], b[:16]) -} - -// bytesWordsAlign XORs multiples of 4 or 8 bytes (depending on architecture.) -// The slice arguments a and b are assumed to be of equal length. -// -// All the byte slices must be aligned to wordsize. -func bytesWordsAlign(dst, a, b []byte) { - dw := *(*[]uintptr)(unsafe.Pointer(&dst)) - aw := *(*[]uintptr)(unsafe.Pointer(&a)) - bw := *(*[]uintptr)(unsafe.Pointer(&b)) - n := len(b) / wordSize - for i := 0; i < n; i++ { - dw[i] = aw[i] ^ bw[i] - } -} - -// BytesA XORs the len(a) bytes in a and b into a -// destination slice. -// The destination should have enough space. -// -// It's used for encoding small bytes slices (< dozens bytes), -// and the slices may not be aligned to 8 bytes or 16 bytes. -// If the length is big, it's better to use 'func Bytes(dst, a, b []byte)' instead -// for gain better performance. -func BytesA(dst, a, b []byte) { - - n := len(a) - bytesN(dst[:n], a[:n], b[:n], n) -} - -// BytesB XORs the len(b) bytes in a and b into a -// destination slice. -// The destination should have enough space. -// -// It's used for encoding small bytes slices (< dozens bytes), -// and the slices may not be aligned to 8 bytes or 16 bytes. -// If the length is big, it's better to use 'func Bytes(dst, a, b []byte)' instead -// for gain better performance. -func BytesB(dst, a, b []byte) { - - n := len(b) - bytesN(dst[:n], a[:n], b[:n], n) -} - -func bytesN(dst, a, b []byte, n int) { - - switch { - case supportsUnaligned: - w := n / wordSize - if w > 0 { - dw := *(*[]uintptr)(unsafe.Pointer(&dst)) - aw := *(*[]uintptr)(unsafe.Pointer(&a)) - bw := *(*[]uintptr)(unsafe.Pointer(&b)) - for i := 0; i < w; i++ { - dw[i] = aw[i] ^ bw[i] - } - } - - for i := (n - n%wordSize); i < n; i++ { - dst[i] = a[i] ^ b[i] - } - default: - for i := 0; i < n; i++ { - dst[i] = a[i] ^ b[i] - } - } -} diff --git a/vendor/github.com/templexxx/xorsimd/xor_test.go b/vendor/github.com/templexxx/xorsimd/xor_test.go deleted file mode 100644 index fdcbd957..00000000 --- a/vendor/github.com/templexxx/xorsimd/xor_test.go +++ /dev/null @@ -1,480 +0,0 @@ -// Copyright (c) 2019. Temple3x (temple3x@gmail.com) -// -// Use of this source code is governed by the MIT License -// that can be found in the LICENSE file. -// -// Copyright 2013 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. -// -// TestEncodeBytes is copied from Go Standard lib: -// crypto/cipher/xor_test.go - -package xorsimd - -import ( - "bytes" - "fmt" - "math/rand" - "testing" - "time" - "unsafe" -) - -const ( - kb = 1024 - mb = 1024 * 1024 - - testSize = kb -) - -func TestBytes8(t *testing.T) { - - rand.Seed(time.Now().UnixNano()) - - for j := 0; j < 1024; j++ { - a := make([]byte, 8) - b := make([]byte, 8) - fillRandom(a) - fillRandom(b) - - dst0 := make([]byte, 8) - Bytes8(dst0, a, b) - - dst1 := make([]byte, 8) - for i := 0; i < 8; i++ { - dst1[i] = a[i] ^ b[i] - } - - if !bytes.Equal(dst0, dst1) { - t.Fatal("not equal", a, b, dst0, dst1) - } - } -} - -func TestBytes16(t *testing.T) { - - rand.Seed(time.Now().UnixNano()) - - for j := 0; j < 1024; j++ { - a := make([]byte, 16) - b := make([]byte, 16) - fillRandom(a) - fillRandom(b) - - dst0 := make([]byte, 16) - Bytes16(dst0, a, b) - - dst1 := make([]byte, 16) - for i := 0; i < 16; i++ { - dst1[i] = a[i] ^ b[i] - } - - if !bytes.Equal(dst0, dst1) { - t.Fatal("not equal", dst0, dst1, a, b) - } - } -} - -const wordSize = int(unsafe.Sizeof(uintptr(0))) - -func TestBytes8Align(t *testing.T) { - - rand.Seed(time.Now().UnixNano()) - - for j := 0; j < 1024; j++ { - a := make([]byte, 8+wordSize) - b := make([]byte, 8+wordSize) - dst0 := make([]byte, 8+wordSize) - dst1 := make([]byte, 8+wordSize) - - al := alignment(a) - offset := 0 - if al != 0 { - offset = wordSize - al - } - a = a[offset : offset+8] - - al = alignment(b) - offset = 0 - if al != 0 { - offset = wordSize - al - } - b = b[offset : offset+8] - - al = alignment(dst0) - offset = 0 - if al != 0 { - offset = wordSize - al - } - dst0 = dst0[offset : offset+8] - - al = alignment(dst1) - offset = 0 - if al != 0 { - offset = wordSize - al - } - dst1 = dst1[offset : offset+8] - - fillRandom(a) - fillRandom(b) - - Bytes8Align(dst0, a, b) - - for i := 0; i < 8; i++ { - dst1[i] = a[i] ^ b[i] - } - - if !bytes.Equal(dst0, dst1) { - t.Fatal("not equal", a, b, dst0, dst1) - } - } -} - -func alignment(s []byte) int { - return int(uintptr(unsafe.Pointer(&s[0])) & uintptr(wordSize-1)) -} - -func TestBytes16Align(t *testing.T) { - - rand.Seed(time.Now().UnixNano()) - - for j := 0; j < 1024; j++ { - a := make([]byte, 16+wordSize) - b := make([]byte, 16+wordSize) - dst0 := make([]byte, 16+wordSize) - dst1 := make([]byte, 16+wordSize) - - al := alignment(a) - offset := 0 - if al != 0 { - offset = wordSize - al - } - a = a[offset : offset+16] - - al = alignment(b) - offset = 0 - if al != 0 { - offset = wordSize - al - } - b = b[offset : offset+16] - - al = alignment(dst0) - offset = 0 - if al != 0 { - offset = wordSize - al - } - dst0 = dst0[offset : offset+16] - - al = alignment(dst1) - offset = 0 - if al != 0 { - offset = wordSize - al - } - dst1 = dst1[offset : offset+16] - - fillRandom(a) - fillRandom(b) - - Bytes16Align(dst0, a, b) - - for i := 0; i < 16; i++ { - dst1[i] = a[i] ^ b[i] - } - - if !bytes.Equal(dst0, dst1) { - t.Fatal("not equal", a, b, dst0, dst1) - } - } -} - -func TestBytesA(t *testing.T) { - - rand.Seed(time.Now().UnixNano()) - - for j := 2; j <= 1024; j++ { - - for alignP := 0; alignP < 2; alignP++ { - p := make([]byte, j)[alignP:] - q := make([]byte, j) - d1 := make([]byte, j) - d2 := make([]byte, j) - - fillRandom(p) - fillRandom(q) - - BytesA(d1, p, q) - for i := 0; i < j-alignP; i++ { - d2[i] = p[i] ^ q[i] - } - if !bytes.Equal(d1, d2) { - t.Fatal("not equal") - } - } - } -} - -func TestBytesB(t *testing.T) { - - rand.Seed(time.Now().UnixNano()) - - for j := 2; j <= 1024; j++ { - - for alignQ := 0; alignQ < 2; alignQ++ { - p := make([]byte, j) - q := make([]byte, j)[alignQ:] - d1 := make([]byte, j) - d2 := make([]byte, j) - - fillRandom(p) - fillRandom(q) - - BytesB(d1, p, q) - for i := 0; i < j-alignQ; i++ { - d2[i] = p[i] ^ q[i] - } - if !bytes.Equal(d1, d2) { - t.Fatal("not equal") - } - } - } -} - -func TestBytes(t *testing.T) { - - rand.Seed(time.Now().UnixNano()) - - for j := 1; j <= 1024; j++ { - - for alignP := 0; alignP < 2; alignP++ { - for alignQ := 0; alignQ < 2; alignQ++ { - for alignD := 0; alignD < 2; alignD++ { - p := make([]byte, j)[alignP:] - q := make([]byte, j)[alignQ:] - d1 := make([]byte, j)[alignD:] - d2 := make([]byte, j)[alignD:] - - fillRandom(p) - fillRandom(q) - - Bytes(d1, p, q) - n := min(p, q, d1) - for i := 0; i < n; i++ { - d2[i] = p[i] ^ q[i] - } - if !bytes.Equal(d1, d2) { - t.Fatal("not equal") - } - } - } - } - } -} - -func min(a, b, c []byte) int { - n := len(a) - if len(b) < n { - n = len(b) - } - if len(c) < n { - n = len(c) - } - return n -} - -func TestEncodeWithFeature(t *testing.T) { - max := testSize - - switch getCPUFeature() { - case avx512: - testEncode(t, max, sse2, -1) - testEncode(t, max, avx2, sse2) - testEncode(t, max, avx512, avx2) - case avx2: - testEncode(t, max, sse2, -1) - testEncode(t, max, avx2, sse2) - case sse2: - testEncode(t, max, sse2, -1) - case generic: - testEncode(t, max, generic, -1) - } -} - -func testEncode(t *testing.T, maxSize, feat, cmpFeat int) { - - rand.Seed(time.Now().UnixNano()) - srcN := randIntn(10, 2) // Cannot be 1, see func encode(dst []byte, src [][]byte, feature int). - - fs := featToStr(feat) - for size := 1; size <= maxSize; size++ { - exp := make([]byte, size) - src := make([][]byte, srcN) - for j := 0; j < srcN; j++ { - src[j] = make([]byte, size) - fillRandom(src[j]) - } - - if cmpFeat < 0 { - encodeTested(exp, src) - } else { - cpuFeature = cmpFeat - Encode(exp, src) - } - - act := make([]byte, size) - cpuFeature = feat - Encode(act, src) - - if !bytes.Equal(exp, act) { - t.Fatalf("%s mismatched with %s, src_num: %d, size: %d", - fs, featToStr(cmpFeat), srcN, size) - } - } - - t.Logf("%s pass src_num:%d, max_size: %d", - fs, srcN, maxSize) -} - -func featToStr(f int) string { - switch f { - case avx512: - return "AVX512" - case avx2: - return "AVX2" - case sse2: - return "SSE2" - case generic: - return "Generic" - default: - return "Tested" - } -} - -func encodeTested(dst []byte, src [][]byte) { - - n := len(dst) - for i := 0; i < n; i++ { - s := src[0][i] - for j := 1; j < len(src); j++ { - s ^= src[j][i] - } - dst[i] = s - } -} - -// randIntn returns, as an int, a non-negative pseudo-random number in [min,n) -// from the default Source. -func randIntn(n, min int) int { - m := rand.Intn(n) - if m < min { - m = min - } - return m -} - -func BenchmarkBytes8(b *testing.B) { - s0 := make([]byte, 8) - s1 := make([]byte, 8) - fillRandom(s0) - fillRandom(s1) - dst0 := make([]byte, 8) - - b.ResetTimer() - b.SetBytes(8) - for i := 0; i < b.N; i++ { - Bytes8(dst0, s0, s1) - } -} - -func BenchmarkBytes16(b *testing.B) { - s0 := make([]byte, 16) - s1 := make([]byte, 16) - fillRandom(s0) - fillRandom(s1) - dst0 := make([]byte, 16) - - b.ResetTimer() - b.SetBytes(16) - for i := 0; i < b.N; i++ { - Bytes16(dst0, s0, s1) - } -} - -func BenchmarkBytesN_16Bytes(b *testing.B) { - s0 := make([]byte, 16) - s1 := make([]byte, 16) - fillRandom(s0) - fillRandom(s1) - dst0 := make([]byte, 16) - - b.ResetTimer() - b.SetBytes(16) - for i := 0; i < b.N; i++ { - BytesA(dst0, s0, s1) - } -} - -func BenchmarkEncode(b *testing.B) { - sizes := []int{4 * kb, mb, 8 * mb} - - srcNums := []int{5, 10} - - var feats []int - switch getCPUFeature() { - case avx512: - feats = append(feats, avx512) - feats = append(feats, avx2) - feats = append(feats, sse2) - case avx2: - feats = append(feats, avx2) - feats = append(feats, sse2) - case sse2: - feats = append(feats, sse2) - default: - feats = append(feats, generic) - } - - b.Run("", benchEncRun(benchEnc, srcNums, sizes, feats)) -} - -func benchEncRun(f func(*testing.B, int, int, int), srcNums, sizes, feats []int) func(*testing.B) { - return func(b *testing.B) { - for _, feat := range feats { - for _, srcNum := range srcNums { - for _, size := range sizes { - b.Run(fmt.Sprintf("(%d+1)-%s-%s", srcNum, byteToStr(size), featToStr(feat)), func(b *testing.B) { - f(b, srcNum, size, feat) - }) - } - } - } - } -} - -func benchEnc(b *testing.B, srcNum, size, feat int) { - dst := make([]byte, size) - src := make([][]byte, srcNum) - for i := 0; i < srcNum; i++ { - src[i] = make([]byte, size) - fillRandom(src[i]) - } - cpuFeature = feat - - b.SetBytes(int64((srcNum + 1) * size)) - b.ResetTimer() - for i := 0; i < b.N; i++ { - encode(dst, src) - } -} - -func fillRandom(p []byte) { - rand.Read(p) -} - -func byteToStr(n int) string { - if n >= mb { - return fmt.Sprintf("%dMB", n/mb) - } - - return fmt.Sprintf("%dKB", n/kb) -} diff --git a/vendor/github.com/templexxx/xorsimd/xoravx2_amd64.s b/vendor/github.com/templexxx/xorsimd/xoravx2_amd64.s deleted file mode 100644 index 23cf924d..00000000 --- a/vendor/github.com/templexxx/xorsimd/xoravx2_amd64.s +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) 2019. Temple3x (temple3x@gmail.com) -// -// Use of this source code is governed by the MIT License -// that can be found in the LICENSE file. - -#include "textflag.h" - -#define dst BX // parity's address -#define d2src SI // two-dimension src_slice's address -#define csrc CX // cnt of src -#define len DX // len of vect -#define pos R8 // job position in vect - -#define csrc_tmp R9 -#define d2src_off R10 -#define src_tmp R11 -#define not_aligned_len R12 -#define src_val0 R13 -#define src_val1 R14 - -// func encodeAVX2(dst []byte, src [][]byte) -TEXT ·encodeAVX2(SB), NOSPLIT, $0 - MOVQ d+0(FP), dst - MOVQ s+24(FP), d2src - MOVQ c+32(FP), csrc - MOVQ l+8(FP), len - TESTQ $127, len - JNZ not_aligned - -aligned: - MOVQ $0, pos - -loop128b: - MOVQ csrc, csrc_tmp // store src_cnt -> csrc_tmp - SUBQ $2, csrc_tmp - MOVQ $0, d2src_off - MOVQ (d2src)(d2src_off*1), src_tmp // get first src_vect's addr -> src_tmp - VMOVDQU (src_tmp)(pos*1), Y0 - VMOVDQU 32(src_tmp)(pos*1), Y1 - VMOVDQU 64(src_tmp)(pos*1), Y2 - VMOVDQU 96(src_tmp)(pos*1), Y3 - -next_vect: - ADDQ $24, d2src_off // len(slice) = 24 - MOVQ (d2src)(d2src_off*1), src_tmp // next data_vect - VMOVDQU (src_tmp)(pos*1), Y4 - VMOVDQU 32(src_tmp)(pos*1), Y5 - VMOVDQU 64(src_tmp)(pos*1), Y6 - VMOVDQU 96(src_tmp)(pos*1), Y7 - VPXOR Y4, Y0, Y0 - VPXOR Y5, Y1, Y1 - VPXOR Y6, Y2, Y2 - VPXOR Y7, Y3, Y3 - SUBQ $1, csrc_tmp - JGE next_vect - - VMOVDQU Y0, (dst)(pos*1) - VMOVDQU Y1, 32(dst)(pos*1) - VMOVDQU Y2, 64(dst)(pos*1) - VMOVDQU Y3, 96(dst)(pos*1) - - ADDQ $128, pos - CMPQ len, pos - JNE loop128b - VZEROUPPER - RET - -loop_1b: - MOVQ csrc, csrc_tmp - MOVQ $0, d2src_off - MOVQ (d2src)(d2src_off*1), src_tmp - SUBQ $2, csrc_tmp - MOVB -1(src_tmp)(len*1), src_val0 // encode from the end of src - -next_vect_1b: - ADDQ $24, d2src_off - MOVQ (d2src)(d2src_off*1), src_tmp - MOVB -1(src_tmp)(len*1), src_val1 - XORB src_val1, src_val0 - SUBQ $1, csrc_tmp - JGE next_vect_1b - - MOVB src_val0, -1(dst)(len*1) - SUBQ $1, len - TESTQ $7, len - JNZ loop_1b - - CMPQ len, $0 - JE ret - TESTQ $127, len - JZ aligned - -not_aligned: - TESTQ $7, len - JNE loop_1b - MOVQ len, not_aligned_len - ANDQ $127, not_aligned_len - -loop_8b: - MOVQ csrc, csrc_tmp - MOVQ $0, d2src_off - MOVQ (d2src)(d2src_off*1), src_tmp - SUBQ $2, csrc_tmp - MOVQ -8(src_tmp)(len*1), src_val0 - -next_vect_8b: - ADDQ $24, d2src_off - MOVQ (d2src)(d2src_off*1), src_tmp - MOVQ -8(src_tmp)(len*1), src_val1 - XORQ src_val1, src_val0 - SUBQ $1, csrc_tmp - JGE next_vect_8b - - MOVQ src_val0, -8(dst)(len*1) - SUBQ $8, len - SUBQ $8, not_aligned_len - JG loop_8b - - CMPQ len, $128 - JGE aligned - RET - -ret: - RET diff --git a/vendor/github.com/templexxx/xorsimd/xoravx512_amd64.s b/vendor/github.com/templexxx/xorsimd/xoravx512_amd64.s deleted file mode 100644 index 2ba6b756..00000000 --- a/vendor/github.com/templexxx/xorsimd/xoravx512_amd64.s +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) 2019. Temple3x (temple3x@gmail.com) -// -// Use of this source code is governed by the MIT License -// that can be found in the LICENSE file. - -#include "textflag.h" - -#define dst BX // parity's address -#define d2src SI // two-dimension src_slice's address -#define csrc CX // cnt of src -#define len DX // len of vect -#define pos R8 // job position in vect - -#define csrc_tmp R9 -#define d2src_off R10 -#define src_tmp R11 -#define not_aligned_len R12 -#define src_val0 R13 -#define src_val1 R14 - -// func encodeAVX512(dst []byte, src [][]byte) -TEXT ·encodeAVX512(SB), NOSPLIT, $0 - MOVQ d+0(FP), dst - MOVQ src+24(FP), d2src - MOVQ c+32(FP), csrc - MOVQ l+8(FP), len - TESTQ $255, len - JNZ not_aligned - -aligned: - MOVQ $0, pos - -loop256b: - MOVQ csrc, csrc_tmp // store src_cnt -> csrc_tmp - SUBQ $2, csrc_tmp - MOVQ $0, d2src_off - MOVQ (d2src)(d2src_off*1), src_tmp // get first src_vect's addr -> src_tmp - VMOVDQU8 (src_tmp)(pos*1), Z0 - VMOVDQU8 64(src_tmp)(pos*1), Z1 - VMOVDQU8 128(src_tmp)(pos*1), Z2 - VMOVDQU8 192(src_tmp)(pos*1), Z3 - -next_vect: - ADDQ $24, d2src_off // len(slice) = 24 - MOVQ (d2src)(d2src_off*1), src_tmp // next data_vect - VMOVDQU8 (src_tmp)(pos*1), Z4 - VMOVDQU8 64(src_tmp)(pos*1), Z5 - VMOVDQU8 128(src_tmp)(pos*1), Z6 - VMOVDQU8 192(src_tmp)(pos*1), Z7 - VPXORQ Z4, Z0, Z0 - VPXORQ Z5, Z1, Z1 - VPXORQ Z6, Z2, Z2 - VPXORQ Z7, Z3, Z3 - SUBQ $1, csrc_tmp - JGE next_vect - - VMOVDQU8 Z0, (dst)(pos*1) - VMOVDQU8 Z1, 64(dst)(pos*1) - VMOVDQU8 Z2, 128(dst)(pos*1) - VMOVDQU8 Z3, 192(dst)(pos*1) - - ADDQ $256, pos - CMPQ len, pos - JNE loop256b - VZEROUPPER - RET - -loop_1b: - MOVQ csrc, csrc_tmp - MOVQ $0, d2src_off - MOVQ (d2src)(d2src_off*1), src_tmp - SUBQ $2, csrc_tmp - MOVB -1(src_tmp)(len*1), src_val0 // encode from the end of src - -next_vect_1b: - ADDQ $24, d2src_off - MOVQ (d2src)(d2src_off*1), src_tmp - MOVB -1(src_tmp)(len*1), src_val1 - XORB src_val1, src_val0 - SUBQ $1, csrc_tmp - JGE next_vect_1b - - MOVB src_val0, -1(dst)(len*1) - SUBQ $1, len - TESTQ $7, len - JNZ loop_1b - - CMPQ len, $0 - JE ret - TESTQ $255, len - JZ aligned - -not_aligned: - TESTQ $7, len - JNE loop_1b - MOVQ len, not_aligned_len - ANDQ $255, not_aligned_len - -loop_8b: - MOVQ csrc, csrc_tmp - MOVQ $0, d2src_off - MOVQ (d2src)(d2src_off*1), src_tmp - SUBQ $2, csrc_tmp - MOVQ -8(src_tmp)(len*1), src_val0 - -next_vect_8b: - ADDQ $24, d2src_off - MOVQ (d2src)(d2src_off*1), src_tmp - MOVQ -8(src_tmp)(len*1), src_val1 - XORQ src_val1, src_val0 - SUBQ $1, csrc_tmp - JGE next_vect_8b - - MOVQ src_val0, -8(dst)(len*1) - SUBQ $8, len - SUBQ $8, not_aligned_len - JG loop_8b - - CMPQ len, $256 - JGE aligned - RET - -ret: - RET diff --git a/vendor/github.com/templexxx/xorsimd/xorbytes_amd64.s b/vendor/github.com/templexxx/xorsimd/xorbytes_amd64.s deleted file mode 100644 index 8f67edd2..00000000 --- a/vendor/github.com/templexxx/xorsimd/xorbytes_amd64.s +++ /dev/null @@ -1,72 +0,0 @@ -#include "textflag.h" - -// func bytesN(dst, a, b *byte, n int) -TEXT ·bytesN(SB), NOSPLIT, $0 - MOVQ d+0(FP), BX - MOVQ a+8(FP), SI - MOVQ b+16(FP), CX - MOVQ n+24(FP), DX - TESTQ $15, DX // AND 15 & len, if not zero jump to not_aligned. - JNZ not_aligned - -aligned: - MOVQ $0, AX // position in slices - -loop16b: - MOVOU (SI)(AX*1), X0 // XOR 16byte forwards. - MOVOU (CX)(AX*1), X1 - PXOR X1, X0 - MOVOU X0, (BX)(AX*1) - ADDQ $16, AX - CMPQ DX, AX - JNE loop16b - RET - -loop_1b: - SUBQ $1, DX // XOR 1byte backwards. - MOVB (SI)(DX*1), DI - MOVB (CX)(DX*1), AX - XORB AX, DI - MOVB DI, (BX)(DX*1) - TESTQ $7, DX // AND 7 & len, if not zero jump to loop_1b. - JNZ loop_1b - CMPQ DX, $0 // if len is 0, ret. - JE ret - TESTQ $15, DX // AND 15 & len, if zero jump to aligned. - JZ aligned - -not_aligned: - TESTQ $7, DX // AND $7 & len, if not zero jump to loop_1b. - JNE loop_1b - SUBQ $8, DX // XOR 8bytes backwards. - MOVQ (SI)(DX*1), DI - MOVQ (CX)(DX*1), AX - XORQ AX, DI - MOVQ DI, (BX)(DX*1) - CMPQ DX, $16 // if len is greater or equal 16 here, it must be aligned. - JGE aligned - -ret: - RET - -// func bytes8(dst, a, b *byte) -TEXT ·bytes8(SB), NOSPLIT, $0 - MOVQ d+0(FP), BX - MOVQ a+8(FP), SI - MOVQ b+16(FP), CX - MOVQ (SI), DI - MOVQ (CX), AX - XORQ AX, DI - MOVQ DI, (BX) - RET - -// func bytes16(dst, a, b *byte) -TEXT ·bytes16(SB), NOSPLIT, $0 - MOVQ d+0(FP), BX - MOVQ a+8(FP), SI - MOVQ b+16(FP), CX - MOVOU (SI), X0 - MOVOU (CX), X1 - PXOR X1, X0 - MOVOU X0, (BX) - RET diff --git a/vendor/github.com/templexxx/xorsimd/xorsse2_amd64.s b/vendor/github.com/templexxx/xorsimd/xorsse2_amd64.s deleted file mode 100644 index 38df9489..00000000 --- a/vendor/github.com/templexxx/xorsimd/xorsse2_amd64.s +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) 2019. Temple3x (temple3x@gmail.com) -// -// Use of this source code is governed by the MIT License -// that can be found in the LICENSE file. - -#include "textflag.h" - -#define dst BX // parity's address -#define d2src SI // two-dimension src_slice's address -#define csrc CX // cnt of src -#define len DX // len of vect -#define pos R8 // job position in vect - -#define csrc_tmp R9 -#define d2src_off R10 -#define src_tmp R11 -#define not_aligned_len R12 -#define src_val0 R13 -#define src_val1 R14 - -// func encodeSSE2(dst []byte, src [][]byte) -TEXT ·encodeSSE2(SB), NOSPLIT, $0 - MOVQ d+0(FP), dst - MOVQ src+24(FP), d2src - MOVQ c+32(FP), csrc - MOVQ l+8(FP), len - TESTQ $63, len - JNZ not_aligned - -aligned: - MOVQ $0, pos - -loop64b: - MOVQ csrc, csrc_tmp - SUBQ $2, csrc_tmp - MOVQ $0, d2src_off - MOVQ (d2src)(d2src_off*1), src_tmp - MOVOU (src_tmp)(pos*1), X0 - MOVOU 16(src_tmp)(pos*1), X1 - MOVOU 32(src_tmp)(pos*1), X2 - MOVOU 48(src_tmp)(pos*1), X3 - -next_vect: - ADDQ $24, d2src_off - MOVQ (d2src)(d2src_off*1), src_tmp - MOVOU (src_tmp)(pos*1), X4 - MOVOU 16(src_tmp)(pos*1), X5 - MOVOU 32(src_tmp)(pos*1), X6 - MOVOU 48(src_tmp)(pos*1), X7 - PXOR X4, X0 - PXOR X5, X1 - PXOR X6, X2 - PXOR X7, X3 - SUBQ $1, csrc_tmp - JGE next_vect - - MOVOU X0, (dst)(pos*1) - MOVOU X1, 16(dst)(pos*1) - MOVOU X2, 32(dst)(pos*1) - MOVOU X3, 48(dst)(pos*1) - - ADDQ $64, pos - CMPQ len, pos - JNE loop64b - RET - -loop_1b: - MOVQ csrc, csrc_tmp - MOVQ $0, d2src_off - MOVQ (d2src)(d2src_off*1), src_tmp - SUBQ $2, csrc_tmp - MOVB -1(src_tmp)(len*1), src_val0 - -next_vect_1b: - ADDQ $24, d2src_off - MOVQ (d2src)(d2src_off*1), src_tmp - MOVB -1(src_tmp)(len*1), src_val1 - XORB src_val1, src_val0 - SUBQ $1, csrc_tmp - JGE next_vect_1b - - MOVB src_val0, -1(dst)(len*1) - SUBQ $1, len - TESTQ $7, len - JNZ loop_1b - - CMPQ len, $0 - JE ret - TESTQ $63, len - JZ aligned - -not_aligned: - TESTQ $7, len - JNE loop_1b - MOVQ len, not_aligned_len - ANDQ $63, not_aligned_len - -loop_8b: - MOVQ csrc, csrc_tmp - MOVQ $0, d2src_off - MOVQ (d2src)(d2src_off*1), src_tmp - SUBQ $2, csrc_tmp - MOVQ -8(src_tmp)(len*1), src_val0 - -next_vect_8b: - ADDQ $24, d2src_off - MOVQ (d2src)(d2src_off*1), src_tmp - MOVQ -8(src_tmp)(len*1), src_val1 - XORQ src_val1, src_val0 - SUBQ $1, csrc_tmp - JGE next_vect_8b - - MOVQ src_val0, -8(dst)(len*1) - SUBQ $8, len - SUBQ $8, not_aligned_len - JG loop_8b - - CMPQ len, $64 - JGE aligned - RET - -ret: - RET diff --git a/vendor/github.com/xtaci/kcp-go/v5/.gitignore b/vendor/github.com/xtaci/kcp-go/v5/.gitignore deleted file mode 100644 index 2f4178cc..00000000 --- a/vendor/github.com/xtaci/kcp-go/v5/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test -/vendor/ - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test -*.prof diff --git a/vendor/github.com/xtaci/kcp-go/v5/.travis.yml b/vendor/github.com/xtaci/kcp-go/v5/.travis.yml deleted file mode 100644 index 6754ef67..00000000 --- a/vendor/github.com/xtaci/kcp-go/v5/.travis.yml +++ /dev/null @@ -1,24 +0,0 @@ -arch: - - amd64 - - ppc64le -language: go - -go: - - 1.11.x - - 1.12.x - - 1.13.x - -env: - - GO111MODULE=on - -before_install: - - go get -t -v ./... - -install: - - go get github.com/xtaci/kcp-go - -script: - - go test -coverprofile=coverage.txt -covermode=atomic -bench . -timeout 10m - -after_success: - - bash <(curl -s https://codecov.io/bash) diff --git a/vendor/github.com/xtaci/kcp-go/v5/LICENSE b/vendor/github.com/xtaci/kcp-go/v5/LICENSE index 8294d134..9aac0a10 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/LICENSE +++ b/vendor/github.com/xtaci/kcp-go/v5/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Daniel Fu +Copyright (c) 2015 xtaci Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/vendor/github.com/xtaci/kcp-go/v5/README.md b/vendor/github.com/xtaci/kcp-go/v5/README.md index f68406d9..9c8041eb 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/README.md +++ b/vendor/github.com/xtaci/kcp-go/v5/README.md @@ -1,12 +1,12 @@ -kcp-go +kcp-go -[![GoDoc][1]][2] [![Powered][9]][10] [![MIT licensed][11]][12] [![Build Status][3]][4] [![Go Report Card][5]][6] [![Coverage Statusd][7]][8] [![Sourcegraph][13]][14] +[![GoDoc][1]][2] [![Powered][9]][10] [![MIT licensed][11]][12] [![Build Status][3]][4] [![Go Report Card][5]][6] [![Coverage Status][7]][8] [![Sourcegraph][13]][14] [1]: https://godoc.org/github.com/xtaci/kcp-go?status.svg -[2]: https://pkg.go.dev/github.com/xtaci/kcp-go -[3]: https://travis-ci.org/xtaci/kcp-go.svg?branch=master -[4]: https://travis-ci.org/xtaci/kcp-go +[2]: https://pkg.go.dev/github.com/xtaci/kcp-go/v5 +[3]: https://img.shields.io/github/created-at/xtaci/kcp-go +[4]: https://img.shields.io/github/created-at/xtaci/kcp-go [5]: https://goreportcard.com/badge/github.com/xtaci/kcp-go [6]: https://goreportcard.com/report/github.com/xtaci/kcp-go [7]: https://codecov.io/gh/xtaci/kcp-go/branch/master/graph/badge.svg @@ -17,34 +17,125 @@ [12]: LICENSE [13]: https://sourcegraph.com/github.com/xtaci/kcp-go/-/badge.svg [14]: https://sourcegraph.com/github.com/xtaci/kcp-go?badge + +[English](README.md) | [中文](README_zh.md) + + +## Table of Contents + +- [Introduction](#introduction) +- [Features](#features) +- [Documentation](#documentation) +- [Layer-Model of KCP-GO](#layer-model-of-kcp-go) +- [Key Design Considerations](#key-design-considerations) + - [1. Slice vs. Container/List](#1-slice-vs-containerlist) + - [2. Timing Accuracy vs. Syscall clock_gettime](#2-timing-accuracy-vs-syscall-clock_gettime) + - [3. Memory Management](#3-memory-management) + - [4. Information Security](#4-information-security) + - [5. Packet Clocking](#5-packet-clocking) + - [6. FEC Design Characteristics](#6-fec-design-characteristics) +- [Specification](#specification) +- [Performance](#performance) +- [Typical Flame Graph](#typical-flame-graph) +- [Connection Termination](#connection-termination) +- [FAQ](#faq) +- [Who is using this?](#who-is-using-this) +- [Examples](#examples) +- [Links](#links) ## Introduction -**kcp-go** is a **Production-Grade Reliable-UDP** library for [golang](https://golang.org/). +**kcp-go** is a **Reliable-UDP** library for [golang](https://golang.org/). -This library intents to provide a **smooth, resilient, ordered, error-checked and anonymous** delivery of streams over **UDP** packets, it has been battle-tested with opensource project [kcptun](https://github.com/xtaci/kcptun). Millions of devices(from low-end MIPS routers to high-end servers) have deployed **kcp-go** powered program in a variety of forms like **online games, live broadcasting, file synchronization and network acceleration**. +This library provides **smooth, resilient, ordered, error-checked, and anonymous** stream delivery over **UDP** packets. Battle-tested with the open-source project [kcptun](https://github.com/xtaci/kcptun), millions of devices—from low-end MIPS routers to high-end servers—have deployed kcp-go-powered programs across various applications, including **online games, live broadcasting, file synchronization, and network acceleration**. -[Lastest Release](https://github.com/xtaci/kcp-go/releases) +[Latest Release](https://github.com/xtaci/kcp-go/releases) ## Features -1. Designed for **Latency-sensitive** scenarios. -1. **Cache friendly** and **Memory optimized** design, offers extremely **High Performance** core. -1. Handles **>5K concurrent connections** on a single commodity server. -1. Compatible with [net.Conn](https://golang.org/pkg/net/#Conn) and [net.Listener](https://golang.org/pkg/net/#Listener), a drop-in replacement for [net.TCPConn](https://golang.org/pkg/net/#TCPConn). -1. [FEC(Forward Error Correction)](https://en.wikipedia.org/wiki/Forward_error_correction) Support with [Reed-Solomon Codes](https://en.wikipedia.org/wiki/Reed%E2%80%93Solomon_error_correction) -1. Packet level encryption support with [AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard), [TEA](https://en.wikipedia.org/wiki/Tiny_Encryption_Algorithm), [3DES](https://en.wikipedia.org/wiki/Triple_DES), [Blowfish](https://en.wikipedia.org/wiki/Blowfish_(cipher)), [Cast5](https://en.wikipedia.org/wiki/CAST-128), [Salsa20]( https://en.wikipedia.org/wiki/Salsa20), etc. in [CFB](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Feedback_.28CFB.29) mode, which generates completely anonymous packet. -1. Only **A fixed number of goroutines** will be created for the entire server application, costs in **context switch** between goroutines have been taken into consideration. -1. Compatible with [skywind3000's](https://github.com/skywind3000) C version with various improvements. -1. Platform-dependent optimizations: [sendmmsg](http://man7.org/linux/man-pages/man2/sendmmsg.2.html) and [recvmmsg](http://man7.org/linux/man-pages/man2/recvmmsg.2.html) were expoloited for linux. +1. Designed for **latency-sensitive** scenarios. +2. **Cache-friendly** and **memory-optimized** design, offering an extremely **high-performance** core. +3. Handles **>5K concurrent connections** on a single commodity server. +4. Compatible with [net.Conn](https://golang.org/pkg/net/#Conn) and [net.Listener](https://golang.org/pkg/net/#Listener), serving as a drop-in replacement for [net.TCPConn](https://golang.org/pkg/net/#TCPConn). +5. [FEC (Forward Error Correction)](https://en.wikipedia.org/wiki/Forward_error_correction) support using [Reed-Solomon Codes](https://en.wikipedia.org/wiki/Reed%E2%80%93Solomon_error_correction). +6. Packet-level encryption support for [AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard), [TEA](https://en.wikipedia.org/wiki/Tiny_Encryption_Algorithm), [3DES](https://en.wikipedia.org/wiki/Triple_DES), [Blowfish](https://en.wikipedia.org/wiki/Blowfish_(cipher)), [Cast5](https://en.wikipedia.org/wiki/CAST-128), [Salsa20](https://en.wikipedia.org/wiki/Salsa20), etc., in [CFB](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Feedback_(CFB)) mode, generating completely anonymous packets. +7. [AEAD](https://en.wikipedia.org/wiki/Authenticated_encryption) packet encryption support. +8. Only **a fixed number of goroutines** are created for the entire server application, with **context switching** costs between goroutines taken into consideration. +9. Compatible with [skywind3000's](https://github.com/skywind3000) C version, with various improvements. +10. Platform-specific optimizations: [sendmmsg](http://man7.org/linux/man-pages/man2/sendmmsg.2.html) and [recvmmsg](http://man7.org/linux/man-pages/man2/recvmmsg.2.html) for Linux. ## Documentation -For complete documentation, see the associated [Godoc](https://godoc.org/github.com/xtaci/kcp-go). +For complete documentation, see the associated [Godoc](https://pkg.go.dev/github.com/xtaci/kcp-go/v5). + + +### Layer-Model of KCP-GO + +layer-model + +## Key Design Considerations + +### 1. Slice vs. Container/List + +`kcp.flush()` loops through the send queue for retransmission checking every 20 ms. + +I wrote a benchmark comparing sequential loops through a *slice* and a *container/list* [here](https://gist.github.com/xtaci/ac2f13f0108494d874b25551134e4c9c): + +``` +BenchmarkLoopSlice-4 2000000000 0.39 ns/op +BenchmarkLoopList-4 100000000 54.6 ns/op +``` + +The list structure introduces **heavy cache misses** compared to the slice, which offers better **locality**. For 5,000 connections with a 32-window size and a 20 ms interval, using a slice costs 6 μs (0.03% CPU) per `kcp.flush()`, whereas using a list costs 8.7 ms (43.5% CPU). + +### 2. Timing Accuracy vs. Syscall clock_gettime + +Timing is **critical** for the **RTT estimator**. Inaccurate timing leads to false retransmissions in KCP, but calling `time.Now()` costs 42 cycles (10.5 ns on a 4 GHz CPU, 15.6 ns on my MacBook Pro 2.7 GHz). + +The benchmark for `time.Now()` is [here](https://gist.github.com/xtaci/f01503b9167f9b520b8896682b67e14d): + +``` +BenchmarkNow-4 100000000 15.6 ns/op +``` + +In kcp-go, after each `kcp.output()` function call, the current clock time is updated upon return. For a single `kcp.flush()` operation, the current time is queried from the system once. For 5,000 connections, this costs 5000 × 15.6 ns = 78 μs (a fixed cost when no packets need to be sent). For 10 MB/s data transfer with a 1400 MTU, `kcp.output()` is called approximately 7,500 times, costing 117 μs for `time.Now()` per second. + +### 3. Memory Management + +Primary memory allocation is performed from a global buffer pool, `xmit.Buf`. In kcp-go, when bytes need to be allocated, they are obtained from this pool, which returns a fixed-capacity 1500 bytes (mtuLimit). The rx queue, tx queue, and FEC queue all receive bytes from this pool and return them after use to prevent unnecessary zeroing of bytes. The pool mechanism maintains a high watermark for slice objects, allowing these in-flight objects to survive periodic garbage collection while also being able to return memory to the runtime when idle. + +### 4. Information Security + +kcp-go ships with built-in packet encryption powered by various block encryption algorithms and operates in [Cipher Feedback Mode](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Feedback_(CFB)). For each packet to be sent, the encryption process begins by encrypting a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce) from the [system entropy](https://en.wikipedia.org/wiki//dev/random), ensuring that encryption of the same plaintext never produces the same ciphertext. + +The contents of packets are completely anonymous with encryption, including the headers (FEC, KCP), checksums, and payload. Note that regardless of which encryption method you choose at the upper layer, if you disable encryption, the transmission will be insecure because the header is ***plaintext*** and susceptible to tampering, such as jamming the *sliding window size*, *round-trip time*, *FEC properties*, and *checksums*. `AES-128` is recommended for minimal encryption, as modern CPUs feature [AES-NI](https://en.wikipedia.org/wiki/AES_instruction_set) instructions and perform better than `salsa20` (see the table above). + +Other possible attacks on kcp-go include: + +- **[Traffic analysis](https://en.wikipedia.org/wiki/Traffic_analysis):** Data flow on specific websites may exhibit patterns during data exchange. This type of eavesdropping has been mitigated by adopting [smux](https://github.com/xtaci/smux) to mix data streams and introduce noise. While a perfect solution has not yet emerged, theoretically, shuffling/mixing messages on a larger-scale network may mitigate this problem. +- **[Replay attack](https://en.wikipedia.org/wiki/Replay_attack):** Since asymmetric encryption has not been introduced into kcp-go, capturing packets and replaying them on a different machine is possible. Note that hijacking the session and decrypting the contents is still *impossible*. Upper layers should use an asymmetric encryption system to guarantee the authenticity of each message (to process each message exactly once), such as HTTPS/OpenSSL/LibreSSL. Signing requests with private keys can eliminate this type of attack. + +### 5. Packet Clocking + +1. **Immediate FastACK**: Send immediately after `fastack` is triggered, without waiting for the fixed `interval`. +2. **Immediate ACK**: Send immediately after accumulating ACKs that fill a full MTU packet, also without waiting for the `interval`. + In high-speed networks, this acts as a higher-frequency "clock signal," potentially boosting unidirectional transmission speed by approximately 6x. For instance, if a batch takes only 1.5ms to process on a high-speed link but still adheres to a fixed 10ms transmission cycle, the actual throughput would be limited to 1/6 of the potential. +3. **Pacing Mechanism**: Introduced a pacing clock to prevent burst congestion where data piles up in the kernel when `snd_wnd` is large, causing the kernel to drop packets. While difficult to implement in user space, a usable version has been achieved, allowing user-space echo to stabilize above 100MB/s. +4. **Data Structure Optimization**: Optimized data structures (e.g., `snd_buf` ringbuffer) to ensure good cache coherency. Queues must not be too long; otherwise, traversal costs introduce extra latency. In high-speed networks, the buffer corresponding to BDP should be kept smaller to minimize latency from data structures. Note that the current KCP structure has O(n) complexity for RTO; changing it to O(1) would require significant refactoring. + +Ultimately, nothing is more critical in a transmission system than the clock (real-time performance). + +### 6. FEC Design Characteristics + +- Reed-Solomon based encoder/decoder lives in the `postProcess`/`packetInput` path, so parity shards are generated and consumed without extra goroutines or lock contention. +- Data/parity ratios are configurable per session, letting operators trade ~20–30% bandwidth overhead for lower tail latency on lossy or long-haul links. +- Parity shards are produced from buffer-pool-backed slices, which avoids repeated allocations and keeps GC pressure flat even during multi-Gbps transfers. +- Decoding favors single-pass recovery: as soon as enough shards arrive, the original packets are reconstructed and pushed into `KCP.Input`, minimizing reordering and retransmission storms. +- When combined with encryption, FEC headers stay protected, preventing traffic shapers from inferring recovery patterns or downgrading throughput. ## Specification -Frame Format +Frame Format ``` NONCE: @@ -62,37 +153,97 @@ FEC SEQID: SIZE: The size of KCP frame plus 2 + +KCP Header ++------------------------------+ +| conv (u32) | ++-------+-------+--------------+ +| cmd | frag | wnd | +| u8 | u8 | u16 | ++------------------------------+ +| ts (u32) | ++------------------------------+ +| sn (u32) | ++------------------------------+ +| una (u32) | ++------------------------------+ +| data (bytes) | ++------------------------------+ ``` +## Performance ``` -+-----------------+ -| SESSION | -+-----------------+ -| KCP(ARQ) | -+-----------------+ -| FEC(OPTIONAL) | -+-----------------+ -| CRYPTO(OPTIONAL)| -+-----------------+ -| UDP(PACKET) | -+-----------------+ -| IP | -+-----------------+ -| LINK | -+-----------------+ -| PHY | -+-----------------+ -(LAYER MODEL OF KCP-GO) +2025/11/26 11:12:51 beginning tests, encryption:salsa20, fec:10/3 +goos: linux +goarch: amd64 +pkg: github.com/xtaci/kcp-go/v5 +cpu: AMD Ryzen 9 5950X 16-Core Processor +BenchmarkSM4 +BenchmarkSM4-32 56077 21672 ns/op 138.43 MB/s 0 B/op 0 allocs/op +BenchmarkAES128 +BenchmarkAES128-32 525854 2228 ns/op 1346.69 MB/s 0 B/op 0 allocs/op +BenchmarkAES192 +BenchmarkAES192-32 473692 2429 ns/op 1234.95 MB/s 0 B/op 0 allocs/op +BenchmarkAES256 +BenchmarkAES256-32 427497 2725 ns/op 1101.06 MB/s 0 B/op 0 allocs/op +BenchmarkTEA +BenchmarkTEA-32 149976 8085 ns/op 371.06 MB/s 0 B/op 0 allocs/op +BenchmarkXOR +BenchmarkXOR-32 12333190 92.35 ns/op 32485.16 MB/s 0 B/op 0 allocs/op +BenchmarkBlowfish +BenchmarkBlowfish-32 70762 16983 ns/op 176.65 MB/s 0 B/op 0 allocs/op +BenchmarkNone +BenchmarkNone-32 47325206 24.49 ns/op 122482.39 MB/s 0 B/op 0 allocs/op +BenchmarkCast5 +BenchmarkCast5-32 66837 18035 ns/op 166.35 MB/s 0 B/op 0 allocs/op +Benchmark3DES +Benchmark3DES-32 18402 64349 ns/op 46.62 MB/s 0 B/op 0 allocs/op +BenchmarkTwofish +BenchmarkTwofish-32 56440 21380 ns/op 140.32 MB/s 0 B/op 0 allocs/op +BenchmarkXTEA +BenchmarkXTEA-32 45616 26124 ns/op 114.84 MB/s 0 B/op 0 allocs/op +BenchmarkSalsa20 +BenchmarkSalsa20-32 525685 2199 ns/op 1363.97 MB/s 0 B/op 0 allocs/op +BenchmarkCRC32 +BenchmarkCRC32-32 19418395 59.05 ns/op 17341.83 MB/s +BenchmarkCsprngSystem +BenchmarkCsprngSystem-32 2912889 404.3 ns/op 39.58 MB/s +BenchmarkCsprngMD5 +BenchmarkCsprngMD5-32 15063580 79.23 ns/op 201.95 MB/s +BenchmarkCsprngSHA1 +BenchmarkCsprngSHA1-32 20186407 60.04 ns/op 333.08 MB/s +BenchmarkCsprngNonceMD5 +BenchmarkCsprngNonceMD5-32 13863704 85.11 ns/op 187.98 MB/s +BenchmarkCsprngNonceAES128 +BenchmarkCsprngNonceAES128-32 97239751 12.56 ns/op 1274.09 MB/s +BenchmarkFECDecode +BenchmarkFECDecode-32 1808791 679.1 ns/op 2208.94 MB/s 1641 B/op 3 allocs/op +BenchmarkFECEncode +BenchmarkFECEncode-32 6671982 181.4 ns/op 8270.76 MB/s 2 B/op 0 allocs/op +BenchmarkFlush +BenchmarkFlush-32 322982 3809 ns/op 0 B/op 0 allocs/op +BenchmarkDebugLog +BenchmarkDebugLog-32 1000000000 0.2146 ns/op +BenchmarkEchoSpeed4K +BenchmarkEchoSpeed4K-32 35583 32875 ns/op 124.59 MB/s 18223 B/op 148 allocs/op +BenchmarkEchoSpeed64K +BenchmarkEchoSpeed64K-32 1995 510301 ns/op 128.43 MB/s 284233 B/op 2297 allocs/op +BenchmarkEchoSpeed512K +BenchmarkEchoSpeed512K-32 259 4058131 ns/op 129.19 MB/s 2243058 B/op 18148 allocs/op +BenchmarkEchoSpeed1M +BenchmarkEchoSpeed1M-32 145 8561996 ns/op 122.47 MB/s 4464227 B/op 36009 allocs/op +BenchmarkSinkSpeed4K +BenchmarkSinkSpeed4K-32 194648 42136 ns/op 97.21 MB/s 2073 B/op 50 allocs/op +BenchmarkSinkSpeed64K +BenchmarkSinkSpeed64K-32 10000 113038 ns/op 579.77 MB/s 29242 B/op 741 allocs/op +BenchmarkSinkSpeed256K +BenchmarkSinkSpeed256K-32 1555 843724 ns/op 621.40 MB/s 229558 B/op 5850 allocs/op +BenchmarkSinkSpeed1M +BenchmarkSinkSpeed1M-32 667 1783214 ns/op 588.03 MB/s 462691 B/op 11694 allocs/op +PASS +ok github.com/xtaci/kcp-go/v5 49.978s ``` - -## Examples - -1. [simple examples](https://github.com/xtaci/kcp-go/tree/master/examples) -2. [kcptun client](https://github.com/xtaci/kcptun/blob/master/client/main.go) -3. [kcptun server](https://github.com/xtaci/kcptun/blob/master/server/main.go) - -## Benchmark ``` === Model Name: MacBook Pro @@ -200,80 +351,49 @@ ok github.com/xtaci/kcp-go/v5 64.151s ## Typical Flame Graph -![Flame Graph in kcptun](flame.png) +![Flame Graph in kcptun](assets/flame.png) -## Key Design Considerations -1. slice vs. container/list - -`kcp.flush()` loops through the send queue for retransmission checking for every 20ms(interval). - -I've wrote a benchmark for comparing sequential loop through *slice* and *container/list* here: - -https://github.com/xtaci/notes/blob/master/golang/benchmark2/cachemiss_test.go - -``` -BenchmarkLoopSlice-4 2000000000 0.39 ns/op -BenchmarkLoopList-4 100000000 54.6 ns/op -``` - -List structure introduces **heavy cache misses** compared to slice which owns better **locality**, 5000 connections with 32 window size and 20ms interval will cost 6us/0.03%(cpu) using slice, and 8.7ms/43.5%(cpu) for list for each `kcp.flush()`. - -2. Timing accuracy vs. syscall clock_gettime - -Timing is **critical** to **RTT estimator**, inaccurate timing leads to false retransmissions in KCP, but calling `time.Now()` costs 42 cycles(10.5ns on 4GHz CPU, 15.6ns on my MacBook Pro 2.7GHz). - -The benchmark for time.Now() lies here: - -https://github.com/xtaci/notes/blob/master/golang/benchmark2/syscall_test.go - -``` -BenchmarkNow-4 100000000 15.6 ns/op -``` - -In kcp-go, after each `kcp.output()` function call, current clock time will be updated upon return, and for a single `kcp.flush()` operation, current time will be queried from system once. For most of the time, 5000 connections costs 5000 * 15.6ns = 78us(a fixed cost while no packet needs to be sent), as for 10MB/s data transfering with 1400 MTU, `kcp.output()` will be called around 7500 times and costs 117us for `time.Now()` in **every second**. - -3. Memory management - -Primary memory allocation are done from a global buffer pool xmit.Buf, in kcp-go, when we need to allocate some bytes, we can get from that pool, and a fixed-capacity 1500 bytes(mtuLimit) will be returned, the rx queue, tx queue and fec queue all receive bytes from there, and they will return the bytes to the pool after using to prevent unnecessary zer0ing of bytes. The pool mechanism maintained a high watermark for slice objects, these in-flight objects from the pool will survive from the perodical garbage collection, meanwhile the pool kept the ability to return the memory to runtime if in idle. - -4. Information security - -kcp-go is shipped with builtin packet encryption powered by various block encryption algorithms and works in [Cipher Feedback Mode](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Feedback_(CFB)), for each packet to be sent, the encryption process will start from encrypting a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce) from the [system entropy](https://en.wikipedia.org/wiki//dev/random), so encryption to same plaintexts never leads to a same ciphertexts thereafter. - -The contents of the packets are completely anonymous with encryption, including the headers(FEC,KCP), checksums and contents. Note that, no matter which encryption method you choose on you upper layer, if you disable encryption, the transmit will be insecure somehow, since the header is ***PLAINTEXT*** to everyone it would be susceptible to header tampering, such as jamming the *sliding window size*, *round-trip time*, *FEC property* and *checksums*. ```AES-128``` is suggested for minimal encryption since modern CPUs are shipped with [AES-NI](https://en.wikipedia.org/wiki/AES_instruction_set) instructions and performs even better than `salsa20`(check the table above). - -Other possible attacks to kcp-go includes: a) [traffic analysis](https://en.wikipedia.org/wiki/Traffic_analysis), dataflow on specific websites may have pattern while interchanging data, but this type of eavesdropping has been mitigated by adapting [smux](https://github.com/xtaci/smux) to mix data streams so as to introduce noises, perfect solution to this has not appeared yet, theroretically by shuffling/mixing messages on larger scale network may mitigate this problem. b) [replay attack](https://en.wikipedia.org/wiki/Replay_attack), since the asymmetrical encryption has not been introduced into kcp-go for some reason, capturing the packets and replay them on a different machine is possible, (notice: hijacking the session and decrypting the contents is still *impossible*), so upper layers should contain a asymmetrical encryption system to guarantee the authenticity of each message(to process message exactly once), such as HTTPS/OpenSSL/LibreSSL, only by signing the requests with private keys can eliminate this type of attack. ## Connection Termination -Control messages like **SYN/FIN/RST** in TCP **are not defined** in KCP, you need some **keepalive/heartbeat mechanism** in the application-level. A real world example is to use some **multiplexing** protocol over session, such as [smux](https://github.com/xtaci/smux)(with embedded keepalive mechanism), see [kcptun](https://github.com/xtaci/kcptun) for example. +Control messages like **SYN/FIN/RST** in TCP **are not defined** in KCP. You need a **keepalive/heartbeat mechanism** at the application level. A practical example is to use a **multiplexing** protocol over the session, such as [smux](https://github.com/xtaci/smux) (which has an embedded keepalive mechanism). See [kcptun](https://github.com/xtaci/kcptun) for a reference implementation. ## FAQ -Q: I'm handling >5K connections on my server, the CPU utilization is so high. +**Q: I'm handling >5K connections on my server, and the CPU utilization is very high.** -A: A standalone `agent` or `gate` server for running kcp-go is suggested, not only for CPU utilization, but also important to the **precision** of RTT measurements(timing) which indirectly affects retransmission. By increasing update `interval` with `SetNoDelay` like `conn.SetNoDelay(1, 40, 1, 1)` will dramatically reduce system load, but lower the performance. +**A:** A standalone `agent` or `gate` server for running kcp-go is recommended, not only to reduce CPU utilization but also to improve the **precision** of RTT measurements (timing), which indirectly affects retransmission. Increasing the update `interval` with `SetNoDelay`, such as `conn.SetNoDelay(1, 40, 1, 1)`, will dramatically reduce system load but may lower performance. -Q: When should I enable FEC? +**Q: When should I enable FEC?** -A: Forward error correction is critical to long-distance transmission, because a packet loss will lead to a huge penalty in time. And for the complicated packet routing network in modern world, round-trip time based loss check will not always be efficient, the big deviation of RTT samples in the long way usually leads to a larger RTO value in typical rtt estimator, which in other words, slows down the transmission. - -Q: Should I enable encryption? +**A:** Forward error correction is critical for long-distance transmission because packet loss incurs a significant time penalty. In the complex packet routing networks of the modern world, round-trip time-based loss checks are not always efficient. The significant deviation of RTT samples over long distances typically leads to a larger RTO value in typical RTT estimators, which slows down transmission. -A: Yes, for the safety of protocol, even if the upper layer has encrypted. +**Q: Should I enable encryption?** + +**A:** Yes, for the security of the protocol, even if the upper layer has encryption. ## Who is using this? -1. https://github.com/xtaci/kcptun -- A Secure Tunnel Based On KCP over UDP. -2. https://github.com/getlantern/lantern -- Lantern delivers fast access to the open Internet. -3. https://github.com/smallnest/rpcx -- A RPC service framework based on net/rpc like alibaba Dubbo and weibo Motan. +1. https://github.com/xtaci/kcptun -- A Secure Tunnel Based on KCP over UDP. +2. https://github.com/getlantern/lantern -- Lantern delivers fast access to the open Internet. +3. https://github.com/smallnest/rpcx -- An RPC service framework based on net/rpc, similar to Alibaba Dubbo and Weibo Motan. 4. https://github.com/gonet2/agent -- A gateway for games with stream multiplexing. 5. https://github.com/syncthing/syncthing -- Open Source Continuous File Synchronization. +6. https://github.com/hanselime/paqet -- A bidirectional packet-level proxy built using raw sockets and KCP. + +### Looking for a C++ client? +1. https://github.com/xtaci/libkcp -- FEC enhanced KCP session library for iOS/Android in C++ + +## Examples + +1. [simple examples](https://github.com/xtaci/kcp-go/tree/master/examples) +2. [kcptun client](https://github.com/xtaci/kcptun/blob/master/client/main.go) +3. [kcptun server](https://github.com/xtaci/kcptun/blob/master/server/main.go) ## Links 1. https://github.com/xtaci/smux/ -- A Stream Multiplexing Library for golang with least memory -1. https://github.com/xtaci/libkcp -- FEC enhanced KCP session library for iOS/Android in C++ +1. **https://github.com/xtaci/libkcp -- FEC enhanced KCP session library for iOS/Android in C++** 1. https://github.com/skywind3000/kcp -- A Fast and Reliable ARQ Protocol 1. https://github.com/klauspost/reedsolomon -- Reed-Solomon Erasure Coding in Go diff --git a/vendor/github.com/xtaci/kcp-go/v5/autotune.go b/vendor/github.com/xtaci/kcp-go/v5/autotune.go index 1f85be33..5491bb17 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/autotune.go +++ b/vendor/github.com/xtaci/kcp-go/v5/autotune.go @@ -1,63 +1,156 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + package kcp -const maxAutoTuneSamples = 258 +import ( + "sort" +) + +const maxAutoTuneSamples = 258 // 256 + 2 extra samples for edge detection -// pulse represents a 0/1 signal with time sequence +// pulse represents a single sample in the signal stream: a boolean bit +// (data=true, parity=false) tagged with its FEC sequence number. type pulse struct { - bit bool // 0 or 1 - seq uint32 // sequence of the signal + bit bool // true = data shard, false = parity shard + seq uint32 // FEC sequence number of this packet } -// autoTune object +// autoTune detects the repeating period of data and parity shards in the +// incoming FEC stream. This allows the decoder to auto-detect the peer's +// dataShards/parityShards configuration. +// +// Algorithm: collect recent samples into a ring buffer, sort by sequence, +// then scan for the first complete pulse (rising edge -> falling edge) +// of the target signal. The pulse width equals the shard count. type autoTune struct { - pulses [maxAutoTuneSamples]pulse + pulses [maxAutoTuneSamples]pulse // fixed-size array to avoid heap allocations + sortCache [maxAutoTuneSamples]pulse // reusable cache for sorting to avoid allocations + head int // oldest element index + tail int // next write position + count int // number of elements } -// Sample adds a signal sample to the pulse buffer +// Sample adds a signal sample to the pulse buffer using a ring buffer func (tune *autoTune) Sample(bit bool, seq uint32) { - tune.pulses[seq%maxAutoTuneSamples] = pulse{bit, seq} + // Write to current tail position + tune.pulses[tune.tail] = pulse{bit: bit, seq: seq} + tune.tail = (tune.tail + 1) % maxAutoTuneSamples + + if tune.count < maxAutoTuneSamples { + tune.count++ + } else { + // Buffer is full, advance head (discard oldest) + tune.head = (tune.head + 1) % maxAutoTuneSamples + } } -// Find a period for a given signal -// returns -1 if not found +// FindPeriod detects the period (pulse width) of the given signal type +// in the collected samples. Returns -1 if no valid period is found. +// +// For bit=true (data shards), it finds how many consecutive data shards +// appear before a parity shard, giving us the dataShards count. +// For bit=false (parity shards), it gives the parityShards count. // -// --- ------ -// | | -// |______________| -// Period -// Falling Edge Rising Edge +// The detection works by finding a complete pulse in the sorted sequence: +// 1. Find the "left edge": where the signal transitions TO the target bit +// 2. Find the "right edge": where the signal transitions AWAY from the target bit +// 3. The period = rightEdge - leftEdge +// +// Example signal (bit=true looking for data period): +// +// Signal Level +// | +// 1.0 | _____ _____ +// | | | | | +// 0.5 | _____ | | _____ | | _____ +// | | | | | | || | | | +// 0.0 |_____| |____| |__| || |__| |_____ +// | +// |-----------------------------------------------------> Time +// A B C D E F G H I func (tune *autoTune) FindPeriod(bit bool) int { - // last pulse and initial index setup - lastPulse := tune.pulses[0] - idx := 1 + // Need at least 3 samples to detect a period (rising and falling edges) + if tune.count < 3 { + return -1 + } + + // Copy elements from ring buffer to sortCache for sorting and analysis. + // Using fixed-size array to avoid heap allocation. + for i := 0; i < tune.count; i++ { + idx := (tune.head + i) % maxAutoTuneSamples + tune.sortCache[i] = tune.pulses[idx] + } + + // Create a slice view over the cache for sorting + sorted := tune.sortCache[:tune.count] + + // Sort the copied data by sequence number (seq) to ensure linear order for period calculation. + sort.Slice(sorted, func(i, j int) bool { + return _itimediff(sorted[i].seq, sorted[j].seq) < 0 + }) // left edge - var leftEdge int - for ; idx < len(tune.pulses); idx++ { - if lastPulse.bit != bit && tune.pulses[idx].bit == bit { // edge found - if lastPulse.seq+1 == tune.pulses[idx].seq { // ensure edge continuity - leftEdge = idx + leftEdge := -1 + lastPulse := sorted[0] + idx := 1 + + for ; idx < len(sorted); idx++ { + if lastPulse.seq+1 == sorted[idx].seq { // continuous sequence + if lastPulse.bit != bit && sorted[idx].bit == bit { // edge found + leftEdge = idx // mark left edge(the changed bit position) break } + } else { + return -1 } - lastPulse = tune.pulses[idx] + lastPulse = sorted[idx] + } + + // no left edge found + if leftEdge == -1 { + return -1 } // right edge - var rightEdge int - lastPulse = tune.pulses[leftEdge] + rightEdge := -1 + lastPulse = sorted[leftEdge] idx = leftEdge + 1 - for ; idx < len(tune.pulses); idx++ { - if lastPulse.seq+1 == tune.pulses[idx].seq { // ensure pulses in this level monotonic - if lastPulse.bit == bit && tune.pulses[idx].bit != bit { // edge found + for ; idx < len(sorted); idx++ { + if lastPulse.seq+1 == sorted[idx].seq { + if lastPulse.bit == bit && sorted[idx].bit != bit { rightEdge = idx break } } else { return -1 } - lastPulse = tune.pulses[idx] + lastPulse = sorted[idx] + } + + // no right edge found + if rightEdge == -1 { + return -1 } return rightEdge - leftEdge diff --git a/vendor/github.com/xtaci/kcp-go/v5/autotune_test.go b/vendor/github.com/xtaci/kcp-go/v5/autotune_test.go index 3dc1ecc6..68ac82f0 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/autotune_test.go +++ b/vendor/github.com/xtaci/kcp-go/v5/autotune_test.go @@ -1,3 +1,25 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + package kcp import ( @@ -7,41 +29,555 @@ import ( ) func TestAutoTune(t *testing.T) { + + // Group1 signals := []uint32{0, 0, 0, 0, 0, 0} + testGroup(t, 1, signals, -1, -1) + + // Group2 + signals = []uint32{0, 1, 0, 1, 0, 1} + testGroup(t, 2, signals, 1, 1) + + // Group3 + signals = []uint32{1, 0, 1, 0, 0, 1} + testGroup(t, 3, signals, 1, 1) + + // Group4 + signals = []uint32{1, 0, 0, 0, 0, 1} + testGroup(t, 4, signals, 4, -1) + + // Group5 + signals = []uint32{1, 1, 1, 1, 1, 1} + testGroup(t, 5, signals, -1, -1) + + // Group6 + signals = []uint32{1, 1, 0, 1, 1, 0} + testGroup(t, 6, signals, 1, 2) + + // Group7 + signals = []uint32{0, 1, 1, 1, 0, 1} + testGroup(t, 7, signals, 1, 3) + + // Group8 + signals = []uint32{1, 1, 1, 1, 1, 1} + testGroup(t, 8, signals, -1, -1) + + // Group9 + signals = []uint32{0, 1, 1, 1, 1, 0} + testGroup(t, 9, signals, -1, 4) + + // Group10 + signals = []uint32{0, 0, 1, 1, 0, 0} + testGroup(t, 10, signals, -1, 2) + + // Group11 + signals = []uint32{0, 0, 0, 1, 1, 1} + testGroup(t, 11, signals, -1, -1) + + // Group12 + signals = []uint32{0, 0, 0, 0, 0, 1} + testGroup(t, 12, signals, -1, -1) + + // Group13 + signals = []uint32{1, 0, 0, 0, 0, 1} + testGroup(t, 13, signals, 4, -1) + // Group14 + signals = []uint32{1, 0, 0, 0, 0, 0} + testGroup(t, 14, signals, -1, -1) +} + +func TestAutoTuneEdge(t *testing.T) { + // Edge Case0: Empty signals + signals := []uint32{} + testGroup(t, 0, signals, -1, -1) + + // Edge Case: 1 signal + signals = []uint32{1} + testGroup(t, 2, signals, -1, -1) + + // Edge Case: 2 signals + signals = []uint32{1, 0} + testGroup(t, 3, signals, -1, -1) + + // Edge Case: 3 signals + signals = []uint32{1, 0, 1} + testGroup(t, 4, signals, 1, -1) +} + +func testGroup(t *testing.T, gid int, signals []uint32, expectedFalse, expectedTrue int) { tune := autoTune{} - for i := 0; i < len(signals); i++ { - if signals[i] == 0 { + for i, signal := range signals { + if signal == 0 { tune.Sample(false, uint32(i)) } else { tune.Sample(true, uint32(i)) } } - assert.Equal(t, -1, tune.FindPeriod(false)) - assert.Equal(t, -1, tune.FindPeriod(true)) + t.Log("Group#", gid, signals, tune.FindPeriod(false), tune.FindPeriod(true)) + assert.Equal(t, expectedTrue, tune.FindPeriod(true)) + assert.Equal(t, expectedFalse, tune.FindPeriod(false)) +} - signals = []uint32{1, 0, 1, 0, 0, 1} - tune = autoTune{} - for i := 0; i < len(signals); i++ { - if signals[i] == 0 { +func TestAutoTuneOverflow(t *testing.T) { + // minimal test + tune := autoTune{} + for i := range 1024 { + if i%maxAutoTuneSamples == 0 { tune.Sample(false, uint32(i)) } else { tune.Sample(true, uint32(i)) } + assert.LessOrEqual(t, tune.count, maxAutoTuneSamples) } - assert.Equal(t, 1, tune.FindPeriod(false)) - assert.Equal(t, 1, tune.FindPeriod(true)) - signals = []uint32{1, 0, 0, 0, 0, 1} - tune = autoTune{} - for i := 0; i < len(signals); i++ { - if signals[i] == 0 { + assert.Equal(t, maxAutoTuneSamples, tune.count) +} + +func TestAutoTunePop(t *testing.T) { + signals := []uint32{0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0} + tune := autoTune{} + for i, signal := range signals { + if signal == 0 { tune.Sample(false, uint32(i)) } else { tune.Sample(true, uint32(i)) } } - assert.Equal(t, -1, tune.FindPeriod(true)) - assert.Equal(t, 4, tune.FindPeriod(false)) + assert.Equal(t, 2, tune.FindPeriod(false)) + assert.Equal(t, 2, tune.FindPeriod(true)) + + // Simulate pop by advancing head (removes oldest element) + tune.head = (tune.head + 1) % maxAutoTuneSamples + tune.count-- + + assert.Equal(t, 2, tune.FindPeriod(false)) + assert.Equal(t, 1, tune.FindPeriod(true)) + + // after popping more + tune.head = (tune.head + 1) % maxAutoTuneSamples + tune.count-- + tune.head = (tune.head + 1) % maxAutoTuneSamples + tune.count-- + tune.head = (tune.head + 1) % maxAutoTuneSamples + tune.count-- + + assert.Equal(t, 1, tune.FindPeriod(false)) + assert.Equal(t, 1, tune.FindPeriod(true)) +} + +// TestAutoTuneRingBufferWrapAround tests that the ring buffer correctly handles +// wrap-around scenarios and still finds the correct period after many overwrites. +func TestAutoTuneRingBufferWrapAround(t *testing.T) { + // Test with various periods after buffer wrap-around + testCases := []struct { + name string + period int // period of the signal + totalSamples int // total samples to generate (should exceed maxAutoTuneSamples) + expectedPeriod int // expected period to be found + bit bool // which bit's period to find + }{ + {"Period3_2xBuffer", 3, maxAutoTuneSamples * 2, 3, true}, + {"Period5_3xBuffer", 5, maxAutoTuneSamples * 3, 5, true}, + {"Period7_4xBuffer", 7, maxAutoTuneSamples * 4, 7, true}, + {"Period10_2xBuffer", 10, maxAutoTuneSamples * 2, 10, true}, + {"Period3_2xBuffer_False", 3, maxAutoTuneSamples * 2, 3, false}, + {"Period5_3xBuffer_False", 5, maxAutoTuneSamples * 3, 5, false}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tune := autoTune{} + period := tc.period + halfPeriod := period / 2 + if halfPeriod == 0 { + halfPeriod = 1 + } + + // Generate periodic signal: N samples of 1, then N samples of 0 + for i := 0; i < tc.totalSamples; i++ { + posInPeriod := i % period + bit := posInPeriod < halfPeriod + tune.Sample(bit, uint32(i)) + } + + // Verify buffer is at maximum capacity + assert.Equal(t, maxAutoTuneSamples, tune.count) + + // Find period and verify + foundPeriod := tune.FindPeriod(tc.bit) + t.Logf("Period=%d, TotalSamples=%d, Found=%d, Expected=%d", + tc.period, tc.totalSamples, foundPeriod, tc.expectedPeriod) + + // The found period should match the expected period + // Note: Due to how period is calculated (right edge - left edge), + // the actual value depends on the signal pattern + assert.True(t, foundPeriod > 0, "Should find a valid period") + }) + } +} + +// TestAutoTuneStablePeriodAfterOverwrite tests that the period remains stable +// even after the ring buffer has been overwritten multiple times. +func TestAutoTuneStablePeriodAfterOverwrite(t *testing.T) { + tune := autoTune{} + period := 6 // total period + trueDuration := 3 + + // Track periods found during continuous sampling + var periodsFound []int + + // Generate a lot of samples with a consistent period + totalSamples := maxAutoTuneSamples * 5 + for i := range totalSamples { + posInPeriod := i % period + bit := posInPeriod < trueDuration + tune.Sample(bit, uint32(i)) + + // After buffer is full, periodically check the found period + if tune.count == maxAutoTuneSamples && i%50 == 0 { + p := tune.FindPeriod(true) + if p > 0 { + periodsFound = append(periodsFound, p) + } + } + } + + // All found periods should be consistent (same value) + assert.NotEmpty(t, periodsFound, "Should have found periods during sampling") + + // Check that periods are stable (all the same or very close) + if len(periodsFound) > 1 { + firstPeriod := periodsFound[0] + for i, p := range periodsFound { + assert.Equal(t, firstPeriod, p, + "Period at sample %d should match first period", i) + } + t.Logf("Found stable period %d across %d checks after buffer overwrite", + firstPeriod, len(periodsFound)) + } +} + +// TestAutoTuneVariousPeriodsFull tests various period lengths when buffer is full +func TestAutoTuneVariousPeriodsFull(t *testing.T) { + // Test different period values + periods := []int{2, 4, 6, 8, 10, 12, 16, 20, 32, 50} + + for _, period := range periods { + t.Run("Period"+string(rune('0'+period/10))+string(rune('0'+period%10)), func(t *testing.T) { + tune := autoTune{} + trueDuration := period / 2 + if trueDuration == 0 { + trueDuration = 1 + } + + // Fill buffer completely and then some more + totalSamples := maxAutoTuneSamples + 100 + for i := range totalSamples { + posInPeriod := i % period + bit := posInPeriod < trueDuration + tune.Sample(bit, uint32(i)) + } + + assert.Equal(t, maxAutoTuneSamples, tune.count) + + periodTrue := tune.FindPeriod(true) + periodFalse := tune.FindPeriod(false) + + t.Logf("Period=%d, TrueDuration=%d, FoundTrue=%d, FoundFalse=%d", + period, trueDuration, periodTrue, periodFalse) + + // Should find valid periods + assert.True(t, periodTrue > 0 || periodFalse > 0, + "Should find at least one valid period") + }) + } +} + +// TestAutoTuneContinuousOverwrite tests continuous overwrite scenarios +// to ensure the ring buffer correctly maintains period detection capability +func TestAutoTuneContinuousOverwrite(t *testing.T) { + tune := autoTune{} + + // First phase: fill with period 4 signal + period1 := 4 + for i := range maxAutoTuneSamples { + bit := (i % period1) < (period1 / 2) + tune.Sample(bit, uint32(i)) + } + assert.Equal(t, maxAutoTuneSamples, tune.count) + + p1 := tune.FindPeriod(true) + t.Logf("Phase 1: Period 4 signal, found period=%d", p1) + assert.True(t, p1 > 0, "Should find period in phase 1") + + // Second phase: continue with period 6 signal + // After enough samples, the old period 4 data should be overwritten + period2 := 6 + baseSeq := uint32(maxAutoTuneSamples) + for i := range maxAutoTuneSamples { + bit := (i % period2) < (period2 / 2) + tune.Sample(bit, baseSeq+uint32(i)) + } + + p2 := tune.FindPeriod(true) + t.Logf("Phase 2: Period 6 signal, found period=%d", p2) + assert.True(t, p2 > 0, "Should find period in phase 2") + + // Third phase: continue with period 8 signal + period3 := 8 + baseSeq = uint32(maxAutoTuneSamples * 2) + for i := range maxAutoTuneSamples { + bit := (i % period3) < (period3 / 2) + tune.Sample(bit, baseSeq+uint32(i)) + } + + p3 := tune.FindPeriod(true) + t.Logf("Phase 3: Period 8 signal, found period=%d", p3) + assert.True(t, p3 > 0, "Should find period in phase 3") +} + +// TestAutoTunePeriodChangeAfterOverwrite tests that when signal pattern changes, +// the new period can be correctly detected after old data is fully overwritten +func TestAutoTunePeriodChangeAfterOverwrite(t *testing.T) { + testCases := []struct { + name string + oldTrueDuration int + oldFalseDuration int + newTrueDuration int + newFalseDuration int + expectedNewTrue int + expectedNewFalse int + }{ + {"Period_2to4", 1, 1, 2, 2, 2, 2}, + {"Period_4to2", 2, 2, 1, 1, 1, 1}, + {"Period_4to8", 2, 2, 4, 4, 4, 4}, + {"Period_8to4", 4, 4, 2, 2, 2, 2}, + {"Period_6to10", 3, 3, 5, 5, 5, 5}, + {"Period_10to6", 5, 5, 3, 3, 3, 3}, + {"Period_3to7", 1, 2, 3, 4, 3, 4}, + {"Period_Asym2T4F_to_5T3F", 2, 4, 5, 3, 5, 3}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tune := autoTune{} + oldPeriod := tc.oldTrueDuration + tc.oldFalseDuration + newPeriod := tc.newTrueDuration + tc.newFalseDuration + + // Phase 1: Fill buffer completely with old period signal + for i := range maxAutoTuneSamples { + posInPeriod := i % oldPeriod + bit := posInPeriod < tc.oldTrueDuration + tune.Sample(bit, uint32(i)) + } + + oldFoundTrue := tune.FindPeriod(true) + oldFoundFalse := tune.FindPeriod(false) + t.Logf("Old period: True=%d (expected %d), False=%d (expected %d)", + oldFoundTrue, tc.oldTrueDuration, oldFoundFalse, tc.oldFalseDuration) + + assert.Equal(t, tc.oldTrueDuration, oldFoundTrue, "Old true period should match") + assert.Equal(t, tc.oldFalseDuration, oldFoundFalse, "Old false period should match") + + // Phase 2: Overwrite with new period signal + // Fill buffer completely to ensure all old data is replaced + baseSeq := uint32(maxAutoTuneSamples) + for i := range maxAutoTuneSamples { + posInPeriod := i % newPeriod + bit := posInPeriod < tc.newTrueDuration + tune.Sample(bit, baseSeq+uint32(i)) + } + + newFoundTrue := tune.FindPeriod(true) + newFoundFalse := tune.FindPeriod(false) + t.Logf("New period: True=%d (expected %d), False=%d (expected %d)", + newFoundTrue, tc.expectedNewTrue, newFoundFalse, tc.expectedNewFalse) + + assert.Equal(t, tc.expectedNewTrue, newFoundTrue, + "New true period should match after overwrite") + assert.Equal(t, tc.expectedNewFalse, newFoundFalse, + "New false period should match after overwrite") + }) + } +} + +// TestAutoTuneMultiplePeriodChanges tests multiple consecutive period changes +func TestAutoTuneMultiplePeriodChanges(t *testing.T) { + tune := autoTune{} + + // Define a sequence of periods to test + periodSequence := []struct { + trueDuration int + falseDuration int + }{ + {2, 2}, // Period 4 + {3, 3}, // Period 6 + {5, 5}, // Period 10 + {1, 1}, // Period 2 + {4, 4}, // Period 8 + {2, 6}, // Period 8 (asymmetric) + {7, 3}, // Period 10 (asymmetric) + } + + seq := uint32(0) + for phase, p := range periodSequence { + period := p.trueDuration + p.falseDuration + + // Fill buffer completely with current period + for i := range maxAutoTuneSamples { + posInPeriod := i % period + bit := posInPeriod < p.trueDuration + tune.Sample(bit, seq) + seq++ + } + + foundTrue := tune.FindPeriod(true) + foundFalse := tune.FindPeriod(false) + + t.Logf("Phase %d: Period=%d (%dT+%dF), FoundTrue=%d, FoundFalse=%d", + phase+1, period, p.trueDuration, p.falseDuration, foundTrue, foundFalse) + + assert.Equal(t, p.trueDuration, foundTrue, + "Phase %d: True period should match", phase+1) + assert.Equal(t, p.falseDuration, foundFalse, + "Phase %d: False period should match", phase+1) + } +} + +// TestAutoTuneGradualPeriodTransition tests the transition period +// when old data is being gradually replaced by new data +func TestAutoTuneGradualPeriodTransition(t *testing.T) { + tune := autoTune{} + + // First: fill with period 4 (2T+2F) + oldTrueDuration := 2 + oldFalseDuration := 2 + oldPeriod := 4 + for i := range maxAutoTuneSamples { + bit := (i % oldPeriod) < oldTrueDuration + tune.Sample(bit, uint32(i)) + } + + // Verify old period for both true and false + assert.Equal(t, oldTrueDuration, tune.FindPeriod(true)) + assert.Equal(t, oldFalseDuration, tune.FindPeriod(false)) + t.Logf("Initial period 4 (2T+2F) established: true=%d, false=%d", + tune.FindPeriod(true), tune.FindPeriod(false)) + + // Now gradually replace with period 6 (3T+3F) + newTrueDuration := 3 + newFalseDuration := 3 + newPeriod := 6 + baseSeq := uint32(maxAutoTuneSamples) + + // Track when the new period becomes stable + transitionCompleteTrue := -1 + transitionCompleteFalse := -1 + for i := range maxAutoTuneSamples { + bit := (i % newPeriod) < newTrueDuration + tune.Sample(bit, baseSeq+uint32(i)) + + // Check every 10 samples + if i%10 == 0 && i > 0 { + foundTrue := tune.FindPeriod(true) + foundFalse := tune.FindPeriod(false) + // Once we find the new period, mark transition complete + if foundTrue == newTrueDuration && transitionCompleteTrue == -1 { + transitionCompleteTrue = i + t.Logf("True transition complete at sample %d, found new period=%d", i, foundTrue) + } + if foundFalse == newFalseDuration && transitionCompleteFalse == -1 { + transitionCompleteFalse = i + t.Logf("False transition complete at sample %d, found new period=%d", i, foundFalse) + } + } + } + + // After full overwrite, should definitely find new period for both + finalPeriodTrue := tune.FindPeriod(true) + finalPeriodFalse := tune.FindPeriod(false) + assert.Equal(t, newTrueDuration, finalPeriodTrue, + "After full overwrite, should find new true period") + assert.Equal(t, newFalseDuration, finalPeriodFalse, + "After full overwrite, should find new false period") + t.Logf("Final period after full overwrite: true=%d, false=%d", finalPeriodTrue, finalPeriodFalse) +} + +// TestAutoTuneExactPeriodAfterWrap tests that exact periods can be detected +// after the ring buffer has wrapped around +func TestAutoTuneExactPeriodAfterWrap(t *testing.T) { + // Test specific patterns where we know the exact expected output + testCases := []struct { + name string + trueDuration int + falseDuration int + expectedTrue int + expectedFalse int + }{ + {"1T_1F", 1, 1, 1, 1}, + {"2T_2F", 2, 2, 2, 2}, + {"3T_3F", 3, 3, 3, 3}, + {"4T_4F", 4, 4, 4, 4}, + {"5T_5F", 5, 5, 5, 5}, + {"2T_4F", 2, 4, 2, 4}, + {"4T_2F", 4, 2, 4, 2}, + {"3T_6F", 3, 6, 3, 6}, + {"6T_3F", 6, 3, 6, 3}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tune := autoTune{} + period := tc.trueDuration + tc.falseDuration + + // Generate enough samples to wrap around multiple times + totalSamples := maxAutoTuneSamples * 3 + for i := range totalSamples { + posInPeriod := i % period + bit := posInPeriod < tc.trueDuration + tune.Sample(bit, uint32(i)) + } + + assert.Equal(t, maxAutoTuneSamples, tune.count) + + periodTrue := tune.FindPeriod(true) + periodFalse := tune.FindPeriod(false) + + t.Logf("Pattern %s: FoundTrue=%d (expected %d), FoundFalse=%d (expected %d)", + tc.name, periodTrue, tc.expectedTrue, periodFalse, tc.expectedFalse) + + assert.Equal(t, tc.expectedTrue, periodTrue, + "True period should match expected") + assert.Equal(t, tc.expectedFalse, periodFalse, + "False period should match expected") + }) + } +} + +// TestAutoTuneSequenceWrapAround tests period detection when sequence numbers wrap around +func TestAutoTuneSequenceWrapAround(t *testing.T) { + tune := autoTune{} + period := 4 + trueDuration := 2 + + // Start from a high sequence number close to uint32 max + startSeq := uint32(0xFFFFFFFF - 100) + + // Generate samples that will cause sequence wrap-around + for i := range maxAutoTuneSamples + 50 { + posInPeriod := i % period + bit := posInPeriod < trueDuration + tune.Sample(bit, startSeq+uint32(i)) + } + + // Note: With sequence wrap-around, FindPeriod might return -1 + // because it checks for continuous sequences (lastPulse.seq+1 == sorted[idx].seq) + // This is expected behavior - the test documents this edge case + periodTrue := tune.FindPeriod(true) + periodFalse := tune.FindPeriod(false) + + t.Logf("Sequence wrap-around test: FoundTrue=%d, FoundFalse=%d", periodTrue, periodFalse) + // We don't assert specific values here as this is an edge case documentation } diff --git a/vendor/github.com/xtaci/kcp-go/v5/batchconn.go b/vendor/github.com/xtaci/kcp-go/v5/batchconn.go deleted file mode 100644 index 6c307010..00000000 --- a/vendor/github.com/xtaci/kcp-go/v5/batchconn.go +++ /dev/null @@ -1,12 +0,0 @@ -package kcp - -import "golang.org/x/net/ipv4" - -const ( - batchSize = 16 -) - -type batchConn interface { - WriteBatch(ms []ipv4.Message, flags int) (int, error) - ReadBatch(ms []ipv4.Message, flags int) (int, error) -} diff --git a/vendor/github.com/xtaci/kcp-go/v5/bufferpool.go b/vendor/github.com/xtaci/kcp-go/v5/bufferpool.go new file mode 100644 index 00000000..512bd41d --- /dev/null +++ b/vendor/github.com/xtaci/kcp-go/v5/bufferpool.go @@ -0,0 +1,65 @@ +// The MIT License (MIT) +// +// # Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package kcp + +import ( + "errors" + "sync" +) + +// pre-allocated error to avoid repeated allocations +var errBufferSizeMismatch = errors.New("buffer size mismatch") + +// A system-wide packet buffer shared among sending, receiving and FEC +// to mitigate high-frequency memory allocation of packets. +var defaultBufferPool = newBufferPool(mtuLimit) + +type bufferPool struct { + xmitBuf sync.Pool +} + +// newBufferPool creates a new buffer pool with buffers of the given size. +func newBufferPool(size int) *bufferPool { + return &bufferPool{ + xmitBuf: sync.Pool{ + New: func() any { + return make([]byte, size) + }, + }, + } +} + +// Get retrieves a buffer from the pool. +func (bp *bufferPool) Get() []byte { + return bp.xmitBuf.Get().([]byte) +} + +// Put returns a buffer to the pool. +func (bp *bufferPool) Put(buf []byte) error { + // Only put back buffers of the correct size. + if cap(buf) != mtuLimit { + return errBufferSizeMismatch + } + bp.xmitBuf.Put(buf[:cap(buf)]) // reset slice length to full capacity + return nil +} diff --git a/vendor/github.com/xtaci/kcp-go/v5/bufferpool_test.go b/vendor/github.com/xtaci/kcp-go/v5/bufferpool_test.go new file mode 100644 index 00000000..a7c49e19 --- /dev/null +++ b/vendor/github.com/xtaci/kcp-go/v5/bufferpool_test.go @@ -0,0 +1,79 @@ +package kcp + +import "testing" + +func TestBufferPoolGetSize(t *testing.T) { + bp := newBufferPool(mtuLimit) + + buf := bp.Get() + + // Check length + if len(buf) != mtuLimit { + t.Fatalf("expected len=%d, got %d", mtuLimit, len(buf)) + return + } + + // Check capacity + if cap(buf) != mtuLimit { + t.Fatalf("expected cap=%d, got %d", mtuLimit, cap(buf)) + return + } +} + +func TestBufferPoolPutAndReuse(t *testing.T) { + bp := newBufferPool(mtuLimit) + + buf := bp.Get() + // Modify buffer to track it + buf[0] = 99 + + // Put back to pool + bp.Put(buf) + + // Get again; it should reuse the same buffer + buf2 := bp.Get() + + // Check if it is reused by comparing pointer address + if &buf2[0] != &buf[0] { + t.Fatalf("expected buffer reuse, but got a new one") + return + } + + if buf2[0] != 99 { + t.Fatalf("expected reused buffer to keep previous data") + return + } +} + +func TestBufferPoolPutWrongSizeIgnored(t *testing.T) { + bp := newBufferPool(mtuLimit) + + // Make a buffer with wrong capacity + wrongBuf := make([]byte, 100) + + bp.Put(wrongBuf) + + // Get should still return a buffer with mtuLimit capacity + buf := bp.Get() + + if cap(buf) != mtuLimit { + t.Fatalf("pool accepted wrong-sized buffer; expected cap=%d, got %d", mtuLimit, cap(buf)) + return + } +} + +func TestBufferPoolPutReturnsError(t *testing.T) { + bp := newBufferPool(mtuLimit) + + // 1. Correct size + buf := make([]byte, mtuLimit) + if err := bp.Put(buf); err != nil { + t.Fatalf("expected nil error for correct size, got %v", err) + } + + // 2. Incorrect size + wrongBuf := make([]byte, mtuLimit+1) + if err := bp.Put(wrongBuf); err == nil { + t.Fatalf("expected error for wrong size, got nil") + } +} diff --git a/vendor/github.com/xtaci/kcp-go/v5/crypt.go b/vendor/github.com/xtaci/kcp-go/v5/crypt.go index d8828522..60db893c 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/crypt.go +++ b/vendor/github.com/xtaci/kcp-go/v5/crypt.go @@ -1,3 +1,25 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + package kcp import ( @@ -5,9 +27,9 @@ import ( "crypto/cipher" "crypto/des" "crypto/sha1" - "unsafe" + "crypto/subtle" + "sync" - xor "github.com/templexxx/xorsimd" "github.com/tjfoc/gmsm/sm4" "golang.org/x/crypto/blowfish" @@ -20,6 +42,11 @@ import ( ) var ( + // a defined initial vector + // https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Initialization_vector_.28IV.29 + // https://en.wikipedia.org/wiki/Initialization_vector + // actually initial vector is not used in this package, we prepend a random nonce to each outgoing packets. + // though IV is fixed, the first 8 bytes of the encrypted data is always random. initialVector = []byte{167, 115, 79, 156, 18, 172, 27, 1, 164, 21, 242, 193, 252, 120, 230, 107} saltxor = `sH3CIVoF#rWLtJo6` ) @@ -37,6 +64,102 @@ type BlockCrypt interface { Decrypt(dst, src []byte) } +var _ BlockCrypt = &aeadCrypt{} + +// aeadCrypt implements BlockCrypt interface using cipher.AEAD +type aeadCrypt struct { + aead cipher.AEAD +} + +func (aeadCrypt) Encrypt(_, _ []byte) { + panic("called Encrypt on AEAD crypt") +} + +func (aeadCrypt) Decrypt(_, _ []byte) { + panic("called Decrypt on AEAD crypt") +} + +func (a *aeadCrypt) Seal(dst, nonce, plaintext, additionalData []byte) []byte { + if dst == nil || cap(dst)-len(dst) < len(plaintext)+a.aead.Overhead() { + panic("AEAD Seal allocated new slice, please increase MTU size") + } + + return a.aead.Seal(dst, nonce, plaintext, additionalData) +} + +func (a *aeadCrypt) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) { + return a.aead.Open(dst, nonce, ciphertext, additionalData) +} + +func (a *aeadCrypt) NonceSize() int { + return a.aead.NonceSize() +} + +func (a *aeadCrypt) Overhead() int { + return a.aead.Overhead() +} + +// NewAEADCrypt creates an AEAD BlockCrypt instance from an existing cipher.AEAD +func NewAEADCrypt(aead cipher.AEAD) BlockCrypt { + if aead == nil { + return nil + } + return &aeadCrypt{aead} +} + +// NewAESGCMCrypt creates an AEAD BlockCrypt instance using AES-GCM +// key must be either 16, 24, or 32 bytes to select +// AES-128, AES-192, or AES-256. +func NewAESGCMCrypt(key []byte) (BlockCrypt, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + return &aeadCrypt{aesgcm}, nil +} + +var _ BlockCrypt = &blockCrypt{} + +// blockCrypt implements BlockCrypt interface using a cipher.Block +type blockCrypt struct { + encMu sync.Mutex + decMu sync.Mutex + encbuf []byte // encryption working buffer + decbuf []byte // decryption working buffer + block cipher.Block + blockSize int // cached block size +} + +//go:nosplit +func (c *blockCrypt) Encrypt(dst, src []byte) { + c.encMu.Lock() + encrypt(c.block, dst, src, c.encbuf) + c.encMu.Unlock() +} + +//go:nosplit +func (c *blockCrypt) Decrypt(dst, src []byte) { + c.decMu.Lock() + decrypt(c.block, dst, src, c.decbuf) + c.decMu.Unlock() +} + +func newBlockCrypt(block cipher.Block) BlockCrypt { + blockSize := block.BlockSize() + return &blockCrypt{ + block: block, + blockSize: blockSize, + encbuf: make([]byte, blockSize), + decbuf: make([]byte, 2*blockSize), + } +} + type salsa20BlockCrypt struct { key [32]byte } @@ -48,175 +171,100 @@ func NewSalsa20BlockCrypt(key []byte) (BlockCrypt, error) { return c, nil } +//go:nosplit func (c *salsa20BlockCrypt) Encrypt(dst, src []byte) { + if len(src) < 8 { + return + } salsa20.XORKeyStream(dst[8:], src[8:], src[:8], &c.key) - copy(dst[:8], src[:8]) + if &dst[0] != &src[0] { + copy(dst[:8], src[:8]) + } } + +//go:nosplit func (c *salsa20BlockCrypt) Decrypt(dst, src []byte) { + if len(src) < 8 { + return + } salsa20.XORKeyStream(dst[8:], src[8:], src[:8], &c.key) - copy(dst[:8], src[:8]) -} - -type sm4BlockCrypt struct { - encbuf [sm4.BlockSize]byte // 64bit alignment enc/dec buffer - decbuf [2 * sm4.BlockSize]byte - block cipher.Block + if &dst[0] != &src[0] { + copy(dst[:8], src[:8]) + } } // NewSM4BlockCrypt https://github.com/tjfoc/gmsm/tree/master/sm4 func NewSM4BlockCrypt(key []byte) (BlockCrypt, error) { - c := new(sm4BlockCrypt) block, err := sm4.NewCipher(key) if err != nil { return nil, err } - c.block = block - return c, nil -} - -func (c *sm4BlockCrypt) Encrypt(dst, src []byte) { encrypt(c.block, dst, src, c.encbuf[:]) } -func (c *sm4BlockCrypt) Decrypt(dst, src []byte) { decrypt(c.block, dst, src, c.decbuf[:]) } - -type twofishBlockCrypt struct { - encbuf [twofish.BlockSize]byte - decbuf [2 * twofish.BlockSize]byte - block cipher.Block + return newBlockCrypt(block), nil } // NewTwofishBlockCrypt https://en.wikipedia.org/wiki/Twofish func NewTwofishBlockCrypt(key []byte) (BlockCrypt, error) { - c := new(twofishBlockCrypt) block, err := twofish.NewCipher(key) if err != nil { return nil, err } - c.block = block - return c, nil -} - -func (c *twofishBlockCrypt) Encrypt(dst, src []byte) { encrypt(c.block, dst, src, c.encbuf[:]) } -func (c *twofishBlockCrypt) Decrypt(dst, src []byte) { decrypt(c.block, dst, src, c.decbuf[:]) } - -type tripleDESBlockCrypt struct { - encbuf [des.BlockSize]byte - decbuf [2 * des.BlockSize]byte - block cipher.Block + return newBlockCrypt(block), nil } // NewTripleDESBlockCrypt https://en.wikipedia.org/wiki/Triple_DES func NewTripleDESBlockCrypt(key []byte) (BlockCrypt, error) { - c := new(tripleDESBlockCrypt) block, err := des.NewTripleDESCipher(key) if err != nil { return nil, err } - c.block = block - return c, nil -} - -func (c *tripleDESBlockCrypt) Encrypt(dst, src []byte) { encrypt(c.block, dst, src, c.encbuf[:]) } -func (c *tripleDESBlockCrypt) Decrypt(dst, src []byte) { decrypt(c.block, dst, src, c.decbuf[:]) } - -type cast5BlockCrypt struct { - encbuf [cast5.BlockSize]byte - decbuf [2 * cast5.BlockSize]byte - block cipher.Block + return newBlockCrypt(block), nil } // NewCast5BlockCrypt https://en.wikipedia.org/wiki/CAST-128 func NewCast5BlockCrypt(key []byte) (BlockCrypt, error) { - c := new(cast5BlockCrypt) block, err := cast5.NewCipher(key) if err != nil { return nil, err } - c.block = block - return c, nil -} - -func (c *cast5BlockCrypt) Encrypt(dst, src []byte) { encrypt(c.block, dst, src, c.encbuf[:]) } -func (c *cast5BlockCrypt) Decrypt(dst, src []byte) { decrypt(c.block, dst, src, c.decbuf[:]) } - -type blowfishBlockCrypt struct { - encbuf [blowfish.BlockSize]byte - decbuf [2 * blowfish.BlockSize]byte - block cipher.Block + return newBlockCrypt(block), nil } // NewBlowfishBlockCrypt https://en.wikipedia.org/wiki/Blowfish_(cipher) func NewBlowfishBlockCrypt(key []byte) (BlockCrypt, error) { - c := new(blowfishBlockCrypt) block, err := blowfish.NewCipher(key) if err != nil { return nil, err } - c.block = block - return c, nil -} - -func (c *blowfishBlockCrypt) Encrypt(dst, src []byte) { encrypt(c.block, dst, src, c.encbuf[:]) } -func (c *blowfishBlockCrypt) Decrypt(dst, src []byte) { decrypt(c.block, dst, src, c.decbuf[:]) } - -type aesBlockCrypt struct { - encbuf [aes.BlockSize]byte - decbuf [2 * aes.BlockSize]byte - block cipher.Block + return newBlockCrypt(block), nil } // NewAESBlockCrypt https://en.wikipedia.org/wiki/Advanced_Encryption_Standard func NewAESBlockCrypt(key []byte) (BlockCrypt, error) { - c := new(aesBlockCrypt) block, err := aes.NewCipher(key) if err != nil { return nil, err } - c.block = block - return c, nil -} - -func (c *aesBlockCrypt) Encrypt(dst, src []byte) { encrypt(c.block, dst, src, c.encbuf[:]) } -func (c *aesBlockCrypt) Decrypt(dst, src []byte) { decrypt(c.block, dst, src, c.decbuf[:]) } - -type teaBlockCrypt struct { - encbuf [tea.BlockSize]byte - decbuf [2 * tea.BlockSize]byte - block cipher.Block + return newBlockCrypt(block), nil } // NewTEABlockCrypt https://en.wikipedia.org/wiki/Tiny_Encryption_Algorithm func NewTEABlockCrypt(key []byte) (BlockCrypt, error) { - c := new(teaBlockCrypt) block, err := tea.NewCipherWithRounds(key, 16) if err != nil { return nil, err } - c.block = block - return c, nil -} - -func (c *teaBlockCrypt) Encrypt(dst, src []byte) { encrypt(c.block, dst, src, c.encbuf[:]) } -func (c *teaBlockCrypt) Decrypt(dst, src []byte) { decrypt(c.block, dst, src, c.decbuf[:]) } - -type xteaBlockCrypt struct { - encbuf [xtea.BlockSize]byte - decbuf [2 * xtea.BlockSize]byte - block cipher.Block + return newBlockCrypt(block), nil } // NewXTEABlockCrypt https://en.wikipedia.org/wiki/XTEA func NewXTEABlockCrypt(key []byte) (BlockCrypt, error) { - c := new(xteaBlockCrypt) block, err := xtea.NewCipher(key) if err != nil { return nil, err } - c.block = block - return c, nil + return newBlockCrypt(block), nil } -func (c *xteaBlockCrypt) Encrypt(dst, src []byte) { encrypt(c.block, dst, src, c.encbuf[:]) } -func (c *xteaBlockCrypt) Decrypt(dst, src []byte) { decrypt(c.block, dst, src, c.decbuf[:]) } - type simpleXORBlockCrypt struct { xortbl []byte } @@ -228,8 +276,18 @@ func NewSimpleXORBlockCrypt(key []byte) (BlockCrypt, error) { return c, nil } -func (c *simpleXORBlockCrypt) Encrypt(dst, src []byte) { xor.Bytes(dst, src, c.xortbl) } -func (c *simpleXORBlockCrypt) Decrypt(dst, src []byte) { xor.Bytes(dst, src, c.xortbl) } +func (c *simpleXORBlockCrypt) Encrypt(dst, src []byte) { + if len(src) == 0 { + return + } + subtle.XORBytes(dst, src, c.xortbl) +} +func (c *simpleXORBlockCrypt) Decrypt(dst, src []byte) { + if len(src) == 0 { + return + } + subtle.XORBytes(dst, src, c.xortbl) +} type noneBlockCrypt struct{} @@ -238,10 +296,40 @@ func NewNoneBlockCrypt(key []byte) (BlockCrypt, error) { return new(noneBlockCrypt), nil } -func (c *noneBlockCrypt) Encrypt(dst, src []byte) { copy(dst, src) } -func (c *noneBlockCrypt) Decrypt(dst, src []byte) { copy(dst, src) } +//go:nosplit +func (c *noneBlockCrypt) Encrypt(dst, src []byte) { + if len(src) == 0 { + return + } + if &dst[0] != &src[0] { + copy(dst, src) + } +} -// packet encryption with local CFB mode +//go:nosplit +func (c *noneBlockCrypt) Decrypt(dst, src []byte) { + if len(src) == 0 { + return + } + if &dst[0] != &src[0] { + copy(dst, src) + } +} + +// ----------------------------------------------------------------------- +// CFB-mode encryption/decryption +// ----------------------------------------------------------------------- +// +// For block ciphers (non-AEAD), packets are encrypted using a local variant +// of CFB (Cipher Feedback) mode. The encrypt/decrypt functions below are +// hand-optimized with 8x loop unrolling to reduce data dependencies +// between consecutive block cipher calls. +// +// Two block sizes are supported: +// - 8-byte blocks (e.g. Blowfish, CAST5, DES, TEA, XTEA) +// - 16-byte blocks (e.g. AES, Twofish, SM4) + +// encrypt dispatches to the appropriate block-size-specific CFB encryptor. func encrypt(block cipher.Block, dst, src, buf []byte) { switch block.BlockSize() { case 8: @@ -253,167 +341,168 @@ func encrypt(block cipher.Block, dst, src, buf []byte) { } } -// optimized encryption for the ciphers which works in 8-bytes +// encrypt8 performs CFB encryption for 8-byte block ciphers. +// Uses 8x loop unrolling for throughput optimization. func encrypt8(block cipher.Block, dst, src, buf []byte) { tbl := buf[:8] block.Encrypt(tbl, initialVector) - n := len(src) / 8 + n := len(src) >> 3 // number of full 8-byte blocks base := 0 - repeat := n / 8 - left := n % 8 - ptr_tbl := (*uint64)(unsafe.Pointer(&tbl[0])) + repeat := n >> 3 // number of 8-block groups (64 bytes each) + left := n & 7 // remaining blocks after groups - for i := 0; i < repeat; i++ { + for range repeat { s := src[base:][0:64] d := dst[base:][0:64] // 1 - *(*uint64)(unsafe.Pointer(&d[0])) = *(*uint64)(unsafe.Pointer(&s[0])) ^ *ptr_tbl + subtle.XORBytes(d[0:8], s[0:8], tbl) block.Encrypt(tbl, d[0:8]) // 2 - *(*uint64)(unsafe.Pointer(&d[8])) = *(*uint64)(unsafe.Pointer(&s[8])) ^ *ptr_tbl + subtle.XORBytes(d[8:16], s[8:16], tbl) block.Encrypt(tbl, d[8:16]) // 3 - *(*uint64)(unsafe.Pointer(&d[16])) = *(*uint64)(unsafe.Pointer(&s[16])) ^ *ptr_tbl + subtle.XORBytes(d[16:24], s[16:24], tbl) block.Encrypt(tbl, d[16:24]) // 4 - *(*uint64)(unsafe.Pointer(&d[24])) = *(*uint64)(unsafe.Pointer(&s[24])) ^ *ptr_tbl + subtle.XORBytes(d[24:32], s[24:32], tbl) block.Encrypt(tbl, d[24:32]) // 5 - *(*uint64)(unsafe.Pointer(&d[32])) = *(*uint64)(unsafe.Pointer(&s[32])) ^ *ptr_tbl + subtle.XORBytes(d[32:40], s[32:40], tbl) block.Encrypt(tbl, d[32:40]) // 6 - *(*uint64)(unsafe.Pointer(&d[40])) = *(*uint64)(unsafe.Pointer(&s[40])) ^ *ptr_tbl + subtle.XORBytes(d[40:48], s[40:48], tbl) block.Encrypt(tbl, d[40:48]) // 7 - *(*uint64)(unsafe.Pointer(&d[48])) = *(*uint64)(unsafe.Pointer(&s[48])) ^ *ptr_tbl + subtle.XORBytes(d[48:56], s[48:56], tbl) block.Encrypt(tbl, d[48:56]) // 8 - *(*uint64)(unsafe.Pointer(&d[56])) = *(*uint64)(unsafe.Pointer(&s[56])) ^ *ptr_tbl + subtle.XORBytes(d[56:64], s[56:64], tbl) block.Encrypt(tbl, d[56:64]) base += 64 } switch left { case 7: - *(*uint64)(unsafe.Pointer(&dst[base])) = *(*uint64)(unsafe.Pointer(&src[base])) ^ *ptr_tbl - block.Encrypt(tbl, dst[base:]) + subtle.XORBytes(dst[base:base+8], src[base:base+8], tbl) + block.Encrypt(tbl, dst[base:base+8]) base += 8 fallthrough case 6: - *(*uint64)(unsafe.Pointer(&dst[base])) = *(*uint64)(unsafe.Pointer(&src[base])) ^ *ptr_tbl - block.Encrypt(tbl, dst[base:]) + subtle.XORBytes(dst[base:base+8], src[base:base+8], tbl) + block.Encrypt(tbl, dst[base:base+8]) base += 8 fallthrough case 5: - *(*uint64)(unsafe.Pointer(&dst[base])) = *(*uint64)(unsafe.Pointer(&src[base])) ^ *ptr_tbl - block.Encrypt(tbl, dst[base:]) + subtle.XORBytes(dst[base:base+8], src[base:base+8], tbl) + block.Encrypt(tbl, dst[base:base+8]) base += 8 fallthrough case 4: - *(*uint64)(unsafe.Pointer(&dst[base])) = *(*uint64)(unsafe.Pointer(&src[base])) ^ *ptr_tbl - block.Encrypt(tbl, dst[base:]) + subtle.XORBytes(dst[base:base+8], src[base:base+8], tbl) + block.Encrypt(tbl, dst[base:base+8]) base += 8 fallthrough case 3: - *(*uint64)(unsafe.Pointer(&dst[base])) = *(*uint64)(unsafe.Pointer(&src[base])) ^ *ptr_tbl - block.Encrypt(tbl, dst[base:]) + subtle.XORBytes(dst[base:base+8], src[base:base+8], tbl) + block.Encrypt(tbl, dst[base:base+8]) base += 8 fallthrough case 2: - *(*uint64)(unsafe.Pointer(&dst[base])) = *(*uint64)(unsafe.Pointer(&src[base])) ^ *ptr_tbl - block.Encrypt(tbl, dst[base:]) + subtle.XORBytes(dst[base:base+8], src[base:base+8], tbl) + block.Encrypt(tbl, dst[base:base+8]) base += 8 fallthrough case 1: - *(*uint64)(unsafe.Pointer(&dst[base])) = *(*uint64)(unsafe.Pointer(&src[base])) ^ *ptr_tbl - block.Encrypt(tbl, dst[base:]) + subtle.XORBytes(dst[base:base+8], src[base:base+8], tbl) + block.Encrypt(tbl, dst[base:base+8]) base += 8 fallthrough case 0: - xorBytes(dst[base:], src[base:], tbl) + subtle.XORBytes(dst[base:], src[base:], tbl) } } -// optimized encryption for the ciphers which works in 16-bytes +// encrypt16 performs CFB encryption for 16-byte block ciphers. +// Uses 8x loop unrolling for throughput optimization. func encrypt16(block cipher.Block, dst, src, buf []byte) { tbl := buf[:16] block.Encrypt(tbl, initialVector) - n := len(src) / 16 + n := len(src) >> 4 // number of full 16-byte blocks base := 0 - repeat := n / 8 - left := n % 8 - for i := 0; i < repeat; i++ { + repeat := n >> 3 // number of 8-block groups (128 bytes each) + left := n & 7 // remaining blocks after groups + for range repeat { s := src[base:][0:128] d := dst[base:][0:128] // 1 - xor.Bytes16Align(d[0:16], s[0:16], tbl) + subtle.XORBytes(d[0:16], s[0:16], tbl) block.Encrypt(tbl, d[0:16]) // 2 - xor.Bytes16Align(d[16:32], s[16:32], tbl) + subtle.XORBytes(d[16:32], s[16:32], tbl) block.Encrypt(tbl, d[16:32]) // 3 - xor.Bytes16Align(d[32:48], s[32:48], tbl) + subtle.XORBytes(d[32:48], s[32:48], tbl) block.Encrypt(tbl, d[32:48]) // 4 - xor.Bytes16Align(d[48:64], s[48:64], tbl) + subtle.XORBytes(d[48:64], s[48:64], tbl) block.Encrypt(tbl, d[48:64]) // 5 - xor.Bytes16Align(d[64:80], s[64:80], tbl) + subtle.XORBytes(d[64:80], s[64:80], tbl) block.Encrypt(tbl, d[64:80]) // 6 - xor.Bytes16Align(d[80:96], s[80:96], tbl) + subtle.XORBytes(d[80:96], s[80:96], tbl) block.Encrypt(tbl, d[80:96]) // 7 - xor.Bytes16Align(d[96:112], s[96:112], tbl) + subtle.XORBytes(d[96:112], s[96:112], tbl) block.Encrypt(tbl, d[96:112]) // 8 - xor.Bytes16Align(d[112:128], s[112:128], tbl) + subtle.XORBytes(d[112:128], s[112:128], tbl) block.Encrypt(tbl, d[112:128]) base += 128 } switch left { case 7: - xor.Bytes16Align(dst[base:], src[base:], tbl) + subtle.XORBytes(dst[base:], src[base:], tbl) block.Encrypt(tbl, dst[base:]) base += 16 fallthrough case 6: - xor.Bytes16Align(dst[base:], src[base:], tbl) + subtle.XORBytes(dst[base:], src[base:], tbl) block.Encrypt(tbl, dst[base:]) base += 16 fallthrough case 5: - xor.Bytes16Align(dst[base:], src[base:], tbl) + subtle.XORBytes(dst[base:], src[base:], tbl) block.Encrypt(tbl, dst[base:]) base += 16 fallthrough case 4: - xor.Bytes16Align(dst[base:], src[base:], tbl) + subtle.XORBytes(dst[base:], src[base:], tbl) block.Encrypt(tbl, dst[base:]) base += 16 fallthrough case 3: - xor.Bytes16Align(dst[base:], src[base:], tbl) + subtle.XORBytes(dst[base:], src[base:], tbl) block.Encrypt(tbl, dst[base:]) base += 16 fallthrough case 2: - xor.Bytes16Align(dst[base:], src[base:], tbl) + subtle.XORBytes(dst[base:], src[base:], tbl) block.Encrypt(tbl, dst[base:]) base += 16 fallthrough case 1: - xor.Bytes16Align(dst[base:], src[base:], tbl) + subtle.XORBytes(dst[base:], src[base:], tbl) block.Encrypt(tbl, dst[base:]) base += 16 fallthrough case 0: - xorBytes(dst[base:], src[base:], tbl) + subtle.XORBytes(dst[base:], src[base:], tbl) } } -// decryption +// decrypt dispatches to the appropriate block-size-specific CFB decryptor. func decrypt(block cipher.Block, dst, src, buf []byte) { switch block.BlockSize() { case 8: @@ -425,194 +514,183 @@ func decrypt(block cipher.Block, dst, src, buf []byte) { } } -// decrypt 8 bytes block, all byte slices are supposed to be 64bit aligned +// decrypt8 performs CFB decryption for 8-byte block ciphers. +// Uses double-buffering (tbl/next) with 8x loop unrolling +// to break the data dependency chain between consecutive blocks. func decrypt8(block cipher.Block, dst, src, buf []byte) { tbl := buf[0:8] next := buf[8:16] block.Encrypt(tbl, initialVector) - n := len(src) / 8 + n := len(src) >> 3 // number of full 8-byte blocks base := 0 - repeat := n / 8 - left := n % 8 - ptr_tbl := (*uint64)(unsafe.Pointer(&tbl[0])) - ptr_next := (*uint64)(unsafe.Pointer(&next[0])) + repeat := n >> 3 // number of 8-block groups (64 bytes each) + left := n & 7 // remaining blocks after groups - for i := 0; i < repeat; i++ { + // 8x loop unrolling: alternates tbl/next to relieve data dependency + for range repeat { s := src[base:][0:64] d := dst[base:][0:64] // 1 block.Encrypt(next, s[0:8]) - *(*uint64)(unsafe.Pointer(&d[0])) = *(*uint64)(unsafe.Pointer(&s[0])) ^ *ptr_tbl + subtle.XORBytes(d[0:8], s[0:8], tbl) // 2 block.Encrypt(tbl, s[8:16]) - *(*uint64)(unsafe.Pointer(&d[8])) = *(*uint64)(unsafe.Pointer(&s[8])) ^ *ptr_next + subtle.XORBytes(d[8:16], s[8:16], next) // 3 block.Encrypt(next, s[16:24]) - *(*uint64)(unsafe.Pointer(&d[16])) = *(*uint64)(unsafe.Pointer(&s[16])) ^ *ptr_tbl + subtle.XORBytes(d[16:24], s[16:24], tbl) // 4 block.Encrypt(tbl, s[24:32]) - *(*uint64)(unsafe.Pointer(&d[24])) = *(*uint64)(unsafe.Pointer(&s[24])) ^ *ptr_next + subtle.XORBytes(d[24:32], s[24:32], next) // 5 block.Encrypt(next, s[32:40]) - *(*uint64)(unsafe.Pointer(&d[32])) = *(*uint64)(unsafe.Pointer(&s[32])) ^ *ptr_tbl + subtle.XORBytes(d[32:40], s[32:40], tbl) // 6 block.Encrypt(tbl, s[40:48]) - *(*uint64)(unsafe.Pointer(&d[40])) = *(*uint64)(unsafe.Pointer(&s[40])) ^ *ptr_next + subtle.XORBytes(d[40:48], s[40:48], next) // 7 block.Encrypt(next, s[48:56]) - *(*uint64)(unsafe.Pointer(&d[48])) = *(*uint64)(unsafe.Pointer(&s[48])) ^ *ptr_tbl + subtle.XORBytes(d[48:56], s[48:56], tbl) // 8 block.Encrypt(tbl, s[56:64]) - *(*uint64)(unsafe.Pointer(&d[56])) = *(*uint64)(unsafe.Pointer(&s[56])) ^ *ptr_next + subtle.XORBytes(d[56:64], s[56:64], next) base += 64 } switch left { case 7: - block.Encrypt(next, src[base:]) - *(*uint64)(unsafe.Pointer(&dst[base])) = *(*uint64)(unsafe.Pointer(&src[base])) ^ *(*uint64)(unsafe.Pointer(&tbl[0])) + block.Encrypt(next, src[base:base+8]) + subtle.XORBytes(dst[base:base+8], src[base:base+8], tbl) tbl, next = next, tbl base += 8 fallthrough case 6: - block.Encrypt(next, src[base:]) - *(*uint64)(unsafe.Pointer(&dst[base])) = *(*uint64)(unsafe.Pointer(&src[base])) ^ *(*uint64)(unsafe.Pointer(&tbl[0])) + block.Encrypt(next, src[base:base+8]) + subtle.XORBytes(dst[base:base+8], src[base:base+8], tbl) tbl, next = next, tbl base += 8 fallthrough case 5: - block.Encrypt(next, src[base:]) - *(*uint64)(unsafe.Pointer(&dst[base])) = *(*uint64)(unsafe.Pointer(&src[base])) ^ *(*uint64)(unsafe.Pointer(&tbl[0])) + block.Encrypt(next, src[base:base+8]) + subtle.XORBytes(dst[base:base+8], src[base:base+8], tbl) tbl, next = next, tbl base += 8 fallthrough case 4: - block.Encrypt(next, src[base:]) - *(*uint64)(unsafe.Pointer(&dst[base])) = *(*uint64)(unsafe.Pointer(&src[base])) ^ *(*uint64)(unsafe.Pointer(&tbl[0])) + block.Encrypt(next, src[base:base+8]) + subtle.XORBytes(dst[base:base+8], src[base:base+8], tbl) tbl, next = next, tbl base += 8 fallthrough case 3: - block.Encrypt(next, src[base:]) - *(*uint64)(unsafe.Pointer(&dst[base])) = *(*uint64)(unsafe.Pointer(&src[base])) ^ *(*uint64)(unsafe.Pointer(&tbl[0])) + block.Encrypt(next, src[base:base+8]) + subtle.XORBytes(dst[base:base+8], src[base:base+8], tbl) tbl, next = next, tbl base += 8 fallthrough case 2: - block.Encrypt(next, src[base:]) - *(*uint64)(unsafe.Pointer(&dst[base])) = *(*uint64)(unsafe.Pointer(&src[base])) ^ *(*uint64)(unsafe.Pointer(&tbl[0])) + block.Encrypt(next, src[base:base+8]) + subtle.XORBytes(dst[base:base+8], src[base:base+8], tbl) tbl, next = next, tbl base += 8 fallthrough case 1: - block.Encrypt(next, src[base:]) - *(*uint64)(unsafe.Pointer(&dst[base])) = *(*uint64)(unsafe.Pointer(&src[base])) ^ *(*uint64)(unsafe.Pointer(&tbl[0])) + block.Encrypt(next, src[base:base+8]) + subtle.XORBytes(dst[base:base+8], src[base:base+8], tbl) tbl, next = next, tbl base += 8 fallthrough case 0: - xorBytes(dst[base:], src[base:], tbl) + subtle.XORBytes(dst[base:], src[base:], tbl) } } +// decrypt16 performs CFB decryption for 16-byte block ciphers. +// Uses double-buffering (tbl/next) with 8x loop unrolling. func decrypt16(block cipher.Block, dst, src, buf []byte) { tbl := buf[0:16] next := buf[16:32] block.Encrypt(tbl, initialVector) - n := len(src) / 16 + n := len(src) >> 4 // number of full 16-byte blocks base := 0 - repeat := n / 8 - left := n % 8 - for i := 0; i < repeat; i++ { + repeat := n >> 3 // number of 8-block groups (128 bytes each) + left := n & 7 // remaining blocks after groups + + // 8x loop unrolling: alternates tbl/next to relieve data dependency + for range repeat { s := src[base:][0:128] d := dst[base:][0:128] // 1 block.Encrypt(next, s[0:16]) - xor.Bytes16Align(d[0:16], s[0:16], tbl) + subtle.XORBytes(d[0:16], s[0:16], tbl) // 2 block.Encrypt(tbl, s[16:32]) - xor.Bytes16Align(d[16:32], s[16:32], next) + subtle.XORBytes(d[16:32], s[16:32], next) // 3 block.Encrypt(next, s[32:48]) - xor.Bytes16Align(d[32:48], s[32:48], tbl) + subtle.XORBytes(d[32:48], s[32:48], tbl) // 4 block.Encrypt(tbl, s[48:64]) - xor.Bytes16Align(d[48:64], s[48:64], next) + subtle.XORBytes(d[48:64], s[48:64], next) // 5 block.Encrypt(next, s[64:80]) - xor.Bytes16Align(d[64:80], s[64:80], tbl) + subtle.XORBytes(d[64:80], s[64:80], tbl) // 6 block.Encrypt(tbl, s[80:96]) - xor.Bytes16Align(d[80:96], s[80:96], next) + subtle.XORBytes(d[80:96], s[80:96], next) // 7 block.Encrypt(next, s[96:112]) - xor.Bytes16Align(d[96:112], s[96:112], tbl) + subtle.XORBytes(d[96:112], s[96:112], tbl) // 8 block.Encrypt(tbl, s[112:128]) - xor.Bytes16Align(d[112:128], s[112:128], next) + subtle.XORBytes(d[112:128], s[112:128], next) base += 128 } switch left { case 7: block.Encrypt(next, src[base:]) - xor.Bytes16Align(dst[base:], src[base:], tbl) + subtle.XORBytes(dst[base:], src[base:], tbl) tbl, next = next, tbl base += 16 fallthrough case 6: block.Encrypt(next, src[base:]) - xor.Bytes16Align(dst[base:], src[base:], tbl) + subtle.XORBytes(dst[base:], src[base:], tbl) tbl, next = next, tbl base += 16 fallthrough case 5: block.Encrypt(next, src[base:]) - xor.Bytes16Align(dst[base:], src[base:], tbl) + subtle.XORBytes(dst[base:], src[base:], tbl) tbl, next = next, tbl base += 16 fallthrough case 4: block.Encrypt(next, src[base:]) - xor.Bytes16Align(dst[base:], src[base:], tbl) + subtle.XORBytes(dst[base:], src[base:], tbl) tbl, next = next, tbl base += 16 fallthrough case 3: block.Encrypt(next, src[base:]) - xor.Bytes16Align(dst[base:], src[base:], tbl) + subtle.XORBytes(dst[base:], src[base:], tbl) tbl, next = next, tbl base += 16 fallthrough case 2: block.Encrypt(next, src[base:]) - xor.Bytes16Align(dst[base:], src[base:], tbl) + subtle.XORBytes(dst[base:], src[base:], tbl) tbl, next = next, tbl base += 16 fallthrough case 1: block.Encrypt(next, src[base:]) - xor.Bytes16Align(dst[base:], src[base:], tbl) + subtle.XORBytes(dst[base:], src[base:], tbl) tbl, next = next, tbl base += 16 fallthrough case 0: - xorBytes(dst[base:], src[base:], tbl) - } -} - -// per bytes xors -func xorBytes(dst, a, b []byte) int { - n := len(a) - if len(b) < n { - n = len(b) - } - if n == 0 { - return 0 - } - - for i := 0; i < n; i++ { - dst[i] = a[i] ^ b[i] + subtle.XORBytes(dst[base:], src[base:], tbl) } - return n } diff --git a/vendor/github.com/xtaci/kcp-go/v5/crypt_test.go b/vendor/github.com/xtaci/kcp-go/v5/crypt_test.go index 2ef4dc8a..db0a8571 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/crypt_test.go +++ b/vendor/github.com/xtaci/kcp-go/v5/crypt_test.go @@ -1,20 +1,45 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + package kcp import ( "bytes" "crypto/aes" - "crypto/md5" + "crypto/cipher" "crypto/rand" - "crypto/sha1" + "encoding/binary" "hash/crc32" "io" "testing" + + "golang.org/x/crypto/chacha20poly1305" ) func TestSM4(t *testing.T) { bc, err := NewSM4BlockCrypt(pass[:16]) if err != nil { t.Fatal(err) + return } cryptTest(t, bc) } @@ -23,6 +48,7 @@ func TestAES(t *testing.T) { bc, err := NewAESBlockCrypt(pass[:32]) if err != nil { t.Fatal(err) + return } cryptTest(t, bc) } @@ -31,6 +57,7 @@ func TestTEA(t *testing.T) { bc, err := NewTEABlockCrypt(pass[:16]) if err != nil { t.Fatal(err) + return } cryptTest(t, bc) } @@ -39,6 +66,7 @@ func TestXOR(t *testing.T) { bc, err := NewSimpleXORBlockCrypt(pass[:32]) if err != nil { t.Fatal(err) + return } cryptTest(t, bc) } @@ -47,6 +75,7 @@ func TestBlowfish(t *testing.T) { bc, err := NewBlowfishBlockCrypt(pass[:32]) if err != nil { t.Fatal(err) + return } cryptTest(t, bc) } @@ -55,6 +84,7 @@ func TestNone(t *testing.T) { bc, err := NewNoneBlockCrypt(pass[:32]) if err != nil { t.Fatal(err) + return } cryptTest(t, bc) } @@ -63,6 +93,7 @@ func TestCast5(t *testing.T) { bc, err := NewCast5BlockCrypt(pass[:16]) if err != nil { t.Fatal(err) + return } cryptTest(t, bc) } @@ -71,6 +102,7 @@ func Test3DES(t *testing.T) { bc, err := NewTripleDESBlockCrypt(pass[:24]) if err != nil { t.Fatal(err) + return } cryptTest(t, bc) } @@ -79,6 +111,7 @@ func TestTwofish(t *testing.T) { bc, err := NewTwofishBlockCrypt(pass[:32]) if err != nil { t.Fatal(err) + return } cryptTest(t, bc) } @@ -87,6 +120,7 @@ func TestXTEA(t *testing.T) { bc, err := NewXTEABlockCrypt(pass[:16]) if err != nil { t.Fatal(err) + return } cryptTest(t, bc) } @@ -95,6 +129,7 @@ func TestSalsa20(t *testing.T) { bc, err := NewSalsa20BlockCrypt(pass[:32]) if err != nil { t.Fatal(err) + return } cryptTest(t, bc) } @@ -111,10 +146,77 @@ func cryptTest(t *testing.T, bc BlockCrypt) { } } +func TestAES256GCM(t *testing.T) { + bc, err := NewAESGCMCrypt(pass[:32]) + if err != nil { + t.Fatal(err) + return + } + + testAEAD(t, bc) +} + +func TestAES128GCM(t *testing.T) { + bc, err := NewAESGCMCrypt(pass[:16]) + if err != nil { + t.Fatal(err) + return + } + + testAEAD(t, bc) +} + +func testAEAD(t *testing.T, bc BlockCrypt) { + aead := bc.(*aeadCrypt) + + nonceSize := aead.NonceSize() + + size := mtuLimit - cryptHeaderSize - aead.Overhead() + data := make([]byte, size) + io.ReadFull(rand.Reader, data) + + // if the size of packet is cannot accommodate the AEAD overhead + // Open and Seal will allocate a new slice internally, we need to + // ensure that it does not happen for our MTU sized packets. + packet := make([]byte, mtuLimit) + + // Seal + dst := packet[:nonceSize] + nonce := packet[:nonceSize] + fillRand(nonce) + + sealedPacket := aead.Seal(dst, nonce, data, nil) + if &sealedPacket[0] != &packet[0] { + t.Fatal("Seal created a new slice") + return + } + + // Open + dst = sealedPacket[:nonceSize] + nonce = sealedPacket[:nonceSize] + ciphertext := sealedPacket[nonceSize:] + + decrypted, err := aead.Open(dst, nonce, ciphertext, nil) + if &decrypted[0] != &sealedPacket[0] { + t.Fatal("Open created a new slice") + return + } + + if err != nil { + t.Fatal(err) + return + } + + if !bytes.Equal(data, decrypted[nonceSize:]) { + t.Fail() + } +} + func BenchmarkSM4(b *testing.B) { bc, err := NewSM4BlockCrypt(pass[:16]) if err != nil { b.Fatal(err) + return } benchCrypt(b, bc) } @@ -123,6 +225,7 @@ func BenchmarkAES128(b *testing.B) { bc, err := NewAESBlockCrypt(pass[:16]) if err != nil { b.Fatal(err) + return } benchCrypt(b, bc) @@ -132,6 +235,7 @@ func BenchmarkAES192(b *testing.B) { bc, err := NewAESBlockCrypt(pass[:24]) if err != nil { b.Fatal(err) + return } benchCrypt(b, bc) @@ -141,6 +245,7 @@ func BenchmarkAES256(b *testing.B) { bc, err := NewAESBlockCrypt(pass[:32]) if err != nil { b.Fatal(err) + return } benchCrypt(b, bc) @@ -150,6 +255,7 @@ func BenchmarkTEA(b *testing.B) { bc, err := NewTEABlockCrypt(pass[:16]) if err != nil { b.Fatal(err) + return } benchCrypt(b, bc) } @@ -158,6 +264,7 @@ func BenchmarkXOR(b *testing.B) { bc, err := NewSimpleXORBlockCrypt(pass[:32]) if err != nil { b.Fatal(err) + return } benchCrypt(b, bc) } @@ -166,6 +273,7 @@ func BenchmarkBlowfish(b *testing.B) { bc, err := NewBlowfishBlockCrypt(pass[:32]) if err != nil { b.Fatal(err) + return } benchCrypt(b, bc) } @@ -174,6 +282,7 @@ func BenchmarkNone(b *testing.B) { bc, err := NewNoneBlockCrypt(pass[:32]) if err != nil { b.Fatal(err) + return } benchCrypt(b, bc) } @@ -182,6 +291,7 @@ func BenchmarkCast5(b *testing.B) { bc, err := NewCast5BlockCrypt(pass[:16]) if err != nil { b.Fatal(err) + return } benchCrypt(b, bc) } @@ -190,6 +300,7 @@ func Benchmark3DES(b *testing.B) { bc, err := NewTripleDESBlockCrypt(pass[:24]) if err != nil { b.Fatal(err) + return } benchCrypt(b, bc) } @@ -206,6 +317,7 @@ func BenchmarkXTEA(b *testing.B) { bc, err := NewXTEABlockCrypt(pass[:16]) if err != nil { b.Fatal(err) + return } benchCrypt(b, bc) } @@ -214,6 +326,7 @@ func BenchmarkSalsa20(b *testing.B) { bc, err := NewSalsa20BlockCrypt(pass[:32]) if err != nil { b.Fatal(err) + return } benchCrypt(b, bc) } @@ -226,8 +339,8 @@ func benchCrypt(b *testing.B, bc BlockCrypt) { b.ReportAllocs() b.SetBytes(int64(len(enc) * 2)) - b.ResetTimer() - for i := 0; i < b.N; i++ { + + for b.Loop() { bc.Encrypt(enc, data) bc.Decrypt(dec, enc) } @@ -236,54 +349,113 @@ func benchCrypt(b *testing.B, bc BlockCrypt) { func BenchmarkCRC32(b *testing.B) { content := make([]byte, 1024) b.SetBytes(int64(len(content))) - for i := 0; i < b.N; i++ { + for b.Loop() { crc32.ChecksumIEEE(content) } } -func BenchmarkCsprngSystem(b *testing.B) { - data := make([]byte, md5.Size) - b.SetBytes(int64(len(data))) +func BenchmarkCFB_AES_128_CRC32(b *testing.B) { + bc, err := NewAESBlockCrypt(pass[:16]) + if err != nil { + b.Fatal(err) + return + } + + data := make([]byte, 1400, mtuLimit) + b.SetBytes(1400) - for i := 0; i < b.N; i++ { - io.ReadFull(rand.Reader, data) + for b.Loop() { + checksum := crc32.ChecksumIEEE(data[cryptHeaderSize:]) + binary.LittleEndian.PutUint32(data[nonceSize:cryptHeaderSize], checksum) + bc.Encrypt(data, data) } } -func BenchmarkCsprngMD5(b *testing.B) { - var data [md5.Size]byte - b.SetBytes(md5.Size) +func BenchmarkAEAD_AES_128_GCM(b *testing.B) { + block, err := aes.NewCipher(pass[:16]) + if err != nil { + panic(err) + } + + aead, err := cipher.NewGCM(block) + if err != nil { + panic(err) + } + + data := make([]byte, 1400, mtuLimit) + b.SetBytes(1400) - for i := 0; i < b.N; i++ { - data = md5.Sum(data[:]) + nonce := data[:aead.NonceSize()] + plaintext := data[aead.NonceSize():] + + for b.Loop() { + aead.Seal(plaintext[:0], nonce, plaintext, nil) } } -func BenchmarkCsprngSHA1(b *testing.B) { - var data [sha1.Size]byte - b.SetBytes(sha1.Size) - for i := 0; i < b.N; i++ { - data = sha1.Sum(data[:]) +func BenchmarkCFB_Salsa20_CRC32(b *testing.B) { + bc, err := NewSalsa20BlockCrypt(pass[:32]) + if err != nil { + b.Fatal(err) + return + } + + data := make([]byte, 1400, mtuLimit) + b.SetBytes(1400) + + for b.Loop() { + checksum := crc32.ChecksumIEEE(data[cryptHeaderSize:]) + binary.LittleEndian.PutUint32(data[nonceSize:cryptHeaderSize], checksum) + bc.Encrypt(data, data) } } -func BenchmarkCsprngNonceMD5(b *testing.B) { - var ng nonceMD5 - ng.Init() - b.SetBytes(md5.Size) - data := make([]byte, md5.Size) - for i := 0; i < b.N; i++ { - ng.Fill(data) +func BenchmarkAEAD_Chacha20_Poly1035(b *testing.B) { + aead, err := chacha20poly1305.New(pass[:32]) + if err != nil { + panic(err) + } + + data := make([]byte, 1400, mtuLimit) + b.SetBytes(1400) + + nonce := data[:aead.NonceSize()] + plaintext := data[aead.NonceSize():] + + for b.Loop() { + aead.Seal(plaintext[:0], nonce, plaintext, nil) } } -func BenchmarkCsprngNonceAES128(b *testing.B) { - var ng nonceAES128 - ng.Init() +func TestCryptErrors(t *testing.T) { + invalidKey := []byte("invalid") - b.SetBytes(aes.BlockSize) - data := make([]byte, aes.BlockSize) - for i := 0; i < b.N; i++ { - ng.Fill(data) + if _, err := NewSM4BlockCrypt(invalidKey); err == nil { + t.Error("NewSM4BlockCrypt should fail with invalid key") + } + if _, err := NewTwofishBlockCrypt(invalidKey); err == nil { + t.Error("NewTwofishBlockCrypt should fail with invalid key") + } + if _, err := NewTripleDESBlockCrypt(invalidKey); err == nil { + t.Error("NewTripleDESBlockCrypt should fail with invalid key") + } + if _, err := NewCast5BlockCrypt(invalidKey); err == nil { + t.Error("NewCast5BlockCrypt should fail with invalid key") + } + // Blowfish supports variable key length, so "invalid" (7 bytes) might be valid. + // Blowfish key size: 1-56 bytes. So 7 bytes is valid. + // Let's try empty key or very long key. + if _, err := NewBlowfishBlockCrypt(nil); err == nil { + t.Error("NewBlowfishBlockCrypt should fail with nil key") + } + + if _, err := NewAESBlockCrypt(invalidKey); err == nil { + t.Error("NewAESBlockCrypt should fail with invalid key") + } + if _, err := NewTEABlockCrypt(invalidKey); err == nil { + t.Error("NewTEABlockCrypt should fail with invalid key") + } + if _, err := NewXTEABlockCrypt(invalidKey); err == nil { + t.Error("NewXTEABlockCrypt should fail with invalid key") } } diff --git a/vendor/github.com/xtaci/kcp-go/v5/entropy.go b/vendor/github.com/xtaci/kcp-go/v5/entropy.go index 156c1cd2..d2ae21a0 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/entropy.go +++ b/vendor/github.com/xtaci/kcp-go/v5/entropy.go @@ -1,52 +1,171 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + package kcp import ( "crypto/aes" "crypto/cipher" - "crypto/md5" - "crypto/rand" + crand "crypto/rand" "io" + "math/rand/v2" + "runtime" + "sync" + + "golang.org/x/sys/cpu" ) -// Entropy defines a entropy source -type Entropy interface { - Init() - Fill(nonce []byte) -} +const reseedInterval = 1 << 24 // reseed after ~16M reads to limit key exposure + +var ( + hasAESAsmAMD64 = cpu.X86.HasAES && cpu.X86.HasSSE41 && cpu.X86.HasSSSE3 + hasAESAsmARM64 = cpu.ARM64.HasAES + hasAESAsmS390X = cpu.S390X.HasAES + hasAESAsmPPC64 = runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le" + + hasAESHardwareSupport = hasAESAsmAMD64 || hasAESAsmARM64 || hasAESAsmS390X || hasAESAsmPPC64 + + entropy io.Reader = NewEntropy() +) + +// NewEntropy creates a new entropy source. +func NewEntropy() io.Reader { + if hasAESHardwareSupport { + return NewEntropyAES() + } -// nonceMD5 nonce generator for packet header -type nonceMD5 struct { - seed [md5.Size]byte + return NewEntropyChacha8() } -func (n *nonceMD5) Init() { /*nothing required*/ } +// SetEntropy sets the global entropy source used by fillRand. +func SetEntropy(r io.Reader) { + entropy = r +} -func (n *nonceMD5) Fill(nonce []byte) { - if n.seed[0] == 0 { // entropy update - io.ReadFull(rand.Reader, n.seed[:]) +// fillRand fills p with random data from the global entropy source. +func fillRand(p []byte) { + if len(p) <= 0 { + return } - n.seed = md5.Sum(n.seed[:]) - copy(nonce, n.seed[:]) + io.ReadFull(entropy, p) } -// nonceAES128 nonce generator for packet headers -type nonceAES128 struct { - seed [aes.BlockSize]byte +// rngAES is an AES-based random number generator. +type rngAES struct { + mutex sync.Mutex block cipher.Block + seed [16]byte + count uint64 } -func (n *nonceAES128) Init() { - var key [16]byte //aes-128 - io.ReadFull(rand.Reader, key[:]) - io.ReadFull(rand.Reader, n.seed[:]) - block, _ := aes.NewCipher(key[:]) - n.block = block +// NewEntropyAES creates a new AES-based entropy source. +func NewEntropyAES() io.Reader { + r := new(rngAES) + + var key [16]byte + io.ReadFull(crand.Reader, key[:]) + io.ReadFull(crand.Reader, r.seed[:]) + + block, err := aes.NewCipher(key[:]) + if err != nil { + panic(err) + } + r.block = block + return r } -func (n *nonceAES128) Fill(nonce []byte) { - if n.seed[0] == 0 { // entropy update - io.ReadFull(rand.Reader, n.seed[:]) +// updateSeed updates the AES seed after a certain number of reads. +func (r *rngAES) updateSeed() { + if r.count < reseedInterval { + r.count++ + return } - n.block.Encrypt(n.seed[:], n.seed[:]) - copy(nonce, n.seed[:]) + + var key [16]byte + io.ReadFull(crand.Reader, key[:]) + io.ReadFull(crand.Reader, r.seed[:]) + + block, err := aes.NewCipher(key[:]) + if err != nil { + panic(err) + } + r.block = block + r.count = 0 +} + +// Read fills p with random data using AES encryption. +func (r *rngAES) Read(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + + r.mutex.Lock() + r.updateSeed() + r.block.Encrypt(r.seed[:], r.seed[:]) + n := copy(p, r.seed[:]) + r.mutex.Unlock() + return n, nil +} + +// rngChacha8 is a ChaCha8-based random number generator. +type rngChacha8 struct { + mutex sync.Mutex + rand *rand.ChaCha8 + count uint64 +} + +// NewEntropyChacha8 creates a new ChaCha8-based entropy source. +func NewEntropyChacha8() io.Reader { + var seed [32]byte + io.ReadFull(crand.Reader, seed[:]) + + return &rngChacha8{ + rand: rand.NewChaCha8(seed), + } +} + +// updateSeed updates the ChaCha8 seed after a certain number of reads. +func (r *rngChacha8) updateSeed() { + if r.count < reseedInterval { + r.count++ + return + } + + var seed [32]byte + io.ReadFull(crand.Reader, seed[:]) + + r.rand.Seed(seed) + r.count = 0 +} + +// Read fills p with random data using ChaCha8. +func (r *rngChacha8) Read(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + + r.mutex.Lock() + r.updateSeed() + n, err := r.rand.Read(p) + r.mutex.Unlock() + return n, err } diff --git a/vendor/github.com/xtaci/kcp-go/v5/entropy_test.go b/vendor/github.com/xtaci/kcp-go/v5/entropy_test.go new file mode 100644 index 00000000..2e066e29 --- /dev/null +++ b/vendor/github.com/xtaci/kcp-go/v5/entropy_test.go @@ -0,0 +1,142 @@ +package kcp + +import ( + "crypto/aes" + "crypto/md5" + "crypto/rand" + "io" + "testing" +) + +func TestEntropyAES(t *testing.T) { + r := NewEntropyAES() + buf := make([]byte, 16) + n, err := r.Read(buf) + if err != nil { + t.Fatal(err) + } + if n != 16 { + t.Fatalf("expected 16 bytes, got %d", n) + } + + // Test empty read + n, err = r.Read(nil) + if err != nil { + t.Fatal(err) + } + if n != 0 { + t.Fatalf("expected 0 bytes, got %d", n) + } +} + +func TestEntropyChacha8(t *testing.T) { + r := NewEntropyChacha8() + buf := make([]byte, 32) + n, err := r.Read(buf) + if err != nil { + t.Fatal(err) + } + if n != 32 { + t.Fatalf("expected 32 bytes, got %d", n) + } + + // Test empty read + n, err = r.Read(nil) + if err != nil { + t.Fatal(err) + } + if n != 0 { + t.Fatalf("expected 0 bytes, got %d", n) + } +} + +func TestSetEntropy(t *testing.T) { + // Save original entropy + orig := entropy + defer SetEntropy(orig) + + // Set custom entropy + custom := NewEntropyChacha8() + SetEntropy(custom) + + if entropy != custom { + t.Fatal("SetEntropy failed") + } + + // Test fillRand with custom entropy + buf := make([]byte, 10) + fillRand(buf) + // We can't easily verify the content is from custom, but we can verify it didn't panic +} + +func TestFillRand(t *testing.T) { + buf := make([]byte, 100) + fillRand(buf) + + // Check if buffer is not all zeros (probabilistic, but highly likely) + allZero := true + for _, b := range buf { + if b != 0 { + allZero = false + break + } + } + if allZero { + t.Fatal("fillRand produced all zeros") + } + + // Test empty buffer + fillRand(nil) +} + +type mockReader struct{} + +func (m *mockReader) Read(p []byte) (n int, err error) { + for i := range p { + p[i] = 0xAA + } + return len(p), nil +} + +func TestSetEntropyMock(t *testing.T) { + orig := entropy + defer SetEntropy(orig) + + SetEntropy(&mockReader{}) + buf := make([]byte, 10) + fillRand(buf) + for _, b := range buf { + if b != 0xAA { + t.Fatalf("expected 0xAA, got %x", b) + } + } +} + +func BenchmarkCsprngSystem(b *testing.B) { + data := make([]byte, md5.Size) + b.SetBytes(int64(len(data))) + + for b.Loop() { + io.ReadFull(rand.Reader, data) + } +} + +func BenchmarkCsprngAES128(b *testing.B) { + var data [aes.BlockSize]byte + b.SetBytes(aes.BlockSize) + + r := NewEntropyAES() + for b.Loop() { + io.ReadFull(r, data[:]) + } +} + +func BenchmarkCsprngChacha8(b *testing.B) { + var data [8]byte + b.SetBytes(8) + + r := NewEntropyChacha8() + for b.Loop() { + io.ReadFull(r, data[:]) + } +} diff --git a/vendor/github.com/xtaci/kcp-go/v5/examples/echo.go b/vendor/github.com/xtaci/kcp-go/v5/examples/echo.go deleted file mode 100644 index af73db79..00000000 --- a/vendor/github.com/xtaci/kcp-go/v5/examples/echo.go +++ /dev/null @@ -1,77 +0,0 @@ -package main - -import ( - "crypto/sha1" - "io" - "log" - "time" - - "github.com/xtaci/kcp-go/v5" - "golang.org/x/crypto/pbkdf2" -) - -func main() { - key := pbkdf2.Key([]byte("demo pass"), []byte("demo salt"), 1024, 32, sha1.New) - block, _ := kcp.NewAESBlockCrypt(key) - if listener, err := kcp.ListenWithOptions("127.0.0.1:12345", block, 10, 3); err == nil { - // spin-up the client - go client() - for { - s, err := listener.AcceptKCP() - if err != nil { - log.Fatal(err) - } - go handleEcho(s) - } - } else { - log.Fatal(err) - } -} - -// handleEcho send back everything it received -func handleEcho(conn *kcp.UDPSession) { - buf := make([]byte, 4096) - for { - n, err := conn.Read(buf) - if err != nil { - log.Println(err) - return - } - - n, err = conn.Write(buf[:n]) - if err != nil { - log.Println(err) - return - } - } -} - -func client() { - key := pbkdf2.Key([]byte("demo pass"), []byte("demo salt"), 1024, 32, sha1.New) - block, _ := kcp.NewAESBlockCrypt(key) - - // wait for server to become ready - time.Sleep(time.Second) - - // dial to the echo server - if sess, err := kcp.DialWithOptions("127.0.0.1:12345", block, 10, 3); err == nil { - for { - data := time.Now().String() - buf := make([]byte, len(data)) - log.Println("sent:", data) - if _, err := sess.Write([]byte(data)); err == nil { - // read back the data - if _, err := io.ReadFull(sess, buf); err == nil { - log.Println("recv:", string(buf)) - } else { - log.Fatal(err) - } - } else { - log.Fatal(err) - } - time.Sleep(time.Second) - } - } else { - log.Fatal(err) - } -} diff --git a/vendor/github.com/xtaci/kcp-go/v5/fec.go b/vendor/github.com/xtaci/kcp-go/v5/fec.go index 0a203ee3..80a4962c 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/fec.go +++ b/vendor/github.com/xtaci/kcp-go/v5/fec.go @@ -1,19 +1,61 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// THE GENERALIZED REED-SOLOMON FEC SCHEME +// +// Encoding: +// ----------- +// Message: | M1 | M2 | M3 | M4 | +// Generate Parity: | P1 | P2 | +// Encoded Codeword:| M1 | M2 | M3 | M4 | P1 | P2 | +// +// Decoding with Erasures: +// ------------------------ +// Received: | M1 | ?? | M3 | M4 | P1 | ?? | +// Erasures: | | E1 | | | | E2 | +// Syndromes: S1, S2, ... +// Error Locator: Λ(x) = ... +// Correct Erasures:Determine values for E1 (M2) and E2 (P2). +// Corrected: | M1 | M2 | M3 | M4 | P1 | P2 | + package kcp import ( + "container/heap" "encoding/binary" "sync/atomic" + "time" "github.com/klauspost/reedsolomon" ) +// FEC (Forward Error Correction) protocol constants const ( - fecHeaderSize = 6 - fecHeaderSizePlus2 = fecHeaderSize + 2 // plus 2B data size - typeData = 0xf1 - typeParity = 0xf2 - fecExpire = 60000 - rxFECMulti = 3 // FEC keeps rxFECMulti* (dataShard+parityShard) ordered packets in memory + fecHeaderSize = 6 // FEC header: seqid(4B) + type(2B) + fecHeaderSizePlus2 = fecHeaderSize + 2 // FEC header + 2B payload size field + typeData = 0xf1 // FEC packet type: data shard + typeParity = 0xf2 // FEC packet type: parity shard + typeOOB = 0xf3 // FEC packet type: out-of-band (unreliable) + maxShardSets = 3 // max concurrent shard sets before discarding old ones ) // fecPacket is a decoded FEC packet @@ -23,32 +65,71 @@ func (bts fecPacket) seqid() uint32 { return binary.LittleEndian.Uint32(bts) } func (bts fecPacket) flag() uint16 { return binary.LittleEndian.Uint16(bts[4:]) } func (bts fecPacket) data() []byte { return bts[6:] } -// fecElement has auxcilliary time field -type fecElement struct { - fecPacket - ts uint32 +// shardHeap holds a corelated set of datashards from the peers +type shardHeap struct { + elements []fecPacket + marks map[uint32]struct{} // to avoid duplicates +} + +func newShardHeap() *shardHeap { + h := &shardHeap{ + marks: make(map[uint32]struct{}), + } + heap.Init(h) + return h +} + +func (h *shardHeap) Len() int { return len(h.elements) } + +func (h *shardHeap) Less(i, j int) bool { + return _itimediff(h.elements[j].seqid(), h.elements[i].seqid()) > 0 +} + +func (h *shardHeap) Swap(i, j int) { h.elements[i], h.elements[j] = h.elements[j], h.elements[i] } +func (h *shardHeap) Push(x any) { + h.elements = append(h.elements, x.(fecPacket)) + h.marks[x.(fecPacket).seqid()] = struct{}{} } -// fecDecoder for decoding incoming packets +func (h *shardHeap) Pop() any { + n := len(h.elements) + x := h.elements[n-1] + h.elements[n-1] = nil // clear to avoid memory leak + h.elements = h.elements[:n-1] + delete(h.marks, x.seqid()) + return x +} + +func (h *shardHeap) Has(sn uint32) bool { + _, exists := h.marks[sn] + return exists +} + +// fecDecoder for decoding incoming packets. +// It collects shards grouped by shard ID and attempts Reed-Solomon recovery +// when enough shards have been received. type fecDecoder struct { - rxlimit int // queue size limit dataShards int parityShards int shardSize int - rx []fecElement // ordered receive queue + shardSet map[uint32]*shardHeap // shardMap[initial shard id] = shardHeap + paws uint32 // Protect Against Wrapped Sequence numbers + + // record the latest recovered shard id + // the shards smaller than this one will be discarded + newestShardId uint32 // caches decodeCache [][]byte flagCache []bool - // zeros - zeros []byte - // RS decoder codec reedsolomon.Encoder - // auto tune fec parameter - autoTune autoTune + // auto-tuning: dynamically adjusts dataShards/parityShards ratio + // by detecting the period of data vs parity pulses in the incoming stream + autoTune autoTune + shouldTune bool // true when a type mismatch is detected, triggering auto-tune } func newFECDecoder(dataShards, parityShards int) *fecDecoder { @@ -56,11 +137,16 @@ func newFECDecoder(dataShards, parityShards int) *fecDecoder { return nil } + if dataShards+parityShards > 256 { + return nil + } + dec := new(fecDecoder) dec.dataShards = dataShards dec.parityShards = parityShards dec.shardSize = dataShards + parityShards - dec.rxlimit = rxFECMulti * dec.shardSize + dec.shardSet = make(map[uint32]*shardHeap) + dec.paws = 0xffffffff / uint32(dec.shardSize) * uint32(dec.shardSize) codec, err := reedsolomon.New(dataShards, parityShards) if err != nil { return nil @@ -68,43 +154,54 @@ func newFECDecoder(dataShards, parityShards int) *fecDecoder { dec.codec = codec dec.decodeCache = make([][]byte, dec.shardSize) dec.flagCache = make([]bool, dec.shardSize) - dec.zeros = make([]byte, mtuLimit) return dec } // decode a fec packet func (dec *fecDecoder) decode(in fecPacket) (recovered [][]byte) { - // sample to auto FEC tuner + // Sample the packet type for auto-tuning if in.flag() == typeData { dec.autoTune.Sample(true, in.seqid()) } else { dec.autoTune.Sample(false, in.seqid()) } - // check if FEC parameters is out of sync - var shouldTune bool - if int(in.seqid())%dec.shardSize < dec.dataShards { + // check seqid < paws to avoid invalid packets + if in.seqid() >= dec.paws { + return nil + } + + // check if the packet type matches the current FEC parameters + if in.seqid()%uint32(dec.shardSize) < uint32(dec.dataShards) { if in.flag() != typeData { // expect typeData - shouldTune = true + dec.shouldTune = true } } else { if in.flag() != typeParity { - shouldTune = true + dec.shouldTune = true } } - if shouldTune { - autoDS := dec.autoTune.FindPeriod(true) - autoPS := dec.autoTune.FindPeriod(false) + // perform auto-tuning if the decoder detects packet type mismatches. + // This means the peer has changed FEC parameters and we must adapt. + if dec.shouldTune { + autoDS := dec.autoTune.FindPeriod(true) // detect data shard period + autoPS := dec.autoTune.FindPeriod(false) // detect parity shard period - // edges found, we can tune parameters now - if autoDS > 0 && autoPS > 0 && autoDS < 256 && autoPS < 256 { - // and make sure it's different + // validate the auto-tuned parameters + if autoDS > 0 && autoPS > 0 && autoDS+autoPS < 256 { if autoDS != dec.dataShards || autoPS != dec.parityShards { + // apply the new FEC parameters dec.dataShards = autoDS dec.parityShards = autoPS dec.shardSize = autoDS + autoPS - dec.rxlimit = rxFECMulti * dec.shardSize + // recycle old shards before creating new shardSet + for _, shard := range dec.shardSet { + for _, pkt := range shard.elements { + defaultBufferPool.Put(pkt) + } + } + dec.shardSet = make(map[uint32]*shardHeap) // empty the shard set codec, err := reedsolomon.New(autoDS, autoPS) if err != nil { return nil @@ -112,56 +209,45 @@ func (dec *fecDecoder) decode(in fecPacket) (recovered [][]byte) { dec.codec = codec dec.decodeCache = make([][]byte, dec.shardSize) dec.flagCache = make([]bool, dec.shardSize) + dec.paws = 0xffffffff / uint32(dec.shardSize) * uint32(dec.shardSize) //log.Println("autotune to :", dec.dataShards, dec.parityShards) } + // reset shouldTune flag regardless of whether parameters changed + // to avoid permanent blocking when detected parameters match current ones + dec.shouldTune = false } + return nil } - // insertion - n := len(dec.rx) - 1 - insertIdx := 0 - for i := n; i >= 0; i-- { - if in.seqid() == dec.rx[i].seqid() { // de-duplicate - return nil - } else if _itimediff(in.seqid(), dec.rx[i].seqid()) > 0 { // insertion - insertIdx = i + 1 - break - } + // get the shard heap for this shard id + shardId := dec.getShardId(in.seqid()) + shard, ok := dec.shardSet[shardId] + if !ok { + shard = newShardHeap() + dec.shardSet[shardId] = shard + atomic.AddUint64(&DefaultSnmp.FECShardSet, 1) } - // make a copy - pkt := fecPacket(xmitBuf.Get().([]byte)[:len(in)]) - copy(pkt, in) - elem := fecElement{pkt, currentMs()} - - // insert into ordered rx queue - if insertIdx == n+1 { - dec.rx = append(dec.rx, elem) - } else { - dec.rx = append(dec.rx, fecElement{}) - copy(dec.rx[insertIdx+1:], dec.rx[insertIdx:]) // shift right - dec.rx[insertIdx] = elem + // ignore duplicate packets + if shard.Has(in.seqid()) { + return nil } - // shard range for current packet - shardBegin := pkt.seqid() - pkt.seqid()%uint32(dec.shardSize) - shardEnd := shardBegin + uint32(dec.shardSize) - 1 - - // max search range in ordered queue for current shard - searchBegin := insertIdx - int(pkt.seqid()%uint32(dec.shardSize)) - if searchBegin < 0 { - searchBegin = 0 - } - searchEnd := searchBegin + dec.shardSize - 1 - if searchEnd >= len(dec.rx) { - searchEnd = len(dec.rx) - 1 + // update statistics for parity shards + if in.flag() == typeParity { + atomic.AddUint64(&DefaultSnmp.FECParityShards, 1) } - // re-construct datashards - if searchEnd-searchBegin+1 >= dec.dataShards { - var numshard, numDataShard, first, maxlen int + // push the packet into the shard heap + pkt := fecPacket(defaultBufferPool.Get()[:len(in)]) + copy(pkt, in) + shard.Push(pkt) + + // try to recover data if we have enough shards + if shard.Len() >= dec.dataShards { + var numDataShard, maxlen int - // zero caches + // prepare the decode cache shards := dec.decodeCache shardsflag := dec.flagCache for k := range dec.decodeCache { @@ -169,95 +255,98 @@ func (dec *fecDecoder) decode(in fecPacket) (recovered [][]byte) { shardsflag[k] = false } - // shard assembly - for i := searchBegin; i <= searchEnd; i++ { - seqid := dec.rx[i].seqid() - if _itimediff(seqid, shardEnd) > 0 { - break - } else if _itimediff(seqid, shardBegin) >= 0 { - shards[seqid%uint32(dec.shardSize)] = dec.rx[i].data() - shardsflag[seqid%uint32(dec.shardSize)] = true - numshard++ - if dec.rx[i].flag() == typeData { - numDataShard++ - } - if numshard == 1 { - first = i - } - if len(dec.rx[i].data()) > maxlen { - maxlen = len(dec.rx[i].data()) - } + // pop all shards from the heap and fill into the decode cache + var pkts []fecPacket + for shard.Len() > 0 { + pkt := shard.Pop().(fecPacket) + pkts = append(pkts, pkt) + seqid := pkt.seqid() + shards[seqid%uint32(dec.shardSize)] = pkt.data() + shardsflag[seqid%uint32(dec.shardSize)] = true + if pkt.flag() == typeData { + numDataShard++ + } + if len(pkt.data()) > maxlen { + maxlen = len(pkt.data()) } } + // case 1: all data shards are present if numDataShard == dec.dataShards { - // case 1: no loss on data shards - dec.rx = dec.freeRange(first, numshard, dec.rx) - } else if numshard >= dec.dataShards { - // case 2: loss on data shards, but it's recoverable from parity shards + atomic.AddUint64(&DefaultSnmp.FECFullShardSet, 1) + } else { // case 2: some data shards are missing, try to recover + // fill '0' into the tail of each shard to make them equal-sized + var newBuffers [][]byte for k := range shards { if shards[k] != nil { dlen := len(shards[k]) shards[k] = shards[k][:maxlen] - copy(shards[k][dlen:], dec.zeros) + clear(shards[k][dlen:]) } else if k < dec.dataShards { - shards[k] = xmitBuf.Get().([]byte)[:0] + // prepare memory for the data recovery + shards[k] = defaultBufferPool.Get()[:0] + newBuffers = append(newBuffers, shards[k]) } } + + // Reed-Solomon Erasure Code Decoding if err := dec.codec.ReconstructData(shards); err == nil { for k := range shards[:dec.dataShards] { if !shardsflag[k] { - // recovered data should be recycled recovered = append(recovered, shards[k]) } } + } else { + // recovery failed, record the error + atomic.AddUint64(&DefaultSnmp.FECErrs, 1) + // recycle new buffers if failed + // NOTE: if success, these buffers are returned in 'recovered' and will be recycled by the caller + for _, buf := range newBuffers { + defaultBufferPool.Put(buf) + } } - dec.rx = dec.freeRange(first, numshard, dec.rx) - } - } - // keep rxlimit - if len(dec.rx) > dec.rxlimit { - if dec.rx[0].flag() == typeData { // track the unrecoverable data - atomic.AddUint64(&DefaultSnmp.FECShortShards, 1) + // record the number of recovered packets + atomic.AddUint64(&DefaultSnmp.FECRecovered, uint64(len(recovered))) } - dec.rx = dec.freeRange(0, 1, dec.rx) - } - // timeout policy - current := currentMs() - numExpired := 0 - for k := range dec.rx { - if _itimediff(current, dec.rx[k].ts) > fecExpire { - numExpired++ - continue + // recycle the packets + for _, pkt := range pkts { + defaultBufferPool.Put(pkt) } - break } - if numExpired > 0 { - dec.rx = dec.freeRange(0, numExpired, dec.rx) + + // update the newest shard id + if _itimediff(shardId*uint32(dec.shardSize), dec.newestShardId*uint32(dec.shardSize)) > 0 { + dec.newestShardId = shardId + atomic.StoreUint64(&DefaultSnmp.FECShardMin, uint64(dec.newestShardId)) } + + // try to discard shard sets that are too old + dec.discardShards() + return } -// free a range of fecPacket -func (dec *fecDecoder) freeRange(first, n int, q []fecElement) []fecElement { - for i := first; i < first+n; i++ { // recycle buffer - xmitBuf.Put([]byte(q[i].fecPacket)) - } - - if first == 0 && n < cap(q)/2 { - return q[n:] - } - copy(q[first:], q[first+n:]) - return q[:len(q)-n] +// getShardId calculates the shard id based on the sequence id +func (dec *fecDecoder) getShardId(seqid uint32) uint32 { + return seqid / uint32(dec.shardSize) } -// release all segments back to xmitBuf -func (dec *fecDecoder) release() { - if n := len(dec.rx); n > 0 { - dec.rx = dec.freeRange(0, n, dec.rx) +// discardShards removes shards that are too old from the shardSet +func (dec *fecDecoder) discardShards() { + for shardId, shard := range dec.shardSet { + // discard shards that are too old + if _itimediff(dec.newestShardId*uint32(dec.shardSize), shardId*uint32(dec.shardSize)) > maxShardSets*int32(dec.shardSize) { + //println("flushing shard", shardId, "minShardId", dec.minShardId, _itimediff(dec.minShardId, shardId)) + for _, pkt := range shard.elements { + defaultBufferPool.Put(pkt) + } + delete(dec.shardSet, shardId) + } } + + atomic.StoreUint64(&DefaultSnmp.FECShardSet, uint64(len(dec.shardSet))) } type ( @@ -276,11 +365,9 @@ type ( payloadOffset int // FEC payload offset // caches - shardCache [][]byte - encodeCache [][]byte - - // zeros - zeros []byte + shardCache [][]byte + encodeCache [][]byte + tsLatestPacket int64 // RS encoder codec reedsolomon.Encoder @@ -311,17 +398,16 @@ func newFECEncoder(dataShards, parityShards, offset int) *fecEncoder { for k := range enc.shardCache { enc.shardCache[k] = make([]byte, mtuLimit) } - enc.zeros = make([]byte, mtuLimit) return enc } // encodes the packet, outputs parity shards if we have collected quorum datashards // notice: the contents of 'ps' will be re-written in successive calling -func (enc *fecEncoder) encode(b []byte) (ps [][]byte) { +func (enc *fecEncoder) encode(b []byte, rto uint32) (ps [][]byte) { // The header format: // | FEC SEQID(4B) | FEC TYPE(2B) | SIZE (2B) | PAYLOAD(SIZE-2) | // |<-headerOffset |<-payloadOffset - enc.markData(b[enc.headerOffset:]) + enc.sealData(b[enc.headerOffset:]) binary.LittleEndian.PutUint16(b[enc.payloadOffset:], uint16(len(b[enc.payloadOffset:]))) // copy data from payloadOffset to fec shard cache @@ -335,47 +421,92 @@ func (enc *fecEncoder) encode(b []byte) (ps [][]byte) { enc.maxSize = sz } - // Generation of Reed-Solomon Erasure Code + // Generation of Reed-Solomon Erasure Code when we have enough datashards + now := time.Now().UnixMilli() if enc.shardCount == enc.dataShards { - // fill '0' into the tail of each datashard - for i := 0; i < enc.dataShards; i++ { - shard := enc.shardCache[i] - slen := len(shard) - copy(shard[slen:enc.maxSize], enc.zeros) - } + // Generate the parity shards if we collect enough datashards, + // the continuity is determined by the time interval between + // the latest 2 data packets. + // + // If the interval is larger than rto, we consider the data is non-continuous, + // thus we skip this parity generation to avoid useless parity packets. + // + // Note that, even we skip this parity generation, we still need to + // increase the seqid to keep the monotonic increasing property. + // This is important for the receiver to detect lost packets. + // see fecEncoder.skipParity() + // also note that the rto is in milliseconds. + // see kcp.UDPSession.rto() + // + if now-enc.tsLatestPacket < int64(rto) { + // clear the tail of each datashard to make them equal-sized + for i := 0; i < enc.dataShards; i++ { + shard := enc.shardCache[i] + slen := len(shard) + clear(shard[slen:enc.maxSize]) + } - // construct equal-sized slice with stripped header - cache := enc.encodeCache - for k := range cache { - cache[k] = enc.shardCache[k][enc.payloadOffset:enc.maxSize] - } + // construct equal-sized slice with stripped header + cache := enc.encodeCache + for k := range cache { + cache[k] = enc.shardCache[k][enc.payloadOffset:enc.maxSize] + } - // encoding - if err := enc.codec.Encode(cache); err == nil { - ps = enc.shardCache[enc.dataShards:] - for k := range ps { - enc.markParity(ps[k][enc.headerOffset:]) - ps[k] = ps[k][:enc.maxSize] + // Reed-Solomon Erasure Code Encoding + if err := enc.codec.Encode(cache); err == nil { + ps = enc.shardCache[enc.dataShards:] + for k := range ps { + enc.sealParity(ps[k][enc.headerOffset:]) // NOTE(x): seal parity will increase the seqid by 1 + ps[k] = ps[k][:enc.maxSize] + } + } else { + // encoding failed, record the error but keep the seqid monotonic increasing + atomic.AddUint64(&DefaultSnmp.FECErrs, 1) + enc.skipParity() } + } else { + // Non-continuous data detected, skip this parity generation. + // Through we do not send non-continuous parity shard, we still need to increase seqid. + enc.skipParity() } - // counters resetting + // reset shard count and max size enc.shardCount = 0 enc.maxSize = 0 } + // record the time of the latest data packet + enc.tsLatestPacket = now + return } -func (enc *fecEncoder) markData(data []byte) { +// sealData and sealParity write the sequence number and type into the FEC header +func (enc *fecEncoder) sealData(data []byte) { binary.LittleEndian.PutUint32(data, enc.next) binary.LittleEndian.PutUint16(data[4:], typeData) - enc.next++ + enc.next = (enc.next + 1) % enc.paws } -func (enc *fecEncoder) markParity(data []byte) { +func (enc *fecEncoder) sealParity(data []byte) { binary.LittleEndian.PutUint32(data, enc.next) binary.LittleEndian.PutUint16(data[4:], typeParity) - // sequence wrap will only happen at parity shard enc.next = (enc.next + 1) % enc.paws } + +// encodeOOB encodes an out-of-band packet +func (enc *fecEncoder) encodeOOB(b []byte) { + enc.sealOOB(b[enc.headerOffset:]) + binary.LittleEndian.PutUint16(b[enc.payloadOffset:], uint16(len(b[enc.payloadOffset:]))) +} + +// sealOOB seals an out-of-band packet +func (enc *fecEncoder) sealOOB(data []byte) { + binary.LittleEndian.PutUint32(data, uint32(0xffffffff)) // use max uint32 as OOB seqid + binary.LittleEndian.PutUint16(data[4:], typeOOB) +} + +// skipParity skips the whole parity block by advancing the seqid +func (enc *fecEncoder) skipParity() { + enc.next = (enc.next + uint32(enc.parityShards)) % enc.paws +} diff --git a/vendor/github.com/xtaci/kcp-go/v5/fec_test.go b/vendor/github.com/xtaci/kcp-go/v5/fec_test.go index 59b64aca..80c66694 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/fec_test.go +++ b/vendor/github.com/xtaci/kcp-go/v5/fec_test.go @@ -1,11 +1,236 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + package kcp import ( "encoding/binary" + "math" "math/rand" "testing" + "time" ) +func TestFECEncodeConsecutive(t *testing.T) { + const dataSize = 10 + const paritySize = 3 + const payLoad = 1500 + + encoder := newFECEncoder(dataSize, paritySize, 0) + t.Logf("dataSize:%v, paritySize:%v", dataSize, paritySize) + group := 0 + sent := 0 + for i := range 100 { + if i%dataSize == 0 { + group++ + } + + data := make([]byte, payLoad) + duration := time.Duration(rand.Int()%300) * time.Millisecond + t.Logf("Sleep: %v, packet %v", duration, sent) + <-time.After(duration) + + ps := encoder.encode(data, 200) + sent++ + + if len(ps) > 0 { + t.Log("has parity:", len(ps)) + for idx, p := range ps { + seqid := binary.LittleEndian.Uint32(p) + expected := uint32((group-1)*(dataSize+paritySize) + dataSize + idx) + if seqid != expected { + t.Fatalf("expected parity shard:%v actual seqid %v", expected, seqid) + return + } + } + continue + } + + if sent%dataSize == 0 { + t.Log("no parity:", len(ps)) + continue + } + } +} + +func TestFECDecodeLoss(t *testing.T) { + // This function lose 3 random packet from 10 datashards and 3 parity shards. + // so each group of 13 packets should be able to recover from the loss. + const dataShards = 10 + const parityShards = 3 + const groupSize = dataShards + parityShards + const payLoad = 1400 + decoder := newFECDecoder(dataShards, parityShards) + t.Logf("dataSize:%v, paritySize:%v", dataShards, parityShards) + sent := 0 + totalRecovered := 0 + totalParityLost := 0 + + for group := range 100 { + losses := make(map[int]bool) + + lost := 0 + parityLost := 0 + for lost < parityShards { + pos := rand.Intn(dataShards + parityShards) + if !losses[pos] { + losses[pos] = true + if pos >= dataShards { + totalParityLost++ + parityLost++ + } + lost++ + } + } + + if len(losses) != parityShards { + t.Fatalf("Expected %v losses, got %v", parityShards, len(losses)) + return + } + + recovered := 0 + for i := range dataShards + parityShards { + sent++ + if losses[i] { + t.Logf("Lost packet %v in group %v", groupSize*group+i, group) + continue + } + + pkt := make([]byte, payLoad) + binary.LittleEndian.PutUint32(pkt, uint32(groupSize*group+i)) + if i%(dataShards+parityShards) >= dataShards { + binary.LittleEndian.PutUint16(pkt[4:], typeParity) + } else { + binary.LittleEndian.PutUint16(pkt[4:], typeData) + } + + rec := decoder.decode(pkt) + if len(rec) > 0 { + totalRecovered += len(rec) + recovered += len(rec) + t.Log("Recovered", len(rec), "packets from group", group) + } + } + + // the recovered packets should equal to the lost data packets + if recovered != lost-parityLost { + t.Fatalf("Expected recovered %v packets, got %v", lost-parityLost, recovered) + } + } + t.Log("Total recovered packets:", totalRecovered) + t.Log("Total parity lost:", totalParityLost) +} + +func TestFECDecodeVariablePacketSizes(t *testing.T) { + const ( + dataShards = 8 + parityShards = 3 + groups = 32 + rto = math.MaxInt32 + minPayload = 8 + maxPayload = 900 + ) + + encoder := newFECEncoder(dataShards, parityShards, 0) + decoder := newFECDecoder(dataShards, parityShards) + if encoder == nil || decoder == nil { + t.Fatalf("failed to create FEC encoder/decoder") + } + + rnd := rand.New(rand.NewSource(42)) + var totalLost, totalRecovered int + + feed := func(raw []byte) { + t.Helper() + packet := append([]byte(nil), raw...) + recovered := decoder.decode(fecPacket(packet)) + for _, r := range recovered { + if len(r) < 2 { + t.Fatalf("recovered shard too small: %d", len(r)) + } + sz := binary.LittleEndian.Uint16(r) + if int(sz) > len(r) { + t.Fatalf("invalid size %d for buffer len %d", sz, len(r)) + } + payload := r[2:sz] + if len(payload) < minPayload { + t.Fatalf("payload shorter than expected: %d", len(payload)) + } + if int(sz) != len(payload)+2 { + t.Fatalf("size field mismatch: got %d expect %d", sz, len(payload)+2) + } + + groupID := binary.LittleEndian.Uint32(payload) + shardIdx := binary.LittleEndian.Uint32(payload[4:]) + for i := 8; i < len(payload); i++ { + expected := byte((int(groupID) + int(shardIdx) + i) & 0xff) + if payload[i] != expected { + t.Fatalf("content mismatch: group %d shard %d offset %d got %d expect %d", + groupID, shardIdx, i-8, payload[i], expected) + } + } + defaultBufferPool.Put(r) + totalRecovered++ + } + } + + for group := range groups { + losses := map[int]struct{}{ + group % dataShards: {}, + (group + 3) % dataShards: {}, + } + + for shard := range dataShards { + payloadLen := minPayload + rnd.Intn(maxPayload-minPayload+1) + buf := make([]byte, fecHeaderSizePlus2+payloadLen) + payload := buf[fecHeaderSizePlus2:] + binary.LittleEndian.PutUint32(payload, uint32(group)) + binary.LittleEndian.PutUint32(payload[4:], uint32(shard)) + for i := 8; i < len(payload); i++ { + payload[i] = byte((group + shard + i) & 0xff) + } + + ps := encoder.encode(buf, uint32(rto)) + t.Log("Encoded group", group, "shard", shard, "payloadLen", payloadLen, "parityShards", len(ps)) + if _, drop := losses[shard]; drop { + totalLost++ + } else { + feed(buf) + } + + if len(ps) > 0 { + for _, p := range ps { + parity := append([]byte(nil), p...) + feed(parity) + } + } + } + } + + if totalRecovered != totalLost { + t.Fatalf("expected %d recoveries, got %d", totalLost, totalRecovered) + } +} + func BenchmarkFECDecode(b *testing.B) { const dataSize = 10 const paritySize = 3 @@ -13,7 +238,7 @@ func BenchmarkFECDecode(b *testing.B) { decoder := newFECDecoder(dataSize, paritySize) b.ReportAllocs() b.SetBytes(payLoad) - for i := 0; i < b.N; i++ { + for i := 0; b.Loop(); i++ { if rand.Int()%(dataSize+paritySize) == 0 { // random loss continue } @@ -36,8 +261,249 @@ func BenchmarkFECEncode(b *testing.B) { b.ReportAllocs() b.SetBytes(payLoad) encoder := newFECEncoder(dataSize, paritySize, 0) - for i := 0; i < b.N; i++ { + for b.Loop() { data := make([]byte, payLoad) - encoder.encode(data) + encoder.encode(data, 200) + } +} + +func TestFECPAWS(t *testing.T) { + const dataShards = 10 + const parityShards = 3 + const shardSize = dataShards + parityShards + const payLoad = 1500 + + encoder := newFECEncoder(dataShards, parityShards, 0) + decoder := newFECDecoder(dataShards, parityShards) + + // Manually set the next sequence number to be near the PAWS boundary + // We want to test the transition from the last group to the first group + // paws is a multiple of shardSize. + // We set next to paws - shardSize, so we are at the start of the last group. + encoder.next = encoder.paws - uint32(shardSize) + + t.Logf("PAWS: %v, Initial Next: %v", encoder.paws, encoder.next) + + var packets []fecPacket + + // 1. Encode the last group before PAWS + // This will generate 'dataShards' data packets and 'parityShards' parity packets. + // Total 'shardSize' packets. + // Their seqids should be [paws-shardSize, ..., paws-1] + for i := range dataShards { + data := make([]byte, payLoad) + // We can put some recognizable data + // Note: fecEncoder writes header at 0-6, and size at 6-8. Payload starts at 8. + binary.LittleEndian.PutUint32(data[8:], uint32(i)) + + ps := encoder.encode(data, 200) + + // Copy data packet + pkt := make([]byte, len(data)) + copy(pkt, data) + packets = append(packets, fecPacket(pkt)) + + // Copy parity packets + for _, p := range ps { + pCopy := make([]byte, len(p)) + copy(pCopy, p) + packets = append(packets, fecPacket(pCopy)) + } + } + + // Verify seqids of the first group + for i, pkt := range packets { + seqid := pkt.seqid() + expected := encoder.paws - uint32(shardSize) + uint32(i) + if seqid != expected { + t.Fatalf("Group 1: expected seqid %v, got %v", expected, seqid) + } + } + t.Log("Group 1 generated successfully") + + // 2. Encode the first group after PAWS + // Their seqids should be [0, ..., shardSize-1] + startIdx := len(packets) + for i := range dataShards { + data := make([]byte, payLoad) + binary.LittleEndian.PutUint32(data[8:], uint32(i+100)) // Different data + + ps := encoder.encode(data, 200) + + pkt := make([]byte, len(data)) + copy(pkt, data) + packets = append(packets, fecPacket(pkt)) + + for _, p := range ps { + pCopy := make([]byte, len(p)) + copy(pCopy, p) + packets = append(packets, fecPacket(pCopy)) + } + } + + // Verify seqids of the second group + for i, pkt := range packets[startIdx:] { + seqid := pkt.seqid() + expected := uint32(i) + if seqid != expected { + t.Fatalf("Group 2: expected seqid %v, got %v", expected, seqid) + } + } + t.Log("Group 2 generated successfully") + + // 3. Feed to decoder with some loss + // We will lose the last data packet of Group 1 and the first data packet of Group 2 + // to test recovery across the boundary (though recovery is per-group). + + // We drop index 9 (seqid paws-4) and index 13 (seqid 0). + // Group 1 indices: 0-12. Data: 0-9. Parity: 10-12. + // Group 2 indices: 13-25. Data: 13-22. Parity: 23-25. + + dropped := make(map[int]bool) + dropped[9] = true + dropped[13] = true + + recoveredCount := 0 + + for i, pkt := range packets { + if dropped[i] { + t.Logf("Dropping packet index %v, seqid %v", i, pkt.seqid()) + continue + } + + recovered := decoder.decode(pkt) + if len(recovered) > 0 { + t.Logf("Recovered %v packets at step %v (seqid %v)", len(recovered), i, pkt.seqid()) + recoveredCount += len(recovered) + for _, r := range recovered { + // Verify recovered data + // r[0:2] is size. r[2:] is payload. + val := binary.LittleEndian.Uint32(r[2:]) + if val == 9 { + t.Log("Recovered packet 9 correctly") + } else if val == 100 { + t.Log("Recovered packet 13 (val 100) correctly") + } else { + t.Errorf("Recovered unexpected data: %v", val) + } + } + } + } + + if recoveredCount != 2 { + t.Fatalf("Expected 2 recovered packets, got %v", recoveredCount) + } + + t.Log("PAWS wrap test passed") +} + +func TestFECRTOAndSkipParity(t *testing.T) { + const dataShards = 3 + const parityShards = 2 + const rto = 50 // 50ms RTO + + enc := newFECEncoder(dataShards, parityShards, 0) + + // Helper to extract seqid from packet + getSeq := func(b []byte) uint32 { + return binary.LittleEndian.Uint32(b) + } + + // --- Scenario 1: Normal case (Time < RTO) --- + // Send 3 packets quickly + t.Log("--- Scenario 1: Normal case (Time < RTO) ---") + + // Packet 0 + p0 := make([]byte, 100) + ps := enc.encode(p0, rto) + if len(ps) != 0 { + t.Fatalf("Expected no parity shards yet") + } + if seq := getSeq(p0); seq != 0 { + t.Fatalf("Expected seq 0, got %d", seq) + } + + // Packet 1 + p1 := make([]byte, 100) + ps = enc.encode(p1, rto) + if len(ps) != 0 { + t.Fatalf("Expected no parity shards yet") + } + if seq := getSeq(p1); seq != 1 { + t.Fatalf("Expected seq 1, got %d", seq) + } + + // Packet 2 (Trigger parity generation) + p2 := make([]byte, 100) + ps = enc.encode(p2, rto) + if len(ps) != parityShards { + t.Fatalf("Expected %d parity shards, got %d", parityShards, len(ps)) + } + if seq := getSeq(p2); seq != 2 { + t.Fatalf("Expected seq 2, got %d", seq) + } + + // Check parity seqids + if seq := getSeq(ps[0]); seq != 3 { + t.Fatalf("Expected parity[0] seq 3, got %d", seq) + } + if seq := getSeq(ps[1]); seq != 4 { + t.Fatalf("Expected parity[1] seq 4, got %d", seq) + } + + // --- Scenario 2: Timeout case (Time > RTO) --- + // Send 2 packets quickly, then sleep, then send 3rd + t.Log("--- Scenario 2: Timeout case (Time > RTO) ---") + + // Packet 3 (Next data seq should be 5) + p3 := make([]byte, 100) + ps = enc.encode(p3, rto) + if len(ps) != 0 { + t.Fatalf("Expected no parity shards yet") + } + if seq := getSeq(p3); seq != 5 { + t.Fatalf("Expected seq 5, got %d", seq) + } + + // Packet 4 + p4 := make([]byte, 100) + ps = enc.encode(p4, rto) + if len(ps) != 0 { + t.Fatalf("Expected no parity shards yet") + } + if seq := getSeq(p4); seq != 6 { + t.Fatalf("Expected seq 6, got %d", seq) + } + + // Sleep longer than RTO + time.Sleep(time.Duration(rto+20) * time.Millisecond) + + // Packet 5 (Trigger parity check -> should skip) + p5 := make([]byte, 100) + ps = enc.encode(p5, rto) + + // Expect NO parity shards because of timeout + if len(ps) != 0 { + t.Fatalf("Expected 0 parity shards due to timeout, got %d", len(ps)) + } + if seq := getSeq(p5); seq != 7 { + t.Fatalf("Expected seq 7, got %d", seq) + } + + // Even though parity was skipped, the sequence ID should have advanced by parityShards (2) + // So next data packet should be 7 + 1 (current) + 2 (skipped parity) = 10? + // Wait, let's trace: + // p5 gets seq 7. + // encode() calls skipParity() -> enc.next += parityShards. + // enc.next was 8 (after p5). 8 + 2 = 10. + // So next packet should have seq 10. + + // --- Verify Sequence ID Growth after Skip --- + t.Log("--- Verify Sequence ID Growth after Skip ---") + + p6 := make([]byte, 100) + ps = enc.encode(p6, rto) + if seq := getSeq(p6); seq != 10 { + t.Fatalf("Expected seq 10 after skipped parity, got %d", seq) } } diff --git a/vendor/github.com/xtaci/kcp-go/v5/flame.png b/vendor/github.com/xtaci/kcp-go/v5/flame.png deleted file mode 100644 index 672f649ef9c72635be58e330cc901a8937db0fa8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57142 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 diff --git a/vendor/github.com/xtaci/kcp-go/v5/go.mod b/vendor/github.com/xtaci/kcp-go/v5/go.mod index 0247f820..67f19c67 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/go.mod +++ b/vendor/github.com/xtaci/kcp-go/v5/go.mod @@ -1,20 +1,24 @@ module github.com/xtaci/kcp-go/v5 require ( - github.com/klauspost/cpuid v1.3.1 // indirect - github.com/klauspost/reedsolomon v1.9.9 - github.com/mmcloughlin/avo v0.0.0-20200803215136-443f81d77104 // indirect + github.com/klauspost/reedsolomon v1.12.0 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.6.1 - github.com/templexxx/cpu v0.0.8 // indirect - github.com/templexxx/xorsimd v0.4.1 - github.com/tjfoc/gmsm v1.3.2 + github.com/tjfoc/gmsm v1.4.1 github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae - golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de - golang.org/x/net v0.0.0-20200707034311-ab3426394381 - golang.org/x/sys v0.0.0-20200808120158-1030fc2bf1d9 // indirect - golang.org/x/tools v0.0.0-20200808161706-5bf02b21f123 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + golang.org/x/crypto v0.45.0 + golang.org/x/net v0.47.0 + golang.org/x/sys v0.38.0 + golang.org/x/time v0.14.0 ) -go 1.13 +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +go 1.24.0 + +toolchain go1.24.2 diff --git a/vendor/github.com/xtaci/kcp-go/v5/go.sum b/vendor/github.com/xtaci/kcp-go/v5/go.sum index d583de7c..7eb7ca81 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/go.sum +++ b/vendor/github.com/xtaci/kcp-go/v5/go.sum @@ -1,71 +1,100 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/klauspost/cpuid v1.2.4/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= -github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= -github.com/klauspost/reedsolomon v1.9.9 h1:qCL7LZlv17xMixl55nq2/Oa1Y86nfO8EqDfv2GHND54= -github.com/klauspost/reedsolomon v1.9.9/go.mod h1:O7yFFHiQwDR6b2t63KPUpccPtNdp5ADgh1gg4fd12wo= -github.com/mmcloughlin/avo v0.0.0-20200803215136-443f81d77104 h1:ULR/QWMgcgRiZLUjSSJMU+fW+RDMstRdmnDWj9Q+AsA= -github.com/mmcloughlin/avo v0.0.0-20200803215136-443f81d77104/go.mod h1:wqKykBG2QzQDJEzvRkcS8x6MiSJkF52hXZsXcjaB3ls= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/reedsolomon v1.12.0 h1:I5FEp3xSwVCcEh3F5A7dofEfhXdF/bWhQWPH+XwBFno= +github.com/klauspost/reedsolomon v1.12.0/go.mod h1:EPLZJeh4l27pUGC3aXOjheaoh1I9yut7xTURiW3LQ9Y= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/templexxx/cpu v0.0.1 h1:hY4WdLOgKdc8y13EYklu9OUTXik80BkxHoWvTO6MQQY= -github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= -github.com/templexxx/cpu v0.0.7 h1:pUEZn8JBy/w5yzdYWgx+0m0xL9uk6j4K91C5kOViAzo= -github.com/templexxx/cpu v0.0.7/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= -github.com/templexxx/cpu v0.0.8 h1:va6GebSxedVdR5XEyPJD49t94p5ZsjWO6Wh/PfbmZnc= -github.com/templexxx/cpu v0.0.8/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= -github.com/templexxx/xorsimd v0.4.1 h1:iUZcywbOYDRAZUasAs2eSCUW8eobuZDy0I9FJiORkVg= -github.com/templexxx/xorsimd v0.4.1/go.mod h1:W+ffZz8jJMH2SXwuKu9WhygqBMbFnp14G2fqEr8qaNo= -github.com/tjfoc/gmsm v1.3.2 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM= -github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= +github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= +github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -golang.org/x/arch v0.0.0-20190909030613-46d78d1859ac/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200808120158-1030fc2bf1d9 h1:yi1hN8dcqI9l8klZfy4B8mJvFmmAxJEePIQQFNSd7Cs= -golang.org/x/sys v0.0.0-20200808120158-1030fc2bf1d9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200425043458-8463f397d07c h1:iHhCR0b26amDCiiO+kBguKZom9aMF+NrFxh9zeKR/XU= -golang.org/x/tools v0.0.0-20200425043458-8463f397d07c/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200808161706-5bf02b21f123 h1:4JSJPND/+4555t1HfXYF4UEqDqiSKCgeV0+hbA8hMs4= -golang.org/x/tools v0.0.0-20200808161706-5bf02b21f123/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/vendor/github.com/xtaci/kcp-go/v5/kcp-go.png b/vendor/github.com/xtaci/kcp-go/v5/kcp-go.png deleted file mode 100644 index 151b7c4f7c49c1a8382552322787a3e7f04ac3c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9153 zcmdUzMOPe5(?HR|H88ll1b5fq65QP-xZB|F?m>dP1P=oQx4~UQa0~9dJiG5Ne2ebe zrx#VVsB^0OMyo2zpdk|?LqS2I$;nEoeQ@MsnIQr`cHFHzAQTiTj;*Ams+^=GxvHy^ zm92v%6qIaqx;BE2`Y5nKUkwrIGbS`ckyokqks)RhI*$Z4c^VXw_+SDHW4q^9XrfM- z;%z-BT_#UUtcE27N=Bv=GiVe>CpEF9rC(mZoX%hP-KMhNCxpE?U_r%bOuW1(nlR+K zg{)D?SC+KYwCr*lAef2xsmCQ}LpkJ2NH^6HjZ zS=_Ki~j=B@q`W;!B2A}>R%92w1dBq zzek*7$m!&PWS_UK!y`v`YZMP6;&w<3OvP@plo(mr;LLPv4`=#svb-5oZv3HHS%}Tx zqXfgUXNowshQp5ncg%TUzNTe0Q~bVAgAe|yMx-dL&zFH`HEAWcAJPi)IVc~7QwV5; zt7D^Zh=YwhAtV0_a%q3jA>c*T%5+MbXyfB$QnN}t+qQz2mRlkaq-KsHy*?X@gr_w` zIF$p~%a=OT?X16y;4|!}5%GGzuPT87pe;KEZE?YDdGjqmR}U=`W5 zU1uJsIM}Eg4J{R9#>Spx!!s3%rfc0t3)LM|f)Xx_XGOQ)Pd2`zFVxQ|LAQgn11!pQ z#`qmvok51dxznmSw4d*dt2y1joKxk27XPT&AC{Bas`T0b7958SH7Lbxju)iYyiq*|sq zW%h&@4CaQW^(3?(T5U)*6EZ?lp-E@s(N7E0r;QsRYHqA-K5Bb>8CYjtdD*?i-gi6L zj}-;<=B@j{80qXww$YgHJ)rVVO=98T)$+Xs0dNnAMTot~Xy0K`K@1ba=zP##+h{Og z98734G0H4>Z6Oj1$tgE5Oal&1s9FQM1K_U--7>5lSlk((EBMnkb30;PFuF5}5XfT& z!6-zW4fP8q90dheSb`0CXB3uM@G=EU6dg7wjN+>-WlCHrg}+o7vsebjj}bsA@k;zW zWiRD$a`Omo>8Fm!8}io?qPe5=viAeyvoFzU}lv?nzdxuQ8^2djwpjKn<^sG=aiy!i6ac71fw0J(jzW=`=hn# zYfMX&UWRol-@=L&*^9hY&FBfzex%i=`HsPlNsj6J`1l0)Xg}Is2|dzW>HozTHO{z5 zewWoO%29u<;H;oLCbZ+eMZG1x{T%;=v&?5M#pbbvyG3u6f7N^y0lqV&RU!&T#<~?6hhp3bc<_z3&_K7D+a`3niXseXn3r7lcIR0J!e%7nV z=+H`A^sg1xbJUYu6<8Ht?O0uFnYP6^WOMKK;BeMilF7MArips}??7dsQ0y5nKLbudTj7^w`d<>Js`Nz7V9k8ZMD78BiO!ME(WeFK z0XezUMU_sjqw4q)rHo0VT!f#DKh+ rPWxmb2Fy&lpd?t7ZrM6=*p4DcCmHBREt%LHs&& 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 diff --git a/vendor/github.com/xtaci/kcp-go/v5/kcp.go b/vendor/github.com/xtaci/kcp-go/v5/kcp.go index 0c6c304f..b2d506a8 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/kcp.go +++ b/vendor/github.com/xtaci/kcp-go/v5/kcp.go @@ -1,98 +1,117 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + package kcp import ( + "container/heap" "encoding/binary" "sync/atomic" "time" ) +// KCP Protocol Constants const ( - IKCP_RTO_NDL = 30 // no delay min rto - IKCP_RTO_MIN = 100 // normal min rto - IKCP_RTO_DEF = 200 - IKCP_RTO_MAX = 60000 - IKCP_CMD_PUSH = 81 // cmd: push data - IKCP_CMD_ACK = 82 // cmd: ack - IKCP_CMD_WASK = 83 // cmd: window probe (ask) - IKCP_CMD_WINS = 84 // cmd: window size (tell) - IKCP_ASK_SEND = 1 // need to send IKCP_CMD_WASK - IKCP_ASK_TELL = 2 // need to send IKCP_CMD_WINS - IKCP_WND_SND = 32 - IKCP_WND_RCV = 32 - IKCP_MTU_DEF = 1400 - IKCP_ACK_FAST = 3 - IKCP_INTERVAL = 100 - IKCP_OVERHEAD = 24 - IKCP_DEADLINK = 20 - IKCP_THRESH_INIT = 2 - IKCP_THRESH_MIN = 2 - IKCP_PROBE_INIT = 7000 // 7 secs to probe window size - IKCP_PROBE_LIMIT = 120000 // up to 120 secs to probe window - IKCP_SN_OFFSET = 12 + // Retransmission Timeout (RTO) bounds, in milliseconds + IKCP_RTO_NDL = 30 // no-delay mode: minimum RTO (ms) + IKCP_RTO_MIN = 100 // normal mode: minimum RTO (ms) + IKCP_RTO_DEF = 200 // default RTO (ms) + IKCP_RTO_MAX = 60000 // maximum RTO (ms), 60 seconds + + // Command types for the KCP segment header (cmd field) + IKCP_CMD_PUSH = 81 // cmd: push data + IKCP_CMD_ACK = 82 // cmd: acknowledge + IKCP_CMD_WASK = 83 // cmd: window probe request (ask) + IKCP_CMD_WINS = 84 // cmd: window size response (tell) + + // Probe flags (bitfield), set in kcp.probe to schedule probe commands + IKCP_ASK_SEND = 1 // schedule sending IKCP_CMD_WASK + IKCP_ASK_TELL = 2 // schedule sending IKCP_CMD_WINS + + // Default window and MTU sizes + IKCP_WND_SND = 32 // default send window size (packets) + IKCP_WND_RCV = 32 // default receive window size (packets) + IKCP_MTU_DEF = 1400 // default MTU (bytes, not including UDP/IP header) + + // Protocol parameters + IKCP_ACK_FAST = 3 // fast retransmit trigger threshold (duplicate ACK count) + IKCP_INTERVAL = 100 // default flush interval (ms) + IKCP_OVERHEAD = 24 // per-segment header size: conv(4) + cmd(1) + frg(1) + wnd(2) + ts(4) + sn(4) + una(4) + len(4) + IKCP_DEADLINK = 20 // max retransmissions before declaring dead link + IKCP_THRESH_INIT = 2 // initial slow-start threshold (packets) + IKCP_THRESH_MIN = 2 // minimum slow-start threshold (packets) + IKCP_PROBE_INIT = 500 // initial window probe timeout (ms) + IKCP_PROBE_LIMIT = 120000 // maximum window probe timeout (ms), 120 seconds + IKCP_SN_OFFSET = 12 // byte offset of sequence number (sn) within the segment header ) -// monotonic reference time point -var refTime time.Time = time.Now() +type PacketType int8 -// currentMs returns current elapsed monotonic milliseconds since program startup -func currentMs() uint32 { return uint32(time.Since(refTime) / time.Millisecond) } - -// output_callback is a prototype which ought capture conn and call conn.Write -type output_callback func(buf []byte, size int) +const ( + IKCP_PACKET_REGULAR PacketType = iota + IKCP_PACKET_FEC +) -/* encode 8 bits unsigned int */ -func ikcp_encode8u(p []byte, c byte) []byte { - p[0] = c - return p[1:] -} +type FlushType int8 -/* decode 8 bits unsigned int */ -func ikcp_decode8u(p []byte, c *byte) []byte { - *c = p[0] - return p[1:] -} +const ( + IKCP_FLUSH_ACKONLY FlushType = 1 << iota + IKCP_FLUSH_FULL +) -/* encode 16 bits unsigned int (lsb) */ -func ikcp_encode16u(p []byte, w uint16) []byte { - binary.LittleEndian.PutUint16(p, w) - return p[2:] -} +type KCPLogType int32 -/* decode 16 bits unsigned int (lsb) */ -func ikcp_decode16u(p []byte, w *uint16) []byte { - *w = binary.LittleEndian.Uint16(p) - return p[2:] -} +const ( + IKCP_LOG_OUTPUT KCPLogType = 1 << iota + IKCP_LOG_INPUT + IKCP_LOG_SEND + IKCP_LOG_RECV + IKCP_LOG_OUT_ACK + IKCP_LOG_OUT_PUSH + IKCP_LOG_OUT_WASK + IKCP_LOG_OUT_WINS + IKCP_LOG_IN_ACK + IKCP_LOG_IN_PUSH + IKCP_LOG_IN_WASK + IKCP_LOG_IN_WINS +) -/* encode 32 bits unsigned int (lsb) */ -func ikcp_encode32u(p []byte, l uint32) []byte { - binary.LittleEndian.PutUint32(p, l) - return p[4:] -} +const ( + IKCP_LOG_OUTPUT_ALL = IKCP_LOG_OUTPUT | IKCP_LOG_OUT_ACK | IKCP_LOG_OUT_PUSH | IKCP_LOG_OUT_WASK | IKCP_LOG_OUT_WINS + IKCP_LOG_INPUT_ALL = IKCP_LOG_INPUT | IKCP_LOG_IN_ACK | IKCP_LOG_IN_PUSH | IKCP_LOG_IN_WASK | IKCP_LOG_IN_WINS + IKCP_LOG_ALL = IKCP_LOG_OUTPUT_ALL | IKCP_LOG_INPUT_ALL | IKCP_LOG_SEND | IKCP_LOG_RECV +) -/* decode 32 bits unsigned int (lsb) */ -func ikcp_decode32u(p []byte, l *uint32) []byte { - *l = binary.LittleEndian.Uint32(p) - return p[4:] -} +// monotonic reference time point +var refTime time.Time = time.Now() -func _imin_(a, b uint32) uint32 { - if a <= b { - return a - } - return b -} +// currentMs returns current elapsed monotonic milliseconds since program startup +func currentMs() uint32 { return uint32(time.Since(refTime) / time.Millisecond) } -func _imax_(a, b uint32) uint32 { - if a >= b { - return a - } - return b -} +// output_callback is a prototype which ought capture conn and call conn.Write +type output_callback func(buf []byte, size int) -func _ibound_(lower, middle, upper uint32) uint32 { - return _imin_(_imax_(lower, middle), upper) -} +// logoutput_callback is a prototype which logging kcp trace output +type logoutput_callback func(msg string, args ...any) func _itimediff(later, earlier uint32) int32 { return (int32)(later - earlier) @@ -115,46 +134,117 @@ type segment struct { data []byte } -// encode a segment into buffer +// encode a segment header into buffer func (seg *segment) encode(ptr []byte) []byte { - ptr = ikcp_encode32u(ptr, seg.conv) - ptr = ikcp_encode8u(ptr, seg.cmd) - ptr = ikcp_encode8u(ptr, seg.frg) - ptr = ikcp_encode16u(ptr, seg.wnd) - ptr = ikcp_encode32u(ptr, seg.ts) - ptr = ikcp_encode32u(ptr, seg.sn) - ptr = ikcp_encode32u(ptr, seg.una) - ptr = ikcp_encode32u(ptr, uint32(len(seg.data))) + _ = ptr[IKCP_OVERHEAD-1] // BCE hint + binary.LittleEndian.PutUint32(ptr, seg.conv) + ptr[4] = seg.cmd + ptr[5] = seg.frg + binary.LittleEndian.PutUint16(ptr[6:], seg.wnd) + binary.LittleEndian.PutUint32(ptr[8:], seg.ts) + binary.LittleEndian.PutUint32(ptr[12:], seg.sn) + binary.LittleEndian.PutUint32(ptr[16:], seg.una) + binary.LittleEndian.PutUint32(ptr[20:], uint32(len(seg.data))) atomic.AddUint64(&DefaultSnmp.OutSegs, 1) - return ptr + return ptr[IKCP_OVERHEAD:] +} + +// segmentHeap is a min-heap of segments, used for receiving segments in order +type segmentHeap struct { + segments []segment + marks map[uint32]struct{} // to avoid duplicates +} + +func newSegmentHeap() *segmentHeap { + h := &segmentHeap{ + marks: make(map[uint32]struct{}), + } + heap.Init(h) + return h } -// KCP defines a single KCP connection +func (h *segmentHeap) Len() int { return len(h.segments) } + +func (h *segmentHeap) Less(i, j int) bool { + return _itimediff(h.segments[j].sn, h.segments[i].sn) > 0 +} + +func (h *segmentHeap) Swap(i, j int) { h.segments[i], h.segments[j] = h.segments[j], h.segments[i] } +func (h *segmentHeap) Push(x any) { + h.segments = append(h.segments, x.(segment)) + h.marks[x.(segment).sn] = struct{}{} +} + +func (h *segmentHeap) Pop() any { + n := len(h.segments) + x := h.segments[n-1] + h.segments[n-1] = segment{} // clear reference to avoid memory leak + h.segments = h.segments[0 : n-1] + delete(h.marks, x.sn) + return x +} + +func (h *segmentHeap) Has(sn uint32) bool { + _, exists := h.marks[sn] + return exists +} + +// KCP defines a single KCP connection's protocol state machine. +// It is a pure ARQ (Automatic Repeat reQuest) implementation with no I/O. type KCP struct { - conv, mtu, mss, state uint32 - snd_una, snd_nxt, rcv_nxt uint32 - ssthresh uint32 - rx_rttvar, rx_srtt int32 - rx_rto, rx_minrto uint32 - snd_wnd, rcv_wnd, rmt_wnd, cwnd, probe uint32 - interval, ts_flush uint32 - nodelay, updated uint32 - ts_probe, probe_wait uint32 - dead_link, incr uint32 - - fastresend int32 - nocwnd, stream int32 - - snd_queue []segment - rcv_queue []segment - snd_buf []segment - rcv_buf []segment - - acklist []ackItem - - buffer []byte - reserved int - output output_callback + // Connection identity and framing + conv uint32 // conversation id, must be equal on both sides + mtu uint32 // maximum transmission unit (bytes) + mss uint32 // maximum segment size = mtu - IKCP_OVERHEAD + state uint32 // connection state, 0 = active, 0xFFFFFFFF = dead link + + // Sequence numbers and acknowledgment tracking + snd_una uint32 // oldest unacknowledged sequence number + snd_nxt uint32 // next sequence number to send + rcv_nxt uint32 // next expected sequence number to receive + + // Congestion control (RFC 5681 / RFC 6937) + ssthresh uint32 // slow-start threshold (packets) + rx_rttvar, rx_srtt int32 // RTT variance and smoothed RTT (ms), per RFC 6298 + rx_rto, rx_minrto uint32 // retransmission timeout and its lower bound (ms) + snd_wnd uint32 // local send window size (packets) + rcv_wnd uint32 // local receive window size (packets) + rmt_wnd uint32 // remote advertised window size (packets) + cwnd uint32 // congestion window (packets) + incr uint32 // bytes accumulated for cwnd increment + + // Window probing + probe uint32 // probe flags (IKCP_ASK_SEND / IKCP_ASK_TELL) + ts_probe uint32 // timestamp for next window probe (ms) + probe_wait uint32 // current probe timeout (ms), doubles on each retry + + // Timers and scheduling + interval uint32 // flush interval (ms) + ts_flush uint32 // next flush timestamp (ms) + nodelay uint32 // 0: normal, 1: no-delay mode (reduces RTO aggressively) + updated uint32 // whether Update() has been called at least once + + // Reliability + dead_link uint32 // max retransmit count before link is considered dead + fastresend int32 // fast retransmit trigger count, 0 = disabled + nocwnd int32 // 1 = disable congestion control + stream int32 // 1 = stream mode (no message boundaries), 0 = message mode + + // Logging + logmask KCPLogType + + // Data queues and buffers + snd_queue *RingBuffer[segment] // send queue: segments waiting to enter the send window + rcv_queue *RingBuffer[segment] // receive queue: ordered segments ready for user read + snd_buf *RingBuffer[segment] // send buffer: segments in-flight (sent but unacknowledged) + rcv_buf *segmentHeap // receive buffer: out-of-order segments awaiting reordering + + acklist []ackItem // pending ACKs to be flushed + + buffer []byte // pre-allocated encoding buffer for flush() + output output_callback // callback to write data to the underlying transport + + log logoutput_callback // trace log callback } type ackItem struct { @@ -175,7 +265,7 @@ func NewKCP(conv uint32, output output_callback) *KCP { kcp.rmt_wnd = IKCP_WND_RCV kcp.mtu = IKCP_MTU_DEF kcp.mss = kcp.mtu - IKCP_OVERHEAD - kcp.buffer = make([]byte, kcp.mtu) + kcp.buffer = make([]byte, (kcp.mtu+IKCP_OVERHEAD)*3) kcp.rx_rto = IKCP_RTO_DEF kcp.rx_minrto = IKCP_RTO_MIN kcp.interval = IKCP_INTERVAL @@ -183,53 +273,43 @@ func NewKCP(conv uint32, output output_callback) *KCP { kcp.ssthresh = IKCP_THRESH_INIT kcp.dead_link = IKCP_DEADLINK kcp.output = output + kcp.snd_buf = NewRingBuffer[segment](IKCP_WND_SND * 2) + kcp.rcv_queue = NewRingBuffer[segment](IKCP_WND_RCV * 2) + kcp.snd_queue = NewRingBuffer[segment](IKCP_WND_SND * 2) + kcp.rcv_buf = newSegmentHeap() return kcp } // newSegment creates a KCP segment func (kcp *KCP) newSegment(size int) (seg segment) { - seg.data = xmitBuf.Get().([]byte)[:size] + seg.data = defaultBufferPool.Get()[:size] return } -// delSegment recycles a KCP segment -func (kcp *KCP) delSegment(seg *segment) { +// recycleSegment recycles a KCP segment +func (kcp *KCP) recycleSegment(seg *segment) { if seg.data != nil { - xmitBuf.Put(seg.data) + defaultBufferPool.Put(seg.data) seg.data = nil } } -// ReserveBytes keeps n bytes untouched from the beginning of the buffer, -// the output_callback function should be aware of this. -// -// Return false if n >= mss -func (kcp *KCP) ReserveBytes(n int) bool { - if n >= int(kcp.mtu-IKCP_OVERHEAD) || n < 0 { - return false - } - kcp.reserved = n - kcp.mss = kcp.mtu - IKCP_OVERHEAD - uint32(n) - return true -} - // PeekSize checks the size of next message in the recv queue func (kcp *KCP) PeekSize() (length int) { - if len(kcp.rcv_queue) == 0 { + seg, ok := kcp.rcv_queue.Peek() + if !ok { return -1 } - seg := &kcp.rcv_queue[0] if seg.frg == 0 { return len(seg.data) } - if len(kcp.rcv_queue) < int(seg.frg+1) { + if kcp.rcv_queue.Len() < int(seg.frg+1) { return -1 } - for k := range kcp.rcv_queue { - seg := &kcp.rcv_queue[k] + for seg := range kcp.rcv_queue.ForEach { length += len(seg.data) if seg.frg == 0 { break @@ -256,46 +336,42 @@ func (kcp *KCP) Recv(buffer []byte) (n int) { } var fast_recover bool - if len(kcp.rcv_queue) >= int(kcp.rcv_wnd) { + if kcp.rcv_queue.Len() >= int(kcp.rcv_wnd) { fast_recover = true } // merge fragment - count := 0 - for k := range kcp.rcv_queue { - seg := &kcp.rcv_queue[k] + for { + seg, ok := kcp.rcv_queue.Pop() + if !ok { + break + } + copy(buffer, seg.data) buffer = buffer[len(seg.data):] n += len(seg.data) - count++ - kcp.delSegment(seg) + kcp.recycleSegment(&seg) if seg.frg == 0 { + kcp.debugLog(IKCP_LOG_RECV, "stream", kcp.stream, "conv", kcp.conv, "sn", seg.sn, "ts", seg.ts, "datalen", n) break } } - if count > 0 { - kcp.rcv_queue = kcp.remove_front(kcp.rcv_queue, count) - } // move available data from rcv_buf -> rcv_queue - count = 0 - for k := range kcp.rcv_buf { - seg := &kcp.rcv_buf[k] - if seg.sn == kcp.rcv_nxt && len(kcp.rcv_queue)+count < int(kcp.rcv_wnd) { + for kcp.rcv_buf.Len() > 0 { + seg := heap.Pop(kcp.rcv_buf).(segment) + if seg.sn == kcp.rcv_nxt && kcp.rcv_queue.Len() < int(kcp.rcv_wnd) { + kcp.rcv_queue.Push(seg) kcp.rcv_nxt++ - count++ } else { + // push back segment + heap.Push(kcp.rcv_buf, seg) break } } - if count > 0 { - kcp.rcv_queue = append(kcp.rcv_queue, kcp.rcv_buf[:count]...) - kcp.rcv_buf = kcp.remove_front(kcp.rcv_buf, count) - } - // fast recover - if len(kcp.rcv_queue) < int(kcp.rcv_wnd) && fast_recover { + if kcp.rcv_queue.Len() < int(kcp.rcv_wnd) && fast_recover { // ready to send back IKCP_CMD_WINS in ikcp_flush // tell remote my window size kcp.probe |= IKCP_ASK_TELL @@ -310,24 +386,24 @@ func (kcp *KCP) Send(buffer []byte) int { return -1 } + kcp.debugLog(IKCP_LOG_SEND, "stream", kcp.stream, "conv", kcp.conv, "datalen", len(buffer)) + // append to previous segment in streaming mode (if possible) if kcp.stream != 0 { - n := len(kcp.snd_queue) - if n > 0 { - seg := &kcp.snd_queue[n-1] - if len(seg.data) < int(kcp.mss) { - capacity := int(kcp.mss) - len(seg.data) - extend := capacity - if len(buffer) < capacity { - extend = len(buffer) + if n := kcp.snd_queue.Len(); n > 0 { + for seg := range kcp.snd_queue.ForEachReverse { + if len(seg.data) < int(kcp.mss) { + capacity := int(kcp.mss) - len(seg.data) + extend := min(len(buffer), capacity) + + // grow slice, the underlying cap is guaranteed to + // be larger than kcp.mss + oldlen := len(seg.data) + seg.data = seg.data[:oldlen+extend] + copy(seg.data[oldlen:], buffer) + buffer = buffer[extend:] } - - // grow slice, the underlying cap is guaranteed to - // be larger than kcp.mss - oldlen := len(seg.data) - seg.data = seg.data[:oldlen+extend] - copy(seg.data[oldlen:], buffer) - buffer = buffer[extend:] + break } } @@ -352,11 +428,7 @@ func (kcp *KCP) Send(buffer []byte) int { for i := 0; i < count; i++ { var size int - if len(buffer) > int(kcp.mss) { - size = int(kcp.mss) - } else { - size = len(buffer) - } + size = min(len(buffer), int(kcp.mss)) seg := kcp.newSegment(size) copy(seg.data, buffer[:size]) if kcp.stream == 0 { // message mode @@ -364,14 +436,16 @@ func (kcp *KCP) Send(buffer []byte) int { } else { // stream mode seg.frg = 0 } - kcp.snd_queue = append(kcp.snd_queue, seg) + + kcp.snd_queue.Push(seg) buffer = buffer[size:] } return 0 } +// update_ack updates the smoothed RTT and RTO based on a new RTT sample. +// Algorithm follows RFC 6298: Computing TCP's Retransmission Timer. func (kcp *KCP) update_ack(rtt int32) { - // https://tools.ietf.org/html/rfc6298 var rto uint32 if kcp.rx_srtt == 0 { kcp.rx_srtt = rtt @@ -391,33 +465,35 @@ func (kcp *KCP) update_ack(rtt int32) { kcp.rx_rttvar += (delta - kcp.rx_rttvar) >> 2 } } - rto = uint32(kcp.rx_srtt) + _imax_(kcp.interval, uint32(kcp.rx_rttvar)<<2) - kcp.rx_rto = _ibound_(kcp.rx_minrto, rto, IKCP_RTO_MAX) + rto = uint32(kcp.rx_srtt) + max(kcp.interval, uint32(kcp.rx_rttvar)<<2) + kcp.rx_rto = min(max(kcp.rx_minrto, rto), IKCP_RTO_MAX) } +// shrink_buf advances snd_una to the oldest unacknowledged segment in snd_buf. func (kcp *KCP) shrink_buf() { - if len(kcp.snd_buf) > 0 { - seg := &kcp.snd_buf[0] + if seg, ok := kcp.snd_buf.Peek(); ok { kcp.snd_una = seg.sn } else { kcp.snd_una = kcp.snd_nxt } } +// parse_ack marks a segment as acknowledged in snd_buf by sequence number. +// The segment is not removed immediately; it stays until snd_una advances past it, +// avoiding expensive shifts in the ring buffer. func (kcp *KCP) parse_ack(sn uint32) { if _itimediff(sn, kcp.snd_una) < 0 || _itimediff(sn, kcp.snd_nxt) >= 0 { return } - for k := range kcp.snd_buf { - seg := &kcp.snd_buf[k] + for seg := range kcp.snd_buf.ForEach { if sn == seg.sn { // mark and free space, but leave the segment here, // and wait until `una` to delete this, then we don't // have to shift the segments behind forward, // which is an expensive operation for large window seg.acked = 1 - kcp.delSegment(seg) + kcp.recycleSegment(seg) break } if _itimediff(sn, seg.sn) < 0 { @@ -426,35 +502,43 @@ func (kcp *KCP) parse_ack(sn uint32) { } } -func (kcp *KCP) parse_fastack(sn, ts uint32) { +// parse_fastack increments the fast-ack counter for segments with sn < the given sn. +// Returns 1 if any segment's fastack counter has reached the fast retransmit threshold. +func (kcp *KCP) parse_fastack(sn, ts uint32) int { + shouldFastAck := 0 if _itimediff(sn, kcp.snd_una) < 0 || _itimediff(sn, kcp.snd_nxt) >= 0 { - return + return 0 } - for k := range kcp.snd_buf { - seg := &kcp.snd_buf[k] + for seg := range kcp.snd_buf.ForEach { if _itimediff(sn, seg.sn) < 0 { break } else if sn != seg.sn && _itimediff(seg.ts, ts) <= 0 { - seg.fastack++ + if seg.fastack != 0xFFFFFFFF { + seg.fastack++ + if seg.fastack >= uint32(kcp.fastresend) { + shouldFastAck = 1 + } + } } } + + return shouldFastAck } +// parse_una removes all segments from snd_buf that have been cumulatively acknowledged +// (i.e., segments with sn < una). Returns the number of segments removed. func (kcp *KCP) parse_una(una uint32) int { count := 0 - for k := range kcp.snd_buf { - seg := &kcp.snd_buf[k] + for seg := range kcp.snd_buf.ForEach { if _itimediff(una, seg.sn) > 0 { - kcp.delSegment(seg) + kcp.recycleSegment(seg) count++ } else { break } } - if count > 0 { - kcp.snd_buf = kcp.remove_front(kcp.snd_buf, count) - } + kcp.snd_buf.Discard(count) return count } @@ -471,51 +555,31 @@ func (kcp *KCP) parse_data(newseg segment) bool { return true } - n := len(kcp.rcv_buf) - 1 - insert_idx := 0 repeat := false - for i := n; i >= 0; i-- { - seg := &kcp.rcv_buf[i] - if seg.sn == sn { - repeat = true - break - } - if _itimediff(sn, seg.sn) > 0 { - insert_idx = i + 1 - break - } - } - - if !repeat { + if !kcp.rcv_buf.Has(sn) { // replicate the content if it's new - dataCopy := xmitBuf.Get().([]byte)[:len(newseg.data)] + dataCopy := defaultBufferPool.Get()[:len(newseg.data)] copy(dataCopy, newseg.data) newseg.data = dataCopy - if insert_idx == n+1 { - kcp.rcv_buf = append(kcp.rcv_buf, newseg) - } else { - kcp.rcv_buf = append(kcp.rcv_buf, segment{}) - copy(kcp.rcv_buf[insert_idx+1:], kcp.rcv_buf[insert_idx:]) - kcp.rcv_buf[insert_idx] = newseg - } + // insert the new segment into rcv_buf + heap.Push(kcp.rcv_buf, newseg) + } else { + repeat = true } // move available data from rcv_buf -> rcv_queue - count := 0 - for k := range kcp.rcv_buf { - seg := &kcp.rcv_buf[k] - if seg.sn == kcp.rcv_nxt && len(kcp.rcv_queue)+count < int(kcp.rcv_wnd) { + for kcp.rcv_buf.Len() > 0 { + seg := heap.Pop(kcp.rcv_buf).(segment) + if seg.sn == kcp.rcv_nxt && kcp.rcv_queue.Len() < int(kcp.rcv_wnd) { + kcp.rcv_queue.Push(seg) kcp.rcv_nxt++ - count++ } else { + // push back segment + heap.Push(kcp.rcv_buf, seg) break } } - if count > 0 { - kcp.rcv_queue = append(kcp.rcv_queue, kcp.rcv_buf[:count]...) - kcp.rcv_buf = kcp.remove_front(kcp.rcv_buf, count) - } return repeat } @@ -526,38 +590,39 @@ func (kcp *KCP) parse_data(newseg segment) bool { // codecs. // // 'ackNoDelay' will trigger immediate ACK, but surely it will not be efficient in bandwidth -func (kcp *KCP) Input(data []byte, regular, ackNoDelay bool) int { +func (kcp *KCP) Input(data []byte, pktType PacketType, ackNoDelay bool) int { snd_una := kcp.snd_una if len(data) < IKCP_OVERHEAD { return -1 } var latest uint32 // the latest ack packet - var flag int + var updateRTT int var inSegs uint64 - var windowSlides bool + var flushSegments int // signal to flush segments for { - var ts, sn, length, una, conv uint32 - var wnd uint16 - var cmd, frg uint8 - if len(data) < int(IKCP_OVERHEAD) { break } - data = ikcp_decode32u(data, &conv) + _ = data[IKCP_OVERHEAD-1] // BCE hint + conv := binary.LittleEndian.Uint32(data) + cmd := data[4] + frg := data[5] + wnd := binary.LittleEndian.Uint16(data[6:]) + ts := binary.LittleEndian.Uint32(data[8:]) + sn := binary.LittleEndian.Uint32(data[12:]) + una := binary.LittleEndian.Uint32(data[16:]) + length := binary.LittleEndian.Uint32(data[20:]) + data = data[IKCP_OVERHEAD:] + if conv != kcp.conv { return -1 } - data = ikcp_decode8u(data, &cmd) - data = ikcp_decode8u(data, &frg) - data = ikcp_decode16u(data, &wnd) - data = ikcp_decode32u(data, &ts) - data = ikcp_decode32u(data, &sn) - data = ikcp_decode32u(data, &una) - data = ikcp_decode32u(data, &length) + kcp.debugLog(IKCP_LOG_INPUT, "conv", conv, "cmd", cmd, "frg", frg, "wnd", wnd, "ts", ts, "sn", sn, "una", una, "len", length, "datalen", len(data)) + if len(data) < int(length) { return -2 } @@ -568,46 +633,45 @@ func (kcp *KCP) Input(data []byte, regular, ackNoDelay bool) int { } // only trust window updates from regular packets. i.e: latest update - if regular { + if pktType == IKCP_PACKET_REGULAR { kcp.rmt_wnd = uint32(wnd) } if kcp.parse_una(una) > 0 { - windowSlides = true + flushSegments |= 1 } kcp.shrink_buf() - if cmd == IKCP_CMD_ACK { + switch cmd { + case IKCP_CMD_ACK: + kcp.debugLog(IKCP_LOG_IN_ACK, "conv", conv, "sn", sn, "una", una, "ts", ts, "rto", kcp.rx_rto) kcp.parse_ack(sn) - kcp.parse_fastack(sn, ts) - flag |= 1 + flushSegments |= kcp.parse_fastack(sn, ts) + updateRTT |= 1 latest = ts - } else if cmd == IKCP_CMD_PUSH { + case IKCP_CMD_PUSH: repeat := true if _itimediff(sn, kcp.rcv_nxt+kcp.rcv_wnd) < 0 { kcp.ack_push(sn, ts) if _itimediff(sn, kcp.rcv_nxt) >= 0 { - var seg segment - seg.conv = conv - seg.cmd = cmd - seg.frg = frg - seg.wnd = wnd - seg.ts = ts - seg.sn = sn - seg.una = una - seg.data = data[:length] // delayed data copying - repeat = kcp.parse_data(seg) + repeat = kcp.parse_data(segment{ + conv: conv, cmd: cmd, frg: frg, wnd: wnd, + ts: ts, sn: sn, una: una, + data: data[:length], // delayed data copying + }) } } - if regular && repeat { + if pktType == IKCP_PACKET_REGULAR && repeat { atomic.AddUint64(&DefaultSnmp.RepeatSegs, 1) } - } else if cmd == IKCP_CMD_WASK { + kcp.debugLog(IKCP_LOG_IN_PUSH, "conv", conv, "sn", sn, "una", una, "ts", ts, "packettype", pktType, "repeat", repeat) + case IKCP_CMD_WASK: // ready to send back IKCP_CMD_WINS in Ikcp_flush // tell remote my window size kcp.probe |= IKCP_ASK_TELL - } else if cmd == IKCP_CMD_WINS { - // do nothing - } else { + kcp.debugLog(IKCP_LOG_IN_WASK, "conv", conv, "wnd", wnd, "ts", ts) + case IKCP_CMD_WINS: + kcp.debugLog(IKCP_LOG_IN_WINS, "conv", conv, "wnd", wnd, "ts", ts) + default: return -3 } @@ -618,14 +682,15 @@ func (kcp *KCP) Input(data []byte, regular, ackNoDelay bool) int { // update rtt with the latest ts // ignore the FEC packet - if flag != 0 && regular { + if updateRTT != 0 && pktType == IKCP_PACKET_REGULAR { current := currentMs() if _itimediff(current, latest) >= 0 { kcp.update_ack(_itimediff(current, latest)) } } - // cwnd update when packet arrived + // Congestion window (cwnd) update on ACK arrival. + // Uses Reno-style algorithm: slow-start below ssthresh, then AIMD. if kcp.nocwnd == 0 { if _itimediff(kcp.snd_una, snd_una) > 0 { if kcp.cwnd < kcp.rmt_wnd { @@ -654,23 +719,44 @@ func (kcp *KCP) Input(data []byte, regular, ackNoDelay bool) int { } } - if windowSlides { // if window has slided, flush - kcp.flush(false) - } else if ackNoDelay && len(kcp.acklist) > 0 { // ack immediately - kcp.flush(true) + // Determine if we need to flush data segments or acks + if flushSegments != 0 { + // If window has slided or, a fastack should be triggered, + // Flush immediately. In previous implementations, we only + // send out fastacks when interval timeouts, so the resending packets + // have to wait until then. Now, we try to flush as soon as we can. + kcp.flush(IKCP_FLUSH_FULL) + } else if len(kcp.acklist) >= int(kcp.mtu/IKCP_OVERHEAD) { // clocking + // This serves as the clock for low-latency network.(i.e. the latency is less than the interval.) + // If the other end is waiting for confirmations, it has to want until the interval timeouts then + // the flush() is triggered to send out the una & acks. In low-latency network, the interval time is too long to wait, + // so acks have to be sent out immediately when there are too many. + kcp.flush(IKCP_FLUSH_ACKONLY) + } else if ackNoDelay && len(kcp.acklist) > 0 { // testing(xtaci): ack immediately if acNoDelay is set + kcp.flush(IKCP_FLUSH_ACKONLY) } return 0 } func (kcp *KCP) wnd_unused() uint16 { - if len(kcp.rcv_queue) < int(kcp.rcv_wnd) { - return uint16(int(kcp.rcv_wnd) - len(kcp.rcv_queue)) + if kcp.rcv_queue.Len() < int(kcp.rcv_wnd) { + return uint16(int(kcp.rcv_wnd) - kcp.rcv_queue.Len()) } return 0 } -// flush pending data -func (kcp *KCP) flush(ackOnly bool) uint32 { +// flush sends pending data through the KCP output callback. +// This is the core scheduling function, organized in 6 phases: +// +// Phase 1: Flush pending ACKs +// Phase 2: Window probing (when remote window is zero) +// Phase 3: Send window probe commands (WASK/WINS) +// Phase 4: Move segments from snd_queue to snd_buf (sliding window) +// Phase 5: Retransmit segments (initial, fast, early, RTO) +// Phase 6: Update SNMP counters and congestion window +// +// Returns the suggested interval (ms) until the next flush call. +func (kcp *KCP) flush(flushType FlushType) (nextUpdate uint32) { var seg segment seg.conv = kcp.conv seg.cmd = IKCP_CMD_ACK @@ -678,42 +764,47 @@ func (kcp *KCP) flush(ackOnly bool) uint32 { seg.una = kcp.rcv_nxt buffer := kcp.buffer - ptr := buffer[kcp.reserved:] // keep n bytes untouched + ptr := buffer // makeSpace makes room for writing makeSpace := func(space int) { size := len(buffer) - len(ptr) if size+space > int(kcp.mtu) { kcp.output(buffer, size) - ptr = buffer[kcp.reserved:] + ptr = buffer } } // flush bytes in buffer if there is any flushBuffer := func() { size := len(buffer) - len(ptr) - if size > kcp.reserved { + if size > 0 { kcp.output(buffer, size) } } - // flush acknowledges - for i, ack := range kcp.acklist { - makeSpace(IKCP_OVERHEAD) - // filter jitters caused by bufferbloat - if _itimediff(ack.sn, kcp.rcv_nxt) >= 0 || len(kcp.acklist)-1 == i { - seg.sn, seg.ts = ack.sn, ack.ts - ptr = seg.encode(ptr) - } - } - kcp.acklist = kcp.acklist[0:0] - - if ackOnly { // flash remain ack segments + defer func() { flushBuffer() - return kcp.interval + atomic.StoreUint64(&DefaultSnmp.RingBufferSndQueue, uint64(kcp.snd_queue.Len())) + atomic.StoreUint64(&DefaultSnmp.RingBufferRcvQueue, uint64(kcp.rcv_queue.Len())) + atomic.StoreUint64(&DefaultSnmp.RingBufferSndBuffer, uint64(kcp.snd_buf.Len())) + }() + + // --- Phase 1: Flush pending ACKs --- + if flushType == IKCP_FLUSH_ACKONLY || flushType == IKCP_FLUSH_FULL { + for i, ack := range kcp.acklist { + makeSpace(IKCP_OVERHEAD) + // filter jitters caused by bufferbloat + if _itimediff(ack.sn, kcp.rcv_nxt) >= 0 || len(kcp.acklist)-1 == i { + seg.sn, seg.ts = ack.sn, ack.ts + ptr = seg.encode(ptr) + kcp.debugLog(IKCP_LOG_OUT_ACK, "conv", seg.conv, "sn", seg.sn, "una", seg.una, "ts", seg.ts) + } + } + kcp.acklist = kcp.acklist[0:0] } - // probe window size (if remote window size equals zero) + // --- Phase 2: Window probing (when remote window is zero) --- if kcp.rmt_wnd == 0 { current := currentMs() if kcp.probe_wait == 0 { @@ -737,11 +828,12 @@ func (kcp *KCP) flush(ackOnly bool) uint32 { kcp.probe_wait = 0 } - // flush window probing commands + // --- Phase 3: Flush window probing commands --- if (kcp.probe & IKCP_ASK_SEND) != 0 { seg.cmd = IKCP_CMD_WASK makeSpace(IKCP_OVERHEAD) ptr = seg.encode(ptr) + kcp.debugLog(IKCP_LOG_OUT_WASK, "conv", seg.conv, "wnd", seg.wnd, "ts", seg.ts) } // flush window probing commands @@ -749,33 +841,36 @@ func (kcp *KCP) flush(ackOnly bool) uint32 { seg.cmd = IKCP_CMD_WINS makeSpace(IKCP_OVERHEAD) ptr = seg.encode(ptr) + kcp.debugLog(IKCP_LOG_OUT_WINS, "conv", seg.conv, "wnd", seg.wnd, "ts", seg.ts) } kcp.probe = 0 - // calculate window size - cwnd := _imin_(kcp.snd_wnd, kcp.rmt_wnd) + // --- Phase 4: Move segments from snd_queue to snd_buf (sliding window) --- + // Effective window = min(snd_wnd, rmt_wnd, cwnd) + cwnd := min(kcp.snd_wnd, kcp.rmt_wnd) if kcp.nocwnd == 0 { - cwnd = _imin_(kcp.cwnd, cwnd) + cwnd = min(kcp.cwnd, cwnd) } - // sliding window, controlled by snd_nxt && sna_una+cwnd newSegsCount := 0 - for k := range kcp.snd_queue { + for { if _itimediff(kcp.snd_nxt, kcp.snd_una+cwnd) >= 0 { break } - newseg := kcp.snd_queue[k] + + newseg, ok := kcp.snd_queue.Pop() + if !ok { + break + } + newseg.conv = kcp.conv newseg.cmd = IKCP_CMD_PUSH newseg.sn = kcp.snd_nxt - kcp.snd_buf = append(kcp.snd_buf, newseg) + kcp.snd_buf.Push(newseg) kcp.snd_nxt++ newSegsCount++ } - if newSegsCount > 0 { - kcp.snd_queue = kcp.remove_front(kcp.snd_queue, newSegsCount) - } // calculate resent resent := uint32(kcp.fastresend) @@ -783,76 +878,80 @@ func (kcp *KCP) flush(ackOnly bool) uint32 { resent = 0xffffffff } - // check for retransmissions + // --- Phase 5: Retransmit segments from snd_buf --- + // Determines which segments need (re)transmission: + // - Initial transmit (xmit == 0) + // - Fast retransmit (fastack >= fastresend threshold) + // - Early retransmit (fastack > 0, no new segments queued) + // - RTO-based retransmit (current >= resendts) current := currentMs() var change, lostSegs, fastRetransSegs, earlyRetransSegs uint64 - minrto := int32(kcp.interval) - - ref := kcp.snd_buf[:len(kcp.snd_buf)] // for bounds check elimination - for k := range ref { - segment := &ref[k] - needsend := false - if segment.acked == 1 { - continue - } - if segment.xmit == 0 { // initial transmit - needsend = true - segment.rto = kcp.rx_rto - segment.resendts = current + segment.rto - } else if segment.fastack >= resent { // fast retransmit - needsend = true - segment.fastack = 0 - segment.rto = kcp.rx_rto - segment.resendts = current + segment.rto - change++ - fastRetransSegs++ - } else if segment.fastack > 0 && newSegsCount == 0 { // early retransmit - needsend = true - segment.fastack = 0 - segment.rto = kcp.rx_rto - segment.resendts = current + segment.rto - change++ - earlyRetransSegs++ - } else if _itimediff(current, segment.resendts) >= 0 { // RTO - needsend = true - if kcp.nodelay == 0 { - segment.rto += kcp.rx_rto - } else { - segment.rto += kcp.rx_rto / 2 + nextUpdate = kcp.interval + + if flushType == IKCP_FLUSH_FULL { + for segment := range kcp.snd_buf.ForEach { + needsend := false + if segment.acked == 1 { + continue } - segment.fastack = 0 - segment.resendts = current + segment.rto - lostSegs++ - } + if segment.xmit == 0 { // initial transmit + needsend = true + segment.rto = kcp.rx_rto + segment.resendts = current + segment.rto + } else if segment.fastack >= resent && segment.fastack != 0xFFFFFFFF { // fast retransmit + needsend = true + segment.fastack = 0xFFFFFFFF // must wait until RTO to reset + segment.rto = kcp.rx_rto + segment.resendts = current + segment.rto + change++ + fastRetransSegs++ + } else if segment.fastack > 0 && segment.fastack != 0xFFFFFFFF && newSegsCount == 0 { // early retransmit + needsend = true + segment.fastack = 0xFFFFFFFF + segment.rto = kcp.rx_rto + segment.resendts = current + segment.rto + change++ + earlyRetransSegs++ + } else if _itimediff(current, segment.resendts) >= 0 { // RTO + needsend = true + if kcp.nodelay == 0 { + segment.rto += kcp.rx_rto + } else { + segment.rto += kcp.rx_rto / 2 + } + segment.fastack = 0 + segment.resendts = current + segment.rto + lostSegs++ + } + + if needsend { + current = currentMs() + segment.xmit++ + segment.ts = current + segment.wnd = seg.wnd + segment.una = seg.una - if needsend { - current = currentMs() - segment.xmit++ - segment.ts = current - segment.wnd = seg.wnd - segment.una = seg.una - - need := IKCP_OVERHEAD + len(segment.data) - makeSpace(need) - ptr = segment.encode(ptr) - copy(ptr, segment.data) - ptr = ptr[len(segment.data):] - - if segment.xmit >= kcp.dead_link { - kcp.state = 0xFFFFFFFF + need := IKCP_OVERHEAD + len(segment.data) + makeSpace(need) + ptr = segment.encode(ptr) + copy(ptr, segment.data) + ptr = ptr[len(segment.data):] + + kcp.debugLog(IKCP_LOG_OUT_PUSH, "conv", segment.conv, "sn", segment.sn, "frg", segment.frg, "una", segment.una, "ts", segment.ts, "xmit", segment.xmit, "datalen", len(segment.data)) + + if segment.xmit >= kcp.dead_link { + kcp.state = 0xFFFFFFFF // mark connection as dead + } } - } - // get the nearest rto - if rto := _itimediff(segment.resendts, current); rto > 0 && rto < minrto { - minrto = rto + // get the nearest rto + if rto := _itimediff(segment.resendts, current); rto > 0 && uint32(rto) < nextUpdate { + nextUpdate = uint32(rto) + } } } - // flash remain segments - flushBuffer() - - // counter updates + // --- Phase 6: Update SNMP counters and congestion window --- sum := lostSegs if lostSegs > 0 { atomic.AddUint64(&DefaultSnmp.LostSegs, lostSegs) @@ -871,24 +970,18 @@ func (kcp *KCP) flush(ackOnly bool) uint32 { // cwnd update if kcp.nocwnd == 0 { - // update ssthresh - // rate halving, https://tools.ietf.org/html/rfc6937 + // Update ssthresh after fast retransmit. + // Rate halving per RFC 6937: ssthresh = inflight / 2 if change > 0 { inflight := kcp.snd_nxt - kcp.snd_una - kcp.ssthresh = inflight / 2 - if kcp.ssthresh < IKCP_THRESH_MIN { - kcp.ssthresh = IKCP_THRESH_MIN - } + kcp.ssthresh = max(inflight/2, IKCP_THRESH_MIN) kcp.cwnd = kcp.ssthresh + resent kcp.incr = kcp.cwnd * kcp.mss } - // congestion control, https://tools.ietf.org/html/rfc5681 + // Congestion control after RTO: reset cwnd per RFC 5681 if lostSegs > 0 { - kcp.ssthresh = cwnd / 2 - if kcp.ssthresh < IKCP_THRESH_MIN { - kcp.ssthresh = IKCP_THRESH_MIN - } + kcp.ssthresh = max(cwnd/2, IKCP_THRESH_MIN) kcp.cwnd = 1 kcp.incr = kcp.mss } @@ -899,7 +992,7 @@ func (kcp *KCP) flush(ackOnly bool) uint32 { } } - return uint32(minrto) + return nextUpdate } // (deprecated) @@ -928,7 +1021,7 @@ func (kcp *KCP) Update() { if _itimediff(current, kcp.ts_flush) >= 0 { kcp.ts_flush = current + kcp.interval } - kcp.flush(false) + kcp.flush(IKCP_FLUSH_FULL) } } @@ -962,8 +1055,7 @@ func (kcp *KCP) Check() uint32 { tm_flush = _itimediff(ts_flush, current) - for k := range kcp.snd_buf { - seg := &kcp.snd_buf[k] + for seg := range kcp.snd_buf.ForEach { diff := _itimediff(seg.resendts, current) if diff <= 0 { return current @@ -986,20 +1078,13 @@ func (kcp *KCP) Check() uint32 { // SetMtu changes MTU size, default is 1400 func (kcp *KCP) SetMtu(mtu int) int { - if mtu < 50 || mtu < IKCP_OVERHEAD { - return -1 - } - if kcp.reserved >= int(kcp.mtu-IKCP_OVERHEAD) || kcp.reserved < 0 { + if mtu <= IKCP_OVERHEAD { return -1 } - buffer := make([]byte, mtu) - if buffer == nil { - return -2 - } kcp.mtu = uint32(mtu) - kcp.mss = kcp.mtu - IKCP_OVERHEAD - uint32(kcp.reserved) - kcp.buffer = buffer + kcp.mss = kcp.mtu - IKCP_OVERHEAD + kcp.buffer = make([]byte, (mtu+IKCP_OVERHEAD)*3) return 0 } @@ -1048,33 +1133,15 @@ func (kcp *KCP) WndSize(sndwnd, rcvwnd int) int { // WaitSnd gets how many packet is waiting to be sent func (kcp *KCP) WaitSnd() int { - return len(kcp.snd_buf) + len(kcp.snd_queue) -} - -// remove front n elements from queue -// if the number of elements to remove is more than half of the size. -// just shift the rear elements to front, otherwise just reslice q to q[n:] -// then the cost of runtime.growslice can always be less than n/2 -func (kcp *KCP) remove_front(q []segment, n int) []segment { - if n > cap(q)/2 { - newn := copy(q, q[n:]) - return q[:newn] - } - return q[n:] + return kcp.snd_buf.Len() + kcp.snd_queue.Len() } -// Release all cached outgoing segments -func (kcp *KCP) ReleaseTX() { - for k := range kcp.snd_queue { - if kcp.snd_queue[k].data != nil { - xmitBuf.Put(kcp.snd_queue[k].data) - } - } - for k := range kcp.snd_buf { - if kcp.snd_buf[k].data != nil { - xmitBuf.Put(kcp.snd_buf[k].data) - } +// SetLogger configures the trace logger +func (kcp *KCP) SetLogger(mask KCPLogType, logger logoutput_callback) { + if logger == nil { + kcp.logmask = 0 + return } - kcp.snd_queue = nil - kcp.snd_buf = nil + kcp.logmask = mask + kcp.log = logger } diff --git a/vendor/github.com/xtaci/kcp-go/v5/kcp_test.go b/vendor/github.com/xtaci/kcp-go/v5/kcp_test.go index 49d55d5a..ce087797 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/kcp_test.go +++ b/vendor/github.com/xtaci/kcp-go/v5/kcp_test.go @@ -1,8 +1,31 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + package kcp import ( + "container/heap" "io" - "net" + "log/slog" "sync" "testing" "time" @@ -18,11 +41,13 @@ func TestLossyConn1(t *testing.T) { client, err := lossyconn.NewLossyConn(0.1, 100) if err != nil { t.Fatal(err) + return } server, err := lossyconn.NewLossyConn(0.1, 100) if err != nil { t.Fatal(err) + return } testlink(t, client, server, 1, 10, 2, 1) } @@ -33,11 +58,13 @@ func TestLossyConn2(t *testing.T) { client, err := lossyconn.NewLossyConn(0.2, 100) if err != nil { t.Fatal(err) + return } server, err := lossyconn.NewLossyConn(0.2, 100) if err != nil { t.Fatal(err) + return } testlink(t, client, server, 1, 10, 2, 1) } @@ -48,11 +75,13 @@ func TestLossyConn3(t *testing.T) { client, err := lossyconn.NewLossyConn(0.3, 100) if err != nil { t.Fatal(err) + return } server, err := lossyconn.NewLossyConn(0.3, 100) if err != nil { t.Fatal(err) + return } testlink(t, client, server, 1, 10, 2, 1) } @@ -63,11 +92,13 @@ func TestLossyConn4(t *testing.T) { client, err := lossyconn.NewLossyConn(0.1, 100) if err != nil { t.Fatal(err) + return } server, err := lossyconn.NewLossyConn(0.1, 100) if err != nil { t.Fatal(err) + return } testlink(t, client, server, 1, 10, 2, 0) } @@ -96,11 +127,11 @@ func testlink(t *testing.T, client *lossyconn.LossyConn, server *lossyconn.Lossy } } - echoTester := func(s *UDPSession, raddr net.Addr) { + echoTester := func(s *UDPSession) { s.SetNoDelay(nodelay, interval, resend, nc) buf := make([]byte, 64) var rtt time.Duration - for i := 0; i < repeat; i++ { + for range repeat { start := time.Now() s.Write(buf) io.ReadFull(s, buf) @@ -114,22 +145,106 @@ func testlink(t *testing.T, client *lossyconn.LossyConn, server *lossyconn.Lossy } go echoServer(listener) - echoTester(sess, server.LocalAddr()) + echoTester(sess) } func BenchmarkFlush(b *testing.B) { kcp := NewKCP(1, func(buf []byte, size int) {}) - kcp.snd_buf = make([]segment, 1024) - for k := range kcp.snd_buf { - kcp.snd_buf[k].xmit = 1 - kcp.snd_buf[k].resendts = currentMs() + 10000 + kcp.snd_buf = NewRingBuffer[segment](1024) + for range kcp.snd_buf.MaxLen() { + kcp.snd_buf.Push(segment{xmit: 1, resendts: currentMs() + 10000}) } - b.ResetTimer() + b.ReportAllocs() var mu sync.Mutex - for i := 0; i < b.N; i++ { + for b.Loop() { mu.Lock() - kcp.flush(false) + kcp.flush(IKCP_FLUSH_FULL) mu.Unlock() } } + +// TestSegmentHeap tests the segmentHeap data structure +func TestSegmentHeap(t *testing.T) { + h := newSegmentHeap() + segments := []segment{ + {sn: 1}, + {sn: 2}, + {sn: 3}, + } + + for _, seg := range segments { + heap.Push(h, seg) + t.Logf("pushed segment with seq %d", seg.sn) + } + + if h.Len() != len(segments) { + t.Errorf("expected length %d, got %d", len(segments), h.Len()) + } + + for i := range segments { + seg := heap.Pop(h).(segment) + if seg.sn != segments[i].sn { + t.Errorf("expected seq %d, got %d", segments[i].sn, seg.sn) + } + } +} + +// BenchmarkDebugLog test DebugLog cost time with build tags debug on/off +// trace log on: +// +// go test -benchmem -run=^$ -bench ^BenchmarkDebugLog$ -tags debug +// +// trace log off: +func TestSetMtuBoundary(t *testing.T) { + kcp := NewKCP(0, func(buf []byte, size int) {}) + + tests := []struct { + name string + mtu int + want int // 0 = success, -1 = failure + }{ + {"negative", -1, -1}, + {"zero", 0, -1}, + {"below overhead", IKCP_OVERHEAD - 1, -1}, + {"equal overhead", IKCP_OVERHEAD, -1}, + {"one above overhead (minimum valid)", IKCP_OVERHEAD + 1, 0}, + {"typical MTU", 1400, 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := kcp.SetMtu(tt.mtu) + if got != tt.want { + t.Errorf("SetMtu(%d) = %d, want %d", tt.mtu, got, tt.want) + } + }) + } + + // verify mss is correctly derived after a valid SetMtu + kcp.SetMtu(1400) + if kcp.mss != 1400-IKCP_OVERHEAD { + t.Errorf("mss = %d, want %d", kcp.mss, 1400-IKCP_OVERHEAD) + } + + // minimum valid MTU yields mss = 1 + kcp.SetMtu(IKCP_OVERHEAD + 1) + if kcp.mss != 1 { + t.Errorf("mss = %d, want 1 for minimum valid MTU", kcp.mss) + } +} + +// go test -benchmem -run=^$ -bench ^BenchmarkDebugLog$ +func BenchmarkDebugLog(b *testing.B) { + kcp := &KCP{ + conv: 123, + snd_wnd: 456, + } + kcp.log = slog.Debug + + for b.Loop() { + // In release mode, this line of code will be completely 'erased' by the compiler, + // as if it doesn't exist at all, and even the parameter's interface conversion will not occur. + kcp.debugLog(IKCP_LOG_OUT_WASK, "conv", kcp.conv, "wnd", kcp.snd_wnd) + } +} diff --git a/vendor/github.com/xtaci/kcp-go/v5/kcp_trace_off.go b/vendor/github.com/xtaci/kcp-go/v5/kcp_trace_off.go new file mode 100644 index 00000000..06c6067f --- /dev/null +++ b/vendor/github.com/xtaci/kcp-go/v5/kcp_trace_off.go @@ -0,0 +1,6 @@ +//go:build !debug + +// if build tag debug is not set, debugLog is a no-op eliminated at compile time +package kcp + +func (kcp *KCP) debugLog(logtype KCPLogType, args ...any) {} diff --git a/vendor/github.com/xtaci/kcp-go/v5/kcp_trace_on.go b/vendor/github.com/xtaci/kcp-go/v5/kcp_trace_on.go new file mode 100644 index 00000000..6ff81aeb --- /dev/null +++ b/vendor/github.com/xtaci/kcp-go/v5/kcp_trace_on.go @@ -0,0 +1,39 @@ +//go:build debug + +// only build tag debug is set, then debugLog will be enabled in compile time +package kcp + +func (kcp *KCP) debugLog(logtype KCPLogType, args ...any) { + if kcp.logmask&logtype == 0 { + return + } + + var msg string + switch logtype { + case IKCP_LOG_OUTPUT: + msg = "[KCP OUTPUT]" + case IKCP_LOG_INPUT: + msg = "[KCP INPUT]" + case IKCP_LOG_SEND: + msg = "[KCP SEND]" + case IKCP_LOG_RECV: + msg = "[KCP RECV]" + case IKCP_LOG_OUT_ACK: + msg = "[KCP OUTPUT ACK]" + case IKCP_LOG_OUT_PUSH: + msg = "[KCP OUTPUT PUSH]" + case IKCP_LOG_OUT_WASK: + msg = "[KCP OUTPUT WASK]" + case IKCP_LOG_OUT_WINS: + msg = "[KCP OUTPUT WINS]" + case IKCP_LOG_IN_ACK: + msg = "[KCP INPUT ACK]" + case IKCP_LOG_IN_PUSH: + msg = "[KCP INPUT PUSH]" + case IKCP_LOG_IN_WASK: + msg = "[KCP INPUT WASK]" + case IKCP_LOG_IN_WINS: + msg = "[KCP INPUT WINS]" + } + kcp.log(msg, args...) +} diff --git a/vendor/github.com/xtaci/kcp-go/v5/pkg.go b/vendor/github.com/xtaci/kcp-go/v5/pkg.go new file mode 100644 index 00000000..8057a66a --- /dev/null +++ b/vendor/github.com/xtaci/kcp-go/v5/pkg.go @@ -0,0 +1,8 @@ +// Package kcp is a Reliable-UDP library for Go. +// +// This library intends to provide a smooth, resilient, ordered, +// error-checked and anonymous delivery of streams over UDP packets. +// +// The interfaces of this package aim to be compatible with +// net.Conn in the standard library, but offer powerful features for advanced users. +package kcp diff --git a/vendor/github.com/xtaci/kcp-go/v5/platform_generic.go b/vendor/github.com/xtaci/kcp-go/v5/platform_generic.go new file mode 100644 index 00000000..b1c160f8 --- /dev/null +++ b/vendor/github.com/xtaci/kcp-go/v5/platform_generic.go @@ -0,0 +1,29 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +//go:build !linux + +package kcp + +type platform struct{} + +func (sess *UDPSession) initPlatform() {} diff --git a/vendor/github.com/xtaci/kcp-go/v5/platform_linux.go b/vendor/github.com/xtaci/kcp-go/v5/platform_linux.go new file mode 100644 index 00000000..7512c0bf --- /dev/null +++ b/vendor/github.com/xtaci/kcp-go/v5/platform_linux.go @@ -0,0 +1,76 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +//go:build linux + +package kcp + +import ( + "net" + "syscall" + + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +type ( + platform struct { + batchConn batchConn + } + + // udpConn is an interface implemented by net.UDPConn. + // It can be used for interface assertions to check if a net.Conn is a UDP connection. + udpConn interface { + SyscallConn() (syscall.RawConn, error) + ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) + } + + // batchConn defines the interface used in batch IO + batchConn interface { + WriteBatch(ms []ipv4.Message, flags int) (int, error) + ReadBatch(ms []ipv4.Message, flags int) (int, error) + } +) + +// newBatchConn creates a batchConn based on the IP version of the provided net.PacketConn. +func newBatchConn(conn net.PacketConn) batchConn { + if _, ok := conn.(udpConn); !ok { + return nil + } + + // Resolve the local UDP address to determine IP version + addr, err := net.ResolveUDPAddr("udp", conn.LocalAddr().String()) + if err != nil { + return nil + } + + // Determine if the connection is IPv4 or IPv6 based on the local address + if addr.IP.To4() != nil { + return ipv4.NewPacketConn(conn) + } + + return ipv6.NewPacketConn(conn) +} + +func (sess *UDPSession) initPlatform() { + sess.platform.batchConn = newBatchConn(sess.conn) +} diff --git a/vendor/github.com/xtaci/kcp-go/v5/readloop.go b/vendor/github.com/xtaci/kcp-go/v5/readloop.go index 697395ab..2ff038a9 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/readloop.go +++ b/vendor/github.com/xtaci/kcp-go/v5/readloop.go @@ -1,39 +1,100 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + package kcp import ( + "net" "sync/atomic" "github.com/pkg/errors" ) +func sameUDPAddr(a, b *net.UDPAddr) bool { + if a == nil || b == nil { + return false + } + if a.Port != b.Port || a.Zone != b.Zone { + return false + } + return a.IP.Equal(b.IP) +} + +// defaultReadLoop is the standard procedure for reading from a connection func (s *UDPSession) defaultReadLoop() { buf := make([]byte, mtuLimit) - var src string + + var src *net.UDPAddr + var srcStr string + if s.remote != nil { + if udp, ok := s.remote.(*net.UDPAddr); ok { + src = udp + } else { + srcStr = s.remote.String() + } + } for { - if n, addr, err := s.conn.ReadFrom(buf); err == nil { - // make sure the packet is from the same source - if src == "" { // set source address - src = addr.String() - } else if addr.String() != src { + n, addr, err := s.conn.ReadFrom(buf) + if err != nil { + s.notifyReadError(errors.WithStack(err)) + return + } + + if s.isClosed() { + return + } + + // make sure the packet is from the same source + if src == nil && srcStr == "" { // set source address if nil + if udp, ok := addr.(*net.UDPAddr); ok { + src = udp + } else { + srcStr = addr.String() + } + } else if src != nil { + udp, ok := addr.(*net.UDPAddr) + if !ok || !sameUDPAddr(src, udp) { atomic.AddUint64(&DefaultSnmp.InErrs, 1) continue } - s.packetInput(buf[:n]) - } else { - s.notifyReadError(errors.WithStack(err)) - return + } else if addr.String() != srcStr { + atomic.AddUint64(&DefaultSnmp.InErrs, 1) + continue } + + s.packetInput(buf[:n]) } } +// defaultReadLoop is the standard procedure for reading and accepting connections on a listener func (l *Listener) defaultMonitor() { buf := make([]byte, mtuLimit) for { - if n, from, err := l.conn.ReadFrom(buf); err == nil { - l.packetInput(buf[:n], from) - } else { + n, from, err := l.conn.ReadFrom(buf) + if err != nil { l.notifyReadError(errors.WithStack(err)) return } + + l.packetInput(buf[:n], from) } } diff --git a/vendor/github.com/xtaci/kcp-go/v5/readloop_generic.go b/vendor/github.com/xtaci/kcp-go/v5/readloop_generic.go index 5dbe4f44..977a0dba 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/readloop_generic.go +++ b/vendor/github.com/xtaci/kcp-go/v5/readloop_generic.go @@ -1,4 +1,26 @@ -// +build !linux +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +//go:build !linux package kcp diff --git a/vendor/github.com/xtaci/kcp-go/v5/readloop_linux.go b/vendor/github.com/xtaci/kcp-go/v5/readloop_linux.go index be194afb..e8b9fc14 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/readloop_linux.go +++ b/vendor/github.com/xtaci/kcp-go/v5/readloop_linux.go @@ -1,81 +1,109 @@ -// +build linux +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +//go:build linux package kcp import ( "net" - "os" "sync/atomic" "github.com/pkg/errors" "golang.org/x/net/ipv4" - "golang.org/x/net/ipv6" ) -// the read loop for a client session +const ( + batchSize = 256 // max packets per recvmmsg/sendmmsg call +) + +// readLoop is the optimized read loop for Linux, utilizing the recvmmsg syscall +// to batch-receive multiple UDP packets in a single system call. func (s *UDPSession) readLoop() { // default version - if s.xconn == nil { + if s.platform.batchConn == nil { s.defaultReadLoop() return } // x/net version - var src string + var src *net.UDPAddr + var srcStr string + if s.remote != nil { + if udp, ok := s.remote.(*net.UDPAddr); ok { + src = udp + } else { + srcStr = s.remote.String() + } + } msgs := make([]ipv4.Message, batchSize) for k := range msgs { msgs[k].Buffers = [][]byte{make([]byte, mtuLimit)} } for { - if count, err := s.xconn.ReadBatch(msgs, 0); err == nil { - for i := 0; i < count; i++ { - msg := &msgs[i] - // make sure the packet is from the same source - if src == "" { // set source address if nil - src = msg.Addr.String() - } else if msg.Addr.String() != src { + count, err := s.platform.batchConn.ReadBatch(msgs, 0) + if err != nil { + s.notifyReadError(errors.WithStack(err)) + return + } + + if s.isClosed() { + return + } + + for i := range count { + msg := &msgs[i] + + // make sure the packet is from the same source + if src == nil && srcStr == "" { // set source address if nil + if udp, ok := msg.Addr.(*net.UDPAddr); ok { + src = udp + } else { + srcStr = msg.Addr.String() + } + } else if src != nil { + udp, ok := msg.Addr.(*net.UDPAddr) + if !ok || !sameUDPAddr(src, udp) { atomic.AddUint64(&DefaultSnmp.InErrs, 1) continue } - - // source and size has validated - s.packetInput(msg.Buffers[0][:msg.N]) + } else if msg.Addr.String() != srcStr { + atomic.AddUint64(&DefaultSnmp.InErrs, 1) + continue } - } else { - // compatibility issue: - // for linux kernel<=2.6.32, support for sendmmsg is not available - // an error of type os.SyscallError will be returned - if operr, ok := err.(*net.OpError); ok { - if se, ok := operr.Err.(*os.SyscallError); ok { - if se.Syscall == "recvmmsg" { - s.defaultReadLoop() - return - } - } - } - s.notifyReadError(errors.WithStack(err)) - return + + // source and size has validated + s.packetInput(msg.Buffers[0][:msg.N]) } } } -// monitor incoming data for all connections of server +// monitor is the optimized version of monitor for linux utilizing recvmmsg syscall func (l *Listener) monitor() { - var xconn batchConn - if _, ok := l.conn.(*net.UDPConn); ok { - addr, err := net.ResolveUDPAddr("udp", l.conn.LocalAddr().String()) - if err == nil { - if addr.IP.To4() != nil { - xconn = ipv4.NewPacketConn(l.conn) - } else { - xconn = ipv6.NewPacketConn(l.conn) - } - } - } + batchConn := newBatchConn(l.conn) // default version - if xconn == nil { + if batchConn == nil { l.defaultMonitor() return } @@ -87,25 +115,15 @@ func (l *Listener) monitor() { } for { - if count, err := xconn.ReadBatch(msgs, 0); err == nil { - for i := 0; i < count; i++ { - msg := &msgs[i] - l.packetInput(msg.Buffers[0][:msg.N], msg.Addr) - } - } else { - // compatibility issue: - // for linux kernel<=2.6.32, support for sendmmsg is not available - // an error of type os.SyscallError will be returned - if operr, ok := err.(*net.OpError); ok { - if se, ok := operr.Err.(*os.SyscallError); ok { - if se.Syscall == "recvmmsg" { - l.defaultMonitor() - return - } - } - } + count, err := batchConn.ReadBatch(msgs, 0) + if err != nil { l.notifyReadError(errors.WithStack(err)) return } + + for i := range count { + msg := &msgs[i] + l.packetInput(msg.Buffers[0][:msg.N], msg.Addr) + } } } diff --git a/vendor/github.com/xtaci/kcp-go/v5/ringbuffer.go b/vendor/github.com/xtaci/kcp-go/v5/ringbuffer.go new file mode 100644 index 00000000..94b21721 --- /dev/null +++ b/vendor/github.com/xtaci/kcp-go/v5/ringbuffer.go @@ -0,0 +1,244 @@ +// The MIT License (MIT) +// +// Copyright (c) 2025 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package kcp + +const ( + RINGBUFFER_MIN = 8 // minimum ring buffer capacity + RINGBUFFER_EXP = 1024 // growth threshold: below this, double; above this, grow by 25% +) + +// RingBuffer is a generic ring (circular) buffer that supports dynamic resizing. +// It provides efficient FIFO queue behavior with amortized constant time operations. +type RingBuffer[T any] struct { + head int // Index of the next element to be popped + tail int // Index of the next empty slot to push into + elements []T // Underlying slice storing elements in circular fashion +} + +// NewRingBuffer creates a new Ring with a specified initial capacity. +// If the provided size is <= 8, it defaults to 8. +func NewRingBuffer[T any](size int) *RingBuffer[T] { + if size <= RINGBUFFER_MIN { + size = RINGBUFFER_MIN // Ensure a minimum size + } + return &RingBuffer[T]{ + head: 0, + tail: 0, + elements: make([]T, size), + } +} + +// Len returns the number of elements currently in the ring. +func (r *RingBuffer[T]) Len() int { + if r.head <= r.tail { + return r.tail - r.head + } + // Wrapped case: elements from head to end + elements from start to tail + return len(r.elements) - r.head + r.tail +} + +// Push adds an element to the tail of the ring. +// If the ring is full, it will grow automatically. +func (r *RingBuffer[T]) Push(v T) { + if r.IsFull() { + r.grow() + } + r.elements[r.tail] = v + r.tail = (r.tail + 1) % len(r.elements) +} + +// Pop removes and returns the element from the head of the ring. +// It returns the zero value and false if the ring is empty. +func (r *RingBuffer[T]) Pop() (T, bool) { + var zero T + if r.Len() == 0 { + return zero, false + } + value := r.elements[r.head] + // Optional: clear the slot to avoid retaining references + r.elements[r.head] = zero + r.head = (r.head + 1) % len(r.elements) + return value, true +} + +// Peek returns the element at the head of the ring without removing it. +// It returns the zero value and false if the ring is empty. +func (r *RingBuffer[T]) Peek() (*T, bool) { + if r.Len() == 0 { + return nil, false + } + return &r.elements[r.head], true +} + +// Discard discards the first N elements from the ring buffer. +// Returns the number of elements that are actually discarded (<= n). +func (r *RingBuffer[T]) Discard(n int) int { + currentLen := r.Len() + n = min(n, currentLen) + if n == currentLen { + r.Clear() + return n + } + cap := len(r.elements) + end := r.head + n + if end < cap { + // no wrap: clear contiguous range + clear(r.elements[r.head:end]) + r.head = end + } else { + // wraps around + clear(r.elements[r.head:cap]) + clear(r.elements[:end-cap]) + r.head = end - cap + } + return n +} + +// ForEach iterates over each element in the ring buffer, +// applying the provided function. If the function returns false, +// iteration stops early. +func (r *RingBuffer[T]) ForEach(fn func(*T) bool) { + if r.Len() == 0 { + return + } + if r.head < r.tail { + // Contiguous data: [head ... tail) + for i := r.head; i < r.tail; i++ { + if !fn(&r.elements[i]) { + return + } + } + } else { + // Wrapped data: [head ... end) + [0 ... tail) + for i := r.head; i < len(r.elements); i++ { + if !fn(&r.elements[i]) { + return + } + } + for i := 0; i < r.tail; i++ { + if !fn(&r.elements[i]) { + return + } + } + } +} + +// ForEachReverse iterates over each element in the ring buffer in reverse order, +// applying the provided function. If the function returns false, +// iteration stops early. +func (r *RingBuffer[T]) ForEachReverse(fn func(*T) bool) { + if r.Len() == 0 { + return + } + + if r.head < r.tail { + // Contiguous data: [head ... tail) + for i := r.tail - 1; i >= r.head; i-- { + if !fn(&r.elements[i]) { + return + } + } + } else { + for i := r.tail - 1; i >= 0; i-- { + if !fn(&r.elements[i]) { + return + } + } + for i := len(r.elements) - 1; i >= r.head; i-- { + if !fn(&r.elements[i]) { + return + } + } + } +} + +// Clear resets the ring to an empty state and reinitializes the buffer. +func (r *RingBuffer[T]) Clear() { + var zero T + // Only clear elements that contain data to avoid retaining references + if r.head <= r.tail { + for i := r.head; i < r.tail; i++ { + r.elements[i] = zero + } + } else { + for i := r.head; i < len(r.elements); i++ { + r.elements[i] = zero + } + for i := 0; i < r.tail; i++ { + r.elements[i] = zero + } + } + r.head = 0 + r.tail = 0 +} + +// IsEmpty returns true if the ring has no elements. +func (r *RingBuffer[T]) IsEmpty() bool { + return r.head == r.tail +} + +// MaxLen returns the maximum capacity of the ring buffer. +func (r *RingBuffer[T]) MaxLen() int { + return len(r.elements) - 1 +} + +// IsFull returns true if the ring buffer is full (tail + 1 == head). +func (r *RingBuffer[T]) IsFull() bool { + return (r.tail+1)%len(r.elements) == r.head +} + +// grow increases the ring buffer's capacity when full. +// Growth policy: +// - If current size < RINGBUFFER_MIN : grow to RINGBUFFER_MIN +// - If size < RINGBUFFER_EXP: double the size +// - If size > RINGBUFFER_EXP: increase by 10% (rounded up) +func (r *RingBuffer[T]) grow() { + currentLength := r.Len() + currentSize := len(r.elements) + var newSize int + + switch { + case currentSize < RINGBUFFER_MIN: + newSize = RINGBUFFER_MIN + case currentSize < RINGBUFFER_EXP: + newSize = currentSize * 2 + default: + newSize = currentSize + (currentSize+9)/10 // +10%, rounded up + } + + newElements := make([]T, newSize) + + // Copy elements to new buffer preserving logical order + if r.head < r.tail { + // Contiguous data: [head ... tail) + copy(newElements, r.elements[r.head:r.tail]) + } else { + // Wrapped data: [head ... end) + [0 ... tail) + n := copy(newElements, r.elements[r.head:]) + copy(newElements[n:], r.elements[:r.tail]) + } + + r.head = 0 + r.tail = currentLength + r.elements = newElements +} diff --git a/vendor/github.com/xtaci/kcp-go/v5/ringbuffer_test.go b/vendor/github.com/xtaci/kcp-go/v5/ringbuffer_test.go new file mode 100644 index 00000000..1f9ddeb9 --- /dev/null +++ b/vendor/github.com/xtaci/kcp-go/v5/ringbuffer_test.go @@ -0,0 +1,301 @@ +package kcp + +import ( + "math/rand" + "testing" +) + +func TestRingSize(t *testing.T) { + r := NewRingBuffer[int](1) + if r.Len() != 0 { + t.Errorf("Expected length 0, got %d", r.Len()) + } + + // re-zero + for i := range 64 { + r.Push(i) + r.Pop() + if r.Len() != 0 { + t.Errorf("Expected length 0 after pushing and popping, got %d", r.Len()) + } + } + + left := 1024 * 1024 + for i := 0; i < left; i++ { + r.Push(i) + } + + for { + use := rand.Int() % (left + 1) + left -= use + for range use { + if _, ok := r.Pop(); !ok { + t.Errorf("Expected to pop value, but got none") + } + } + if r.Len() != left { + t.Errorf("Expected length %d after popping, got %d", left, r.Len()) + } + + t.Log("use", use, "left", left, "head", r.head, "tail", r.tail, "len", r.Len(), "maxlen", r.MaxLen()) + if left == 0 { + break + } + } + + if r.Len() != 0 { + t.Errorf("Expected length 0 after pushing and popping, got %d", r.Len()) + } +} + +func TestRingSize2(t *testing.T) { + // emulate a condition where head = 32, tail=31, and Len() = 63 + r := NewRingBuffer[int](64) + for i := range 63 { + r.Push(i) + } + for range 32 { + if _, ok := r.Pop(); !ok { + t.Errorf("Expected to pop value, but got none") + } + } + for i := range 32 { + r.Push(i + 63) + if r.Len() != i+(63-32+1) { + t.Errorf("Expected length %d after popping 31 elements, got %d", i+(63-32+1), r.Len()) + } + } + + // check head, tail and Len() + if r.head != 32 { + t.Errorf("Expected head to be 32, got %d", r.head) + } + if r.tail != 31 { + t.Errorf("Expected tail to be 31, got %d", r.tail) + } + if r.Len() != 63 { + t.Errorf("Expected length to be 63, got %d", r.Len()) + } + + t.Log("head", r.head, "tail", r.tail, "len", r.Len(), "maxlen", r.MaxLen()) + + // now we push one element to trigger grow + r.Push(95) + if r.MaxLen() != 127 { + t.Errorf("Expected capacity to be 128 after grow, got %d", r.MaxLen()) + } + + if r.Len() != 64 { + t.Errorf("Expected length to be 64 after pushing one element, got %d", r.Len()) + } + + if r.head != 0 { + t.Errorf("Expected head to be 0 after grow, got %d", r.head) + } + + if r.tail != 64 { + t.Errorf("Expected tail to be 64 after grow, got %d", r.tail) + } +} + +func TestRingBuffer(t *testing.T) { + r := NewRingBuffer[int](1) + if r.Len() != 0 { + t.Errorf("Expected length 0, got %d", r.Len()) + } + + for i := range 64 { + r.Push(i) + if r.Len() != i+1 { + t.Errorf("Expected length %d, got %d", i+1, r.Len()) + } + } + + for i := range 32 { + val, ok := r.Pop() + if !ok || val != i { + t.Errorf("Expected to pop %d, got %d (ok: %v)", i, val, ok) + } + } + + if r.Len() != 32 { + t.Errorf("Expected length 32 after popping 32 elements, got %d", r.Len()) + } + + // Push more elements to test the ring's behavior + size := r.Len() + for i := range 32 { + r.Push(i) + if r.Len() != i+1+size { + t.Errorf("Expected length %d, got %d", i+1+size, r.Len()) + } + } + + if r.Len() != 64 { + t.Errorf("Expected length 64 after pushing 32 more elements, got %d", r.Len()) + } + + // ringbuffer should be [32 ... 63, 0 ... 31] + evicted := 0 + round := 0 + expectedHead := []int{62, 28} + for !r.IsEmpty() { + evicted += r.Discard(30) + if round < len(expectedHead) { + if v, ok := r.Peek(); !ok || *v != expectedHead[round] { + t.Errorf("Invalid discard state: unexpected %d", *v) + } + } else if _, ok := r.Peek(); ok { + t.Errorf("Unexpected non-nil head element") + } + if r.Len()+evicted != 64 { + t.Errorf("Unexpected ringbuffer length after discard op") + } + round++ + } +} + +func TestRingBufferGrow(t *testing.T) { + initialSize := 4 + r := NewRingBuffer[int](initialSize) + + expectedCapacities := []int{8, 16, 32, 64, 128} + + // Push enough elements to trigger multiple grows + pushCount := 100 + for i := range pushCount { + r.Push(i) + + // Check if capacity is among expected + capacity := r.MaxLen() + valid := false + for _, ec := range expectedCapacities { + if capacity == ec-1 { + valid = true + break + } + } + if !valid && capacity <= RINGBUFFER_EXP { + t.Errorf("unexpected capacity during growth: %d", capacity) + } + } + + // Push to 1024 + for i := pushCount; i < 1024; i++ { + r.Push(i) + } + + // Make sure the growth is 10% increase after 4096 + r.Push(1) + if r.MaxLen() != 1126 { + t.Errorf("expected capacity to be 1126, got %d", r.MaxLen()) + } + + // Check values are preserved in correct order + for i := range pushCount { + v, ok := r.Pop() + if !ok { + t.Fatalf("expected to pop value at index %d", i) + return + } + if v != i { + t.Errorf("expected value %d, got %d", i, v) + } + } +} + +func TestRingForEach(t *testing.T) { + r := NewRingBuffer[int](10) + for i := range 10 { + r.Push(i) + } + + sum := 0 + i := 0 + r.ForEach(func(v *int) bool { + if *v != i { + t.Errorf("Expected index %d, got %d", i, *v) + } + i++ + sum += *v + return true + }) + + if sum != 45 { // 0 + 1 + ... + 9 = 45 + t.Errorf("Expected sum to be 45, got %d", sum) + } + + count := 0 + r.ForEach(func(v *int) bool { + count++ + return true + }) + + if count != 10 { + t.Errorf("Expected count to be 10, got %d", count) + } +} + +// TestRingBufferDiscardBoundary tests discarding elements that land exactly +// at the end of the backing array. This previously caused head to equal +// len(elements), leading to an index-out-of-range panic in Peek(). +func TestRingBufferDiscardBoundary(t *testing.T) { + // Create a ring buffer with backing array length 64 (MaxLen = 63). + r := NewRingBuffer[int](64) + + // Fill to capacity (63 elements), head=0, tail=63. + for i := range 63 { + r.Push(i) + } + + // Pop 1 element so head=1, push 1 so tail wraps to 0. Still 63 elements. + r.Pop() + r.Push(63) + // Now head=1, tail=0, len=63. + + // Discard exactly 63 elements. head + 63 = 64 = len(elements). + // This must wrap head to 0, not leave it at 64. + r.Discard(63) + + if r.Len() != 0 { + t.Fatalf("expected empty ring, got Len()=%d", r.Len()) + } + + // Push a new element and Peek — this panicked before the fix. + r.Push(99) + if v, ok := r.Peek(); !ok || *v != 99 { + t.Fatalf("expected Peek()=99, got %v (ok=%v)", v, ok) + } +} + +func TestRingForEachReverse(t *testing.T) { + r := NewRingBuffer[int](10) + for i := range 10 { + r.Push(i) + } + + sum := 0 + i := 9 + r.ForEachReverse(func(v *int) bool { + if *v != i { + t.Errorf("Expected index %d, got %d", i, *v) + } + i-- + sum += *v + return true + }) + + if sum != 45 { // 0 + 1 + ... + 9 = 45 + t.Errorf("Expected sum to be 45, got %d", sum) + } + + count := 0 + r.ForEachReverse(func(v *int) bool { + count++ + return true + }) + + if count != 10 { + t.Errorf("Expected count to be 10, got %d", count) + } +} diff --git a/vendor/github.com/xtaci/kcp-go/v5/sess.go b/vendor/github.com/xtaci/kcp-go/v5/sess.go index 2dedd745..4b7d6118 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/sess.go +++ b/vendor/github.com/xtaci/kcp-go/v5/sess.go @@ -1,13 +1,52 @@ -// Package kcp-go is a Reliable-UDP library for golang. +// The MIT License (MIT) // -// This library intents to provide a smooth, resilient, ordered, -// error-checked and anonymous delivery of streams over UDP packets. +// # Copyright (c) 2015 xtaci // -// The interfaces of this package aims to be compatible with -// net.Conn in standard library, but offers powerful features for advanced users. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// [THE GENERALIZED DATA PIPELINE FOR KCP-GO] +// +// Outgoing Data Pipeline: Incoming Data Pipeline: +// Stream (Input Data) Packet Network (Network Interface Card) +// | | +// v v +// KCP Output (Reliable Transport Layer) Reader/Listener (Reception Queue) +// | | +// v v +// FEC Encoding (Forward Error Correction) Decryption (Data Security) +// | | +// v v +// CRC32 Checksum (Error Detection) CRC32 Checksum (Error Detection) +// | | +// v v +// Encryption (Data Security) FEC Decoding (Forward Error Correction) +// | | +// v v +// TxQueue (Transmission Queue) KCP Input (Reliable Transport Layer) +// | | +// v v +// Packet Network (Network Transmission) Stream (Input Data) + package kcp import ( + "context" "crypto/rand" "encoding/binary" "hash/crc32" @@ -20,43 +59,63 @@ import ( "github.com/pkg/errors" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" + "golang.org/x/time/rate" ) +// Session-layer constants const ( // 16-bytes nonce for each packet nonceSize = 16 - // 4-bytes packet checksum + // 4-bytes CRC32 checksum per packet crcSize = 4 - // overall crypto header size + // overall crypto header size: nonce + CRC32 cryptHeaderSize = nonceSize + crcSize - // maximum packet size + // maximum packet size (Ethernet MTU) mtuLimit = 1500 - // accept backlog + // conversation ID field size (bytes) + convSize = 4 + + // accept backlog: max pending connections for Listener acceptBacklog = 128 + + // devBacklog: channel buffer size for post-processing pipeline + devBacklog = 2048 + + // max latency for consecutive FEC encoding (ms). + // If the interval between two data packets exceeds this, + // parity generation is skipped. + maxFECEncodeLatency = 500 + + // max number of packets batched in a single sendmmsg/writev call + maxBatchSize = 64 ) var ( errInvalidOperation = errors.New("invalid operation") - errTimeout = errors.New("timeout") + errTimeout = timeoutError{} + errNotOwner = errors.New("not the owner of this connection") ) -var ( - // a system-wide packet buffer shared among sending, receiving and FEC - // to mitigate high-frequency memory allocation for packets, bytes from xmitBuf - // is aligned to 64bit - xmitBuf sync.Pool -) +// timeoutError implements net.Error +type timeoutError struct{} -func init() { - xmitBuf.New = func() interface{} { - return make([]byte, mtuLimit) - } +func (timeoutError) Error() string { return "timeout" } +func (timeoutError) Timeout() bool { return true } +func (timeoutError) Temporary() bool { return true } + +// sendRequest defines a write request before encoding and transmission +type sendRequest struct { + buffer []byte + oob bool } +// OOB callback function +type OOBCallBackType func([]byte) + type ( // UDPSession defines a KCP session implemented by UDP UDPSession struct { @@ -76,13 +135,13 @@ type ( fecEncoder *fecEncoder // settings - remote net.Addr // remote peer address - rd time.Time // read deadline - wd time.Time // write deadline - headerSize int // the header size additional to a KCP frame - ackNoDelay bool // send ack immediately for each incoming packet(testing purpose) - writeDelay bool // delay kcp.flush() for Write() for bulk transfer - dup int // duplicate udp packets(testing purpose) + remote net.Addr // remote peer address + rd atomic.Value // read deadline + wd atomic.Value // write deadline + headerSize int // the header size additional to a KCP frame + ackNoDelay bool // send ack immediately for each incoming packet(testing purpose) + writeDelay bool // delay kcp.flush() for Write() for bulk transfer + dup int // duplicate udp packets(testing purpose) // notifications die chan struct{} // notify current session has Closed @@ -98,15 +157,22 @@ type ( socketReadErrorOnce sync.Once socketWriteErrorOnce sync.Once - // nonce generator - nonce Entropy - // packets waiting to be sent on wire - txqueue []ipv4.Message - xconn batchConn // for x/net - xconnWriteError error + chPostProcessing chan sendRequest + + // platform-dependent optimizations + platform platform + + // rate limiter (bytes per second) + rateLimiter atomic.Value mu sync.Mutex + + // callbackForOOB is an optional callback for handling received out-of-band (OOB) data. + // + // OOB data bypasses the KCP reliable data path and is delivered unreliably. + // The callback is invoked synchronously from the KCP input processing path. + callbackForOOB atomic.Value } setReadBuffer interface { @@ -126,53 +192,65 @@ type ( func newUDPSession(conv uint32, dataShards, parityShards int, l *Listener, conn net.PacketConn, ownConn bool, remote net.Addr, block BlockCrypt) *UDPSession { sess := new(UDPSession) sess.die = make(chan struct{}) - sess.nonce = new(nonceAES128) - sess.nonce.Init() sess.chReadEvent = make(chan struct{}, 1) sess.chWriteEvent = make(chan struct{}, 1) sess.chSocketReadError = make(chan struct{}) sess.chSocketWriteError = make(chan struct{}) + sess.chPostProcessing = make(chan sendRequest, devBacklog) sess.remote = remote sess.conn = conn sess.ownConn = ownConn sess.l = l sess.block = block sess.recvbuf = make([]byte, mtuLimit) - - // cast to writebatch conn - if _, ok := conn.(*net.UDPConn); ok { - addr, err := net.ResolveUDPAddr("udp", conn.LocalAddr().String()) - if err == nil { - if addr.IP.To4() != nil { - sess.xconn = ipv4.NewPacketConn(conn) - } else { - sess.xconn = ipv6.NewPacketConn(conn) - } - } + sess.initPlatform() + + // calculate additional header size introduced by encryption + switch block := sess.block.(type) { + case nil: + sess.headerSize = 0 + case *aeadCrypt: + sess.headerSize = block.NonceSize() + default: + sess.headerSize = cryptHeaderSize } // FEC codec initialization sess.fecDecoder = newFECDecoder(dataShards, parityShards) - if sess.block != nil { - sess.fecEncoder = newFECEncoder(dataShards, parityShards, cryptHeaderSize) - } else { - sess.fecEncoder = newFECEncoder(dataShards, parityShards, 0) - } + sess.fecEncoder = newFECEncoder(dataShards, parityShards, sess.headerSize) - // calculate additional header size introduced by FEC and encryption - if sess.block != nil { - sess.headerSize += cryptHeaderSize - } + // calculate additional header size introduced by FEC if sess.fecEncoder != nil { sess.headerSize += fecHeaderSizePlus2 } sess.kcp = NewKCP(conv, func(buf []byte, size int) { - if size >= IKCP_OVERHEAD+sess.headerSize { - sess.output(buf[:size]) + // A basic check for the minimum packet size + if size >= IKCP_OVERHEAD { + // make a copy + bts := defaultBufferPool.Get()[:size+sess.headerSize] + // copy the data to a new buffer, and reserve header space + copy(bts[sess.headerSize:], buf) + + // delivery to post processing (non-blocking to avoid deadlock under lock) + select { + case sess.chPostProcessing <- sendRequest{bts, false}: + case <-sess.die: + return + default: + // drop and recycle to avoid blocking; KCP will retransmit if needed + defaultBufferPool.Put(bts) + } } }) - sess.kcp.ReserveBytes(sess.headerSize) + + // Set Default MTU + if !sess.SetMtu(IKCP_MTU_DEF) { + panic("Overhead too large") + } + + // create post-processing goroutine + go sess.postProcess() if sess.l == nil { // it's a client connection go sess.readLoop() @@ -195,9 +273,32 @@ func newUDPSession(conv uint32, dataShards, parityShards int, l *Listener, conn // Read implements net.Conn func (s *UDPSession) Read(b []byte) (n int, err error) { + var timeout *time.Timer + var c <-chan time.Time + +RESET_TIMER: + // deadline for current reading operation + if trd, ok := s.rd.Load().(time.Time); ok && !trd.IsZero() { + if timeout == nil { + timeout = time.NewTimer(time.Until(trd)) + c = timeout.C + defer timeout.Stop() + } else { + // Pre-Go 1.23: Reset does not drain the channel; + // callers must drain at the goto-site before arriving here. + timeout.Reset(time.Until(trd)) + } + } else if timeout != nil { + timeout.Stop() + c = nil // disable timeout select case + } + for { s.mu.Lock() - if len(s.bufptr) > 0 { // copy from buffer into b + // bufptr points to the current position of recvbuf, + // if previous 'b' is insufficient to accommodate the data, the + // remaining data will be stored in bufptr for next read. + if len(s.bufptr) > 0 { n = copy(b, s.bufptr) s.bufptr = s.bufptr[n:] s.mu.Unlock() @@ -206,48 +307,47 @@ func (s *UDPSession) Read(b []byte) (n int, err error) { } if size := s.kcp.PeekSize(); size > 0 { // peek data size from kcp - if len(b) >= size { // receive data into 'b' directly + // if 'b' is large enough to accommodate the data, read directly + // from kcp.recv() to 'b', like 'DMA'. + if len(b) >= size { s.kcp.Recv(b) s.mu.Unlock() atomic.AddUint64(&DefaultSnmp.BytesReceived, uint64(size)) return size, nil } - // if necessary resize the stream buffer to guarantee a sufficient buffer space + // otherwise, read to recvbuf first, then copy to 'b'. + // dynamically adjust the buffer size to the maximum of 'packet size' when necessary. if cap(s.recvbuf) < size { + // usually recvbuf has a size of maximum packet size s.recvbuf = make([]byte, size) } - // resize the length of recvbuf to correspond to data size + // resize the length of recvbuf to match the data size s.recvbuf = s.recvbuf[:size] - s.kcp.Recv(s.recvbuf) - n = copy(b, s.recvbuf) // copy to 'b' + s.kcp.Recv(s.recvbuf) // read data to recvbuf first + n = copy(b, s.recvbuf) // then copy bytes to 'b' as many as possible s.bufptr = s.recvbuf[n:] // pointer update + s.mu.Unlock() atomic.AddUint64(&DefaultSnmp.BytesReceived, uint64(n)) return n, nil } - // deadline for current reading operation - var timeout *time.Timer - var c <-chan time.Time - if !s.rd.IsZero() { - if time.Now().After(s.rd) { - s.mu.Unlock() - return 0, errors.WithStack(errTimeout) - } - - delay := time.Until(s.rd) - timeout = time.NewTimer(delay) - c = timeout.C - } s.mu.Unlock() - // wait for read event or timeout or error + // if it runs here, that means we have to block the call, and wait until the + // next data packet arrives. select { case <-s.chReadEvent: if timeout != nil { - timeout.Stop() + if !timeout.Stop() { + select { + case <-timeout.C: + default: + } + } + goto RESET_TIMER } case <-c: return 0, errors.WithStack(errTimeout) @@ -264,7 +364,27 @@ func (s *UDPSession) Write(b []byte) (n int, err error) { return s.WriteBuffers( // WriteBuffers write a vector of byte slices to the underlying connection func (s *UDPSession) WriteBuffers(v [][]byte) (n int, err error) { + var timeout *time.Timer + var c <-chan time.Time + +RESET_TIMER: + if twd, ok := s.wd.Load().(time.Time); ok && !twd.IsZero() { + if timeout == nil { + timeout = time.NewTimer(time.Until(twd)) + c = timeout.C + defer timeout.Stop() + } else { + // Pre-Go 1.23: Reset does not drain the channel; + // callers must drain at the goto-site before arriving here. + timeout.Reset(time.Until(twd)) + } + } else if timeout != nil { + timeout.Stop() + c = nil // disable timeout select case + } + for { + // check for connection close and socket error select { case <-s.chSocketWriteError: return 0, s.socketWriteError.Load().(error) @@ -277,9 +397,11 @@ func (s *UDPSession) WriteBuffers(v [][]byte) (n int, err error) { // make sure write do not overflow the max sliding window on both side waitsnd := s.kcp.WaitSnd() - if waitsnd < int(s.kcp.snd_wnd) && waitsnd < int(s.kcp.rmt_wnd) { + if waitsnd < int(s.kcp.snd_wnd) { + // transmit all data sequentially, make sure every packet size is within 'mss' for _, b := range v { n += len(b) + // handle each slice for packet splitting for { if len(b) <= int(s.kcp.mss) { s.kcp.Send(b) @@ -292,32 +414,31 @@ func (s *UDPSession) WriteBuffers(v [][]byte) (n int, err error) { } waitsnd = s.kcp.WaitSnd() - if waitsnd >= int(s.kcp.snd_wnd) || waitsnd >= int(s.kcp.rmt_wnd) || !s.writeDelay { - s.kcp.flush(false) - s.uncork() + if waitsnd >= int(s.kcp.snd_wnd) || !s.writeDelay { + // put the packets on wire immediately if the inflight window is full + // or if we've specified write no delay(NO merging of outgoing bytes) + // we don't have to wait until the periodical update() procedure uncorks. + s.kcp.flush(IKCP_FLUSH_FULL) } s.mu.Unlock() atomic.AddUint64(&DefaultSnmp.BytesSent, uint64(n)) return n, nil } - var timeout *time.Timer - var c <-chan time.Time - if !s.wd.IsZero() { - if time.Now().After(s.wd) { - s.mu.Unlock() - return 0, errors.WithStack(errTimeout) - } - delay := time.Until(s.wd) - timeout = time.NewTimer(delay) - c = timeout.C - } s.mu.Unlock() + // if it runs here, that means we have to block the call, and wait until the + // transmit buffer to become available again. select { case <-s.chWriteEvent: if timeout != nil { - timeout.Stop() + if !timeout.Stop() { + select { + case <-timeout.C: + default: + } + } + goto RESET_TIMER } case <-c: return 0, errors.WithStack(errTimeout) @@ -329,16 +450,12 @@ func (s *UDPSession) WriteBuffers(v [][]byte) (n int, err error) { } } -// uncork sends data in txqueue if there is any -func (s *UDPSession) uncork() { - if len(s.txqueue) > 0 { - s.tx(s.txqueue) - // recycle - for k := range s.txqueue { - xmitBuf.Put(s.txqueue[k].Buffers[0]) - s.txqueue[k].Buffers = nil - } - s.txqueue = s.txqueue[:0] +func (s *UDPSession) isClosed() bool { + select { + case <-s.die: + return true + default: + return false } } @@ -350,31 +467,27 @@ func (s *UDPSession) Close() error { once = true }) - if once { - atomic.AddUint64(&DefaultSnmp.CurrEstab, ^uint64(0)) + if !once { + return errors.WithStack(io.ErrClosedPipe) + } - // try best to send all queued messages - s.mu.Lock() - s.kcp.flush(false) - s.uncork() - // release pending segments - s.kcp.ReleaseTX() - if s.fecDecoder != nil { - s.fecDecoder.release() - } - s.mu.Unlock() + atomic.AddUint64(&DefaultSnmp.CurrEstab, ^uint64(0)) - if s.l != nil { // belongs to listener - s.l.closeSession(s.remote) - return nil - } else if s.ownConn { // client socket close - return s.conn.Close() - } else { - return nil - } - } else { - return errors.WithStack(io.ErrClosedPipe) + // try best to send all queued messages especially the data in txqueue + s.mu.Lock() + s.kcp.flush((IKCP_FLUSH_FULL)) + s.mu.Unlock() + + if s.l != nil { // belongs to listener + s.l.closeSession(s.remote) + return nil + } + + if s.ownConn { // client socket close + return s.conn.Close() } + + return nil } // LocalAddr returns the local network address. The Addr returned is shared by all invocations of LocalAddr, so do not modify it. @@ -385,10 +498,8 @@ func (s *UDPSession) RemoteAddr() net.Addr { return s.remote } // SetDeadline sets the deadline associated with the listener. A zero time value disables the deadline. func (s *UDPSession) SetDeadline(t time.Time) error { - s.mu.Lock() - defer s.mu.Unlock() - s.rd = t - s.wd = t + s.rd.Store(t) + s.wd.Store(t) s.notifyReadEvent() s.notifyWriteEvent() return nil @@ -396,18 +507,14 @@ func (s *UDPSession) SetDeadline(t time.Time) error { // SetReadDeadline implements the Conn SetReadDeadline method. func (s *UDPSession) SetReadDeadline(t time.Time) error { - s.mu.Lock() - defer s.mu.Unlock() - s.rd = t + s.rd.Store(t) s.notifyReadEvent() return nil } // SetWriteDeadline implements the Conn SetWriteDeadline method. func (s *UDPSession) SetWriteDeadline(t time.Time) error { - s.mu.Lock() - defer s.mu.Unlock() - s.wd = t + s.wd.Store(t) s.notifyWriteEvent() return nil } @@ -415,45 +522,48 @@ func (s *UDPSession) SetWriteDeadline(t time.Time) error { // SetWriteDelay delays write for bulk transfer until the next update interval func (s *UDPSession) SetWriteDelay(delay bool) { s.mu.Lock() - defer s.mu.Unlock() s.writeDelay = delay + s.mu.Unlock() } // SetWindowSize set maximum window size func (s *UDPSession) SetWindowSize(sndwnd, rcvwnd int) { s.mu.Lock() - defer s.mu.Unlock() s.kcp.WndSize(sndwnd, rcvwnd) + s.mu.Unlock() } // SetMtu sets the maximum transmission unit(not including UDP header) func (s *UDPSession) SetMtu(mtu int) bool { - if mtu > mtuLimit { - return false + mtu = min(mtuLimit, mtu) + + mtu -= s.headerSize + if aead, ok := s.block.(*aeadCrypt); ok { + mtu -= aead.Overhead() } s.mu.Lock() defer s.mu.Unlock() - s.kcp.SetMtu(mtu) - return true + ret := s.kcp.SetMtu(mtu) // kcp mtu is not including udp header + return ret == 0 } -// SetStreamMode toggles the stream mode on/off +// Deprecated: toggles the stream mode on/off func (s *UDPSession) SetStreamMode(enable bool) { s.mu.Lock() - defer s.mu.Unlock() if enable { s.kcp.stream = 1 } else { s.kcp.stream = 0 } + s.mu.Unlock() } // SetACKNoDelay changes ack flush option, set true to flush ack immediately, func (s *UDPSession) SetACKNoDelay(nodelay bool) { s.mu.Lock() - defer s.mu.Unlock() s.ackNoDelay = nodelay + s.mu.Unlock() } // (deprecated) @@ -461,16 +571,16 @@ func (s *UDPSession) SetACKNoDelay(nodelay bool) { // SetDUP duplicates udp packets for kcp output. func (s *UDPSession) SetDUP(dup int) { s.mu.Lock() - defer s.mu.Unlock() s.dup = dup + s.mu.Unlock() } // SetNoDelay calls nodelay() of kcp // https://github.com/skywind3000/kcp/blob/master/README.en.md#protocol-configuration func (s *UDPSession) SetNoDelay(nodelay, interval, resend, nc int) { s.mu.Lock() - defer s.mu.Unlock() s.kcp.NoDelay(nodelay, interval, resend, nc) + s.mu.Unlock() } // SetDSCP sets the 6bit DSCP field in IPv4 header, or 8bit Traffic Class in IPv6 header. @@ -531,51 +641,160 @@ func (s *UDPSession) SetWriteBuffer(bytes int) error { return errInvalidOperation } -// post-processing for sending a packet from kcp core -// steps: -// 1. FEC packet generation -// 2. CRC32 integrity -// 3. Encryption -// 4. TxQueue -func (s *UDPSession) output(buf []byte) { - var ecc [][]byte - - // 1. FEC encoding - if s.fecEncoder != nil { - ecc = s.fecEncoder.encode(buf) - } - - // 2&3. crc32 & encryption - if s.block != nil { - s.nonce.Fill(buf[:nonceSize]) - checksum := crc32.ChecksumIEEE(buf[cryptHeaderSize:]) - binary.LittleEndian.PutUint32(buf[nonceSize:], checksum) - s.block.Encrypt(buf, buf) - - for k := range ecc { - s.nonce.Fill(ecc[k][:nonceSize]) - checksum := crc32.ChecksumIEEE(ecc[k][cryptHeaderSize:]) - binary.LittleEndian.PutUint32(ecc[k][nonceSize:], checksum) - s.block.Encrypt(ecc[k], ecc[k]) - } +// SetRateLimit sets the rate limit for this session in bytes per second, +// by setting to 0 will disable rate limiting. +func (s *UDPSession) SetRateLimit(bytesPerSecond uint32) { + var limiter *rate.Limiter + if bytesPerSecond == 0 { + limiter = rate.NewLimiter(rate.Inf, maxBatchSize*mtuLimit) + } else { + limiter = rate.NewLimiter(rate.Limit(bytesPerSecond), maxBatchSize*mtuLimit) } - // 4. TxQueue - var msg ipv4.Message - for i := 0; i < s.dup+1; i++ { - bts := xmitBuf.Get().([]byte)[:len(buf)] - copy(bts, buf) - msg.Buffers = [][]byte{bts} - msg.Addr = s.remote - s.txqueue = append(s.txqueue, msg) + s.rateLimiter.Store(limiter) +} + +// SetLogger configures the kcp trace logger +func (s *UDPSession) SetLogger(mask KCPLogType, logger logoutput_callback) { + s.kcp.SetLogger(mask, logger) +} + +// Control applys a procedure to the underly socket fd. +// CAUTION: BE VERY CAREFUL TO USE THIS FUNCTION, YOU MAY BREAK THE PROTOCOL. +func (s *UDPSession) Control(f func(conn net.PacketConn) error) error { + if !s.ownConn { + return errNotOwner } - for k := range ecc { - bts := xmitBuf.Get().([]byte)[:len(ecc[k])] - copy(bts, ecc[k]) - msg.Buffers = [][]byte{bts} - msg.Addr = s.remote - s.txqueue = append(s.txqueue, msg) + s.mu.Lock() + defer s.mu.Unlock() + return f(s.conn) +} + +// postProcess is the goroutine that handles the outgoing packet pipeline. +// It runs the following stages sequentially for each packet: +// 1. FEC encoding — generate parity shards (Reed-Solomon) +// 2. Encryption — AEAD (e.g. AES-GCM) or CFB mode with CRC32 +// 3. TX batching — accumulate packets and flush via sendmmsg/writev +// +// Pipeline: KCP output -> chPostProcessing -> [FEC] -> [Encrypt] -> TxQueue -> Network +func (s *UDPSession) postProcess() { + txqueue := make([]ipv4.Message, 0, devBacklog) + chDie := s.die + + ctx := context.Background() + bytesToSend := 0 + for { + select { + case req := <-s.chPostProcessing: // dequeue from post processing + buf := req.buffer + oob := req.oob + + var ecc [][]byte + + // --- Stage 1: FEC encoding --- + if s.fecEncoder != nil { + if !oob { + ecc = s.fecEncoder.encode(buf, maxFECEncodeLatency) + } else { + s.fecEncoder.encodeOOB(buf) + } + } + + // --- Stage 2: Encryption --- + // Two modes supported: + // - AEAD (e.g. AES-GCM): nonce + authenticated ciphertext, no separate CRC + // - CFB (legacy block ciphers): random nonce + CRC32 checksum + CFB encryption + switch block := s.block.(type) { + case nil: + case *aeadCrypt: // AEAD mode + nonceSize := block.NonceSize() + + dst := buf[:nonceSize] + nonce := buf[:nonceSize] + plaintext := buf[nonceSize:] + + fillRand(nonce) + buf = block.Seal(dst, nonce, plaintext, nil) + + for k := range ecc { + dst := ecc[k][:nonceSize] + nonce := ecc[k][:nonceSize] + plaintext := ecc[k][nonceSize:] + + fillRand(nonce) + ecc[k] = block.Seal(dst, nonce, plaintext, nil) + } + default: // Cipher Feedback (CFB) mode + fillRand(buf[:nonceSize]) + checksum := crc32.ChecksumIEEE(buf[cryptHeaderSize:]) + binary.LittleEndian.PutUint32(buf[nonceSize:], checksum) + block.Encrypt(buf, buf) + + for k := range ecc { + fillRand(ecc[k][:nonceSize]) + checksum := crc32.ChecksumIEEE(ecc[k][cryptHeaderSize:]) + binary.LittleEndian.PutUint32(ecc[k][nonceSize:], checksum) + block.Encrypt(ecc[k], ecc[k]) + } + } + + // --- Stage 3: TX batching --- + var msg ipv4.Message + msg.Addr = s.remote + + // original copy, move buf to txqueue directly + msg.Buffers = [][]byte{buf} + bytesToSend += len(buf) + txqueue = append(txqueue, msg) + + // dup copies for testing if set + for i := 0; i < s.dup; i++ { + bts := defaultBufferPool.Get()[:len(buf)] + copy(bts, buf) + msg.Buffers = [][]byte{bts} + bytesToSend += len(bts) + txqueue = append(txqueue, msg) + } + + // parity + for k := range ecc { + bts := defaultBufferPool.Get()[:len(ecc[k])] + copy(bts, ecc[k]) + msg.Buffers = [][]byte{bts} + bytesToSend += len(bts) + txqueue = append(txqueue, msg) + } + + // transmit when chPostProcessing is empty or we've reached max batch size + if len(s.chPostProcessing) == 0 || len(txqueue) >= maxBatchSize { + if limiter, ok := s.rateLimiter.Load().(*rate.Limiter); ok { + // WaitN only returns error if the limiter is misconfigured + // or context is cancelled. In either case, we continue sending. + _ = limiter.WaitN(ctx, bytesToSend) + } + s.tx(txqueue) + s.kcp.debugLog(IKCP_LOG_OUTPUT, "conv", s.kcp.conv, "datalen", bytesToSend) + // recycle + for k := range txqueue { + defaultBufferPool.Put(txqueue[k].Buffers[0]) + txqueue[k].Buffers = nil + } + txqueue = txqueue[:0] + bytesToSend = 0 + } + + // re-enable die channel + chDie = s.die + + case <-chDie: + // remaining packets in txqueue should be sent out + if len(s.chPostProcessing) > 0 { + chDie = nil // block chDie temporarily + continue + } + return + } } } @@ -585,12 +804,11 @@ func (s *UDPSession) update() { case <-s.die: default: s.mu.Lock() - interval := s.kcp.flush(false) + interval := s.kcp.flush(IKCP_FLUSH_FULL) waitsnd := s.kcp.WaitSnd() - if waitsnd < int(s.kcp.snd_wnd) && waitsnd < int(s.kcp.rmt_wnd) { + if waitsnd < int(s.kcp.snd_wnd) { s.notifyWriteEvent() } - s.uncork() s.mu.Unlock() // self-synchronized timed scheduling SystemTimedSched.Put(s.update, time.Now().Add(time.Duration(interval)*time.Millisecond)) @@ -621,6 +839,98 @@ func (s *UDPSession) GetSRTTVar() int32 { return s.kcp.rx_rttvar } +// SetOOBHandler registers a callback for receiving out-of-band (OOB) data. +// +// OOB data is delivered unreliably and bypasses the KCP reliable data path. +// The callback is invoked synchronously from the KCP input processing path. +// +// The callback MUST return quickly and MUST NOT perform any blocking operations. +// Blocking inside the callback will stall processing of all other KCP packets. +// +// Passing a nil callback unregisters the current OOB callback. +// +// OOB support requires FEC to be enabled, as the OOB packet format +// reuses the FEC header layout for demultiplexing. +func (s *UDPSession) SetOOBHandler(callback OOBCallBackType) error { + if s.fecEncoder == nil { + return errors.New("OOB requires FEC to be enabled") + } + if callback == nil { + s.callbackForOOB.Store(OOBCallBackType(func([]byte) {})) + return nil + } + s.callbackForOOB.Store(callback) + return nil +} + +// GetOOBMaxSize returns the maximum payload size for an OOB packet. +// +// The returned value is the maximum number of bytes that can be carried as +// OOB data in a single packet, based on the current MTU and protocol layout. +// +// If FEC is not enabled, OOB is unsupported and this function returns 0. +func (s *UDPSession) GetOOBMaxSize() int { + if s.fecEncoder == nil { + return 0 + } + // Packet layout: | conv (4B) | OOB payload | + return int(s.kcp.mtu) - convSize +} + +// SendOOB sends an out-of-band (OOB) data packet. +// +// OOB packets: +// - Are unreliable: they are NOT retransmitted if lost. +// - Are unordered: delivery order is not guaranteed. +// - Are unacknowledged: no ACKs are generated. +// - Bypass the KCP reliable data path. +// - Reuse the FEC header layout for demultiplexing, but are NOT protected by FEC. +// +// The OOB payload MUST fit into a single packet. +// If the payload is too large, an error is returned. +// +// If the internal send queue is full, the OOB packet is dropped silently. +func (s *UDPSession) SendOOB(data []byte) error { + if s.fecEncoder == nil { + return errors.New("OOB requires FEC to be enabled") + } + + // lock the session during OOB packet construction + s.mu.Lock() + defer s.mu.Unlock() + + // Packet layout: | conv (4B) | OOB payload | + size := convSize + len(data) + if size > int(s.kcp.mtu) { + return errors.New("OOB payload too large") + } + + // Allocate buffer with reserved header space. + // s.headerSize includes the space needed by the FEC encoder. + buf := defaultBufferPool.Get()[:size+s.headerSize] + // Encode conversation ID. + binary.LittleEndian.PutUint32(buf[s.headerSize:], s.kcp.conv) + + // Copy OOB payload immediately after the conversation ID. + copy(buf[s.headerSize+convSize:], data) + + // Enqueue the packet for post-processing. + // Performs OOB framing, encryption, and transmission, bypassing FEC and KCP. + select { + case s.chPostProcessing <- sendRequest{buf, true}: + return nil + case <-s.die: + // Session is closing. + defaultBufferPool.Put(buf) + return errors.WithStack(io.ErrClosedPipe) + default: + // Drop silently to avoid blocking the sender. + // OOB delivery is best-effort by design. + defaultBufferPool.Put(buf) + return nil + } +} + func (s *UDPSession) notifyReadEvent() { select { case s.chReadEvent <- struct{}{}: @@ -649,119 +959,171 @@ func (s *UDPSession) notifyWriteError(err error) { }) } -// packet input stage +// ----------------------------------------------------------------------- +// Packet input pipeline (decryption -> integrity check -> FEC -> KCP) +// ----------------------------------------------------------------------- + +// packetInput is the entry point for incoming packets. +// It handles decryption and CRC32 verification before passing data to kcpInput. +// +// Pipeline: Network -> [Decrypt] -> [CRC32] -> kcpInput func (s *UDPSession) packetInput(data []byte) { - decrypted := false - if s.block != nil && len(data) >= cryptHeaderSize { - s.block.Decrypt(data, data) + switch block := s.block.(type) { + case nil: + case *aeadCrypt: + nonceSize := block.NonceSize() + if len(data) < nonceSize+block.Overhead() { + return + } + + nonce := data[:nonceSize] + ciphertext := data[nonceSize:] + + plaintext, err := block.Open(ciphertext[:0], nonce, ciphertext, nil) + if err != nil { + atomic.AddUint64(&DefaultSnmp.InCsumErrors, 1) + return + } + + data = plaintext + default: + // decryption and crc32 check + if len(data) < cryptHeaderSize { + return + } + + block.Decrypt(data, data) data = data[nonceSize:] + checksum := crc32.ChecksumIEEE(data[crcSize:]) - if checksum == binary.LittleEndian.Uint32(data) { - data = data[crcSize:] - decrypted = true - } else { + if checksum != binary.LittleEndian.Uint32(data) { atomic.AddUint64(&DefaultSnmp.InCsumErrors, 1) + return } - } else if s.block == nil { - decrypted = true + + data = data[crcSize:] } - if decrypted && len(data) >= IKCP_OVERHEAD { - s.kcpInput(data) + // basic check for minimum packet size + // NOTE: OOB allows sending small packets and even empty packets. + if len(data) < min(IKCP_OVERHEAD, fecHeaderSizePlus2+convSize) { + atomic.AddUint64(&DefaultSnmp.KCPInErrors, 1) + return } + + s.kcpInput(data) } +// kcpInput routes a decrypted packet into the KCP state machine, +// handling FEC decoding and OOB delivery. +// +// Packet demultiplexing uses the 16-bit field at offset 4: +// - 0xf1 (typeData) / 0xf2 (typeParity): FEC-encoded packet +// - 0xf3 (typeOOB): out-of-band packet (unreliable, bypasses KCP) +// - other values: raw KCP packet (no FEC) +// +// Note: KCP cmd values [81-84] with frg [0-255] do not collide with +// FEC type markers 0x00f1/0x00f2/0x00f3 in little-endian. func (s *UDPSession) kcpInput(data []byte) { - var kcpInErrors, fecErrs, fecRecovered, fecParityShards uint64 + atomic.AddUint64(&DefaultSnmp.InPkts, 1) + atomic.AddUint64(&DefaultSnmp.InBytes, uint64(len(data))) + // 16bit kcp cmd [81-84] and frg [0-255] will not overlap with FEC type 0x00f1 0x00f2 fecFlag := binary.LittleEndian.Uint16(data[4:]) - if fecFlag == typeData || fecFlag == typeParity { // 16bit kcp cmd [81-84] and frg [0-255] will not overlap with FEC type 0x00f1 0x00f2 - if len(data) >= fecHeaderSizePlus2 { - f := fecPacket(data) - if f.flag() == typeParity { - fecParityShards++ - } - // lock - s.mu.Lock() - // if fecDecoder is not initialized, create one with default parameter - if s.fecDecoder == nil { - s.fecDecoder = newFECDecoder(1, 1) - } - recovers := s.fecDecoder.decode(f) - if f.flag() == typeData { - if ret := s.kcp.Input(data[fecHeaderSizePlus2:], true, s.ackNoDelay); ret != 0 { - kcpInErrors++ - } + switch fecFlag { + case typeData, typeParity: // packet with FEC + if len(data) < fecHeaderSizePlus2 { + atomic.AddUint64(&DefaultSnmp.InErrs, 1) + return + } + + var kcpInErrors uint64 + f := fecPacket(data) + + // lock + s.mu.Lock() + defer s.mu.Unlock() + + // if fecDecoder is not initialized, create one with default parameter + // lazy initialization + if s.fecDecoder == nil { + s.fecDecoder = newFECDecoder(1, 1) + } + + // KCP input for data packets + // only data packets are fed into kcp directly + // parity packets are only used for recovery + if f.flag() == typeData { + if ret := s.kcp.Input(data[fecHeaderSizePlus2:], IKCP_PACKET_REGULAR, s.ackNoDelay); ret != 0 { + kcpInErrors++ } + } - for _, r := range recovers { - if len(r) >= 2 { // must be larger than 2bytes - sz := binary.LittleEndian.Uint16(r) - if int(sz) <= len(r) && sz >= 2 { - if ret := s.kcp.Input(r[2:sz], false, s.ackNoDelay); ret == 0 { - fecRecovered++ - } else { - kcpInErrors++ - } - } else { - fecErrs++ + // FEC decoding + // If there're some packets recovered from FEC, feed them into kcp + recovers := s.fecDecoder.decode(f) + for _, r := range recovers { + if len(r) >= 2 { // must be larger than 2bytes + sz := binary.LittleEndian.Uint16(r) + if int(sz) <= len(r) && sz >= 2 { + if ret := s.kcp.Input(r[2:sz], IKCP_PACKET_FEC, s.ackNoDelay); ret != 0 { + kcpInErrors++ } - } else { - fecErrs++ } - // recycle the recovers - xmitBuf.Put(r) } + // recycle the buffer + defaultBufferPool.Put(r) + } - // to notify the readers to receive the data - if n := s.kcp.PeekSize(); n > 0 { - s.notifyReadEvent() - } - // to notify the writers - waitsnd := s.kcp.WaitSnd() - if waitsnd < int(s.kcp.snd_wnd) && waitsnd < int(s.kcp.rmt_wnd) { - s.notifyWriteEvent() - } + // to notify the readers to receive the data if there's any + if n := s.kcp.PeekSize(); n > 0 { + s.notifyReadEvent() + } - s.uncork() - s.mu.Unlock() - } else { - atomic.AddUint64(&DefaultSnmp.InErrs, 1) + // to notify the writers if the window size allows to send more packets + // and the remote window size is not full. + waitsnd := s.kcp.WaitSnd() + if waitsnd < int(s.kcp.snd_wnd) { + s.notifyWriteEvent() } - } else { + + if kcpInErrors > 0 { + atomic.AddUint64(&DefaultSnmp.KCPInErrors, kcpInErrors) + } + case typeOOB: + // Count received OOB packet + atomic.AddUint64(&DefaultSnmp.OOBPackets, 1) + // If an OOB callback is registered, invoke it synchronously. + // The callback is responsible for ensuring non-blocking behavior. + if callback := s.callbackForOOB.Load(); callback != nil { + // Data layout: | FEC header (fecHeaderSizePlus2) | conv (4B) | OOB payload | + callback.(OOBCallBackType)(data[fecHeaderSizePlus2+convSize:]) + } + default: // packet without FEC s.mu.Lock() - if ret := s.kcp.Input(data, true, s.ackNoDelay); ret != 0 { - kcpInErrors++ + defer s.mu.Unlock() + + if ret := s.kcp.Input(data, IKCP_PACKET_REGULAR, s.ackNoDelay); ret != 0 { + atomic.AddUint64(&DefaultSnmp.KCPInErrors, 1) } + if n := s.kcp.PeekSize(); n > 0 { s.notifyReadEvent() } + waitsnd := s.kcp.WaitSnd() - if waitsnd < int(s.kcp.snd_wnd) && waitsnd < int(s.kcp.rmt_wnd) { + if waitsnd < int(s.kcp.snd_wnd) { s.notifyWriteEvent() } - s.uncork() - s.mu.Unlock() - } - - atomic.AddUint64(&DefaultSnmp.InPkts, 1) - atomic.AddUint64(&DefaultSnmp.InBytes, uint64(len(data))) - if fecParityShards > 0 { - atomic.AddUint64(&DefaultSnmp.FECParityShards, fecParityShards) - } - if kcpInErrors > 0 { - atomic.AddUint64(&DefaultSnmp.KCPInErrors, kcpInErrors) - } - if fecErrs > 0 { - atomic.AddUint64(&DefaultSnmp.FECErrs, fecErrs) + return } - if fecRecovered > 0 { - atomic.AddUint64(&DefaultSnmp.FECRecovered, fecRecovered) - } - } +// ----------------------------------------------------------------------- +// Listener: server-side session multiplexer +// ----------------------------------------------------------------------- + type ( // Listener defines a server which will be waiting to accept incoming connections Listener struct { @@ -771,10 +1133,9 @@ type ( conn net.PacketConn // the underlying packet connection ownConn bool // true if we created conn internally, false if provided by caller - sessions map[string]*UDPSession // all sessions accepted by this Listener - sessionLock sync.RWMutex - chAccepts chan *UDPSession // Listen() backlog - chSessionClosed chan net.Addr // session close queue + sessions map[string]*UDPSession // all sessions accepted by this Listener + sessionLock sync.RWMutex + chAccepts chan *UDPSession // Listen() backlog die chan struct{} // notify the listener has closed dieOnce sync.Once @@ -788,65 +1149,127 @@ type ( } ) -// packet input stage +// packetInput is the Listener's packet input handler. +// It decrypts the packet, demultiplexes by remote address, +// and dispatches to existing sessions or creates new ones. func (l *Listener) packetInput(data []byte, addr net.Addr) { - decrypted := false - if l.block != nil && len(data) >= cryptHeaderSize { - l.block.Decrypt(data, data) + switch block := l.block.(type) { + case nil: + case *aeadCrypt: + nonceSize := block.NonceSize() + if len(data) < nonceSize+block.Overhead() { + return + } + + nonce := data[:nonceSize] + ciphertext := data[nonceSize:] + + plaintext, err := block.Open(ciphertext[:0], nonce, ciphertext, nil) + if err != nil { + atomic.AddUint64(&DefaultSnmp.InCsumErrors, 1) + return + } + + data = plaintext + default: + // decryption and crc32 check + if len(data) < cryptHeaderSize { + return + } + + block.Decrypt(data, data) data = data[nonceSize:] + checksum := crc32.ChecksumIEEE(data[crcSize:]) - if checksum == binary.LittleEndian.Uint32(data) { - data = data[crcSize:] - decrypted = true - } else { + if checksum != binary.LittleEndian.Uint32(data) { atomic.AddUint64(&DefaultSnmp.InCsumErrors, 1) + return } - } else if l.block == nil { - decrypted = true + + data = data[crcSize:] } - if decrypted && len(data) >= IKCP_OVERHEAD { - l.sessionLock.RLock() - s, ok := l.sessions[addr.String()] - l.sessionLock.RUnlock() + // basic check for minimum packet size + // NOTE: OOB allows sending small packets and even empty packets. + if len(data) < min(IKCP_OVERHEAD, fecHeaderSizePlus2+convSize) { + return + } - var conv, sn uint32 - convRecovered := false - fecFlag := binary.LittleEndian.Uint16(data[4:]) - if fecFlag == typeData || fecFlag == typeParity { // 16bit kcp cmd [81-84] and frg [0-255] will not overlap with FEC type 0x00f1 0x00f2 - // packet with FEC - if fecFlag == typeData && len(data) >= fecHeaderSizePlus2+IKCP_OVERHEAD { - conv = binary.LittleEndian.Uint32(data[fecHeaderSizePlus2:]) - sn = binary.LittleEndian.Uint32(data[fecHeaderSizePlus2+IKCP_SN_OFFSET:]) - convRecovered = true - } - } else { - // packet without FEC - conv = binary.LittleEndian.Uint32(data) - sn = binary.LittleEndian.Uint32(data[IKCP_SN_OFFSET:]) - convRecovered = true + // look for existing session + l.sessionLock.RLock() + s, exist := l.sessions[addr.String()] + l.sessionLock.RUnlock() + + var conv, sn uint32 + hasConv := false + + // try to get conversation id from the packet + // 16bit kcp cmd [81-84] and frg [0-255] will not overlap with FEC type 0x00f1 0x00f2 + fecFlag := binary.LittleEndian.Uint16(data[4:]) + + switch fecFlag { + case typeData: + // data packet of FEC, conversation id inside + if len(data) < fecHeaderSizePlus2+IKCP_OVERHEAD { + break } - if ok { // existing connection - if !convRecovered || conv == s.kcp.conv { // parity data or valid conversation - s.kcpInput(data) - } else if sn == 0 { // should replace current connection - s.Close() - s = nil - } + hasConv = true + conv = binary.LittleEndian.Uint32(data[fecHeaderSizePlus2:]) + sn = binary.LittleEndian.Uint32(data[fecHeaderSizePlus2+IKCP_SN_OFFSET:]) + case typeParity: + // parity packet of FEC, conversation id inside + case typeOOB: + // OOB packets always carry the conversation ID immediately after the FEC header. + hasConv = true + // Data layout: | FEC header (fecHeaderSizePlus2) | conv (4B) | OOB payload | + conv = binary.LittleEndian.Uint32(data[fecHeaderSizePlus2:]) + default: + // packet without FEC + if len(data) < IKCP_OVERHEAD { // basic check for minimum kcp packet size + return } + hasConv = true + conv = binary.LittleEndian.Uint32(data) + sn = binary.LittleEndian.Uint32(data[IKCP_SN_OFFSET:]) + } - if s == nil && convRecovered { // new session - if len(l.chAccepts) < cap(l.chAccepts) { // do not let the new sessions overwhelm accept queue - s := newUDPSession(conv, l.dataShards, l.parityShards, l, l.conn, false, addr, l.block) - s.kcpInput(data) - l.sessionLock.Lock() - l.sessions[addr.String()] = s - l.sessionLock.Unlock() - l.chAccepts <- s - } + // on an existing connection + if exist { + // If we have a valid conversation id or we cannot get conversation id from the packet, + // just feed the data into the existing session. + if !hasConv || conv == s.kcp.conv { + s.kcpInput(data) + return } + // conversation id mismatched, only accept reset packet with sn == 0 + if sn != 0 { + return + } + // Close will remove the session from listener's session map, + // So we can create a new session with the same addr below. + s.Close() + } + + // The connection does not exist, try to create a new one. + // But if we don't have a valid conversation id, nothing we can do here except dropping the packet. + if !hasConv { + return + } + + // Now we have a valid conversation id here without a session object, create a new session. + // do not let the new sessions overwhelm accept queue + if len(l.chAccepts) >= cap(l.chAccepts) { + return } + + // new session + s = newUDPSession(conv, l.dataShards, l.parityShards, l, l.conn, false, addr, l.block) + s.kcpInput(data) + l.sessionLock.Lock() + l.sessions[addr.String()] = s + l.sessionLock.Unlock() + l.chAccepts <- s } func (l *Listener) notifyReadError(err error) { @@ -865,16 +1288,16 @@ func (l *Listener) notifyReadError(err error) { // SetReadBuffer sets the socket read buffer for the Listener func (l *Listener) SetReadBuffer(bytes int) error { - if nc, ok := l.conn.(setReadBuffer); ok { - return nc.SetReadBuffer(bytes) + if conn, ok := l.conn.(setReadBuffer); ok { + return conn.SetReadBuffer(bytes) } return errInvalidOperation } // SetWriteBuffer sets the socket write buffer for the Listener func (l *Listener) SetWriteBuffer(bytes int) error { - if nc, ok := l.conn.(setWriteBuffer); ok { - return nc.SetWriteBuffer(bytes) + if conn, ok := l.conn.(setWriteBuffer); ok { + return conn.SetWriteBuffer(bytes) } return errInvalidOperation } @@ -885,23 +1308,28 @@ func (l *Listener) SetWriteBuffer(bytes int) error { // this function instead. func (l *Listener) SetDSCP(dscp int) error { // interface enabled - if ts, ok := l.conn.(setDSCP); ok { - return ts.SetDSCP(dscp) + if conn, ok := l.conn.(setDSCP); ok { + return conn.SetDSCP(dscp) } - if nc, ok := l.conn.(net.Conn); ok { - var succeed bool - if err := ipv4.NewConn(nc).SetTOS(dscp << 2); err == nil { - succeed = true - } - if err := ipv6.NewConn(nc).SetTrafficClass(dscp); err == nil { - succeed = true - } + conn, ok := l.conn.(net.Conn) + if !ok { + return errInvalidOperation + } - if succeed { - return nil - } + var succeed bool + if err := ipv4.NewConn(conn).SetTOS(dscp << 2); err == nil { + succeed = true + } + + if err := ipv6.NewConn(conn).SetTrafficClass(dscp); err == nil { + succeed = true } + + if succeed { + return nil + } + return errInvalidOperation } @@ -914,7 +1342,10 @@ func (l *Listener) Accept() (net.Conn, error) { func (l *Listener) AcceptKCP() (*UDPSession, error) { var timeout <-chan time.Time if tdeadline, ok := l.rd.Load().(time.Time); ok && !tdeadline.IsZero() { - timeout = time.After(time.Until(tdeadline)) + timer := time.NewTimer(time.Until(tdeadline)) + defer timer.Stop() + + timeout = timer.C } select { @@ -943,7 +1374,9 @@ func (l *Listener) SetReadDeadline(t time.Time) error { } // SetWriteDeadline implements the Conn SetWriteDeadline method. -func (l *Listener) SetWriteDeadline(t time.Time) error { return errInvalidOperation } +func (l *Listener) SetWriteDeadline(t time.Time) error { + return errInvalidOperation +} // Close stops listening on the UDP address, and closes the socket func (l *Listener) Close() error { @@ -953,21 +1386,31 @@ func (l *Listener) Close() error { once = true }) - var err error - if once { - if l.ownConn { - err = l.conn.Close() - } - } else { - err = errors.WithStack(io.ErrClosedPipe) + if !once { + return errors.WithStack(io.ErrClosedPipe) } - return err + + if l.ownConn { + return l.conn.Close() + } + + return nil +} + +// Control applys a procedure to the underly socket fd. +// CAUTION: BE VERY CAREFUL TO USE THIS FUNCTION, YOU MAY BREAK THE PROTOCOL. +func (l *Listener) Control(f func(conn net.PacketConn) error) error { + l.sessionLock.Lock() + defer l.sessionLock.Unlock() + + return f(l.conn) } // closeSession notify the listener that a session has closed func (l *Listener) closeSession(remote net.Addr) (ret bool) { l.sessionLock.Lock() defer l.sessionLock.Unlock() + if _, ok := l.sessions[remote.String()]; ok { delete(l.sessions, remote.String()) return true @@ -976,10 +1419,18 @@ func (l *Listener) closeSession(remote net.Addr) (ret bool) { } // Addr returns the listener's network address, The Addr returned is shared by all invocations of Addr, so do not modify it. -func (l *Listener) Addr() net.Addr { return l.conn.LocalAddr() } +func (l *Listener) Addr() net.Addr { + return l.conn.LocalAddr() +} + +// ----------------------------------------------------------------------- +// Public API: Dial, Listen, and connection factory functions +// ----------------------------------------------------------------------- // Listen listens for incoming KCP packets addressed to the local address laddr on the network "udp", -func Listen(laddr string) (net.Listener, error) { return ListenWithOptions(laddr, nil, 0, 0) } +func Listen(laddr string) (net.Listener, error) { + return ListenWithOptions(laddr, nil, 0, 0) +} // ListenWithOptions listens for incoming KCP packets addressed to the local address laddr on the network "udp" with packet encryption. // @@ -993,6 +1444,7 @@ func ListenWithOptions(laddr string, block BlockCrypt, dataShards, parityShards if err != nil { return nil, errors.WithStack(err) } + conn, err := net.ListenUDP("udp", udpaddr) if err != nil { return nil, errors.WithStack(err) @@ -1012,7 +1464,6 @@ func serveConn(block BlockCrypt, dataShards, parityShards int, conn net.PacketCo l.ownConn = ownConn l.sessions = make(map[string]*UDPSession) l.chAccepts = make(chan *UDPSession, acceptBacklog) - l.chSessionClosed = make(chan net.Addr) l.die = make(chan struct{}) l.dataShards = dataShards l.parityShards = parityShards @@ -1023,7 +1474,9 @@ func serveConn(block BlockCrypt, dataShards, parityShards int, conn net.PacketCo } // Dial connects to the remote address "raddr" on the network "udp" without encryption and FEC -func Dial(raddr string) (net.Conn, error) { return DialWithOptions(raddr, nil, 0, 0) } +func Dial(raddr string) (net.Conn, error) { + return DialWithOptions(raddr, nil, 0, 0) +} // DialWithOptions connects to the remote address "raddr" on the network "udp" with packet encryption // @@ -1038,6 +1491,7 @@ func DialWithOptions(raddr string, block BlockCrypt, dataShards, parityShards in if err != nil { return nil, errors.WithStack(err) } + network := "udp4" if udpaddr.IP.To4() == nil { network = "udp" @@ -1053,6 +1507,11 @@ func DialWithOptions(raddr string, block BlockCrypt, dataShards, parityShards in return newUDPSession(convid, dataShards, parityShards, nil, conn, true, udpaddr, block), nil } +// NewConn4 establishes a session and talks KCP protocol over a packet connection. +func NewConn4(convid uint32, raddr net.Addr, block BlockCrypt, dataShards, parityShards int, ownConn bool, conn net.PacketConn) (*UDPSession, error) { + return newUDPSession(convid, dataShards, parityShards, nil, conn, ownConn, raddr, block), nil +} + // NewConn3 establishes a session and talks KCP protocol over a packet connection. func NewConn3(convid uint32, raddr net.Addr, block BlockCrypt, dataShards, parityShards int, conn net.PacketConn) (*UDPSession, error) { return newUDPSession(convid, dataShards, parityShards, nil, conn, false, raddr, block), nil diff --git a/vendor/github.com/xtaci/kcp-go/v5/sess_test.go b/vendor/github.com/xtaci/kcp-go/v5/sess_test.go index fbe3ad1a..c7872843 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/sess_test.go +++ b/vendor/github.com/xtaci/kcp-go/v5/sess_test.go @@ -1,24 +1,58 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + package kcp import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" "crypto/sha1" "fmt" "io" "log" + "log/slog" + mrand "math/rand" "net" "net/http" _ "net/http/pprof" + "os" + "reflect" + "runtime" "sync" "sync/atomic" "testing" "time" + "github.com/pkg/errors" "golang.org/x/crypto/pbkdf2" ) -var baseport = uint32(10000) -var key = []byte("testkey") -var pass = pbkdf2.Key(key, []byte("testsalt"), 4096, 32, sha1.New) +var ( + baseport = uint32(10000) + key = []byte("testkey") + pass = pbkdf2.Key(key, []byte("testsalt"), 4096, 32, sha1.New) +) func init() { go func() { @@ -28,32 +62,38 @@ func init() { log.Println("beginning tests, encryption:salsa20, fec:10/3") } -func dialEcho(port int) (*UDPSession, error) { - //block, _ := NewNoneBlockCrypt(pass) - //block, _ := NewSimpleXORBlockCrypt(pass) - //block, _ := NewTEABlockCrypt(pass[:16]) - //block, _ := NewAESBlockCrypt(pass) - block, _ := NewSalsa20BlockCrypt(pass) +func nextPort() int { + port := int(atomic.AddUint32(&baseport, 1)) + port %= 65536 + if port <= 1024 { + port += 1024 + } + return port +} + +func dialEcho(port int, block BlockCrypt) (*UDPSession, error) { + // block, _ := NewNoneBlockCrypt(pass) + // block, _ := NewSimpleXORBlockCrypt(pass) + // block, _ := NewTEABlockCrypt(pass[:16]) + // block, _ := NewAESBlockCrypt(pass) sess, err := DialWithOptions(fmt.Sprintf("127.0.0.1:%v", port), block, 10, 3) if err != nil { panic(err) } - sess.SetStreamMode(true) - sess.SetStreamMode(false) sess.SetStreamMode(true) sess.SetWindowSize(1024, 1024) sess.SetReadBuffer(16 * 1024 * 1024) sess.SetWriteBuffer(16 * 1024 * 1024) - sess.SetStreamMode(true) sess.SetNoDelay(1, 10, 2, 1) sess.SetMtu(1400) sess.SetMtu(1600) sess.SetMtu(1400) sess.SetACKNoDelay(true) sess.SetACKNoDelay(false) - sess.SetDeadline(time.Now().Add(time.Minute)) - return sess, err + sess.SetRateLimit(200 * 1024 * 1024) + sess.SetLogger(IKCP_LOG_ALL, newLoggerWithMilliseconds().Info) + return sess, nil } func dialSink(port int) (*UDPSession, error) { @@ -66,41 +106,40 @@ func dialSink(port int) (*UDPSession, error) { sess.SetWindowSize(1024, 1024) sess.SetReadBuffer(16 * 1024 * 1024) sess.SetWriteBuffer(16 * 1024 * 1024) - sess.SetStreamMode(true) sess.SetNoDelay(1, 10, 2, 1) sess.SetMtu(1400) sess.SetACKNoDelay(false) sess.SetDeadline(time.Now().Add(time.Minute)) - return sess, err + return sess, nil } func dialTinyBufferEcho(port int) (*UDPSession, error) { - //block, _ := NewNoneBlockCrypt(pass) - //block, _ := NewSimpleXORBlockCrypt(pass) - //block, _ := NewTEABlockCrypt(pass[:16]) - //block, _ := NewAESBlockCrypt(pass) + // block, _ := NewNoneBlockCrypt(pass) + // block, _ := NewSimpleXORBlockCrypt(pass) + // block, _ := NewTEABlockCrypt(pass[:16]) + // block, _ := NewAESBlockCrypt(pass) block, _ := NewSalsa20BlockCrypt(pass) sess, err := DialWithOptions(fmt.Sprintf("127.0.0.1:%v", port), block, 10, 3) if err != nil { panic(err) } - return sess, err + return sess, nil } -////////////////////////// -func listenEcho(port int) (net.Listener, error) { - //block, _ := NewNoneBlockCrypt(pass) - //block, _ := NewSimpleXORBlockCrypt(pass) - //block, _ := NewTEABlockCrypt(pass[:16]) - //block, _ := NewAESBlockCrypt(pass) - block, _ := NewSalsa20BlockCrypt(pass) - return ListenWithOptions(fmt.Sprintf("127.0.0.1:%v", port), block, 10, 0) +// //////////////////////// +func listenEcho(port int, block BlockCrypt) (net.Listener, error) { + // block, _ := NewNoneBlockCrypt(pass) + // block, _ := NewSimpleXORBlockCrypt(pass) + // block, _ := NewTEABlockCrypt(pass[:16]) + // block, _ := NewAESBlockCrypt(pass) + return ListenWithOptions(fmt.Sprintf("127.0.0.1:%v", port), block, 10, 1) } + func listenTinyBufferEcho(port int) (net.Listener, error) { - //block, _ := NewNoneBlockCrypt(pass) - //block, _ := NewSimpleXORBlockCrypt(pass) - //block, _ := NewTEABlockCrypt(pass[:16]) - //block, _ := NewAESBlockCrypt(pass) + // block, _ := NewNoneBlockCrypt(pass) + // block, _ := NewSimpleXORBlockCrypt(pass) + // block, _ := NewTEABlockCrypt(pass[:16]) + // block, _ := NewAESBlockCrypt(pass) block, _ := NewSalsa20BlockCrypt(pass) return ListenWithOptions(fmt.Sprintf("127.0.0.1:%v", port), block, 10, 3) } @@ -109,8 +148,8 @@ func listenSink(port int) (net.Listener, error) { return ListenWithOptions(fmt.Sprintf("127.0.0.1:%v", port), nil, 0, 0) } -func echoServer(port int) net.Listener { - l, err := listenEcho(port) +func echoServer(port int, block BlockCrypt) net.Listener { + l, err := listenEcho(port, block) if err != nil { panic(err) } @@ -182,14 +221,15 @@ func tinyBufferEchoServer(port int) net.Listener { func handleEcho(conn *UDPSession) { conn.SetStreamMode(true) - conn.SetWindowSize(4096, 4096) + conn.SetWindowSize(1024, 1024) conn.SetNoDelay(1, 10, 2, 1) conn.SetDSCP(46) conn.SetMtu(1400) conn.SetACKNoDelay(false) conn.SetReadDeadline(time.Now().Add(time.Hour)) conn.SetWriteDeadline(time.Now().Add(time.Hour)) - buf := make([]byte, 65536) + conn.SetRateLimit(200 * 1024 * 1024) + buf := make([]byte, 512*1024) for { n, err := conn.Read(buf) if err != nil { @@ -201,14 +241,14 @@ func handleEcho(conn *UDPSession) { func handleSink(conn *UDPSession) { conn.SetStreamMode(true) - conn.SetWindowSize(4096, 4096) + conn.SetWindowSize(1024, 1024) conn.SetNoDelay(1, 10, 2, 1) conn.SetDSCP(46) conn.SetMtu(1400) conn.SetACKNoDelay(false) conn.SetReadDeadline(time.Now().Add(time.Hour)) conn.SetWriteDeadline(time.Now().Add(time.Hour)) - buf := make([]byte, 65536) + buf := make([]byte, 16*1024*1024) for { _, err := conn.Read(buf) if err != nil { @@ -232,84 +272,301 @@ func handleTinyBufferEcho(conn *UDPSession) { /////////////////////////// func TestTimeout(t *testing.T) { - port := int(atomic.AddUint32(&baseport, 1)) - l := echoServer(port) + port := nextPort() + block1, _ := NewSalsa20BlockCrypt(pass) + l := echoServer(port, block1) defer l.Close() - cli, err := dialEcho(port) + block2, _ := NewSalsa20BlockCrypt(pass) + cli, err := dialEcho(port, block2) if err != nil { panic(err) } + defer cli.Close() + buf := make([]byte, 10) - //timeout + // timeout cli.SetDeadline(time.Now().Add(time.Second)) <-time.After(2 * time.Second) n, err := cli.Read(buf) if n != 0 || err == nil { t.Fail() + return } - cli.Close() } -func TestSendRecv(t *testing.T) { - port := int(atomic.AddUint32(&baseport, 1)) - l := echoServer(port) +func TestCFBSendRecv(t *testing.T) { + port := nextPort() + block1, _ := NewTripleDESBlockCrypt(pass) + l := echoServer(port, block1) defer l.Close() - cli, err := dialEcho(port) + block2, _ := NewTripleDESBlockCrypt(pass) + cli, err := dialEcho(port, block2) if err != nil { panic(err) } + defer cli.Close() cli.SetWriteDelay(true) - cli.SetDUP(1) - const N = 100 - buf := make([]byte, 10) - for i := 0; i < N; i++ { - msg := fmt.Sprintf("hello%v", i) - cli.Write([]byte(msg)) - if n, err := cli.Read(buf); err == nil { - if string(buf[:n]) != msg { - t.Fail() + + randomEchoTest(t, cli, 100*1024*1024) +} + +func TestSalsa20SendRecv(t *testing.T) { + port := nextPort() + block1, _ := NewSalsa20BlockCrypt(pass) + l := echoServer(port, block1) + defer l.Close() + + block2, _ := NewSalsa20BlockCrypt(pass) + cli, err := dialEcho(port, block2) + if err != nil { + panic(err) + } + defer cli.Close() + cli.SetWriteDelay(true) + + randomEchoTest(t, cli, 100*1024*1024) +} + +func TestAEADSendRecv(t *testing.T) { + port := nextPort() + block1, _ := NewAESGCMCrypt(pass) + l := echoServer(port, block1) + defer l.Close() + + block2, _ := NewAESGCMCrypt(pass) + cli, err := dialEcho(port, block2) + if err != nil { + panic(err) + } + defer cli.Close() + cli.SetWriteDelay(true) + + randomEchoTest(t, cli, 100*1024*1024) +} + +func TestPlainTextSendRecv(t *testing.T) { + port := nextPort() + l := echoServer(port, nil) + defer l.Close() + + cli, err := dialEcho(port, nil) + if err != nil { + panic(err) + } + defer cli.Close() + cli.SetWriteDelay(true) + + randomEchoTest(t, cli, 100*1024*1024) +} + +func Test1GBEcho(t *testing.T) { + port := nextPort() + l := echoServer(port, nil) + defer l.Close() + + cli, err := dialEcho(port, nil) + if err != nil { + panic(err) + } + defer cli.Close() + cli.SetWriteDelay(true) + randomEchoTest(t, cli, 1*1024*1024*1024) +} + +func Test6GBEcho(t *testing.T) { + port := nextPort() + l := echoServer(port, nil) + defer l.Close() + + cli, err := dialEcho(port, nil) + if err != nil { + panic(err) + } + defer cli.Close() + cli.SetWriteDelay(true) + randomEchoTest(t, cli, 6*1024*1024*1024) +} + +func randomEchoTest(t *testing.T, cli *UDPSession, N int64) { + seed := time.Now().UnixNano() + writerSrc := mrand.NewSource(seed) + readerSrc := mrand.NewSource(seed) + + bytesSent := int64(0) + bytesReceived := int64(0) + + // Writer goroutine + go func() { + r := mrand.New(writerSrc) + lenRand := mrand.New(mrand.NewSource(seed + 1)) + lastPrint := int64(0) + sndbuf := make([]byte, 1<<20) + for bytesSent < N { + length := lenRand.Intn(1<<20) + 1 // Random length between 1 and 1MB + if bytesSent+int64(length) > N { + length = int(N - bytesSent) } - } else { - panic(err) + payload := sndbuf[:length] + if _, err := r.Read(payload); err != nil { + t.Errorf("Random fill error: %v", err) + return + } + + n, err := cli.Write(payload) + if err != nil { + t.Errorf("Write error: %v", err) + return + } + bytesSent += int64(n) + if bytesSent-lastPrint >= 1<<28 { // print every 256MB + lastPrint = bytesSent + t.Logf("Sent %d%% (%d/%d bytes)", int(bytesSent*100/N), bytesSent, N) + } + } + }() + + // Reader goroutine + r := mrand.New(readerSrc) + lenRand := mrand.New(mrand.NewSource(seed + 2)) + lastPrint := int64(0) + rcvbuf := make([]byte, 1<<20) + expbuf := make([]byte, 1<<20) + for bytesReceived < N { + length := lenRand.Intn(1<<20) + 1 // Random length between 1 and 1MB + if bytesReceived+int64(length) > N { + length = int(N - bytesReceived) + } + buf := rcvbuf[:length] + n, err := cli.Read(buf) + if err != nil && err != io.EOF { + t.Fatalf("Read error: %v", err) + } + expected := expbuf[:n] + if _, err := r.Read(expected); err != nil { + t.Fatalf("Random fill error: %v", err) + } + if !bytes.Equal(buf[:n], expected) { + for i := range n { + if buf[i] != expected[i] { + t.Fatalf("Data mismatch at byte %d: got %v, want %v", bytesReceived+int64(i), buf[i], expected[i]) + } + } + t.Fatalf("Data mismatch at byte %d", bytesReceived) + } + bytesReceived += int64(n) + if bytesReceived-lastPrint >= 1<<28 { // print every 256MB + lastPrint = bytesReceived + t.Logf("Received %d%% (%d/%d bytes)", int(bytesReceived*100/N), bytesReceived, N) } } - cli.Close() } func TestSendVector(t *testing.T) { - port := int(atomic.AddUint32(&baseport, 1)) - l := echoServer(port) + port := nextPort() + block1, _ := NewSalsa20BlockCrypt(pass) + l := echoServer(port, block1) defer l.Close() - cli, err := dialEcho(port) + block2, _ := NewSalsa20BlockCrypt(pass) + cli, err := dialEcho(port, block2) if err != nil { panic(err) } + defer cli.Close() cli.SetWriteDelay(false) - const N = 100 - buf := make([]byte, 20) - v := make([][]byte, 2) - for i := 0; i < N; i++ { - v[0] = []byte(fmt.Sprintf("hello%v", i)) - v[1] = []byte(fmt.Sprintf("world%v", i)) - msg := fmt.Sprintf("hello%vworld%v", i, i) - cli.WriteBuffers(v) - if n, err := cli.Read(buf); err == nil { - if string(buf[:n]) != msg { - t.Error(string(buf[:n]), msg) + randomEchoVectorTest(t, cli) +} + +func randomEchoVectorTest(t *testing.T, cli *UDPSession) { + seed := time.Now().UnixNano() + writerSrc := mrand.NewSource(seed) + readerSrc := mrand.NewSource(seed) + + bytesSent := int64(0) + bytesReceived := int64(0) + + const N = 100 * 1024 * 1024 + + // Writer goroutine + go func() { + r := mrand.New(writerSrc) + lastPrint := 0 + v := make([][]byte, 2) + for bytesSent < N { + length1 := mrand.Intn(1<<20) + 1 // Random length between 1 and 1MB + if bytesSent+int64(length1) > N { + length1 = int(N - bytesSent) + } + sndbuf1 := make([]byte, length1) + + for i := range sndbuf1 { + sndbuf1[i] = byte(r.Int()) + } + + length2 := mrand.Intn(1<<20) + 1 // Random length between 1 and 1MB + if bytesSent+int64(length2) > N { + length2 = int(N - bytesSent) + } + + sndbuf2 := make([]byte, length2) + for i := range sndbuf2 { + sndbuf2[i] = byte(r.Int()) + } + + v[0] = sndbuf1 + v[1] = sndbuf2 + + n, err := cli.WriteBuffers(v) + if err != nil { + t.Errorf("Write error: %v", err) + return + } + + if n != length1+length2 { + t.Errorf("Write length mismatch: got %v, want %v", n, length1+length2) + return + } + + bytesSent += int64(n) + if percent := int(bytesSent * 100 / N); percent >= lastPrint+10 { + lastPrint = percent + t.Logf("Sent %d%% (%d/%d bytes)", percent, bytesSent, N) } - } else { - panic(err) + } + }() + + // Reader goroutine + r := mrand.New(readerSrc) + lastPrint := 0 + for bytesReceived < N { + length := mrand.Intn(1<<20) + 1 // Random length between 1 and 1MB + if bytesReceived+int64(length) > N { + length = int(N - bytesReceived) + } + rcvbuf := make([]byte, length) + n, err := cli.Read(rcvbuf) + if err != nil && err != io.EOF { + t.Fatalf("Read error: %v", err) + } + for i := range n { + expectedByte := byte(r.Int()) + if rcvbuf[i] != expectedByte { + t.Fatalf("Data mismatch at byte %d: got %v, want %v", bytesReceived+int64(i), rcvbuf[i], expectedByte) + } + } + bytesReceived += int64(n) + if percent := int(bytesReceived * 100 / N); percent >= lastPrint+10 { + lastPrint = percent + t.Logf("Received %d%% (%d/%d bytes)", percent, bytesReceived, N) } } - cli.Close() } func TestTinyBufferReceiver(t *testing.T) { - port := int(atomic.AddUint32(&baseport, 1)) + port := nextPort() l := tinyBufferEchoServer(port) defer l.Close() @@ -317,10 +574,12 @@ func TestTinyBufferReceiver(t *testing.T) { if err != nil { panic(err) } + defer cli.Close() + const N = 100 snd := byte(0) fillBuffer := func(buf []byte) { - for i := 0; i < len(buf); i++ { + for i := range buf { buf[i] = snd snd++ } @@ -328,7 +587,7 @@ func TestTinyBufferReceiver(t *testing.T) { rcv := byte(0) check := func(buf []byte) bool { - for i := 0; i < len(buf); i++ { + for i := range buf { if buf[i] != rcv { return false } @@ -338,37 +597,43 @@ func TestTinyBufferReceiver(t *testing.T) { } sndbuf := make([]byte, 7) rcvbuf := make([]byte, 7) - for i := 0; i < N; i++ { + for range N { fillBuffer(sndbuf) cli.Write(sndbuf) - if n, err := io.ReadFull(cli, rcvbuf); err == nil { - if !check(rcvbuf[:n]) { - t.Fail() - } - } else { + + n, err := io.ReadFull(cli, rcvbuf) + if err != nil { panic(err) } + + if !check(rcvbuf[:n]) { + t.Fail() + return + } } - cli.Close() } func TestClose(t *testing.T) { var n int var err error - port := int(atomic.AddUint32(&baseport, 1)) - l := echoServer(port) + port := nextPort() + block1, _ := NewSalsa20BlockCrypt(pass) + l := echoServer(port, block1) defer l.Close() - cli, err := dialEcho(port) + block2, _ := NewSalsa20BlockCrypt(pass) + cli, err := dialEcho(port, block2) if err != nil { panic(err) } + defer cli.Close() // double close cli.Close() if cli.Close() == nil { t.Fatal("double close misbehavior") + return } // write after close @@ -376,15 +641,20 @@ func TestClose(t *testing.T) { n, err = cli.Write(buf) if n != 0 || err == nil { t.Fatal("write after close misbehavior") + return } // write, close, read, read - cli, err = dialEcho(port) + block3, _ := NewSalsa20BlockCrypt(pass) + cli, err = dialEcho(port, block3) if err != nil { panic(err) } - if n, err = cli.Write(buf); err != nil { + defer cli.Close() + + if _, err = cli.Write(buf); err != nil { t.Fatal("write misbehavior") + return } // wait until data arrival @@ -394,37 +664,40 @@ func TestClose(t *testing.T) { n, err = io.ReadFull(cli, buf) if err != nil { t.Fatal("closed conn drain bytes failed", err, n) + return } // after drain, read should return error n, err = cli.Read(buf) if n != 0 || err == nil { t.Fatal("write->close->drain->read misbehavior", err, n) + return } - cli.Close() } func TestParallel1024CLIENT_64BMSG_64CNT(t *testing.T) { - port := int(atomic.AddUint32(&baseport, 1)) - l := echoServer(port) + port := nextPort() + block, _ := NewSalsa20BlockCrypt(pass) + l := echoServer(port, block) defer l.Close() var wg sync.WaitGroup wg.Add(1024) - for i := 0; i < 1024; i++ { + for range 1024 { go parallel_client(&wg, port) } wg.Wait() } func parallel_client(wg *sync.WaitGroup, port int) (err error) { - cli, err := dialEcho(port) + block, _ := NewSalsa20BlockCrypt(pass) + cli, err := dialEcho(port, block) if err != nil { panic(err) } + defer cli.Close() err = echo_tester(cli, 64, 64) - cli.Close() wg.Done() return } @@ -446,21 +719,24 @@ func BenchmarkEchoSpeed1M(b *testing.B) { } func speedclient(b *testing.B, nbytes int) { - port := int(atomic.AddUint32(&baseport, 1)) - l := echoServer(port) + port := nextPort() + block1, _ := NewSalsa20BlockCrypt(pass) + l := echoServer(port, block1) defer l.Close() b.ReportAllocs() - cli, err := dialEcho(port) + block2, _ := NewSalsa20BlockCrypt(pass) + cli, err := dialEcho(port, block2) if err != nil { panic(err) } + defer cli.Close() if err := echo_tester(cli, nbytes, b.N); err != nil { b.Fail() + return } b.SetBytes(int64(nbytes)) - cli.Close() } func BenchmarkSinkSpeed4K(b *testing.B) { @@ -480,7 +756,7 @@ func BenchmarkSinkSpeed1M(b *testing.B) { } func sinkclient(b *testing.B, nbytes int) { - port := int(atomic.AddUint32(&baseport, 1)) + port := nextPort() l := sinkServer(port) defer l.Close() @@ -489,32 +765,34 @@ func sinkclient(b *testing.B, nbytes int) { if err != nil { panic(err) } + defer cli.Close() sink_tester(cli, nbytes, b.N) b.SetBytes(int64(nbytes)) - cli.Close() } func echo_tester(cli net.Conn, msglen, msgcount int) error { + go func() { + buf := make([]byte, msglen) + for range msgcount { + // send packet + if _, err := cli.Write(buf); err != nil { + panic(err) + } + } + }() + + // receive packet + nrecv := 0 buf := make([]byte, msglen) - for i := 0; i < msgcount; i++ { - // send packet - if _, err := cli.Write(buf); err != nil { + for { + n, err := cli.Read(buf) + if err != nil { return err } - - // receive packet - nrecv := 0 - for { - n, err := cli.Read(buf) - if err != nil { - return err - } else { - nrecv += n - if nrecv == msglen { - break - } - } + nrecv += n + if nrecv == msglen*msgcount { + break } } return nil @@ -523,7 +801,7 @@ func echo_tester(cli net.Conn, msglen, msgcount int) error { func sink_tester(cli *UDPSession, msglen, msgcount int) error { // sender buf := make([]byte, msglen) - for i := 0; i < msgcount; i++ { + for range msgcount { if _, err := cli.Write(buf); err != nil { return err } @@ -540,23 +818,27 @@ func TestSNMP(t *testing.T) { } func TestListenerClose(t *testing.T) { - port := int(atomic.AddUint32(&baseport, 1)) + port := nextPort() l, err := ListenWithOptions(fmt.Sprintf("127.0.0.1:%v", port), nil, 10, 3) if err != nil { t.Fail() + return } + defer l.Close() l.SetReadDeadline(time.Now().Add(time.Second)) l.SetWriteDeadline(time.Now().Add(time.Second)) l.SetDeadline(time.Now().Add(time.Second)) time.Sleep(2 * time.Second) if _, err := l.Accept(); err == nil { t.Fail() + return } l.Close() fakeaddr, _ := net.ResolveUDPAddr("udp6", "127.0.0.1:1111") if l.closeSession(fakeaddr) { t.Fail() + return } } @@ -575,22 +857,27 @@ func newClosedFlagPacketConn(c net.PacketConn) *closedFlagPacketConn { return &closedFlagPacketConn{c, false} } -// Listener should close a net.PacketConn that it created. +// Listener should not close a net.PacketConn that it did not create. // https://github.com/xtaci/kcp-go/issues/165 -func TestListenerOwnedPacketConn(t *testing.T) { - // ListenWithOptions creates its own net.PacketConn. - l, err := ListenWithOptions("127.0.0.1:0", nil, 0, 0) +func TestListenerNonOwnedPacketConn(t *testing.T) { + // Create a net.PacketConn not owned by the Listener. + c, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + panic(err) + } + defer c.Close() + // Make it remember when it has been closed. + pconn := newClosedFlagPacketConn(c) + + l, err := ServeConn(nil, 0, 0, pconn) if err != nil { panic(err) } defer l.Close() - // Replace the internal net.PacketConn with one that remembers when it - // has been closed. - pconn := newClosedFlagPacketConn(l.conn) - l.conn = pconn if pconn.Closed { - t.Fatal("owned PacketConn closed before Listener.Close()") + t.Fatal("non-owned PacketConn closed before Listener.Close()") + return } err = l.Close() @@ -598,15 +885,19 @@ func TestListenerOwnedPacketConn(t *testing.T) { panic(err) } - if !pconn.Closed { - t.Fatal("owned PacketConn not closed after Listener.Close()") + if pconn.Closed { + t.Fatal("non-owned PacketConn closed after Listener.Close()") + return } } -// Listener should not close a net.PacketConn that it did not create. +// UDPSession should not close a net.PacketConn that it did not create. // https://github.com/xtaci/kcp-go/issues/165 -func TestListenerNonOwnedPacketConn(t *testing.T) { - // Create a net.PacketConn not owned by the Listener. +func TestUDPSessionNonOwnedPacketConn(t *testing.T) { + l := sinkServer(0) + defer l.Close() + + // Create a net.PacketConn not owned by the UDPSession. c, err := net.ListenPacket("udp", "127.0.0.1:0") if err != nil { panic(err) @@ -615,88 +906,750 @@ func TestListenerNonOwnedPacketConn(t *testing.T) { // Make it remember when it has been closed. pconn := newClosedFlagPacketConn(c) - l, err := ServeConn(nil, 0, 0, pconn) + client, err := NewConn2(l.Addr(), nil, 0, 0, pconn) if err != nil { panic(err) } - defer l.Close() + defer client.Close() if pconn.Closed { - t.Fatal("non-owned PacketConn closed before Listener.Close()") + t.Fatal("non-owned PacketConn closed before UDPSession.Close()") + return } - err = l.Close() + err = client.Close() if err != nil { panic(err) } if pconn.Closed { - t.Fatal("non-owned PacketConn closed after Listener.Close()") + t.Fatal("non-owned PacketConn closed after UDPSession.Close()") + return } } -// UDPSession should close a net.PacketConn that it created. -// https://github.com/xtaci/kcp-go/issues/165 -func TestUDPSessionOwnedPacketConn(t *testing.T) { - l := sinkServer(0) +// this function test the data correctness with FEC and encryption enabled +func TestReliability(t *testing.T) { + port := nextPort() + block1, _ := NewSalsa20BlockCrypt(pass) + l := echoServer(port, block1) defer l.Close() - // DialWithOptions creates its own net.PacketConn. - client, err := DialWithOptions(l.Addr().String(), nil, 0, 0) + block2, _ := NewSalsa20BlockCrypt(pass) + cli, err := dialEcho(port, block2) if err != nil { panic(err) } - defer client.Close() - // Replace the internal net.PacketConn with one that remembers when it - // has been closed. - pconn := newClosedFlagPacketConn(client.conn) - client.conn = pconn + defer cli.Close() + cli.SetWriteDelay(false) - if pconn.Closed { - t.Fatal("owned PacketConn closed before UDPSession.Close()") + const N = 100000 + buf := make([]byte, 128) + msg := make([]byte, 128) + + for range N { + io.ReadFull(rand.Reader, msg) + cli.Write([]byte(msg)) + + n, err := io.ReadFull(cli, buf) + if err != nil { + panic(err) + } + + if !bytes.Equal(buf[:n], msg) { + t.Fail() + return + } } +} - err = client.Close() +func TestControl(t *testing.T) { + port := nextPort() + block, _ := NewSalsa20BlockCrypt(pass) + l, err := ListenWithOptions(fmt.Sprintf("127.0.0.1:%v", port), block, 10, 1) if err != nil { panic(err) } + defer l.Close() + + errorA := errors.New("A") + err = l.Control(func(conn net.PacketConn) error { + fmt.Printf("Listener Control: conn: %v\n", conn) + return errorA + }) - if !pconn.Closed { - t.Fatal("owned PacketConn not closed after UDPSession.Close()") + if err != errorA { + t.Fatal(err) + return + } + + block2, _ := NewSalsa20BlockCrypt(pass) + cli, err := dialEcho(port, block2) + if err != nil { + panic(err) + } + defer cli.Close() + + errorB := errors.New("B") + err = cli.Control(func(conn net.PacketConn) error { + fmt.Printf("Client Control: conn: %v\n", conn) + return errorB + }) + + if err != errorB { + t.Fatal(err) + return } } -// UDPSession should not close a net.PacketConn that it did not create. -// https://github.com/xtaci/kcp-go/issues/165 -func TestUDPSessionNonOwnedPacketConn(t *testing.T) { - l := sinkServer(0) +func TestSessionReadAfterClosed(t *testing.T) { + us, _ := net.ListenPacket("udp", "127.0.0.1:0") + uc, _ := net.ListenPacket("udp", "127.0.0.1:0") + defer us.Close() + defer uc.Close() + + knockDoor := func(c net.Conn, myid string) (string, error) { + c.SetDeadline(time.Now().Add(time.Second * 3)) + _, err := c.Write([]byte(myid)) + c.SetDeadline(time.Time{}) + if err != nil { + return "", err + } + c.SetDeadline(time.Now().Add(time.Second * 3)) + var buf [1024]byte + n, err := c.Read(buf[:]) + c.SetDeadline(time.Time{}) + return string(buf[:n]), err + } + + check := func(c1, c2 *UDPSession) { + done := make(chan struct{}, 1) + go func() { + rid, err := knockDoor(c2, "4321") + done <- struct{}{} + if err != nil { + panic(err) + } + + if rid != "1234" { + panic("mismatch id") + } + }() + + rid, err := knockDoor(c1, "1234") + if err != nil { + panic(err) + } + + if rid != "4321" { + panic("mismatch id") + } + <-done + } + + c1, err := NewConn3(0, uc.LocalAddr(), nil, 0, 0, us) + if err != nil { + panic(err) + } + defer c1.Close() + + c2, err := NewConn3(0, us.LocalAddr(), nil, 0, 0, uc) + if err != nil { + panic(err) + } + defer c2.Close() + + check(c1, c2) + c1.Close() + c2.Close() + // log.Println("conv id 0 is closed") + + c1, err = NewConn3(4321, uc.LocalAddr(), nil, 0, 0, us) + if err != nil { + panic(err) + } + defer c1.Close() + + c2, err = NewConn3(4321, us.LocalAddr(), nil, 0, 0, uc) + if err != nil { + panic(err) + } + defer c2.Close() + + check(c1, c2) + c1.Close() + c2.Close() +} + +func TestSetMTU(t *testing.T) { + port := nextPort() + block1, _ := NewSalsa20BlockCrypt(pass) + l := echoServer(port, block1) defer l.Close() - // Create a net.PacketConn not owned by the UDPSession. - c, err := net.ListenPacket("udp", "127.0.0.1:0") + block2, _ := NewSalsa20BlockCrypt(pass) + cli, err := dialEcho(port, block2) if err != nil { panic(err) } - defer c.Close() - // Make it remember when it has been closed. - pconn := newClosedFlagPacketConn(c) + defer cli.Close() + + // Session headerSize = cryptHeaderSize(20) + fecHeaderSizePlus2(8) = 28 + // UDPSession.SetMtu subtracts headerSize before calling kcp.SetMtu, + // so the minimum user-facing MTU that yields kcp mtu = IKCP_OVERHEAD+1 is: + // IKCP_OVERHEAD + 1 + headerSize = 25 + 28 = 53 + sessionOverhead := cryptHeaderSize + fecHeaderSizePlus2 + minValidMTU := IKCP_OVERHEAD + sessionOverhead + 1 + + tests := []struct { + name string + mtu int + want bool + }{ + {"reject: kcp mtu would be negative", IKCP_OVERHEAD, false}, + {"reject: kcp mtu would equal IKCP_OVERHEAD", minValidMTU - 1, false}, + {"accept: minimum valid MTU", minValidMTU, true}, + {"accept: typical MTU", 1400, true}, + } - client, err := NewConn2(l.Addr(), nil, 0, 0, pconn) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := cli.SetMtu(tt.mtu) + if got != tt.want { + t.Errorf("SetMtu(%d) = %v, want %v", tt.mtu, got, tt.want) + } + }) + } + + // echo round-trip at standard MTU + cli.SetMtu(1500) + cli.SetWriteDelay(false) + cli.SetLogger(IKCP_LOG_ALL, newLoggerWithMilliseconds().Info) + + sendBytes := make([]byte, 1500) + rand.Read(sendBytes) + cli.Write(sendBytes) + + buf := make([]byte, 1500) + + n, err := io.ReadFull(cli, buf) if err != nil { panic(err) } - defer client.Close() - if pconn.Closed { - t.Fatal("non-owned PacketConn closed before UDPSession.Close()") + if !bytes.Equal(buf[:n], sendBytes) { + t.Fail() + return } +} - err = client.Close() +func newLoggerWithMilliseconds() *slog.Logger { + timeFormat := "2006-01-02 15:04:05.000" + handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelInfo, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + a.Value = slog.StringValue(time.Now().Format(timeFormat)) + } + return a + }, + }) + return slog.New(handler) +} + +// TestSetLogger if want logging kcp trace need set build tags with debug +// trace log on: +// +// go test -run ^TestSetLogger$ -tags debug +// +// trace log off: +// +// go test -run ^TestSetLogger$ +func TestSetLogger(t *testing.T) { + port := nextPort() + block1, _ := NewSalsa20BlockCrypt(pass) + l := echoServer(port, block1) + defer l.Close() + + block2, _ := NewSalsa20BlockCrypt(pass) + cli, err := dialEcho(port, block2) if err != nil { panic(err) } + defer cli.Close() - if pconn.Closed { - t.Fatal("non-owned PacketConn closed after UDPSession.Close()") + cli.SetWriteDelay(true) + cli.SetDUP(1) + cli.SetLogger(IKCP_LOG_ALL, newLoggerWithMilliseconds().Info) + const N = 10 + buf := make([]byte, 10) + for i := range N { + msg := fmt.Sprintf("trace%v", i) + cli.Write([]byte(msg)) + + n, err := cli.Read(buf) + if err != nil { + panic(err) + } + + if string(buf[:n]) != msg { + t.Fail() + return + } + } +} + +type largeNonceAEAD struct { + cipher.AEAD +} + +func (*largeNonceAEAD) NonceSize() int { + return 1400 +} + +func (*largeNonceAEAD) Overhead() int { + return 0 +} + +func TestLargeNonce(t *testing.T) { + port := nextPort() + + aead := new(largeNonceAEAD) + block := NewAEADCrypt(aead) + + defer func() { + if recover() != "Overhead too large" { + t.Fatal("expect panic with Overhead too large") + return + } + }() + + cli, err := dialEcho(port, block) + if err != nil { + panic(err) + } + defer cli.Close() +} + +type largeOverheadAEAD struct { + cipher.AEAD +} + +func (*largeOverheadAEAD) NonceSize() int { + return 0 +} + +func (*largeOverheadAEAD) Overhead() int { + return 1400 +} + +func TestLargeOverhead(t *testing.T) { + port := nextPort() + + aead := new(largeOverheadAEAD) + block := NewAEADCrypt(aead) + + defer func() { + if recover() != "Overhead too large" { + t.Fatal("expect panic with Overhead too large") + return + } + }() + + cli, err := dialEcho(port, block) + if err != nil { + panic(err) + } + defer cli.Close() +} + +type checkAllocatedAEAD struct { + cipher.AEAD +} + +func (aead *checkAllocatedAEAD) Seal(dst, nonce, plaintext, additionalData []byte) []byte { + if dst == nil || cap(dst)-len(dst) < len(plaintext)+aead.AEAD.Overhead() { + panic("AEAD Seal will allocate new slice") + } + + ciphertext := aead.AEAD.Seal(dst, nonce, plaintext, additionalData) + if &ciphertext[0] != &dst[:1][0] { + panic("AEAD Seal allocated new slice") + } + return ciphertext +} + +func TestSealAllocated(t *testing.T) { + aes, err := aes.NewCipher(pass[:16]) + if err != nil { + panic(err) + } + + aesgcm, err := cipher.NewGCM(aes) + if err != nil { + panic(err) + } + + port := nextPort() + block := NewAEADCrypt(&checkAllocatedAEAD{aesgcm}) + + l := echoServer(port, block) + defer l.Close() + + cli, err := dialEcho(port, block) + if err != nil { + panic(err) + } + defer cli.Close() + + b := make([]byte, 100*1024*1024) // 100 MB + cli.Write(b) +} + +func TestSessionGetters(t *testing.T) { + sess := new(UDPSession) + sess.kcp = NewKCP(1, func(buf []byte, size int) {}) + + if sess.GetConv() != 1 { + t.Error("GetConv failed") + } + // RTO, SRTT, SRTTVar are dynamic, just check they don't panic + sess.GetRTO() + sess.GetSRTT() + sess.GetSRTTVar() +} + +func TestTimedSchedClose(t *testing.T) { + ts := NewTimedSched(1) + ts.Close() +} + +func TestListenDial(t *testing.T) { + l, err := Listen("127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer l.Close() + t.Logf("Listening on %s", l.Addr().String()) + + ch := make(chan struct{}) + go func() { + conn, err := l.Accept() + if err != nil { + return + } + t.Log("Accepted connection") + conn.Close() + close(ch) + }() + + conn, err := Dial(l.Addr().String()) + if err != nil { + t.Fatal(err) + } + t.Log("Dialed") + conn.Write([]byte("hello")) + t.Log("Wrote data") + time.Sleep(100 * time.Millisecond) + conn.Close() + <-ch +} + +// TestOOB verifies the end-to-end transmission and reception of OOB (Out-Of-Band) data: +// 1. The server registers an OOB callback and echoes back any received OOB data. +// 2. The client registers an OOB callback to validate the content and length of echoed OOB data. +// 3. The client sends OOB data of varying lengths in a loop, counting the number of echoes for each length. +// 4. Finally, it checks that all lengths of OOB data are correctly echoed back. +// This test ensures the OOB data channel is functional and the content is accurate. +func TestOOB(t *testing.T) { + port := nextPort() + block1, _ := NewAESGCMCrypt(pass) + + l, err := listenEcho(port, block1) + if err != nil { + panic(err) + } + defer l.Close() + + go func() { + // Server listens for OOB data and echoes it back + kcplistener := l.(*Listener) + kcplistener.SetReadBuffer(4 * 1024 * 1024) + kcplistener.SetWriteBuffer(4 * 1024 * 1024) + for { + s, err := l.Accept() + if err != nil { + return + } + sess := s.(*UDPSession) + sess.SetReadBuffer(4 * 1024 * 1024) + sess.SetWriteBuffer(4 * 1024 * 1024) + // Register OOB callback, echo back received OOB data immediately + sess.SetOOBHandler(func(buf []byte) { + if err := sess.SendOOB(buf); err != nil { + t.Errorf("server failed to echo OOB payload: %v", err) + } + }) + go handleEcho(sess) + } + }() + + block2, _ := NewAESGCMCrypt(pass) + cli, err := dialEcho(port, block2) + if err != nil { + panic(err) + } + defer cli.Close() + cli.SetWriteDelay(false) + + size := cli.GetOOBMaxSize() + if size < 1000 { + t.Errorf("unexpectedly small max OOB size: %d", size) + } + t.Log("Max OOB size:", size) + + sizePlus1 := size + 1 + counts := make([]atomic.Int32, sizePlus1) + + // Client registers OOB callback to validate echoed OOB data content and length + cli.SetOOBHandler(func(buf []byte) { + for i, b := range buf { + if b != byte(i) { + t.Fatalf( + "OOB echo payload mismatch at offset %d: expected %d, got %d", + i, byte(i), b, + ) + break + } + } + counts[len(buf)].Add(1) + }) + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + // Stress test for normal data channel to ensure main channel does not affect OOB + randomEchoTest(t, cli, 10*1024*1024) + }() + + go func() { + defer wg.Done() + // Send OOB data of varying lengths in a loop, content is [0,1,2,...] + buf := make([]byte, size) + for i := range len(buf) { + buf[i] = byte(i) + } + for i := range 10 * 1024 * 1024 { + if err := cli.SendOOB(buf[:i%sizePlus1]); err != nil { + t.Errorf("client failed to send OOB payload: %v", err) + } + if i%128 == 0 { + runtime.Gosched() + } + } + }() + + wg.Wait() + + // Check that all lengths of OOB data are correctly echoed back + for i := range counts { + if counts[i].Load() == 0 { + t.Errorf("missing OOB echo for payload length %d", i) + } + } +} + +// TestOOB_OneSideHandler verifies that OOB data can be received and processed +// when only the server sets the OOB handler and the client does not. +// +// The server OOB handler updates the shared 'counts' slice, which is initialized +// in the main goroutine and referenced by the handler for each OOB payload length. +// This test ensures that the server can receive and correctly process OOB packets +// of all possible lengths, even if the client does not set any OOB handler. +func TestOOB_OneSideHandler(t *testing.T) { + port := nextPort() + block1, _ := NewAESGCMCrypt(pass) + + l, err := listenEcho(port, block1) + if err != nil { + panic(err) + } + defer l.Close() + + counts := make([]atomic.Int32, mtuLimit) + errCh := make(chan error, 1) + + // Get OOB max payload size and initialize the shared counts slice. + go func() { + // Server sets OOB handler and counts received OOB packets by length. + // The handler directly updates the shared 'counts' slice. + kcplistener := l.(*Listener) + kcplistener.SetReadBuffer(4 * 1024 * 1024) + kcplistener.SetWriteBuffer(4 * 1024 * 1024) + for { + s, err := l.Accept() + if err != nil { + return + } + sess := s.(*UDPSession) + sess.SetReadBuffer(4 * 1024 * 1024) + sess.SetWriteBuffer(4 * 1024 * 1024) + // Only the server sets the OOB handler. + // The handler references the 'counts' slice from the main goroutine. + sess.SetOOBHandler(func(buf []byte) { + // Validate OOB payload content and count by length. + for i, b := range buf { + if b != byte(i) { + select { + case errCh <- fmt.Errorf("OOB payload mismatch at offset %d: expected %d, got %d", i, byte(i), b): + default: + } + } + } + counts[len(buf)].Add(1) + }) + go handleEcho(sess) + } + }() + + block2, _ := NewAESGCMCrypt(pass) + cli, err := dialEcho(port, block2) + if err != nil { + panic(err) + } + defer cli.Close() + cli.SetWriteDelay(false) + + size := cli.GetOOBMaxSize() + sizePlus1 := size + 1 + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + // Client does NOT set OOB handler, only sends OOB packets of varying lengths. + buf := make([]byte, size) + for i := range len(buf) { + buf[i] = byte(i) + } + for i := range 10 * 1024 * 1024 { + if err := cli.SendOOB(buf[:i%sizePlus1]); err != nil { + t.Errorf("client failed to send OOB payload: %v", err) + } + if i%128 == 0 { + runtime.Gosched() + } + } + }() + + wg.Wait() + + // Give the server time to process incoming OOB packets. + time.Sleep(500 * time.Millisecond) + + select { + case err := <-errCh: + t.Fatalf("%v", err) + default: + } + + // Check that all lengths of OOB data are received by the server. + for i := range counts[:sizePlus1] { + if counts[i].Load() == 0 { + t.Errorf("server missing OOB for payload length %d", i) + } + } +} + +func TestSetOOBHandler_Basic(t *testing.T) { + sess := new(UDPSession) + sess.kcp = NewKCP(1, func(buf []byte, size int) {}) + // Should return error if FEC is not enabled + err := sess.SetOOBHandler(func([]byte) {}) + if err == nil { + t.Error("expected error when FEC is not enabled") + } + + // Should allow register/unregister callback after FEC enabled + sess.fecEncoder = newFECEncoder(1, 1, 0) + if err := sess.SetOOBHandler(nil); err != nil { + t.Error("unregister nil callback should not error") + } + // After setting nil, callbackForOOB should be a dummy handler, and calling it should not panic + defer func() { + if r := recover(); r != nil { + t.Errorf("dummy OOB handler should not panic, got panic: %v", r) + } + }() + if f := sess.callbackForOOB.Load(); f != nil { + typeName := reflect.TypeOf(f).String() + t.Logf("callbackForOOB type: %s", typeName) + + if cb, ok := f.(OOBCallBackType); ok { + cb([]byte("dummy")) + } else { + t.Errorf("callbackForOOB type assertion failed: got %T", f) + } + } else { + t.Error("callbackForOOB should not be nil after SetOOBHandler(nil)") + } + called := false + cb := func([]byte) { called = true } + if err := sess.SetOOBHandler(cb); err != nil { + t.Errorf("register callback failed: %v", err) + } + // Simulate callback invocation + if f := sess.callbackForOOB.Load(); f != nil { + if cb2, ok := f.(OOBCallBackType); ok { + cb2([]byte("test")) + if !called { + t.Error("callback not called as expected") + } + } else { + t.Errorf("callbackForOOB type assertion failed: got %T", f) + } + } +} + +func TestGetOOBMaxSize(t *testing.T) { + sess := new(UDPSession) + sess.kcp = NewKCP(1, func(buf []byte, size int) {}) + // Should return 0 if FEC is not enabled + if n := sess.GetOOBMaxSize(); n != 0 { + t.Errorf("expected 0 when FEC not enabled, got %d", n) + } + // Should return mtu-4 after FEC enabled + sess.fecEncoder = newFECEncoder(1, 1, 0) + sess.kcp.mtu = 1400 + if n := sess.GetOOBMaxSize(); n != 1396 { + t.Errorf("expected 1396, got %d", n) + } +} + +func TestSendOOB_Errors(t *testing.T) { + sess := new(UDPSession) + sess.kcp = NewKCP(1, func(buf []byte, size int) {}) + // Should error if FEC is not enabled + err := sess.SendOOB([]byte("abc")) + if err == nil || err.Error() != "OOB requires FEC to be enabled" { + t.Errorf("expected 'OOB requires FEC to be enabled', got %v", err) + } + // NOTE: SendOOB no longer requires callbackForOOB to be non-nil on sender side. + // It should not return error if callback is not set after FEC is enabled. + sess.fecEncoder = newFECEncoder(1, 1, 0) + err = sess.SendOOB([]byte("abc")) + if err != nil { + t.Errorf("expected no error when callback not set, got %v", err) + } + // Should error if payload is too large + sess.kcp.mtu = 8 + cb := func([]byte) {} + sess.SetOOBHandler(cb) + err = sess.SendOOB([]byte("123456789")) + if err == nil || err.Error() != "OOB payload too large" { + t.Errorf("expected 'OOB payload too large', got %v", err) } } diff --git a/vendor/github.com/xtaci/kcp-go/v5/snmp.go b/vendor/github.com/xtaci/kcp-go/v5/snmp.go index f9618107..731f9129 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/snmp.go +++ b/vendor/github.com/xtaci/kcp-go/v5/snmp.go @@ -1,36 +1,64 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + package kcp import ( - "fmt" + "strconv" "sync/atomic" ) // Snmp defines network statistics indicator type Snmp struct { - BytesSent uint64 // bytes sent from upper level - BytesReceived uint64 // bytes received to upper level - MaxConn uint64 // max number of connections ever reached - ActiveOpens uint64 // accumulated active open connections - PassiveOpens uint64 // accumulated passive open connections - CurrEstab uint64 // current number of established connections - InErrs uint64 // UDP read errors reported from net.PacketConn - InCsumErrors uint64 // checksum errors from CRC32 - KCPInErrors uint64 // packet iput errors reported from KCP - InPkts uint64 // incoming packets count - OutPkts uint64 // outgoing packets count - InSegs uint64 // incoming KCP segments - OutSegs uint64 // outgoing KCP segments - InBytes uint64 // UDP bytes received - OutBytes uint64 // UDP bytes sent - RetransSegs uint64 // accmulated retransmited segments - FastRetransSegs uint64 // accmulated fast retransmitted segments - EarlyRetransSegs uint64 // accmulated early retransmitted segments - LostSegs uint64 // number of segs inferred as lost - RepeatSegs uint64 // number of segs duplicated - FECRecovered uint64 // correct packets recovered from FEC - FECErrs uint64 // incorrect packets recovered from FEC - FECParityShards uint64 // FEC segments received - FECShortShards uint64 // number of data shards that's not enough for recovery + BytesSent uint64 // bytes sent from upper level + BytesReceived uint64 // bytes received to upper level + MaxConn uint64 // max number of connections ever reached + ActiveOpens uint64 // accumulated active open connections + PassiveOpens uint64 // accumulated passive open connections + CurrEstab uint64 // current number of established connections + InErrs uint64 // UDP read errors reported from net.PacketConn + InCsumErrors uint64 // checksum errors from CRC32 + KCPInErrors uint64 // packet input errors reported from KCP + InPkts uint64 // incoming packets count + OutPkts uint64 // outgoing packets count + InSegs uint64 // incoming KCP segments + OutSegs uint64 // outgoing KCP segments + InBytes uint64 // UDP bytes received + OutBytes uint64 // UDP bytes sent + RetransSegs uint64 // accumulated retransmitted segments + FastRetransSegs uint64 // accumulated fast retransmitted segments + EarlyRetransSegs uint64 // accumulated early retransmitted segments + LostSegs uint64 // number of segs inferred as lost + RepeatSegs uint64 // number of segs duplicated + FECFullShardSet uint64 // number of FEC segments that are full + FECRecovered uint64 // correct packets recovered from FEC + FECErrs uint64 // incorrect packets recovered from FEC + FECParityShards uint64 // FEC segments received + FECShardSet uint64 // number of shard sets that are not yet complete + FECShardMin uint64 // minimum shard ID among active FEC shard sets + RingBufferSndQueue uint64 // Len of segments in send queue ring buffer + RingBufferRcvQueue uint64 // Len of segments in receive queue ring buffer + RingBufferSndBuffer uint64 // Len of segments in send buffer ring buffer + OOBPackets uint64 // number of OOB packets received } func newSnmp() *Snmp { @@ -60,10 +88,16 @@ func (s *Snmp) Header() []string { "EarlyRetransSegs", "LostSegs", "RepeatSegs", + "FECFullShards", "FECParityShards", "FECErrs", "FECRecovered", - "FECShortShards", + "FECShardSet", + "FECShardMin", + "RingBufferSndQueue", + "RingBufferRcvQueue", + "RingBufferSndBuffer", + "OOBPackets", } } @@ -71,30 +105,36 @@ func (s *Snmp) Header() []string { func (s *Snmp) ToSlice() []string { snmp := s.Copy() return []string{ - fmt.Sprint(snmp.BytesSent), - fmt.Sprint(snmp.BytesReceived), - fmt.Sprint(snmp.MaxConn), - fmt.Sprint(snmp.ActiveOpens), - fmt.Sprint(snmp.PassiveOpens), - fmt.Sprint(snmp.CurrEstab), - fmt.Sprint(snmp.InErrs), - fmt.Sprint(snmp.InCsumErrors), - fmt.Sprint(snmp.KCPInErrors), - fmt.Sprint(snmp.InPkts), - fmt.Sprint(snmp.OutPkts), - fmt.Sprint(snmp.InSegs), - fmt.Sprint(snmp.OutSegs), - fmt.Sprint(snmp.InBytes), - fmt.Sprint(snmp.OutBytes), - fmt.Sprint(snmp.RetransSegs), - fmt.Sprint(snmp.FastRetransSegs), - fmt.Sprint(snmp.EarlyRetransSegs), - fmt.Sprint(snmp.LostSegs), - fmt.Sprint(snmp.RepeatSegs), - fmt.Sprint(snmp.FECParityShards), - fmt.Sprint(snmp.FECErrs), - fmt.Sprint(snmp.FECRecovered), - fmt.Sprint(snmp.FECShortShards), + strconv.FormatUint(snmp.BytesSent, 10), + strconv.FormatUint(snmp.BytesReceived, 10), + strconv.FormatUint(snmp.MaxConn, 10), + strconv.FormatUint(snmp.ActiveOpens, 10), + strconv.FormatUint(snmp.PassiveOpens, 10), + strconv.FormatUint(snmp.CurrEstab, 10), + strconv.FormatUint(snmp.InErrs, 10), + strconv.FormatUint(snmp.InCsumErrors, 10), + strconv.FormatUint(snmp.KCPInErrors, 10), + strconv.FormatUint(snmp.InPkts, 10), + strconv.FormatUint(snmp.OutPkts, 10), + strconv.FormatUint(snmp.InSegs, 10), + strconv.FormatUint(snmp.OutSegs, 10), + strconv.FormatUint(snmp.InBytes, 10), + strconv.FormatUint(snmp.OutBytes, 10), + strconv.FormatUint(snmp.RetransSegs, 10), + strconv.FormatUint(snmp.FastRetransSegs, 10), + strconv.FormatUint(snmp.EarlyRetransSegs, 10), + strconv.FormatUint(snmp.LostSegs, 10), + strconv.FormatUint(snmp.RepeatSegs, 10), + strconv.FormatUint(snmp.FECFullShardSet, 10), + strconv.FormatUint(snmp.FECParityShards, 10), + strconv.FormatUint(snmp.FECErrs, 10), + strconv.FormatUint(snmp.FECRecovered, 10), + strconv.FormatUint(snmp.FECShardSet, 10), + strconv.FormatUint(snmp.FECShardMin, 10), + strconv.FormatUint(snmp.RingBufferSndQueue, 10), + strconv.FormatUint(snmp.RingBufferRcvQueue, 10), + strconv.FormatUint(snmp.RingBufferSndBuffer, 10), + strconv.FormatUint(snmp.OOBPackets, 10), } } @@ -121,10 +161,16 @@ func (s *Snmp) Copy() *Snmp { d.EarlyRetransSegs = atomic.LoadUint64(&s.EarlyRetransSegs) d.LostSegs = atomic.LoadUint64(&s.LostSegs) d.RepeatSegs = atomic.LoadUint64(&s.RepeatSegs) + d.FECFullShardSet = atomic.LoadUint64(&s.FECFullShardSet) d.FECParityShards = atomic.LoadUint64(&s.FECParityShards) d.FECErrs = atomic.LoadUint64(&s.FECErrs) d.FECRecovered = atomic.LoadUint64(&s.FECRecovered) - d.FECShortShards = atomic.LoadUint64(&s.FECShortShards) + d.FECShardSet = atomic.LoadUint64(&s.FECShardSet) + d.FECShardMin = atomic.LoadUint64(&s.FECShardMin) + d.RingBufferSndQueue = atomic.LoadUint64(&s.RingBufferSndQueue) + d.RingBufferRcvQueue = atomic.LoadUint64(&s.RingBufferRcvQueue) + d.RingBufferSndBuffer = atomic.LoadUint64(&s.RingBufferSndBuffer) + d.OOBPackets = atomic.LoadUint64(&s.OOBPackets) return d } @@ -150,10 +196,16 @@ func (s *Snmp) Reset() { atomic.StoreUint64(&s.EarlyRetransSegs, 0) atomic.StoreUint64(&s.LostSegs, 0) atomic.StoreUint64(&s.RepeatSegs, 0) + atomic.StoreUint64(&s.FECFullShardSet, 0) atomic.StoreUint64(&s.FECParityShards, 0) atomic.StoreUint64(&s.FECErrs, 0) atomic.StoreUint64(&s.FECRecovered, 0) - atomic.StoreUint64(&s.FECShortShards, 0) + atomic.StoreUint64(&s.FECShardSet, 0) + atomic.StoreUint64(&s.FECShardMin, 0) + atomic.StoreUint64(&s.RingBufferSndQueue, 0) + atomic.StoreUint64(&s.RingBufferRcvQueue, 0) + atomic.StoreUint64(&s.RingBufferSndBuffer, 0) + atomic.StoreUint64(&s.OOBPackets, 0) } // DefaultSnmp is the global KCP connection statistics collector diff --git a/vendor/github.com/xtaci/kcp-go/v5/timedsched.go b/vendor/github.com/xtaci/kcp-go/v5/timedsched.go index 2db7c206..e53571c3 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/timedsched.go +++ b/vendor/github.com/xtaci/kcp-go/v5/timedsched.go @@ -1,3 +1,25 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + package kcp import ( @@ -7,8 +29,9 @@ import ( "time" ) -// SystemTimedSched is the library level timed-scheduler -var SystemTimedSched *TimedSched = NewTimedSched(runtime.NumCPU()) +// SystemTimedSched is the library-level timed scheduler, shared by all sessions. +// It drives periodic KCP flush()/update() calls, avoiding one goroutine per session. +var SystemTimedSched *TimedSched = NewTimedSched(max(runtime.NumCPU(), 2)) type timedFunc struct { execute func() @@ -18,27 +41,44 @@ type timedFunc struct { // a heap for sorted timed function type timedFuncHeap []timedFunc -func (h timedFuncHeap) Len() int { return len(h) } -func (h timedFuncHeap) Less(i, j int) bool { return h[i].ts.Before(h[j].ts) } -func (h timedFuncHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } -func (h *timedFuncHeap) Push(x interface{}) { *h = append(*h, x.(timedFunc)) } -func (h *timedFuncHeap) Pop() interface{} { +func (h timedFuncHeap) Len() int { return len(h) } +func (h timedFuncHeap) Less(i, j int) bool { return h[i].ts.Before(h[j].ts) } +func (h timedFuncHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } +func (h *timedFuncHeap) Push(x any) { *h = append(*h, x.(timedFunc)) } +func (h *timedFuncHeap) Pop() any { old := *h n := len(old) x := old[n-1] - old[n-1].execute = nil // avoid memory leak - *h = old[0 : n-1] + old[n-1] = timedFunc{} // clear to avoid memory leak (both execute and ts) + *h = old[:n-1] return x } -// TimedSched represents the control struct for timed parallel scheduler +// TimedSched is a two-stage parallel scheduler for timed task execution. +// +// Architecture (two-stage pipeline): +// +// Stage 1 - "prepend" goroutine: +// External callers submit tasks via Put(). Tasks are appended to a shared +// slice under a mutex (fast, non-blocking). The prepend goroutine drains +// this slice and feeds tasks one-by-one into chTask. +// +// Stage 2 - "sched" goroutines (N = NumCPU): +// Each sched goroutine maintains a local min-heap of pending tasks. +// It receives tasks from chTask, executes overdue ones immediately, +// and uses a timer for the earliest future task. +// +// Why two stages? +// - Stage 1 decouples callers from the scheduler's internal heap, +// ensuring Put() never blocks on heap operations. +// - Stage 2 runs in parallel, distributing timer-driven work across CPUs. type TimedSched struct { - // prepending tasks + // Stage 1: task collection prependTasks []timedFunc prependLock sync.Mutex chPrependNotify chan struct{} - // tasks will be distributed through chTask + // Stage 2: parallel execution chTask chan timedFunc dieOnce sync.Once @@ -52,16 +92,20 @@ func NewTimedSched(parallel int) *TimedSched { ts.die = make(chan struct{}) ts.chPrependNotify = make(chan struct{}, 1) - for i := 0; i < parallel; i++ { + for range parallel { go ts.sched() } go ts.prepend() return ts } +// sched is a worker goroutine (Stage 2) that manages a local min-heap +// of timed tasks. It executes tasks when their deadline arrives. func (ts *TimedSched) sched() { - var tasks timedFuncHeap timer := time.NewTimer(0) + defer timer.Stop() + + var tasks timedFuncHeap drained := false for { select { @@ -97,28 +141,22 @@ func (ts *TimedSched) sched() { } } +// prepend is the Stage 1 goroutine that collects externally submitted tasks +// and feeds them into the Stage 2 worker pool via chTask. func (ts *TimedSched) prepend() { var tasks []timedFunc for { select { case <-ts.chPrependNotify: ts.prependLock.Lock() - // keep cap to reuse slice - if cap(tasks) < cap(ts.prependTasks) { - tasks = make([]timedFunc, 0, cap(ts.prependTasks)) - } - tasks = tasks[:len(ts.prependTasks)] - copy(tasks, ts.prependTasks) - for k := range ts.prependTasks { - ts.prependTasks[k].execute = nil // avoid memory leak - } - ts.prependTasks = ts.prependTasks[:0] + // swap slices to minimize time under lock + tasks, ts.prependTasks = ts.prependTasks, tasks[:0] ts.prependLock.Unlock() for k := range tasks { select { case ts.chTask <- tasks[k]: - tasks[k].execute = nil // avoid memory leak + tasks[k] = timedFunc{} // clear to avoid memory leak case <-ts.die: return } diff --git a/vendor/github.com/xtaci/kcp-go/v5/tx.go b/vendor/github.com/xtaci/kcp-go/v5/tx.go index 3397b82e..9fe6e7b4 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/tx.go +++ b/vendor/github.com/xtaci/kcp-go/v5/tx.go @@ -1,3 +1,25 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + package kcp import ( @@ -7,17 +29,19 @@ import ( "golang.org/x/net/ipv4" ) +// defaultTx is the default transmission function for UDP sessions. func (s *UDPSession) defaultTx(txqueue []ipv4.Message) { nbytes := 0 npkts := 0 for k := range txqueue { - if n, err := s.conn.WriteTo(txqueue[k].Buffers[0], txqueue[k].Addr); err == nil { - nbytes += n - npkts++ - } else { + n, err := s.conn.WriteTo(txqueue[k].Buffers[0], txqueue[k].Addr) + if err != nil { s.notifyWriteError(errors.WithStack(err)) break } + + nbytes += n + npkts++ } atomic.AddUint64(&DefaultSnmp.OutPkts, uint64(npkts)) atomic.AddUint64(&DefaultSnmp.OutBytes, uint64(nbytes)) diff --git a/vendor/github.com/xtaci/kcp-go/v5/tx_generic.go b/vendor/github.com/xtaci/kcp-go/v5/tx_generic.go index 0b4f3494..f85d2e20 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/tx_generic.go +++ b/vendor/github.com/xtaci/kcp-go/v5/tx_generic.go @@ -1,4 +1,26 @@ -// +build !linux +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +//go:build !linux package kcp diff --git a/vendor/github.com/xtaci/kcp-go/v5/tx_linux.go b/vendor/github.com/xtaci/kcp-go/v5/tx_linux.go index 4f19df56..82e15812 100644 --- a/vendor/github.com/xtaci/kcp-go/v5/tx_linux.go +++ b/vendor/github.com/xtaci/kcp-go/v5/tx_linux.go @@ -1,19 +1,41 @@ -// +build linux +// The MIT License (MIT) +// +// Copyright (c) 2015 xtaci +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +//go:build linux package kcp import ( - "net" - "os" "sync/atomic" "github.com/pkg/errors" "golang.org/x/net/ipv4" ) +// tx is the optimized transmit path for Linux, utilizing the sendmmsg syscall +// to batch-send multiple UDP packets in a single system call. func (s *UDPSession) tx(txqueue []ipv4.Message) { // default version - if s.xconn == nil || s.xconnWriteError != nil { + if s.platform.batchConn == nil { s.defaultTx(txqueue) return } @@ -22,28 +44,17 @@ func (s *UDPSession) tx(txqueue []ipv4.Message) { nbytes := 0 npkts := 0 for len(txqueue) > 0 { - if n, err := s.xconn.WriteBatch(txqueue, 0); err == nil { - for k := range txqueue[:n] { - nbytes += len(txqueue[k].Buffers[0]) - } - npkts += n - txqueue = txqueue[n:] - } else { - // compatibility issue: - // for linux kernel<=2.6.32, support for sendmmsg is not available - // an error of type os.SyscallError will be returned - if operr, ok := err.(*net.OpError); ok { - if se, ok := operr.Err.(*os.SyscallError); ok { - if se.Syscall == "sendmmsg" { - s.xconnWriteError = se - s.defaultTx(txqueue) - return - } - } - } + n, err := s.platform.batchConn.WriteBatch(txqueue, 0) + if err != nil { s.notifyWriteError(errors.WithStack(err)) break } + + for k := range txqueue[:n] { + nbytes += len(txqueue[k].Buffers[0]) + } + npkts += n + txqueue = txqueue[n:] } atomic.AddUint64(&DefaultSnmp.OutPkts, uint64(npkts))