Skip to content

Commit a937f03

Browse files
committed
feat: Enhance WireGuard support by rejecting legacy outbound configurations and adding validation tests
1 parent 9969a93 commit a937f03

5 files changed

Lines changed: 91 additions & 9 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
run: ./run lib core
4141
- name: WireGuard endpoint schema and box tests
4242
working-directory: libcore
43-
run: go test -v -run 'Test(WireGuardEndpointConfigsCreateAndClose|MissingWireGuardRouteTagDoesNotFallbackToDirect|LegacyWireGuardOutboundIsRejected)$' .
43+
run: go test -v -tags='with_conntrack,with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api' -run 'Test(WireGuardEndpointConfigsCreateAndClose|MissingWireGuardRouteTagDoesNotFallbackToDirect|LegacyWireGuardOutboundIsRejected)$' .
4444
- name: Verify LibCore AAR
4545
run: |
4646
test -s app/libs/libcore.aar

libcore/box.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package libcore
33
import (
44
"context"
55
"crypto/sha256"
6+
"encoding/json"
67
"errors"
78
"fmt"
89
"io"
@@ -36,6 +37,33 @@ import (
3637
var mainInstance *BoxInstance
3738
var boxInstanceSequence atomic.Uint64
3839

40+
type outboundTypeProbe struct {
41+
Outbounds []struct {
42+
Type string `json:"type"`
43+
} `json:"outbounds"`
44+
}
45+
46+
// rejectLegacyWireGuardOutbound runs before sing-box decodes registered outbound
47+
// options. The registry intentionally maps WireGuard to option.StubOptions, whose
48+
// strict decoder otherwise reports the first removed field as "unknown" before
49+
// the registry factory can return the actionable migration error.
50+
//
51+
// Only the top-level outbound type is inspected. Malformed JSON is left to the
52+
// official context-aware decoder below so this probe neither replaces schema
53+
// validation nor exposes legacy credentials in an error or diagnostic.
54+
func rejectLegacyWireGuardOutbound(config string) error {
55+
var probe outboundTypeProbe
56+
if json.Unmarshal([]byte(config), &probe) != nil {
57+
return nil
58+
}
59+
for _, outbound := range probe.Outbounds {
60+
if outbound.Type == constant.TypeWireGuard {
61+
return E.New(legacyWireGuardOutboundError)
62+
}
63+
}
64+
return nil
65+
}
66+
3967
func diagnosticTagID(tag string) string {
4068
digest := sha256.Sum256([]byte(tag))
4169
return fmt.Sprintf("%x", digest[:6])
@@ -153,7 +181,10 @@ func newSingBoxInstance(config string, localTransport LocalDNSTransport, platfor
153181

154182
// parse options
155183
var options option.Options
156-
err = options.UnmarshalJSONContext(ctx, []byte(config))
184+
err = rejectLegacyWireGuardOutbound(config)
185+
if err == nil {
186+
err = options.UnmarshalJSONContext(ctx, []byte(config))
187+
}
157188
if err != nil {
158189
if !platformLog {
159190
log.Printf("URLTestTrace goId=%d stage=parse-config failed elapsed=%s errorType=%T", diagnosticID, time.Since(createStarted), err)

libcore/box_include.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ import (
4848
E "github.com/sagernet/sing/common/exceptions"
4949
)
5050

51+
const legacyWireGuardOutboundError = "WireGuard outbound is deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, use WireGuard endpoint instead"
52+
5153
func nekoboxAndroidInboundRegistry() *inbound.Registry {
5254
registry := inbound.NewRegistry()
5355

@@ -100,7 +102,7 @@ func nekoboxAndroidOutboundRegistry() *outbound.Registry {
100102
// 见下方 nekoboxAndroidEndpointRegistry);镜像官方 include/registry.go 的 stub,
101103
// 让旧式 wireguard outbound 配置得到明确报错而非 "unknown outbound type"。
102104
outbound.Register[option.StubOptions](registry, C.TypeWireGuard, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.StubOptions) (adapter.Outbound, error) {
103-
return nil, E.New("WireGuard outbound is deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, use WireGuard endpoint instead")
105+
return nil, E.New(legacyWireGuardOutboundError)
104106
})
105107

106108
return registry

libcore/wireguard_endpoint_test.go

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,14 @@ func TestWireGuardEndpointConfigsCreateAndClose(t *testing.T) {
135135
if err != nil {
136136
t.Fatalf("create test box: %v", err)
137137
}
138+
defer func() {
139+
if closeErr := instance.Close(); closeErr != nil {
140+
t.Errorf("cleanup test box: %v", closeErr)
141+
}
142+
}()
143+
if err = instance.Start(); err != nil {
144+
t.Fatalf("start test box: %v", err)
145+
}
138146
if _, loaded := instance.Outbound().Outbound(testCase.finalTag); !loaded {
139147
t.Fatalf("final tag %q is not available through unified outbound lookup", testCase.finalTag)
140148
}
@@ -171,21 +179,43 @@ func TestWireGuardEndpointConfigsCreateAndClose(t *testing.T) {
171179

172180
func TestMissingWireGuardRouteTagDoesNotFallbackToDirect(t *testing.T) {
173181
config := testConfig("", `{"type":"direct","tag":"direct"}`, "missing-wg")
174-
if _, err := NewTestSingBoxInstance(config, nil); err == nil {
175-
t.Fatal("missing endpoint route tag was silently accepted")
182+
instance, err := NewTestSingBoxInstance(config, nil)
183+
if err != nil {
184+
t.Fatalf("create test box before route validation: %v", err)
185+
}
186+
defer func() {
187+
if closeErr := instance.Close(); closeErr != nil {
188+
t.Errorf("close test box: %v", closeErr)
189+
}
190+
}()
191+
192+
err = instance.Start()
193+
if err == nil || !strings.Contains(err.Error(), "default outbound not found: missing-wg") {
194+
t.Fatalf("expected missing endpoint route tag rejection at start, got %v", err)
195+
}
196+
if defaultOutbound := instance.Outbound().Default(); defaultOutbound != nil {
197+
t.Fatalf("missing endpoint route tag fell back to outbound %q", defaultOutbound.Tag())
176198
}
177199
}
178200

179201
func TestLegacyWireGuardOutboundIsRejected(t *testing.T) {
180-
config := fmt.Sprintf(`{
202+
cases := map[string]string{
203+
"type-only": `{"outbounds":[{"type":"wireguard","tag":"wg"}]}`,
204+
"legacy-fields": fmt.Sprintf(`{
181205
"outbounds": [{
182206
"type": "wireguard",
183207
"tag": "wg",
184208
"local_address": ["10.0.0.2/32"],
185209
"private_key": %q
186210
}]
187-
}`, testWireGuardPrivateKey)
188-
if _, err := NewTestSingBoxInstance(config, nil); err == nil || !strings.Contains(err.Error(), "use WireGuard endpoint instead") {
189-
t.Fatalf("expected explicit legacy outbound rejection, got %v", err)
211+
}`, testWireGuardPrivateKey),
212+
}
213+
214+
for name, config := range cases {
215+
t.Run(name, func(t *testing.T) {
216+
if _, err := NewTestSingBoxInstance(config, nil); err == nil || !strings.Contains(err.Error(), "use WireGuard endpoint instead") {
217+
t.Fatalf("expected explicit legacy outbound rejection, got %v", err)
218+
}
219+
})
190220
}
191221
}

tools/diagnostics/roo_check_wireguard_endpoint.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import re
34
from pathlib import Path
45

56

@@ -20,6 +21,13 @@ def reject(text: str, needle: str, description: str) -> None:
2021
raise SystemExit(f"forbidden {description}: {needle}")
2122

2223

24+
def build_tags(text: str, source: str) -> set[str]:
25+
match = re.search(r"-tags='([^']+)'", text)
26+
if match is None:
27+
raise SystemExit(f"missing Go build tags in {source}")
28+
return {tag.strip() for tag in match.group(1).split(",") if tag.strip()}
29+
30+
2331
def main() -> None:
2432
options = read("app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java")
2533
formatter = read(
@@ -32,6 +40,8 @@ def main() -> None:
3240
proxy_instance = read(
3341
"app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt"
3442
)
43+
libcore_build = read("libcore/build.sh")
44+
ci_workflow = read(".github/workflows/ci.yml")
3545

3646
require(options, "public List<SingBoxOption> endpoints;", "top-level endpoints")
3747
require(options, "class Endpoint_WireGuardOptions", "WireGuard endpoint model")
@@ -56,6 +66,15 @@ def main() -> None:
5666
reject(test_instance, "endpointTags=", "raw test endpoint tag logging")
5767
reject(proxy_instance, "endpointTags=", "raw production endpoint tag logging")
5868

69+
production_tags = build_tags(libcore_build, "libcore/build.sh")
70+
ci_test_tags = build_tags(ci_workflow, ".github/workflows/ci.yml")
71+
if ci_test_tags != production_tags:
72+
raise SystemExit(
73+
"WireGuard Go test tags differ from production libcore tags: "
74+
f"missing={sorted(production_tags - ci_test_tags)}, "
75+
f"extra={sorted(ci_test_tags - production_tags)}"
76+
)
77+
5978
diagnostic_lines = [
6079
line.strip()
6180
for source in (builder, test_instance, proxy_instance)

0 commit comments

Comments
 (0)