Skip to content

Commit 9283cb0

Browse files
committed
feat: add loopback-address support for tun
1 parent ae7967f commit 9283cb0

File tree

9 files changed

+82
-111
lines changed

9 files changed

+82
-111
lines changed

common/structure/structure_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,20 +239,23 @@ func (n *num) UnmarshalText(text []byte) (err error) {
239239

240240
func TestStructure_TextUnmarshaller(t *testing.T) {
241241
rawMap := map[string]any{
242-
"num": "255",
243-
"num_p": "127",
242+
"num": "255",
243+
"num_p": "127",
244+
"num_arr": []string{"1", "2", "3"},
244245
}
245246

246247
s := &struct {
247-
Num num `test:"num"`
248-
NumP *num `test:"num_p"`
248+
Num num `test:"num"`
249+
NumP *num `test:"num_p"`
250+
NumArr []num `test:"num_arr"`
249251
}{}
250252

251253
err := decoder.Decode(rawMap, s)
252254
assert.Nil(t, err)
253255
assert.Equal(t, 255, s.Num.a)
254256
assert.NotNil(t, s.NumP)
255257
assert.Equal(t, s.NumP.a, 127)
258+
assert.Equal(t, s.NumArr, []num{{1}, {2}, {3}})
256259

257260
// test WeaklyTypedInput
258261
rawMap["num"] = 256

config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ type RawTun struct {
270270
AutoRedirect bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"`
271271
AutoRedirectInputMark uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"`
272272
AutoRedirectOutputMark uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"`
273+
LoopbackAddress []netip.Addr `yaml:"loopback-address" json:"loopback-address,omitempty"`
273274
StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"`
274275
RouteAddress []netip.Prefix `yaml:"route-address" json:"route-address,omitempty"`
275276
RouteAddressSet []string `yaml:"route-address-set" json:"route-address-set,omitempty"`
@@ -1559,6 +1560,7 @@ func parseTun(rawTun RawTun, general *General) error {
15591560
AutoRedirect: rawTun.AutoRedirect,
15601561
AutoRedirectInputMark: rawTun.AutoRedirectInputMark,
15611562
AutoRedirectOutputMark: rawTun.AutoRedirectOutputMark,
1563+
LoopbackAddress: rawTun.LoopbackAddress,
15621564
StrictRoute: rawTun.StrictRoute,
15631565
RouteAddress: rawTun.RouteAddress,
15641566
RouteAddressSet: rawTun.RouteAddressSet,

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ require (
3131
github.com/metacubex/sing-shadowsocks v0.2.11-0.20250531133822-e545de386d4c
3232
github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250531133559-f4d53bd59335
3333
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
34-
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c
34+
github.com/metacubex/sing-tun v0.4.7-0.20250611091011-60774779fdd8
3535
github.com/metacubex/sing-vmess v0.2.2
3636
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f
3737
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250531133559-f4d53bd59335 h1:n
128128
github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250531133559-f4d53bd59335/go.mod h1:WP8+S0kqtnSbX1vlIpo5i8Irm/ijZITEPBcZ26B5unY=
129129
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
130130
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
131-
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c h1:Y6jk7AH5BEg9Dsvczrf/KokYsvxeKSZZlCLHg+hC4ro=
132-
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c/go.mod h1:HDaHDL6onAX2ZGbAGUXKp++PohRdNb7Nzt6zxzhox+U=
131+
github.com/metacubex/sing-tun v0.4.7-0.20250611091011-60774779fdd8 h1:4zWKqxTx75TbfW2EmlQ3hxM6RTRg2PYOAVMCnU4I61I=
132+
github.com/metacubex/sing-tun v0.4.7-0.20250611091011-60774779fdd8/go.mod h1:2YywXPWW8Z97kTH7RffOeykKzU+l0aiKlglWV1PAS64=
133133
github.com/metacubex/sing-vmess v0.2.2 h1:nG6GIKF1UOGmlzs+BIetdGHkFZ20YqFVIYp5Htqzp+4=
134134
github.com/metacubex/sing-vmess v0.2.2/go.mod h1:CVDNcdSLVYFgTHQlubr88d8CdqupAUDqLjROos+H9xk=
135135
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=

hub/route/configs.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ type tunSchema struct {
7575
AutoRedirect *bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"`
7676
AutoRedirectInputMark *uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"`
7777
AutoRedirectOutputMark *uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"`
78+
LoopbackAddress *[]netip.Addr `yaml:"loopback-address" json:"loopback-address,omitempty"`
7879
StrictRoute *bool `yaml:"strict-route" json:"strict-route,omitempty"`
7980
RouteAddress *[]netip.Prefix `yaml:"route-address" json:"route-address,omitempty"`
8081
RouteAddressSet *[]string `yaml:"route-address-set" json:"route-address-set,omitempty"`
@@ -174,6 +175,9 @@ func pointerOrDefaultTun(p *tunSchema, def LC.Tun) LC.Tun {
174175
if p.AutoRedirectOutputMark != nil {
175176
def.AutoRedirectOutputMark = *p.AutoRedirectOutputMark
176177
}
178+
if p.LoopbackAddress != nil {
179+
def.LoopbackAddress = *p.LoopbackAddress
180+
}
177181
if p.StrictRoute != nil {
178182
def.StrictRoute = *p.StrictRoute
179183
}

listener/config/tun.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,6 @@ import (
99
"golang.org/x/exp/slices"
1010
)
1111

12-
func StringSliceToNetipPrefixSlice(ss []string) ([]netip.Prefix, error) {
13-
lps := make([]netip.Prefix, 0, len(ss))
14-
for _, s := range ss {
15-
prefix, err := netip.ParsePrefix(s)
16-
if err != nil {
17-
return nil, err
18-
}
19-
lps = append(lps, prefix)
20-
}
21-
return lps, nil
22-
}
23-
2412
type Tun struct {
2513
Enable bool `yaml:"enable" json:"enable"`
2614
Device string `yaml:"device" json:"device"`
@@ -39,6 +27,7 @@ type Tun struct {
3927
AutoRedirect bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"`
4028
AutoRedirectInputMark uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"`
4129
AutoRedirectOutputMark uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"`
30+
LoopbackAddress []netip.Addr `yaml:"loopback-address" json:"loopback-address,omitempty"`
4231
StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"`
4332
RouteAddress []netip.Prefix `yaml:"route-address" json:"route-address,omitempty"`
4433
RouteAddressSet []string `yaml:"route-address-set" json:"route-address-set,omitempty"`
@@ -142,6 +131,9 @@ func (t *Tun) Equal(other Tun) bool {
142131
if t.AutoRedirectOutputMark != other.AutoRedirectOutputMark {
143132
return false
144133
}
134+
if !slices.Equal(t.RouteAddress, other.RouteAddress) {
135+
return false
136+
}
145137
if t.StrictRoute != other.StrictRoute {
146138
return false
147139
}

listener/inbound/tun.go

Lines changed: 59 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package inbound
22

33
import (
4-
"errors"
5-
"strings"
4+
"encoding"
5+
"net/netip"
66

77
C "github.com/metacubex/mihomo/constant"
88
LC "github.com/metacubex/mihomo/listener/config"
@@ -12,50 +12,55 @@ import (
1212

1313
type TunOption struct {
1414
BaseOption
15-
Device string `inbound:"device,omitempty"`
16-
Stack string `inbound:"stack,omitempty"`
17-
DNSHijack []string `inbound:"dns-hijack,omitempty"`
18-
AutoRoute bool `inbound:"auto-route,omitempty"`
19-
AutoDetectInterface bool `inbound:"auto-detect-interface,omitempty"`
20-
21-
MTU uint32 `inbound:"mtu,omitempty"`
22-
GSO bool `inbound:"gso,omitempty"`
23-
GSOMaxSize uint32 `inbound:"gso-max-size,omitempty"`
24-
Inet4Address []string `inbound:"inet4-address,omitempty"`
25-
Inet6Address []string `inbound:"inet6-address,omitempty"`
26-
IPRoute2TableIndex int `inbound:"iproute2-table-index,omitempty"`
27-
IPRoute2RuleIndex int `inbound:"iproute2-rule-index,omitempty"`
28-
AutoRedirect bool `inbound:"auto-redirect,omitempty"`
29-
AutoRedirectInputMark uint32 `inbound:"auto-redirect-input-mark,omitempty"`
30-
AutoRedirectOutputMark uint32 `inbound:"auto-redirect-output-mark,omitempty"`
31-
StrictRoute bool `inbound:"strict-route,omitempty"`
32-
RouteAddress []string `inbound:"route-address,omitempty"`
33-
RouteAddressSet []string `inbound:"route-address-set,omitempty"`
34-
RouteExcludeAddress []string `inbound:"route-exclude-address,omitempty"`
35-
RouteExcludeAddressSet []string `inbound:"route-exclude-address-set,omitempty"`
36-
IncludeInterface []string `inbound:"include-interface,omitempty"`
37-
ExcludeInterface []string `inbound:"exclude-interface,omitempty"`
38-
IncludeUID []uint32 `inbound:"include-uid,omitempty"`
39-
IncludeUIDRange []string `inbound:"include-uid-range,omitempty"`
40-
ExcludeUID []uint32 `inbound:"exclude-uid,omitempty"`
41-
ExcludeUIDRange []string `inbound:"exclude-uid-range,omitempty"`
42-
ExcludeSrcPort []uint16 `inbound:"exclude-src-port,omitempty"`
43-
ExcludeSrcPortRange []string `inbound:"exclude-src-port-range,omitempty"`
44-
ExcludeDstPort []uint16 `inbound:"exclude-dst-port,omitempty"`
45-
ExcludeDstPortRange []string `inbound:"exclude-dst-port-range,omitempty"`
46-
IncludeAndroidUser []int `inbound:"include-android-user,omitempty"`
47-
IncludePackage []string `inbound:"include-package,omitempty"`
48-
ExcludePackage []string `inbound:"exclude-package,omitempty"`
49-
EndpointIndependentNat bool `inbound:"endpoint-independent-nat,omitempty"`
50-
UDPTimeout int64 `inbound:"udp-timeout,omitempty"`
51-
FileDescriptor int `inbound:"file-descriptor,omitempty"`
52-
53-
Inet4RouteAddress []string `inbound:"inet4-route-address,omitempty"`
54-
Inet6RouteAddress []string `inbound:"inet6-route-address,omitempty"`
55-
Inet4RouteExcludeAddress []string `inbound:"inet4-route-exclude-address,omitempty"`
56-
Inet6RouteExcludeAddress []string `inbound:"inet6-route-exclude-address,omitempty"`
15+
Device string `inbound:"device,omitempty"`
16+
Stack C.TUNStack `inbound:"stack,omitempty"`
17+
DNSHijack []string `inbound:"dns-hijack,omitempty"`
18+
AutoRoute bool `inbound:"auto-route,omitempty"`
19+
AutoDetectInterface bool `inbound:"auto-detect-interface,omitempty"`
20+
21+
MTU uint32 `inbound:"mtu,omitempty"`
22+
GSO bool `inbound:"gso,omitempty"`
23+
GSOMaxSize uint32 `inbound:"gso-max-size,omitempty"`
24+
Inet4Address []netip.Prefix `inbound:"inet4-address,omitempty"`
25+
Inet6Address []netip.Prefix `inbound:"inet6-address,omitempty"`
26+
IPRoute2TableIndex int `inbound:"iproute2-table-index,omitempty"`
27+
IPRoute2RuleIndex int `inbound:"iproute2-rule-index,omitempty"`
28+
AutoRedirect bool `inbound:"auto-redirect,omitempty"`
29+
AutoRedirectInputMark uint32 `inbound:"auto-redirect-input-mark,omitempty"`
30+
AutoRedirectOutputMark uint32 `inbound:"auto-redirect-output-mark,omitempty"`
31+
LoopbackAddress []netip.Addr `inbound:"loopback-address,omitempty"`
32+
StrictRoute bool `inbound:"strict-route,omitempty"`
33+
RouteAddress []netip.Prefix `inbound:"route-address,omitempty"`
34+
RouteAddressSet []string `inbound:"route-address-set,omitempty"`
35+
RouteExcludeAddress []netip.Prefix `inbound:"route-exclude-address,omitempty"`
36+
RouteExcludeAddressSet []string `inbound:"route-exclude-address-set,omitempty"`
37+
IncludeInterface []string `inbound:"include-interface,omitempty"`
38+
ExcludeInterface []string `inbound:"exclude-interface,omitempty"`
39+
IncludeUID []uint32 `inbound:"include-uid,omitempty"`
40+
IncludeUIDRange []string `inbound:"include-uid-range,omitempty"`
41+
ExcludeUID []uint32 `inbound:"exclude-uid,omitempty"`
42+
ExcludeUIDRange []string `inbound:"exclude-uid-range,omitempty"`
43+
ExcludeSrcPort []uint16 `inbound:"exclude-src-port,omitempty"`
44+
ExcludeSrcPortRange []string `inbound:"exclude-src-port-range,omitempty"`
45+
ExcludeDstPort []uint16 `inbound:"exclude-dst-port,omitempty"`
46+
ExcludeDstPortRange []string `inbound:"exclude-dst-port-range,omitempty"`
47+
IncludeAndroidUser []int `inbound:"include-android-user,omitempty"`
48+
IncludePackage []string `inbound:"include-package,omitempty"`
49+
ExcludePackage []string `inbound:"exclude-package,omitempty"`
50+
EndpointIndependentNat bool `inbound:"endpoint-independent-nat,omitempty"`
51+
UDPTimeout int64 `inbound:"udp-timeout,omitempty"`
52+
FileDescriptor int `inbound:"file-descriptor,omitempty"`
53+
54+
Inet4RouteAddress []netip.Prefix `inbound:"inet4-route-address,omitempty"`
55+
Inet6RouteAddress []netip.Prefix `inbound:"inet6-route-address,omitempty"`
56+
Inet4RouteExcludeAddress []netip.Prefix `inbound:"inet4-route-exclude-address,omitempty"`
57+
Inet6RouteExcludeAddress []netip.Prefix `inbound:"inet6-route-exclude-address,omitempty"`
5758
}
5859

60+
var _ encoding.TextUnmarshaler = (*netip.Addr)(nil) // ensure netip.Addr can decode direct by structure package
61+
var _ encoding.TextUnmarshaler = (*netip.Prefix)(nil) // ensure netip.Prefix can decode direct by structure package
62+
var _ encoding.TextUnmarshaler = (*C.TUNStack)(nil) // ensure C.TUNStack can decode direct by structure package
63+
5964
func (o TunOption) Equal(config C.InboundConfig) bool {
6065
return optionToString(o) == optionToString(config)
6166
}
@@ -72,68 +77,31 @@ func NewTun(options *TunOption) (*Tun, error) {
7277
if err != nil {
7378
return nil, err
7479
}
75-
stack, exist := C.StackTypeMapping[strings.ToLower(options.Stack)]
76-
if !exist {
77-
return nil, errors.New("invalid tun stack")
78-
}
79-
80-
routeAddress, err := LC.StringSliceToNetipPrefixSlice(options.RouteAddress)
81-
if err != nil {
82-
return nil, err
83-
}
84-
routeExcludeAddress, err := LC.StringSliceToNetipPrefixSlice(options.RouteExcludeAddress)
85-
if err != nil {
86-
return nil, err
87-
}
88-
89-
inet4Address, err := LC.StringSliceToNetipPrefixSlice(options.Inet4Address)
90-
if err != nil {
91-
return nil, err
92-
}
93-
inet6Address, err := LC.StringSliceToNetipPrefixSlice(options.Inet6Address)
94-
if err != nil {
95-
return nil, err
96-
}
97-
inet4RouteAddress, err := LC.StringSliceToNetipPrefixSlice(options.Inet4RouteAddress)
98-
if err != nil {
99-
return nil, err
100-
}
101-
inet6RouteAddress, err := LC.StringSliceToNetipPrefixSlice(options.Inet6RouteAddress)
102-
if err != nil {
103-
return nil, err
104-
}
105-
inet4RouteExcludeAddress, err := LC.StringSliceToNetipPrefixSlice(options.Inet4RouteExcludeAddress)
106-
if err != nil {
107-
return nil, err
108-
}
109-
inet6RouteExcludeAddress, err := LC.StringSliceToNetipPrefixSlice(options.Inet6RouteExcludeAddress)
110-
if err != nil {
111-
return nil, err
112-
}
11380
return &Tun{
11481
Base: base,
11582
config: options,
11683
tun: LC.Tun{
11784
Enable: true,
11885
Device: options.Device,
119-
Stack: stack,
86+
Stack: options.Stack,
12087
DNSHijack: options.DNSHijack,
12188
AutoRoute: options.AutoRoute,
12289
AutoDetectInterface: options.AutoDetectInterface,
12390
MTU: options.MTU,
12491
GSO: options.GSO,
12592
GSOMaxSize: options.GSOMaxSize,
126-
Inet4Address: inet4Address,
127-
Inet6Address: inet6Address,
93+
Inet4Address: options.Inet4Address,
94+
Inet6Address: options.Inet6Address,
12895
IPRoute2TableIndex: options.IPRoute2TableIndex,
12996
IPRoute2RuleIndex: options.IPRoute2RuleIndex,
13097
AutoRedirect: options.AutoRedirect,
13198
AutoRedirectInputMark: options.AutoRedirectInputMark,
13299
AutoRedirectOutputMark: options.AutoRedirectOutputMark,
100+
LoopbackAddress: options.LoopbackAddress,
133101
StrictRoute: options.StrictRoute,
134-
RouteAddress: routeAddress,
102+
RouteAddress: options.RouteAddress,
135103
RouteAddressSet: options.RouteAddressSet,
136-
RouteExcludeAddress: routeExcludeAddress,
104+
RouteExcludeAddress: options.RouteExcludeAddress,
137105
RouteExcludeAddressSet: options.RouteExcludeAddressSet,
138106
IncludeInterface: options.IncludeInterface,
139107
ExcludeInterface: options.ExcludeInterface,
@@ -152,10 +120,10 @@ func NewTun(options *TunOption) (*Tun, error) {
152120
UDPTimeout: options.UDPTimeout,
153121
FileDescriptor: options.FileDescriptor,
154122

155-
Inet4RouteAddress: inet4RouteAddress,
156-
Inet6RouteAddress: inet6RouteAddress,
157-
Inet4RouteExcludeAddress: inet4RouteExcludeAddress,
158-
Inet6RouteExcludeAddress: inet6RouteExcludeAddress,
123+
Inet4RouteAddress: options.Inet4RouteAddress,
124+
Inet6RouteAddress: options.Inet6RouteAddress,
125+
Inet4RouteExcludeAddress: options.Inet4RouteExcludeAddress,
126+
Inet6RouteExcludeAddress: options.Inet6RouteExcludeAddress,
159127
},
160128
}, nil
161129
}

listener/parse.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func ParseListener(mapping map[string]any) (C.InboundListener, error) {
6464
listener, err = IN.NewTunnel(tunnelOption)
6565
case "tun":
6666
tunOption := &IN.TunOption{
67-
Stack: C.TunGvisor.String(),
67+
Stack: C.TunGvisor,
6868
DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query
6969
}
7070
err = decoder.Decode(mapping, tunOption)

listener/sing_tun/server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,8 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis
347347
IPRoute2RuleIndex: ruleIndex,
348348
AutoRedirectInputMark: inputMark,
349349
AutoRedirectOutputMark: outputMark,
350+
Inet4LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is4),
351+
Inet6LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is6),
350352
StrictRoute: options.StrictRoute,
351353
Inet4RouteAddress: inet4RouteAddress,
352354
Inet6RouteAddress: inet6RouteAddress,

0 commit comments

Comments
 (0)