mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-08 03:07:15 +00:00
24cb6bfe1f
Every packet crossing the embedded AmneziaWG interface allocated instead of reusing gVisor's pools, in both directions. stackTun.Write injected each decrypted packet and never called DecRef, so the packet buffer and its chunk were never returned; stackTun.Read copied each view out and never released it. gVisor's own link endpoints settle the ownership question -- loopback.go and sharedmem.go both DecRef immediately after DeliverNetworkPacket, because the injector owns the buffer. AttachUDPHandler compounded it by cloning a packet buffer it then dropped on the floor, on top of a Data().AsRange().ToSlice() that already returns an owned copy, so the clone bought nothing and stranded a pooled buffer plus a cloned view per datagram. Measured with the benchmarks added here: stackTunWrite (upload) 794ns -> 107ns 4 -> 0 allocs stackTunRead (download) 707ns -> 129ns 3 -> 0 allocs UDP datagram, end to end 2.69us -> 1.58us 8 -> 2 allocs The remaining UDP allocation is the ToSlice copy itself. Through a real handshaked tunnel -- both devices in one process over loopback, so ChaCha20-Poly1305 and the UDP syscalls dominate -- it is worth -48% bytes/op and -33% allocs/op, and about +4.8% throughput in each direction (n=18, p<=0.01). On a small VPS, where the allocation pressure is not spread over 24 idle cores, the throughput share should be larger; that part is reasoning, not something measured here. The three regression tests assert allocations per packet rather than timing, since the defect is the pool miss, not the nanoseconds. Thresholds leave room for the extra allocation -race adds.
103 lines
4.0 KiB
Go
103 lines
4.0 KiB
Go
package amneziawgnet
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
|
|
"gvisor.dev/gvisor/pkg/buffer"
|
|
"gvisor.dev/gvisor/pkg/tcpip"
|
|
"gvisor.dev/gvisor/pkg/tcpip/checksum"
|
|
"gvisor.dev/gvisor/pkg/tcpip/header"
|
|
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
|
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
|
)
|
|
|
|
// UDPHandler is called for every UDP packet a tunnel client sends, with its
|
|
// source (the peer's tunnel-internal address) and its real,
|
|
// dynamically-arbitrary destination -- recovered the same way the TCP
|
|
// forwarder recovers its destination, from the packet's own transport
|
|
// endpoint ID, never from a preconfigured table. The handler owns all flow
|
|
// tracking and reply delivery (via WriteUDPReply): gVisor has no
|
|
// udp.NewForwarder the way it does for TCP, so unlike AttachTCPForwarder
|
|
// this can't just hand back a ready net.Conn.
|
|
type UDPHandler func(src, dst netip.AddrPort, payload []byte)
|
|
|
|
// AttachUDPHandler attaches a raw UDP handler to gstack, independently
|
|
// enabling the same promiscuous+spoofing mode AttachTCPForwarder needs --
|
|
// safe and idempotent to call regardless of whether AttachTCPForwarder was
|
|
// attached to the same stack first, or at all. Adapted from xtls/xray-core's
|
|
// proxy/wireguard/tun.go UDP path (MIT), which hand-tracks flows for the
|
|
// identical reason: gVisor doesn't provide a UDP forwarder.
|
|
func AttachUDPHandler(gstack *stack.Stack, handler UDPHandler) {
|
|
enablePromiscuousRouting(gstack)
|
|
|
|
gstack.SetTransportProtocolHandler(udp.ProtocolNumber, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
|
|
// ToSlice already returns an owned copy, so cloning pkt here would only
|
|
// strand a pooled packet buffer and its chunks on every datagram.
|
|
data := pkt.Data().AsRange().ToSlice()
|
|
src := netip.AddrPortFrom(addrFromTcpip(id.RemoteAddress), id.RemotePort)
|
|
dst := netip.AddrPortFrom(addrFromTcpip(id.LocalAddress), id.LocalPort)
|
|
handler(src, dst, data)
|
|
return true
|
|
})
|
|
}
|
|
|
|
// WriteUDPReply injects a UDP packet into gstack as if it arrived from
|
|
// `from` addressed to `to` -- i.e. a reply travelling back into the tunnel
|
|
// toward the client -- constructed by hand since gVisor exposes no
|
|
// connected-socket-style Write for an address the stack doesn't itself own.
|
|
func WriteUDPReply(gstack *stack.Stack, from, to netip.AddrPort, payload []byte) error {
|
|
udpLen := header.UDPMinimumSize + len(payload)
|
|
srcIP := tcpip.AddrFromSlice(from.Addr().AsSlice())
|
|
dstIP := tcpip.AddrFromSlice(to.Addr().AsSlice())
|
|
|
|
isIPv4 := from.Addr().Is4()
|
|
ipHdrSize := header.IPv6MinimumSize
|
|
ipProtocol := header.IPv6ProtocolNumber
|
|
if isIPv4 {
|
|
ipHdrSize = header.IPv4MinimumSize
|
|
ipProtocol = header.IPv4ProtocolNumber
|
|
}
|
|
|
|
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
|
|
ReserveHeaderBytes: ipHdrSize + header.UDPMinimumSize,
|
|
Payload: buffer.MakeWithData(payload),
|
|
})
|
|
defer pkt.DecRef()
|
|
|
|
udpHdr := header.UDP(pkt.TransportHeader().Push(header.UDPMinimumSize))
|
|
udpHdr.Encode(&header.UDPFields{
|
|
SrcPort: from.Port(),
|
|
DstPort: to.Port(),
|
|
Length: uint16(udpLen),
|
|
})
|
|
xsum := header.PseudoHeaderChecksum(header.UDPProtocolNumber, srcIP, dstIP, uint16(udpLen))
|
|
udpHdr.SetChecksum(^udpHdr.CalculateChecksum(checksum.Checksum(payload, xsum)))
|
|
|
|
if isIPv4 {
|
|
ipHdr := header.IPv4(pkt.NetworkHeader().Push(header.IPv4MinimumSize))
|
|
ipHdr.Encode(&header.IPv4Fields{
|
|
TotalLength: uint16(header.IPv4MinimumSize + udpLen),
|
|
TTL: 64,
|
|
Protocol: uint8(header.UDPProtocolNumber),
|
|
SrcAddr: srcIP,
|
|
DstAddr: dstIP,
|
|
})
|
|
ipHdr.SetChecksum(^ipHdr.CalculateChecksum())
|
|
} else {
|
|
ipHdr := header.IPv6(pkt.NetworkHeader().Push(header.IPv6MinimumSize))
|
|
ipHdr.Encode(&header.IPv6Fields{
|
|
PayloadLength: uint16(udpLen),
|
|
TransportProtocol: header.UDPProtocolNumber,
|
|
HopLimit: 64,
|
|
SrcAddr: srcIP,
|
|
DstAddr: dstIP,
|
|
})
|
|
}
|
|
|
|
if tcpipErr := gstack.WriteRawPacket(1, ipProtocol, buffer.MakeWithView(pkt.ToView())); tcpipErr != nil {
|
|
return fmt.Errorf("amneziawgnet: WriteRawPacket: %s", tcpipErr)
|
|
}
|
|
return nil
|
|
}
|