Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions constant/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const (
TypeTrojan = "trojan"
TypeNaive = "naive"
TypeWireGuard = "wireguard"
TypeAmneziaWG = "amneziawg"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
TypeHysteria = "hysteria"
TypeTor = "tor"
TypeSSH = "ssh"
Expand Down Expand Up @@ -66,6 +67,8 @@ func ProxyDisplayName(proxyType string) string {
return "Naive"
case TypeWireGuard:
return "WireGuard"
case TypeAmneziaWG:
return "AmneziaWG"
case TypeHysteria:
return "Hysteria"
case TypeTor:
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
module github.com/sagernet/sing-box

go 1.24.0
go 1.24.4

toolchain go1.24.9

require (
github.com/amnezia-vpn/amneziawg-go v1.0.4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify amneziawg-go v1.0.4 exists and check for security advisories

# Check if the version exists on GitHub
echo "=== Checking release tags ==="
gh api repos/amnezia-vpn/amneziawg-go/tags --jq '.[].name' | head -10

# Check for security advisories
echo "=== Checking security advisories ==="
gh api graphql -f query='
{
  securityVulnerabilities(first: 5, ecosystem: GO, package: "github.com/amnezia-vpn/amneziawg-go") {
    nodes {
      advisory {
        summary
        severity
        publishedAt
      }
      vulnerableVersionRange
      firstPatchedVersion {
        identifier
      }
    }
  }
}'

Repository: hawkff/sing-box

Length of output: 319


🏁 Script executed:

#!/bin/bash
# Check all tags for v1.0.4
echo "=== Searching for v1.0.4 tag ==="
gh api repos/amnezia-vpn/amneziawg-go/tags --paginate --jq '.[].name' | grep -i "1.0.4" || echo "v1.0.4 not found"

# Also check releases
echo "=== Checking releases ==="
gh api repos/amnezia-vpn/amneziawg-go/releases --jq '.[].tag_name' | grep -i "1.0.4" || echo "v1.0.4 not found in releases"

# Check the go.mod file in the actual repository
echo "=== Checking go.mod content ==="
cat go.mod | grep -A2 -B2 "amneziawg-go"

Repository: hawkff/sing-box

Length of output: 332


Nonexistent dependency version: v1.0.4 does not exist in amneziawg-go repository.

The specified version v1.0.4 does not exist in the amnezia-vpn/amneziawg-go repository. The latest available version is v0.2.19. This will cause Go module resolution to fail during builds. Either correct the version number or ensure the intended version is released before merging.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go.mod` at line 8, The amneziawg-go dependency in go.mod is pinned to version
v1.0.4 which does not exist in the repository. Update the version specification
for the github.com/amnezia-vpn/amneziawg-go dependency to an actual existing
version, such as v0.2.19 which is the latest available version, to resolve the
module resolution failure.

github.com/anytls/sing-anytls v0.0.11
github.com/caddyserver/certmagic v0.23.0
github.com/coder/websocket v1.8.13
Expand Down Expand Up @@ -129,10 +130,12 @@ require (
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
github.com/tevino/abool v1.2.0 // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/amnezia-vpn/amneziawg-go v1.0.4 h1:hyS3dEY+znvfGVZznYdLWaKGPBwJzGqJLuO/9s3sTok=
github.com/amnezia-vpn/amneziawg-go v1.0.4/go.mod h1:uD0Cz0XbnhE0k1vpJZiUq47YDP5vne9FtV9Bc1lQJEs=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc=
Expand Down Expand Up @@ -228,6 +230,8 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:U
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
Expand Down Expand Up @@ -255,6 +259,8 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
Expand Down Expand Up @@ -326,6 +332,8 @@ gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20250606233247-e3c4c4cad86f h1:zmc4cHEcCudRt2O8VsCW7nYLfAsbVY2i910/DAop1TM=
gvisor.dev/gvisor v0.0.0-20250606233247-e3c4c4cad86f/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
Expand Down
33 changes: 33 additions & 0 deletions option/wireguard.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,36 @@ type LegacyWireGuardPeer struct {
AllowedIPs badoption.Listable[netip.Prefix] `json:"allowed_ips,omitempty"`
Reserved []uint8 `json:"reserved,omitempty"`
}

type AmneziaWGOutboundOptions struct {
DialerOptions
SystemInterface bool `json:"system_interface,omitempty"`
InterfaceName string `json:"interface_name,omitempty"`
LocalAddress badoption.Listable[netip.Prefix] `json:"local_address"`
PrivateKey string `json:"private_key"`
Peers []LegacyWireGuardPeer `json:"peers,omitempty"`
ServerOptions
PeerPublicKey string `json:"peer_public_key"`
PreSharedKey string `json:"pre_shared_key,omitempty"`
Reserved []uint8 `json:"reserved,omitempty"`
MTU uint32 `json:"mtu,omitempty"`
Network NetworkList `json:"network,omitempty"`

// AmneziaWG obfuscation parameters.
JunkPacketCount int32 `json:"jc,omitempty"`
JunkPacketMinSize int32 `json:"jmin,omitempty"`
JunkPacketMaxSize int32 `json:"jmax,omitempty"`
InitPacketJunkSize int32 `json:"s1,omitempty"`
ResponsePacketJunkSize int32 `json:"s2,omitempty"`
CookieReplyPacketJunkSize int32 `json:"s3,omitempty"`
TransportPacketJunkSize int32 `json:"s4,omitempty"`
InitPacketMagicHeader string `json:"h1,omitempty"`
ResponsePacketMagicHeader string `json:"h2,omitempty"`
UnderloadPacketMagicHeader string `json:"h3,omitempty"`
TransportPacketMagicHeader string `json:"h4,omitempty"`
Comment on lines +79 to +82

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Magic-header fields typed as string while AmneziaWG protocol defines them as uint32

H1H4 (InitPacketMagicHeader / ResponsePacketMagicHeader / etc.) are random 32-bit integers in the AmneziaWG specification. Every existing AmneziaWG client (including the amneziawg-go reference config and NekoBox itself) represents them as integers in config. Typing them string means users must supply their values as decimal strings (e.g. "h1": "1234567890") which is inconsistent with peer clients and is likely to cause confusion or integration friction with config converters that export numeric values.

SpecialJunk1 string `json:"i1,omitempty"`
SpecialJunk2 string `json:"i2,omitempty"`
SpecialJunk3 string `json:"i3,omitempty"`
SpecialJunk4 string `json:"i4,omitempty"`
SpecialJunk5 string `json:"i5,omitempty"`
}
167 changes: 167 additions & 0 deletions protocol/amneziawg/outbound.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package amneziawg

import (
"context"
"net"
"net/netip"

"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/common/dialer"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/transport/amneziawg"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)

func RegisterOutbound(registry *outbound.Registry) {
outbound.Register[option.AmneziaWGOutboundOptions](registry, C.TypeAmneziaWG, NewOutbound)
}

type Outbound struct {
outbound.Adapter
ctx context.Context
dnsRouter adapter.DNSRouter
logger logger.ContextLogger
localAddresses []netip.Prefix
endpoint *amneziawg.Endpoint
}

func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.AmneziaWGOutboundOptions) (adapter.Outbound, error) {
outbound := &Outbound{
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeAmneziaWG, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
ctx: ctx,
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
logger: logger,
localAddresses: options.LocalAddress,
}
outboundDialer, err := dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
RemoteIsDomain: options.ServerIsDomain() || common.Any(options.Peers, func(it option.LegacyWireGuardPeer) bool {
return it.ServerIsDomain()
}),
ResolverOnDetour: true,
})
if err != nil {
return nil, err
}
peers := common.Map(options.Peers, func(it option.LegacyWireGuardPeer) amneziawg.PeerOptions {
return amneziawg.PeerOptions{
Endpoint: it.ServerOptions.Build(),
PublicKey: it.PublicKey,
PreSharedKey: it.PreSharedKey,
AllowedIPs: it.AllowedIPs,
Reserved: it.Reserved,
}
})
if len(peers) == 0 {
peers = []amneziawg.PeerOptions{{
Endpoint: options.ServerOptions.Build(),
PublicKey: options.PeerPublicKey,
PreSharedKey: options.PreSharedKey,
AllowedIPs: []netip.Prefix{netip.PrefixFrom(netip.IPv4Unspecified(), 0), netip.PrefixFrom(netip.IPv6Unspecified(), 0)},
Reserved: options.Reserved,
}}
}
wgEndpoint, err := amneziawg.NewEndpoint(amneziawg.EndpointOptions{
Context: ctx,
Logger: logger,
System: options.SystemInterface,
Dialer: outboundDialer,
CreateDialer: func(interfaceName string) N.Dialer {
return common.Must1(dialer.NewDefault(ctx, option.DialerOptions{
BindInterface: interfaceName,
}))
},
Comment on lines +78 to +82

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check CreateDialer signature and usage across the codebase
rg -n "CreateDialer" --type go -C3

Repository: hawkff/sing-box

Length of output: 5242


Panic risk in CreateDialer callback at runtime.

common.Must1 will panic if dialer.NewDefault returns an error. Since this callback is invoked at runtime (in device_system.go), a panic would crash the application rather than returning a graceful error.

The callback signature func(interfaceName string) N.Dialer provides no mechanism to propagate errors. Changing this requires updating the signature in both EndpointOptions and DeviceOptions structs, and handling errors at all call sites (including transport/amneziawg/device_system.go:39).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@protocol/amneziawg/outbound.go` around lines 78 - 82, The CreateDialer
callback uses common.Must1 which will panic at runtime if dialer.NewDefault
returns an error, crashing the application. To fix this, change the callback
signature from returning only N.Dialer to returning (N.Dialer, error) in both
the EndpointOptions and DeviceOptions structs. Then replace the common.Must1
call with proper error handling where the callback returns the dialer and any
error from dialer.NewDefault directly, and update the call site in
device_system.go that invokes this callback to properly handle the returned
error instead of allowing a panic.

Name: options.InterfaceName,
MTU: options.MTU,
Address: options.LocalAddress,
PrivateKey: options.PrivateKey,
ResolvePeer: func(domain string) (netip.Addr, error) {
endpointAddresses, lookupErr := outbound.dnsRouter.Lookup(ctx, domain, outboundDialer.(dialer.ResolveDialer).QueryOptions())
if lookupErr != nil {
return netip.Addr{}, lookupErr
}
return endpointAddresses[0], nil
},
Comment on lines +87 to +93

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unsafe type assertion may panic.

The type assertion outboundDialer.(dialer.ResolveDialer) will panic if outboundDialer does not implement dialer.ResolveDialer. Use the comma-ok idiom for safety.

🐛 Proposed fix
 		ResolvePeer: func(domain string) (netip.Addr, error) {
-			endpointAddresses, lookupErr := outbound.dnsRouter.Lookup(ctx, domain, outboundDialer.(dialer.ResolveDialer).QueryOptions())
+			resolveDialer, ok := outboundDialer.(dialer.ResolveDialer)
+			if !ok {
+				return netip.Addr{}, E.New("dialer does not support DNS resolution")
+			}
+			endpointAddresses, lookupErr := outbound.dnsRouter.Lookup(ctx, domain, resolveDialer.QueryOptions())
 			if lookupErr != nil {
 				return netip.Addr{}, lookupErr
 			}
 			return endpointAddresses[0], nil
 		},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@protocol/amneziawg/outbound.go` around lines 87 - 93, The type assertion
`outboundDialer.(dialer.ResolveDialer)` in the ResolvePeer function is unsafe
and will panic if outboundDialer does not implement the dialer.ResolveDialer
interface. Replace the direct type assertion with the comma-ok idiom by
assigning the result to two variables (value and ok boolean), check if the
assertion succeeded, and return an appropriate error if it fails before
attempting to call QueryOptions() on the asserted type.

Peers: peers,

JunkPacketCount: options.JunkPacketCount,
JunkPacketMinSize: options.JunkPacketMinSize,
JunkPacketMaxSize: options.JunkPacketMaxSize,
InitPacketJunkSize: options.InitPacketJunkSize,
ResponsePacketJunkSize: options.ResponsePacketJunkSize,
CookieReplyPacketJunkSize: options.CookieReplyPacketJunkSize,
TransportPacketJunkSize: options.TransportPacketJunkSize,
InitPacketMagicHeader: options.InitPacketMagicHeader,
ResponsePacketMagicHeader: options.ResponsePacketMagicHeader,
UnderloadPacketMagicHeader: options.UnderloadPacketMagicHeader,
TransportPacketMagicHeader: options.TransportPacketMagicHeader,
SpecialJunk1: options.SpecialJunk1,
SpecialJunk2: options.SpecialJunk2,
SpecialJunk3: options.SpecialJunk3,
SpecialJunk4: options.SpecialJunk4,
SpecialJunk5: options.SpecialJunk5,
})
if err != nil {
return nil, err
}
outbound.endpoint = wgEndpoint
return outbound, nil
}

func (o *Outbound) Start(stage adapter.StartStage) error {
switch stage {
case adapter.StartStateStart:
return o.endpoint.Start(false)
case adapter.StartStatePostStart:
return o.endpoint.Start(true)
}
return nil
}

func (o *Outbound) Close() error {
return o.endpoint.Close()
}

func (o *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
switch network {
case N.NetworkTCP:
o.logger.InfoContext(ctx, "outbound connection to ", destination)
case N.NetworkUDP:
o.logger.InfoContext(ctx, "outbound packet connection to ", destination)
}
if destination.IsFqdn() {
destinationAddresses, err := o.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
if err != nil {
return nil, err
}
return N.DialSerial(ctx, o.endpoint, network, destination, destinationAddresses)
} else if !destination.Addr.IsValid() {
return nil, E.New("invalid destination: ", destination)
}
return o.endpoint.DialContext(ctx, network, destination)
}

func (o *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
o.logger.InfoContext(ctx, "outbound packet connection to ", destination)
if destination.IsFqdn() {
destinationAddresses, err := o.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
if err != nil {
return nil, err
}
packetConn, _, err := N.ListenSerial(ctx, o.endpoint, destination, destinationAddresses)
if err != nil {
return nil, err
}
return packetConn, err
}
return o.endpoint.ListenPacket(ctx, destination)
}
Loading