You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
200 lines
4.5 KiB
200 lines
4.5 KiB
package tcputil
|
|
|
|
import (
|
|
"net"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/xtaci/kcp-go/v5"
|
|
"github.com/xtaci/smux"
|
|
)
|
|
|
|
type kcpProfile struct {
|
|
nodelay int
|
|
interval int
|
|
resend int
|
|
nc int
|
|
sndWnd int
|
|
rcvWnd int
|
|
mtu int
|
|
ackNoDelay bool
|
|
}
|
|
|
|
func selectedKCPProfile() kcpProfile {
|
|
profile := strings.ToLower(strings.TrimSpace(os.Getenv("VK_TURN_KCP_PROFILE")))
|
|
var cfg kcpProfile
|
|
switch profile {
|
|
case "legacy", "fast":
|
|
cfg = kcpProfile{
|
|
nodelay: 1,
|
|
interval: 10,
|
|
resend: 2,
|
|
nc: 1,
|
|
sndWnd: 4096,
|
|
rcvWnd: 4096,
|
|
mtu: 1280,
|
|
ackNoDelay: true,
|
|
}
|
|
case "cc", "balanced":
|
|
cfg = kcpProfile{
|
|
nodelay: 1,
|
|
interval: 20,
|
|
resend: 2,
|
|
nc: 0,
|
|
sndWnd: 512,
|
|
rcvWnd: 512,
|
|
mtu: 1200,
|
|
ackNoDelay: true,
|
|
}
|
|
case "slow", "conservative":
|
|
cfg = kcpProfile{
|
|
nodelay: 0,
|
|
interval: 40,
|
|
resend: 2,
|
|
nc: 0,
|
|
sndWnd: 256,
|
|
rcvWnd: 256,
|
|
mtu: 1150,
|
|
ackNoDelay: false,
|
|
}
|
|
default:
|
|
cfg = kcpProfile{
|
|
nodelay: 1,
|
|
interval: 20,
|
|
resend: 2,
|
|
nc: 1,
|
|
sndWnd: 512,
|
|
rcvWnd: 512,
|
|
mtu: 1200,
|
|
ackNoDelay: true,
|
|
}
|
|
}
|
|
|
|
cfg.nodelay = envInt("VK_TURN_KCP_NODELAY", cfg.nodelay)
|
|
cfg.interval = envInt("VK_TURN_KCP_INTERVAL", cfg.interval)
|
|
cfg.resend = envInt("VK_TURN_KCP_RESEND", cfg.resend)
|
|
cfg.nc = envInt("VK_TURN_KCP_NC", cfg.nc)
|
|
cfg.sndWnd = envInt("VK_TURN_KCP_SNDWND", cfg.sndWnd)
|
|
cfg.rcvWnd = envInt("VK_TURN_KCP_RCVWND", cfg.rcvWnd)
|
|
cfg.mtu = envInt("VK_TURN_KCP_MTU", cfg.mtu)
|
|
cfg.ackNoDelay = envBool("VK_TURN_KCP_ACK_NODELAY", cfg.ackNoDelay)
|
|
return cfg
|
|
}
|
|
|
|
func envInt(name string, fallback int) int {
|
|
raw := strings.TrimSpace(os.Getenv(name))
|
|
if raw == "" {
|
|
return fallback
|
|
}
|
|
value, err := strconv.Atoi(raw)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
return value
|
|
}
|
|
|
|
func envBool(name string, fallback bool) bool {
|
|
raw := strings.ToLower(strings.TrimSpace(os.Getenv(name)))
|
|
switch raw {
|
|
case "1", "true", "yes", "on":
|
|
return true
|
|
case "0", "false", "no", "off":
|
|
return false
|
|
default:
|
|
return fallback
|
|
}
|
|
}
|
|
|
|
// DtlsPacketConn wraps a net.Conn (DTLS) as a net.PacketConn for KCP.
|
|
// Each DTLS Read/Write preserves message boundaries (datagram semantics).
|
|
type DtlsPacketConn struct {
|
|
conn net.Conn
|
|
}
|
|
|
|
func NewDtlsPacketConn(conn net.Conn) *DtlsPacketConn {
|
|
return &DtlsPacketConn{conn: conn}
|
|
}
|
|
|
|
func (d *DtlsPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
|
n, err := d.conn.Read(b)
|
|
return n, d.conn.RemoteAddr(), err
|
|
}
|
|
|
|
func (d *DtlsPacketConn) WriteTo(b []byte, _ net.Addr) (int, error) {
|
|
return d.conn.Write(b)
|
|
}
|
|
|
|
func (d *DtlsPacketConn) Close() error {
|
|
return d.conn.Close()
|
|
}
|
|
|
|
func (d *DtlsPacketConn) LocalAddr() net.Addr {
|
|
return d.conn.LocalAddr()
|
|
}
|
|
|
|
func (d *DtlsPacketConn) SetDeadline(t time.Time) error {
|
|
return d.conn.SetDeadline(t)
|
|
}
|
|
|
|
func (d *DtlsPacketConn) SetReadDeadline(t time.Time) error {
|
|
return d.conn.SetReadDeadline(t)
|
|
}
|
|
|
|
func (d *DtlsPacketConn) SetWriteDeadline(t time.Time) error {
|
|
return d.conn.SetWriteDeadline(t)
|
|
}
|
|
|
|
// NewKCPOverDTLS creates a KCP session over a DTLS connection.
|
|
// isServer: true for server-side (listener), false for client-side (dialer).
|
|
func NewKCPOverDTLS(dtlsConn net.Conn, isServer bool) (*kcp.UDPSession, error) {
|
|
pc := NewDtlsPacketConn(dtlsConn)
|
|
|
|
block, err := kcp.NewNoneBlockCrypt(nil) // DTLS already encrypts
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var sess *kcp.UDPSession
|
|
|
|
if isServer {
|
|
// Server: listen on the PacketConn and accept one session
|
|
var listener *kcp.Listener
|
|
listener, err = kcp.ServeConn(block, 0, 0, pc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = listener.SetDeadline(time.Now().Add(30 * time.Second)); err != nil {
|
|
return nil, err
|
|
}
|
|
sess, err = listener.AcceptKCP()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// Client: dial through the PacketConn
|
|
sess, err = kcp.NewConn2(dtlsConn.RemoteAddr(), block, 0, 0, pc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
profile := selectedKCPProfile()
|
|
sess.SetNoDelay(profile.nodelay, profile.interval, profile.resend, profile.nc)
|
|
sess.SetWindowSize(profile.sndWnd, profile.rcvWnd)
|
|
sess.SetMtu(profile.mtu)
|
|
sess.SetACKNoDelay(profile.ackNoDelay)
|
|
|
|
return sess, nil
|
|
}
|
|
|
|
// DefaultSmuxConfig returns smux config tuned for TURN tunnel.
|
|
func DefaultSmuxConfig() *smux.Config {
|
|
cfg := smux.DefaultConfig()
|
|
cfg.MaxReceiveBuffer = 4 * 1024 * 1024
|
|
cfg.MaxStreamBuffer = 1 * 1024 * 1024
|
|
cfg.KeepAliveInterval = 10 * time.Second
|
|
cfg.KeepAliveTimeout = 30 * time.Second
|
|
return cfg
|
|
}
|
|
|