mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 11:15:03 +03:00
fix(outbound): keep imported links to remote-server settings
A share link describes a remote server, but extra=, fm= and the vmess JSON copy provider objects verbatim, so a link could carry keys that only make sense on the importing host. Both link parsers now drop dial-redirect and host file keys (matched case-folded, as the core binds them). Outbound subscriptions also refuse outbounds that dial this host or its network without allowPrivate, any that target the core's own API or metrics port, and any that take their server from DNS records. Rows stored earlier are filtered the same way when the config is built.
This commit is contained in:
@@ -859,12 +859,31 @@ export function parseWireguardLink(link: string): Raw | null {
|
|||||||
export function parseOutboundLink(link: string): Raw | null {
|
export function parseOutboundLink(link: string): Raw | null {
|
||||||
const trimmed = link.trim();
|
const trimmed = link.trim();
|
||||||
if (!trimmed) return null;
|
if (!trimmed) return null;
|
||||||
return (
|
const outbound =
|
||||||
parseVmessLink(trimmed) ??
|
parseVmessLink(trimmed) ??
|
||||||
parseVlessLink(trimmed) ??
|
parseVlessLink(trimmed) ??
|
||||||
parseTrojanLink(trimmed) ??
|
parseTrojanLink(trimmed) ??
|
||||||
parseShadowsocksLink(trimmed) ??
|
parseShadowsocksLink(trimmed) ??
|
||||||
parseHysteria2Link(trimmed) ??
|
parseHysteria2Link(trimmed) ??
|
||||||
parseWireguardLink(trimmed)
|
parseWireguardLink(trimmed);
|
||||||
);
|
if (outbound) dropHostLocalKeys(outbound);
|
||||||
|
return outbound;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys that redirect the core's dial or name files on the importing host; extra=, fm= and
|
||||||
|
// vmess JSON copy provider objects verbatim, and the core binds keys case-folded.
|
||||||
|
const HOST_LOCAL_KEYS = new Set(['dialerproxy', 'certificatefile', 'keyfile', 'masterkeylog']);
|
||||||
|
|
||||||
|
function dropHostLocalKeys(value: unknown): void {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach(dropHostLocalKeys);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!value || typeof value !== 'object') return;
|
||||||
|
const obj = value as Raw;
|
||||||
|
for (const key of Object.keys(obj)) {
|
||||||
|
// Upper-casing first folds the long s and the Kelvin sign the way Go's decoder does.
|
||||||
|
if (HOST_LOCAL_KEYS.has(key.toUpperCase().toLowerCase())) delete obj[key];
|
||||||
|
else dropHostLocalKeys(obj[key]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1124,3 +1124,32 @@ describe('obfs=gecko packetSize validation', () => {
|
|||||||
expect(packetSizeOf(link)).toBeUndefined();
|
expect(packetSizeOf(link)).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('parseOutboundLink — host-local keys', () => {
|
||||||
|
it('drops keys that redirect the dial or name host files, however they are cased', () => {
|
||||||
|
const extra = encodeURIComponent(
|
||||||
|
JSON.stringify({
|
||||||
|
downloadSettings: {
|
||||||
|
address: '8.8.8.8',
|
||||||
|
port: 443,
|
||||||
|
sockopt: { dialerProxy: 'api', tcpFastOpen: true },
|
||||||
|
tlsSettings: {
|
||||||
|
serverName: 'cdn.example.com',
|
||||||
|
certificates: [{ CERTIFICATEFILE: '/dev/zero', keyFile: '/etc/shadow' }],
|
||||||
|
maſterKeyLog: '/root/keys',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const fm = encodeURIComponent(
|
||||||
|
JSON.stringify({ tcp: [{ type: 'header-custom', settings: { DialerProxy: 'api' } }] }),
|
||||||
|
);
|
||||||
|
const out = parseOutboundLink(
|
||||||
|
`vless://uuid@8.8.8.8:443?type=xhttp&security=tls&extra=${extra}&fm=${fm}#node`,
|
||||||
|
);
|
||||||
|
const json = JSON.stringify(out);
|
||||||
|
expect(json).not.toMatch(/dialerproxy|certificatefile|keyfile|maſterkeylog/i);
|
||||||
|
expect(json).toContain('"tcpFastOpen":true');
|
||||||
|
expect(json).toContain('"serverName":"cdn.example.com"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -118,7 +119,15 @@ func splitLines(s string) []string {
|
|||||||
// - hysteria2:// (also hy2://)
|
// - hysteria2:// (also hy2://)
|
||||||
// - wireguard:// (also wg://)
|
// - wireguard:// (also wg://)
|
||||||
func ParseLink(link string) (*ParseResult, error) {
|
func ParseLink(link string) (*ParseResult, error) {
|
||||||
link = strings.TrimSpace(link)
|
res, err := parseLinkByScheme(strings.TrimSpace(link))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dropHostLocalKeys(res.Outbound)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLinkByScheme(link string) (*ParseResult, error) {
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(link, "vmess://"):
|
case strings.HasPrefix(link, "vmess://"):
|
||||||
return parseVmess(link)
|
return parseVmess(link)
|
||||||
@@ -137,6 +146,29 @@ func ParseLink(link string) (*ParseResult, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hostLocalKeys redirect the core's dial or name files on the importing host; extra=, fm= and
|
||||||
|
// vmess JSON copy provider keys verbatim, and the core binds keys case-folded.
|
||||||
|
var hostLocalKeys = []string{"dialerProxy", "certificateFile", "keyFile", "masterKeyLog"}
|
||||||
|
|
||||||
|
func dropHostLocalKeys(v any) {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
for key, child := range v {
|
||||||
|
if slices.ContainsFunc(hostLocalKeys, func(k string) bool { return strings.EqualFold(k, key) }) {
|
||||||
|
delete(v, key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dropHostLocalKeys(child)
|
||||||
|
}
|
||||||
|
case Outbound:
|
||||||
|
dropHostLocalKeys(map[string]any(v))
|
||||||
|
case []any:
|
||||||
|
for _, child := range v {
|
||||||
|
dropHostLocalKeys(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- vmess ---
|
// --- vmess ---
|
||||||
|
|
||||||
func parseVmess(link string) (*ParseResult, error) {
|
func parseVmess(link string) (*ParseResult, error) {
|
||||||
|
|||||||
@@ -612,3 +612,48 @@ func TestParseLink_PutsUDPHopMaskLast(t *testing.T) {
|
|||||||
t.Fatalf("imported finalmask\n got: %s\nwant: %s", got, want)
|
t.Fatalf("imported finalmask\n got: %s\nwant: %s", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A share link describes a remote server; keys that redirect the dial or name files on
|
||||||
|
// the importing host must not survive, however the core's case-folding decoder spells them.
|
||||||
|
func TestParseLinkDropsHostLocalKeys(t *testing.T) {
|
||||||
|
extra := url.QueryEscape(`{"downloadSettings":{"address":"8.8.8.8","port":443,"network":"xhttp",` +
|
||||||
|
`"sockopt":{"dialerProxy":"api","tcpFastOpen":true},` +
|
||||||
|
`"tlsSettings":{"serverName":"cdn.example.com","CERTIFICATES":[{"certificateFile":"/dev/zero","keyFile":"/etc/shadow"}],"maſterKeyLog":"/root/keys"}}}`)
|
||||||
|
fm := url.QueryEscape(`{"tcp":[{"type":"header-custom","settings":{"DialerProxy":"api"}}]}`)
|
||||||
|
res, err := ParseLink("vless://uuid@8.8.8.8:443?type=xhttp&security=tls&sni=cdn.example.com&extra=" + extra + "&fm=" + fm + "#node")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse vless: %v", err)
|
||||||
|
}
|
||||||
|
raw, err := json.Marshal(res.Outbound)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
var hostLocal []string
|
||||||
|
var walk func(v any)
|
||||||
|
walk = func(v any) {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
for k, child := range v {
|
||||||
|
for _, banned := range []string{"dialerProxy", "certificateFile", "keyFile", "masterKeyLog"} {
|
||||||
|
if strings.EqualFold(k, banned) {
|
||||||
|
hostLocal = append(hostLocal, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(child)
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
for _, child := range v {
|
||||||
|
walk(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var decoded any
|
||||||
|
_ = json.Unmarshal(raw, &decoded)
|
||||||
|
walk(decoded)
|
||||||
|
if len(hostLocal) > 0 {
|
||||||
|
t.Fatalf("outbound keeps host-local keys %v: %s", hostLocal, raw)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(raw), `"tcpFastOpen":true`) || !strings.Contains(string(raw), `"serverName":"cdn.example.com"`) {
|
||||||
|
t.Fatalf("remote-server settings were dropped too: %s", raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -423,13 +425,19 @@ func (s *OutboundSubscriptionService) fetchAndStore(sub *model.OutboundSubscript
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop core-rejected links before tagging: prevTagByIndex indexes the persisted
|
// Drop refused and core-rejected links before tagging: prevTagByIndex indexes the
|
||||||
// (filtered) list, so positions must be counted in that same list.
|
// persisted (filtered) list, so positions must be counted in that same list.
|
||||||
var droppedByCore []string
|
var dropped []string
|
||||||
|
ownPorts := xrayOwnPorts()
|
||||||
keptLinks, keptIdentities := parsed[:0], identities[:0]
|
keptLinks, keptIdentities := parsed[:0], identities[:0]
|
||||||
for i, ob := range parsed {
|
for i, ob := range parsed {
|
||||||
if _, dropped := filterOutboundsRejectedByCore(fmt.Sprintf("outbound sub %d", sub.Id), []any{map[string]any(ob)}); len(dropped) > 0 {
|
if reason := internalDialTarget(map[string]any(ob), sub.AllowPrivate, ownPorts); reason != "" {
|
||||||
droppedByCore = append(droppedByCore, dropped...)
|
logger.Warningf("outbound sub %d: dropping outbound %.64q: %s", sub.Id, ob["tag"], reason)
|
||||||
|
dropped = append(dropped, fmt.Sprintf("%.64s: %s", ob["tag"], reason))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, rejected := filterOutboundsRejectedByCore(fmt.Sprintf("outbound sub %d", sub.Id), []any{map[string]any(ob)}); len(rejected) > 0 {
|
||||||
|
dropped = append(dropped, rejected...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
keptLinks = append(keptLinks, ob)
|
keptLinks = append(keptLinks, ob)
|
||||||
@@ -461,8 +469,8 @@ func (s *OutboundSubscriptionService) fetchAndStore(sub *model.OutboundSubscript
|
|||||||
sub.LinkIdentities = string(identJSON)
|
sub.LinkIdentities = string(identJSON)
|
||||||
sub.LastUpdated = time.Now().Unix()
|
sub.LastUpdated = time.Now().Unix()
|
||||||
sub.LastError = ""
|
sub.LastError = ""
|
||||||
if len(droppedByCore) > 0 {
|
if len(dropped) > 0 {
|
||||||
sub.LastError = fmt.Sprintf("dropped %d outbound(s) the xray core rejects: %s", len(droppedByCore), droppedByCore[0])
|
sub.LastError = fmt.Sprintf("dropped %d outbound(s): %s", len(dropped), dropped[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := database.GetDB().Save(sub).Error; err != nil {
|
if err := database.GetDB().Save(sub).Error; err != nil {
|
||||||
@@ -561,6 +569,7 @@ func (s *OutboundSubscriptionService) activeOutboundsSplit() (prepend []any, app
|
|||||||
if err := db.Where("enabled = ?", true).Order("priority asc, id asc").Find(&subs).Error; err != nil {
|
if err := db.Where("enabled = ?", true).Order("priority asc, id asc").Find(&subs).Error; err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
ownPorts := xrayOwnPorts()
|
||||||
for _, sub := range subs {
|
for _, sub := range subs {
|
||||||
if strings.TrimSpace(sub.LastFetchedOutbounds) == "" {
|
if strings.TrimSpace(sub.LastFetchedOutbounds) == "" {
|
||||||
continue
|
continue
|
||||||
@@ -570,6 +579,14 @@ func (s *OutboundSubscriptionService) activeOutboundsSplit() (prepend []any, app
|
|||||||
logger.Warningf("outbound sub %d has corrupt LastFetchedOutbounds: %v", sub.Id, err)
|
logger.Warningf("outbound sub %d has corrupt LastFetchedOutbounds: %v", sub.Id, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Rows stored before the fetch refused these, or while allowPrivate was on.
|
||||||
|
arr = slices.DeleteFunc(arr, func(ob any) bool {
|
||||||
|
reason := internalDialTarget(ob, sub.AllowPrivate, ownPorts)
|
||||||
|
if reason != "" {
|
||||||
|
logger.Warningf("outbound sub %d: dropping a stored outbound: %s", sub.Id, reason)
|
||||||
|
}
|
||||||
|
return reason != ""
|
||||||
|
})
|
||||||
arr, _ = filterOutboundsRejectedByCore(fmt.Sprintf("outbound sub %d", sub.Id), arr)
|
arr, _ = filterOutboundsRejectedByCore(fmt.Sprintf("outbound sub %d", sub.Id), arr)
|
||||||
if sub.Prepend {
|
if sub.Prepend {
|
||||||
prepend = append(prepend, arr...)
|
prepend = append(prepend, arr...)
|
||||||
@@ -672,3 +689,75 @@ Consequences for balancers / routing:
|
|||||||
We deliberately do *not* mutate the saved xrayTemplateConfig. Subscription
|
We deliberately do *not* mutate the saved xrayTemplateConfig. Subscription
|
||||||
outbounds are always injected at runtime in GetXrayConfig.
|
outbounds are always injected at runtime in GetXrayConfig.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// xrayOwnPorts lists the ports of Xray's gRPC API and metrics listeners, which ask
|
||||||
|
// for no credentials: being reachable from loopback only is their whole protection.
|
||||||
|
func xrayOwnPorts() []string {
|
||||||
|
ports := []string{strconv.Itoa(reservedAPIPort())}
|
||||||
|
metrics, _ := (&XrayMetricsService{}).discoverListen()
|
||||||
|
if _, port, err := net.SplitHostPort(metrics); err == nil {
|
||||||
|
ports = append(ports, port)
|
||||||
|
}
|
||||||
|
return ports
|
||||||
|
}
|
||||||
|
|
||||||
|
// internalDialTarget says why a subscription outbound must not be loaded, or "". Keys
|
||||||
|
// match at any depth (xhttp downloadSettings is a second server) and case-folded like the core.
|
||||||
|
func internalDialTarget(outbound any, allowPrivate bool, ownPorts []string) string {
|
||||||
|
switch v := outbound.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
for key, child := range v {
|
||||||
|
text, isText := child.(string)
|
||||||
|
switch {
|
||||||
|
case strings.EqualFold(key, "port"):
|
||||||
|
// A public name may resolve to loopback by dial time, so the port alone decides.
|
||||||
|
if slices.Contains(ownPorts, fmt.Sprint(child)) {
|
||||||
|
return "its port belongs to Xray's own API or metrics listener"
|
||||||
|
}
|
||||||
|
case strings.EqualFold(key, "addressPortStrategy"):
|
||||||
|
if text != "" && !strings.EqualFold(text, "none") {
|
||||||
|
return "addressPortStrategy takes its server from DNS records"
|
||||||
|
}
|
||||||
|
case strings.EqualFold(key, "address"), strings.EqualFold(key, "endpoint"):
|
||||||
|
// WireGuard's settings.address is a list of tunnel addresses, never dialled.
|
||||||
|
if !isText {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if _, port, err := net.SplitHostPort(strings.TrimSpace(text)); err == nil && slices.Contains(ownPorts, port) {
|
||||||
|
return "its port belongs to Xray's own API or metrics listener"
|
||||||
|
}
|
||||||
|
if !allowPrivate && namesInternalHost(text) {
|
||||||
|
return fmt.Sprintf("it connects to the private/internal address %.64q and allowPrivate is off", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if reason := internalDialTarget(child, allowPrivate, ownPorts); reason != "" {
|
||||||
|
return reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
for _, child := range v {
|
||||||
|
if reason := internalDialTarget(child, allowPrivate, ownPorts); reason != "" {
|
||||||
|
return reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// namesInternalHost reports whether host, with or without a port, names this host or
|
||||||
|
// its network once xray-core and Go's dialer have read it.
|
||||||
|
func namesInternalHost(host string) bool {
|
||||||
|
host = strings.TrimSpace(host)
|
||||||
|
if h, _, err := net.SplitHostPort(host); err == nil {
|
||||||
|
host = h
|
||||||
|
}
|
||||||
|
host = strings.Trim(host, "[] ")
|
||||||
|
host, _, _ = strings.Cut(host, "%")
|
||||||
|
if ip := net.ParseIP(host); ip != nil {
|
||||||
|
return isBlockedIP(ip)
|
||||||
|
}
|
||||||
|
// An empty host is the local system and a name without a dot comes from
|
||||||
|
// /etc/hosts or the search domain ("env:" has none either).
|
||||||
|
host = strings.ToLower(strings.TrimSuffix(host, "."))
|
||||||
|
return !strings.Contains(host, ".") || host == "localhost" || strings.HasSuffix(host, ".localhost")
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -453,3 +454,53 @@ func outboundsContainTag(outbounds []any, tag string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The core dials a subscription outbound's server itself, so a provider must not point
|
||||||
|
// one at Xray's credential-less API, whatever allowPrivate says, nor steer the dial past the check.
|
||||||
|
func TestOutboundSubscriptionRefreshDropsLinksIntoXrayListeners(t *testing.T) {
|
||||||
|
setupSettingTestDB(t)
|
||||||
|
const uuid = "00000000-0000-4000-8000-000000000000"
|
||||||
|
srv := url.QueryEscape(`{"downloadSettings":{"address":"8.8.8.8","port":443,"network":"xhttp","sockopt":{"addressPortStrategy":"SrvPortAndAddress"}}}`)
|
||||||
|
subID := serveOutboundSubscription(t, "p-", func(int) string {
|
||||||
|
return "vless://" + uuid + "@127.0.0.1:62789?security=tls&type=tcp#Api\n" +
|
||||||
|
"vless://" + uuid + "@8.8.8.8:443?security=tls&type=tcp#B\n" +
|
||||||
|
"vless://" + uuid + "@8.8.8.8:443?security=tls&type=xhttp&extra=" + srv + "#Srv"
|
||||||
|
})
|
||||||
|
tags, _ := refreshOutboundTags(t, subID)
|
||||||
|
if !slices.Equal(tags, []string{"p-b"}) {
|
||||||
|
t.Fatalf("stored tags = %v, want only p-b", tags)
|
||||||
|
}
|
||||||
|
sub, err := (&OutboundSubscriptionService{}).Get(subID)
|
||||||
|
if err != nil || !strings.Contains(sub.LastError, "dropped 2 outbound(s)") {
|
||||||
|
t.Fatalf("lastError = %q (%v), want the two refused links reported", sub.LastError, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActiveOutboundsDropStoredPrivateDialTargets(t *testing.T) {
|
||||||
|
setupSettingTestDB(t)
|
||||||
|
stored := `[{"tag":"p-lan","protocol":"vless","settings":{"address":"10.0.0.7","port":443,"id":"00000000-0000-4000-8000-000000000000","encryption":"none"},"streamSettings":{"network":"tcp","security":"tls"}},` +
|
||||||
|
`{"tag":"p-pub","protocol":"vless","settings":{"address":"8.8.8.8","port":443,"id":"00000000-0000-4000-8000-000000000000","encryption":"none"},"streamSettings":{"network":"tcp","security":"tls"}}]`
|
||||||
|
sub := &model.OutboundSubscription{Url: "https://provider.example/sub", TagPrefix: "p-", Enabled: true, LastFetchedOutbounds: stored}
|
||||||
|
if err := database.GetDB().Create(sub).Error; err != nil {
|
||||||
|
t.Fatalf("seed subscription: %v", err)
|
||||||
|
}
|
||||||
|
obs, err := (&OutboundSubscriptionService{}).AllActiveOutbounds()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AllActiveOutbounds: %v", err)
|
||||||
|
}
|
||||||
|
if outboundsContainTag(obs, "p-lan") || !outboundsContainTag(obs, "p-pub") {
|
||||||
|
t.Fatalf("active outbounds = %v, want the stored LAN target dropped and the public one kept", obs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamesInternalHost(t *testing.T) {
|
||||||
|
for host, want := range map[string]bool{
|
||||||
|
"127.0.0.1": true, " [::1]:62789 ": true, "fe80::1%eth0": true, "10.1.2.3:80": true,
|
||||||
|
"localhost": true, "api.localhost.": true, "intranet": true, "": true, "env:HOME": true,
|
||||||
|
"8.8.8.8": false, "cdn.example.com": false, "[2001:4860:4860::8888]:443": false,
|
||||||
|
} {
|
||||||
|
if got := namesInternalHost(host); got != want {
|
||||||
|
t.Errorf("namesInternalHost(%q) = %v, want %v", host, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user