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
667 lines
24 KiB
Go
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
|
|
}
|