mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
7a41c59494
* fix(amneziawg): honor inbound listen when binding UDP socket AmneziaWG inbounds ignored the listen field and always opened awgconn.NewDefaultBind(), so multi-IP hosts replied from the primary address and handshakes to a secondary IP never completed (#6367). Carry Inbound.Listen on amneziawg.Instance, open a Bind pinned to that address (wildcard when empty/0.0.0.0/::), and include listen in addressFingerprint so edits rebuild the Device. Fixes #6367 * fix(amneziawg): fall back to wildcard when listen is unusable Invalid or non-local listen values no longer hard-fail inbound startup; treat ::0/[::0] as wildcards and normalize listen in the bind fingerprint. * fix(amneziawg): use ListenConfig.ListenPacket for noctx --------- Co-authored-by: mrchatam <mrchatam@users.noreply.github.com> Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com>
141 lines
4.6 KiB
Go
141 lines
4.6 KiB
Go
package amneziawgnet
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/amnezia-vpn/amneziawg-go/v3/device"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
|
|
)
|
|
|
|
// buildClientUAPIConfig renders a client-mode UAPI set string: the device
|
|
// lines of buildUAPIConfig plus per-peer endpoint/keepalive for dialing.
|
|
func buildClientUAPIConfig(inst amneziawg.OutboundInstance, opts DeviceOptions) (string, error) {
|
|
var b strings.Builder
|
|
|
|
privHex, err := wireguard.KeyToHex(inst.PrivateKey)
|
|
if err != nil {
|
|
return "", fmt.Errorf("invalid private key: %w", err)
|
|
}
|
|
fmt.Fprintf(&b, "private_key=%s\n", privHex)
|
|
if inst.ListenPort > 0 {
|
|
fmt.Fprintf(&b, "listen_port=%d\n", inst.ListenPort)
|
|
}
|
|
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)
|
|
writeOptionalLine(&b, "h1", o.H1)
|
|
writeOptionalLine(&b, "h2", o.H2)
|
|
writeOptionalLine(&b, "h3", o.H3)
|
|
writeOptionalLine(&b, "h4", o.H4)
|
|
writeOptionalLine(&b, "i1", o.I1)
|
|
writeOptionalLine(&b, "i2", o.I2)
|
|
writeOptionalLine(&b, "i3", o.I3)
|
|
writeOptionalLine(&b, "i4", o.I4)
|
|
writeOptionalLine(&b, "i5", o.I5)
|
|
|
|
// An omitted line means "unchanged" to amneziawg-go, so a cleared key can
|
|
// only reach a live device as the all-zero one that disables the feature.
|
|
hpHex := strings.Repeat("0", 64)
|
|
if opts.HeaderProtectionKey != "" {
|
|
var err error
|
|
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)
|
|
}
|
|
if opts.RekeyAfterTime != "" {
|
|
fmt.Fprintf(&b, "rekey_after_time=%s\n", opts.RekeyAfterTime)
|
|
}
|
|
if opts.RekeyTimeout != "" {
|
|
fmt.Fprintf(&b, "rekey_timeout=%s\n", opts.RekeyTimeout)
|
|
}
|
|
if opts.RejectAfterTime != "" {
|
|
fmt.Fprintf(&b, "reject_after_time=%s\n", opts.RejectAfterTime)
|
|
}
|
|
if opts.KeepaliveTimeout != "" {
|
|
fmt.Fprintf(&b, "keepalive_timeout=%s\n", opts.KeepaliveTimeout)
|
|
}
|
|
if opts.MaxHandshakeAttempts != "" {
|
|
fmt.Fprintf(&b, "max_handshake_attempts=%s\n", opts.MaxHandshakeAttempts)
|
|
}
|
|
fmt.Fprintf(&b, "random_trailers=%t\n", opts.RandomTrailers)
|
|
fmt.Fprintf(&b, "disable_cookies=%t\n", opts.DisableCookies)
|
|
|
|
for _, p := range inst.Peers {
|
|
pubHex, err := wireguard.KeyToHex(p.PublicKey)
|
|
if err != nil {
|
|
return "", fmt.Errorf("peer %q: invalid public key: %w", p.Endpoint, 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.Endpoint, err)
|
|
}
|
|
fmt.Fprintf(&b, "preshared_key=%s\n", pskHex)
|
|
}
|
|
fmt.Fprintf(&b, "endpoint=%s\n", p.Endpoint)
|
|
if p.KeepAlive > 0 {
|
|
fmt.Fprintf(&b, "persistent_keepalive_interval=%d\n", p.KeepAlive)
|
|
}
|
|
for _, allowedIP := range p.AllowedIPs {
|
|
fmt.Fprintf(&b, "allowed_ip=%s\n", allowedIP)
|
|
}
|
|
}
|
|
|
|
return b.String(), nil
|
|
}
|
|
|
|
// newUnconfiguredClientDevice builds the tun/netstack/device trio for a
|
|
// client-mode instance; same construction rules as newUnconfiguredDevice.
|
|
func newUnconfiguredClientDevice(inst amneziawg.OutboundInstance, opts DeviceOptions) (*Device, error) {
|
|
addrs, err := hostAddresses(inst.Address)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("amneziawgnet: %w", err)
|
|
}
|
|
|
|
mtu := amneziawg.EffectiveMTU(inst.MTU, inst.Obfuscation.S4)
|
|
|
|
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, fmt.Sprintf("(awg-out %s) ", inst.Tag))
|
|
}
|
|
bind := newResolvingBind("")
|
|
dev := device.NewDevice(tun, bind, logger)
|
|
|
|
return &Device{Device: dev, Stack: gstack, localAddrs: addrs}, nil
|
|
}
|
|
|
|
// ConfigureClient applies inst/opts via UAPI and brings the interface up;
|
|
// same single-call contract as Configure.
|
|
func (d *Device) ConfigureClient(inst amneziawg.OutboundInstance, opts DeviceOptions) error {
|
|
conf, err := buildClientUAPIConfig(inst, opts)
|
|
if err != nil {
|
|
d.Close()
|
|
return fmt.Errorf("amneziawgnet: %w", err)
|
|
}
|
|
if err := d.IpcSet(conf); err != nil {
|
|
d.Close()
|
|
return fmt.Errorf("amneziawgnet: IpcSet for outbound %q: %w", inst.Tag, err)
|
|
}
|
|
if err := d.Up(); err != nil {
|
|
d.Close()
|
|
return fmt.Errorf("amneziawgnet: bring up outbound %q: %w", inst.Tag, err)
|
|
}
|
|
return nil
|
|
}
|