feat(amneziawg): add AmneziaWG as an outbound protocol (#6320)

* feat(amneziawg): add AmneziaWG as an outbound protocol

- AmneziaWG outbound protocol end-to-end: config schema, socks bridge, netstack, panel UI
- Route amneziawg outbounds to HTTP probe in TCP mode (backend + frontend classifiers) with pinning test
- Add 2-minute idle read deadline to pumpUDPEgress to reap idle egress sessions
- Require SOCKS5 username/password auth on the egress server (reject NO-AUTH with 0xFF) with test
- Bound the egress TCP tunnel dial with portForwardDialTimeout (10s), matching portfwd.go
- Resolve UDP domain targets off the association's reader loop via deliverUDPDatagram; race-safe getOrDial starts the reply pump at session creation; client passed by value into resolver goroutines (pinned by TestEgressUDPDatagramDomainInterleavedClients)
- Reconcile early-returns on an empty desired set and closes the egress listener; EgressBasePort (64900) is reserved against local inbound port conflicts like the internal API port, with pinning tests for both the port reservation (TestCheckPortConflict_EgressPortBlockedLocal) and the Reconcile empty-desired Close/Listen lifecycle (TestOutboundManagerReconcileEmptyDesiredClosesEgress)
- Eliminate acceptLoop shutdown race by validating listener != nil and registering to tracked under s.mu before wg.Add; bound pre-auth handshake with deadline (pinned by TestEgressServerCloseDuringConcurrentAccepts)
- Support AAAA and dual-stack domain resolution in tunnel DNS resolver with v6 default fallback (DefaultTunnelDNSServerV6); add DNS field to frontend protocol form; avoid unneeded cache flushes on unchanged SetStack ticks

* fix(amneziawg): resolve IPv6-only DNS default fallback and validate required keys

- Default to IPv6 tunnel DNS on IPv6-only outbounds with blank dns
- Require non-empty secretKey and peer publicKey in ValidateAmneziaWGOutbound
- Add end-to-end IPv6 tunnel domain resolution test and test empty key rejection
- Trim comment blocks exceeding 2 lines across modified files
- Fix Storybook test execution on environments with POSIX locale

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
Rouzbeh†
2026-09-10 16:20:48 +03:30
committed by GitHub
parent 876497db6e
commit d5ab84e8d5
50 changed files with 4169 additions and 33 deletions
+67 -11
View File
@@ -1,24 +1,20 @@
package job
import (
"encoding/json"
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
"github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
"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 embedded AmneziaWG interfaces
// (internal/amneziawgnet -- amneziawg-go over a gVisor netstack, no kernel
// module) against the enabled AmneziaWG inbounds in the database,
// rebuilding/reconfiguring any that drifted. Unlike the retired
// kernel-module Manager this job used to drive, there is no traffic/
// online-status accounting here at all: once a peer's decapsulated traffic
// is relayed into Xray's own SOCKS5 inbound (see
// internal/web/service/xray.go's injectAmneziawgnetSocks, and
// internal/amneziawgnet.Manager's automatic forwarder/relay wiring), it's
// an ordinary Xray user, and XrayTrafficJob's existing, protocol-blind
// stats/online-status polling already picks it up for free.
// AmneziaWGJob converges embedded AmneziaWG interfaces (inbounds AND the
// template's "amneziawg" outbounds) every 10s; stats stay with Xray.
type AmneziaWGJob struct {
inboundService service.InboundService
settingService service.SettingService
}
// NewAmneziaWGJob creates a new AmneziaWG reconcile job instance.
@@ -52,4 +48,64 @@ func (j *AmneziaWGJob) Run() {
})
}
amneziawgnet.GetManager().Reconcile(wanted)
outboundDesired, err := j.desiredOutboundInstances()
if err != nil {
logger.Warning("amneziawg job: get desired outbound instances failed:", err)
return
}
amneziawgnet.GetOutboundManager().Reconcile(outboundDesired)
}
// desiredOutboundInstances derives client instances per template "amneziawg" outbound.
func (j *AmneziaWGJob) desiredOutboundInstances() ([]amneziawgnet.OutboundDesired, error) {
template, err := j.settingService.GetXrayConfigTemplate()
if err != nil {
return nil, err
}
if template == "" {
return nil, nil
}
cfg := &xray.Config{}
if err := json.Unmarshal([]byte(template), cfg); err != nil {
return nil, err
}
if len(cfg.OutboundConfigs) == 0 {
return nil, nil
}
var raws []json.RawMessage
if err := json.Unmarshal(cfg.OutboundConfigs, &raws); err != nil {
return nil, err
}
out := make([]amneziawgnet.OutboundDesired, 0, len(raws))
for _, raw := range raws {
if !amneziawg.IsAmneziaWGOutbound(raw) {
continue
}
var probe struct {
Tag string `json:"tag"`
}
if err := json.Unmarshal(raw, &probe); err != nil || probe.Tag == "" {
continue
}
inst, ok := amneziawg.InstanceFromOutbound(probe.Tag, raw)
if !ok {
continue
}
out = append(out, amneziawgnet.OutboundDesired{
Instance: inst,
Options: amneziawgnet.DeviceOptions{
HeaderProtectionKey: inst.Obfuscation.HeaderProtectionKey,
ContentPaddingAddition: inst.Obfuscation.ContentPaddingAddition,
RekeyAfterTime: inst.Obfuscation.RekeyAfterTime,
RekeyTimeout: inst.Obfuscation.RekeyTimeout,
RejectAfterTime: inst.Obfuscation.RejectAfterTime,
KeepaliveTimeout: inst.Obfuscation.KeepaliveTimeout,
MaxHandshakeAttempts: inst.Obfuscation.MaxHandshakeAttempts,
RandomTrailers: inst.Obfuscation.RandomTrailers,
DisableCookies: inst.Obfuscation.DisableCookies,
},
})
}
return out, nil
}
+1 -1
View File
@@ -221,7 +221,7 @@ func probeTCPEndpoint(endpoint string, timeout time.Duration) TestEndpointResult
// dial neither proves reachability nor measures latency. Such outbounds
// must go through the real xray handshake probe instead.
func outboundTransportIsUDP(ob map[string]any) bool {
if protocol, _ := ob["protocol"].(string); protocol == "hysteria" || protocol == "wireguard" {
if protocol, _ := ob["protocol"].(string); protocol == "hysteria" || protocol == "wireguard" || protocol == "amneziawg" {
return true
}
if stream, ok := ob["streamSettings"].(map[string]any); ok {
@@ -18,6 +18,7 @@ import (
"sync"
"time"
"github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
"github.com/mhsanaei/3x-ui/v3/internal/config"
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
"github.com/mhsanaei/3x-ui/v3/internal/xray"
@@ -384,6 +385,33 @@ func buildBatchTestConfig(items []*httpBatchItem, allOutbounds []any, ports []in
outbounds = append(outbounds, it.outbound)
}
}
// Bridge amneziawg entries like GetXrayConfig does -- one raw entry fails
// the whole temp config; drop unbridgeable ones, not unrelated items.
bridged := make([]any, 0, len(outbounds))
for _, ob := range outbounds {
m, ok := ob.(map[string]any)
if !ok {
bridged = append(bridged, ob)
continue
}
if p, _ := m["protocol"].(string); p != "amneziawg" {
bridged = append(bridged, ob)
continue
}
raw, err := json.Marshal(m)
if err != nil {
continue
}
repl, ok := amneziawgnet.BuildSocksBridge(raw)
if !ok {
continue
}
var replacement any
if json.Unmarshal(repl, &replacement) == nil {
bridged = append(bridged, replacement)
}
}
outbounds = bridged
for _, ob := range outbounds {
outbound, ok := ob.(map[string]any)
if !ok {
@@ -557,6 +557,33 @@ func TestTestOutboundsTCPModeForcesUDPToHTTPProbe(t *testing.T) {
}
}
func TestTestOutboundsTCPModeForcesAmneziaWGToHTTPProbe(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
withStubProcess(t, func(cfg *xray.Config, configPath string) batchProcess {
return &stubProcess{cfg: cfg, serveSocks: true}
})
withEgressTraceProbe(t, func(*url.URL) *TestEgressResult {
return &TestEgressResult{IPv4: "198.51.100.2", Country: "ZZ", Warp: "off"}
})
batch := mustJSON(t, []any{map[string]any{"tag": "awg", "protocol": "amneziawg"}})
results, err := (&OutboundService{}).TestOutbounds(batch, srv.URL, "", "tcp")
if err != nil {
t.Fatalf("TestOutbounds: %v", err)
}
r := results[0]
if !r.Success || r.Mode != "http" {
t.Errorf("amneziawg outbound in tcp mode = %+v, want success with mode %q", r, "http")
}
if r.Egress == nil || r.Egress.IPv4 != "198.51.100.2" {
t.Errorf("amneziawg outbound egress = %+v", r.Egress)
}
}
func TestProbeModeLabel(t *testing.T) {
cases := []struct{ mode, want string }{
{"tcp", "tcp"},
+12
View File
@@ -185,6 +185,18 @@ func checkPortConflictTx(db *gorm.DB, inbound *model.Inbound, ignoreId int) (*po
}, nil
}
// Egress SOCKS server holds loopback EgressBasePort when AWG outbounds are
// active; conflict check prevents inbounds from colliding with it.
if inbound.NodeID == nil && inbound.Port == int(amneziawgnet.EgressBasePort) &&
newBits&transportTCP != 0 && listenOverlaps("127.0.0.1", inbound.Listen) {
return &portConflictDetail{
Tag: "amneziawg-egress",
Listen: "127.0.0.1",
Port: inbound.Port,
Transports: transportTCP,
}, nil
}
// Every enabled local AmneziaWG inbound gets its own automatic Xray
// SOCKS5 relay inbound (see injectAmneziawgnetSocks) on 127.0.0.1 at a
// port derived purely from its id (amneziawgnet.SOCKSPortForInbound) --
+21 -4
View File
@@ -739,10 +739,27 @@ func TestCheckPortConflict_ReservedAPIPortUDPCoexists(t *testing.T) {
// it's now ignored -- see the "RouteThroughXrayOff" test below.
const amneziawgRoutedSettings = `{"server":{"privateKey":"priv","publicKey":"pub","subnetIp":"10.8.1.0","subnetCidr":24,"routeThroughXray":true},"clients":[{"email":"a@x","enable":true,"publicKey":"pub-a","allowedIPs":["10.8.1.2/32"]}]}`
// An enabled AmneziaWG inbound's automatic Xray SOCKS5 relay inbound
// (injectAmneziawgnetSocks) is a synthetic loopback inbound, not a database
// row, so checkPortConflict needs its own check to catch a collision --
// exactly the same shape of problem as the reserved API port above.
// A local TCP inbound on EgressBasePort must conflict with the AmneziaWG
// egress SOCKS server (which is not in the database).
func TestCheckPortConflict_EgressPortBlockedLocal(t *testing.T) {
setupConflictDB(t)
svc := &InboundService{}
candidate := &model.Inbound{
Tag: "vless-bridge",
Listen: "0.0.0.0",
Port: int(amneziawgnet.EgressBasePort),
Protocol: model.VLESS,
}
got, err := svc.checkPortConflict(candidate, 0)
if err != nil {
t.Fatalf("checkPortConflict: %v", err)
}
if got == nil {
t.Fatalf("a local inbound on the egress port %d must conflict", amneziawgnet.EgressBasePort)
}
}
func TestCheckPortConflict_AmneziawgnetSocksRelayBlockedLocal(t *testing.T) {
setupConflictDB(t)
seedInboundConflict(t, "awg-1", "0.0.0.0", 51820, model.AmneziaWG, ``, amneziawgRoutedSettings)
+5
View File
@@ -161,6 +161,11 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
// still carry sessionPlacement/sessionKey; lift them too (same reason as
// the per-inbound lift below).
xrayConfig.OutboundConfigs = liftOutboundsXhttpSessionIDKeys(xrayConfig.OutboundConfigs)
// Bridge amneziawg outbounds before anything else reads OutboundConfigs;
// the core has no amneziawg proxy and would reject the raw entry.
if err := transformAmneziaWGOutbounds(xrayConfig); err != nil {
return nil, err
}
_, _, _ = s.inboundService.AddTraffic(nil, nil)
@@ -0,0 +1,51 @@
package service
import (
"encoding/json"
"fmt"
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
"github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
json_util "github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
"github.com/mhsanaei/3x-ui/v3/internal/xray"
)
// transformAmneziaWGOutbounds swaps each template "amneziawg" outbound for
// its socks bridge; unbridgeable entries fail generation rather than skip.
func transformAmneziaWGOutbounds(cfg *xray.Config) error {
if len(cfg.OutboundConfigs) == 0 {
return nil
}
var outbounds []json.RawMessage
if err := json.Unmarshal(cfg.OutboundConfigs, &outbounds); err != nil {
return err
}
changed := false
for i, raw := range outbounds {
if !amneziawg.IsAmneziaWGOutbound(raw) {
continue
}
var probe struct {
Tag string `json:"tag"`
}
tagErr := json.Unmarshal(raw, &probe)
replacement, ok := amneziawgnet.BuildSocksBridge(raw)
if !ok {
if tagErr != nil {
return fmt.Errorf("amneziawg outbound %d: unreadable tag: %w", i, tagErr)
}
return fmt.Errorf("amneziawg outbound %d (%q): cannot bridge: tag must be a non-empty string", i, probe.Tag)
}
outbounds[i] = replacement
changed = true
}
if !changed {
return nil
}
bs, err := json.Marshal(outbounds)
if err != nil {
return err
}
cfg.OutboundConfigs = json_util.RawMessage(bs)
return nil
}
@@ -0,0 +1,289 @@
package service
import (
"encoding/json"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
"github.com/mhsanaei/3x-ui/v3/internal/xray"
)
func amneziawgnetEgressPortForTest() int { return amneziawgnet.EgressBasePort }
func wgKeypairForTest() (priv, pub string, err error) {
return wgutil.GenerateWireguardKeypair()
}
func makeAWGOutboundConfig(t *testing.T) *xray.Config {
t.Helper()
cfg := &xray.Config{}
err := json.Unmarshal([]byte(`{
"outbounds": [
{"protocol": "freedom", "tag": "direct"},
{"protocol": "amneziawg", "tag": "awg-hop", "settings": {"secretKey": "x"}}
]
}`), cfg)
if err != nil {
t.Fatal(err)
}
return cfg
}
func TestTransformAmneziaWGOutbounds(t *testing.T) {
cfg := makeAWGOutboundConfig(t)
if err := transformAmneziaWGOutbounds(cfg); err != nil {
t.Fatal(err)
}
var outbounds []struct {
Protocol string `json:"protocol"`
Tag string `json:"tag"`
Settings struct {
Address string `json:"address"`
Port int `json:"port"`
User string `json:"user"`
Pass string `json:"pass"`
} `json:"settings"`
}
if err := json.Unmarshal(cfg.OutboundConfigs, &outbounds); err != nil {
t.Fatal(err)
}
if len(outbounds) != 2 {
t.Fatalf("outbound count = %d, want 2 (no additions or drops)", len(outbounds))
}
if outbounds[0].Protocol != "freedom" || outbounds[0].Tag != "direct" {
t.Fatalf("first outbound disturbed: %+v", outbounds[0])
}
got := outbounds[1]
if got.Protocol != "socks" {
t.Fatalf("amneziawg outbound not swapped to socks: %q", got.Protocol)
}
if got.Tag != "awg-hop" {
t.Fatalf("tag not preserved: %q", got.Tag)
}
if got.Settings.Address != "127.0.0.1" {
t.Fatalf("bridge address = %q, want 127.0.0.1", got.Settings.Address)
}
if got.Settings.Port != amneziawgnetEgressPortForTest() {
t.Fatalf("bridge port = %d", got.Settings.Port)
}
if got.Settings.User != "awg-hop" {
t.Fatalf("SOCKS username = %q, want the outbound tag", got.Settings.User)
}
if got.Settings.Pass == "" {
t.Fatal("SOCKS password must be set (egress server enforces it)")
}
}
func TestTransformAmneziaWGOutbounds_NoopWithoutAWG(t *testing.T) {
before := &xray.Config{}
if err := json.Unmarshal([]byte(`{"outbounds":[{"protocol":"freedom","tag":"direct"}]}`), before); err != nil {
t.Fatal(err)
}
cfg := &xray.Config{}
if err := json.Unmarshal(before.OutboundConfigs, &cfg.OutboundConfigs); err != nil {
t.Fatal(err)
}
orig := json_util.RawMessage(append([]byte(nil), cfg.OutboundConfigs...))
if err := transformAmneziaWGOutbounds(cfg); err != nil {
t.Fatal(err)
}
if string(cfg.OutboundConfigs) != string(orig) {
t.Fatalf("config without amneziawg outbounds must stay byte-identical:\nbefore=%s\nafter=%s", orig, cfg.OutboundConfigs)
}
}
func TestCheckXrayConfig_AcceptsValidAWGOutbound(t *testing.T) {
// A syntactically valid AWG outbound must pass panel-side validation --
// the Xray-core loader would reject the unknown protocol outright.
priv, pub, err := wgKeypairForTest()
if err != nil {
t.Fatal(err)
}
template := `{
"outbounds": [{
"protocol": "amneziawg",
"tag": "awg-hop",
"settings": {
"mtu": 1420,
"secretKey": "` + priv + `",
"address": ["10.8.0.2/32"],
"jc": 4, "jmin": 40, "jmax": 100, "s1": 15, "s2": 80, "s3": 12, "s4": 12,
"h1": "100-800", "h2": "900-1600", "h3": "1700-2400", "h4": "2500-3200",
"peers": [{
"publicKey": "` + pub + `",
"allowedIPs": ["0.0.0.0/0"],
"endpoint": "203.0.113.7:51820",
"keepAlive": 25
}]
}
}]
}`
svc := &XraySettingService{}
if err := svc.CheckXrayConfig(template); err != nil {
t.Fatalf("valid amneziawg outbound rejected: %v", err)
}
}
func TestCheckXrayConfig_RejectsBrokenAWGOutbound(t *testing.T) {
// The emptied field's partner must be a real key, or the case is decided
// by that partner and stays green with the empty-key guard removed.
priv, pub, err := wgKeypairForTest()
if err != nil {
t.Fatal(err)
}
cases := []struct {
name string
template string
}{
{
name: "not a key",
template: `{
"outbounds": [{
"protocol": "amneziawg",
"tag": "awg-bad",
"settings": {
"secretKey": "not-a-key",
"address": ["10.8.0.2/32"],
"peers": [{"publicKey": "alsobad", "allowedIPs": ["0.0.0.0/0"], "endpoint": "203.0.113.7:51820"}]
}
}]
}`,
},
{
name: "empty secretKey",
template: `{
"outbounds": [{
"protocol": "amneziawg",
"tag": "awg-empty-sec",
"settings": {
"secretKey": "",
"address": ["10.8.0.2/32"],
"peers": [{"publicKey": "` + pub + `", "allowedIPs": ["0.0.0.0/0"], "endpoint": "203.0.113.7:51820"}]
}
}]
}`,
},
{
name: "empty peer publicKey",
template: `{
"outbounds": [{
"protocol": "amneziawg",
"tag": "awg-empty-pub",
"settings": {
"secretKey": "` + priv + `",
"address": ["10.8.0.2/32"],
"peers": [{"publicKey": "", "allowedIPs": ["0.0.0.0/0"], "endpoint": "203.0.113.7:51820"}]
}
}]
}`,
},
}
svc := &XraySettingService{}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if err := svc.CheckXrayConfig(tc.template); err == nil {
t.Fatalf("%s: expected error, got nil", tc.name)
}
})
}
}
func TestTransformAmneziaWGOutbounds_PreservesSiblingKeys(t *testing.T) {
cfg := &xray.Config{}
err := json.Unmarshal([]byte(`{
"outbounds": [
{"protocol": "amneziawg", "tag": "awg-hop", "sendThrough": "0.0.0.0",
"targetStrategy": "UseIPv4",
"mux": {"enabled": false},
"streamSettings": {"sockopt": {"tcpFastOpen": true}},
"settings": {"secretKey": "x"}}
]
}`), cfg)
if err != nil {
t.Fatal(err)
}
if err := transformAmneziaWGOutbounds(cfg); err != nil {
t.Fatal(err)
}
var outbounds []struct {
Protocol string `json:"protocol"`
Tag string `json:"tag"`
SendThrough string `json:"sendThrough"`
TargetStrategy string `json:"targetStrategy"`
Mux map[string]any `json:"mux"`
StreamSettings map[string]any `json:"streamSettings"`
}
if err := json.Unmarshal(cfg.OutboundConfigs, &outbounds); err != nil {
t.Fatal(err)
}
if len(outbounds) != 1 {
t.Fatalf("outbound count = %d, want 1", len(outbounds))
}
got := outbounds[0]
if got.SendThrough != "0.0.0.0" {
t.Fatalf("sendThrough dropped: %q", got.SendThrough)
}
if got.TargetStrategy != "UseIPv4" {
t.Fatalf("targetStrategy dropped: %q", got.TargetStrategy)
}
if got.Mux == nil {
t.Fatal("mux dropped")
}
if got.StreamSettings == nil {
t.Fatal("streamSettings.sockopt dropped")
}
}
func TestTransformAmneziaWGOutbounds_EmptyTagIsAnError(t *testing.T) {
cfg := &xray.Config{}
if err := json.Unmarshal([]byte(`{
"outbounds": [
{"protocol": "freedom", "tag": "direct"},
{"protocol": "amneziawg", "tag": "", "settings": {"secretKey": "x"}}
]
}`), cfg); err != nil {
t.Fatal(err)
}
if err := transformAmneziaWGOutbounds(cfg); err == nil {
t.Fatal("empty-tag amneziawg outbound must fail config generation, not silently pass through")
}
}
func TestCheckXrayConfig_RejectsEmptyTagAWGOutbound(t *testing.T) {
priv, pub, err := wgKeypairForTest()
if err != nil {
t.Fatal(err)
}
template := `{
"outbounds": [{
"protocol": "amneziawg",
"tag": "",
"settings": {
"secretKey": "` + priv + `",
"address": ["10.8.0.2/32"],
"peers": [{"publicKey": "` + pub + `", "allowedIPs": ["0.0.0.0/0"], "endpoint": "203.0.113.7:51820"}]
}
}]
}`
svc := &XraySettingService{}
if err := svc.CheckXrayConfig(template); err == nil {
t.Fatal("empty-tag amneziawg outbound accepted by CheckXrayConfig")
}
}
func TestCheckXrayConfig_RejectsNonStringTagAWGOutbound(t *testing.T) {
template := `{
"outbounds": [{
"protocol": "amneziawg",
"tag": 123,
"settings": {"secretKey": "x"}
}]
}`
svc := &XraySettingService{}
if err := svc.CheckXrayConfig(template); err == nil {
t.Fatal("non-string tag amneziawg outbound accepted by CheckXrayConfig")
}
}
+15
View File
@@ -8,6 +8,7 @@ import (
"strconv"
"strings"
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
"github.com/mhsanaei/3x-ui/v3/internal/xray"
)
@@ -58,6 +59,20 @@ func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {
coreVersion = process.GetXrayVersion()
}
for _, outbound := range outbounds {
// Panel pseudo-protocol: validated panel-side because the core's
// loader would reject it outright.
if amneziawg.IsAmneziaWGOutbound(outbound) {
var probe struct {
Tag string `json:"tag"`
}
if err := json.Unmarshal(outbound, &probe); err != nil {
return common.NewError("xray template config invalid: amneziawg outbound tag unreadable:", err)
}
if err := amneziawg.ValidateAmneziaWGOutbound(probe.Tag, outbound); err != nil {
return err
}
continue
}
if err := xray.ValidateOutboundConfig(outbound); err != nil {
if shouldSkipLegacyUnencryptedOutboundRejection(coreVersion, err) {
continue
+4 -1
View File
@@ -1857,7 +1857,10 @@
"randomTrailers": "RandomTrailers",
"randomTrailersHint": "يضيف بايتات عشوائية إلى نهاية كل حزمة. يتطلب AmneziaWG 3.1+ على الطرفين.",
"disableCookies": "DisableCookies",
"disableCookiesHint": "عدم إرسال ردود الكوكي — يزيل بصمة DPI لكنه يضعف الحماية من الفيضانات."
"disableCookiesHint": "عدم إرسال ردود الكوكي — يزيل بصمة DPI لكنه يضعف الحماية من الفيضانات.",
"listenPort": "Listen Port (optional)",
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tun": {
"userLevel": "مستوى المستخدم"
+4 -1
View File
@@ -1975,7 +1975,10 @@
"randomTrailers": "RandomTrailers",
"randomTrailersHint": "Appends random bytes to every packet. Both ends need AmneziaWG 3.1+.",
"disableCookies": "DisableCookies",
"disableCookiesHint": "Never send cookie replies — removes a DPI fingerprint; weakens flood mitigation."
"disableCookiesHint": "Never send cookie replies — removes a DPI fingerprint; weakens flood mitigation.",
"listenPort": "Listen Port (optional)",
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tun": {
"userLevel": "User Level"
+4 -1
View File
@@ -1857,7 +1857,10 @@
"randomTrailers": "RandomTrailers",
"randomTrailersHint": "Añade bytes aleatorios a cada paquete. Ambos extremos necesitan AmneziaWG 3.1+.",
"disableCookies": "DisableCookies",
"disableCookiesHint": "No enviar cookie replies — elimina una huella para DPI, pero debilita la mitigación de inundaciones."
"disableCookiesHint": "No enviar cookie replies — elimina una huella para DPI, pero debilita la mitigación de inundaciones.",
"listenPort": "Listen Port (optional)",
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tun": {
"userLevel": "Nivel de Usuario"
+4 -1
View File
@@ -1857,7 +1857,10 @@
"randomTrailers": "RandomTrailers",
"randomTrailersHint": "به انتهای هر بسته بایت‌های تصادفی می‌افزاید. هر دو طرف باید AmneziaWG 3.1+ باشند.",
"disableCookies": "DisableCookies",
"disableCookiesHint": "هرگز پاسخ کوکی ارسال نشود — اثر انگشت DPI را حذف می‌کند اما دفاع در برابر سیل‌آسا را ضعیف می‌کند."
"disableCookiesHint": "هرگز پاسخ کوکی ارسال نشود — اثر انگشت DPI را حذف می‌کند اما دفاع در برابر سیل‌آسا را ضعیف می‌کند.",
"listenPort": "Listen Port (optional)",
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tun": {
"userLevel": "سطح کاربر"
+4 -1
View File
@@ -1857,7 +1857,10 @@
"randomTrailers": "RandomTrailers",
"randomTrailersHint": "Menambahkan byte acak ke setiap paket. Kedua sisi butuh AmneziaWG 3.1+.",
"disableCookies": "DisableCookies",
"disableCookiesHint": "Tidak pernah mengirim cookie reply — menghapus sidik jari DPI, tetapi melemahkan mitigasi banjir."
"disableCookiesHint": "Tidak pernah mengirim cookie reply — menghapus sidik jari DPI, tetapi melemahkan mitigasi banjir.",
"listenPort": "Listen Port (optional)",
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tun": {
"userLevel": "Level Pengguna"
+4 -1
View File
@@ -1857,7 +1857,10 @@
"randomTrailers": "RandomTrailers",
"randomTrailersHint": "各パケットにランダムなバイトを追加します。両端にAmneziaWG 3.1+が必要です。",
"disableCookies": "DisableCookies",
"disableCookiesHint": "cookie replyを送信しません。DPIの指紋を消しますが、フラッド緩和は弱まります。"
"disableCookiesHint": "cookie replyを送信しません。DPIの指紋を消しますが、フラッド緩和は弱まります。",
"listenPort": "Listen Port (optional)",
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tun": {
"userLevel": "ユーザーレベル"
+4 -1
View File
@@ -1857,7 +1857,10 @@
"randomTrailers": "RandomTrailers",
"randomTrailersHint": "Acrescenta bytes aleatórios a cada pacote. Ambos os lados precisam do AmneziaWG 3.1+.",
"disableCookies": "DisableCookies",
"disableCookiesHint": "Nunca enviar cookie replies — remove uma impressão digital de DPI, mas enfraquece a mitigação de inundações."
"disableCookiesHint": "Nunca enviar cookie replies — remove uma impressão digital de DPI, mas enfraquece a mitigação de inundações.",
"listenPort": "Listen Port (optional)",
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tun": {
"userLevel": "Nível do Usuário"
+4 -1
View File
@@ -1857,7 +1857,10 @@
"randomTrailers": "RandomTrailers",
"randomTrailersHint": "Добавляет случайные байты в конец каждого пакета. Обе стороны должны поддерживать AmneziaWG 3.1+.",
"disableCookies": "DisableCookies",
"disableCookiesHint": "Не отправлять cookie reply — убирает сигнатуру для DPI, но ослабляет защиту от флуда."
"disableCookiesHint": "Не отправлять cookie reply — убирает сигнатуру для DPI, но ослабляет защиту от флуда.",
"listenPort": "Listen Port (optional)",
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tun": {
"userLevel": "Уровень пользователя"
+4 -1
View File
@@ -1857,7 +1857,10 @@
"randomTrailers": "RandomTrailers",
"randomTrailersHint": "Her paketin sonuna rastgele baytlar ekler. Her iki uç da AmneziaWG 3.1+ gerektirir.",
"disableCookies": "DisableCookies",
"disableCookiesHint": "Cookie reply asla gönderilmez — bir DPI parmak izini kaldırır ancak taşma korumasını zayıflatır."
"disableCookiesHint": "Cookie reply asla gönderilmez — bir DPI parmak izini kaldırır ancak taşma korumasını zayıflatır.",
"listenPort": "Listen Port (optional)",
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tun": {
"userLevel": "Kullanıcı Seviyesi"
+4 -1
View File
@@ -1857,7 +1857,10 @@
"randomTrailers": "RandomTrailers",
"randomTrailersHint": "Додає випадкові байти в кінець кожного пакета. Обидві сторони мають підтримувати AmneziaWG 3.1+.",
"disableCookies": "DisableCookies",
"disableCookiesHint": "Ніколи не надсилати cookie reply — прибирає відбиток для DPI, але послаблює захист від флуду."
"disableCookiesHint": "Ніколи не надсилати cookie reply — прибирає відбиток для DPI, але послаблює захист від флуду.",
"listenPort": "Listen Port (optional)",
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tun": {
"userLevel": "Рівень користувача"
+4 -1
View File
@@ -1857,7 +1857,10 @@
"randomTrailers": "RandomTrailers",
"randomTrailersHint": "Thêm các byte ngẫu nhiên vào cuối mỗi gói. Cả hai đầu cần AmneziaWG 3.1+.",
"disableCookies": "DisableCookies",
"disableCookiesHint": "Không bao giờ gửi cookie reply — xóa một dấu vết DPI nhưng làm yếu khả năng chống flood."
"disableCookiesHint": "Không bao giờ gửi cookie reply — xóa một dấu vết DPI nhưng làm yếu khả năng chống flood.",
"listenPort": "Listen Port (optional)",
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tun": {
"userLevel": "Mức Người Dùng"
+4 -1
View File
@@ -1857,7 +1857,10 @@
"randomTrailers": "RandomTrailers",
"randomTrailersHint": "在每个数据包末尾追加随机字节。两端都需要 AmneziaWG 3.1+。",
"disableCookies": "DisableCookies",
"disableCookiesHint": "从不发送 cookie reply——消除一个 DPI 指纹,但会削弱抗洪泛能力。"
"disableCookiesHint": "从不发送 cookie reply——消除一个 DPI 指纹,但会削弱抗洪泛能力。",
"listenPort": "Listen Port (optional)",
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tun": {
"userLevel": "用户级别"
+4 -1
View File
@@ -1857,7 +1857,10 @@
"randomTrailers": "RandomTrailers",
"randomTrailersHint": "在每個封包結尾附加隨機位元組。兩端都需要 AmneziaWG 3.1+。",
"disableCookies": "DisableCookies",
"disableCookiesHint": "永不傳送 cookie reply——消除一個 DPI 指紋,但會削弱抗洪泛能力。"
"disableCookiesHint": "永不傳送 cookie reply——消除一個 DPI 指紋,但會削弱抗洪泛能力。",
"listenPort": "Listen Port (optional)",
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tun": {
"userLevel": "用戶級別"
+1
View File
@@ -699,6 +699,7 @@ func (s *Server) stop(stopXray bool, stopTgBot bool) error {
_ = s.xrayService.StopXray()
mtproto.GetManager().StopAll()
amneziawgnet.GetManager().StopAll()
amneziawgnet.GetOutboundManager().StopAll()
}
if s.cron != nil {
s.cron.Stop()