mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-28 02:02:08 +03:00
feat(amneziawg): add native AmneziaWG protocol backend
AmneziaWG (WireGuard plus DPI-resistant obfuscation) needs no Docker here — it runs as a genuine kernel interface via awg-quick/awg, managed the same way internal/mtproto manages mtg: one Inbound row is one desired Instance, and a Manager reconciles running interfaces toward the database every 10s (internal/web/job/amneziawg_job.go) plus immediately after a client edit (applyLocalAmneziaWG). Clients reuse model.Client verbatim (the same PrivateKey/PublicKey/ PreSharedKey/AllowedIPs fields WireGuard already uses), so bulk operations, the QR/share-link modal and subscriptions come from the shared inbound infrastructure instead of a parallel implementation. internal/amneziawg owns the obfuscation param generator/validator (ported from coinman-dev/3ax-ui, upgraded to AmneziaWG 2.0's S3/S4 padding and I1 signature packet) and the exec wrapper around awg-quick/awg, with fingerprint-based reconcile (noop / reload-via- syncconf / full restart) mirroring mtproto.Manager so a same-protocol edit doesn't force an unnecessary interface bounce that would drop every peer's connection. Frontend and install.sh's DKMS/awg-tools setup are tracked separately; this is backend-only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
)
|
||||
|
||||
// AmneziaWGJob reconciles the running AmneziaWG interfaces against the
|
||||
// enabled AmneziaWG inbounds in the database, restarts/reloads any that
|
||||
// drifted, and folds the per-peer traffic scraped from `awg show dump` into
|
||||
// the usual client and inbound traffic accounting. Mirrors MtprotoJob.
|
||||
type AmneziaWGJob struct {
|
||||
inboundService service.InboundService
|
||||
}
|
||||
|
||||
// NewAmneziaWGJob creates a new AmneziaWG reconcile/traffic job instance.
|
||||
func NewAmneziaWGJob() *AmneziaWGJob {
|
||||
return new(AmneziaWGJob)
|
||||
}
|
||||
|
||||
// Run reconciles desired AmneziaWG inbounds with running interfaces and
|
||||
// records per-peer traffic deltas and online status.
|
||||
func (j *AmneziaWGJob) Run() {
|
||||
desired, err := j.inboundService.DesiredAmneziaWGInstances()
|
||||
if err != nil {
|
||||
logger.Warning("amneziawg job: get desired instances failed:", err)
|
||||
return
|
||||
}
|
||||
|
||||
activeTags := make([]string, 0, len(desired))
|
||||
for _, inst := range desired {
|
||||
activeTags = append(activeTags, inst.Tag)
|
||||
}
|
||||
|
||||
mgr := amneziawg.GetManager()
|
||||
mgr.Reconcile(desired)
|
||||
|
||||
deltas, onlineEmails := mgr.CollectTraffic()
|
||||
|
||||
clientTraffics := make([]*xray.ClientTraffic, 0, len(deltas))
|
||||
inboundUp := make(map[string]int64)
|
||||
inboundDown := make(map[string]int64)
|
||||
for _, d := range deltas {
|
||||
clientTraffics = append(clientTraffics, &xray.ClientTraffic{
|
||||
Email: d.Email,
|
||||
Up: d.Up,
|
||||
Down: d.Down,
|
||||
})
|
||||
inboundUp[d.Tag] += d.Up
|
||||
inboundDown[d.Tag] += d.Down
|
||||
}
|
||||
|
||||
traffics := make([]*xray.Traffic, 0, len(inboundUp))
|
||||
for tag, up := range inboundUp {
|
||||
traffics = append(traffics, &xray.Traffic{
|
||||
IsInbound: true,
|
||||
Tag: tag,
|
||||
Up: up,
|
||||
Down: inboundDown[tag],
|
||||
})
|
||||
}
|
||||
|
||||
if len(traffics) > 0 || len(clientTraffics) > 0 {
|
||||
if _, _, err := j.inboundService.AddTraffic(traffics, clientTraffics); err != nil {
|
||||
logger.Warning("amneziawg job: add traffic failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
j.inboundService.RefreshLocalOnlineClients(onlineEmails, activeTags)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/mtproto"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
@@ -53,6 +54,13 @@ func (l *Local) AddInbound(_ context.Context, ib *model.Inbound) error {
|
||||
}
|
||||
return mtproto.GetManager().Ensure(inst)
|
||||
}
|
||||
if ib.Protocol == model.AmneziaWG {
|
||||
inst, ok := amneziawg.InstanceFromInbound(ib)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return amneziawg.GetManager().Ensure(inst)
|
||||
}
|
||||
body, err := json.MarshalIndent(ib.GenXrayInboundConfig(), "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -67,6 +75,10 @@ func (l *Local) DelInbound(_ context.Context, ib *model.Inbound) error {
|
||||
mtproto.GetManager().Remove(ib.Id)
|
||||
return nil
|
||||
}
|
||||
if ib.Protocol == model.AmneziaWG {
|
||||
amneziawg.GetManager().Remove(ib.Id)
|
||||
return nil
|
||||
}
|
||||
return l.withAPI(func(api *xray.XrayAPI) error {
|
||||
return api.DelInbound(ib.Tag)
|
||||
})
|
||||
@@ -76,6 +88,9 @@ func (l *Local) UpdateInbound(ctx context.Context, oldIb, newIb *model.Inbound)
|
||||
if oldIb.Protocol == model.MTProto || newIb.Protocol == model.MTProto {
|
||||
return l.updateMtprotoInbound(ctx, oldIb, newIb)
|
||||
}
|
||||
if oldIb.Protocol == model.AmneziaWG || newIb.Protocol == model.AmneziaWG {
|
||||
return l.updateAmneziaWGInbound(ctx, oldIb, newIb)
|
||||
}
|
||||
_ = l.DelInbound(ctx, oldIb)
|
||||
if !newIb.Enable {
|
||||
return nil
|
||||
@@ -112,8 +127,36 @@ func (l *Local) updateMtprotoInbound(ctx context.Context, oldIb, newIb *model.In
|
||||
return mtproto.GetManager().Ensure(inst)
|
||||
}
|
||||
|
||||
// updateAmneziaWGInbound mirrors updateMtprotoInbound: it skips the
|
||||
// Remove+Ensure sequence a plain Del+Add would force so that, on an
|
||||
// AmneziaWG-to-AmneziaWG edit, Manager.Ensure's own fingerprint comparison
|
||||
// can pick a peers-only `syncconf` instead of always bouncing the interface
|
||||
// (see internal/amneziawg.Manager.ensureLocked).
|
||||
func (l *Local) updateAmneziaWGInbound(ctx context.Context, oldIb, newIb *model.Inbound) error {
|
||||
if oldIb.Protocol == model.AmneziaWG && newIb.Protocol != model.AmneziaWG {
|
||||
amneziawg.GetManager().Remove(oldIb.Id)
|
||||
if !newIb.Enable {
|
||||
return nil
|
||||
}
|
||||
return l.AddInbound(ctx, newIb)
|
||||
}
|
||||
if oldIb.Protocol != model.AmneziaWG {
|
||||
_ = l.DelInbound(ctx, oldIb)
|
||||
}
|
||||
if !newIb.Enable {
|
||||
amneziawg.GetManager().Remove(newIb.Id)
|
||||
return nil
|
||||
}
|
||||
inst, ok := amneziawg.InstanceFromInbound(newIb)
|
||||
if !ok {
|
||||
amneziawg.GetManager().Remove(newIb.Id)
|
||||
return nil
|
||||
}
|
||||
return amneziawg.GetManager().Ensure(inst)
|
||||
}
|
||||
|
||||
func (l *Local) AddUser(_ context.Context, ib *model.Inbound, userMap map[string]any) error {
|
||||
if ib.Protocol == model.MTProto {
|
||||
if ib.Protocol == model.MTProto || ib.Protocol == model.AmneziaWG {
|
||||
return nil
|
||||
}
|
||||
return l.withAPI(func(api *xray.XrayAPI) error {
|
||||
@@ -122,7 +165,7 @@ func (l *Local) AddUser(_ context.Context, ib *model.Inbound, userMap map[string
|
||||
}
|
||||
|
||||
func (l *Local) RemoveUser(_ context.Context, ib *model.Inbound, email string) error {
|
||||
if ib.Protocol == model.MTProto {
|
||||
if ib.Protocol == model.MTProto || ib.Protocol == model.AmneziaWG {
|
||||
return nil
|
||||
}
|
||||
return l.withAPI(func(api *xray.XrayAPI) error {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
||||
"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"
|
||||
)
|
||||
|
||||
// defaultAmneziaWGSubnetBase resolves the /CIDR base new peer addresses are
|
||||
// allocated from, out of the inbound's own configured server subnet — unlike
|
||||
// WireGuard, which always falls back to a fixed 10.0.0.0/24.
|
||||
func defaultAmneziaWGSubnetBase(settingsJSON string) (string, error) {
|
||||
var parsed amneziawg.InboundSettings
|
||||
if err := json.Unmarshal([]byte(settingsJSON), &parsed); err != nil {
|
||||
return "", fmt.Errorf("amneziawg: invalid settings: %w", err)
|
||||
}
|
||||
if parsed.Server == nil {
|
||||
return "", fmt.Errorf("amneziawg: settings missing server block")
|
||||
}
|
||||
cidr := parsed.Server.SubnetCIDR
|
||||
if cidr <= 0 {
|
||||
cidr = 24
|
||||
}
|
||||
return fmt.Sprintf("%s/%d", parsed.Server.SubnetIP, cidr), nil
|
||||
}
|
||||
|
||||
// defaultAmneziaWGClients fills in blank AmneziaWG 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 own configured 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. Mirrors defaultWireguardClients, reusing
|
||||
// its IP allocation and validation helpers — the only real difference is
|
||||
// where the allocation base comes from.
|
||||
func defaultAmneziaWGClients(settingsJSON string, existing, clients []model.Client, interfaceClients []any) error {
|
||||
base, err := defaultAmneziaWGSubnetBase(settingsJSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
used := make([]string, 0)
|
||||
for i := range existing {
|
||||
used = append(used, existing[i].AllowedIPs...)
|
||||
}
|
||||
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}
|
||||
} else {
|
||||
normalized, err := normalizeWireguardAllowedIPs(c.AllowedIPs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(normalized) == 0 {
|
||||
return common.NewError("amneziawg: allowedIPs has no usable entry")
|
||||
}
|
||||
if hit := wireguardAllowedIPsCollision(normalized, used); hit != "" {
|
||||
return common.NewError("amneziawg: allowedIPs entry already used by another client:", hit)
|
||||
}
|
||||
c.AllowedIPs = normalized
|
||||
}
|
||||
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
|
||||
}
|
||||
interfaceClients[i] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -362,6 +362,11 @@ func (s *ClientService) addInboundClient(inboundSvc *InboundService, data *model
|
||||
return false, dErr
|
||||
}
|
||||
}
|
||||
if oldInbound.Protocol == model.AmneziaWG {
|
||||
if dErr := defaultAmneziaWGClients(oldInbound.Settings, existingClients, clients, interfaceClients); dErr != nil {
|
||||
return false, dErr
|
||||
}
|
||||
}
|
||||
|
||||
for _, client := range clients {
|
||||
if strings.TrimSpace(client.Email) == "" {
|
||||
@@ -465,6 +470,8 @@ func (s *ClientService) addInboundClient(inboundSvc *InboundService, data *model
|
||||
needRestart = true
|
||||
} else if oldInbound.Protocol == model.MTProto {
|
||||
inboundSvc.applyLocalMtproto(oldInbound.Id)
|
||||
} else if oldInbound.Protocol == model.AmneziaWG {
|
||||
inboundSvc.applyLocalAmneziaWG(oldInbound.Id)
|
||||
} else {
|
||||
for _, client := range clients {
|
||||
if len(client.Email) == 0 {
|
||||
@@ -596,10 +603,10 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
|
||||
}
|
||||
}
|
||||
|
||||
// WireGuard keys are never rotated by an edit: when the incoming payload omits
|
||||
// them (a metadata-only change), carry the stored credentials forward so the
|
||||
// settings JSON and the running peer keep the client's identity.
|
||||
if oldInbound.Protocol == model.WireGuard && clientIndex >= 0 && clientIndex < len(oldClients) {
|
||||
// WireGuard/AmneziaWG keys are never rotated by an edit: when the incoming
|
||||
// payload omits them (a metadata-only change), carry the stored credentials
|
||||
// forward so the settings JSON and the running peer keep the client's identity.
|
||||
if (oldInbound.Protocol == model.WireGuard || oldInbound.Protocol == model.AmneziaWG) && clientIndex >= 0 && clientIndex < len(oldClients) {
|
||||
old := oldClients[clientIndex]
|
||||
if clients[0].PrivateKey == "" {
|
||||
clients[0].PrivateKey = old.PrivateKey
|
||||
@@ -676,7 +683,7 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
|
||||
if v, ok2 := newMap["subId"].(string); ok2 {
|
||||
clients[0].SubID = v
|
||||
}
|
||||
if oldInbound.Protocol == model.WireGuard {
|
||||
if oldInbound.Protocol == model.WireGuard || oldInbound.Protocol == model.AmneziaWG {
|
||||
newMap["privateKey"] = clients[0].PrivateKey
|
||||
newMap["publicKey"] = clients[0].PublicKey
|
||||
newMap["allowedIPs"] = clients[0].AllowedIPs
|
||||
@@ -843,6 +850,8 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
|
||||
needRestart = true
|
||||
} else if oldInbound.Protocol == model.MTProto {
|
||||
inboundSvc.applyLocalMtproto(oldInbound.Id)
|
||||
} else if oldInbound.Protocol == model.AmneziaWG {
|
||||
inboundSvc.applyLocalAmneziaWG(oldInbound.Id)
|
||||
} else {
|
||||
if oldClients[clientIndex].Enable {
|
||||
err1 := rt.RemoveUser(context.Background(), oldInbound, oldEmail)
|
||||
@@ -1024,6 +1033,10 @@ func (s *ClientService) DelInboundClientByEmail(inboundSvc *InboundService, inbo
|
||||
// it (removing the last client stops the sidecar) regardless of the
|
||||
// client's enable state.
|
||||
inboundSvc.applyLocalMtproto(oldInbound.Id)
|
||||
} else if oldInbound.Protocol == model.AmneziaWG {
|
||||
// Same reasoning as MTProto above: the interface config is
|
||||
// regenerated from the full peer set, so any delete re-applies it.
|
||||
inboundSvc.applyLocalAmneziaWG(oldInbound.Id)
|
||||
} else if needApiDel {
|
||||
// Local inbound: a disabled client isn't in the running Xray, so only
|
||||
// a live one (needApiDel) needs an API removal.
|
||||
|
||||
@@ -729,6 +729,9 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
|
||||
if err := s.normalizeMtprotoXrayPort(inbound, ""); err != nil {
|
||||
return inbound, false, err
|
||||
}
|
||||
if err := s.normalizeAmneziaWGSettings(inbound); err != nil {
|
||||
return inbound, false, err
|
||||
}
|
||||
inbound.SubSortIndex = normalizeSubSortIndex(inbound.SubSortIndex)
|
||||
if err := normalizeInboundShareAddressStrict(inbound); err != nil {
|
||||
return inbound, false, err
|
||||
@@ -1149,6 +1152,9 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
return inbound, false, err
|
||||
}
|
||||
s.normalizeMtprotoSecret(inbound)
|
||||
if err := s.normalizeAmneziaWGSettings(inbound); err != nil {
|
||||
return inbound, false, err
|
||||
}
|
||||
inbound.SubSortIndex = normalizeSubSortIndex(inbound.SubSortIndex)
|
||||
|
||||
oldInbound, err := s.GetInbound(inbound.Id)
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||
wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
)
|
||||
|
||||
// DesiredAmneziaWGInstances derives the AmneziaWG interfaces this panel
|
||||
// should be running: one instance per enabled local AmneziaWG inbound,
|
||||
// serving only the peers of clients that are both enabled in the inbound
|
||||
// settings and not depletion-disabled in client_traffics. That is the same
|
||||
// effective peer set buildRuntimeInboundForAPI pushes on interactive edits,
|
||||
// so the reconcile job and the push path agree on one fingerprint — see
|
||||
// DesiredMtprotoInstances, which this mirrors exactly.
|
||||
func (s *InboundService) DesiredAmneziaWGInstances() ([]amneziawg.Instance, error) {
|
||||
db := database.GetDB()
|
||||
var inbounds []*model.Inbound
|
||||
err := db.Model(model.Inbound{}).
|
||||
Where("protocol = ? AND enable = ? AND node_id IS NULL", model.AmneziaWG, true).
|
||||
Find(&inbounds).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(inbounds) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ids := make([]int, 0, len(inbounds))
|
||||
for _, ib := range inbounds {
|
||||
ids = append(ids, ib.Id)
|
||||
}
|
||||
var disabledRows []xray.ClientTraffic
|
||||
err = db.Model(xray.ClientTraffic{}).
|
||||
Where("inbound_id IN ? AND enable = ?", ids, false).
|
||||
Select("inbound_id", "email").
|
||||
Find(&disabledRows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
disabled := make(map[int]map[string]struct{}, len(disabledRows))
|
||||
for _, row := range disabledRows {
|
||||
if disabled[row.InboundId] == nil {
|
||||
disabled[row.InboundId] = map[string]struct{}{}
|
||||
}
|
||||
disabled[row.InboundId][row.Email] = struct{}{}
|
||||
}
|
||||
|
||||
instances := make([]amneziawg.Instance, 0, len(inbounds))
|
||||
for _, ib := range inbounds {
|
||||
inst, ok := amneziawg.InstanceFromInbound(ib)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if off := disabled[ib.Id]; len(off) > 0 {
|
||||
kept := make([]amneziawg.Peer, 0, len(inst.Peers))
|
||||
for _, p := range inst.Peers {
|
||||
if _, skip := off[p.Email]; !skip {
|
||||
kept = append(kept, p)
|
||||
}
|
||||
}
|
||||
inst.Peers = kept
|
||||
}
|
||||
if len(inst.Peers) == 0 {
|
||||
continue
|
||||
}
|
||||
instances = append(instances, inst)
|
||||
}
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
// applyLocalAmneziaWG pushes a single local AmneziaWG inbound's current peer
|
||||
// set to its interface right after a client edit commits, so an add,
|
||||
// removal, re-key or enable-toggle takes effect immediately instead of
|
||||
// waiting up to 10s for the reconcile job. It re-reads the inbound so it sees
|
||||
// the committed settings, filters depleted clients exactly like the
|
||||
// reconcile job, and is a no-op for node-owned or non-AmneziaWG inbounds.
|
||||
// Failures are logged and swallowed: the reconcile job is the backstop.
|
||||
// Mirrors applyLocalMtproto.
|
||||
func (s *InboundService) applyLocalAmneziaWG(inboundId int) {
|
||||
inbound, err := s.GetInbound(inboundId)
|
||||
if err != nil || inbound == nil || inbound.Protocol != model.AmneziaWG || inbound.NodeID != nil {
|
||||
return
|
||||
}
|
||||
rt, err := s.runtimeFor(inbound)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
payload := inbound
|
||||
if inbound.Enable {
|
||||
if built, bErr := s.buildRuntimeInboundForAPI(database.GetDB(), inbound); bErr == nil {
|
||||
payload = built
|
||||
}
|
||||
}
|
||||
if err := rt.UpdateInbound(context.Background(), inbound, payload); err != nil {
|
||||
logger.Debug("amneziawg: immediate apply failed for inbound", inboundId, ":", err)
|
||||
}
|
||||
}
|
||||
|
||||
// defaultAmneziaWGServer builds a fresh server block: a random AmneziaWG 2.0
|
||||
// obfuscation set, the default tunnel subnet/DNS, and a freshly generated
|
||||
// keypair.
|
||||
func defaultAmneziaWGServer() (*amneziawg.ServerSettings, error) {
|
||||
server := &amneziawg.ServerSettings{
|
||||
SubnetIP: "10.8.1.0",
|
||||
SubnetCIDR: 24,
|
||||
PrimaryDNS: "8.8.8.8",
|
||||
SecondaryDNS: "8.8.4.4",
|
||||
Obfuscation20: amneziawg.GenerateObfuscation20("default"),
|
||||
}
|
||||
if err := fillAmneziaWGServerKeys(server); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return server, nil
|
||||
}
|
||||
|
||||
// fillAmneziaWGServerKeys generates a real WireGuard-compatible keypair for
|
||||
// the server block when one is missing.
|
||||
func fillAmneziaWGServerKeys(server *amneziawg.ServerSettings) error {
|
||||
priv, pub, err := wgutil.GenerateWireguardKeypair()
|
||||
if err != nil {
|
||||
return fmt.Errorf("amneziawg: generate server keypair: %w", err)
|
||||
}
|
||||
server.PrivateKey = priv
|
||||
server.PublicKey = pub
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeAmneziaWGSettings ensures an AmneziaWG inbound's settings have a
|
||||
// valid server block, generating one (fresh obfuscation params + keypair) on
|
||||
// first save and validating a manually-edited one so a bad entry can't bring
|
||||
// the interface down on the next apply. A no-op for every other protocol.
|
||||
func (s *InboundService) normalizeAmneziaWGSettings(inbound *model.Inbound) error {
|
||||
if inbound.Protocol != model.AmneziaWG {
|
||||
return nil
|
||||
}
|
||||
|
||||
trimmed := strings.TrimSpace(inbound.Settings)
|
||||
if trimmed == "" || trimmed == "null" || trimmed == "{}" {
|
||||
server, err := defaultAmneziaWGServer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings := amneziawg.InboundSettings{Server: server, Clients: []model.Client{}}
|
||||
bs, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inbound.Settings = string(bs)
|
||||
return nil
|
||||
}
|
||||
|
||||
var parsed amneziawg.InboundSettings
|
||||
if err := json.Unmarshal([]byte(inbound.Settings), &parsed); err != nil {
|
||||
return fmt.Errorf("amneziawg: invalid settings: %w", err)
|
||||
}
|
||||
if parsed.Server == nil {
|
||||
server, err := defaultAmneziaWGServer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parsed.Server = server
|
||||
} else if parsed.Server.PrivateKey == "" {
|
||||
if err := fillAmneziaWGServerKeys(parsed.Server); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := amneziawg.ValidateObfuscation(parsed.Server.Obfuscation20); err != nil {
|
||||
return fmt.Errorf("amneziawg: %w", err)
|
||||
}
|
||||
|
||||
bs, err := json.MarshalIndent(parsed, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inbound.Settings = string(bs)
|
||||
return nil
|
||||
}
|
||||
@@ -20,7 +20,7 @@ const (
|
||||
func inboundTransports(protocol model.Protocol, streamSettings, settings string) transportBits {
|
||||
// protocols that ignore streamSettings entirely.
|
||||
switch protocol {
|
||||
case model.Hysteria, model.WireGuard:
|
||||
case model.Hysteria, model.WireGuard, model.AmneziaWG:
|
||||
return transportUDP
|
||||
case model.MTProto:
|
||||
return transportTCP
|
||||
|
||||
@@ -158,6 +158,7 @@ func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
|
||||
model.Tunnel: true,
|
||||
model.Mixed: true,
|
||||
model.WireGuard: true,
|
||||
model.AmneziaWG: true,
|
||||
model.HTTP: true,
|
||||
}
|
||||
|
||||
@@ -202,6 +203,7 @@ func (t *Tgbot) getInboundsAttachPicker() (*telego.InlineKeyboardMarkup, error)
|
||||
model.Tunnel: true,
|
||||
model.Mixed: true,
|
||||
model.WireGuard: true,
|
||||
model.AmneziaWG: true,
|
||||
model.HTTP: true,
|
||||
}
|
||||
selected := make(map[int]bool, len(receiver_inbound_IDs))
|
||||
|
||||
@@ -139,7 +139,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
if inbound.NodeID != nil {
|
||||
continue
|
||||
}
|
||||
if inbound.Protocol == model.MTProto {
|
||||
if inbound.Protocol == model.MTProto || inbound.Protocol == model.AmneziaWG {
|
||||
continue
|
||||
}
|
||||
settings := map[string]any{}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/config"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/eventbus"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||
@@ -288,6 +289,7 @@ const (
|
||||
cadenceXrayRestart = "@every 30s"
|
||||
cadenceXrayTraffic = "@every 5s"
|
||||
cadenceMtproto = "@every 10s"
|
||||
cadenceAmneziaWG = "@every 10s"
|
||||
cadenceClientIPScan = "@every 10s"
|
||||
cadenceNodeHeartbeat = "@every 5s"
|
||||
cadenceNodeTraffic = "@every 5s"
|
||||
@@ -327,6 +329,11 @@ func (s *Server) startTask(restartXray bool) {
|
||||
_, _ = s.cron.AddJob(cadenceMtproto, mtJob)
|
||||
go mtJob.Run()
|
||||
|
||||
// Reconcile AmneziaWG interfaces and scrape their traffic
|
||||
awgJob := job.NewAmneziaWGJob()
|
||||
_, _ = s.cron.AddJob(cadenceAmneziaWG, awgJob)
|
||||
go awgJob.Run()
|
||||
|
||||
// check client ips from log file every 10 sec
|
||||
_, _ = s.cron.AddJob(cadenceClientIPScan, job.NewCheckClientIpJob())
|
||||
|
||||
@@ -680,6 +687,7 @@ func (s *Server) stop(stopXray bool, stopTgBot bool) error {
|
||||
if stopXray {
|
||||
_ = s.xrayService.StopXray()
|
||||
mtproto.GetManager().StopAll()
|
||||
amneziawg.GetManager().StopAll()
|
||||
}
|
||||
if s.cron != nil {
|
||||
s.cron.Stop()
|
||||
|
||||
Reference in New Issue
Block a user