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.
348 lines
9.1 KiB
348 lines
9.1 KiB
package api
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"math/big"
|
|
"net"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
connectip "github.com/Diniboy1123/connect-ip-go"
|
|
"github.com/cacggghp/vk-turn-proxy/client/warp/internal"
|
|
"github.com/songgao/water"
|
|
"golang.zx2c4.com/wireguard/tun"
|
|
)
|
|
|
|
// Verbose controls whether diagnostic logs like tunnel stats are printed.
|
|
var Verbose bool
|
|
|
|
// NetBuffer is a pool of byte slices with a fixed capacity.
|
|
// Helps to reduce memory allocations and improve performance.
|
|
// It uses a sync.Pool to manage the byte slices.
|
|
// The capacity of the byte slices is set when the pool is created.
|
|
type NetBuffer struct {
|
|
capacity int
|
|
buf sync.Pool
|
|
}
|
|
|
|
// Get returns a byte slice from the pool.
|
|
func (n *NetBuffer) Get() []byte {
|
|
return *(n.buf.Get().(*[]byte))
|
|
}
|
|
|
|
// Put places a byte slice back into the pool.
|
|
// It checks if the capacity of the byte slice matches the pool's capacity.
|
|
// If it doesn't match, the byte slice is not returned to the pool.
|
|
func (n *NetBuffer) Put(buf []byte) {
|
|
if cap(buf) != n.capacity {
|
|
return
|
|
}
|
|
n.buf.Put(&buf)
|
|
}
|
|
|
|
// NewNetBuffer creates a new NetBuffer with the specified capacity.
|
|
// The capacity must be greater than 0.
|
|
func NewNetBuffer(capacity int) *NetBuffer {
|
|
if capacity <= 0 {
|
|
panic("capacity must be greater than 0")
|
|
}
|
|
return &NetBuffer{
|
|
capacity: capacity,
|
|
buf: sync.Pool{
|
|
New: func() interface{} {
|
|
b := make([]byte, capacity)
|
|
return &b
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// TunnelDevice abstracts a TUN device so that we can use the same tunnel-maintenance code
|
|
// regardless of the underlying implementation.
|
|
type TunnelDevice interface {
|
|
// ReadPacket reads a packet from the device (using the given mtu) and returns its contents.
|
|
ReadPacket(buf []byte) (int, error)
|
|
// WritePacket writes a packet to the device.
|
|
WritePacket(pkt []byte) error
|
|
}
|
|
|
|
// NetstackAdapter wraps a tun.Device (e.g. from netstack) to satisfy TunnelDevice.
|
|
type NetstackAdapter struct {
|
|
dev tun.Device
|
|
tunnelBufPool sync.Pool
|
|
tunnelSizesPool sync.Pool
|
|
}
|
|
|
|
func (n *NetstackAdapter) ReadPacket(buf []byte) (int, error) {
|
|
packetBufsPtr := n.tunnelBufPool.Get().(*[][]byte)
|
|
sizesPtr := n.tunnelSizesPool.Get().(*[]int)
|
|
|
|
defer func() {
|
|
(*packetBufsPtr)[0] = nil
|
|
n.tunnelBufPool.Put(packetBufsPtr)
|
|
n.tunnelSizesPool.Put(sizesPtr)
|
|
}()
|
|
|
|
(*packetBufsPtr)[0] = buf
|
|
(*sizesPtr)[0] = 0
|
|
|
|
_, err := n.dev.Read(*packetBufsPtr, *sizesPtr, 0)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return (*sizesPtr)[0], nil
|
|
}
|
|
|
|
func (n *NetstackAdapter) WritePacket(pkt []byte) error {
|
|
// Write expects a slice of packet buffers.
|
|
_, err := n.dev.Write([][]byte{pkt}, 0)
|
|
return err
|
|
}
|
|
|
|
// NewNetstackAdapter creates a new NetstackAdapter.
|
|
func NewNetstackAdapter(dev tun.Device) TunnelDevice {
|
|
return &NetstackAdapter{
|
|
dev: dev,
|
|
tunnelBufPool: sync.Pool{
|
|
New: func() interface{} {
|
|
buf := make([][]byte, 1)
|
|
return &buf
|
|
},
|
|
},
|
|
tunnelSizesPool: sync.Pool{
|
|
New: func() interface{} {
|
|
sizes := make([]int, 1)
|
|
return &sizes
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// WaterAdapter wraps a *water.Interface so it satisfies TunnelDevice.
|
|
type WaterAdapter struct {
|
|
iface *water.Interface
|
|
}
|
|
|
|
func (w *WaterAdapter) ReadPacket(buf []byte) (int, error) {
|
|
n, err := w.iface.Read(buf)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return n, nil
|
|
}
|
|
|
|
func (w *WaterAdapter) WritePacket(pkt []byte) error {
|
|
_, err := w.iface.Write(pkt)
|
|
return err
|
|
}
|
|
|
|
// NewWaterAdapter creates a new WaterAdapter.
|
|
func NewWaterAdapter(iface *water.Interface) TunnelDevice {
|
|
return &WaterAdapter{iface: iface}
|
|
}
|
|
|
|
// GetRelayConnFunc is a function type that returns a pre-allocated packet connection
|
|
// for use as a TURN relay (e.g. from VK TURN). If nil is provided, a direct UDP
|
|
// connection to the MASQUE endpoint will be created.
|
|
type GetRelayConnFunc func(ctx context.Context) (net.PacketConn, error)
|
|
|
|
// MaintainTunnel continuously connects to the MASQUE server, then starts two
|
|
// forwarding goroutines: one forwarding from the device to the IP connection (and handling
|
|
// any ICMP reply), and the other forwarding from the IP connection to the device.
|
|
// If an error occurs in either loop, the connection is closed and a reconnect is attempted.
|
|
//
|
|
// Parameters:
|
|
// - ctx: context.Context - The context for the connection.
|
|
// - tlsConfig: *tls.Config - The TLS configuration for secure communication.
|
|
// - keepalivePeriod: time.Duration - The keepalive period for the QUIC connection.
|
|
// - initialPacketSize: uint16 - The initial packet size for the QUIC connection.
|
|
// - endpoint: *net.UDPAddr - The UDP address of the MASQUE server.
|
|
// - device: TunnelDevice - The TUN device to forward packets to and from.
|
|
// - mtu: int - The MTU of the TUN device.
|
|
// - reconnectDelay: time.Duration - The delay between reconnect attempts.
|
|
// - getRelayConn: GetRelayConnFunc - Optional function to obtain a TURN relay connection.
|
|
// If nil, a direct UDP connection to the endpoint is used.
|
|
// - onReady: func(bool) - Optional callback fired with true when connected, and false when disconnected.
|
|
func MaintainTunnel(ctx context.Context, tlsConfig *tls.Config, keepalivePeriod time.Duration, initialPacketSize uint16, endpoint *net.UDPAddr, device TunnelDevice, mtu int, reconnectDelay time.Duration, getRelayConn GetRelayConnFunc, onReady func(bool)) {
|
|
packetBufferPool := NewNetBuffer(mtu)
|
|
for {
|
|
// Check if context is done before attempting connection
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
}
|
|
|
|
log.Printf("Establishing MASQUE connection to %s:%d", endpoint.IP, endpoint.Port)
|
|
|
|
// Optionally obtain a TURN relay packet connection
|
|
var baseConn net.PacketConn
|
|
if getRelayConn != nil {
|
|
var err error
|
|
baseConn, err = getRelayConn(ctx)
|
|
if err != nil {
|
|
log.Printf("Failed to obtain TURN relay connection: %v", err)
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(reconnectDelay):
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
|
|
udpConn, tr, ipConn, rsp, err := ConnectTunnel(
|
|
ctx,
|
|
tlsConfig,
|
|
internal.DefaultQuicConfig(keepalivePeriod, initialPacketSize),
|
|
internal.ConnectURI,
|
|
endpoint,
|
|
baseConn,
|
|
)
|
|
if err != nil {
|
|
log.Printf("Failed to connect tunnel: %v", err)
|
|
if udpConn != nil {
|
|
udpConn.Close()
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(reconnectDelay):
|
|
}
|
|
continue
|
|
}
|
|
if rsp.StatusCode != 200 {
|
|
log.Printf("Tunnel connection failed: %s", rsp.Status)
|
|
ipConn.Close()
|
|
if udpConn != nil {
|
|
udpConn.Close()
|
|
}
|
|
if tr != nil {
|
|
tr.Close()
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(reconnectDelay):
|
|
}
|
|
continue
|
|
}
|
|
|
|
log.Println("Connected to MASQUE server")
|
|
if onReady != nil {
|
|
onReady(true)
|
|
}
|
|
errChan := make(chan error, 2)
|
|
|
|
// Packet counters for diagnostics
|
|
var txPkts, rxPkts atomic.Int64
|
|
go func() {
|
|
ticker := time.NewTicker(10 * time.Second)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
if Verbose {
|
|
log.Printf("[Warp] Tunnel stats: TX=%d pkts, RX=%d pkts", txPkts.Load(), rxPkts.Load())
|
|
}
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
for {
|
|
buf := packetBufferPool.Get()
|
|
n, err := device.ReadPacket(buf)
|
|
if err != nil {
|
|
packetBufferPool.Put(buf)
|
|
errChan <- fmt.Errorf("failed to read from TUN device: %v", err)
|
|
return
|
|
}
|
|
txPkts.Add(1)
|
|
|
|
paddedSize := n
|
|
if n < mtu-100 {
|
|
randOffset, _ := rand.Int(rand.Reader, big.NewInt(64))
|
|
paddedSize = n + int(randOffset.Int64())
|
|
if paddedSize > mtu {
|
|
paddedSize = mtu
|
|
}
|
|
if paddedSize > n {
|
|
_, _ = rand.Read(buf[n:paddedSize])
|
|
}
|
|
}
|
|
|
|
icmp, err := ipConn.WritePacket(buf[:paddedSize])
|
|
if err != nil {
|
|
packetBufferPool.Put(buf)
|
|
if errors.As(err, new(*connectip.CloseError)) {
|
|
errChan <- fmt.Errorf("connection closed while writing to IP connection: %v", err)
|
|
return
|
|
}
|
|
log.Printf("Error writing to IP connection: %v, continuing...", err)
|
|
continue
|
|
}
|
|
packetBufferPool.Put(buf)
|
|
|
|
if len(icmp) > 0 {
|
|
if err := device.WritePacket(icmp); err != nil {
|
|
if errors.As(err, new(*connectip.CloseError)) {
|
|
errChan <- fmt.Errorf("connection closed while writing ICMP to TUN device: %v", err)
|
|
return
|
|
}
|
|
log.Printf("Error writing ICMP to TUN device: %v, continuing...", err)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
buf := packetBufferPool.Get()
|
|
defer packetBufferPool.Put(buf)
|
|
for {
|
|
n, err := ipConn.ReadPacket(buf, true)
|
|
if err != nil {
|
|
if errors.As(err, new(*connectip.CloseError)) {
|
|
errChan <- fmt.Errorf("connection closed while reading from IP connection: %v", err)
|
|
return
|
|
}
|
|
log.Printf("Error reading from IP connection: %v, continuing...", err)
|
|
continue
|
|
}
|
|
rxPkts.Add(1)
|
|
if err := device.WritePacket(buf[:n]); err != nil {
|
|
errChan <- fmt.Errorf("failed to write to TUN device: %v", err)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
err = <-errChan
|
|
if onReady != nil {
|
|
onReady(false)
|
|
}
|
|
log.Printf("Tunnel connection lost: %v. Reconnecting...", err)
|
|
ipConn.Close()
|
|
if udpConn != nil {
|
|
udpConn.Close()
|
|
}
|
|
if tr != nil {
|
|
tr.Close()
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(reconnectDelay):
|
|
}
|
|
}
|
|
}
|
|
|