RKNnoVPN/runtime/internal/core/scripts_test.go
loop-uh 2ccfe8ce0a
Some checks failed
RKNnoVPN Linux and Android CI / policy-and-go (push) Successful in 14s
RKNnoVPN Linux and Android CI / android-debug (push) Successful in 2m5s
Full build and Forgejo release / Privacy Lint (push) Successful in 3s
Full build and Forgejo release / Local-first Runtime Guardrail (push) Successful in 2s
Full build and Forgejo release / Go Tests (push) Successful in 13s
Full build and Forgejo release / Resolve sing-box release (push) Successful in 1s
Full build and Forgejo release / Resolve Xray-core release (push) Successful in 1s
Full build and Forgejo release / Android Guardrails & Tests (push) Failing after 16s
Full build and Forgejo release / Build APK (push) Has been skipped
Full build and Forgejo release / Build Runtime CLI (arm64) (push) Successful in 11s
Full build and Forgejo release / Build Runtime CLI (armv7) (push) Successful in 11s
Full build and Forgejo release / Build sing-box (arm64) (push) Failing after 2s
Full build and Forgejo release / Build sing-box (armv7) (push) Failing after 2s
Full build and Forgejo release / Build Xray-core (arm64) (push) Failing after 3s
Full build and Forgejo release / Build Xray-core (armv7) (push) Failing after 2s
Full build and Forgejo release / Build Magisk Module (push) Has been skipped
Full build and Forgejo release / Create Release (push) Has been skipped
Перенести выпуск RKNnoVPN на Forgejo
2026-08-07 03:08:36 +03:00

667 lines
24 KiB
Go

