mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-05 09:57:14 +00:00
58671533bb
New internal/amneziawgnet package: builds a real amneziawg-go Device over a gVisor netstack from an existing amneziawg.Instance, with a TCP/UDP forwarder that recovers each tunnel connection's real destination and a peer-identity index keyed by AllowedIPs. This is the foundation for migrating AmneziaWG off the kernel-module+TPROXY path (see the AmneziaWG-go vs kernel-module decision) -- nothing wires into live traffic yet, that's Phase 2 (relay into Xray's own SOCKS5 inbound). Covered by three real end-to-end tests: a genuine handshake + TCP forwarder + identity resolution, the same for UDP (including a reply routed back through the tunnel), and the manager's reconfigure-in-place vs. rebuild lifecycle. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
195 lines
6.5 KiB
Go
195 lines
6.5 KiB
Go
// Package amneziawgnet embeds amneziawg-go (a userspace AmneziaWG
|
|
// implementation, https://github.com/amnezia-vpn/amneziawg-go) directly in
|
|
// the panel process, as an alternative to internal/amneziawg's
|
|
// kernel-module (DKMS) + awg-quick approach. A gVisor userspace network
|
|
// stack (gvisor.dev/gvisor/pkg/tcpip -- already an indirect dependency via
|
|
// xray-core's own proxy/wireguard support) terminates each tunnel, and a
|
|
// forwarder recovers each connection's real, dynamically-arbitrary
|
|
// destination for the caller to relay onward (see Phase 2 of the migration
|
|
// plan: a loopback SOCKS5 dial into Xray, giving native stats/routing/
|
|
// sniffing for free).
|
|
package amneziawgnet
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
"os"
|
|
"syscall"
|
|
|
|
awgtun "github.com/amnezia-vpn/amneziawg-go/v3/tun"
|
|
|
|
"gvisor.dev/gvisor/pkg/buffer"
|
|
"gvisor.dev/gvisor/pkg/tcpip"
|
|
"gvisor.dev/gvisor/pkg/tcpip/header"
|
|
"gvisor.dev/gvisor/pkg/tcpip/link/channel"
|
|
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
|
|
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
|
|
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
|
"gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
|
|
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
|
|
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
|
)
|
|
|
|
// stackTun implements amneziawg-go's tun.Device directly against a gVisor
|
|
// channel endpoint, the same approach amneziawg-go's own tun/netstack
|
|
// package and xray-core's proxy/wireguard/netstack.go both take. Neither of
|
|
// those exposes the raw *stack.Stack a forwarder needs (amneziawg-go's Net
|
|
// type keeps it unexported), so this is a local, from-source reimplementation
|
|
// rather than a wrapper -- adapted from amneziawg-go v3.0.3's
|
|
// tun/netstack/tun.go (MIT licensed), trimmed to the constructor this
|
|
// package needs.
|
|
type stackTun struct {
|
|
ep *channel.Endpoint
|
|
stack *stack.Stack
|
|
events chan awgtun.Event
|
|
notifyHandle *channel.NotificationHandle
|
|
incomingPacket chan *buffer.View
|
|
mtu int
|
|
}
|
|
|
|
// createNetTUNWithStack builds a gVisor-backed tun.Device for the given
|
|
// local addresses (interface address(es), one per family) and returns the
|
|
// underlying *stack.Stack alongside it so a caller can attach a forwarder
|
|
// (see forwarder.go / udp.go).
|
|
func createNetTUNWithStack(localAddresses []netip.Addr, mtu int) (awgtun.Device, *stack.Stack, error) {
|
|
opts := stack.Options{
|
|
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol},
|
|
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol6, icmp.NewProtocol4},
|
|
// HandleLocal must stay false: promiscuous+spoofing mode (see
|
|
// forwarder.go) is what lets a destination other than the stack's
|
|
// own configured address reach the forwarder at all.
|
|
HandleLocal: false,
|
|
}
|
|
dev := &stackTun{
|
|
ep: channel.New(1024, uint32(mtu), ""),
|
|
stack: stack.New(opts),
|
|
events: make(chan awgtun.Event, 10),
|
|
incomingPacket: make(chan *buffer.View),
|
|
mtu: mtu,
|
|
}
|
|
sackEnabledOpt := tcpip.TCPSACKEnabled(true)
|
|
if err := dev.stack.SetTransportProtocolOption(tcp.ProtocolNumber, &sackEnabledOpt); err != nil {
|
|
return nil, nil, fmt.Errorf("amneziawgnet: enable TCP SACK: %s", err)
|
|
}
|
|
dev.notifyHandle = dev.ep.AddNotify(dev)
|
|
if err := dev.stack.CreateNIC(1, dev.ep); err != nil {
|
|
return nil, nil, fmt.Errorf("amneziawgnet: CreateNIC: %s", err)
|
|
}
|
|
|
|
var hasV4, hasV6 bool
|
|
for _, ip := range localAddresses {
|
|
var protoNumber tcpip.NetworkProtocolNumber
|
|
switch {
|
|
case ip.Is4():
|
|
protoNumber = ipv4.ProtocolNumber
|
|
hasV4 = true
|
|
case ip.Is6():
|
|
protoNumber = ipv6.ProtocolNumber
|
|
hasV6 = true
|
|
default:
|
|
continue
|
|
}
|
|
protoAddr := tcpip.ProtocolAddress{
|
|
Protocol: protoNumber,
|
|
AddressWithPrefix: tcpip.AddrFromSlice(ip.AsSlice()).WithPrefix(),
|
|
}
|
|
if err := dev.stack.AddProtocolAddress(1, protoAddr, stack.AddressProperties{}); err != nil {
|
|
return nil, nil, fmt.Errorf("amneziawgnet: AddProtocolAddress(%v): %s", ip, err)
|
|
}
|
|
}
|
|
if hasV4 {
|
|
dev.stack.AddRoute(tcpip.Route{Destination: header.IPv4EmptySubnet, NIC: 1})
|
|
}
|
|
if hasV6 {
|
|
dev.stack.AddRoute(tcpip.Route{Destination: header.IPv6EmptySubnet, NIC: 1})
|
|
}
|
|
dev.events <- awgtun.EventUp
|
|
return dev, dev.stack, nil
|
|
}
|
|
|
|
func (t *stackTun) Name() (string, error) { return "amneziawgnet", nil }
|
|
func (t *stackTun) File() *os.File { return nil }
|
|
func (t *stackTun) Events() <-chan awgtun.Event { return t.events }
|
|
func (t *stackTun) MTU() (int, error) { return t.mtu, nil }
|
|
func (t *stackTun) BatchSize() int { return 1 }
|
|
|
|
func (t *stackTun) Read(buf [][]byte, sizes []int, offset int) (int, error) {
|
|
view, ok := <-t.incomingPacket
|
|
if !ok {
|
|
return 0, os.ErrClosed
|
|
}
|
|
n, err := view.Read(buf[0][offset:])
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
sizes[0] = n
|
|
return 1, nil
|
|
}
|
|
|
|
func (t *stackTun) Write(buf [][]byte, offset int) (int, error) {
|
|
for _, b := range buf {
|
|
packet := b[offset:]
|
|
if len(packet) == 0 {
|
|
continue
|
|
}
|
|
pkb := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: buffer.MakeWithData(packet)})
|
|
switch packet[0] >> 4 {
|
|
case 4:
|
|
t.ep.InjectInbound(header.IPv4ProtocolNumber, pkb)
|
|
case 6:
|
|
t.ep.InjectInbound(header.IPv6ProtocolNumber, pkb)
|
|
default:
|
|
return 0, syscall.EAFNOSUPPORT
|
|
}
|
|
}
|
|
return len(buf), nil
|
|
}
|
|
|
|
func (t *stackTun) WriteNotify() {
|
|
pkt := t.ep.Read()
|
|
if pkt == nil {
|
|
return
|
|
}
|
|
view := pkt.ToView()
|
|
pkt.DecRef()
|
|
t.incomingPacket <- view
|
|
}
|
|
|
|
func (t *stackTun) Close() error {
|
|
t.stack.RemoveNIC(1)
|
|
t.stack.Close()
|
|
t.ep.RemoveNotify(t.notifyHandle)
|
|
t.ep.Close()
|
|
if t.events != nil {
|
|
close(t.events)
|
|
}
|
|
if t.incomingPacket != nil {
|
|
close(t.incomingPacket)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// enablePromiscuousRouting puts the NIC into promiscuous + spoofing mode,
|
|
// the precondition both AttachTCPForwarder and AttachUDPHandler need to see
|
|
// packets addressed to a destination other than the stack's own configured
|
|
// local address. Safe to call from both (and more than once): gVisor's
|
|
// SetPromiscuousMode/SetSpoofing just set a bool on the NIC, not something
|
|
// that accumulates or needs undoing between calls.
|
|
func enablePromiscuousRouting(gstack *stack.Stack) {
|
|
gstack.SetPromiscuousMode(1, true)
|
|
gstack.SetSpoofing(1, true)
|
|
}
|
|
|
|
// addrFromTcpip converts a gVisor tcpip.Address (4 or 16 raw bytes) to the
|
|
// stdlib netip.Addr type the rest of this package and its callers use.
|
|
func addrFromTcpip(a tcpip.Address) netip.Addr {
|
|
if a.Len() == 4 {
|
|
var b [4]byte
|
|
copy(b[:], a.AsSlice())
|
|
return netip.AddrFrom4(b)
|
|
}
|
|
var b [16]byte
|
|
copy(b[:], a.AsSlice())
|
|
return netip.AddrFrom16(b)
|
|
}
|