mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-05 01:47:15 +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>
186 lines
6.7 KiB
Go
186 lines
6.7 KiB
Go
package amneziawgnet
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
"strings"
|
|
|
|
awgconn "github.com/amnezia-vpn/amneziawg-go/v3/conn"
|
|
"github.com/amnezia-vpn/amneziawg-go/v3/device"
|
|
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
|
|
)
|
|
|
|
// defaultMTU matches internal/amneziawg's own kernel-module interface
|
|
// default -- 1420, WireGuard/AmneziaWG's usual accounting for tunnel
|
|
// encapsulation overhead on a standard 1500-byte-MTU host link.
|
|
const defaultMTU = 1420
|
|
|
|
// DeviceOptions carries the AmneziaWG 3.0 header-protection fields, kept out
|
|
// of amneziawg.Instance/Obfuscation20 deliberately: those are the shared,
|
|
// DB-backed types the still-live kernel-module path also reads and writes,
|
|
// and 3.0 header protection is a device-wide, strictly opt-in setting (see
|
|
// the migration plan's "Reference material" section) that isn't wired into
|
|
// that shared schema yet. Zero-value DeviceOptions means classic
|
|
// (non-3.0) obfuscation only, matching the kernel-module path's own
|
|
// defaults today.
|
|
type DeviceOptions struct {
|
|
// HeaderProtectionKey is a base64 32-byte key. Empty disables AWG 3.0
|
|
// header protection entirely. Non-empty requires every one of
|
|
// Obfuscation20.S1-S4 to be >= 12 (amneziawg-go's own HeaderCipherNonceSize
|
|
// requirement) -- IpcSet will reject the config otherwise.
|
|
HeaderProtectionKey string
|
|
// ContentPaddingAddition is a "low-high" range (or a bare integer) per
|
|
// amneziawg-tools' own u16_range_from_string grammar. Empty disables it.
|
|
ContentPaddingAddition string
|
|
// Logger is passed to device.NewDevice as-is; nil uses a silent logger
|
|
// (device.NewLogger(device.LogLevelSilent, "")).
|
|
Logger *device.Logger
|
|
}
|
|
|
|
// Device is one running embedded AmneziaWG interface: an amneziawg-go
|
|
// Device over a gVisor netstack, plus the raw *stack.Stack a caller needs to
|
|
// attach a TCP/UDP forwarder (see forwarder.go / udp.go). Closing it tears
|
|
// down both the WireGuard device and the underlying tun/stack.
|
|
type Device struct {
|
|
*device.Device
|
|
Stack *stack.Stack
|
|
}
|
|
|
|
// NewDevice constructs and brings up an embedded AmneziaWG interface for
|
|
// inst: a gVisor-backed tun.Device sized to inst.MTU (or defaultMTU),
|
|
// addressed with inst.Address, configured via UAPI with inst.Obfuscation,
|
|
// inst.PrivateKey, opts' AWG 3.0 fields, and one UAPI peer per inst.Peers
|
|
// entry. It does not attach a forwarder or start relaying traffic --
|
|
// that's the caller's job (see AttachTCPForwarder / AttachUDPHandler),
|
|
// keeping this constructor usable both for a real relay and for a plain
|
|
// mechanical test.
|
|
func NewDevice(inst amneziawg.Instance, opts DeviceOptions) (*Device, error) {
|
|
addrs, err := hostAddresses(inst.Address)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("amneziawgnet: %w", err)
|
|
}
|
|
|
|
mtu := inst.MTU
|
|
if mtu <= 0 {
|
|
mtu = defaultMTU
|
|
}
|
|
|
|
tun, gstack, err := createNetTUNWithStack(addrs, mtu)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("amneziawgnet: create netstack: %w", err)
|
|
}
|
|
|
|
logger := opts.Logger
|
|
if logger == nil {
|
|
logger = device.NewLogger(device.LogLevelSilent, "")
|
|
}
|
|
dev := device.NewDevice(tun, awgconn.NewDefaultBind(), logger)
|
|
|
|
conf, err := buildUAPIConfig(inst, opts)
|
|
if err != nil {
|
|
dev.Close()
|
|
return nil, fmt.Errorf("amneziawgnet: %w", err)
|
|
}
|
|
if err := dev.IpcSet(conf); err != nil {
|
|
dev.Close()
|
|
return nil, fmt.Errorf("amneziawgnet: IpcSet for inbound %d: %w", inst.Id, err)
|
|
}
|
|
if err := dev.Up(); err != nil {
|
|
dev.Close()
|
|
return nil, fmt.Errorf("amneziawgnet: bring up inbound %d: %w", inst.Id, err)
|
|
}
|
|
|
|
return &Device{Device: dev, Stack: gstack}, nil
|
|
}
|
|
|
|
// hostAddresses parses each of inst.Address's CIDR strings (e.g.
|
|
// "10.8.1.1/24") down to the bare host address the netstack's NIC gets
|
|
// configured with -- the interface's own address, not the subnet it routes.
|
|
func hostAddresses(addresses []string) ([]netip.Addr, error) {
|
|
out := make([]netip.Addr, 0, len(addresses))
|
|
for _, a := range addresses {
|
|
prefix, err := netip.ParsePrefix(a)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid interface address %q: %w", a, err)
|
|
}
|
|
out = append(out, prefix.Addr())
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// buildUAPIConfig renders inst (plus opts' AWG 3.0 fields) as a WireGuard
|
|
// UAPI "set" configuration string -- private_key/listen_port/jc.../s1-s4/
|
|
// h1-h4/i1 device lines, the AWG 3.0 device lines when opts asks for them,
|
|
// then one public_key/preshared_key/allowed_ip block per peer. Field names
|
|
// and format match amneziawg-go v3.0.3's device/uapi.go exactly (confirmed
|
|
// against its real source during Phase 0 spiking, not just its docs).
|
|
func buildUAPIConfig(inst amneziawg.Instance, opts DeviceOptions) (string, error) {
|
|
var b strings.Builder
|
|
|
|
privHex, err := wireguard.KeyToHex(inst.PrivateKey)
|
|
if err != nil {
|
|
return "", fmt.Errorf("invalid server private key: %w", err)
|
|
}
|
|
fmt.Fprintf(&b, "private_key=%s\n", privHex)
|
|
fmt.Fprintf(&b, "listen_port=%d\n", inst.ListenPort)
|
|
// replace_peers makes every apply a full resync (matches this package's
|
|
// own Manager.Ensure semantics): peers no longer in inst.Peers are
|
|
// dropped instead of lingering from a previous IpcSet call.
|
|
b.WriteString("replace_peers=true\n")
|
|
|
|
o := inst.Obfuscation
|
|
fmt.Fprintf(&b, "jc=%d\njmin=%d\njmax=%d\n", o.Jc, o.Jmin, o.Jmax)
|
|
fmt.Fprintf(&b, "s1=%d\ns2=%d\ns3=%d\ns4=%d\n", o.S1, o.S2, o.S3, o.S4)
|
|
writeHLine(&b, "h1", o.H1)
|
|
writeHLine(&b, "h2", o.H2)
|
|
writeHLine(&b, "h3", o.H3)
|
|
writeHLine(&b, "h4", o.H4)
|
|
if o.I1 != "" {
|
|
fmt.Fprintf(&b, "i1=%s\n", o.I1)
|
|
}
|
|
|
|
if opts.HeaderProtectionKey != "" {
|
|
hpHex, err := wireguard.KeyToHex(opts.HeaderProtectionKey)
|
|
if err != nil {
|
|
return "", fmt.Errorf("invalid header protection key: %w", err)
|
|
}
|
|
fmt.Fprintf(&b, "header_protection_key=%s\n", hpHex)
|
|
}
|
|
if opts.ContentPaddingAddition != "" {
|
|
fmt.Fprintf(&b, "content_padding_addition=%s\n", opts.ContentPaddingAddition)
|
|
}
|
|
|
|
for _, p := range inst.Peers {
|
|
pubHex, err := wireguard.KeyToHex(p.PublicKey)
|
|
if err != nil {
|
|
return "", fmt.Errorf("peer %q: invalid public key: %w", p.Email, err)
|
|
}
|
|
fmt.Fprintf(&b, "public_key=%s\n", pubHex)
|
|
if p.PresharedKey != "" {
|
|
pskHex, err := wireguard.KeyToHex(p.PresharedKey)
|
|
if err != nil {
|
|
return "", fmt.Errorf("peer %q: invalid preshared key: %w", p.Email, err)
|
|
}
|
|
fmt.Fprintf(&b, "preshared_key=%s\n", pskHex)
|
|
}
|
|
for _, allowedIP := range p.AllowedIPs {
|
|
fmt.Fprintf(&b, "allowed_ip=%s\n", allowedIP)
|
|
}
|
|
}
|
|
|
|
return b.String(), nil
|
|
}
|
|
|
|
// writeHLine writes an hN UAPI line only when v is set -- an empty H value
|
|
// means "let amneziawg-go fall back to its own default," mirroring how
|
|
// internal/amneziawg's generateServerConfig treats the same optional field.
|
|
func writeHLine(b *strings.Builder, name, v string) {
|
|
if v == "" {
|
|
return
|
|
}
|
|
fmt.Fprintf(b, "%s=%s\n", name, v)
|
|
}
|