package core
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"git.zapret.moe/zapretdiscordyoutube/RKNnoVPN/runtime/internal/config"
)
func TestParsePackagesListUIDs(t *testing.T) {
uids, err := parsePackagesListUIDs(`
com.example.app 10123 0 /data/user/0/com.example.app default 3003
bad-line
com.example.other not-a-uid
`)
if err != nil {
t.Fatal(err)
}
if uids["com.example.app"] != 10123 {
t.Fatalf("expected parsed UID, got %#v", uids)
}
if _, ok := uids["com.example.other"]; ok {
t.Fatalf("invalid UID entry should be ignored: %#v", uids)
}
}
func TestParseCmdPackageUIDs(t *testing.T) {
uids, err := parseCmdPackageUIDs(`
package:com.example.app uid:10123
package:com.example.other uid:10124
`)
if err != nil {
t.Fatal(err)
}
if uids["com.example.app"] != 10123 || uids["com.example.other"] != 10124 {
t.Fatalf("unexpected cmd package parse result: %#v", uids)
}
}
func TestResolvePackageUIDsFallsBackWhenPackagesListMissing(t *testing.T) {
withPackageResolverTestEnv(t, "", func(asShell bool) (string, error) {
if asShell {
return "", errors.New("shell fallback should not be needed")
}
return "package:com.example.app uid:10123", nil
})
result := ResolvePackageUIDsDetailed([]string{"com.example.app"})
if result.Source != "cmd_package" {
t.Fatalf("expected cmd_package fallback, got %#v", result)
}
if result.UIDString != "10123" {
t.Fatalf("expected resolved UID, got %#v", result)
}
}
func TestResolvePackageUIDsFallsBackToShellCommand(t *testing.T) {
withPackageResolverTestEnv(t, "", func(asShell bool) (string, error) {
if !asShell {
return "", errors.New("cmd denied")
}
return "package:com.example.app uid:10123", nil
})
result := ResolvePackageUIDsDetailed([]string{"com.example.app"})
if result.Source != "cmd_package_shell" {
t.Fatalf("expected cmd_package_shell fallback, got %#v", result)
}
if result.UIDString != "10123" {
t.Fatalf("expected resolved UID, got %#v", result)
}
}
func TestResolvePackageUIDsFallsBackWhenPackagesListIsStale(t *testing.T) {
withPackageResolverTestEnv(t, "com.example.old 10111 0 /data/user/0/com.example.old default\n", func(asShell bool) (string, error) {
if asShell {
return "", errors.New("shell fallback should not be needed")
}
return "package:com.example.app uid:10123", nil
})
result := ResolvePackageUIDsDetailed([]string{"com.example.app"})
if result.Source != "cmd_package" {
t.Fatalf("expected stale packages.list to fall back to cmd_package, got %#v", result)
}
if result.UIDString != "10123" || len(result.UnresolvedPackages) != 0 {
t.Fatalf("expected resolved package, got %#v", result)
}
}
func TestResolvePackageUIDsExpandsAndroidUsers(t *testing.T) {
withPackageResolverTestEnv(t, "com.example.app 10123 0 /data/user/0/com.example.app default\n", func(bool) (string, error) {
return "", errors.New("fallback should not be needed")
})
if err := os.Mkdir(filepath.Join(dataUserPath, "10"), 0755); err != nil {
t.Fatal(err)
}
result := ResolvePackageUIDsDetailed([]string{"com.example.app"})
if result.UIDString != "10123 1010123" {
t.Fatalf("expected user 0 and user 10 UIDs, got %#v", result)
}
}
func TestResolvePackageUIDsReportsUnresolvedPackages(t *testing.T) {
withPackageResolverTestEnv(t, "com.example.other 10124 0 /data/user/0/com.example.other default\n", func(bool) (string, error) {
return "", errors.New("cmd unavailable")
})
result := ResolvePackageUIDsDetailed([]string{"com.example.missing"})
if result.UIDString != "" {
t.Fatalf("missing package should not resolve UIDs, got %#v", result)
}
if strings.Join(result.UnresolvedPackages, ",") != "com.example.missing" {
t.Fatalf("expected unresolved package report, got %#v", result)
}
}
func TestBuildAppRoutingEnvModes(t *testing.T) {
withPackageResolverTestEnv(t, "com.example.app 10123 0 /data/user/0/com.example.app default\n", func(bool) (string, error) {
return "", errors.New("fallback should not be needed")
})
whitelist := BuildAppRoutingEnv("whitelist", []string{"com.example.app"}, nil, false)
if whitelist.AppMode != "whitelist" || whitelist.ProxyUIDs != "10123" || whitelist.DirectUIDs != "" || whitelist.DNSScope != "uids" || whitelist.DNSMode != "per_uid" {
t.Fatalf("unexpected whitelist env: %#v", whitelist)
}
blacklist := BuildAppRoutingEnv("blacklist", []string{"com.example.app"}, nil, false)
if blacklist.AppMode != "blacklist" || blacklist.DirectUIDs != "10123" || blacklist.ProxyUIDs != "" || blacklist.DNSScope != "all_except_uids" || blacklist.DNSMode != "per_uid" {
t.Fatalf("unexpected blacklist env: %#v", blacklist)
}
all := BuildAppRoutingEnv("all", []string{"com.example.app"}, nil, false)
if all.AppMode != "all" || all.ProxyUIDs != "" || all.DirectUIDs != "" || all.DNSScope != "all" || all.DNSMode != "all" {
t.Fatalf("unexpected all env: %#v", all)
}
off := BuildAppRoutingEnv("off", []string{"com.example.app"}, nil, false)
if off.AppMode != "off" || off.ProxyUIDs != "" || off.DirectUIDs != "" || off.DNSScope != "off" || off.DNSMode != "off" {
t.Fatalf("unexpected off env: %#v", off)
}
}
func TestBuildAppRoutingEnvGlobalKeepsBuiltInRussianAppsDirect(t *testing.T) {
withPackageResolverTestEnv(t, `
com.example.app 10123 0 /data/user/0/com.example.app default
com.yandex.browser 10130 0 /data/user/0/com.yandex.browser default
ru.yandex.yandexmaps 10131 0 /data/user/0/ru.yandex.yandexmaps default
com.vkontakte.android 10132 0 /data/user/0/com.vkontakte.android default
ru.mts.mymts 10133 0 /data/user/0/ru.mts.mymts default
ru.sberbankmobile 10134 0 /data/user/0/ru.sberbankmobile default
com.idamob.tinkoff.android 10135 0 /data/user/0/com.idamob.tinkoff.android default
com.vk.vkvideo 10136 0 /data/user/0/com.vk.vkvideo default
com.wildberries.ru 10137 0 /data/user/0/com.wildberries.ru default
ru.kinopoisk 10138 0 /data/user/0/ru.kinopoisk default
ru.ozon.app.android 10139 0 /data/user/0/ru.ozon.app.android default
ru.sbcs.store 10140 0 /data/user/0/ru.sbcs.store default
ru.vk.store 10141 0 /data/user/0/ru.vk.store default
ru.vtb24.mobilebanking.android 10142 0 /data/user/0/ru.vtb24.mobilebanking.android default
ru.yandex.music 10143 0 /data/user/0/ru.yandex.music default
com.avito.android 10144 0 /data/user/0/com.avito.android default
ru.alfabank.mobile.android 10145 0 /data/user/0/ru.alfabank.mobile.android default
ru.dublgis.dgismobile 10146 0 /data/user/0/ru.dublgis.dgismobile default
ru.megamarket.marketplace 10147 0 /data/user/0/ru.megamarket.marketplace default
ru.ok.android 10148 0 /data/user/0/ru.ok.android default
ru.oneme.app 10149 0 /data/user/0/ru.oneme.app default
rtb.mobile.android 10150 0 /data/user/0/rtb.mobile.android default
com.uma.musicvk 10151 0 /data/user/0/com.uma.musicvk default
com.allgoritm.youla 10152 0 /data/user/0/com.allgoritm.youla default
ru.tander.magnit 10153 0 /data/user/0/ru.tander.magnit default
ru.perekrestok.app 10154 0 /data/user/0/ru.perekrestok.app default
ru.beru.android 10155 0 /data/user/0/ru.beru.android default
ru.foodfox.client 10156 0 /data/user/0/ru.foodfox.client default
ru.mail.mailapp 10157 0 /data/user/0/ru.mail.mailapp default
ru.megafon.mlk 10158 0 /data/user/0/ru.megafon.mlk default
ru.nspk.mirpay 10159 0 /data/user/0/ru.nspk.mirpay default
ru.yandex.taxi 10160 0 /data/user/0/ru.yandex.taxi default
ru.rostel 10161 0 /data/user/0/ru.rostel default
ru.zen.android 10162 0 /data/user/0/ru.zen.android default
com.programmisty.emiasapp 10163 0 /data/user/0/com.programmisty.emiasapp default
com.edadeal.android 10164 0 /data/user/0/com.edadeal.android default
com.v2raytun.android 10165 0 /data/user/0/com.v2raytun.android default
com.happproxy 10166 0 /data/user/0/com.happproxy default
org.amnezia.awg 10167 0 /data/user/0/org.amnezia.awg default
ang.hiddify.com 10168 0 /data/user/0/ang.hiddify.com default
`, func(bool) (string, error) {
return "", errors.New("fallback should not be needed")
})
env := BuildAppRoutingEnv("all", nil, nil, false)
if env.AppMode != "all" || env.ProxyUIDs != "" || env.DirectUIDs != "" || env.DNSScope != "all" || env.DNSMode != "all" {
t.Fatalf("unexpected global env: %#v", env)
}
for _, uid := range []string{
"10130", "10131", "10132", "10133", "10134", "10135", "10136", "10137",
"10138", "10139", "10140", "10141", "10142", "10143", "10144", "10145",
"10146", "10147", "10148", "10149", "10150", "10151", "10152", "10153",
"10154", "10155", "10156", "10157", "10158", "10159", "10160", "10161",
"10162", "10163", "10164",
} {
if !strings.Contains(" "+env.BypassUIDs+" ", " "+uid+" ") {
t.Fatalf("global mode must hard-bypass built-in Russian apps, missing uid %s in %#v", uid, env)
}
}
if strings.Contains(" "+env.BypassUIDs+" ", " 10123 ") {
t.Fatalf("ordinary app must remain proxied in global mode, got %#v", env)
}
}
func TestResolveAlwaysDirectPackageNamesIncludesInstalledBuiltInsAndManual(t *testing.T) {
withPackageResolverTestEnv(t, `
com.example.direct 10123 0 /data/user/0/com.example.direct default
com.example.other 10124 0 /data/user/0/com.example.other default
com.yandex.browser 10132 0 /data/user/0/com.yandex.browser default
com.edadeal.android 10125 0 /data/user/0/com.edadeal.android default
com.programmisty.emiasapp 10126 0 /data/user/0/com.programmisty.emiasapp default
com.vkontakte.android 10127 0 /data/user/0/com.vkontakte.android default
com.v2raytun.android 10128 0 /data/user/0/com.v2raytun.android default
com.happproxy 10129 0 /data/user/0/com.happproxy default
org.amnezia.awg 10130 0 /data/user/0/org.amnezia.awg default
ang.hiddify.com 10131 0 /data/user/0/ang.hiddify.com default
`, func(bool) (string, error) {
return "", errors.New("fallback should not be needed")
})
names := ResolveAlwaysDirectPackageNames([]string{"com.example.direct"}, false)
got := strings.Join(names, " ")
for _, want := range []string{"com.edadeal.android", "com.example.direct", "com.programmisty.emiasapp", "com.vkontakte.android", "com.yandex.browser"} {
if !strings.Contains(" "+got+" ", " "+want+" ") {
t.Fatalf("expected %s in always-direct package names, got %#v", want, names)
}
}
for _, want := range []string{"ang.hiddify.com", "com.happproxy", "com.v2raytun.android", "org.amnezia.awg"} {
if !strings.Contains(" "+got+" ", " "+want+" ") {
t.Fatalf("expected VPN/proxy package %s in always-direct package names, got %#v", want, names)
}
}
if strings.Contains(" "+got+" ", " com.example.other ") {
t.Fatalf("ordinary package must not be included, got %#v", names)
}
}
func TestRequestedRussianServiceAppsAreBuiltInAlwaysDirect(t *testing.T) {
for _, packageName := range []string{
"ru.fourpda.client",
"ru.aliexpress.buyer",
"ru.aviasales",
"ru.burgerking",
"ru.bestprice.fixprice",
"ru.more.play",
"ru.pepper",
"ru.kfc.kfc_delivery",
"com.apegroup.mcdonaldsrussia",
"ru.rutube.app",
"com.sevensky.app",
"com.punicapp.whoosh",
"ru.rt.video.app.mobile",
"ru.gazprombank.android.mobilebank.app",
"ru.letobank.Prometheus",
"ru.dodopizza.app",
"ru.ivi.client",
"com.icemobile.lenta.prod",
"ru.rzd.pass",
"club.chizhik",
} {
if !IsBuiltInAlwaysDirectPackage(packageName) {
t.Fatalf("%s must be built-in always-direct", packageName)
}
}
}
func TestClashRoyaleIsNotBuiltInAlwaysDirect(t *testing.T) {
if IsBuiltInAlwaysDirectPackage("com.supercell.clashroyale") {
t.Fatal("Clash Royale must not be direct only because it contains the word clash")
}
}
func TestBuiltInAlwaysDirectCanBeExcludedByUser(t *testing.T) {
excluded := map[string]bool{"ru.sberbankmobile": true}
if IsBuiltInAlwaysDirectPackageWithExclusions("ru.sberbankmobile", excluded) {
t.Fatal("user exclusion should override built-in always-direct package")
}
if !IsBuiltInAlwaysDirectPackageWithExclusions("ru.alfabank.mobile.android", excluded) {
t.Fatal("unexcluded built-in package should stay always-direct")
}
}
func TestBuildAppRoutingEnvCanHardBypassSystemApps(t *testing.T) {
withPackageResolverTestEnv(t, `
com.example.app 10123 0 /data/user/0/com.example.app default
com.android.systemui 10100 0 /data/user/0/com.android.systemui platform
`, func(bool) (string, error) {
return "package:com.android.systemui uid:10100", nil
})
env := BuildAppRoutingEnv("all", nil, nil, true)
if !strings.Contains(" "+env.BypassUIDs+" ", " 10100 ") {
t.Fatalf("system app UID must be hard-bypassed, got %#v", env)
}
if strings.Contains(" "+env.BypassUIDs+" ", " 10123 ") {
t.Fatalf("ordinary app must not be system hard-bypassed, got %#v", env)
}
names := ResolveAlwaysDirectPackageNames(nil, true)
got := strings.Join(names, " ")
if !strings.Contains(" "+got+" ", " com.android.systemui ") {
t.Fatalf("system package must be visible in always-direct package names, got %#v", names)
}
}
func TestBuildRuntimeAppRoutingEnvDirectHardBypass(t *testing.T) {
withPackageResolverTestEnv(t, "com.example.app 10123 0 /data/user/0/com.example.app default\n", func(bool) (string, error) {
return "", errors.New("fallback should not be needed")
})
env := BuildRuntimeAppRoutingEnv("whitelist", []string{"com.example.app"}, []string{"com.android.vending"}, false, "direct")
if env.AppMode != "off" || env.ProxyUIDs != "" || env.DirectUIDs != "" {
t.Fatalf("direct routing must disable app interception, got %#v", env)
}
if env.DNSScope != "off" || env.DNSMode != "off" {
t.Fatalf("direct routing must disable DNS interception, got %#v", env)
}
if env.BypassUIDs == "" {
t.Fatalf("direct routing should preserve hard-bypass UID protection, got %#v", env)
}
}
func TestBuildChainedProxyProtectionEnvAllowsMutualLocalProxyUIDs(t *testing.T) {
withProcNetTCPTestEnv(t, `
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 0100007F:2A38 00000000:0000 0A 00000000:00000000 00:00000000 00000000 10123 0 111 1 00000000
1: 0100007F:2A39 00000000:0000 0A 00000000:00000000 00:00000000 00000000 10124 0 112 1 00000000
`, "")
cfg := config.DefaultConfig()
cfg.Profile.Nodes = jsonRawMessage(t, `{
"id":"nekobox",
"name":"NekoBox local",
"protocol":"SOCKS",
"server":"127.0.0.1",
"port":10808,
"outbound":{"protocol":"socks","settings":{"address":"127.0.0.1","port":10808}}
}`, `{
"id":"bbdpi",
"name":"BBDPI local",
"protocol":"SOCKS",
"server":"localhost",
"port":10809,
"outbound":{"protocol":"socks","settings":{"address":"localhost","port":10809}}
}`)
ports, uids, rules := BuildChainedProxyProtectionEnv(cfg)
if ports != "10808 10809" {
t.Fatalf("unexpected chain proxy ports: %q", ports)
}
if uids != "10123 10124" {
t.Fatalf("unexpected chain proxy UIDs: %q", uids)
}
if rules != "10808:10123 10809:10124" {
t.Fatalf("unexpected chain proxy rules: %q", rules)
}
}
func TestBuildChainedProxyProtectionEnvProtectsUnownedLocalPorts(t *testing.T) {
withProcNetTCPTestEnv(t, `
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 0100007F:2A38 00000000:0000 0A 00000000:00000000 00:00000000 00000000 10123 0 111 1 00000000
`, "")
cfg := config.DefaultConfig()
cfg.Proxy.APIPort = 10808
cfg.Profile.Nodes = jsonRawMessage(t, `{
"id":"api-duplicate",
"protocol":"SOCKS",
"server":"127.0.0.1",
"port":10808,
"outbound":{"protocol":"socks","settings":{"address":"127.0.0.1","port":10808}}
}`, `{
"id":"not-listening",
"protocol":"SOCKS",
"server":"127.0.0.1",
"port":10809,
"outbound":{"protocol":"socks","settings":{"address":"127.0.0.1","port":10809}}
}`)
ports, uids, rules := BuildChainedProxyProtectionEnv(cfg)
if ports != "10809" || uids != "" || rules != "" {
t.Fatalf("unowned local ports must be protected without allow UIDs, got ports=%q uids=%q rules=%q", ports, uids, rules)
}
}
func TestBuildChainedProxyProtectionEnvUsesDeclaredOwnerPackageBeforeListener(t *testing.T) {
withProcNetTCPTestEnv(t, "", "")
withPackageResolverTestEnv(t, `
com.proxy.owner 10123 0 /data/user/0/com.proxy.owner default 3003
`, func(bool) (string, error) {
return "", fmt.Errorf("unexpected package command")
})
cfg := config.DefaultConfig()
cfg.Profile.Nodes = jsonRawMessage(t, `{
"id":"nekobox",
"name":"NekoBox local",
"protocol":"SOCKS",
"server":"127.0.0.1",
"port":10808,
"ownerPackage":"com.proxy.owner",
"outbound":{"protocol":"socks","settings":{"address":"127.0.0.1","port":10808}}
}`)
ports, uids, rules := BuildChainedProxyProtectionEnv(cfg)
if ports != "10808" {
t.Fatalf("unexpected chain proxy ports: %q", ports)
}
if uids != "10123" {
t.Fatalf("unexpected owner package UIDs: %q", uids)
}
if rules != "10808:10123" {
t.Fatalf("unexpected owner package rules: %q", rules)
}
}
func TestBuildChainedProxyProtectionEnvIncludesIPv6AndWildcardLocalUpstreams(t *testing.T) {
withProcNetTCPTestEnv(t, "", "")
cfg := config.DefaultConfig()
cfg.Profile.Nodes = jsonRawMessage(t, `{
"id":"ipv6-local",
"protocol":"SOCKS",
"server":"::1",
"port":10810,
"outbound":{"protocol":"socks","settings":{"address":"::1","port":10810}}
}`, `{
"id":"wildcard-local",
"protocol":"SOCKS",
"server":"0.0.0.0",
"port":10811,
"outbound":{"protocol":"socks","settings":{"address":"0.0.0.0","port":10811}}
}`)
ports, uids, rules := BuildChainedProxyProtectionEnv(cfg)
if ports != "10810 10811" {
t.Fatalf("IPv6/wildcard local upstream ports must be protected, got %q", ports)
}
if uids != "" || rules != "" {
t.Fatalf("unowned local upstreams should get DROP-only protection, got uids=%q rules=%q", uids, rules)
}
}
func TestBuildChainedProxyProtectionEnvSkipsReservedHelperPorts(t *testing.T) {
withProcNetTCPTestEnv(t, "", "")
cfg := config.DefaultConfig()
cfg.Proxy.APIPort = 19090
cfg.Profile.Inbounds = json.RawMessage(`{"socksPort":10808,"httpPort":10809}`)
cfg.Profile.Nodes = jsonRawMessage(t, `{
"id":"helper-socks",
"protocol":"SOCKS",
"server":"127.0.0.1",
"port":10808,
"outbound":{"protocol":"socks","settings":{"address":"127.0.0.1","port":10808}}
}`, `{
"id":"api-port",
"protocol":"SOCKS",
"server":"127.0.0.1",
"port":19090,
"outbound":{"protocol":"socks","settings":{"address":"127.0.0.1","port":19090}}
}`, `{
"id":"real-upstream",
"protocol":"SOCKS",
"server":"127.0.0.1",
"port":10810,
"outbound":{"protocol":"socks","settings":{"address":"127.0.0.1","port":10810}}
}`)
ports, _, _ := BuildChainedProxyProtectionEnv(cfg)
if ports != "10810" {
t.Fatalf("reserved helper/API ports must be skipped, got %q", ports)
}
}
func TestVerifyChainedProxyOwnerPackagesRejectsMismatchedListener(t *testing.T) {
withProcNetTCPTestEnv(t, `
sl local_address rem_address st tx_queue tx_queue tr tm->when retrnsmt uid timeout inode
0: 0100007F:2A38 00000000:0000 0A 00000000:00000000 00:00000000 00000000 10999 0 111 1 00000000
`, "")
withPackageResolverTestEnv(t, `
com.proxy.owner 10123 0 /data/user/0/com.proxy.owner default 3003
`, func(bool) (string, error) {
return "", fmt.Errorf("unexpected package command")
})
cfg := config.DefaultConfig()
cfg.Profile.Nodes = jsonRawMessage(t, `{
"id":"nekobox",
"protocol":"SOCKS",
"server":"127.0.0.1",
"port":10808,
"ownerPackage":"com.proxy.owner",
"outbound":{"protocol":"socks","settings":{"address":"127.0.0.1","port":10808}}
}`)
err := VerifyChainedProxyOwnerPackages(cfg)
if err == nil || !strings.Contains(err.Error(), "expected package com.proxy.owner") {
t.Fatalf("expected owner mismatch error, got %v", err)
}
}
func TestVerifyChainedProxyOwnerPackagesRejectsAnyMismatchedListenerOnSamePort(t *testing.T) {
withProcNetTCPTestEnv(t, `
sl local_address rem_address st tx_queue tx_queue tr tm->when retrnsmt uid timeout inode
0: 00000000:2A38 00000000:0000 0A 00000000:00000000 00:00000000 00000000 10999 0 111 1 00000000
1: 0100007F:2A38 00000000:0000 0A 00000000:00000000 00:00000000 00000000 10123 0 112 1 00000000
`, "")
withPackageResolverTestEnv(t, `
com.proxy.owner 10123 0 /data/user/0/com.proxy.owner default 3003
`, func(bool) (string, error) {
return "", fmt.Errorf("unexpected package command")
})
cfg := config.DefaultConfig()
cfg.Profile.Nodes = jsonRawMessage(t, `{
"id":"nekobox",
"protocol":"SOCKS",
"server":"127.0.0.1",
"port":10808,
"ownerPackage":"com.proxy.owner",
"outbound":{"protocol":"socks","settings":{"address":"127.0.0.1","port":10808}}
}`)
err := VerifyChainedProxyOwnerPackages(cfg)
if err == nil || !strings.Contains(err.Error(), "owned by UID 10999") {
t.Fatalf("expected any same-port mismatched owner to fail, got %v", err)
}
}
func TestVerifyChainedProxyOwnerPackagesChecksIPv6LoopbackListener(t *testing.T) {
withProcNetTCPTestEnv(t, "", `
sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 00000000000000000000000001000000:2A38 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 10999 0 111 1 00000000
`)
withPackageResolverTestEnv(t, `
com.proxy.owner 10123 0 /data/user/0/com.proxy.owner default 3003
`, func(bool) (string, error) {
return "", fmt.Errorf("unexpected package command")
})
cfg := config.DefaultConfig()
cfg.Profile.Nodes = jsonRawMessage(t, `{
"id":"ipv6-local",
"protocol":"SOCKS",
"server":"::1",
"port":10808,
"ownerPackage":"com.proxy.owner",
"outbound":{"protocol":"socks","settings":{"address":"::1","port":10808}}
}`)
err := VerifyChainedProxyOwnerPackages(cfg)
if err == nil || !strings.Contains(err.Error(), "expected package com.proxy.owner") {
t.Fatalf("expected IPv6 owner mismatch error, got %v", err)
}
}
func TestVerifyChainedProxyOwnerPackagesAllowsNotYetListeningPort(t *testing.T) {
withProcNetTCPTestEnv(t, "", "")
withPackageResolverTestEnv(t, `
com.proxy.owner 10123 0 /data/user/0/com.proxy.owner default 3003
`, func(bool) (string, error) {
return "", fmt.Errorf("unexpected package command")
})
cfg := config.DefaultConfig()
cfg.Profile.Nodes = jsonRawMessage(t, `{
"id":"nekobox",
"protocol":"SOCKS",
"server":"127.0.0.1",
"port":10808,
"ownerPackage":"com.proxy.owner",
"outbound":{"protocol":"socks","settings":{"address":"127.0.0.1","port":10808}}
}`)
if err := VerifyChainedProxyOwnerPackages(cfg); err != nil {
t.Fatalf("not-yet-listening declared proxy should pass verification: %v", err)
}
}
func TestVerifyChainedProxyOwnerPackagesRejectsUnresolvedOwner(t *testing.T) {
withProcNetTCPTestEnv(t, "", "")
withPackageResolverTestEnv(t, `
com.proxy.other 10124 0 /data/user/0/com.proxy.other default 3003
`, func(bool) (string, error) {
return "", fmt.Errorf("cmd unavailable")
})
cfg := config.DefaultConfig()
cfg.Profile.Nodes = jsonRawMessage(t, `{
"id":"nekobox",
"protocol":"SOCKS",
"server":"127.0.0.1",
"port":10808,
"ownerPackage":"com.proxy.owner",
"outbound":{"protocol":"socks","settings":{"address":"127.0.0.1","port":10808}}
}`)
err := VerifyChainedProxyOwnerPackages(cfg)
if err == nil || !strings.Contains(err.Error(), "did not resolve to a UID") {
t.Fatalf("expected unresolved owner error, got %v", err)
}
}
func withProcNetTCPTestEnv(t *testing.T, tcp string, tcp6 string) {
t.Helper()
oldFiles := procNetTCPFiles
t.Cleanup(func() {
procNetTCPFiles = oldFiles
})
dir := t.TempDir()
paths := []string{}
if tcp != "" {
path := filepath.Join(dir, "tcp")
if err := os.WriteFile(path, []byte(tcp), 0644); err != nil {
t.Fatal(err)
}
paths = append(paths, path)
}
if tcp6 != "" {
path := filepath.Join(dir, "tcp6")
if err := os.WriteFile(path, []byte(tcp6), 0644); err != nil {
t.Fatal(err)
}
paths = append(paths, path)
}
procNetTCPFiles = paths
}
func jsonRawMessage(t *testing.T, values ...string) []json.RawMessage {
t.Helper()
raw := make([]json.RawMessage, 0, len(values))
for _, value := range values {
raw = append(raw, json.RawMessage(value))
}
return raw
}
func withPackageResolverTestEnv(t *testing.T, packagesList string, command func(bool) (string, error)) {
t.Helper()
oldPackageListPath := packageListPath
oldDataUserPath := dataUserPath
oldRunPackageUIDCommand := runPackageUIDCommand
oldRunSystemPackageUIDCommand := runSystemPackageUIDCommand
t.Cleanup(func() {
packageListPath = oldPackageListPath
dataUserPath = oldDataUserPath
runPackageUIDCommand = oldRunPackageUIDCommand
runSystemPackageUIDCommand = oldRunSystemPackageUIDCommand
})
tempDir := t.TempDir()
dataUserPath = filepath.Join(tempDir, "user")
if err := os.MkdirAll(dataUserPath, 0755); err != nil {
t.Fatal(err)
}
packageListPath = filepath.Join(tempDir, "packages.list")
if packagesList == "" {
packageListPath = filepath.Join(tempDir, "missing-packages.list")
} else if err := os.WriteFile(packageListPath, []byte(packagesList), 0644); err != nil {
t.Fatal(err)
}
runPackageUIDCommand = command
runSystemPackageUIDCommand = command
}