-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add AmneziaWG outbound #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 🤖 Prompt for AI Agents |
||
| github.com/anytls/sing-anytls v0.0.11 | ||
| github.com/caddyserver/certmagic v0.23.0 | ||
| github.com/coder/websocket v1.8.13 | ||
|
|
@@ -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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| 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"` | ||
| } | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check CreateDialer signature and usage across the codebase
rg -n "CreateDialer" --type go -C3Repository: hawkff/sing-box Length of output: 5242 Panic risk in CreateDialer callback at runtime.
The callback signature 🤖 Prompt for AI Agents |
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unsafe type assertion may panic. The type assertion 🐛 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 |
||
| 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) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.