package service import ( "net/netip" "strconv" "strings" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/util/common" wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard" ) const defaultWireguardBase = "10.0.0.0/24" func keepAliveStr(seconds int) string { if seconds <= 0 { return "" } return strconv.Itoa(seconds) } func wireguardHostAddr(s string) netip.Addr { s = strings.TrimSpace(s) if s == "" { return netip.Addr{} } if p, err := netip.ParsePrefix(s); err == nil { return p.Addr() } if a, err := netip.ParseAddr(s); err == nil { return a } return netip.Addr{} } func wireguardAllocationBase(used []string, fallback string) string { for _, u := range used { a := wireguardHostAddr(u) if !a.IsValid() || !a.Is4() || a.IsUnspecified() { continue } if p, err := a.Prefix(24); err == nil { return p.String() } } return fallback } // allocateWireguardAddress returns the first free /32 host address in base that // is not already present in used. The server holds the first host (.1), so // allocation starts at the second host (.2). func allocateWireguardAddress(used []string, base string) (string, error) { if base == "" { base = defaultWireguardBase } prefix, err := netip.ParsePrefix(base) if err != nil { return "", err } taken := make(map[netip.Addr]struct{}, len(used)) for _, u := range used { if a := wireguardHostAddr(u); a.IsValid() { taken[a] = struct{}{} } } addr := prefix.Masked().Addr().Next().Next() for prefix.Contains(addr) { if _, ok := taken[addr]; !ok { return addr.String() + "/32", nil } addr = addr.Next() } return "", common.NewError("wireguard: no free address available in", base) } // defaultWireguardClients fills in blank WireGuard credentials for newly added // clients: a generated keypair when none was provided, a derived public key when // only a private key was given, and a unique tunnel address allocated from the // inbound's subnet. It mutates both the typed clients and the parallel raw client // maps that get persisted into the inbound settings. Existing values are never // overwritten, so editing a client never rotates its keys. func defaultWireguardClients(existing, clients []model.Client, interfaceClients []any) error { used := make([]string, 0) for i := range existing { used = append(used, existing[i].AllowedIPs...) } base := wireguardAllocationBase(used, defaultWireguardBase) for i := range clients { c := &clients[i] if c.PrivateKey == "" && c.PublicKey == "" { priv, pub, err := wgutil.GenerateWireguardKeypair() if err != nil { return err } c.PrivateKey = priv c.PublicKey = pub } else if c.PublicKey == "" && c.PrivateKey != "" { pub, err := wgutil.PublicKeyFromPrivate(c.PrivateKey) if err != nil { return err } c.PublicKey = pub } if len(c.AllowedIPs) == 0 { addr, err := allocateWireguardAddress(used, base) if err != nil { return err } c.AllowedIPs = []string{addr} } used = append(used, c.AllowedIPs...) if i < len(interfaceClients) { if m, ok := interfaceClients[i].(map[string]any); ok { m["privateKey"] = c.PrivateKey m["publicKey"] = c.PublicKey m["allowedIPs"] = c.AllowedIPs if c.PreSharedKey != "" { m["preSharedKey"] = c.PreSharedKey } if c.KeepAlive > 0 { m["keepAlive"] = c.KeepAlive } interfaceClients[i] = m } } } return nil }